Skip to content
Snippets Groups Projects
modbluetooth_card10.c 23.55 KiB
#include "modbluetooth_card10.h"
#include "extmod/modbluetooth.h"
#include "py/runtime.h"
#include "py/mperrno.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>

enum notification_status {
	NOTIFICATION_STATUS_UNKNOWN,
	NOTIFICATION_STATUS_PENDING,
	NOTIFICATION_STATUS_SUCCESS,
	NOTIFICATION_STATUS_OVERFLOW
};

typedef struct _pendig_gattc_operation_obj_t {
	mp_obj_base_t base;
	uint16_t conn_handle;
	uint16_t value_handle;
	unsigned int mode;
	uint16_t value_len;
	uint8_t value[];
} pending_gattc_operation_obj_t;

STATIC const mp_obj_type_t pending_gattc_operation_type;

typedef struct _mp_bluetooth_card10_root_pointers_t {
	// Characteristic (and descriptor) value storage.
	mp_gatts_db_t gatts_db;
	mp_gatts_db_t gatts_status;
	mp_obj_t attc_pending;
} mp_bluetooth_card10_root_pointers_t;

#define GATTS_DB (MP_STATE_PORT(bluetooth_card10_root_pointers)->gatts_db)
#define GATTS_STATUS                                                           \
	(MP_STATE_PORT(bluetooth_card10_root_pointers)->gatts_status)
#define ATTC_PENDING                                                           \
	(MP_STATE_PORT(bluetooth_card10_root_pointers)->attc_pending)

typedef struct {
	enum notification_status notification_status;
} gatts_status_entry_t;

static bool active = false;
static mp_obj_bluetooth_uuid_t uuid_filter;

const char *const not_implemented_message =
	"Not (yet) implemented on card10. See https://git.card10.badge.events.ccc.de/card10/firmware/-/issues/8";

static void raise(void)
{
	mp_raise_NotImplementedError(not_implemented_message);
}

static void gatts_status_create_entry(mp_gatts_db_t db, uint16_t handle)
{
	mp_map_elem_t *elem = mp_map_lookup(
		db,
		MP_OBJ_NEW_SMALL_INT(handle),
		MP_MAP_LOOKUP_ADD_IF_NOT_FOUND
	);
	gatts_status_entry_t *entry = m_new(gatts_status_entry_t, 1);
	entry->notification_status  = NOTIFICATION_STATUS_UNKNOWN;
	elem->value                 = MP_OBJ_FROM_PTR(entry);
}

static gatts_status_entry_t *
gatts_status_lookup(mp_gatts_db_t db, uint16_t handle)
{
	mp_map_elem_t *elem =
		mp_map_lookup(db, MP_OBJ_NEW_SMALL_INT(handle), MP_MAP_LOOKUP);
	if (!elem) {
		mp_raise_OSError(-EACCES);
	}
	return MP_OBJ_TO_PTR(elem->value);
}

static void clear_events(void)
{
	struct epic_ble_event ble_event;
	int ret;

	do {
		ret = epic_ble_get_event(&ble_event);
		if (ret >= 0) {
			epic_ble_free_event(&ble_event);
		}
	} while (ret >= 0);
}

