diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 96fbbbeab7d5b1201587ea3a6904089ab795a8a9..39cf79daadc761ff3160b4a26c7cc6c0d422b033 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -162,6 +162,13 @@ 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    0x161
+#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
 /* clang-format on */
 
 typedef uint32_t api_int_id_t;
@@ -2499,5 +2506,12 @@ 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));
 
+API(API_BLE_ATTS_DYN_CREATE_GROUP, void *AttsDynCreateGroup(uint16_t startHandle, uint16_t endHandle));
+API(API_BLE_ATTS_DYN_DELETE_GROUP, void AttsDynDeleteGroup(void *pSvcHandle));
+API(API_BLE_ATTS_DYN_ADD_ATTR_DYN, void AttsDynAddAttrDyn(void *pSvcHandle, const uint8_t *pUuid, uint8_t uuidLen, const uint8_t *pValue, uint16_t len, uint16_t maxLen, uint8_t settings, uint8_t permissions));
+API(API_BLE_ATTS_DYN_ADD_ATTR, void AttsDynAddAttr(void *pSvcHandle, const uint8_t *pUuid, const uint8_t *pValue, uint16_t len, uint16_t maxLen, uint8_t settings, uint8_t permissions));
+API(API_BLE_ATTS_SET_ATTR, uint8_t AttsSetAttr(uint16_t handle, uint16_t valueLen, uint8_t *pValue));
+API(API_BLE_ATTS_HANDLE_VALUE_NTF, void AttsHandleValueNtf(uint8_t connId, 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));
 #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..c1c2fd29b108559e8e97e54f8422d59e9cd693e3 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,10 @@ 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);
+
 /**@}*/
 
 /** \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..0348e2d5de5d43422771fea5a8641d0f8276f27b 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,73 @@ 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);
+  }
+}
diff --git a/pycardium/modules/modbluetooth_card10.c b/pycardium/modules/modbluetooth_card10.c
index c8443e11be9576519a13e353636ff15df39e1fd0..b59b2f192af8039cefc71168cfb34386f80002cc 100644
--- a/pycardium/modules/modbluetooth_card10.c
+++ b/pycardium/modules/modbluetooth_card10.c
@@ -1,6 +1,7 @@
 #include "extmod/modbluetooth.h"
 #include "py/runtime.h"
 #include <stdint.h>
+#include <string.h>
 
 const char *const not_implemented_message =
 	"Not (yet) implemented on card10. See https://git.card10.badge.events.ccc.de/card10/firmware/-/issues/8";
@@ -13,7 +14,6 @@ static void raise(void)
 // Enables the Bluetooth stack.
 int mp_bluetooth_init(void)
 {
-	raise();
 	return 0;
 }
 
@@ -26,13 +26,12 @@ void mp_bluetooth_deinit(void)
 // Returns true when the Bluetooth stack is enabled.
 bool mp_bluetooth_is_enabled(void)
 {
-	raise();
-	return false;
+	return true;
 }
 
 bool mp_bluetooth_is_active(void)
 {
-	return false;
+	return true;
 }
 
 // Gets the MAC addr of this device in big-endian format.
@@ -73,9 +72,50 @@ 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();
 	return 0;
 }
+
+#define ATTS_SET_UUID_128                0x01    /*!< \brief Set if the UUID is 128 bits in length */
+#define ATTS_SET_WRITE_CBACK             0x02    /*!< \brief Set if the group callback is executed when
+                                                             this attribute is written by a client device */
+#define ATTS_SET_READ_CBACK              0x04    /*!< \brief Set if the group callback is executed when
+                                                             this attribute is read by a client device */
+#define ATTS_SET_VARIABLE_LEN            0x08    /*!< \brief Set if the attribute has a variable length */
+#define ATTS_SET_ALLOW_OFFSET            0x10    /*!< \brief Set if writes are allowed with an offset */
+#define ATTS_SET_CCC                     0x20    /*!< \brief Set if the attribute is a client characteristic
+                                                             configuration descriptor */
+#define ATTS_SET_ALLOW_SIGNED            0x40    /*!< \brief Set if signed writes are allowed */
+#define ATTS_SET_REQ_SIGNED              0x80    /*!< \brief Set if signed writes are required if link
+                                                             is not encrypted */
+
+#define ATTS_PERMIT_READ                 0x01    /*!< \brief Set if attribute can be read */
+#define ATTS_PERMIT_READ_AUTH            0x02    /*!< \brief Set if attribute read requires authentication */
+#define ATTS_PERMIT_READ_AUTHORIZ        0x04    /*!< \brief Set if attribute read requires authorization */
+#define ATTS_PERMIT_READ_ENC             0x08    /*!< \brief Set if attribute read requires encryption */
+#define ATTS_PERMIT_WRITE                0x10    /*!< \brief Set if attribute can be written */
+#define ATTS_PERMIT_WRITE_AUTH           0x20    /*!< \brief Set if attribute write requires authentication */
+#define ATTS_PERMIT_WRITE_AUTHORIZ       0x40    /*!< \brief Set if attribute write requires authorization */
+#define ATTS_PERMIT_WRITE_ENC            0x80    /*!< \brief Set if attribute write requires encryption */
+
+#define ATT_PROP_BROADCAST            0x01      /*!< \brief Permit broadcasts */
+#define ATT_PROP_READ                 0x02      /*!< \brief Permit reads */
+#define ATT_PROP_WRITE_NO_RSP         0x04      /*!< \brief Permit writes without response */
+#define ATT_PROP_WRITE                0x08      /*!< \brief Permit writes with response */
+#define ATT_PROP_NOTIFY               0x10      /*!< \brief Permit notifications */
+#define ATT_PROP_INDICATE             0x20      /*!< \brief Permit indications */
+#define ATT_PROP_AUTHENTICATED        0x40      /*!< \brief Permit signed writes */
+#define ATT_PROP_EXTENDED             0x80      /*!< \brief More properties defined in extended properties */
+
+#define ATT_16_UUID_LEN                            2
+#define UINT16_TO_BYTES(n)                         ((uint8_t) (n)), ((uint8_t)((n) >> 8))
+#define ATT_UUID_PRIMARY_SERVICE                   0x2800    /*!< \brief Primary Service */
+#define ATT_UUID_CHARACTERISTIC                    0x2803    /*!< \brief Characteristic */
+#define ATT_UUID_CLIENT_CHAR_CONFIG                0x2902    /*!< \brief Client Characteristic Configuration */
+
+const uint8_t attPrimSvcUuid[ATT_16_UUID_LEN] =    {UINT16_TO_BYTES(ATT_UUID_PRIMARY_SERVICE)};
+const uint8_t attChUuid[ATT_16_UUID_LEN] =         {UINT16_TO_BYTES(ATT_UUID_CHARACTERISTIC)};
+const uint8_t attCliChCfgUuid[ATT_16_UUID_LEN] =   {UINT16_TO_BYTES(ATT_UUID_CLIENT_CHAR_CONFIG)};
+
 // // 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(
