GFRM200: over-air-download (OAD) support

	* mostly working code to detect and interact with OAD service
	* still debugging; waiting for sample firmware to test download
	* also: log hog/hid bytes (remote keypresses)
	* also: battery level check

Change-Id: I309430252f7ae4314676f82648637758bb7fc52d
diff --git a/Makefile.plugins b/Makefile.plugins
index dbcb491..05c9a88 100644
--- a/Makefile.plugins
+++ b/Makefile.plugins
@@ -66,6 +66,12 @@
 builtin_sources += profiles/input/hog.c profiles/input/uhid_copy.h \
 			profiles/input/suspend.h profiles/input/suspend-dummy.c
 
+builtin_modules += oad
+builtin_sources += profiles/oad/oad.c
+
+builtin_modules += battery
+builtin_sources += profiles/battery/battery.c
+
 if EXPERIMENTAL
 builtin_modules += health
 builtin_sources += profiles/health/mcap_lib.h profiles/health/mcap_internal.h \
diff --git a/plugins/gfrm.c b/plugins/gfrm.c
index 4a542aa..780a3a4 100644
--- a/plugins/gfrm.c
+++ b/plugins/gfrm.c
@@ -165,7 +165,7 @@
 	}
 
 	bdaddr_type = bdaddr_and_type[6];
-	baswap(&bdaddr, &bdaddr_and_type);
+	baswap(&bdaddr, (bdaddr_t*) bdaddr_and_type);
 	ba2str(&bdaddr, bdaddr_str);
 
 	gfrm_dev = gfrm_find_dev_by_bdaddr_type(bdaddr_type);