static void handle_att_event(struct epic_att_event *att_event)
{
	if (att_event->hdr.event == ATTS_HANDLE_VALUE_CNF) {
		gatts_status_entry_t *e =
			gatts_status_lookup(GATTS_STATUS, att_event->handle);
		if (att_event->hdr.status == ATT_SUCCESS) {
			e->notification_status = NOTIFICATION_STATUS_SUCCESS;
		} else if (att_event->hdr.status == ATT_ERR_OVERFLOW) {
			e->notification_status = NOTIFICATION_STATUS_OVERFLOW;
		}
	}
#if MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE
	if (att_event->hdr.event == ATTC_READ_BY_GROUP_TYPE_RSP) {
		// TODO: can we get the connection id from the event?

		uint8_t *v         = att_event->pValue;
		uint8_t entry_size = v[0];

		if (att_event->hdr.status == ATT_SUCCESS) {
			for (size_t i = 1; i < att_event->valueLen;
			     i += entry_size) {
				uint16_t start_handle = (v[i + 1] << 8) | v[i];
				uint16_t end_handle =
					(v[i + 3] << 8) | v[i + 2];
				mp_obj_bluetooth_uuid_t service_uuid;
				service_uuid.base.type =
					&mp_type_bluetooth_uuid;
				if (entry_size == 6) {
					memcpy(service_uuid.data, v + i + 4, 2);
					service_uuid.type =
						MP_BLUETOOTH_UUID_TYPE_16;
				} else if (entry_size == 20) {
					memcpy(service_uuid.data,
					       v + i + 4,
					       16);
					service_uuid.type =
						MP_BLUETOOTH_UUID_TYPE_128;
				}
				mp_bluetooth_gattc_on_primary_service_result(
					1,
					start_handle,
					end_handle,
					&service_uuid
				);
			}
		}

		if (att_event->hdr.status != ATT_SUCCESS ||
		    att_event->continuing == 0) {
			uint16_t status = 0;
			mp_bluetooth_gattc_on_discover_complete(
				MP_BLUETOOTH_IRQ_GATTC_SERVICE_DONE, 1, status
			);
		}
	}

	if (att_event->hdr.event == ATTC_READ_BY_TYPE_RSP) {
		/* if read by type successful */
		if (att_event->hdr.status == ATT_SUCCESS) {
			// TODO: can we get the connection id from the event?
			uint8_t *v         = att_event->pValue;
			uint8_t entry_size = v[0];

			for (size_t i = 1; i < att_event->valueLen;
			     i += entry_size) {
				uint16_t def_handle = (v[i + 1] << 8) | v[i];
				uint8_t properties  = v[i + 2];
				uint16_t value_handle =
					(v[i + 4] << 8) | v[i + 3];
				mp_obj_bluetooth_uuid_t characteristic_uuid;
				characteristic_uuid.base.type =
					&mp_type_bluetooth_uuid;
				if (entry_size == 2 + 1 + 2 + 2) {
					memcpy(characteristic_uuid.data,
					       v + i + 5,
					       2);
					characteristic_uuid.type =
						MP_BLUETOOTH_UUID_TYPE_16;
				} else if (entry_size == 2 + 1 + 2 + 16) {
					memcpy(characteristic_uuid.data,
					       v + i + 5,
					       16);
					characteristic_uuid.type =
						MP_BLUETOOTH_UUID_TYPE_128;
				}

				// TODO: uuid_filter is set: compare against uuid
				mp_bluetooth_gattc_on_characteristic_result(
					1,
					def_handle,
					value_handle,
					properties,
					&characteristic_uuid
				);
			}
		}

		if (att_event->hdr.status != ATT_SUCCESS ||
		    att_event->continuing == 0) {
			uint16_t status = 0;
			mp_bluetooth_gattc_on_discover_complete(
				MP_BLUETOOTH_IRQ_GATTC_CHARACTERISTIC_DONE,
				1,
				status
			);
		}
	}

	if (att_event->hdr.event == ATTC_FIND_INFO_RSP) {
		if (att_event->hdr.status == ATT_SUCCESS) {
			// TODO: can we get the connection id from the event?
			uint8_t *v         = att_event->pValue;
			uint8_t entry_size = v[0] == 1 ? 4 : 18;

			for (size_t i = 1; i < att_event->valueLen;
			     i += entry_size) {
				uint16_t descriptor_handle =
					(v[i + 1] << 8) | v[i];
				mp_obj_bluetooth_uuid_t descriptor_uuid;
				descriptor_uuid.base.type =
					&mp_type_bluetooth_uuid;
				if (entry_size == 2 + 2) {
					memcpy(descriptor_uuid.data,
					       v + i + 2,
					       2);
					descriptor_uuid.type =
						MP_BLUETOOTH_UUID_TYPE_16;
				} else if (entry_size == 2 + 16) {
					memcpy(descriptor_uuid.data,
					       v + i + 2,
					       16);
					descriptor_uuid.type =
						MP_BLUETOOTH_UUID_TYPE_128;
				}

				mp_bluetooth_gattc_on_descriptor_result(
					1, descriptor_handle, &descriptor_uuid
				);
			}
		}

		if (att_event->hdr.status != ATT_SUCCESS ||
		    att_event->continuing == 0) {
			uint16_t status = 0;
			mp_bluetooth_gattc_on_discover_complete(
				MP_BLUETOOTH_IRQ_GATTC_DESCRIPTOR_DONE,
				1,
				status
			);
		}
	}

	if (att_event->hdr.event == ATTC_READ_RSP) {
		mp_bluetooth_gattc_on_data_available(
			MP_BLUETOOTH_IRQ_GATTC_READ_RESULT,
			1,
			att_event->handle,
			(const uint8_t **)&att_event->pValue,
			&att_event->valueLen,
			1
		);
		mp_bluetooth_gattc_on_read_write_status(
			MP_BLUETOOTH_IRQ_GATTC_READ_DONE,
			1,
			att_event->handle,
			att_event->hdr.status
		);
	}

	if (att_event->hdr.event == ATTC_HANDLE_VALUE_NTF ||
	    att_event->hdr.event == ATTC_HANDLE_VALUE_IND) {
		uint16_t ev = att_event->hdr.event == ATTC_HANDLE_VALUE_NTF ?
				      MP_BLUETOOTH_IRQ_GATTC_NOTIFY :
				      MP_BLUETOOTH_IRQ_GATTC_INDICATE;
		mp_bluetooth_gattc_on_data_available(
			ev,
			1,
			att_event->handle,
			(const uint8_t **)&att_event->pValue,
			&att_event->valueLen,
			1
		);
	}

	if (att_event->hdr.event == ATTC_WRITE_RSP) {
		mp_bluetooth_gattc_on_read_write_status(
			MP_BLUETOOTH_IRQ_GATTC_WRITE_DONE,
			1,
			att_event->handle,
			att_event->hdr.status
		);
	}

	bool send_attc = false;
	size_t len;
	mp_obj_t *items;

	if (att_event->hdr.event == ATTC_WRITE_CMD_RSP) {
		mp_obj_list_get(ATTC_PENDING, &len, &items);
		for (size_t i = 0; i < len; i++) {
			pending_gattc_operation_obj_t *op =
				MP_OBJ_TO_PTR(items[i]);
			if (op->mode == MP_BLUETOOTH_WRITE_MODE_NO_RESPONSE &&
			    op->conn_handle == 1 &&
			    op->value_handle == att_event->handle) {
				mp_obj_subscr(
					ATTC_PENDING,
					MP_OBJ_NEW_SMALL_INT(i),
					MP_OBJ_NULL
				);
				send_attc = true;
				break;
			}
		}
	}

	if (att_event->hdr.event == ATTC_WRITE_RSP) {
		mp_obj_list_get(ATTC_PENDING, &len, &items);
		for (size_t i = 0; i < len; i++) {
			pending_gattc_operation_obj_t *op =
				MP_OBJ_TO_PTR(items[i]);
			if (op->mode == MP_BLUETOOTH_WRITE_MODE_WITH_RESPONSE &&
			    op->conn_handle == 1 &&
			    op->value_handle == att_event->handle) {
				mp_obj_subscr(
					ATTC_PENDING,
					MP_OBJ_NEW_SMALL_INT(i),
					MP_OBJ_NULL
				);
				send_attc = true;
				break;
			}
		}
	}

	if (send_attc) {
		mp_obj_list_get(ATTC_PENDING, &len, &items);
		if (len > 0) {
			pending_gattc_operation_obj_t *op =
				MP_OBJ_TO_PTR(items[0]);
			if (op->mode == MP_BLUETOOTH_WRITE_MODE_NO_RESPONSE) {
				epic_ble_attc_write_no_rsp(
					op->conn_handle,
					op->value_handle,
					op->value,
					op->value_len
				);
			} else if (
				op->mode ==
				MP_BLUETOOTH_WRITE_MODE_WITH_RESPONSE) {
				epic_ble_attc_write(
					op->conn_handle,
					op->value_handle,
					op->value,
					op->value_len
				);
			}
		}
	}
#endif
}

