diff --git a/epicardium/ble/ble_api.h b/epicardium/ble/ble_api.h
index 971ed38e15ae48049feef8a8162183a190adef81..3c50745e91285c782ef6650dfa9c4645c360de64 100644
--- a/epicardium/ble/ble_api.h
+++ b/epicardium/ble/ble_api.h
@@ -29,6 +29,7 @@ void bleValueUpdate(attEvt_t *pMsg);
 void bleDiscCback(dmConnId_t connId, uint8_t status);
 void ble_trigger_event(enum ble_event_type event);
 void ble_epic_att_api_init(void);
+void ble_epic_att_api_event(attEvt_t *att_event);
 
 /**************************************************************************************************
   Data Types
diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c
index 1992815e416261cd20797ada305a2c448a1e81a6..373f5f37e37bc4633a090c19b63e4335e500f262 100644
--- a/epicardium/ble/ble_main.c
+++ b/epicardium/ble/ble_main.c
@@ -706,7 +706,6 @@ static void scannerScanReport(dmEvt_t *pMsg)
 /*************************************************************************************************/
 #define ATT_CONNECTION_OPENED         0x81
 #define ATT_CONNECTION_CLOSED         0x82
-void send_att_event(attEvt_t *att_event);
 static void bleProcMsg(bleMsg_t *pMsg)
 {
   hciLeConnCmplEvt_t *connOpen;
@@ -758,7 +757,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
       HidProcMsg(&pMsg->hdr);
       UartProcMsg(pMsg);
       e.hdr.event = ATT_CONNECTION_OPENED; e.hdr.param = connOpen->hdr.param;
-      send_att_event(&e);
+      ble_epic_att_api_event(&e);
       break;
 
     case DM_CONN_CLOSE_IND:
@@ -795,7 +794,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
       advertising_mode = APP_MODE_NONE;
       AppAdvStop();
       e.hdr.param = pMsg->dm.connClose.hdr.param; e.hdr.event = ATT_CONNECTION_CLOSED;
-      send_att_event(&e);
+      ble_epic_att_api_event(&e);
       bleClose(pMsg);
       break;
 
@@ -936,7 +935,7 @@ static void BleHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg)
         LOG_INFO("ble", "Ble got evt %d (%s): %d", pMsg->event, att_events[pMsg->event - ATT_CBACK_START], pMsg->status);
       /* process discovery-related ATT messages */
       AppDiscProcAttMsg((attEvt_t *) pMsg);