@@ -88,13 +128,80 @@ int mp_bluetooth_gatts_register_service(
 	uint16_t *handles,
 	size_t num_characteristics
 ) {
-	raise();
+	void *pSHdl;
+
+	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] & ATT_PROP_NOTIFY) {
+			// TODO: Also handle indications
+			num_ccds++;
+		}
+	}
+
+	// One handle for the service, one per characteristic, one per value, one per (CCC) descriptor
+	// TODO: Need to account for CCCDs
+	const uint16_t numHandles = 1 + num_characteristics * 2 + num_ccds + num_descriptors_total;
+
+	const uint16_t startHandle = 0x200;
+	const uint16_t endHandle = startHandle + numHandles - 1;
+
+	pSHdl = AttsDynCreateGroup(startHandle, endHandle);
+	// TODO NULL check
+
+	/* Primary service */
+	uint16_t uuid_len = service_uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 : 16;
+	uint16_t currentHandle = startHandle;
+	AttsDynAddAttr(pSHdl, attPrimSvcUuid, service_uuid->data, uuid_len, uuid_len, 0, ATTS_PERMIT_READ);
+
+	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];
+		uint8_t uuid_len = uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 : 16;
+
+
+		/* Characteristic */
+		currentHandle++;
+		uint8_t characteristic[1 + 2 + 16] = {flags, UINT16_TO_BYTES(currentHandle + 1)};
+		memcpy(characteristic + 3, uuid->data, uuid_len);
+		uint8_t characteristic_len = 1 + 2 + uuid_len;
+		AttsDynAddAttr(pSHdl, attChUuid, characteristic, characteristic_len, characteristic_len, 0, ATTS_PERMIT_READ);
+
+		/* Value */
+		currentHandle++;
+		uint8_t permissions = 0;
+		uint8_t settings = ATTS_SET_VARIABLE_LEN;
+		if(flags & ATT_PROP_READ) {
+			permissions |= ATTS_PERMIT_READ;
+		}
+		if(flags & ATT_PROP_WRITE) {
+			permissions |= ATTS_PERMIT_WRITE;
+		}
+
+		AttsDynAddAttrDyn(pSHdl, uuid->data, uuid_len, NULL, 0, MP_BLUETOOTH_DEFAULT_ATTR_LEN,
+				settings, permissions);
+		handles[handle_index] = currentHandle;
+
+		if(flags & ATT_PROP_NOTIFY) {
+			/* CCCD */
+			currentHandle++;
+			//uint8_t initCcc[] = {UINT16_TO_BYTES(0x0000)};
+			uint8_t initCcc[] = {UINT16_TO_BYTES(0x0001)};
+			AttsDynAddAttr(pSHdl, attCliChCfgUuid, initCcc, sizeof(initCcc), sizeof(initCcc),
+					//ATTS_SET_READ_CBACK | ATTS_SET_WRITE_CBACK, ATTS_PERMIT_READ | ATTS_PERMIT_WRITE);
+					0, ATTS_PERMIT_READ | ATTS_PERMIT_WRITE);
+		}
+
+		handle_index++;
+	}
+
 	return 0;
 }
 // Register any queued services.
 int mp_bluetooth_gatts_register_service_end()
 {
-	raise();
 	return 0;
 }
 
@@ -102,15 +209,18 @@ 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;
+	uint16_t pLen;
+	uint8_t ret = AttsGetAttr(value_handle, &pLen, value);
+
+	*value_len = pLen;
+	return ret;
 }
 // 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;
+	uint8_t ret = AttsSetAttr(value_handle, value_len, (uint8_t *)value);
+	return ret;
 }
 // Notify the central that it should do a read.
 int mp_bluetooth_gatts_notify(uint16_t conn_handle, uint16_t value_handle)
@@ -125,7 +235,7 @@ int mp_bluetooth_gatts_notify_send(
 	const uint8_t *value,
 	size_t value_len
 ) {
-	raise();
+	AttsHandleValueNtf(conn_handle, value_handle, value_len, (uint8_t *)value);
 	return 0;
 }
 // Indicate the central.