static void handle_dm_event(struct epic_dm_event *dm_event)
{
	struct epic_wsf_header *hdr = (struct epic_wsf_header *)dm_event;
	if (hdr->event == DM_CONN_OPEN_IND) {
		struct epic_hciLeConnCmpl_event *e =
			(struct epic_hciLeConnCmpl_event *)dm_event;
		mp_bluetooth_gap_on_connected_disconnected(
			MP_BLUETOOTH_IRQ_CENTRAL_CONNECT,
			e->hdr.param,
			e->addrType,
			e->peerAddr
		);
	} else if (hdr->event == DM_CONN_CLOSE_IND) {
		struct epic_hciDisconnectCmpl_event *e =
			(struct epic_hciDisconnectCmpl_event *)dm_event;
		uint8_t addr[6] = {};
		mp_bluetooth_gap_on_connected_disconnected(
			MP_BLUETOOTH_IRQ_CENTRAL_DISCONNECT,
			e->hdr.param,
			0xFF,
			addr
		);
	}
}

static void handle_att_write(struct epic_att_write *att_write)
{
	mp_bluetooth_gatts_db_entry_t *entry =
		mp_bluetooth_gatts_db_lookup(GATTS_DB, att_write->handle);
	if (!entry) {
		return;
	}

	// TODO: Use `offset` arg.
	size_t append_offset = 0;
	if (entry->append) {
		append_offset = entry->data_len;
	}
	entry->data_len =
		MIN(entry->data_alloc, att_write->valueLen + append_offset);
	memcpy(entry->data + append_offset,
	       att_write->buffer,
	       entry->data_len - append_offset);

	mp_bluetooth_gatts_on_write(att_write->hdr.param, att_write->handle);
}

