| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2014 Google, Inc. (edjames@google.com) |
| * Copyright (C) 2012 Marcel Holtmann <marcel@holtmann.org> |
| * Copyright (C) 2012 Nordic Semiconductor Inc. |
| * Copyright (C) 2012 Instituto Nokia de Tecnologia - INdT |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| /* much lifted from input/hog.c */ |
| |
| /* OAD - over air download of firmware to devices */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdlib.h> |
| #include <stdbool.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/param.h> |
| #include <unistd.h> |
| #include <glib.h> |
| |
| #include "lib/bluetooth.h" |
| #include "lib/sdp.h" |
| #include "lib/uuid.h" |
| |
| #include "src/adapter.h" |
| #include "src/device.h" |
| #include "src/log.h" |
| #include "src/plugin.h" |
| #include "src/profile.h" |
| #include "src/service.h" |
| #include "attrib/att.h" |
| #include "attrib/gattrib.h" |
| #include "attrib/gatt.h" |
| #include "src/shared/util.h" |
| |
| #define OAD_UUID "f000ffc0-0451-4000-b000-000000000000" |
| #define OAD_IMG_IDENTITY_UUID "f000ffc1-0451-4000-b000-000000000000" |
| #define OAD_IMG_BLOCK_UUID "f000ffc2-0451-4000-b000-000000000000" |
| |
| #define OAD_MAJOR_VERSION(x) ((x) >> 8) |
| #define OAD_MINOR_VERSION(x) ((x) & 0xff) |
| |
| /* these codes may be returned instead of GP versions */ |
| #define OAD_VERSION_BAD 0xfefe |
| #define OAD_VERSION_BUSY 0xffff |
| |
| /* remote has 2 processors, 2 firmwares. */ |
| #define OAD_GP_FIRMWARE_GFRM200 "gfrm200.gp.bin" |
| #define OAD_TI_FIRMWARE_GFRM200 "gfrm200.ti.bin" |
| #define OAD_GP_FIRMWARE_GFRM210 "gfrm210.gp.bin" |
| #define OAD_TI_FIRMWARE_GFRM210 "gfrm210.ti.bin" |
| |
| /* remote checks in every 15 minutes */ |
| #define OAD_UPGRADE_DELAY_SECS 1 // delay between wake and upgrade check |
| #define OAD_DELAY_NO_UPGRADE (8*60*60) // check for new ACS files (not likely) |
| #define OAD_DELAY_NO_UPGRADE_DEBUG 150 // check faster when /tmp firmware exists |
| #define OAD_DELAY_UPGRADING 60 // recheck in 1 minute (in case of 2 firmware update) |
| |
| #define OAD_HEADER_LEN 8 // on-disk header. See OAD_Formatv7.xlsx |
| |
| enum OAD_Status { |
| OADSTATUS_None = 0, |
| OADSTATUS_DataTransfer = 1, |
| OADSTATUS_TransferSuccess = 2, |
| OADSTATUS_TIActive = 0x81, // maybe flashing |
| OADSTATUS_GPActive = 0x82, // maybe rebooting |
| OADSTATUS_CRCError = 0xE0, |
| OADSTATUS_LVD = 0xE1, // low voltage detection |
| OADSTATUS_KeyPress = 0xE2, // user interrupted (not idle) |
| OADSTATUS_GPWrite = 0xE3, // GP write failed? |
| OADSTATUS_Bootloader = 0xE5, // Bootloader rejected? |
| OADSTATUS_ImageError = 0xE6, // Image format error? |
| }; |
| |
| enum OAD_Action { |
| OADACTION_GetRemoteInfo = 0, |
| OADACTION_TransferOADData = 1, |
| OADACTION_StartUpdate = 2, |
| }; |
| |
| /* firmware is upgraded in this order */ |
| enum OAD_FirmwareType { |
| OADFW_Unknown = -1, |
| OADFW_TI = 0, |
| OADFW_GP = 1, |
| OADFW_Total = 2, |
| }; |
| |
| struct oad_fwinfo { |
| uint16_t version; |
| uint32_t size; |
| uint8_t header[OAD_HEADER_LEN]; |
| char file[MAXPATHLEN]; |
| }; |
| |
| struct oad_service { |
| guint attio_id; |
| uint16_t ccc_handle; |
| uint16_t value_handle; |
| int notify_enabled; |
| }; |
| |
| struct oad_device { |
| uint16_t id; |
| struct btd_device *device; |
| GAttrib *attrib; |
| struct gatt_primary *oad_primary; |
| |
| struct oad_service identity; |
| struct oad_service block; |
| |
| time_t nextCheck; |
| struct oad_fwinfo disk[OADFW_Total]; |
| struct oad_fwinfo remote[OADFW_Total]; |
| |
| int force_upgrade; |
| int retries; |
| int num_blocks; |
| int progress; |
| |
| uint8_t header[OAD_HEADER_LEN]; |
| int fd; |
| guint tid; // Timer id to delay starting upgrade |
| }; |
| |
| struct oad_characteristic { |
| struct oad_device *oaddev; |
| char uuid[MAX_LEN_UUID_STR + 1]; |
| }; |
| |
| static GSList *oad_devices = NULL; |
| |
| static struct oad_device *oad_new_device(struct btd_device *device, uint16_t id) |
| { |
| struct oad_device *oaddev; |
| |
| oaddev = g_try_new0(struct oad_device, 1); |
| if (!oaddev) |
| return NULL; |
| |
| oaddev->id = id; |
| oaddev->device = btd_device_ref(device); |
| oaddev->fd = -1; |
| |
| return oaddev; |
| } |
| |
| static void oad_free_device(struct oad_device *oaddev) |
| { |
| if (oaddev->fd != -1) { |
| close(oaddev->fd); |
| oaddev->fd = -1; |
| } |
| btd_device_unref(oaddev->device); |
| g_attrib_unref(oaddev->attrib); |
| g_free(oaddev->oad_primary); |
| g_free(oaddev); |
| } |
| |
| static void oad_block_char_write_cb(guint8 status, const guint8 *pdu, guint16 len, gpointer user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| |
| if (status != 0) { |
| error("OAD %s failed: %s", __func__, att_ecode2str(status)); |
| return; |
| } |
| oaddev->retries = 0; // successful write |
| } |
| |
| static void oad_identity_char_write_cb(guint8 status, const guint8 *pdu, guint16 len, gpointer user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| |
| if (status != 0) { |
| error("OAD %s failed: %s", __func__, att_ecode2str(status)); |
| return; |
| } |
| } |
| |
| static void oad_start_transfer(struct oad_device* oaddev) |
| { |
| uint8_t attr_val[OAD_HEADER_LEN+1]; // + 1 byte action |
| |
| memcpy(attr_val, oaddev->header, OAD_HEADER_LEN); |
| attr_val[OAD_HEADER_LEN] = OADACTION_TransferOADData; /* start OAD image transfer */ |
| |
| gatt_write_char(oaddev->attrib, oaddev->identity.value_handle, attr_val, |
| sizeof(attr_val), oad_identity_char_write_cb, oaddev); |
| } |
| |
| static void oad_block_notify_handler(const uint8_t *pdu, uint16_t len, gpointer user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| int retry = 0; |
| |
| |
| /* should be at least opcode (1b) + handle (7a00) */ |
| if (len < 3) { |
| error("OAD Invalid PDU received"); |
| return; |
| } |
| |
| // 2 bytes block_num + status byte |
| if (len != 6) { |
| error("OAD block notify format error: expected 6 bytes, got %d", len); |
| return; |
| } |
| |
| /* request for block */ |
| uint16_t block_num = get_le16(pdu + 3); |
| enum OAD_Status status = (enum OAD_Status) pdu[5]; |
| |
| switch (status) { |
| case OADSTATUS_DataTransfer: // send a block |
| { |
| uint8_t block[18]; // 2 bytes block_num + 16 data bytes |
| |
| memset(block, 0, sizeof block); |
| put_le16(block_num, block); |
| |
| if (lseek(oaddev->fd, 16 * block_num, SEEK_SET) < 0) { |
| perror("OAD firmware seek"); |
| goto error; |
| } |
| if (read(oaddev->fd, block+2, sizeof block - 2) < 0) { |
| perror("OAD firmware read"); |
| goto error; |
| } |
| |
| int progress = 0; |
| if (oaddev->num_blocks > 0) progress = 100 * block_num / oaddev->num_blocks; |
| if (oaddev->progress != progress) { |
| oaddev->progress = progress; |
| DBG("OAD sending block %d (%d%%)", block_num, progress); |
| } |
| gatt_write_char(oaddev->attrib, oaddev->block.value_handle, block, |
| sizeof(block), oad_block_char_write_cb, oaddev); |
| } |
| break; |
| case OADSTATUS_TransferSuccess: // OK, successful transfer |
| { |
| DBG("OAD firmware transfer successful"); |
| close(oaddev->fd); |
| oaddev->fd = -1; |
| |
| uint8_t attr_val[OAD_HEADER_LEN+1]; // + 1 byte action |
| memcpy(attr_val, oaddev->header, OAD_HEADER_LEN); |
| attr_val[OAD_HEADER_LEN] = OADACTION_StartUpdate; /* commit fw image */ |
| DBG("OAD committing firmware"); |
| gatt_write_char(oaddev->attrib, oaddev->identity.value_handle, attr_val, |
| sizeof(attr_val), oad_identity_char_write_cb, oaddev); |
| } |
| break; |
| case OADSTATUS_GPActive: |
| DBG("OAD GP is active (good)"); |
| break; |
| case OADSTATUS_TIActive: |
| DBG("OAD TI is active (good)"); |
| break; |
| |
| case OADSTATUS_CRCError: |
| DBG("OAD firmware transfer checksum error"); |
| goto error; |
| case OADSTATUS_LVD: |
| DBG("OAD firmware upgrade low battery error"); |
| goto error; |
| case OADSTATUS_KeyPress: |
| DBG("OAD firmware upgrade user interrupted"); |
| goto error; |
| case OADSTATUS_GPWrite: |
| DBG("OAD firmware upgrade GP write error"); |
| goto error; |
| case OADSTATUS_Bootloader: |
| DBG("OAD firmware upgrade Bootloader error"); |
| goto error; |
| case OADSTATUS_ImageError: |
| DBG("OAD firmware upgrade Image error"); |
| goto error; |
| |
| default: |
| /* 0xe0 or greater appear to be unrecoverable errors */ |
| if (status >= OADSTATUS_CRCError) { |
| DBG("OAD unexpected block notify error 0x%02x", status); |
| goto error; |
| } |
| DBG("OAD unexpected block notify status 0x%02x (ignoring)", status); |
| break; |
| } |
| /* success so far*/ |
| return; |
| |
| error: |
| DBG("OAD upgrade stopping due to error %d", status); |
| close(oaddev->fd); |
| oaddev->fd = -1; |
| } |
| |
| static time_t oad_wallclock(void) |
| { |
| struct timespec ts; |
| |
| if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { |
| return 0; |
| } |
| return ts.tv_sec; |
| } |
| |
| static int oad_is_time_for_upgrade_check(struct oad_device* oaddev) |
| { |
| time_t now = oad_wallclock(); |
| |
| if (now < oaddev->nextCheck) { |
| int next = oaddev->nextCheck - now; |
| DBG("OAD not time for upgrade check yet (%d seconds left)", next); |
| return 0; |
| } |
| DBG("OAD time for upgrade check"); |
| return 1; |
| } |
| |
| static void oad_get_firmware_info(struct oad_device *oaddev, const char* file) |
| { |
| uint8_t header[OAD_HEADER_LEN]; |
| |
| int fd = open(file, O_RDONLY); |
| if (fd < 0) { |
| error("OAD open: %s: %s", file, strerror(errno)); |
| return; |
| } |
| int len = read(fd, header, sizeof header); |
| if (len < 0) { |
| error("OAD read: %s: %s", file, strerror(errno)); |
| close(fd); |
| return; |
| } |
| if (len < sizeof header) { |
| error("OAD short read: %s: wanted %lu, got %d", file, sizeof header, len); |
| close(fd); |
| return; |
| } |
| close(fd); |
| fd = -1; |
| |
| /* See OAD_Formatv7.xlsx, 'Image Info' tab. 8 bytes are: |
| * 2 bytes checksum |
| * 2 bytes version |
| * 3 bytes size |
| * 1 byte id: "T" TI image, "G" GP image |
| */ |
| uint16_t version = get_le16(header + 2); |
| uint32_t size = get_le32(header+4) & 0x00ffffff; |
| int id = header[7]; |
| enum OAD_FirmwareType type; |
| |
| switch(id) { |
| case 'T': |
| case 'U': |
| type = OADFW_TI; |
| break; |
| case 'G': |
| case 'H': |
| type = OADFW_GP; |
| break; |
| default: |
| type = OADFW_Unknown; |
| break; |
| } |
| if (type == OADFW_Unknown) { |
| error("OAD unknown firmware id '%c': file=%s version=%d.%d", |
| id, file, OAD_MAJOR_VERSION(version), OAD_MINOR_VERSION(version)); |
| return; |
| } |
| int index = (int)type; |
| if (version <= oaddev->disk[index].version) { |
| error("OAD ignoring older firmware: file=%s type=%d version=%d.%d", |
| file, type, OAD_MAJOR_VERSION(version), OAD_MINOR_VERSION(version)); |
| return; |
| } |
| if (strlen(file) >= sizeof oaddev->disk[index].file) { |
| error("OAD firmware file name too long: %s", file); |
| return; |
| } |
| |
| oaddev->disk[index].version = version; |
| oaddev->disk[index].size = size; |
| memcpy(oaddev->disk[index].header, header, sizeof oaddev->disk[index].header); |
| strncpy(oaddev->disk[index].file, file, sizeof oaddev->disk[index].file); |
| oaddev->disk[index].file[sizeof oaddev->disk[index].file - 1] = '\0'; |
| } |
| |
| static const char* oad_firmware_type_string(enum OAD_FirmwareType type) |
| { |
| switch (type) { |
| case OADFW_GP: return "GP"; |
| case OADFW_TI: return "TI"; |
| } |
| return "Unknown"; |
| } |
| |
| static void oad_find_firmware(struct oad_device *oaddev) |
| { |
| char name[MAX_NAME_LENGTH + 1] = ""; |
| const char *tmp_gp; |
| const char *tmp_ti; |
| const char *etc_gp; |
| const char *etc_ti; |
| |
| device_get_name(oaddev->device, name, sizeof(name)); |
| if (g_strcmp0(name, "GFRM200") == 0) { |
| tmp_gp = "/tmp/" OAD_GP_FIRMWARE_GFRM200; |
| tmp_ti = "/tmp/" OAD_TI_FIRMWARE_GFRM200; |
| etc_gp = "/etc/remote/" OAD_GP_FIRMWARE_GFRM200; |
| etc_ti = "/etc/remote/" OAD_TI_FIRMWARE_GFRM200; |
| } else if (g_strcmp0(name, "GFRM201") == 0 || |
| g_strcmp0(name, "GFRM210") == 0) { |
| tmp_gp = "/tmp/" OAD_GP_FIRMWARE_GFRM210; |
| tmp_ti = "/tmp/" OAD_TI_FIRMWARE_GFRM210; |
| etc_gp = "/etc/remote/" OAD_GP_FIRMWARE_GFRM210; |
| etc_ti = "/etc/remote/" OAD_TI_FIRMWARE_GFRM210; |
| } else { |
| error("OAD unknown model %s, skipping", name); |
| } |
| |
| memset(oaddev->disk, 0, sizeof oaddev->disk); |
| |
| /* check in tmp first, to make debugging easier */ |
| if (access(tmp_gp, R_OK) == 0 || access(tmp_ti, R_OK) == 0) { |
| DBG("OAD found firmware in /tmp, ignoring /etc/remote, enabling downgrade."); |
| oaddev->force_upgrade = 1; // allow downgrade |
| oad_get_firmware_info(oaddev, tmp_gp); |
| oad_get_firmware_info(oaddev, tmp_ti); |
| } else { |
| oaddev->force_upgrade = 0; |
| oad_get_firmware_info(oaddev, etc_gp); |
| oad_get_firmware_info(oaddev, etc_ti); |
| } |
| |
| int i; |
| for (i = 0; i < OADFW_Total; i++) { |
| enum OAD_FirmwareType type = (enum OAD_FirmwareType) i; |
| const char* typeStr = oad_firmware_type_string(type); |
| if (strlen(oaddev->disk[type].file) == 0) { |
| error("OAD no %s firmware found on disk", typeStr); |
| } else { |
| DBG("OAD found %s firmware: file=%s version=%d.%d", |
| typeStr, oaddev->disk[i].file, |
| OAD_MAJOR_VERSION(oaddev->disk[i].version), |
| OAD_MINOR_VERSION(oaddev->disk[i].version)); |
| } |
| } |
| } |
| |
| static void oad_check_for_upgrade(struct oad_device *oaddev) |
| { |
| if (!oad_is_time_for_upgrade_check(oaddev)) { |
| return; |
| } |
| |
| oad_find_firmware(oaddev); |
| |
| struct oad_fwinfo* fp = NULL; |
| int i; |
| for (i = 0; i < OADFW_Total; i++) { |
| enum OAD_FirmwareType type = (enum OAD_FirmwareType) i; |
| const char* typeStr = oad_firmware_type_string(type); |
| if (strlen(oaddev->disk[i].file) == 0) { |
| continue; |
| } |
| if (oaddev->disk[i].version == oaddev->remote[i].version || |
| (!oaddev->force_upgrade && |
| oaddev->disk[i].version < oaddev->remote[i].version && |
| oaddev->remote[i].version != OAD_VERSION_BAD)) { |
| DBG("OAD %s firmware is up to date", typeStr); |
| } else { |
| fp = &oaddev->disk[i]; |
| break; |
| } |
| } |
| |
| if (fp == NULL) { |
| int next = oaddev->force_upgrade |
| ? OAD_DELAY_NO_UPGRADE_DEBUG |
| : OAD_DELAY_NO_UPGRADE; |
| DBG("OAD next check is in %d seconds", next); |
| oaddev->nextCheck = oad_wallclock() + next; |
| return; |
| } |
| oaddev->nextCheck = oad_wallclock() + OAD_DELAY_UPGRADING; |
| |
| DBG("OAD starting upgrade with %s", fp->file); |
| |
| if (oaddev->fd != -1) { |
| close(oaddev->fd); |
| } |
| oaddev->fd = open(fp->file, O_RDONLY); |
| if (oaddev->fd < 0) { |
| error("OAD open: %s: %s", fp->file, strerror(errno)); |
| return; |
| } |
| if (oaddev->tid) { |
| g_source_remove(oaddev->tid); |
| oaddev->tid = 0; |
| } |
| |
| oaddev->progress = 0; |
| oaddev->retries = 0; |
| oaddev->num_blocks = (fp->size + 15) / 16; |
| memcpy(oaddev->header, fp->header, sizeof oaddev->header); |
| oad_start_transfer(oaddev); |
| } |
| |
| static void oad_identity_notify_handler(const uint8_t *pdu, uint16_t len, gpointer user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| |
| |
| /* should be at least opcode (1b) + handle (76 00) */ |
| if (len < 3) { |
| error("OAD Invalid PDU received"); |
| return; |
| } |
| |
| /* See OAD_Formatv7.xlsx, 'Image Info' tab. 10 bytes are: |
| * char gp_version[2]; // 09 00 is 0.9 |
| * char gp_size[3]; // 00 00 01 is 0x010000 == 64k |
| * char ti_version[2]; // 2a 00 is 0.42 |
| * char ti_size[3]; // 00 00 01 is 0x010000 == 64k |
| */ |
| |
| if (len != 13) { |
| error("OAD identity data format error: expected 13 bytes, got %d", len); |
| return; |
| } |
| |
| oaddev->remote[OADFW_GP].version = get_le16(pdu+3); |
| oaddev->remote[OADFW_GP].size = get_le32(pdu+5) & 0x00ffffff; |
| |
| oaddev->remote[OADFW_TI].version = get_le16(pdu+8); |
| unsigned char size[4]; |
| memcpy(size, pdu+10, 3); |
| size[3] = 0; // need 4 bytes for get_le32 |
| oaddev->remote[OADFW_TI].size = get_le32(size) & 0x00ffffff; |
| |
| int i; |
| for (i = 0; i < OADFW_Total; i++) { |
| enum OAD_FirmwareType type = (enum OAD_FirmwareType) i; |
| const char* typeStr = oad_firmware_type_string(type); |
| DBG("OAD %s firmware on remote: version=%d.%d", |
| typeStr, OAD_MAJOR_VERSION(oaddev->remote[i].version), |
| OAD_MINOR_VERSION(oaddev->remote[i].version)); |
| } |
| oad_check_for_upgrade(oaddev); |
| } |
| |
| static void oad_check_fwversion(struct oad_device *oaddev) |
| { |
| uint8_t attr_val[9]; // 8 byte header + 1 command byte (all zeros) |
| |
| if (!oaddev->identity.notify_enabled || !oaddev->block.notify_enabled) { |
| return; |
| } |
| |
| DBG("OAD sending fw version request"); |
| memset(attr_val, 0, sizeof attr_val); |
| attr_val[8] = OADACTION_GetRemoteInfo; /* trigger fw version notify */ |
| gatt_write_char(oaddev->attrib, oaddev->identity.value_handle, attr_val, |
| sizeof(attr_val), oad_identity_char_write_cb, oaddev); |
| } |
| |
| static void oad_ccc_char_write_identity_cb(guint8 status, const guint8 *pdu, guint16 len, gpointer user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| |
| if (status != 0) { |
| error("OAD %s failed: %s", __func__, att_ecode2str(status)); |
| return; |
| } |
| oaddev->identity.notify_enabled = 1; |
| |
| oad_check_fwversion(oaddev); |
| } |
| |
| static void oad_ccc_char_write_block_cb(guint8 status, const guint8 *pdu, guint16 len, gpointer user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| |
| if (status != 0) { |
| error("OAD %s failed: %s", __func__, att_ecode2str(status)); |
| return; |
| } |
| oaddev->block.notify_enabled = 1; |
| |
| oad_check_fwversion(oaddev); |
| } |
| |
| static void oad_enable_notify(struct oad_device* oaddev) |
| { |
| if (oaddev->identity.ccc_handle == 0 || oaddev->block.ccc_handle == 0 || |
| oaddev->identity.value_handle == 0 || oaddev->block.value_handle == 0) { |
| error("OAD discovery not complete, cannot enable notifiers"); |
| return; |
| } |
| |
| oaddev->identity.attio_id = |
| g_attrib_register(oaddev->attrib, |
| ATT_OP_HANDLE_NOTIFY, oaddev->identity.value_handle, |
| oad_identity_notify_handler, oaddev, NULL); |
| oaddev->block.attio_id = |
| g_attrib_register(oaddev->attrib, |
| ATT_OP_HANDLE_NOTIFY, oaddev->block.value_handle, |
| oad_block_notify_handler, oaddev, NULL); |
| |
| uint8_t attr_val[2]; |
| put_le16(GATT_CLIENT_CHARAC_CFG_NOTIF_BIT, attr_val); |
| DBG("OAD enabling notify for OAD_IMG_IDENTITY_UUID"); |
| gatt_write_char(oaddev->attrib, oaddev->identity.ccc_handle, attr_val, |
| sizeof(attr_val), oad_ccc_char_write_identity_cb, oaddev); |
| DBG("OAD enabling notify for OAD_IMG_BLOCK_UUID"); |
| gatt_write_char(oaddev->attrib, oaddev->block.ccc_handle, attr_val, |
| sizeof(attr_val), oad_ccc_char_write_block_cb, oaddev); |
| } |
| |
| static gboolean oad_upgrade_timer(gpointer data) |
| { |
| struct oad_device *oaddev = data; |
| |
| g_source_remove(oaddev->tid); |
| oaddev->tid = 0; |
| |
| DBG("OAD timer fired"); |
| oad_enable_notify(oaddev); |
| } |
| |
| static void oad_desc_discovered_cb(uint8_t status, GSList *descs, void* user_data) |
| { |
| struct oad_characteristic *ch = user_data; |
| struct oad_device *oaddev = ch->oaddev; |
| struct GSList *list = NULL; |
| |
| if (status != 0) { |
| error("OAD %s failed: %s", __func__, att_ecode2str(status)); |
| goto done; |
| } |
| |
| for (; descs; descs = descs->next) { |
| struct gatt_desc *desc = descs->data; |
| |
| if (desc->uuid16 != GATT_CLIENT_CHARAC_CFG_UUID) |
| continue; |
| |
| if (g_strcmp0(ch->uuid, OAD_IMG_IDENTITY_UUID) == 0) { |
| DBG("OAD found OAD_IMG_IDENTITY_UUID ccc_handle=0x%04x", desc->handle); |
| oaddev->identity.ccc_handle = desc->handle; |
| |
| } else if (g_strcmp0(ch->uuid, OAD_IMG_BLOCK_UUID) == 0) { |
| DBG("OAD found OAD_IMG_BLOCK_UUID ccc_handle=0x%04x", desc->handle); |
| oaddev->block.ccc_handle = desc->handle; |
| } |
| } |
| if (oaddev->identity.ccc_handle && oaddev->block.ccc_handle) |
| oaddev->tid = g_timeout_add_seconds(OAD_UPGRADE_DELAY_SECS, oad_upgrade_timer, oaddev); |
| |
| done: |
| g_free(ch); |
| } |
| |
| static void oad_discover_desc(struct oad_device *oaddev, struct gatt_char *c, struct gatt_char *c_next) |
| { |
| struct gatt_primary *prim = oaddev->oad_primary; |
| struct oad_characteristic *ch; |
| uint16_t start, end; |
| |
| start = c->value_handle + 1; |
| |
| if (c_next != NULL) { |
| if (start == c_next->handle) |
| return; |
| end = c_next->handle - 1; |
| } else if (c->value_handle != prim->range.end) { |
| end = prim->range.end; |
| } else { |
| return; |
| } |
| |
| ch = g_new0(struct oad_characteristic, 1); |
| ch->oaddev = oaddev; |
| memcpy(ch->uuid, c->uuid, sizeof(c->uuid)); |
| |
| DBG("OAD discovering descriptors start=0x%04x end=0x%04x", start, end); |
| |
| gatt_discover_desc(oaddev->attrib, start, end, NULL, oad_desc_discovered_cb, ch); |
| } |
| |
| static void oad_char_discovered_cb(uint8_t status, GSList *chars, void* user_data) |
| { |
| struct oad_device *oaddev = user_data; |
| struct gatt_primary *prim = oaddev->oad_primary; |
| bt_uuid_t img_identity_uuid, img_block_uuid; |
| GSList *l; |
| uint16_t info_handle = 0, proto_mode_handle = 0; |
| |
| DBG("OAD inspecting characteristics"); |
| if (status != 0) { |
| error("OAD %s failed: %s", __func__, att_ecode2str(status)); |
| return; |
| } |
| |
| for (l = chars; l; l = g_slist_next(l)) { |
| struct gatt_char *chr, *next; |
| bt_uuid_t uuid; |
| uint16_t start, end; |
| |
| chr = l->data; |
| next = l->next ? l->next->data : NULL; |
| |
| start = chr->value_handle + 1; |
| end = (next ? next->handle - 1 : prim->range.end); |
| |
| if (g_strcmp0(chr->uuid, OAD_IMG_IDENTITY_UUID) == 0) { |
| oaddev->identity.value_handle = chr->value_handle; |
| DBG("OAD found OAD_IMG_IDENTITY_UUID value_handle=0x%04x", chr->value_handle); |
| oad_discover_desc(oaddev, chr, next); |
| } else if (g_strcmp0(chr->uuid, OAD_IMG_BLOCK_UUID) == 0) { |
| oaddev->block.value_handle = chr->value_handle; |
| DBG("OAD found OAD_IMG_BLOCK_UUID value_handle=0x%04x", chr->value_handle); |
| oad_discover_desc(oaddev, chr, next); |
| } |
| |
| } |
| } |
| |
| static int oad_accept(struct btd_service *service) |
| { |
| struct oad_device *oaddev = btd_service_get_user_data(service); |
| struct gatt_primary *prim = oaddev->oad_primary; |
| struct btd_device *device = btd_service_get_device(service); |
| GAttrib *attrib = btd_device_get_attrib(device); |
| |
| oaddev->attrib = g_attrib_ref(attrib); |
| oaddev->identity.notify_enabled = 0; |
| oaddev->block.notify_enabled = 0; |
| |
| btd_service_connecting_complete(service, 0); |
| |
| if (oaddev->identity.ccc_handle == 0 || oaddev->block.ccc_handle == 0 || |
| oaddev->identity.value_handle == 0 || oaddev->block.value_handle == 0) { |
| DBG("OAD discovering characteristics"); |
| |
| gatt_discover_char(oaddev->attrib, prim->range.start, prim->range.end, NULL, |
| oad_char_discovered_cb, oaddev); |
| } else { |
| if (oad_is_time_for_upgrade_check(oaddev)) { |
| oad_enable_notify(oaddev); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int oad_disconnect(struct btd_service *service) |
| { |
| struct oad_device *oaddev = btd_service_get_user_data(service); |
| |
| if (oaddev->identity.attio_id > 0) { |
| g_attrib_unregister(oaddev->attrib, oaddev->identity.attio_id); |
| oaddev->identity.attio_id = 0; |
| } |
| if (oaddev->block.attio_id > 0) { |
| g_attrib_unregister(oaddev->attrib, oaddev->block.attio_id); |
| oaddev->block.attio_id = 0; |
| } |
| if (oaddev->attrib) { |
| g_attrib_unref(oaddev->attrib); |
| oaddev->attrib = NULL; |
| } |
| |
| btd_service_disconnecting_complete(service, 0); |
| |
| return 0; |
| } |
| |
| static struct oad_device *oad_register_device(struct btd_device *device, struct gatt_primary *prim) |
| { |
| struct oad_device *oaddev; |
| GIOCondition cond = G_IO_IN | G_IO_ERR | G_IO_NVAL; |
| GIOChannel *io; |
| |
| oaddev = oad_new_device(device, prim->range.start); |
| if (!oaddev) |
| return NULL; |
| |
| oaddev->oad_primary = g_memdup(prim, sizeof(*prim)); |
| |
| return oaddev; |
| } |
| |
| static int oad_unregister_device(struct oad_device *oaddev) |
| { |
| if (oaddev->tid) { |
| g_source_remove(oaddev->tid); |
| oaddev->tid = 0; |
| } |
| oad_free_device(oaddev); |
| |
| return 0; |
| } |
| |
| static int oad_probe(struct btd_service *service) |
| { |
| struct btd_device *device = btd_service_get_device(service); |
| const char *path = device_get_path(device); |
| GSList *primaries, *l; |
| |
| DBG("OAD path %s", path); |
| |
| primaries = btd_device_get_primaries(device); |
| if (primaries == NULL) |
| return -EINVAL; |
| |
| for (l = primaries; l; l = g_slist_next(l)) { |
| struct gatt_primary *prim = l->data; |
| struct oad_device *oaddev; |
| |
| if (strcmp(prim->uuid, OAD_UUID) != 0) |
| continue; |
| |
| DBG("OAD matched OAD_UUID %s", prim->uuid); |
| oaddev = oad_register_device(device, prim); |
| if (oaddev == NULL) |
| continue; |
| |
| btd_service_set_user_data(service, oaddev); |
| oad_devices = g_slist_append(oad_devices, oaddev); |
| } |
| |
| return 0; |
| } |
| |
| static void oad_remove_device(gpointer a, gpointer b) |
| { |
| struct oad_device *oaddev = a; |
| struct btd_device *device = b; |
| |
| if (oaddev->device != device) |
| return; |
| |
| oad_devices = g_slist_remove(oad_devices, oaddev); |
| oad_unregister_device(oaddev); |
| } |
| |
| static void oad_remove(struct btd_service *service) |
| { |
| struct btd_device *device = btd_service_get_device(service); |
| const char *path = device_get_path(device); |
| |
| DBG("OAD path %s", path); |
| |
| g_slist_foreach(oad_devices, oad_remove_device, device); |
| } |
| |
| static struct btd_profile oad_profile = { |
| .name = "OAD", |
| .remote_uuid = OAD_UUID, |
| .device_probe = oad_probe, |
| .device_remove = oad_remove, |
| .accept = oad_accept, |
| .disconnect = oad_disconnect, |
| .auto_connect = true, |
| }; |
| |
| static int oad_init(void) |
| { |
| int err; |
| |
| return btd_profile_register(&oad_profile); |
| } |
| |
| static void oad_exit(void) |
| { |
| btd_profile_unregister(&oad_profile); |
| } |
| |
| BLUETOOTH_PLUGIN_DEFINE(oad, VERSION, BLUETOOTH_PLUGIN_PRIORITY_LOW, oad_init, oad_exit) |