diff --git a/epicardium/ble/ble.c b/epicardium/ble/ble.c
index bfb33db010e1b66505a8fdabbfc78f1df269e9fe..0b066379664dd565edf62cf4b16b2b93796a744d 100644
--- a/epicardium/ble/ble.c
+++ b/epicardium/ble/ble.c
@@ -30,6 +30,9 @@ struct log_packet_header {
 	uint32_t timestamp_us_h;
 	uint32_t timestamp_us_l;
 };
+
+static uint8_t bdAddr[6] = { 0xCA, 0x4D, 0x10, 0x00, 0x00, 0x00 };
+
 static const uint8_t log_header[] = {
 	'b', 't', 's', 'n', 'o', 'o', 'p', 0, 0, 0, 0, 1, 0, 0, 0x03, 0xea
 };
@@ -178,7 +181,6 @@ static void WsfInit(void)
 /* TODO: We need a source of MACs */
 static void setAddress(void)
 {
-	uint8_t bdAddr[6] = { 0xCA, 0x4D, 0x10, 0x00, 0x00, 0x00 };
 	char buf[32];
 
 	int result = epic_config_get_string("ble_mac", buf, sizeof(buf));
@@ -222,6 +224,11 @@ static void setAddress(void)
 	HciVsSetBdAddr(bdAddr);
 }
 /*************************************************************************************************/