-      send_att_event((attEvt_t *)pMsg);
+      ble_epic_att_api_event((attEvt_t *)pMsg);
     }
     else if (pMsg->event >= L2C_COC_CBACK_START && pMsg->event <= L2C_COC_CBACK_CBACK_END)
     {
diff --git a/epicardium/ble/epic_att_api.c b/epicardium/ble/epic_att_api.c
index b492d32fbb8cf9232e6eff0803a96c0f24e077d0..c155e80ee9a02585690438a65007ac549d8527bb 100644
--- a/epicardium/ble/epic_att_api.c
+++ b/epicardium/ble/epic_att_api.c
@@ -6,6 +6,7 @@
 #include "util/bstream.h"
 #include "wsf_msg.h"
 #include "att_api.h"
+#include "wsf_buf.h"
 
 #include "FreeRTOS.h"
 #include "queue.h"
@@ -13,29 +14,53 @@
 #include <stdio.h>
 #include <string.h>
 
-#define ATT_QUEUE_SIZE 10
-static QueueHandle_t att_queue;
-static uint8_t att_queue_buffer[sizeof(attEvt_t) * ATT_QUEUE_SIZE];
-static StaticQueue_t att_queue_data;
+#define ATT_EVENT_QUEUE_SIZE 10
+#define ATT_WRITE_QUEUE_SIZE 10
+
+static QueueHandle_t att_event_queue;
+static uint8_t att_event_queue_buffer
+	[sizeof(struct epic_att_event) * ATT_EVENT_QUEUE_SIZE];
+static StaticQueue_t att_event_queue_data;
+
+static QueueHandle_t att_write_queue;
+static uint8_t att_write_queue_buffer
+	[sizeof(struct epic_att_write) * ATT_WRITE_QUEUE_SIZE];
+static StaticQueue_t att_write_queue_data;
 
 int epic_ble_get_att_event(struct epic_att_event *e)
 {
-	if (xQueueReceive(att_queue, e, 0) != pdTRUE) {
+	if (xQueueReceive(att_event_queue, e, 0) != pdTRUE) {
 		return -ENOENT;
 	}
-	return uxQueueMessagesWaiting(att_queue);
+	return uxQueueMessagesWaiting(att_event_queue);
+}
+
+void ble_epic_att_api_event(attEvt_t *att_event)
+{
+	bool enabled;
+	epic_interrupt_is_enabled(EPIC_INT_BLE, &enabled);
+
+	if (enabled) {
+		if (xQueueSend(att_event_queue, att_event, 0) != pdTRUE) {
+			LOG_WARN("ble", "could not queue att event");
+		}
+		ble_trigger_event(BLE_EVENT_ATT_EVENT);
+	}
 }
 
-// TODO: make static again once ble_main does not use it anymore
-void send_att_event(attEvt_t *att_event)
+int epic_ble_get_att_write(struct epic_att_write *w)
 {
-	if (xQueueSend(att_queue, att_event, 0) != pdTRUE) {
-		LOG_WARN("ble", "could not queue att event");
+	if (xQueueReceive(att_write_queue, w, 0) != pdTRUE) {
+		return -ENOENT;
 	}
-	ble_trigger_event(BLE_EVENT_ATT_EVENT);
+	return uxQueueMessagesWaiting(att_event_queue);
 }
 
-#define ATT_WRITE_CALLBACK 0x80
+int epic_ble_free_att_write(struct epic_att_write *w)
+{
+	WsfBufFree(w->buffer);
+	return 0;
+}
 
 static uint8_t DynAttsWriteCback(
 	dmConnId_t connId,
@@ -46,16 +71,31 @@ static uint8_t DynAttsWriteCback(
 	uint8_t *pValue,
 	attsAttr_t *pAttr
 ) {
-	if (AttsSetAttr(handle, len, pValue) == ATT_SUCCESS) {
-		attEvt_t att_event;
-		att_event.hdr.event = ATT_WRITE_CALLBACK;
-		att_event.hdr.param = connId;
-		att_event.handle    = handle;
-		att_event.valueLen  = len;
-		send_att_event(&att_event);
+	printf("DynAttsWriteCback %d, %d, %d\n", handle, len, offset);
+	struct epic_att_write att_write;
+	att_write.hdr.param = connId;
+	att_write.handle    = handle;
+	att_write.valueLen  = len;
+	att_write.operation = operation;
+	att_write.offset    = offset;
+
+	att_write.buffer = WsfBufAlloc(len);
+
+	if (att_write.buffer) {
+		memcpy(att_write.buffer, pValue, len);
+
+		if (xQueueSend(att_write_queue, &att_write, 0) != pdTRUE) {
+			LOG_WARN("ble", "could not queue att write");
+			epic_ble_free_att_write(&att_write);
+		}
+
+		ble_trigger_event(BLE_EVENT_ATT_WRITE);
+	} else {
+		LOG_WARN("ble", "could allocate att write");
 	}
 
-	return ATT_SUCCESS;
+	// TODO: handle offset != 0
+	return AttsSetAttr(handle, len, pValue);
 }
 
 #define ATTS_DYN_GROUP_COUNT 8
@@ -104,11 +144,17 @@ int epic_ble_atts_dyn_delete_groups(void)
 
 void ble_epic_att_api_init(void)
 {
-	att_queue = xQueueCreateStatic(
-		ATT_QUEUE_SIZE,
-		sizeof(attEvt_t),
-		att_queue_buffer,
-		&att_queue_data
+	att_event_queue = xQueueCreateStatic(
+		ATT_EVENT_QUEUE_SIZE,
+		sizeof(struct epic_att_event),
+		att_event_queue_buffer,
+		&att_event_queue_data
+	);
+	att_write_queue = xQueueCreateStatic(
+		ATT_WRITE_QUEUE_SIZE,
+		sizeof(struct epic_att_write),
+		att_write_queue_buffer,
+		&att_write_queue_data
 	);
 }
 
@@ -124,3 +170,21 @@ int epic_ble_atts_handle_value_ntf(
 	AttsHandleValueNtf(connId, handle, valueLen, pValue);
 	return 0;
 }
+
+int epic_ble_atts_handle_value_ind(
+	uint8_t connId, uint16_t handle, uint16_t valueLen, uint8_t *pValue
+) {
+	if (!DmConnInUse(connId)) {
+		return -EIO;
+	}
+
+	// TODO: There is a race condition here. Ideally AttsHandleValueInd would return an error or
+	// raise a callback
+	AttsHandleValueInd(connId, handle, valueLen, pValue);
+	return 0;
+}
+
+int epic_ble_atts_set_buffer(uint16_t value_handle, size_t len, bool append)
+{
+	return AttsDynResize(value_handle, len);
+}
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index f56b6c00269f60736edd88d833e00e8d28f8d8d1..ec74d98132d7aae9394d6ec9ecadcc55f3c222c9 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -163,16 +163,20 @@ typedef _Bool bool;
 #define API_BLE_HID_SEND_REPORT        0x150
 
 #define API_BLE_ATTS_DYN_CREATE_GROUP         0x160
-//#define API_BLE_ATTS_DYN_DELETE_GROUP         0x160
+//#define API_BLE_ATTS_DYN_DELETE_GROUP         0x161
 #define API_BLE_ATTS_DYN_REGISTER             0x162
 #define API_BLE_ATTS_DYN_ADD_ATTR_DYN         0x163
 #define API_BLE_ATTS_DYN_ADD_ATTR             0x164
 #define API_BLE_ATTS_SET_ATTR                 0x165
 #define API_BLE_ATTS_HANDLE_VALUE_NTF         0x166
-#define API_BLE_ATTS_GET_ATTR                 0x167
-#define API_BLE_ATT_GET_EVENT                 0x168
-#define API_BLE_ATTS_SEND_SERVICE_CHANGED_IND 0x169
-#define API_BLE_ATTS_DYN_DELETE_GROUPS        0x16A
+#define API_BLE_ATTS_HANDLE_VALUE_IND         0x167
+#define API_BLE_ATTS_GET_ATTR                 0x168
+#define API_BLE_ATT_GET_EVENT                 0x169
+#define API_BLE_ATTS_SEND_SERVICE_CHANGED_IND 0x16A
+#define API_BLE_ATTS_DYN_DELETE_GROUPS        0x16B
+#define API_BLE_ATTS_SET_BUFFER               0x1BC
+#define API_BLE_GET_ATT_WRITE                 0x1BD
+#define API_BLE_FREE_ATT_WRITE                0x1BE
 
 /* clang-format on */
 
@@ -2343,6 +2347,7 @@ enum ble_event_type {
 	/** New scan data is available  */
 	BLE_EVENT_SCAN_REPORT                             = 4,
 	BLE_EVENT_ATT_EVENT                               = 5,
+	BLE_EVENT_ATT_WRITE                               = 6,
 };
 
 
@@ -2524,7 +2529,8 @@ API(API_BLE_ATTS_SEND_SERVICE_CHANGED_IND, int epic_atts_dyn_send_service_change
 API(API_BLE_ATTS_SET_ATTR, uint8_t AttsSetAttr(uint16_t handle, uint16_t valueLen, uint8_t *pValue));
 API(API_BLE_ATTS_GET_ATTR, uint8_t AttsGetAttr(uint16_t handle, uint16_t *pLen, uint8_t **pValue));
 API(API_BLE_ATTS_HANDLE_VALUE_NTF, int epic_ble_atts_handle_value_ntf(uint8_t connId, uint16_t handle, uint16_t valueLen, uint8_t *pValue));
-
+API(API_BLE_ATTS_HANDLE_VALUE_IND, int epic_ble_atts_handle_value_ind(uint8_t connId, uint16_t handle, uint16_t valueLen, uint8_t *pValue));
+API(API_BLE_ATTS_SET_BUFFER, int epic_ble_atts_set_buffer(uint16_t value_handle, size_t len, bool append));
 
 struct epic_wsf_header
 {
@@ -2543,9 +2549,21 @@ struct epic_att_event
   uint16_t              mtu;          /*!< \brief Negotiated MTU value */
 };
 
-
 API(API_BLE_ATT_GET_EVENT, int epic_ble_get_att_event(struct epic_att_event *e));
 
+struct epic_att_write
+{
+  struct epic_wsf_header hdr;          /*!< \brief Header structure */
+  uint16_t              valueLen;     /*!< \brief Value length */
+  uint16_t              handle;       /*!< \brief Attribute handle */
+  uint8_t operation;
+  uint16_t offset;
+  void *buffer;
+};
+
+API(API_BLE_GET_ATT_WRITE, int epic_ble_get_att_write(struct epic_att_write *w));
+API(API_BLE_FREE_ATT_WRITE, int epic_ble_free_att_write(struct epic_att_write *w));
+
 
 #endif /* _EPICARDIUM_H */
 
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-host/include/att_api.h b/lib/sdk/Libraries/BTLE/stack/ble-host/include/att_api.h
index 0138ea019d14c696078fc62a213537e9be38c893..f50e3a4863d1b169a34e92334934d870b1133794 100644
--- a/lib/sdk/Libraries/BTLE/stack/ble-host/include/att_api.h
+++ b/lib/sdk/Libraries/BTLE/stack/ble-host/include/att_api.h
@@ -981,6 +981,8 @@ void AttsDynAddAttr(void *pSvcHandle, const uint8_t *pUuid, const uint8_t *pValu
 /*************************************************************************************************/
 void AttsDynAddAttrConst(void *pSvcHandle, const uint8_t *pUuid, const uint8_t *pValue,
                          const uint16_t len, uint8_t settings, uint8_t permissions);
+
+uint8_t AttsDynResize(uint16_t handle, uint16_t maxLen);
 /**@}*/
 
 /** \name ATT Server Testing
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-host/sources/stack/att/atts_dyn.c b/lib/sdk/Libraries/BTLE/stack/ble-host/sources/stack/att/atts_dyn.c
index 0348e2d5de5d43422771fea5a8641d0f8276f27b..ad4ad54e30a0dad6c24de9119654ba0adc15b994 100644
--- a/lib/sdk/Libraries/BTLE/stack/ble-host/sources/stack/att/atts_dyn.c
+++ b/lib/sdk/Libraries/BTLE/stack/ble-host/sources/stack/att/atts_dyn.c
@@ -421,3 +421,36 @@ void AttsDynAddAttrDyn(void *pSvcHandle, const uint8_t *pUuid, uint8_t uuidLen,
     AttsAddGroup(&pGroup->group);
   }
 }
+
+uint8_t AttsDynResize(uint16_t handle, uint16_t maxLen)
+{
+  attsAttr_t  *pAttr;
+  attsGroup_t *pGroup;
+  void        *pValue;
+  uint8_t     err = ATT_SUCCESS;
+
+  /* find attribute */
+  if ((pAttr = attsFindByHandle(handle, &pGroup)) != NULL)
+  {
+    /* Allocate a buffer for the value of the attribute */
+    pValue = attsDynAlloc(maxLen);
+    WSF_ASSERT(pValue);
+
+    if (pValue == NULL)
+    {
+      return ATT_ERR_MEMORY;
+    }
+
+    pAttr->maxLen = maxLen;
+    pAttr->pValue = pValue;
+    memset(pValue, 0, maxLen);
+  }
+  /* else attribute not found */
+  else
+  {
+    err = ATT_ERR_NOT_FOUND;
+  }
+
+  return err;
+
+}
diff --git a/pycardium/modules/modbluetooth_card10.c b/pycardium/modules/modbluetooth_card10.c
index c2dc063e046914422b8c2bd2a4e163b06372d238..5395a68150d9bdf397727e493ea2afc9e5765055 100644
--- a/pycardium/modules/modbluetooth_card10.c
+++ b/pycardium/modules/modbluetooth_card10.c
@@ -86,7 +86,6 @@ const uint8_t attCliChCfgUuid[] = { UINT16_TO_BYTES(
 	0x79 /*!< \brief Responsed delayed pending higher layer */
 
 // card10 att interface specific events
-#define ATT_WRITE_CALLBACK 0x80
 #define ATT_CONNECTION_OPENED 0x81
 #define ATT_CONNECTION_CLOSED 0x82
 
@@ -105,27 +104,31 @@ const char *const not_implemented_message =
 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_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)
 
 typedef struct {
 	enum notification_status notification_status;
-} gatts_db_entry_t;
+} gatts_status_entry_t;
 
-static void gatts_db_create_entry(mp_gatts_db_t db, uint16_t handle)
+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_db_entry_t *entry    = m_new(gatts_db_entry_t, 1);
-	entry->notification_status = NOTIFICATION_STATUS_UNKNOWN;
-	elem->value                = MP_OBJ_FROM_PTR(entry);
+	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_db_entry_t *gatts_db_lookup(mp_gatts_db_t db, uint16_t handle)
+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);
@@ -143,82 +146,102 @@ static void raise(void)
 static void clear_events(void)
 {
 	struct epic_att_event att_event;
+	struct epic_att_write att_write;
 	epic_ble_get_event();
 	while (epic_ble_get_att_event(&att_event) >= 0)
 		;
+	while (epic_ble_get_att_write(&att_write) >= 0)
+		epic_ble_free_att_write(&att_write);
+}
+
+static void handle_att_event(struct epic_att_event *att_event)
+{
+	printf("MP got att event %d,%d,%d\n",
+	       att_event->hdr.event,
+	       att_event->hdr.status,
+	       att_event->handle);
+
+	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;
+		}
+	} else if (att_event->hdr.event == ATT_CONNECTION_OPENED) {
+		uint8_t event        = MP_BLUETOOTH_IRQ_CENTRAL_CONNECT;
+		uint16_t conn_handle = att_event->hdr.param;
+		uint8_t addr_type    = 0;  // TODO
+		uint8_t addr[8]      = {}; // TODO
+		mp_bluetooth_gap_on_connected_disconnected(
+			event, conn_handle, addr_type, addr
+		);
+	} else if (att_event->hdr.event == ATT_CONNECTION_CLOSED) {
+		uint8_t event        = MP_BLUETOOTH_IRQ_CENTRAL_DISCONNECT;
+		uint16_t conn_handle = att_event->hdr.param;
+		uint8_t addr_type    = 0;  // TODO
+		uint8_t addr[8]      = {}; // TODO
+		mp_bluetooth_gap_on_connected_disconnected(
+			event, conn_handle, addr_type, addr
+		);
+	}
+}
+
+static void handle_att_write(struct epic_att_write *att_write)
+{
+	printf("MP got att write %d,%d\n",
+	       att_write->handle,
+	       att_write->valueLen);
+
+	mp_bluetooth_gatts_db_entry_t *entry =
+		mp_bluetooth_gatts_db_lookup(GATTS_DB, att_write->handle);
+	if (!entry) {
+		printf("ble: epic_att_write handle not found\n");
+		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_att_event att_event;
 	enum ble_event_type event = epic_ble_get_event();
 
 	if (event == BLE_EVENT_ATT_EVENT) {
+		struct epic_att_event att_event;
 		int ret;
 		do {
 			ret = epic_ble_get_att_event(&att_event);
+			if (ret >= 0) {
+				handle_att_event(&att_event);
+			}
+		} while (ret >= 0);
+	}
 
+	if (event == BLE_EVENT_ATT_WRITE) {
+		struct epic_att_write att_write;
+		int ret;
+		do {
+			ret = epic_ble_get_att_write(&att_write);
 			if (ret >= 0) {
-				printf("MP got att event %d,%d,%d\n",
-				       att_event.hdr.event,
-				       att_event.hdr.status,
-				       att_event.handle);
-				if (att_event.hdr.event ==
-				    ATTS_HANDLE_VALUE_CNF) {
-					gatts_db_entry_t *e = gatts_db_lookup(
-						GATTS_DB, 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;
-					}
-				} else if (
-					att_event.hdr.event ==
-					ATT_WRITE_CALLBACK) {
-					mp_bluetooth_gatts_on_write(
-						att_event.hdr.param,
-						att_event.handle
-					);
-				} else if (
-					att_event.hdr.event ==
-					ATT_CONNECTION_OPENED) {
-					uint8_t event =
-						MP_BLUETOOTH_IRQ_CENTRAL_CONNECT;
-					uint16_t conn_handle =
-						att_event.hdr.param;
-					uint8_t addr_type = 0;  // TODO
-					uint8_t addr[8]   = {}; // TODO
-					mp_bluetooth_gap_on_connected_disconnected(
-						event,
-						conn_handle,
-						addr_type,
-						addr
-					);
-				} else if (
-					att_event.hdr.event ==
-					ATT_CONNECTION_CLOSED) {
-					uint8_t event =
-						MP_BLUETOOTH_IRQ_CENTRAL_DISCONNECT;
-					uint16_t conn_handle =
-						att_event.hdr.param;
-					uint8_t addr_type = 0;  // TODO
-					uint8_t addr[8]   = {}; // TODO
-					mp_bluetooth_gap_on_connected_disconnected(
-						event,
-						conn_handle,
-						addr_type,
-						addr
-					);
-				}
+				handle_att_write(&att_write);
+				epic_ble_free_att_write(&att_write);
 			}
 		} while (ret >= 0);
 	}
+
 	return mp_const_none;
 }
 static MP_DEFINE_CONST_FUN_OBJ_1(ble_event_obj, mp_ble_poll_events);
@@ -230,7 +253,10 @@ 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);
+
 	mp_interrupt_set_callback(
 		MP_ROM_INT(EPIC_INT_BLE), (mp_obj_t *)&ble_event_obj
 	);
@@ -393,20 +419,29 @@ int mp_bluetooth_gatts_register_service(
 			uuid_len,
 			NULL,
 			0,
-			MP_BLUETOOTH_DEFAULT_ATTR_LEN + 10,
+			MP_BLUETOOTH_DEFAULT_ATTR_LEN,
 			settings,
 			permissions
 		);
 		handles[handle_index] = currentHandle;
-		//mp_bluetooth_gatts_db_create_entry(GATTS_DB, currentHandle, MP_BLUETOOTH_DEFAULT_ATTR_LEN);
-		gatts_db_create_entry(GATTS_DB, currentHandle);
+		mp_bluetooth_gatts_db_create_entry(
+			GATTS_DB, currentHandle, MP_BLUETOOTH_DEFAULT_ATTR_LEN
+		);
+		gatts_status_create_entry(GATTS_STATUS, currentHandle);
 
 		if ((flags & MP_BLUETOOTH_CHARACTERISTIC_FLAG_NOTIFY) ||
 		    flags & (MP_BLUETOOTH_CHARACTERISTIC_FLAG_INDICATE)) {
 			/* CCCD */
 			currentHandle++;
-			//uint8_t initCcc[] = {UINT16_TO_BYTES(0x0000)};
-			uint8_t initCcc[] = { UINT16_TO_BYTES(0x0001) };
+			uint8_t initCcc[2] = {};
+			// TODO: activate notification/indications by default
+			// Not according to spec
+			if (flags & MP_BLUETOOTH_CHARACTERISTIC_FLAG_NOTIFY) {
+				initCcc[0] |= 0x01;
+			}
+			if (flags & MP_BLUETOOTH_CHARACTERISTIC_FLAG_INDICATE) {
+				initCcc[0] |= 0x02;
+			}
 			// TODO: Handle CCC data
 			// Settings is 0 on purpose. If set to ATTS_SET_CCC the stack starts to try and
 			// manage the CCC data itself.
@@ -423,7 +458,7 @@ int mp_bluetooth_gatts_register_service(
 			);
 
 			//mp_bluetooth_gatts_db_create_entry(GATTS_DB, handles[handle_index] + 1, MP_BLUETOOTH_CCCB_LEN);
-			//gatts_db_create_entry(GATTS_DB, handles[handle_index] + 1);
+			//gatts_status_create_entry(GATTS_STATUS, handles[handle_index] + 1);
 			//int ret = mp_bluetooth_gatts_db_write(GATTS_DB, handles[handle_index] + 1, cccb_buf, sizeof(cccb_buf));
 		}
 
@@ -443,17 +478,23 @@ int mp_bluetooth_gatts_register_service_end()
 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
+	);
+#if 0
 	uint16_t pLen;
 	uint8_t ret = AttsGetAttr(value_handle, &pLen, value);
 
 	*value_len = pLen;
 	return ret;
+#endif
 }
 // 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
 ) {
-	//return mp_bluetooth_gatts_db_write(MP_STATE_PORT(bluetooth_btstack_root_pointers)->gatts_db, value_handle, value, value_len);
+	// TODO: which return value to choose?
+	mp_bluetooth_gatts_db_write(GATTS_DB, value_handle, value, value_len);
 	uint8_t ret = AttsSetAttr(value_handle, value_len, (uint8_t *)value);
 	return ret;
 }
@@ -463,7 +504,8 @@ 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_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
 	);
@@ -475,10 +517,12 @@ int mp_bluetooth_gatts_notify_send(
 	const uint8_t *value,
 	size_t value_len
 ) {
-	gatts_db_entry_t *e    = gatts_db_lookup(GATTS_DB, value_handle);
+	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
+
+	int ret = epic_ble_atts_handle_value_ntf(
+		conn_handle, value_handle, value_len, (uint8_t *)value
 	);
 
 	if (ret < 0) {
@@ -498,7 +542,34 @@ int mp_bluetooth_gatts_notify_send(
 // Indicate the central.
 int mp_bluetooth_gatts_indicate(uint16_t conn_handle, uint16_t value_handle)
 {
-	raise();
+	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;
 }
 
@@ -506,8 +577,9 @@ int mp_bluetooth_gatts_indicate(uint16_t conn_handle, uint16_t value_handle)
 // 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)
 {
-	raise();
-	return 0;
+	// 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.