static mp_obj_t mp_ble_poll_events(mp_obj_t interrupt_id)
{
	struct epic_ble_event ble_event;
	int ret;

	do {
		ret = epic_ble_get_event(&ble_event);
		if (ret >= 0) {
			if (ble_event.type == BLE_EVENT_ATT_EVENT) {
				handle_att_event(ble_event.att_event);
			}
			if (ble_event.type == BLE_EVENT_DM_EVENT) {
				handle_dm_event(ble_event.dm_event);
			}
			if (ble_event.type == BLE_EVENT_ATT_WRITE) {
				handle_att_write(ble_event.att_write);
			}
			epic_ble_free_event(&ble_event);
		}
	} while (ret >= 0);
	return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(ble_event_obj, mp_ble_poll_events);

mp_obj_t mp_interrupt_set_callback(mp_obj_t id_in, mp_obj_t callback_obj);

// Enables the Bluetooth stack.
int mp_bluetooth_init(void)
{
	MP_STATE_PORT(bluetooth_card10_root_pointers) =
		m_new0(mp_bluetooth_card10_root_pointers_t, 1);

	mp_bluetooth_gatts_db_create(&GATTS_DB);
	mp_bluetooth_gatts_db_create(&GATTS_STATUS);
	ATTC_PENDING = mp_obj_new_list(0, NULL);

	mp_interrupt_set_callback(
		MP_ROM_INT(EPIC_INT_BLE), (mp_obj_t *)&ble_event_obj
	);
	clear_events();
	epic_interrupt_enable(EPIC_INT_BLE);
	active = true;
	return 0;
}

// Disables the Bluetooth stack. Is a no-op when not enabled.
void mp_bluetooth_deinit(void)
{
	epic_interrupt_disable(EPIC_INT_BLE);
	active = false;
}

// Returns true when the Bluetooth stack is enabled.
bool mp_bluetooth_is_active(void)
{
	return active;
}

// Gets the current address of this device in big-endian format.
void mp_bluetooth_get_current_address(uint8_t *addr_type, uint8_t *addr)
{
	*addr_type = 0; // Public address
	epic_ble_get_address(addr);
}

// Sets the addressing mode to use.
void mp_bluetooth_set_address_mode(uint8_t addr_mode)
{
	raise();
}

size_t mp_bluetooth_gap_get_device_name(const uint8_t **buf)
{
	uint16_t len;
	epic_ble_get_device_name((uint8_t **)buf, &len);
	return len;
}

int mp_bluetooth_gap_set_device_name(const uint8_t *buf, size_t len)
{
	return epic_ble_set_device_name(buf, len);
}

// Start advertisement. Will re-start advertisement when already enabled.
// Returns errno on failure.
int mp_bluetooth_gap_advertise_start(
	bool connectable,
	int32_t interval_us,
	const uint8_t *adv_data,
	size_t adv_data_len,
	const uint8_t *sr_data,
	size_t sr_data_len
) {
	// Dropping any current connection starts advertising on the card10
	int connection = epic_ble_is_connection_open();
	if (connection > 0) {
		epic_ble_close_connection(connection);
	}

	return epic_ble_advertise(
		interval_us,
		adv_data,
		adv_data_len,
		sr_data,
		sr_data_len,
		connectable
	);
}

// Stop advertisement. No-op when already stopped.
void mp_bluetooth_gap_advertise_stop(void)
{
	epic_ble_advertise_stop();
}

// Start adding services. Must be called before mp_bluetooth_register_service.
int mp_bluetooth_gatts_register_service_begin(bool append)
{
	if (!append) {
		epic_ble_atts_dyn_delete_groups();
	}
	return 0;
}

// Add a service with the given list of characteristics to the queue to be registered.
// The value_handles won't be valid until after mp_bluetooth_register_service_end is called.
int mp_bluetooth_gatts_register_service(
	mp_obj_bluetooth_uuid_t *service_uuid,
	mp_obj_bluetooth_uuid_t **characteristic_uuids,
	uint16_t *characteristic_flags,
	mp_obj_bluetooth_uuid_t **descriptor_uuids,
	uint16_t *descriptor_flags,
	uint8_t *num_descriptors,
	uint16_t *handles,
	size_t num_characteristics
) {
	void *pSvcHandle;

	size_t num_descriptors_total = 0;
	size_t num_ccds              = 0;
	for (size_t i = 0; i < num_characteristics; i++) {
		num_descriptors_total += num_descriptors[i];
		if ((characteristic_flags[i] &
		     MP_BLUETOOTH_CHARACTERISTIC_FLAG_NOTIFY) ||
		    (characteristic_flags[i] &
		     MP_BLUETOOTH_CHARACTERISTIC_FLAG_INDICATE)) {
			num_ccds++;
		}
	}

	// One handle for the service, one per characteristic, one per value, one per (CCC) descriptor
	const uint16_t numHandles =
		1 + num_characteristics * 2 + num_ccds + num_descriptors_total;

	uint8_t uuid_len =
		service_uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 : 16;

	// TODO: handle error
	epic_atts_dyn_create_service(
		service_uuid->data, uuid_len, numHandles, &pSvcHandle
	);

	size_t descriptor_index = 0;
	size_t handle_index     = 0;

	for (size_t i = 0; i < num_characteristics; i++) {
		uint8_t flags                 = characteristic_flags[i];
		mp_obj_bluetooth_uuid_t *uuid = characteristic_uuids[i];
		uuid_len = uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 : 16;

		uint16_t value_handle;
		epic_atts_dyn_add_characteristic(
			pSvcHandle,
			uuid->data,
			uuid_len,
			flags,
			MP_BLUETOOTH_DEFAULT_ATTR_LEN,
			&value_handle
		);

		;
		handles[handle_index] = value_handle;
		mp_bluetooth_gatts_db_create_entry(
			GATTS_DB, value_handle, MP_BLUETOOTH_DEFAULT_ATTR_LEN
		);
		gatts_status_create_entry(GATTS_STATUS, value_handle);

		if ((flags & MP_BLUETOOTH_CHARACTERISTIC_FLAG_NOTIFY) ||
		    flags & (MP_BLUETOOTH_CHARACTERISTIC_FLAG_INDICATE)) {
			/* CCCD */
			uint8_t cccd_buf[2] = {};
			// TODO: Handle CCC data.
			// Until then: activate notification/indications by default.
			if (flags & MP_BLUETOOTH_CHARACTERISTIC_FLAG_NOTIFY) {
				cccd_buf[0] |= 0x01;
			}
			if (flags & MP_BLUETOOTH_CHARACTERISTIC_FLAG_INDICATE) {
				cccd_buf[0] |= 0x02;
			}

			uint16_t cccd_handle;
			uint8_t ccc_uuid[] = { 0x02, 0x29 };
			uint8_t flags = MP_BLUETOOTH_CHARACTERISTIC_FLAG_READ |
					MP_BLUETOOTH_CHARACTERISTIC_FLAG_WRITE;
			epic_ble_atts_dyn_add_descriptor(
				pSvcHandle,
				ccc_uuid,
				sizeof(ccc_uuid),
				flags,
				cccd_buf,
				sizeof(cccd_buf),
				sizeof(cccd_buf),
				&cccd_handle
			);

			mp_bluetooth_gatts_db_create_entry(
				GATTS_DB, cccd_handle, sizeof(cccd_buf)
			);
			int ret = mp_bluetooth_gatts_db_write(
				GATTS_DB,
				cccd_handle,
				cccd_buf,
				sizeof(cccd_buf)
			);
			if (ret) {
				return ret;
			}
		}

		handle_index++;

		for (size_t j = 0; j < num_descriptors[i]; j++) {
			flags = descriptor_flags[descriptor_index];
			mp_obj_bluetooth_uuid_t *uuid =
				descriptor_uuids[descriptor_index];
			uuid_len = uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 :
									     16;

			uint16_t descriptor_handle;
			epic_ble_atts_dyn_add_descriptor(
				pSvcHandle,
				uuid->data,
				uuid_len,
				flags,
				NULL,
				0,
				MP_BLUETOOTH_DEFAULT_ATTR_LEN,
				&descriptor_handle
			);

			handles[handle_index] = descriptor_handle;
			mp_bluetooth_gatts_db_create_entry(
				GATTS_DB,
				descriptor_handle,
				MP_BLUETOOTH_DEFAULT_ATTR_LEN
			);

			descriptor_index++;
			handle_index++;
		}
	}

	return 0;
}
// Register any queued services.
int mp_bluetooth_gatts_register_service_end()
{
	epic_atts_dyn_send_service_changed_ind();
	return 0;
}

// Read the value from the local gatts db (likely this has been written by a central).
int mp_bluetooth_gatts_read(
	uint16_t value_handle, uint8_t **value, size_t *value_len
) {
	return mp_bluetooth_gatts_db_read(
		GATTS_DB, value_handle, value, value_len
	);
}
// Write a value to the local gatts db (ready to be queried by a central).
int mp_bluetooth_gatts_write(
	uint16_t value_handle, const uint8_t *value, size_t value_len
) {
	// TODO: which return value to choose?
	mp_bluetooth_gatts_db_write(GATTS_DB, value_handle, value, value_len);
	int ret = epic_ble_atts_set_attr(value_handle, value, value_len);
	return ret;
}
// Notify the central that it should do a read.
int mp_bluetooth_gatts_notify(uint16_t conn_handle, uint16_t value_handle)
{
	// Note: cordio doesn't appear to support sending a notification without a value, so include the stored value.
	uint8_t *data = NULL;
	size_t len    = 0;
	//mp_bluetooth_gatts_read(value_handle, &data, &len);
	mp_bluetooth_gatts_db_read(GATTS_DB, value_handle, &data, &len);
	return mp_bluetooth_gatts_notify_send(
		conn_handle, value_handle, data, len
	);
}
// Notify the central, including a data payload. (Note: does not set the gatts db value).
int mp_bluetooth_gatts_notify_send(
	uint16_t conn_handle,
	uint16_t value_handle,
	const uint8_t *value,
	size_t value_len
) {
	// TODO: We could make use of a list similar to GATTC operations to
	// avoid polling for the return value in this function
	gatts_status_entry_t *e =
		gatts_status_lookup(GATTS_STATUS, value_handle);
	e->notification_status = NOTIFICATION_STATUS_PENDING;

	int ret = epic_ble_atts_handle_value_ntf(
		conn_handle, value_handle, value_len, (uint8_t *)value
	);

	if (ret < 0) {
		return ret;
	}

	while (e->notification_status == NOTIFICATION_STATUS_PENDING) {
		mp_ble_poll_events(0);
	}

	if (e->notification_status != NOTIFICATION_STATUS_SUCCESS) {
		// TODO: better error mapping
		return -EIO;
	}
	return 0;
}
// Indicate the central.
int mp_bluetooth_gatts_indicate(uint16_t conn_handle, uint16_t value_handle)
{
	// TODO: We could make use of a list similar to GATTC operations to
	// avoid polling for the return value in this function
	gatts_status_entry_t *e =
		gatts_status_lookup(GATTS_STATUS, value_handle);
	e->notification_status = NOTIFICATION_STATUS_PENDING;

	// Note: cordio doesn't appear to support sending a notification without a value, so include the stored value.
	uint8_t *value   = NULL;
	size_t value_len = 0;
	mp_bluetooth_gatts_read(value_handle, &value, &value_len);

	int ret = epic_ble_atts_handle_value_ind(
		conn_handle, value_handle, value_len, (uint8_t *)value
	);

	if (ret < 0) {
		return ret;
	}

	while (e->notification_status == NOTIFICATION_STATUS_PENDING) {
		mp_ble_poll_events(0);
	}

	if (e->notification_status != NOTIFICATION_STATUS_SUCCESS) {
		// TODO: better error mapping
		return -EIO;
	}

	// TODO: How does the cordio stack signal that the indication was acked?
	// Need to call mp_bluetooth_gatts_on_indicate_complete afterwards
	return 0;
}

// Resize and enable/disable append-mode on a value.
// Append-mode means that remote writes will append and local reads will clear after reading.
int mp_bluetooth_gatts_set_buffer(uint16_t value_handle, size_t len, bool append)
{
	// TODO; which return value to use?
	mp_bluetooth_gatts_db_resize(GATTS_DB, value_handle, len, append);
	return -epic_ble_atts_set_buffer(value_handle, len, append);
}

// Disconnect from a central or peripheral.
int mp_bluetooth_gap_disconnect(uint16_t conn_handle)
{
	epic_ble_close_connection(conn_handle);
	return 0;
}

// Set/get the MTU that we will respond to a MTU exchange with.
int mp_bluetooth_get_preferred_mtu(void)
{
	raise();
	return 0;
}

int mp_bluetooth_set_preferred_mtu(uint16_t mtu)
{
	raise();
	return 0;
}

#if MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE
// Start a discovery (scan). Set duration to zero to run continuously.
int mp_bluetooth_gap_scan_start(
	int32_t duration_ms,
	int32_t interval_us,
	int32_t window_us,
	bool active_scan
) {
	raise();
	return 0;
}

// Stop discovery (if currently active).
int mp_bluetooth_gap_scan_stop(void)
{
	raise();
	return 0;
}

// Connect to a found peripheral.
int mp_bluetooth_gap_peripheral_connect(
	uint8_t addr_type, const uint8_t *addr, int32_t duration_ms
) {
	raise();
	return 0;
}

// Find all primary services on the connected peripheral.
int mp_bluetooth_gattc_discover_primary_services(
	uint16_t conn_handle, const mp_obj_bluetooth_uuid_t *uuid
) {
	if (uuid) {
		uint8_t uuid_len =
			uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 : 16;
		return epic_ble_attc_discover_primary_services(
			conn_handle, uuid->data, uuid_len
		);
	} else {
		return epic_ble_attc_discover_primary_services(
			conn_handle, NULL, 0
		);
	}
}
// Find all characteristics on the specified service on a connected peripheral.
int mp_bluetooth_gattc_discover_characteristics(
	uint16_t conn_handle,
	uint16_t start_handle,
	uint16_t end_handle,
	const mp_obj_bluetooth_uuid_t *uuid
) {
	if (uuid) {
		uuid_filter = *uuid;
	} else {
		uuid_filter.type = 0;
	}
	return epic_ble_attc_discover_characteristics(
		conn_handle, start_handle, end_handle
	);
}

// Find all descriptors on the specified characteristic on a connected peripheral.
int mp_bluetooth_gattc_discover_descriptors(
	uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle
) {
	return epic_ble_attc_discover_descriptors(
		conn_handle, start_handle, end_handle
	);
}

// Initiate read of a value from the remote peripheral.
int mp_bluetooth_gattc_read(uint16_t conn_handle, uint16_t value_handle)
{
	// TODO: Make use of ATTC_PENDING list
	return epic_ble_attc_read(conn_handle, value_handle);
}

// Write the value to the remote peripheral.
int mp_bluetooth_gattc_write(
	uint16_t conn_handle,
	uint16_t value_handle,
	const uint8_t *value,
	size_t *value_len,
	unsigned int mode
) {
	int err = 0;

	if (mode != MP_BLUETOOTH_WRITE_MODE_WITH_RESPONSE &&
	    mode != MP_BLUETOOTH_WRITE_MODE_NO_RESPONSE) {
		return MP_EINVAL;
	}
	pending_gattc_operation_obj_t *op = m_new_obj_var(
		pending_gattc_operation_obj_t, uint8_t, *value_len
	);
	op->base.type    = &pending_gattc_operation_type;
	op->conn_handle  = conn_handle;
	op->value_handle = value_handle;
	op->mode         = mode;
	op->value_len    = *value_len;
	memcpy(op->value, value, *value_len);
	mp_obj_list_append(ATTC_PENDING, MP_OBJ_FROM_PTR(op));

	size_t len;
	mp_obj_t *items;
	mp_obj_list_get(ATTC_PENDING, &len, &items);

	if (len == 1) {
		if (mode == MP_BLUETOOTH_WRITE_MODE_NO_RESPONSE) {
			err = epic_ble_attc_write_no_rsp(
				conn_handle, value_handle, value, *value_len
			);
		} else if (mode == MP_BLUETOOTH_WRITE_MODE_WITH_RESPONSE) {
			err = epic_ble_attc_write(
				conn_handle, value_handle, value, *value_len
			);
		}
	}
	return err;
}

// Initiate MTU exchange for a specific connection using the preferred MTU.
int mp_bluetooth_gattc_exchange_mtu(uint16_t conn_handle)
{
	raise();
	return 0;
}
#endif