+void epic_ble_get_address(uint8_t *addr)
+{
+	memcpy(addr, bdAddr, sizeof(bdAddr));
+}
+/*************************************************************************************************/
 static void vTimerCallback(xTimerHandle pxTimer)
 {
 	//printf("wake\n");
@@ -428,7 +435,6 @@ void vBleTask(void *pvParameters)
 	setAddress();
 
 	BleStart();
-	AttsDynInit();
 
 	bleuart_init();
 	bleFileTransfer_init();
diff --git a/epicardium/ble/ble_api.h b/epicardium/ble/ble_api.h
index 93261a58d1b8ac12dbd0091d70ea007d21af933f..38c60f731a27ada94022ad2dcbd11f467824f0d6 100644
--- a/epicardium/ble/ble_api.h
+++ b/epicardium/ble/ble_api.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include "epicardium.h"
+
 #include <stdint.h>
 #include "wsf_types.h"
 
@@ -25,3 +27,24 @@ void BleStart(void);
 /* ATT client module interface. Used by main BLE module */
 void bleValueUpdate(attEvt_t *pMsg);
 void bleDiscCback(dmConnId_t connId, uint8_t status);
+
+void ble_epic_att_api_init(void);
+void ble_epic_att_api_event(attEvt_t *att_event);
+void ble_epic_att_api_free_att_write_data(struct epic_att_write *w);
+
+void ble_epic_ble_api_trigger_event(enum epic_ble_event_type type, void *data);
+void ble_epic_ble_api_init(void);
+void ble_epic_dm_api_event(dmEvt_t *dm_event);
+
+/**************************************************************************************************
+  Data Types
+**************************************************************************************************/
+
+/*! Application message type */
+typedef union
+{
+  wsfMsgHdr_t     hdr;
+  dmEvt_t         dm;
+  attsCccEvt_t    ccc;
+  attEvt_t        att;
+} bleMsg_t;
diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c
index 05e1da53cbef9cd782f6821eab686b429ab418df..7e96fc7cc5049b576c8794267634ff465a5f3c1d 100644
--- a/epicardium/ble/ble_main.c
+++ b/epicardium/ble/ble_main.c
@@ -17,36 +17,28 @@
 #include <string.h>
 #include "wsf_types.h"
 #include "util/bstream.h"
-#include "fs/fs_util.h"
 #include "wsf_msg.h"
 #include "wsf_trace.h"
-#include "hci_api.h"
 #include "l2c_api.h"
 #include "dm_api.h"
 #include "att_api.h"
+#include "gatt/gatt_api.h"
 #include "smp_api.h"
 #include "app_api.h"
 #include "app_db.h"
-#include "app_ui.h"
-#include "app_hw.h"
 #include "svc_ch.h"
 #include "svc_core.h"
-#include "svc_hrs.h"
 #include "svc_dis.h"
 #include "svc_batt.h"
 #include "svc_hid.h"
-#include "svc_rscs.h"
-#include "bas/bas_api.h"
-#include "hrps/hrps_api.h"
-#include "rscp/rscp_api.h"
 #include "profiles/gap_api.h"
 #include "cccd.h"
 #include "ess.h"
 #include "hid.h"
+#include "uart.h"
 
 #include "ble_api.h"
 #include "epicardium.h"
-#include "api/interrupt-sender.h"
 #include "modules/log.h"
 #include "modules/config.h"
 
@@ -55,24 +47,10 @@
 static bool active;
 static uint8_t advertising_mode = APP_MODE_NONE;
 static uint8_t advertising_mode_target = APP_MODE_NONE;
-static enum ble_event_type ble_event;
 static struct epic_scan_report scan_reports[SCAN_REPORTS_NUM];
 static int scan_reports_head;
 static int scan_reports_tail;
 
-/**************************************************************************************************
-  Data Types
-**************************************************************************************************/
-
-/*! Application message type */
-typedef union
-{
-  wsfMsgHdr_t     hdr;
-  dmEvt_t         dm;
-  attsCccEvt_t    ccc;
-  attEvt_t        att;
-} bleMsg_t;
-
 /**************************************************************************************************
   Configurable Parameters
 **************************************************************************************************/
@@ -243,6 +221,7 @@ static const attsCccSet_t bleCccSet[BLE_NUM_CCC_IDX] =
   {HID_INPUT_REPORT_2_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_MOUSE_CCC_HDL */
   {HID_INPUT_REPORT_3_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_CONSUMER_CCC_HDL */
   {ESS_IAQ_CH_CCC_HDL,    ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_IAQ_CCC_IDX */
+  {UART_TX_CH_CCC_HDL,    ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_IAQ_CCC_IDX */
 };
 
 /**************************************************************************************************
@@ -435,8 +414,6 @@ static void bleCccCback(attsCccEvt_t *pEvt)
   }
 }
 
-
-
 /*************************************************************************************************/
 /*!
  *  \brief  Process CCC state change.
@@ -642,27 +619,6 @@ void epic_ble_compare_response(bool confirmed)
 		/* error condition */
 	}
 }
-static void trigger_event(enum ble_event_type event)
-{
-	bool enabled;
-	epic_interrupt_is_enabled(EPIC_INT_BLE, &enabled);
-
-	/* Print a warning if the app is missing events. Missing scan results
-	 * is considered OK though, as they are queued and periodic. */
-	if(ble_event && enabled && ble_event != BLE_EVENT_SCAN_REPORT) {
-		LOG_WARN("ble", "Application missed event %u", ble_event);
-	}
-
-	ble_event = event;
-	api_interrupt_trigger(EPIC_INT_BLE);
-}
-
-enum ble_event_type epic_ble_get_event(void)
-{
-	enum ble_event_type event = ble_event;
-	ble_event = 0;
-	return event;
-}
 
 static void bleHandleNumericComparison(dmSecCnfIndEvt_t *pCnfInd)
 {
@@ -673,7 +629,7 @@ static void bleHandleNumericComparison(dmSecCnfIndEvt_t *pCnfInd)
 	pair_connId = (dmConnId_t)pCnfInd->hdr.param;
 	pair_confirm_value = DmSecGetCompareValue(pCnfInd->confirm);
 	LOG_INFO("ble", "Confirm Value: %ld", pair_confirm_value);
-	trigger_event(BLE_EVENT_HANDLE_NUMERIC_COMPARISON);
+	ble_epic_ble_api_trigger_event(BLE_EVENT_HANDLE_NUMERIC_COMPARISON, NULL);
 }
 
 int epic_ble_get_scan_report(struct epic_scan_report *rpt)
@@ -694,7 +650,7 @@ static void scannerScanReport(dmEvt_t *pMsg)
 
 	int next_head = (scan_reports_head + 1) % SCAN_REPORTS_NUM;
 	if(next_head == scan_reports_tail) {
-		trigger_event(BLE_EVENT_SCAN_REPORT);
+		ble_epic_ble_api_trigger_event(BLE_EVENT_SCAN_REPORT, NULL);
 		return;
 	}
 	scan_reports_head = next_head;
@@ -710,7 +666,7 @@ static void scannerScanReport(dmEvt_t *pMsg)
 
 	scan_report->directAddrType = pMsg->scanReport.directAddrType;
 	memcpy(scan_report->directAddr, pMsg->scanReport.directAddr, BDA_ADDR_LEN);
-	trigger_event(BLE_EVENT_SCAN_REPORT);
+	ble_epic_ble_api_trigger_event(BLE_EVENT_SCAN_REPORT, NULL);
 
 	if((scan_reports_head + 1) % SCAN_REPORTS_NUM == scan_reports_tail) {
 		LOG_WARN("ble", "Application missing scan results");
@@ -739,6 +695,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
 
     case ATTS_HANDLE_VALUE_CNF:
       HidProcMsg(&pMsg->hdr);
+      UartProcMsg(pMsg);
       break;
 
     case ATTS_CCC_STATE_IND:
@@ -773,6 +730,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
                connOpen->peerAddr[1], connOpen->peerAddr[0]);
       bleESS_ccc_update();
       HidProcMsg(&pMsg->hdr);
+      UartProcMsg(pMsg);
       break;
 
     case DM_CONN_CLOSE_IND:
@@ -808,7 +766,6 @@ static void bleProcMsg(bleMsg_t *pMsg)
        */
       advertising_mode = APP_MODE_NONE;
       AppAdvStop();
-
       bleClose(pMsg);
       break;
 
@@ -821,7 +778,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
       AppDbNvmStoreBond(last_pairing);
 
       pair_connId = DM_CONN_ID_NONE;
-      trigger_event(BLE_EVENT_PAIRING_COMPLETE);
+      ble_epic_ble_api_trigger_event(BLE_EVENT_PAIRING_COMPLETE, NULL);
       /* After a successful pairing, bonding is disabled again.
        * We don't want that for now. */
       AppSetBondable(TRUE);
@@ -846,7 +803,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
       DmSecGenerateEccKeyReq();
 
       pair_connId = DM_CONN_ID_NONE;
-      trigger_event(BLE_EVENT_PAIRING_FAILED);
+      ble_epic_ble_api_trigger_event(BLE_EVENT_PAIRING_FAILED, NULL);
       break;
 
     case DM_SEC_ENCRYPT_IND:
@@ -942,12 +899,18 @@ static void BleHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg)
 
       /* process discovery-related messages */
       AppDiscProcDmMsg((dmEvt_t *) pMsg);
+
+      ble_epic_dm_api_event((dmEvt_t *)pMsg);
     }
     else if (pMsg->event >= ATT_CBACK_START && pMsg->event <= ATT_CBACK_END)
     {
-      LOG_INFO("ble", "Ble got evt %d: %s", pMsg->event, att_events[pMsg->event - ATT_CBACK_START]);
+      /* Don't spam the console with successful notfication/indications */
+      if (!(pMsg->event == ATTS_HANDLE_VALUE_CNF && pMsg->status == ATT_SUCCESS)) {
+        LOG_INFO("ble", "Ble got evt %d (%s): %d %d", pMsg->event, att_events[pMsg->event - ATT_CBACK_START], ((bleMsg_t *)pMsg)->att.handle, pMsg->status);
+      }
       /* process discovery-related ATT messages */
       AppDiscProcAttMsg((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)
     {
@@ -972,7 +935,6 @@ static void BleHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg)
 /*************************************************************************************************/
 void BleStart(void)
 {
-
   BleHandlerInit();
 
   /* Register for stack callbacks */
@@ -992,7 +954,14 @@ void BleStart(void)
   if(config_get_boolean_with_default("ble_hid_enable", false)) {
     hid_init();
   }
+
+  ble_epic_ble_api_init();
+
+  /* Set Service Changed CCCD index. */
+  GattSetSvcChangedIdx(BLE_GATT_SC_CCC_IDX);
+
   /* Reset the device */
   DmDevReset();
 }
+
 /* clang-format on */
diff --git a/epicardium/ble/cccd.h b/epicardium/ble/cccd.h
index 5c3edf3cb64361af9e5cb841731a80318f102d90..0e49f311871c7c3ee6a872ce42fecf7f48853097 100644
--- a/epicardium/ble/cccd.h
+++ b/epicardium/ble/cccd.h
@@ -13,6 +13,7 @@ enum
   HIDAPP_IN_MOUSE_CCC_HDL,              /*! HID Input Report characteristic for mouse inputs */
   HIDAPP_IN_CONSUMER_CCC_HDL,             /*! HID Input Report characteristic for consumer control inputs */
   BLE_ESS_IAQ_CCC_IDX,                    /*! Environmental sensing service, IAQ characteristic */
+  UART_TX_CH_CCC_IDX,
   BLE_NUM_CCC_IDX
 };
 
diff --git a/epicardium/ble/epic_att_api.c b/epicardium/ble/epic_att_api.c
new file mode 100644
index 0000000000000000000000000000000000000000..74faa187d50ec813083fdc84d17ef1ac75f220bf
--- /dev/null
+++ b/epicardium/ble/epic_att_api.c
@@ -0,0 +1,280 @@
+#include "ble_api.h"
+#include "epicardium.h"
+#include "modules/log.h"
+
+#include "wsf_types.h"
+#include "util/bstream.h"
+#include "wsf_msg.h"
+#include "att_api.h"
+#include "wsf_buf.h"
+#include "gatt/gatt_api.h"
+
+#include "FreeRTOS.h"
+#include "queue.h"
+
+#include <stdio.h>
+#include <string.h>
+
+/* We allow up to 10 dynamically defined services */
+#define ATTS_DYN_GROUP_COUNT 10
+#define ATTS_DYN_START_HANDLE 0x200
+
+static void *dyn_groups[ATTS_DYN_GROUP_COUNT] = {};
+static int next_dyn_group                     = 0;
+static int next_handle                        = ATTS_DYN_START_HANDLE;
+
+void ble_epic_att_api_event(attEvt_t *att_event)
+{
+	if (att_event->handle >= ATTS_DYN_START_HANDLE &&
+	    att_event->handle < next_handle) {
+		attEvt_t *e = WsfBufAlloc(sizeof(*e));
+
+		if (e) {
+			memcpy(e, att_event, sizeof(*e));
+			ble_epic_ble_api_trigger_event(BLE_EVENT_ATT_EVENT, e);
+		} else {
+			LOG_WARN("ble", "could not allocate att event");
+		}
+	}
+}
+
+void ble_epic_att_api_free_att_write_data(struct epic_att_write *w)
+{
+	WsfBufFree(w->buffer);
+	WsfBufFree(w);
+}
+
+static uint8_t DynAttsWriteCback(
+	dmConnId_t connId,
+	uint16_t handle,
+	uint8_t operation,
+	uint16_t offset,
+	uint16_t len,
+	uint8_t *pValue,
+	attsAttr_t *pAttr
+) {
+	struct epic_att_write *att_write = WsfBufAlloc(sizeof(*att_write));
+
+	if (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);
+			ble_epic_ble_api_trigger_event(
+				BLE_EVENT_ATT_WRITE, att_write
+			);
+		} else {
+			LOG_WARN("ble", "could allocate att write data");
+			WsfBufFree(att_write);
+		}
+	} else {
+		LOG_WARN("ble", "could allocate att write");
+	}
+
+	// TODO: handle offset != 0
+	return AttsSetAttr(handle, len, pValue);
+}
+
+int epic_atts_dyn_create_service(
+	const uint8_t *uuid,
+	uint8_t uuid_len,
+	uint16_t group_size,
+	void **pSvcHandle
+) {
+	uint16_t start_handle = next_handle;
+	uint16_t end_handle   = start_handle + group_size - 1;
+
+	*pSvcHandle = AttsDynCreateGroup(start_handle, end_handle);
+	dyn_groups[next_dyn_group++] = *pSvcHandle;
+
+	AttsDynRegister(*pSvcHandle, NULL, DynAttsWriteCback);
+
+	AttsDynAddAttr(
+		*pSvcHandle,
+		attPrimSvcUuid,
+		uuid,
+		uuid_len,
+		uuid_len,
+		0,
+		ATTS_PERMIT_READ
+	);
+	next_handle++;
+
+	// TODO: validate parameters and pointer and current service count
+	return 0;
+}
+
+int epic_atts_dyn_add_characteristic(
+	void *pSvcHandle,
+	const uint8_t *uuid,
+	uint8_t uuid_len,
+	uint8_t flags,
+	uint16_t maxLen,
+	uint16_t *value_handle
+) {
+	uint8_t settings = ATTS_SET_VARIABLE_LEN;
+	if (flags & ATT_PROP_WRITE) {
+		settings |= ATTS_SET_WRITE_CBACK;
+	}
+
+	uint8_t permissions = 0;
+	if (flags & ATT_PROP_READ) {
+		permissions |= ATTS_PERMIT_READ;
+	}
+	if (flags & ATT_PROP_WRITE) {
+		permissions |= ATTS_PERMIT_WRITE;
+	}
+
+	uint8_t characteristic[1 + 2 + 16] = {
+		flags, UINT16_TO_BYTES(next_handle + 1)
+	};
+	memcpy(characteristic + 3, uuid, uuid_len);
+	uint8_t characteristic_len = 1 + 2 + uuid_len;
+
+	/* Characteristic */
+	AttsDynAddAttr(
+		pSvcHandle,
+		attChUuid,
+		characteristic,
+		characteristic_len,
+		characteristic_len,
+		0,
+		ATTS_PERMIT_READ
+	);
+	next_handle++;
+
+	/* Value */
+	*value_handle = next_handle;
+	if ((flags & ATT_PROP_READ) == 0) {
+		AttsDynAddAttrDynConst(
+			pSvcHandle,
+			uuid,
+			uuid_len,
+			NULL,
+			0,
+			maxLen,
+			settings,
+			permissions
+		);
+	} else {
+		AttsDynAddAttrDyn(
+			pSvcHandle,
+			uuid,
+			uuid_len,
+			NULL,
+			0,
+			maxLen,
+			settings,
+			permissions
+		);
+	}
+	next_handle++;
+	return 0;
+}
+
+int epic_ble_atts_dyn_add_descriptor(
+	void *pSvcHandle,
+	const uint8_t *uuid,
+	uint8_t uuid_len,
+	uint8_t flags,
+	const uint8_t *value,
+	uint16_t value_len,
+	uint16_t maxLen,
+	uint16_t *descriptor_handle
+) {
+	uint8_t settings = 0;
+	if (flags & ATT_PROP_WRITE) {
+		settings |= ATTS_SET_WRITE_CBACK;
+	}
+
+	uint8_t permissions = 0;
+	if (flags & ATT_PROP_READ) {
+		permissions |= ATTS_PERMIT_READ;
+	}
+	if (flags & ATT_PROP_WRITE) {
+		permissions |= ATTS_PERMIT_WRITE;
+	}
+
+	*descriptor_handle = next_handle;
+	AttsDynAddAttrDyn(
+		pSvcHandle,
+		uuid,
+		uuid_len,
+		value,
+		value_len,
+		maxLen,
+		settings,
+		permissions
+	);
+	next_handle++;
+
+	return 0;
+}
+
+int epic_atts_dyn_send_service_changed_ind(void)
+{
+	/* Indicate to the server that our GATT DB changed.
+	 * TODO: Handling of CCCDs in pairings might still be broken:
+	 * See https://git.card10.badge.events.ccc.de/card10/firmware/-/issues/197 */
+	GattSendServiceChangedInd(
+		DM_CONN_ID_NONE, ATT_HANDLE_START, ATT_HANDLE_MAX
+	);
+	return 0;
+}
+
+int epic_ble_atts_dyn_delete_groups(void)
+{
+	for (int i = 0; i < ATTS_DYN_GROUP_COUNT; i++) {
+		if (dyn_groups[i]) {
+			AttsDynDeleteGroup(dyn_groups[i]);
+			dyn_groups[i] = NULL;
+		}
+	}
+	next_dyn_group = 0;
+	next_handle    = ATTS_DYN_START_HANDLE;
+	return 0;
+}
+
+void ble_epic_att_api_init(void)
+{
+}
+
+int epic_ble_atts_handle_value_ntf(
+	uint8_t connId, uint16_t handle, uint16_t valueLen, uint8_t *pValue
+) {
+	if (!DmConnInUse(connId)) {
+		return -EIO;
+	}
+
+	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;
+	}
+
+	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);
+}
+
+int epic_ble_atts_set_attr(
+	uint16_t handle, const uint8_t *value, uint16_t value_len
+) {
+	uint8_t ret = AttsSetAttr(handle, value_len, (uint8_t *)value);
+	return ret;
+}
diff --git a/epicardium/ble/epic_ble_api.c b/epicardium/ble/epic_ble_api.c
new file mode 100644
index 0000000000000000000000000000000000000000..726d26a949ece4dbefbebc4d7e9f0b8b8c9e818e
--- /dev/null
+++ b/epicardium/ble/epic_ble_api.c
@@ -0,0 +1,115 @@
+#include "ble_api.h"
+
+#include "epicardium.h"
+#include "modules/log.h"
+#include "api/interrupt-sender.h"
+
+#include "wsf_buf.h"
+#include "app_api.h"
+#include "svc_core.h"
+
+#include "FreeRTOS.h"
+#include "queue.h"
+
+#include <stdint.h>
+#include <string.h>
+
+#define BLE_EVENT_QUEUE_SIZE 10
+
+static QueueHandle_t ble_event_queue;
+static uint8_t ble_event_queue_buffer
+	[sizeof(struct epic_ble_event) * BLE_EVENT_QUEUE_SIZE];
+static StaticQueue_t ble_event_queue_data;
+
+int epic_ble_free_event(struct epic_ble_event *e)
+{
+	if (e->data) {
+		if (e->type == BLE_EVENT_ATT_WRITE) {
+			ble_epic_att_api_free_att_write_data(e->att_write);
+		} else {
+			// Generic free
+			WsfBufFree(e->data);
+		}
+	}
+	return 0;
+}
+
+void ble_epic_ble_api_trigger_event(enum epic_ble_event_type type, void *data)
+{
+	bool enabled;
+	epic_interrupt_is_enabled(EPIC_INT_BLE, &enabled);
+
+	struct epic_ble_event e = { .type = type, .data = data };
+
+	if (enabled) {
+		if (xQueueSend(ble_event_queue, &e, 0) != pdTRUE) {
+			/* Print a warning if the app is missing events. Missing scan results
+			* is considered OK though, as they are queued and periodic. */
+			if (type != BLE_EVENT_SCAN_REPORT) {
+				LOG_WARN(
+					"ble",
+					"Application missed event %u",
+					type
+				);
+			}
+			epic_ble_free_event(&e);
+		}
+
+		api_interrupt_trigger(EPIC_INT_BLE);
+	} else {
+		epic_ble_free_event(&e);
+	}
+}
+
+int epic_ble_get_event(struct epic_ble_event *e)
+{
+	if (xQueueReceive(ble_event_queue, e, 0) != pdTRUE) {
+		return -ENOENT;
+	}
+	return uxQueueMessagesWaiting(ble_event_queue);
+}
+
+void ble_epic_ble_api_init(void)
+{
+	ble_event_queue = xQueueCreateStatic(
+		BLE_EVENT_QUEUE_SIZE,
+		sizeof(struct epic_ble_event),
+		ble_event_queue_buffer,
+		&ble_event_queue_data
+	);
+
+	ble_epic_att_api_init();
+}
+
+void ble_epic_dm_api_event(dmEvt_t *dm_event)
+{
+	dmEvt_t *e = WsfBufAlloc(sizeof(*e));
+
+	if (e) {
+		memcpy(e, dm_event, sizeof(*e));
+		ble_epic_ble_api_trigger_event(BLE_EVENT_DM_EVENT, e);
+	} else {
+		LOG_WARN("ble", "could not allocate dm event");
+	}
+}
+
+void epic_ble_close_connection(uint8_t connId)
+{
+	AppConnClose(connId);
+}
+
+int epic_ble_is_connection_open(void)
+{
+	return AppConnIsOpen();
+}
+
+int epic_ble_set_device_name(const uint8_t *buf, uint16_t len)
+{
+	return epic_ble_atts_set_attr(GAP_DN_HDL, buf, len);
+}
+
+int epic_ble_get_device_name(uint8_t **buf, uint16_t *len)
+{
+	uint8_t ret = AttsGetAttr(GAP_DN_HDL, len, buf);
+	return ret;
+}
diff --git a/epicardium/ble/hid.c b/epicardium/ble/hid.c
index 4386f5b1dd0cbbce6d1e59b33d4fb55a14315033..56f5114218af7ad7db5c04b583c08ecbaeab4a0b 100644
--- a/epicardium/ble/hid.c
+++ b/epicardium/ble/hid.c
@@ -208,17 +208,9 @@ static void hidAppReportInit(void)
 void hid_work_init(void);
 void hid_init(void)
 {
-#ifdef HID_ATT_DYNAMIC
-	/* Initialize the dynamic service system */
-	AttsDynInit();
-	/* Add the HID service dynamically */
-	pSHdl = SvcHidAddGroupDyn();
-	AttsDynRegister(pSHdl, NULL, HidAttsWriteCback);
-#else
-	/* Add the HID service statically */
 	SvcHidAddGroup();
 	SvcHidRegister(HidAttsWriteCback, NULL);
-#endif /* HID_ATT_DYNAMIC */
+
 	/* Initialize the HID profile */
 	HidInit(&hidAppHidConfig);
 
diff --git a/epicardium/ble/meson.build b/epicardium/ble/meson.build
index 341ad22571c4d5bc94c9bcf997c320682659142e..2372440b4e5416aeb6fa57c52154afce12608b7d 100644
--- a/epicardium/ble/meson.build
+++ b/epicardium/ble/meson.build
@@ -1,5 +1,7 @@
 ble_sources = files(
   'ble.c',
+  'epic_ble_api.c',
+  'epic_att_api.c',
   'stack.c',
   'ble_main.c',
   'ble_attc.c',
diff --git a/epicardium/ble/stack.c b/epicardium/ble/stack.c
index d5e092179ef76827ccbea05ae3a10059e9ffac1c..4bf4696fd8639c1bf9b3101f3b58101e4220962b 100644
--- a/epicardium/ble/stack.c
+++ b/epicardium/ble/stack.c
@@ -184,6 +184,7 @@ void StackInit(void)
   AttHandlerInit(handlerId);
   AttsInit();
   AttsIndInit();
+  AttsDynInit();
   AttcInit();
 
   handlerId = WsfOsSetNextHandler(SmpHandler);
diff --git a/epicardium/ble/uart.c b/epicardium/ble/uart.c
index 5fdfb74ac9885975e125f51216bd2953986eca9c..55bf3ee8d6ebeb4df026303d5a51a6a40728155f 100644
--- a/epicardium/ble/uart.c
+++ b/epicardium/ble/uart.c
@@ -1,8 +1,13 @@
+#include "uart.h"
+#include "cccd.h"
+
 #include "modules/modules.h"
 
 #include "wsf_types.h"
 #include "util/bstream.h"
 #include "att_api.h"
+#include "dm_api.h"
+#include "app_api.h"
 
 #include "FreeRTOS.h"
 #include "timers.h"
@@ -11,24 +16,10 @@
 #include <string.h>
 #include <stdbool.h>
 
-#define UART_START_HDL 0x800            /*!< \brief Service start handle. */
-#define UART_END_HDL (UART_MAX_HDL - 1) /*!< \brief Service end handle. */
-
 /**************************************************************************************************
  Handles
 **************************************************************************************************/
 
-/*! \brief UART Service Handles */
-enum { UART_SVC_HDL = UART_START_HDL, /*!< \brief UART service declaration */
-       UART_RX_CH_HDL,                /*!< \brief UART rx characteristic */
-       UART_RX_HDL,                   /*!< \brief UART rx value */
-       UART_TX_CH_HDL,                /*!< \brief UART tx characteristic */
-       UART_TX_HDL,                   /*!< \brief UART tx value */
-       UART_TX_CH_CCC_HDL,            /*!< \brief UART tx CCCD */
-       UART_MAX_HDL                   /*!< \brief Maximum handle. */
-};
-/**@}*/
-
 /* clang-format off */
 static const uint8_t UARTSvc[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5,0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x01,0x00,0x40,0x6E};
 static const uint16_t UARTSvc_len = sizeof(UARTSvc);
@@ -41,7 +32,10 @@ static const uint8_t uartTxCh[] = {ATT_PROP_READ | ATT_PROP_NOTIFY, UINT16_TO_BY
 static const uint16_t uartTxCh_len = sizeof(uartTxCh);
 static const uint8_t attUartTxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x03,0x00,0x40,0x6E};
 
-static uint8_t ble_uart_tx_buf[128];
+static uint8_t uartValTxChCcc[] = {UINT16_TO_BYTES(0x0000)};
+static const uint16_t uartLenTxChCcc = sizeof(uartValTxChCcc);
+
+static uint8_t ble_uart_tx_buf[20];
 static uint16_t ble_uart_buf_tx_fill = 0;
 /* clang-format on */
 
@@ -96,18 +90,17 @@ static const attsAttr_t uartAttrCfgList[] = {
 	},
 	/* UART tx CCC descriptor */
 	{
-		.pUuid       = attCliChCfgUuid,
-		.pValue      = NULL,
-		.pLen        = NULL,
-		.maxLen      = 0,
-		.settings    = ATTS_SET_CCC,
-		.permissions = ATTS_PERMIT_WRITE | ATTS_PERMIT_WRITE_ENC |
-			       ATTS_PERMIT_WRITE_AUTH | ATTS_PERMIT_READ |
-			       ATTS_PERMIT_READ_ENC | ATTS_PERMIT_READ_AUTH,
+		.pUuid    = attCliChCfgUuid,
+		.pValue   = uartValTxChCcc,
+		.pLen     = (uint16_t *)&uartLenTxChCcc,
+		.maxLen   = sizeof(uartValTxChCcc),
+		.settings = ATTS_SET_CCC,
+		.permissions =
+			(ATTS_PERMIT_READ |
+			 ATTS_PERMIT_WRITE) // How about security?
 	},
-};
 
-dmConnId_t active_connection = 0;
+};
 
 static uint8_t UARTWriteCback(
 	dmConnId_t connId,
@@ -118,62 +111,80 @@ static uint8_t UARTWriteCback(
 	uint8_t *pValue,
 	attsAttr_t *pAttr
 ) {
-	active_connection = connId;
+	static bool was_r = false;
 
-	//printf("UARTWriteCback %d: ", len);
 	int i;
 	for (i = 0; i < len; i++) {
-		//printf("%c", pValue[i]);
+		if (pValue[i] == '\n' && !was_r) {
+			serial_enqueue_char('\r');
+		}
+		was_r = pValue[i] == '\r';
 		serial_enqueue_char(pValue[i]);
 	}
-	serial_enqueue_char('\r');
-	//printf("\n");
-
-#if 0
-  AttsSetAttr(UART_TX_HDL, len, pValue);
-  AttsHandleValueNtf(connId, UART_TX_HDL, len, pValue);
-#endif
 
 	return ATT_SUCCESS;
 }
 
-static int ble_uart_lasttick = 0;
+static bool done;
+static bool again;
 
-void ble_uart_write(uint8_t *pValue, uint8_t len)
+static void ble_uart_flush(void)
 {
-	for (int i = 0; i < len; i++) {
-		if (pValue[i] >= 0x20 && pValue[i] < 0x7f) {
-			ble_uart_tx_buf[ble_uart_buf_tx_fill] = pValue[i];
-			ble_uart_buf_tx_fill++;
-		}
+	if (ble_uart_buf_tx_fill == 0) {
+		return;
+	}
 
-		if (ble_uart_buf_tx_fill == 128 || pValue[i] == '\r' ||
-		    pValue[i] == '\n') {
-			if (ble_uart_buf_tx_fill > 0) {
-				if (active_connection) {
-					int x = xTaskGetTickCount() -
-						ble_uart_lasttick;
-					if (x < 100) {
-						/*
-						 * TODO: Ugly hack if we already
-						 *     send something recently.
-						 *     Figure out how fast we
-						 *     can send or use indications.
-						 */
-						vTaskDelay(100 - x);
-					}
+	dmConnId_t connId = AppConnIsOpen();
+	if (connId != DM_CONN_ID_NONE) {
+		if (AttsCccEnabled(connId, UART_TX_CH_CCC_IDX)) {
+			done   = false;
+			again  = true;
+			int t0 = xTaskGetTickCount();
+
+			while (!done && ((xTaskGetTickCount() - t0) < 1000)) {
+				if (again) {
+					again = false;
 					AttsHandleValueNtf(
-						active_connection,
+						connId,
 						UART_TX_HDL,
 						ble_uart_buf_tx_fill,
 						ble_uart_tx_buf
 					);
-					ble_uart_lasttick = xTaskGetTickCount();
 				}
-				ble_uart_buf_tx_fill = 0;
+				/* This function is supposed to only be called
+				 * from the API scheduler with lowest priority.
+				 *
+				 * If that is not the case anymore, use a delay
+				 * instead of the yield. Ideally refactor to avoid
+				 * the delay.
+				 */
+				//vTaskDelay(5);
+				taskYIELD();
 			}
 		}
 	}
+	ble_uart_buf_tx_fill = 0;
+}
+
+static void ble_uart_write_char(uint8_t c)
+{
+	ble_uart_tx_buf[ble_uart_buf_tx_fill] = c;
+	ble_uart_buf_tx_fill++;
+
+	// TODO: increase buffer if configured MTU allows it
+	if (ble_uart_buf_tx_fill == sizeof(ble_uart_tx_buf)) {
+		ble_uart_flush();
+	}
+}
+
+void ble_uart_write(uint8_t *pValue, uint8_t len)
+{
+	for (int i = 0; i < len; i++) {
+		ble_uart_write_char(pValue[i]);
+	}
+
+	// TODO schedule timer in a few ms to flush the buffer
+	ble_uart_flush();
 }
 
 static attsGroup_t uartCfgGroup = {
@@ -188,3 +199,19 @@ void bleuart_init(void)
 	/* Add the UART service */
 	AttsAddGroup(&uartCfgGroup);
 }
+
+void UartProcMsg(bleMsg_t *pMsg)
+{
+	if (pMsg->hdr.event == ATTS_HANDLE_VALUE_CNF) {
+		if (pMsg->att.handle == UART_TX_HDL) {
+			if (pMsg->hdr.status == ATT_SUCCESS) {
+				done = true;
+			} else if (pMsg->hdr.status == ATT_ERR_OVERFLOW) {
+				again = true;
+			}
+		}
+	}
+	if (pMsg->hdr.event == DM_CONN_OPEN_IND) {
+		ble_uart_buf_tx_fill = 0;
+	}
+}
diff --git a/epicardium/ble/uart.h b/epicardium/ble/uart.h
new file mode 100644
index 0000000000000000000000000000000000000000..4d3079b5e4bb167e93498e6e87e77704a11c226d
--- /dev/null
+++ b/epicardium/ble/uart.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "ble_api.h"
+
+#define UART_START_HDL 0x800            /*!< \brief Service start handle. */
+#define UART_END_HDL (UART_MAX_HDL - 1) /*!< \brief Service end handle. */
+
+/*! \brief UART Service Handles */
+enum { UART_SVC_HDL = UART_START_HDL, /*!< \brief UART service declaration */
+       UART_RX_CH_HDL,                /*!< \brief UART rx characteristic */
+       UART_RX_HDL,                   /*!< \brief UART rx value */
+       UART_TX_CH_HDL,                /*!< \brief UART tx characteristic */
+       UART_TX_HDL,                   /*!< \brief UART tx value */
+       UART_TX_CH_CCC_HDL,            /*!< \brief UART tx CCCD */
+       UART_MAX_HDL                   /*!< \brief Maximum handle. */
+};
+
+void UartProcMsg(bleMsg_t *pMsg);
+
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 7715568ca50e77289ae51aacd54bcc40d1e4deb2..3b93ea5bd1cdf3718867bc52986e34728f5e38c2 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -159,9 +159,28 @@ typedef _Bool bool;
 #define API_BLE_GET_SCAN_REPORT       0x144
 #define API_BLE_GET_LAST_PAIRING_NAME 0x145
 #define API_BLE_GET_PEER_DEVICE_NAME  0x146
+#define API_BLE_FREE_EVENT            0x147
 
 #define API_BLE_HID_SEND_REPORT        0x150
 
+#define API_BLE_ATTS_DYN_CREATE_GROUP         0x160
+#define API_BLE_ATTS_DYN_DELETE_GROUP         0x161
+#define API_BLE_ATTS_DYN_DELETE_GROUPS        0x169
+#define API_BLE_ATTS_DYN_ADD_CHARACTERISTIC   0x16B
+#define API_BLE_ATTS_DYN_ADD_DESCRIPTOR       0x163
+#define API_BLE_ATTS_SET_BUFFER               0x16A
+#define API_BLE_ATTS_SEND_SERVICE_CHANGED_IND 0x168
+
+#define API_BLE_ATTS_SET_ATTR                 0x170
+#define API_BLE_ATTS_HANDLE_VALUE_NTF         0x171
+#define API_BLE_ATTS_HANDLE_VALUE_IND         0x172
+
+#define API_BLE_CLOSE_CONNECTION              0x180
+#define API_BLE_IS_CONNECTION_OPEN            0x181
+#define API_BLE_SET_DEVICE_NAME               0x182
+#define API_BLE_GET_DEVICE_NAME               0x183
+#define API_BLE_GET_ADDRESS                   0x184
+
 /* clang-format on */
 
 typedef uint32_t api_int_id_t;
@@ -2319,7 +2338,7 @@ API(API_CONFIG_SET_STRING, int epic_config_set_string(const char *key, const cha
 /**
  * BLE event type
  */
-enum ble_event_type {
+enum epic_ble_event_type {
 	/** No event pending */
 	BLE_EVENT_NONE                                    = 0,
 	/** Numeric comparison requested */
@@ -2330,25 +2349,143 @@ enum ble_event_type {
 	BLE_EVENT_PAIRING_COMPLETE                        = 3,
 	/** New scan data is available  */
 	BLE_EVENT_SCAN_REPORT                             = 4,
+	BLE_EVENT_ATT_EVENT                               = 5,
+	BLE_EVENT_ATT_WRITE                               = 6,
+	BLE_EVENT_DM_EVENT                                = 7,
+};
+
+/**
+ * MicroPython Bluetooth support data types. Please
+ * do not use them until they are stabilized.
+ */
+typedef uint8_t bdAddr_t[6];
+
+struct epic_wsf_header
+{
+	/** General purpose parameter passed to event handler */
+	uint16_t param;
+	/** General purpose event value passed to event handler */
+	uint8_t event;
+	/** General purpose status value passed to event handler */
+	uint8_t status;
 };
 
+struct epic_att_event
+{
+	/** Header structure */
+	struct epic_wsf_header hdr;
+	/** Value */
+	uint8_t *pValue;
+	/** Value length */
+	uint16_t valueLen;
+	/** Attribute handle */
+	uint16_t handle;
+	/** TRUE if more response packets expected */
+	uint8_t continuing;
+	/** Negotiated MTU value */
+	uint16_t mtu;
+};
+
+struct epic_hciLeConnCmpl_event
+{        /** Event header */
+	struct epic_wsf_header hdr;
+	/** Status. */
+	uint8_t status;
+	/** Connection handle. */
+	uint16_t handle;
+	/** Local connection role. */
+	uint8_t role;
+	/** Peer address type. */
+	uint8_t addrType;
+	/** Peer address. */
+	bdAddr_t peerAddr;
+	/** Connection interval */
+	uint16_t connInterval;
+	/** Connection latency. */
+	uint16_t connLatency;
+	/** Supervision timeout. */
+	uint16_t supTimeout;
+	/** Clock accuracy. */
+	uint8_t clockAccuracy;
+
+	/** enhanced fields */
+	/** Local RPA. */
+	bdAddr_t localRpa;
+	/** Peer RPA. */
+	bdAddr_t peerRpa;
+};
+
+/*! \brief Disconnect complete event */
+struct epic_hciDisconnectCmpl_event
+{
+	/** Event header */
+	struct epic_wsf_header hdr;
+	/** Disconnect complete status. */
+	uint8_t status;
+	/** Connect handle. */
+	uint16_t handle;
+	/** Reason. */
+	uint8_t reason;
+};
+
+struct epic_dm_event
+{
+	union {
+		/** LE connection complete. */
+		struct epic_hciLeConnCmpl_event leConnCmpl;
+		/** Disconnect complete. */
+		struct epic_hciDisconnectCmpl_event disconnectCmpl;
+	};
+};
+
+struct epic_att_write
+{
+	/** Header structure */
+	struct epic_wsf_header hdr;
+	/** Value length */
+	uint16_t valueLen;
+	/** Attribute handle */
+	uint16_t handle;
+
+	uint8_t operation;
+	uint16_t offset;
+	void *buffer;
+};
+
+struct epic_ble_event {
+	enum epic_ble_event_type type;
+	union {
+		void *data;
+		struct epic_att_event *att_event;
+		struct epic_dm_event *dm_event;
+		struct epic_att_write *att_write;
+	};
+};
 
 /**
- * Scan report data. Bases on ``hciLeAdvReportEvt_t`` from BLE stack.
+ * Scan report data. Based on ``hciLeAdvReportEvt_t`` from BLE stack.
  *
  * TODO: 64 bytes for data is an arbitrary number ATM */
 struct epic_scan_report
 {
-  uint8_t             data[64];       /*!< \brief advertising or scan response data. */
-  uint8_t             len;            /*!< \brief length of advertising or scan response data. */
-  int8_t              rssi;           /*!< \brief RSSI. */
-  uint8_t             eventType;      /*!< \brief Advertising event type. */
-  uint8_t             addrType;       /*!< \brief Address type. */
-  uint8_t             addr[6];        /*!< \brief Device address. */
-
-  /* \brief direct fields */
-  uint8_t             directAddrType; /*!< \brief Direct advertising address type. */
-  uint8_t             directAddr[6];  /*!< \brief Direct advertising address. */
+	/** advertising or scan response data. */
+	uint8_t data[64];
+	/** length of advertising or scan response data. */
+	uint8_t len;
+	/** RSSI. */
+	int8_t rssi;
+	/** Advertising event type. */
+	uint8_t eventType;
+	/** Address type. */
+	uint8_t addrType;
+	/** Device address. */
+	uint8_t addr[6];
+
+	/** direct fields */
+	/** Direct advertising address type. */
+	uint8_t directAddrType;
+	/** Direct advertising address. */
+	uint8_t directAddr[6];
 };
 
 /**
@@ -2379,15 +2516,10 @@ API_ISR(EPIC_INT_BLE, epic_isr_ble);
 /**
  * Retreive the event which triggered :c:func:`epic_isr_ble`
  *
- * The handling code needs to ensure to handle interrupts in a timely
- * manner as new events will overwrite each other. Reading the event
- * automatically resets it to :c:data:`BLE_EVENT_NONE`.
- *
- * :return: Event which triggered the interrupt.
- *
  * .. versionadded:: 1.16
+ * .. versionchanged:: 1.17
  */
-API(API_BLE_GET_EVENT, enum ble_event_type epic_ble_get_event(void));
+API(API_BLE_GET_EVENT, int epic_ble_get_event(struct epic_ble_event *e));
 
 /**
  * Retrieve the compare value of an ongoing pairing procedure.
@@ -2499,5 +2631,35 @@ API(API_BLE_GET_SCAN_REPORT, int epic_ble_get_scan_report(struct epic_scan_repor
  */
 API(API_BLE_HID_SEND_REPORT, int epic_ble_hid_send_report(uint8_t report_id, uint8_t *data, uint8_t len));
 
+
+/**
+ * MicroPython BLE support API
+ *
+ * Please do not use this API outside the MicroPython Bluetooth module. The API designed to work
+ * specifically with that module.
+ *
+ * The MicroPython Bluetooth module is still in flux so this API will continue to change as well.
+ */
+API(API_BLE_ATTS_DYN_CREATE_GROUP, int epic_atts_dyn_create_service(const uint8_t *uuid, uint8_t uuid_len, uint16_t group_size, void **pSvcHandle));
+//API(API_BLE_ATTS_DYN_DELETE_GROUP, void AttsDynDeleteGroup(void *pSvcHandle));
+API(API_BLE_ATTS_DYN_DELETE_GROUPS, int epic_ble_atts_dyn_delete_groups(void));
+
+API(API_BLE_ATTS_DYN_ADD_CHARACTERISTIC, int epic_atts_dyn_add_characteristic(void *pSvcHandle, const uint8_t *uuid, uint8_t uuid_len, uint8_t flags, uint16_t maxLen, uint16_t *value_handle));
+API(API_BLE_ATTS_DYN_ADD_DESCRIPTOR, int epic_ble_atts_dyn_add_descriptor(void *pSvcHandle, const uint8_t *uuid, uint8_t uuid_len, uint8_t flags, const uint8_t *value, uint16_t value_len, uint16_t maxLen, uint16_t *descriptor_handle));
+
+API(API_BLE_ATTS_SEND_SERVICE_CHANGED_IND, int epic_atts_dyn_send_service_changed_ind(void));
+
+API(API_BLE_ATTS_SET_ATTR, int epic_ble_atts_set_attr(uint16_t handle, const uint8_t *value, uint16_t value_len));
+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));
+
+API(API_BLE_FREE_EVENT, int epic_ble_free_event(struct epic_ble_event *e));
+API(API_BLE_CLOSE_CONNECTION, void epic_ble_close_connection(uint8_t connId));
+API(API_BLE_IS_CONNECTION_OPEN, int epic_ble_is_connection_open(void));
+API(API_BLE_SET_DEVICE_NAME, int epic_ble_set_device_name(const uint8_t *buf, uint16_t len));
+API(API_BLE_GET_DEVICE_NAME, int epic_ble_get_device_name(uint8_t **buf, uint16_t *len));
+API(API_BLE_GET_ADDRESS, void epic_ble_get_address(uint8_t *addr));
+
 #endif /* _EPICARDIUM_H */
 
diff --git a/epicardium/modules/serial.c b/epicardium/modules/serial.c
index f2ec9e9344c9566005fc3e896f70ece2bdbc798e..6be231d6538ecd436708ce024b74b6ce8ea2188f 100644
--- a/epicardium/modules/serial.c
+++ b/epicardium/modules/serial.c
@@ -50,10 +50,7 @@ void serial_return_to_synchronous()
 	write_stream_buffer = NULL;
 }
 
-/*
- * API-call to write a string.  Output goes to both CDCACM and UART
- */
-void epic_uart_write_str(const char *str, size_t length)
+static void write_str_(const char *str, size_t length)
 {
 	if (length == 0) {
 		return;
@@ -129,6 +126,22 @@ void epic_uart_write_str(const char *str, size_t length)
 	}
 }
 
+/*
+ * API-call to write a string.  Output goes to CDCACM, UART and BLE
+ *
+ * This is user data from core 1.
+ */
+void epic_uart_write_str(const char *str, size_t length)
+{
+	/* Make sure that we are not in an interrupt when talking to BLE.
+	 * Should not be the case if this is called from core 1
+	 * anyways. */
+	if (!xPortIsInsideInterrupt()) {
+		ble_uart_write((uint8_t *)str, length);
+	}
+	write_str_(str, length);
+}
+
 static void serial_flush_from_isr(void)
 {
 	uint8_t rx_data[32];
@@ -196,7 +209,6 @@ static void serial_flush_from_thread(void)
 		taskEXIT_CRITICAL();
 
 		cdcacm_write((uint8_t *)&rx_data, received_bytes);
-		ble_uart_write((uint8_t *)&rx_data, received_bytes);
 	} while (received_bytes > 0);
 }
 
@@ -243,6 +255,11 @@ int epic_uart_read_str(char *buf, size_t cnt)
 	return i;
 }
 
+/*
+ * Write a string from epicardium. Output goes to CDCACM and UART
+ *
+ * This mainly log data from epicardium, not user date from core 1.
+ */
 long _write_epicardium(int fd, const char *buf, size_t cnt)
 {
 	/*
@@ -252,12 +269,12 @@ long _write_epicardium(int fd, const char *buf, size_t cnt)
 	size_t i, last = 0;
 	for (i = 0; i < cnt; i++) {
 		if (buf[i] == '\n') {
-			epic_uart_write_str(&buf[last], i - last);
-			epic_uart_write_str("\r", 1);
+			write_str_(&buf[last], i - last);
+			write_str_("\r", 1);
 			last = i;
 		}
 	}
-	epic_uart_write_str(&buf[last], cnt - last);
+	write_str_(&buf[last], cnt - last);
 	return cnt;
 }
 
diff --git a/lib/micropython/gen-qstr.sh b/lib/micropython/gen-qstr.sh
index 747ae3f614104dadf98e0d579ccc3c0ae22a1c6f..e339e7d000d60d74051ce05478381616d190ed8d 100755
--- a/lib/micropython/gen-qstr.sh
+++ b/lib/micropython/gen-qstr.sh
@@ -18,12 +18,12 @@ gcc -E -DNO_QSTR -I"$SOURCE_DIR/micropython" -I"$PROJECT_SRC" -I"$OUTPUT_DIR" "$
 rm -rf "$OUTPUT_DIR/qstr"
 
 # Generate qstr.split
-"$PYTHON" "$SOURCE_DIR/micropython/py/makeqstrdefs.py" split \
-    "$OUTPUT_DIR/qstr.i.last" "$OUTPUT_DIR/qstr" "$OUTPUT_DIR/qstrdefs.collected.h" >/dev/null
+"$PYTHON" "$SOURCE_DIR/micropython/py/makeqstrdefs.py" split qstr\
+    "$OUTPUT_DIR/qstr.i.last" "$OUTPUT_DIR/qstr" "$OUTPUT_DIR/qstrdefs.collected.h"
 
 # Generate qstr.collected.h
-"$PYTHON" "$SOURCE_DIR/micropython/py/makeqstrdefs.py" cat \
-    "$OUTPUT_DIR/qstr.i.last" "$OUTPUT_DIR/qstr" "$OUTPUT_DIR/qstrdefs.collected.h" >/dev/null
+"$PYTHON" "$SOURCE_DIR/micropython/py/makeqstrdefs.py" cat qstr\
+    "$OUTPUT_DIR/qstr.i.last" "$OUTPUT_DIR/qstr" "$OUTPUT_DIR/qstrdefs.collected.h"
 
 # Preprocess Header ... I did not come up with this, this is code copied from
 #    the official make file.  Seriously.
diff --git a/lib/micropython/meson.build b/lib/micropython/meson.build
index bf021db1a6a636e6c46a2896019ac5b57802a5c3..f85af62c96aa1c3a14c32f6c99c3ec9bb6690a4e 100644
--- a/lib/micropython/meson.build
+++ b/lib/micropython/meson.build
@@ -157,6 +157,7 @@ micropython_sources = files(
   'micropython/py/pystack.c',
   'micropython/py/qstr.c',
   'micropython/py/reader.c',
+  'micropython/py/ringbuf.c',
   'micropython/py/repl.c',
   'micropython/py/runtime.c',
   'micropython/py/runtime_utils.c',
diff --git a/lib/micropython/micropython b/lib/micropython/micropython
index 1f371947309c5ea6023b6d9065415697cbc75578..b0932fcf2e2f9a81abf7737ed4b2573bd9ad4a49 160000
--- a/lib/micropython/micropython
+++ b/lib/micropython/micropython
@@ -1 +1 @@
-Subproject commit 1f371947309c5ea6023b6d9065415697cbc75578
+Subproject commit b0932fcf2e2f9a81abf7737ed4b2573bd9ad4a49
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..713f6525c5007dadf425d4228620f6f544324dd1 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,15 @@ 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);
+
+void AttsDynAddAttrDyn(void *pSvcHandle, const uint8_t *pUuid, uint8_t uuidLen, const uint8_t *pValue,
+                    uint16_t len, const uint16_t maxLen, uint8_t settings, uint8_t permissions);
+
+void AttsDynAddAttrDynConst(void *pSvcHandle, const uint8_t *pUuid, uint8_t uuidLen, const uint8_t *pValue,
+                    uint16_t len, const uint16_t maxLen, 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 30dc989ff0afe0e83a65dcc4f703521c0af4d5d8..f523cf687575f9a68c22fd2e092dfda239c7b0e6 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
@@ -351,3 +351,162 @@ void AttsDynAddAttrConst(void *pSvcHandle, const uint8_t *pUuid, const uint8_t *
     AttsAddGroup(&pGroup->group);
   }
 }
+
+void AttsDynAddAttrDyn(void *pSvcHandle, const uint8_t *pUuid, uint8_t uuidLen, const uint8_t *pValue,
+                    uint16_t len, const uint16_t maxLen, uint8_t settings, uint8_t permissions)
+{
+  attsAttr_t *pAttr;
+  attsDynGroupCb_t *pGroup = pSvcHandle;
+  uint16_t handle = pGroup->group.startHandle + pGroup->currentAttr++;
+
+  /* Verify inputs */
+  WSF_ASSERT(handle <= pGroup->group.endHandle);
+  WSF_ASSERT(pUuid);
+  WSF_ASSERT(len <= maxLen);
+
+  pAttr = pGroup->group.pAttr + (handle - pGroup->group.startHandle);
+
+
+  /* Allocate a buffer for the UUID of the attribute */
+  pAttr->pUuid = attsDynAlloc(uuidLen);
+  WSF_ASSERT(pAttr->pUuid);
+
+  if (pAttr->pUuid == NULL)
+  {
+    return;
+  }
+
+  /* Allocate a buffer for the length of the attribute */
+  pAttr->pLen = attsDynAlloc(sizeof(uint16_t));
+  WSF_ASSERT(pAttr->pLen);
+
+  if (pAttr->pLen == NULL)
+  {
+    return;
+  }
+
+  /* Allocate a buffer for the value of the attribute */
+  pAttr->pValue = attsDynAlloc(maxLen);
+  WSF_ASSERT(pAttr->pValue);
+
+  if (pAttr->pValue == NULL)
+  {
+    return;
+  }
+
+  /* Set the attribute values */
+  memcpy(pAttr->pUuid, pUuid, uuidLen);
+  pAttr->maxLen = maxLen;
+  pAttr->settings = settings;
+  pAttr->permissions = permissions;
+
+
+
+  if (pValue)
+  {
+    /* Copy the initial value */
+    memcpy(pAttr->pValue, pValue, len);
+    *pAttr->pLen = len;
+  }
+  else
+  {
+    /* No initial value, zero value and length */
+    memset(pAttr->pValue, 0, maxLen);
+    *pAttr->pLen = 0;
+  }
+
+  /* Add the service when the last attribute has been added */
+  if (handle == pGroup->group.endHandle)
+  {
+    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)
+  {
+    /* Only alllocate a value if there is a change and if
+     * it was allocated before. */
+    if(pAttr->pValue && pAttr->maxLen != maxLen )
+    {
+      /* Allocate new buffer for the value of the attribute */
+      pValue = attsDynAlloc(maxLen);
+      WSF_ASSERT(pValue);
+
+      if (pValue == NULL)
+      {
+        return ATT_ERR_MEMORY;
+      }
+
+      pAttr->pValue = pValue;
+      memset(pValue, 0, maxLen);
+    }
+
+    pAttr->maxLen = maxLen;
+  }
+  /* else attribute not found */
+  else
+  {
+    err = ATT_ERR_NOT_FOUND;
+  }
+
+  return err;
+
+}
+
+void AttsDynAddAttrDynConst(void *pSvcHandle, const uint8_t *pUuid, uint8_t uuidLen, const uint8_t *pValue,
+                    uint16_t len, const uint16_t maxLen, uint8_t settings, uint8_t permissions)
+{
+  attsAttr_t *pAttr;
+  attsDynGroupCb_t *pGroup = pSvcHandle;
+  uint16_t handle = pGroup->group.startHandle + pGroup->currentAttr++;
+
+  /* Verify inputs */
+  WSF_ASSERT(handle <= pGroup->group.endHandle);
+  WSF_ASSERT(pUuid);
+  WSF_ASSERT(len <= maxLen);
+
+  pAttr = pGroup->group.pAttr + (handle - pGroup->group.startHandle);
+
+
+  /* Allocate a buffer for the UUID of the attribute */
+  pAttr->pUuid = attsDynAlloc(uuidLen);
+  WSF_ASSERT(pAttr->pUuid);
+
+  if (pAttr->pUuid == NULL)
+  {
+    return;
+  }
+
+  /* Allocate a buffer for the length of the attribute */
+  pAttr->pLen = attsDynAlloc(sizeof(uint16_t));
+  WSF_ASSERT(pAttr->pLen);
+
+  if (pAttr->pLen == NULL)
+  {
+    return;
+  }
+
+  pAttr->pValue = pValue;
+  *pAttr->pLen = len;
+
+  /* Set the attribute values */
+  memcpy(pAttr->pUuid, pUuid, uuidLen);
+  pAttr->maxLen = maxLen;
+  pAttr->settings = settings;
+  pAttr->permissions = permissions;
+
+  /* Add the service when the last attribute has been added */
+  if (handle == pGroup->group.endHandle)
+  {
+    AttsAddGroup(&pGroup->group);
+  }
+}
+
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_api.h b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_api.h
index 028016242eea1d038513f5ca619edb6f61401635..8dd5fcb013715abb9e2a6061349f157b28a150fd 100644
--- a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_api.h
+++ b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_api.h
@@ -74,6 +74,31 @@ void GattDiscover(dmConnId_t connId, uint16_t *pHdlList);
 /*************************************************************************************************/
 uint8_t GattValueUpdate(uint16_t *pHdlList, attEvt_t *pMsg);
 
+/*************************************************************************************************/
+/*!
+ *  \brief  Set Index of the Service Changed CCCD in the ATT Server.
+ *
+ *  \param  idx  Index of the Service Changed CCCD in the ATT Server.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void GattSetSvcChangedIdx(uint8_t idx);
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Send Service Change Indications to the specified connections if they are configured to
+ *          do so.
+ *
+ *  \param  connId    DM Connection identifier or \ref DM_CONN_ID_NONE to send to all connections.
+ *  \param  start     start handle for service changed value.
+ *  \param  end       end handle for service changed value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void GattSendServiceChangedInd(dmConnId_t connId, uint16_t start, uint16_t end);
+
 /*! \} */    /* GATT_PROFILE */
 
 #ifdef __cplusplus
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_main.c b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_main.c
index d7aeb9af7b81add8fa9f09aebd5afbf1e0330915..ad52cc678af7d1119f246b8e43a05dbef892cb2e 100644
--- a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_main.c
+++ b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/profiles/gatt/gatt_main.c
@@ -19,13 +19,30 @@
 
 #include "wsf_types.h"
 #include "wsf_assert.h"
+#include "util/bstream.h"
 #include "app_api.h"
 #include "gatt/gatt_api.h"
+#include "svc_core.h"
+#include "att_api.h"
+
+/**************************************************************************************************
+Data Types
+**************************************************************************************************/
+
+/* Control block. */
+typedef struct
+{
+  bool_t  svcChangedCccdIdxSet; /* Check if Service Changed CCCD index has been initialized. */
+  uint8_t svcChangedCccdIdx;    /* Stored index of Service Changed CCCD. */
+} gattServCb_t;
 
 /**************************************************************************************************
   Local Variables
 **************************************************************************************************/
 
+/* Control block. */
+gattServCb_t gattServCb;
+
 /*! GATT service characteristics for discovery */
 
 /*! Service changed */
@@ -46,7 +63,7 @@ static const attcDiscChar_t gattScCcc =
 static const attcDiscChar_t *gattDiscCharList[] =
 {
   &gattSc,                    /* Service changed */
-  &gattScCcc                  /* Service changed client characteristic configuration descriptor */
+  &gattScCcc,                 /* Service changed client characteristic configuration descriptor */
 };
 
 /* sanity check:  make sure handle list length matches characteristic list length */
@@ -101,3 +118,68 @@ uint8_t GattValueUpdate(uint16_t *pHdlList, attEvt_t *pMsg)
 
   return status;
 }
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set Index of the Service Changed CCCD in the ATT Server.
+ *
+ *  \param  idx  Index of the Service Changed CCCD in the ATT Server.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void GattSetSvcChangedIdx(uint8_t idx)
+{
+  gattServCb.svcChangedCccdIdxSet = TRUE;
+  gattServCb.svcChangedCccdIdx = idx;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Send Service Change Indications to the specified connections if they are configured to
+ *          do so.
+ *
+ *  \param  connId    DM Connection identifier or \ref DM_CONN_ID_NONE to send to all connections.
+ *  \param  start     start handle for service changed value.
+ *  \param  end       end handle for service changed value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void GattSendServiceChangedInd(dmConnId_t connId, uint16_t start, uint16_t end)
+{
+  uint8_t svcChangedValues[4];
+  uint8_t *p;
+
+  if (!gattServCb.svcChangedCccdIdxSet)
+  {
+    return;
+  }
+
+  p = svcChangedValues;
+  UINT16_TO_BSTREAM(p, start);
+  UINT16_TO_BSTREAM(p, end);
+
+  /* If connection is not specified */
+  if (connId == DM_CONN_ID_NONE)
+  {
+    /* Send to all. */
+    for (connId = 1; connId <= DM_CONN_MAX; connId++)
+    {
+      if (AttsCccEnabled(connId, gattServCb.svcChangedCccdIdx))
+      {
+        AttsHandleValueInd(connId, GATT_SC_HDL, sizeof(svcChangedValues), svcChangedValues);
+      }
+    }
+  }
+  else
+  {
+    /* Send to only this one. */
+    if (AttsCccEnabled(connId, gattServCb.svcChangedCccdIdx))
+    {
+      AttsHandleValueInd(connId, GATT_SC_HDL, sizeof(svcChangedValues), svcChangedValues);
+    }
+  }
+}
+
+
diff --git a/preload/apps/ble/__init__.py b/preload/apps/ble/__init__.py
index b50a69808d7c1ac081771ff13d841ba5efc9547e..9eff8891c0343d6fc6283b191a7378dd7a44e3bd 100644
--- a/preload/apps/ble/__init__.py
+++ b/preload/apps/ble/__init__.py
@@ -6,7 +6,7 @@ import sys_ble
 import interrupt
 import config
 
-ble_event = None
+ble_events = []
 is_active = False
 
 STATE_IDLE = 1
@@ -16,8 +16,13 @@ STATE_FAIL = 4
 
 
 def ble_callback(_):
-    global ble_event
-    ble_event = sys_ble.get_event()
+    global ble_events
+
+    while True:
+        event = sys_ble.get_event()
+        if event == sys_ble.EVENT_NONE:
+            break
+        ble_events.append(event)
 
 
 def init():
@@ -95,6 +100,11 @@ while True:
     v = ~v_old & v_new
     v_old = v_new
 
+    ble_event = None
+    if len(ble_events) > 0:
+        ble_event = ble_events[0]
+        ble_events = ble_events[1:]
+
     if state == STATE_IDLE:
         # print config screen
         disp.clear()
@@ -104,7 +114,6 @@ while True:
 
         # wait for button press or ble_event
         if ble_event == sys_ble.EVENT_HANDLE_NUMERIC_COMPARISON:
-            ble_event = None
             state = STATE_NUMERIC_COMPARISON
         if v & buttons.TOP_RIGHT:
             toggle()
@@ -128,7 +137,6 @@ while True:
 
         # wait for button press or ble_event
         if ble_event == sys_ble.EVENT_PAIRING_FAILED:
-            ble_event = None
             state = STATE_FAIL
         if v & buttons.BOTTOM_LEFT:
             sys_ble.confirm_compare_value(True)
@@ -144,10 +152,8 @@ while True:
     elif state == STATE_WAIT_FOR_COMPLETION:
         # Wait for pairing to complete
         if ble_event == sys_ble.EVENT_PAIRING_FAILED:
-            ble_event = None
             state = STATE_FAIL
         elif ble_event == sys_ble.EVENT_PAIRING_COMPLETE:
-            ble_event = None
             pairing_name = sys_ble.get_last_pairing_name().split("/")[-1].split(".")[0]
             disp.clear()
             disp.print("BLE Pairing", posy=0, fg=[0, 0, 255])
diff --git a/preload/apps/exnostat/__init__.py b/preload/apps/exnostat/__init__.py
index 57c69fc19a1377123c47c1bf8c2b576753526888..60cf16d3bb07f33325053e8ff81b0462e505b0dc 100644
--- a/preload/apps/exnostat/__init__.py
+++ b/preload/apps/exnostat/__init__.py
@@ -100,14 +100,17 @@ def process_scan_report(scan_report):
 
 
 def ble_callback(_):
-    event = sys_ble.get_event()
-    if event == sys_ble.EVENT_SCAN_REPORT:
-        while True:
-            scan_report = sys_ble.get_scan_report()
-            if scan_report == None:
-                break
-            process_scan_report(scan_report)
-        prune()
+    while True:
+        event = sys_ble.get_event()
+        if event == sys_ble.EVENT_NONE:
+            break
+        if event == sys_ble.EVENT_SCAN_REPORT:
+            while True:
+                scan_report = sys_ble.get_scan_report()
+                if scan_report == None:
+                    break
+                process_scan_report(scan_report)
+            prune()
 
 
 def show_stats():
diff --git a/pycardium/modules/fat_file.c b/pycardium/modules/fat_file.c
index ce89e32e8061d81fae9cb6159552dc434a6bf3b7..ed4f1669c61462b78f5a5a3dc279476989511031 100644
--- a/pycardium/modules/fat_file.c
+++ b/pycardium/modules/fat_file.c
@@ -111,11 +111,11 @@ file_obj_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, int *errcode)
 STATIC const mp_arg_t file_open_args[] = {
 	{ MP_QSTR_file,
 	  MP_ARG_OBJ | MP_ARG_REQUIRED,
-	  { .u_rom_obj = MP_ROM_PTR(&mp_const_none_obj) } },
+	  { .u_rom_obj = mp_const_none } },
 	{ MP_QSTR_mode, MP_ARG_OBJ, { .u_obj = MP_OBJ_NEW_QSTR(MP_QSTR_r) } },
 	{ MP_QSTR_encoding,
 	  MP_ARG_OBJ | MP_ARG_KW_ONLY,
-	  { .u_rom_obj = MP_ROM_PTR(&mp_const_none_obj) } },
+	  { .u_rom_obj = mp_const_none } },
 };
 #define FILE_OPEN_NUM_ARGS MP_ARRAY_SIZE(file_open_args)
 
diff --git a/pycardium/modules/modbluetooth_card10.c b/pycardium/modules/modbluetooth_card10.c
index 0bff819a6aec832559e69a8fee90676218212a40..a13d3f7934cc10080b041995807cf3d04a3264ac 100644
--- a/pycardium/modules/modbluetooth_card10.c
+++ b/pycardium/modules/modbluetooth_card10.c
@@ -1,38 +1,212 @@
+#include "modbluetooth_card10.h"
 #include "extmod/modbluetooth.h"
 #include "py/runtime.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
+};
 
 const char *const not_implemented_message =
 	"Not (yet) implemented on card10. See https://git.card10.badge.events.ccc.de/card10/firmware/-/issues/8";
 
