blob: 2936b6a578fcb7d894a59da0de546ec666128de2 [file] [log] [blame]
/*
*
* 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 <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <bluetooth/bluetooth.h>
#include <glib.h>
#include "src/log.h"
#include "lib/uuid.h"
#include "src/adapter.h"
#include "src/device.h"
#include "src/profile.h"
#include "src/service.h"
#include "src/plugin.h"
#include "attrib/att.h"
#include "attrib/gattrib.h"
#include "src/attio.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 MAJOR_VERSION(x) ((x) >> 8)
#define MINOR_VERSION(x) ((x) & 0xff)
#define SLOT_VERSION(x) (!(MINOR_VERSION(x) & 0x1))
#define FIRMWARE_SLOT0 "/tmp/gfrm200.slot0.bin"
#define FIRMWARE_SLOT1 "/tmp/gfrm200.slot1.bin"
//#define CHECK_TIME (24*60*60)
#define CHECK_TIME 10
enum OAD_Status {
OADSTATUS_None = 0,
OADSTATUS_DataTransfer = 1,
OADSTATUS_TransferSuccess = 2,
OADSTATUS_CRCError = 0xE0,
OADSTATUS_LVD = 0xE1, // low voltage detection
OADSTATUS_KeyPress = 0xE2, // user interrupted (not idle)
OADSTATUS_GP = 0xE3, // bootloader said no
OADSTATUS_Host = 0xE4, // host did something wrong (eg, timeout)
};
enum OAD_Action {
OADACTION_GetRemoteInfo = 0,
OADACTION_TransferOADData = 1,
OADACTION_StartUpdate = 2,
};
struct fwinfo {
uint16_t gp_version;
uint16_t ti_version;
uint32_t gp_size;
uint32_t ti_size;
int slot;
};
struct 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;
guint attioid;
struct gatt_primary *oad_primary;
struct service identity;
struct service block;
time_t lastCheck;
struct fwinfo remote;
uint8_t header[8];
int fd;
guint tid; /* Timer id to delay starting upgrade */
};
struct characteristic {
struct oad_device *oaddev;
char uuid[MAX_LEN_UUID_STR + 1];
};
static GSList *devices = NULL;
//#define FAKEFW 1
#ifdef FAKEFW
static unsigned char HEADER[] = { 0xaa, 0x55, 0x01, 0x02, 0x0d, 0x04, 0x00, 0x54, 0, 0, 0, 0, 0, 0, 0, 0, };
#endif
static struct oad_device *oad_new_device(struct btd_device *device,
uint16_t id)
{
struct oad_device *oaddev;
DBG("OAD trace");
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)
{
DBG("OAD trace");
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 block_char_write_cb(guint8 status, const guint8 *pdu, guint16 len,
gpointer user_data)
{
struct oad_device *oaddev = user_data;
DBG("OAD trace");
if (status != 0) {
error("OAD %s failed: %s", __func__, att_ecode2str(status));
return;
}
}
static void identity_char_write_cb(guint8 status, const guint8 *pdu, guint16 len,
gpointer user_data)
{
struct oad_device *oaddev = user_data;
DBG("OAD trace");
if (status != 0) {
error("OAD %s failed: %s", __func__, att_ecode2str(status));
return;
}
}
static void block_notify_handler(const uint8_t *pdu, uint16_t len,
gpointer user_data)
{
struct oad_device *oaddev = user_data;
DBG("OAD trace");
/* 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 seek");
close(oaddev->fd);
oaddev->fd = -1;
return;
}
if (read(oaddev->fd, block+2, sizeof block - 2) != sizeof block - 2) {
perror("OAD short read");
close(oaddev->fd);
oaddev->fd = -1;
return;
}
#ifdef FAKEFW
if (block_num == 0) {
memcpy(block+2, HEADER, sizeof block - 2);
} else {
memset(block+2, 0, sizeof block - 2);
}
#endif
DBG("OAD sending block %d", block_num);
gatt_write_char(oaddev->attrib, oaddev->block.value_handle, block,
sizeof(block), 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[9];
memset(attr_val, 0, sizeof attr_val);
attr_val[8] = OADACTION_StartUpdate; /* commit fw image */
DBG("OAD committing firmware");
gatt_write_char(oaddev->attrib, oaddev->identity.value_handle, attr_val,
sizeof(attr_val), identity_char_write_cb, oaddev);
}
break;
case OADSTATUS_CRCError: // checksum error
DBG("OAD firmware transfer checksum error");
close(oaddev->fd);
oaddev->fd = -1;
break;
case OADSTATUS_LVD: // ???
case OADSTATUS_KeyPress: // ???
case OADSTATUS_GP: // ???
case OADSTATUS_Host: // ???
default:
DBG("OAD unexpected block notify status 0x%02x", status);
close(oaddev->fd);
oaddev->fd = -1;
break;
}
}
static time_t wallclock(void)
{
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return 0;
}
return ts.tv_sec;
}
static int isTimeForUpgradeCheck(struct oad_device* oaddev)
{
DBG("OAD trace");
time_t now = wallclock();
time_t dt = now - oaddev->lastCheck;
if (dt < CHECK_TIME) {
DBG("OAD not time for upgrade check yet");
return 0;
}
DBG("OAD time for upgrade check");
return 1;
}
static void checkForUpgrade(struct oad_device *oaddev)
{
DBG("OAD trace");
if (!isTimeForUpgradeCheck(oaddev)) {
return;
}
oaddev->lastCheck = wallclock();
int slot2 = !oaddev->remote.slot; // alternate between slot 0 and 1
char* fw_file = slot2 ? FIRMWARE_SLOT1 : FIRMWARE_SLOT0;
int fd = open(fw_file, O_RDONLY);
if (fd < 0) {
error("OAD open: %s: %s", fw_file, strerror(errno));
return;
}
int len = read(fd, oaddev->header, sizeof (oaddev->header));
#ifdef FAKEFW
memcpy(oaddev->header, HEADER, sizeof oaddev->header);
#endif
if (len < 0) {
error("OAD read: %s: %s", fw_file, strerror(errno));
close(fd);
return;
}
if (len < sizeof oaddev->header) {
error("OAD short read: %s: wanted %d, got %d", fw_file, sizeof oaddev->header, len);
close(fd);
return;
}
/* 8 bytes of header appear to be:
* 2 bytes checksum
* 2 bytes version
* 3 bytes size
* 1 byte type: "T" TI image, "G" GP image
*/
uint16_t version = get_le16(oaddev->header + 2);
int type = oaddev->header[7];
if (type == 'T') {
int slot = SLOT_VERSION(version);
DBG("OAD firmware on disk: file=%s ti_version=%d.%d slot=%d",
fw_file, MAJOR_VERSION(version), MINOR_VERSION(version), slot);
if (version <= oaddev->remote.ti_version) {
DBG("OAD TI firmware is up to date");
close(fd);
return;
}
} else if (type == 'G') {
DBG("OAD firmware on disk: file=%s gp_version=%d.%d",
fw_file, MAJOR_VERSION(version), MINOR_VERSION(version));
if (version <= oaddev->remote.gp_version) {
DBG("OAD GP firmware is up to date");
close(fd);
return;
}
} else {
error("OAD firmware on disk: unknown type '%c': file=%s version=%d.%d",
type, fw_file, MAJOR_VERSION(version), MINOR_VERSION(version));
close(fd);
return;
}
DBG("OAD starting upgrade");
if (oaddev->fd != -1) {
close(oaddev->fd);
}
oaddev->fd = fd; /* fd is left open */
if (oaddev->tid) {
g_source_remove(oaddev->tid);
}
uint8_t attr_val[9];
memcpy(attr_val, oaddev->header, sizeof oaddev->header);
attr_val[8] = OADACTION_TransferOADData; /* start OAD image transfer */
gatt_write_char(oaddev->attrib, oaddev->identity.value_handle, attr_val,
sizeof(attr_val), identity_char_write_cb, oaddev);
}
static void identity_notify_handler(const uint8_t *pdu, uint16_t len,
gpointer user_data)
{
struct oad_device *oaddev = user_data;
DBG("OAD trace");
/* should be at least opcode (1b) + handle (76 00) */
if (len < 3) {
error("OAD Invalid PDU received");
return;
}
/* Next 10 bytes appear to be:
* 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.gp_version = get_le16(pdu+3);
oaddev->remote.gp_size = get_le32(pdu+5) & 0x00ffffff;
oaddev->remote.slot = SLOT_VERSION(oaddev->remote.gp_version);
oaddev->remote.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.ti_size = get_le32(size);
DBG("OAD firmware on remote: gp_version=%d.%d gp_size=%d ti_version=%d.%d ti_size=%d slot=%d",
MAJOR_VERSION(oaddev->remote.gp_version), MINOR_VERSION(oaddev->remote.gp_version),
oaddev->remote.gp_size,
MAJOR_VERSION(oaddev->remote.ti_version), MINOR_VERSION(oaddev->remote.ti_version),
oaddev->remote.ti_size,
oaddev->remote.slot);
checkForUpgrade(oaddev);
}
static void check_fwversion(struct oad_device *oaddev)
{
uint8_t attr_val[9]; // 8 byte header + 1 command byte (all zeros)
DBG("OAD trace");
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), identity_char_write_cb, oaddev);
}
static void ccc_char_write_identity_cb(guint8 status, const guint8 *pdu, guint16 len,
gpointer user_data)
{
struct oad_device *oaddev = user_data;
DBG("OAD trace");
if (status != 0) {
error("OAD %s failed: %s", __func__, att_ecode2str(status));
return;
}
oaddev->identity.notify_enabled = 1;
check_fwversion(oaddev);
}
static void ccc_char_write_block_cb(guint8 status, const guint8 *pdu, guint16 len,
gpointer user_data)
{
struct oad_device *oaddev = user_data;
DBG("OAD trace");
if (status != 0) {
error("OAD %s failed: %s", __func__, att_ecode2str(status));
return;
}
oaddev->block.notify_enabled = 1;
check_fwversion(oaddev);
}
static void enableNotify(struct oad_device* oaddev)
{
DBG("OAD trace");
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,
identity_notify_handler, oaddev, NULL);
oaddev->block.attio_id =
g_attrib_register(oaddev->attrib,
ATT_OP_HANDLE_NOTIFY, oaddev->block.value_handle,
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), 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), ccc_char_write_block_cb, oaddev);
}
static gboolean upgrade_timer(gpointer data)
{
struct oad_device *oaddev = data;
g_source_remove(oaddev->tid);
oaddev->tid = 0;
DBG("OAD timer fired");
enableNotify(oaddev);
}
static void desc_discovered_cb(uint8_t status, GSList *descs, void* user_data)
{
struct characteristic *ch = user_data;
struct oad_device *oaddev = ch->oaddev;
struct GSList *list = NULL;
DBG("OAD trace");
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(10, upgrade_timer, oaddev);
done:
g_free(ch);
}
static void discover_desc(struct oad_device *oaddev, struct gatt_char *c,
struct gatt_char *c_next)
{
struct gatt_primary *prim = oaddev->oad_primary;
struct characteristic *ch;
uint16_t start, end;
DBG("OAD trace");
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 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, desc_discovered_cb, ch);
}
static void 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;
DBG("OAD 0x%04x UUID: %s properties: %02x",
chr->handle, chr->uuid, chr->properties);
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);
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);
discover_desc(oaddev, chr, next);
}
}
}
static void attio_connected_cb(GAttrib *attrib, gpointer user_data)
{
struct oad_device *oaddev = user_data;
struct gatt_primary *prim = oaddev->oad_primary;
GSList *l;
DBG("OAD trace");
DBG("OAD connected");
oaddev->attrib = g_attrib_ref(attrib);
oaddev->identity.notify_enabled = 0;
oaddev->block.notify_enabled = 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,
char_discovered_cb, oaddev);
} else {
if (isTimeForUpgradeCheck(oaddev)) {
enableNotify(oaddev);
}
}
}
static void attio_disconnected_cb(gpointer user_data)
{
struct oad_device *oaddev = user_data;
GSList *l;
DBG("OAD trace");
DBG("OAD disconnected");
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;
}
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;
DBG("OAD trace");
oaddev = oad_new_device(device, prim->range.start);
if (!oaddev)
return NULL;
oaddev->oad_primary = g_memdup(prim, sizeof(*prim));
oaddev->attioid = btd_device_add_attio_callback(device,
attio_connected_cb,
attio_disconnected_cb,
oaddev);
return oaddev;
}
static int oad_unregister_device(struct oad_device *oaddev)
{
DBG("OAD trace");
btd_device_remove_attio_callback(oaddev->device, oaddev->attioid);
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 trace");
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;
DBG("OAD uuid=%s", prim->uuid);
if (strcmp(prim->uuid, OAD_UUID) != 0)
continue;
DBG("OAD matched OAD uuid", prim->uuid);
oaddev = oad_register_device(device, prim);
if (oaddev == NULL)
continue;
devices = g_slist_append(devices, oaddev);
}
return 0;
}
static void remove_device(gpointer a, gpointer b)
{
struct oad_device *oaddev = a;
struct btd_device *device = b;
DBG("OAD trace");
if (oaddev->device != device)
return;
devices = g_slist_remove(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 trace");
DBG("OAD path %s", path);
g_slist_foreach(devices, remove_device, device);
}
static struct btd_profile oad_profile = {
.name = "OAD",
.remote_uuid = OAD_UUID,
.device_probe = oad_probe,
.device_remove = oad_remove,
};
static int oad_init(void)
{
int err;
DBG("OAD trace");
return btd_profile_register(&oad_profile);
}
static void oad_exit(void)
{
DBG("OAD trace");
btd_profile_unregister(&oad_profile);
}
BLUETOOTH_PLUGIN_DEFINE(oad, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
oad_init, oad_exit)