diff --git a/profiles/battery/battery.c b/profiles/battery/battery.c
new file mode 100644
index 0000000..1bbaa48
--- /dev/null
+++ b/profiles/battery/battery.c
@@ -0,0 +1,351 @@
+/*
+ *
+ *  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 */
+
+/* battery - read battery level */
+
+#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 BATT_UUID		"0000180f-0000-1000-8000-00805f9b34fb"
+#define BATT_LEVEL_UUID		"00002a19-0000-1000-8000-00805f9b34fb"
+
+//#define CHECK_TIME		(24*60*60)
+#define CHECK_TIME		10
+
+struct service {
+	guint			attio_id;
+	uint16_t		value_handle;
+	int			value;
+};
+
+struct batt_device {
+	uint16_t		id;
+	struct btd_device	*device;
+	GAttrib			*attrib;
+	guint			attioid;
+	struct gatt_primary	*batt_primary;
+
+	struct service		level;
+
+	time_t			lastCheck;
+};
+
+static GSList *devices = NULL;
+
+static struct batt_device *batt_new_device(struct btd_device *device,
+								uint16_t id)
+{
+	struct batt_device *battdev;
+
+	DBG("BATT trace");
+	battdev = g_try_new0(struct batt_device, 1);
+	if (!battdev)
+		return NULL;
+
+	battdev->id = id;
+	battdev->device = btd_device_ref(device);
+
+	return battdev;
+}
+
+static void batt_free_device(struct batt_device *battdev)
+{
+	DBG("BATT trace");
+	btd_device_unref(battdev->device);
+	g_attrib_unref(battdev->attrib);
+	g_free(battdev->batt_primary);
+	g_free(battdev);
+}
+
+static time_t wallclock(void)
+{
+	struct timespec ts;
+
+	if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
+		return 0;
+	}
+	return ts.tv_sec;
+}
+
+static void level_read_char_cb(guint8 status, const guint8 *pdu, guint16 len,
+							gpointer user_data)
+{
+	struct batt_device *battdev = user_data;
+
+	DBG("BATT trace");
+	if (status != 0) {
+		error("BATT %s failed: %s", __func__, att_ecode2str(status));
+		return;
+	}
+	int level = pdu[1];
+	DBG("BATT level=%d", level);
+}
+
+static void checkLevel(struct batt_device *battdev)
+{
+	battdev->lastCheck = wallclock();
+	if (battdev->level.value_handle == 0)
+		return;
+
+	gatt_read_char(battdev->attrib, battdev->level.value_handle,
+						level_read_char_cb, battdev);
+}
+
+static int isTimeForLevelCheck(struct batt_device* battdev)
+{
+	DBG("BATT trace");
+
+	time_t now = wallclock();
+	time_t dt = now - battdev->lastCheck;
+	if (dt < CHECK_TIME) {
+		DBG("BATT not time for level check yet");
+		return 0;
+	}
+	DBG("BATT time for battery check");
+	return 1;
+}
+
+static void char_discovered_cb(uint8_t status, GSList *chars, void* user_data)
+{
+	struct batt_device *battdev = user_data;
+	struct gatt_primary *prim = battdev->batt_primary;
+	bt_uuid_t img_identity_uuid, img_block_uuid;
+	GSList *l;
+	uint16_t info_handle = 0, proto_mode_handle = 0;
+
+	DBG("BATT inspecting characteristics");
+	if (status != 0) {
+		error("BATT %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("BATT 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, BATT_LEVEL_UUID) == 0) {
+			battdev->level.value_handle = chr->value_handle;
+			DBG("BATT found BATT_LEVEL_UUID value_handle=0x%04x", chr->value_handle);
+			//discover_desc(battdev, chr, next);
+			if (isTimeForLevelCheck(battdev)) {
+				checkLevel(battdev);
+			}
+		}
+	}
+}
+
+static void attio_connected_cb(GAttrib *attrib, gpointer user_data)
+{
+	struct batt_device *battdev = user_data;
+	struct gatt_primary *prim = battdev->batt_primary;
+	GSList *l;
+
+	DBG("BATT trace");
+	DBG("BATT connected");
+
+	battdev->attrib = g_attrib_ref(attrib);
+	battdev->level.value = -1;
+
+	if (battdev->level.value_handle == 0) {
+		DBG("BATT discovering characteristics");
+
+		gatt_discover_char(battdev->attrib, prim->range.start,
+						prim->range.end, NULL,
+						char_discovered_cb, battdev);
+	} else {
+		if (isTimeForLevelCheck(battdev)) {
+			checkLevel(battdev);
+		}
+	}
+}
+
+static void attio_disconnected_cb(gpointer user_data)
+{
+	struct batt_device *battdev = user_data;
+	GSList *l;
+
+	DBG("BATT trace");
+	DBG("BATT disconnected");
+
+	if (battdev->level.attio_id > 0) {
+		g_attrib_unregister(battdev->attrib, battdev->level.attio_id);
+		battdev->level.attio_id = 0;
+	}
+	if (battdev->attrib)
+		g_attrib_unref(battdev->attrib);
+	battdev->attrib = NULL;
+}
+
+static struct batt_device *batt_register_device(struct btd_device *device,
+						struct gatt_primary *prim)
+{
+	struct batt_device *battdev;
+	GIOCondition cond = G_IO_IN | G_IO_ERR | G_IO_NVAL;
+	GIOChannel *io;
+
+	DBG("BATT trace");
+	battdev = batt_new_device(device, prim->range.start);
+	if (!battdev)
+		return NULL;
+
+	battdev->batt_primary = g_memdup(prim, sizeof(*prim));
+
+	battdev->attioid = btd_device_add_attio_callback(device,
+							attio_connected_cb,
+							attio_disconnected_cb,
+							battdev);
+
+	return battdev;
+}
+
+static int batt_unregister_device(struct batt_device *battdev)
+{
+	DBG("BATT trace");
+	btd_device_remove_attio_callback(battdev->device, battdev->attioid);
+	batt_free_device(battdev);
+
+	return 0;
+}
+
+static int batt_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("BATT trace");
+	DBG("BATT 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 batt_device *battdev;
+
+		DBG("BATT uuid=%s", prim->uuid);
+		if (strcmp(prim->uuid, BATT_UUID) != 0)
+			continue;
+
+		DBG("BATT matched BATT uuid %s", prim->uuid);
+		battdev = batt_register_device(device, prim);
+		if (battdev == NULL)
+			continue;
+
+		devices = g_slist_append(devices, battdev);
+	}
+
+	return 0;
+}
+
+static void remove_device(gpointer a, gpointer b)
+{
+	struct batt_device *battdev = a;
+	struct btd_device *device = b;
+
+	DBG("BATT trace");
+	if (battdev->device != device)
+		return;
+
+	devices = g_slist_remove(devices, battdev);
+	batt_unregister_device(battdev);
+}
+
+static void batt_remove(struct btd_service *service)
+{
+	struct btd_device *device = btd_service_get_device(service);
+	const char *path = device_get_path(device);
+
+	DBG("BATT trace");
+	DBG("BATT path %s", path);
+
+	g_slist_foreach(devices, remove_device, device);
+}
+
+static struct btd_profile batt_profile = {
+	.name		= "Battery",
+	.remote_uuid	= BATT_UUID,
+	.device_probe	= batt_probe,
+	.device_remove	= batt_remove,
+};
+
+static int batt_init(void)
+{
+	int err;
+
+	DBG("BATT trace");
+	return btd_profile_register(&batt_profile);
+}
+
+static void batt_exit(void)
+{
+	DBG("BATT trace");
+	btd_profile_unregister(&batt_profile);
+}
+
+BLUETOOTH_PLUGIN_DEFINE(battery, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
+							batt_init, batt_exit)
diff --git a/profiles/input/hog.c b/profiles/input/hog.c
index 3753343..24a1d90 100644
--- a/profiles/input/hog.c
+++ b/profiles/input/hog.c
@@ -103,6 +103,127 @@
 static gboolean suspend_supported = FALSE;
 static GSList *devices = NULL;
 