+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_status_entry_t;
+
+static bool active = false;
+
+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 raise(void)
 {
 	mp_raise_NotImplementedError(not_implemented_message);
 }
 
+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;
+		}
+	}
+}
+
+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)
 {
-	raise();
+	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
+	);
+	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)
 {
-	raise();
+	epic_interrupt_disable(EPIC_INT_BLE);
+	active = false;
 }
 
 // Returns true when the Bluetooth stack is enabled.
-bool mp_bluetooth_is_enabled(void)
+bool mp_bluetooth_is_active(void)
 {
-	raise();
-	return false;
+	return active;
 }
 
 // Gets the MAC addr of this device in big-endian format.
 void mp_bluetooth_get_device_addr(uint8_t *addr)
 {
+	epic_ble_get_address(addr);
+}
+
+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.
@@ -45,7 +219,12 @@ int mp_bluetooth_gap_advertise_start(
 	const uint8_t *sr_data,
 	size_t sr_data_len
 ) {
-	raise();
+	// Dropping any current connection starts advertising on the card10
+	// TODO: modify the advertising data
+	int connection = epic_ble_is_connection_open();
+	if (connection > 0) {
+		epic_ble_close_connection(connection);
+	}
 	return 0;
 }
 
@@ -58,9 +237,12 @@ void mp_bluetooth_gap_advertise_stop(void)
 // Start adding services. Must be called before mp_bluetooth_register_service.
 int mp_bluetooth_gatts_register_service_begin(bool append)
 {
-	raise();
+	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(
@@ -73,13 +255,138 @@ int mp_bluetooth_gatts_register_service(
 	uint16_t *handles,
 	size_t num_characteristics
 ) {
-	raise();
+	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()
 {
-	raise();
+	epic_atts_dyn_send_service_changed_ind();
 	return 0;
 }
 
@@ -87,36 +394,91 @@ int mp_bluetooth_gatts_register_service_end()
 int mp_bluetooth_gatts_read(
 	uint16_t value_handle, uint8_t **value, size_t *value_len
 ) {
-	raise();
-	return 0;
+	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
 ) {
-	raise();
-	return 0;
+	// 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)
 {
-	raise();
-	return 0;
+	// 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
+	size_t value_len
 ) {
-	raise();
+	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)
 {
-	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;
 }
 
@@ -124,14 +486,15 @@ 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.
 int mp_bluetooth_gap_disconnect(uint16_t conn_handle)
 {
-	raise();
+	epic_ble_close_connection(conn_handle);
 	return 0;
 }
 
diff --git a/pycardium/modules/modbluetooth_card10.h b/pycardium/modules/modbluetooth_card10.h
new file mode 100644
index 0000000000000000000000000000000000000000..0e2296a7e99fcfa862e2721f5c947dd139afb923
--- /dev/null
+++ b/pycardium/modules/modbluetooth_card10.h
@@ -0,0 +1,131 @@
+#pragma once
+
+/*
+ * card10 related BLE defines. They are taken from the cordio (not card10!)
+ * BLE stack which is used by the card10.
+ */
+
+#define ATT_SUCCESS 0x00         /*!< \brief Operation successful */
+#define ATT_ERR_HANDLE 0x01      /*!< \brief Invalid handle */
+#define ATT_ERR_READ 0x02        /*!< \brief Read not permitted */
+#define ATT_ERR_WRITE 0x03       /*!< \brief Write not permitted */
+#define ATT_ERR_INVALID_PDU 0x04 /*!< \brief Invalid pdu */
+#define ATT_ERR_AUTH 0x05        /*!< \brief Insufficient authentication */
+#define ATT_ERR_NOT_SUP 0x06     /*!< \brief Request not supported */
+#define ATT_ERR_OFFSET 0x07      /*!< \brief Invalid offset */
+#define ATT_ERR_AUTHOR 0x08      /*!< \brief Insufficient authorization */
+#define ATT_ERR_QUEUE_FULL 0x09  /*!< \brief Prepare queue full */
+#define ATT_ERR_NOT_FOUND 0x0A   /*!< \brief Attribute not found */
+#define ATT_ERR_NOT_LONG 0x0B    /*!< \brief Attribute not long */
+#define ATT_ERR_KEY_SIZE 0x0C    /*!< \brief Insufficient encryption key size */
+#define ATT_ERR_LENGTH 0x0D      /*!< \brief Invalid attribute value length */
+#define ATT_ERR_UNLIKELY 0x0E    /*!< \brief Other unlikely error */
+#define ATT_ERR_ENC 0x0F         /*!< \brief Insufficient encryption */
+#define ATT_ERR_GROUP_TYPE 0x10  /*!< \brief Unsupported group type */
+#define ATT_ERR_RESOURCES 0x11   /*!< \brief Insufficient resources */
+#define ATT_ERR_DATABASE_OUT_OF_SYNC                                           \
+	0x12 /*!< \brief Client out of synch with database */
+#define ATT_ERR_VALUE_NOT_ALLOWED 0x13 /*!< \brief Value not allowed */
+#define ATT_ERR_WRITE_REJ 0xFC         /*!< \brief Write request rejected */
+#define ATT_ERR_CCCD 0xFD              /*!< \brief CCCD improperly configured */
+#define ATT_ERR_IN_PROGRESS 0xFE /*!< \brief Procedure already in progress */
+#define ATT_ERR_RANGE 0xFF       /*!< \brief Value out of range */
+/**@}*/
+
+/** \name Proprietary Internal Error Codes
+ * These codes may be sent to application but are not present in any ATT PDU.
+ */
+/**@{*/
+#define ATT_ERR_MEMORY 0x70      /*!< \brief Out of memory */
+#define ATT_ERR_TIMEOUT 0x71     /*!< \brief Transaction timeout */
+#define ATT_ERR_OVERFLOW 0x72    /*!< \brief Transaction overflow */
+#define ATT_ERR_INVALID_RSP 0x73 /*!< \brief Invalid response PDU */
+#define ATT_ERR_CANCELLED 0x74   /*!< \brief Request cancelled */
+#define ATT_ERR_UNDEFINED 0x75   /*!< \brief Other undefined error */
+#define ATT_ERR_REQ_NOT_FOUND                                                  \
+	0x76 /*!< \brief Required characteristic not found */
+#define ATT_ERR_MTU_EXCEEDED                                                   \
+	0x77 /*!< \brief Attribute PDU length exceeded MTU size */
+#define ATT_CONTINUING 0x78 /*!< \brief Procedure continuing */
+#define ATT_RSP_PENDING                                                        \
+	0x79 /*!< \brief Responsed delayed pending higher layer */
+
+#define DM_CBACK_START 0x20 /*!< \brief DM callback event starting value */
+
+/*! \brief DM callback events */
+enum { DM_RESET_CMPL_IND = DM_CBACK_START, /*!< \brief Reset complete */
+       DM_ADV_START_IND,                   /*!< \brief Advertising started */
+       DM_ADV_STOP_IND,                    /*!< \brief Advertising stopped */
+       DM_ADV_NEW_ADDR_IND, /*!< \brief New resolvable address has been generated */
+       DM_SCAN_START_IND,    /*!< \brief Scanning started */
+       DM_SCAN_STOP_IND,     /*!< \brief Scanning stopped */
+       DM_SCAN_REPORT_IND,   /*!< \brief Scan data received from peer device */
+       DM_CONN_OPEN_IND,     /*!< \brief Connection opened */
+       DM_CONN_CLOSE_IND,    /*!< \brief Connection closed */
+       DM_CONN_UPDATE_IND,   /*!< \brief Connection update complete */
+       DM_SEC_PAIR_CMPL_IND, /*!< \brief Pairing completed successfully */
+       DM_SEC_PAIR_FAIL_IND, /*!< \brief Pairing failed or other security failure */
+       DM_SEC_ENCRYPT_IND,      /*!< \brief Connection encrypted */
+       DM_SEC_ENCRYPT_FAIL_IND, /*!< \brief Encryption failed */
+       DM_SEC_AUTH_REQ_IND, /*!< \brief PIN or OOB data requested for pairing */
+       DM_SEC_KEY_IND,      /*!< \brief Security key indication */
+       DM_SEC_LTK_REQ_IND,  /*!< \brief LTK requested for encyption */
+       DM_SEC_PAIR_IND,     /*!< \brief Incoming pairing request from master */
+       DM_SEC_SLAVE_REQ_IND, /*!< \brief Incoming security request from slave */
+       DM_SEC_CALC_OOB_IND, /*!< \brief Result of OOB Confirm Calculation Generation */
+       DM_SEC_ECC_KEY_IND, /*!< \brief Result of ECC Key Generation */
+       DM_SEC_COMPARE_IND, /*!< \brief Result of Just Works/Numeric Comparison Compare Value Calculation */
+       DM_SEC_KEYPRESS_IND, /*!< \brief Keypress indication from peer in passkey security */
+       DM_PRIV_RESOLVED_ADDR_IND, /*!< \brief Private address resolved */
+       DM_PRIV_GENERATE_ADDR_IND, /*!< \brief Private resolvable address generated */
+       DM_CONN_READ_RSSI_IND,           /*!< \brief Connection RSSI read */
+       DM_PRIV_ADD_DEV_TO_RES_LIST_IND, /*!< \brief Device added to resolving list */
+       DM_PRIV_REM_DEV_FROM_RES_LIST_IND, /*!< \brief Device removed from resolving list */
+       DM_PRIV_CLEAR_RES_LIST_IND,     /*!< \brief Resolving list cleared */
+       DM_PRIV_READ_PEER_RES_ADDR_IND, /*!< \brief Peer resolving address read */
+       DM_PRIV_READ_LOCAL_RES_ADDR_IND, /*!< \brief Local resolving address read */
+       DM_PRIV_SET_ADDR_RES_ENABLE_IND, /*!< \brief Address resolving enable set */
+       DM_REM_CONN_PARAM_REQ_IND, /*!< \brief Remote connection parameter requested */
+       DM_CONN_DATA_LEN_CHANGE_IND, /*!< \brief Data length changed */
+       DM_CONN_WRITE_AUTH_TO_IND, /*!< \brief Write authenticated payload complete */
+       DM_CONN_AUTH_TO_EXPIRED_IND, /*!< \brief Authenticated payload timeout expired */
+       DM_PHY_READ_IND,             /*!< \brief Read PHY */
+       DM_PHY_SET_DEF_IND,          /*!< \brief Set default PHY */
+       DM_PHY_UPDATE_IND,           /*!< \brief PHY update */
+       DM_ADV_SET_START_IND,  /*!< \brief Advertising set(s) started */
+       DM_ADV_SET_STOP_IND,   /*!< \brief Advertising set(s) stopped */
+       DM_SCAN_REQ_RCVD_IND,  /*!< \brief Scan request received */
+       DM_EXT_SCAN_START_IND, /*!< \brief Extended scanning started */
+       DM_EXT_SCAN_STOP_IND,  /*!< \brief Extended scanning stopped */
+       DM_EXT_SCAN_REPORT_IND, /*!< \brief Extended scan data received from peer device */
+       DM_PER_ADV_SET_START_IND, /*!< \brief Periodic advertising set started */
+       DM_PER_ADV_SET_STOP_IND,  /*!< \brief Periodic advertising set stopped */
+       DM_PER_ADV_SYNC_EST_IND, /*!< \brief Periodic advertising sync established */
+       DM_PER_ADV_SYNC_EST_FAIL_IND, /*!< \brief Periodic advertising sync establishment failed */
+       DM_PER_ADV_SYNC_LOST_IND, /*!< \brief Periodic advertising sync lost */
+       DM_PER_ADV_SYNC_TRSF_EST_IND, /*!< \brief Periodic advertising sync transfer established */
+       DM_PER_ADV_SYNC_TRSF_EST_FAIL_IND, /*!< \brief Periodic advertising sync transfer establishment failed */
+       DM_PER_ADV_SYNC_TRSF_IND, /*!< \brief Periodic advertising sync info transferred */
+       DM_PER_ADV_SET_INFO_TRSF_IND, /*!< \brief Periodic advertising set sync info transferred */
+       DM_PER_ADV_REPORT_IND, /*!< \brief Periodic advertising data received from peer device */
+       DM_REMOTE_FEATURES_IND, /*!< \brief Remote features from peer device */
+       DM_READ_REMOTE_VER_INFO_IND, /*!< \brief Remote LL version information read */
+       DM_CONN_IQ_REPORT_IND, /*!< \brief IQ samples from CTE of received packet from peer device */
+       DM_CTE_REQ_FAIL_IND,             /*!< \brief CTE request failed */
+       DM_CONN_CTE_RX_SAMPLE_START_IND, /*!< \brief Sampling received CTE started */
+       DM_CONN_CTE_RX_SAMPLE_STOP_IND, /*!< \brief Sampling received CTE stopped */
+       DM_CONN_CTE_TX_CFG_IND, /*!< \brief Connection CTE transmit parameters configured */
+       DM_CONN_CTE_REQ_START_IND, /*!< \brief Initiating connection CTE request started */
+       DM_CONN_CTE_REQ_STOP_IND, /*!< \brief Initiating connection CTE request stopped */
+       DM_CONN_CTE_RSP_START_IND, /*!< \brief Responding to connection CTE request started */
+       DM_CONN_CTE_RSP_STOP_IND, /*!< \brief Responding to connection CTE request stopped */
+       DM_READ_ANTENNA_INFO_IND, /*!< \brief Antenna information read */
+       DM_L2C_CMD_REJ_IND,       /*!< \brief L2CAP Command Reject */
+       DM_ERROR_IND,             /*!< \brief General error */
+       DM_HW_ERROR_IND,          /*!< \brief Hardware error */
+       DM_VENDOR_SPEC_IND        /*!< \brief Vendor specific event */
+};
+
+#define ATTS_HANDLE_VALUE_CNF 15
+
+
diff --git a/pycardium/modules/sys_ble.c b/pycardium/modules/sys_ble.c
index 7b95d8d4d9081a72ed687eeb0c40fd750589e09e..b491f9e148d51d1df6d579fcd636f527e3c2df79 100644
--- a/pycardium/modules/sys_ble.c
+++ b/pycardium/modules/sys_ble.c
@@ -84,7 +84,12 @@ static MP_DEFINE_CONST_FUN_OBJ_0(
 
 static mp_obj_t mp_ble_get_event(void)
 {
-	return mp_obj_new_int(epic_ble_get_event());
+	struct epic_ble_event e;
+	if (epic_ble_get_event(&e) >= 0) {
+		epic_ble_free_event(&e);
+		return mp_obj_new_int(e.type);
+	}
+	return mp_obj_new_int(BLE_EVENT_NONE);
 }
 static MP_DEFINE_CONST_FUN_OBJ_0(ble_get_event_obj, mp_ble_get_event);
 
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 8749da69403d4eeadd7e799f612c94662bc1d5fd..1bcb79a1804deb525996192b5ec822d2df42a034 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -77,6 +77,7 @@ int mp_hal_csprng_read_int(void);
 #define MODULE_CONFIG_ENABLED               (1)
 #define MODULE_BLE_ENABLED                  (1)
 
+#define MICROPY_BLUETOOTH_CARD10            (1)
 /*
  * This port is intended to be 32-bit, but unfortunately, int32_t for
  * different targets may be defined in different ways - either as int
@@ -112,9 +113,18 @@ typedef long mp_off_t;
 #define EPIC_INT_NUM 1
 #endif
 
+#if MICROPY_BLUETOOTH_CARD10
+struct _mp_bluetooth_card10_root_pointers_t;
+#define MICROPY_PORT_ROOT_POINTER_BLUETOOTH_CARD10 struct _mp_bluetooth_card10_root_pointers_t *bluetooth_card10_root_pointers;
+#else
+#define MICROPY_PORT_ROOT_POINTER_BLUETOOTH_CARD10
+#endif
+
+
 /* For some reason, we need to define readline history manually */
 #define MICROPY_PORT_ROOT_POINTERS \
     const char *readline_hist[16]; \
     mp_obj_t interrupt_callbacks[EPIC_INT_NUM]; \
     void *spo2_memory; \
+    MICROPY_PORT_ROOT_POINTER_BLUETOOTH_CARD10 \
 
diff --git a/pycardium/mphalport.c b/pycardium/mphalport.c
index 0c5c5b2cc84135584aa74df1253dc9b9f5f15c8c..49fbf9232d5c93527cdfe21f7948b7e7f9fa8419 100644
--- a/pycardium/mphalport.c
+++ b/pycardium/mphalport.c
@@ -232,7 +232,7 @@ static void systick_delay_sleep(uint32_t us)
 		 * One example of this happeing is the KeyboardInterrupt
 		 * (CTRL+C) which will abort the running code and exit to REPL.
 		 */
-		mp_handle_pending();
+		mp_handle_pending(true);
 	}
 }