+static const char* uhidEventToButton(struct uhid_event* ep)
+{
+	if (ep->type != UHID_INPUT) {
+		error("unexpected uhid event type %d", ep->type);
+		return NULL;
+	}
+	if (ep->u.input.size < 1) {
+		error("empty uhid event");
+		return NULL;
+	}
+
+	/* see "SRS_RC1534059_V0 11.pdf" */
+	/* strings match vendor/broadcom/drivers/bt/3rdparty/embedded/brcm/linux/bthid/bthid.c */
+	switch (ep->u.input.data[0]) {
+	case 0x02:			// usage page 0x01, 0x06
+		if (ep->u.input.size != 9) {
+			error("unexpected uhid size: wanted %d, got %d", 9, ep->u.input.size);
+		  	return NULL;
+		}
+		switch (ep->u.input.data[3]) {
+		case 0x00:    return "RELEASE2";
+		case 0x1E:    return "DIGIT_1";
+		case 0x1F:    return "DIGIT_2";
+		case 0x20:    return "DIGIT_3";
+		case 0x21:    return "DIGIT_4";
+		case 0x22:    return "DIGIT_5";
+		case 0x23:    return "DIGIT_6";
+		case 0x24:    return "DIGIT_7";
+		case 0x25:    return "DIGIT_8";
+		case 0x26:    return "DIGIT_9";
+		case 0x27:    return "DIGIT_0";
+		case 0x28:    return "OK";
+		case 0x4F:    return "RIGHT";
+		case 0x50:    return "LEFT";
+		case 0x51:    return "DOWN";
+		case 0x52:    return "UP";
+		}
+		break;
+
+	case 0x03:			// usage page 0x01, 0x0c
+		if (ep->u.input.size != 3) {
+			error("unexpected uhid size: wanted %d, got %d", 3, ep->u.input.size);
+		  	return NULL;
+		}
+		switch (ep->u.input.data[1] | (ep->u.input.data[2] << 8)) {
+		case 0x00:    return "RELEASE3";
+		case 0x30:    return "TV_BOX_POWER";
+		case 0x40:    return "MENU";
+		case 0x4C:    return "DIGIT_DEL";
+		case 0x63:    return "INPUT";
+		case 0x7F:    return "TV_POWER";
+		case 0x83:    return "BACK";
+		case 0x8D:    return "GUIDE";
+		case 0x94:    return "EXIT";
+		case 0x95:    return "INFO";
+		case 0x9C:    return "CH_UP";
+		case 0x9D:    return "CH_DOWN";
+		case 0xB0:    return "PLAY";
+		case 0xB1:    return "PAUSE";
+		case 0xB2:    return "RECORD";
+		case 0xB3:    return "FAST_FORWARD";
+		case 0xB4:    return "REWIND";
+		case 0xB5:    return "SKIP_FORWARD";
+		case 0xB6:    return "SKIP_BACKWARD";
+		case 0xB7:    return "STOP";
+		case 0xE2:    return "MUTE";
+		case 0xE9:    return "VOL_UP";
+		case 0xEA:    return "VOL_DOWN";
+		case 0x221:   return "SEARCH";
+		case 0x224:   return "PREV";
+		}
+		break;
+
+	case 0x81:			// usage page 0xff, 0x00
+		if (ep->u.input.size != 2) {
+			error("unexpected uhid size: wanted %d, got %d", 2, ep->u.input.size);
+			return NULL;
+		}
+		switch (ep->u.input.data[1]) {
+		case 0x00:    return "RELEASE81";
+		case 0x01:    return "LIVE";
+		}
+		break;
+	}
+	return NULL;
+}
+
+static void dataToHexString(char* str, int strLen, uint8_t* data, int dataLen)
+{
+	static char* hex = "0123456789abcdef";
+	if (strLen < 1) return;
+
+	char* sp = str;
+	char* esp = str + strLen;
+	uint8_t* dp = data;
+	uint8_t* edp = data + dataLen;
+
+	while (sp + 3 < esp && dp < edp) {
+		*sp++ = hex[(*dp >> 4) & 0x0f];
+		*sp++ = hex[(*dp >> 0) & 0x0f];
+		dp++;
+	}
+	*sp = '\0';
+}
+
+static void logButton(const guint8* pdu, guint16 len, struct uhid_event* ep)
+{
+	char pduStr[1024];
+	char eventStr[1024];
+
+	dataToHexString(pduStr, sizeof pduStr, (uint8_t*) pdu, len);
+	dataToHexString(eventStr, sizeof eventStr, (uint8_t*) ep->u.input.data, ep->u.input.size);
+
+	const char* button = uhidEventToButton(ep);
+	if (button == NULL) button = "UNKNOWN";		// hthid.c again
+
+	// for backward compatibility with log parsers, use this format:
+	// BTHID bthid_write: [00241cb74c8f]Sending report to HID: {1f4102} -> KEY_DIGIT_2
+	info("BTHID bthid_write: [%s]Sending report to HID: {%s} -> KEY_%s", pduStr, eventStr, button);
+}
+
 static void report_value_cb(const guint8 *pdu, guint16 len, gpointer user_data)
 {
 	struct report *report = user_data;
@@ -134,6 +255,8 @@
 		ev.u.input.size = len;
 	}
 
+	logButton(pdu, len, &ev);
+
 	err = bt_uhid_send(hogdev->uhid, &ev);
 	if (err < 0) {
 		error("bt_uhid_send: %s (%d)", strerror(-err), -err);
diff --git a/profiles/oad/oad.c b/profiles/oad/oad.c
new file mode 100644
index 0000000..2936b6a
--- /dev/null
+++ b/profiles/oad/oad.c
@@ -0,0 +1,778 @@
+/*
+ *
+ *  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)