diff --git a/epicardium/ble/epic_att_api.c b/epicardium/ble/epic_att_api.c
index 74faa187d50ec813083fdc84d17ef1ac75f220bf..bcdd420ad00337039f366ef33c5bb609782960fc 100644
--- a/epicardium/ble/epic_att_api.c
+++ b/epicardium/ble/epic_att_api.c
@@ -25,15 +25,29 @@ 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 (att_event->hdr.event != ATTS_HANDLE_VALUE_CNF ||
+	    (att_event->handle >= ATTS_DYN_START_HANDLE &&
+	     att_event->handle < next_handle)) {
+		size_t value_len = 0;
+		if (att_event->hdr.event == ATTC_READ_BY_GROUP_TYPE_RSP ||
+		    att_event->hdr.event == ATTC_READ_BY_TYPE_RSP ||
+		    att_event->hdr.event == ATTC_FIND_INFO_RSP ||
+		    att_event->hdr.event == ATTC_HANDLE_VALUE_NTF ||
+		    att_event->hdr.event == ATTC_HANDLE_VALUE_IND) {
+			value_len = att_event->valueLen;
+		}
+		attEvt_t *e = WsfBufAlloc(sizeof(*e) + value_len);
 
 		if (e) {
 			memcpy(e, att_event, sizeof(*e));
+			memcpy(e + 1, att_event->pValue, value_len);
 			ble_epic_ble_api_trigger_event(BLE_EVENT_ATT_EVENT, e);
 		} else {
-			LOG_WARN("ble", "could not allocate att event");
+			LOG_WARN(
+				"ble",
+				"could not allocate att event of size %d",
+				sizeof(*e) + att_event->valueLen
+			);
 		}
 	}
 }
@@ -278,3 +292,70 @@ int epic_ble_atts_set_attr(
 	uint8_t ret = AttsSetAttr(handle, value_len, (uint8_t *)value);
 	return ret;
 }
+
+int epic_ble_attc_discover_primary_services(
+	uint8_t connId, const uint8_t *uuid, uint8_t uuid_len
+) {
+	if (uuid_len == 0 || uuid == NULL) {
+		AttcReadByGroupTypeReq(
+			connId, 1, 0xFFFF, 2, (uint8_t *)attPrimSvcUuid, TRUE
+		);
+	} else {
+		AttcFindByTypeValueReq(
+			connId,
+			ATT_HANDLE_START,
+			ATT_HANDLE_MAX,
+			ATT_UUID_PRIMARY_SERVICE,
+			uuid_len,
+			(uint8_t *)uuid,
+			FALSE
+		);
+	}
+	return 0;
+}
+
+int epic_ble_attc_discover_characteristics(
+	uint8_t connId, uint16_t start_handle, uint16_t end_handle
+) {
+	AttcReadByTypeReq(
+		connId,
+		start_handle,
+		end_handle,
+		ATT_16_UUID_LEN,
+		(uint8_t *)attChUuid,
+		TRUE
+	);
+	return 0;
+}
+
+int epic_ble_attc_discover_descriptors(
+	uint8_t connId, uint16_t start_handle, uint16_t end_handle
+) {
+	AttcFindInfoReq(connId, start_handle, end_handle, TRUE);
+	return 0;
+}
+
+int epic_ble_attc_read(uint8_t connId, uint16_t value_handle)
+{
+	AttcReadReq(connId, value_handle);
+	return 0;
+}
+
+int epic_ble_attc_write_no_rsp(
+	uint8_t connId,
+	uint16_t value_handle,
+	const uint8_t *value,
+	uint16_t value_len
+) {
+	AttcWriteCmd(connId, value_handle, value_len, (uint8_t *)value);
+	return 0;
+}
+int epic_ble_attc_write(
+	uint8_t connId,
+	uint16_t value_handle,
+	const uint8_t *value,
+	uint16_t value_len
+) {
+	AttcWriteReq(connId, value_handle, value_len, (uint8_t *)value);
+	return 0;
+}
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 75901978e91b0c8104bd4d70a3c4c21f5669a585..2cbe812e426020c21a25afe6c1b6043444a80a95 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -182,6 +182,12 @@ typedef _Bool bool;
 #define API_BLE_GET_ADDRESS                   0x184
 #define API_BLE_ADVERTISE                     0x185
 #define API_BLE_ADVERTISE_STOP                0x186
+#define API_BLE_DISCOVER_PRIMARY_SERVICES     0x187
+#define API_BLE_DISCOVER_CHARACTERISTICS      0x188
+#define API_BLE_DISCOVER_DESCRIPTORS          0x189
+#define API_BLE_ATTC_READ                     0x18A
+#define API_BLE_ATTC_WRITE_NO_RSP             0x18B
+#define API_BLE_ATTC_WRITE                    0x18C
 
 /* clang-format on */
 
@@ -2666,5 +2672,11 @@ API(API_BLE_GET_ADDRESS, void epic_ble_get_address(uint8_t *addr));
 API(API_BLE_ADVERTISE, int epic_ble_advertise(int interval_us, const uint8_t *adv_data, size_t adv_data_len, const uint8_t *sr_data, size_t sr_data_len, bool connectable));
 API(API_BLE_ADVERTISE_STOP, int epic_ble_advertise_stop(void));
 
+API(API_BLE_DISCOVER_PRIMARY_SERVICES, int epic_ble_attc_discover_primary_services(uint8_t connId, const uint8_t *uuid, uint8_t uuid_len));
+API(API_BLE_DISCOVER_CHARACTERISTICS, int epic_ble_attc_discover_characteristics(uint8_t connId, uint16_t start_handle, uint16_t end_handle));
+API(API_BLE_DISCOVER_DESCRIPTORS, int epic_ble_attc_discover_descriptors(uint8_t connId, uint16_t start_handle, uint16_t end_handle));
+API(API_BLE_ATTC_READ, int epic_ble_attc_read(uint8_t connId, uint16_t value_handle));
+API(API_BLE_ATTC_WRITE_NO_RSP, int epic_ble_attc_write_no_rsp(uint8_t connId, uint16_t value_handle, const uint8_t *value, uint16_t value_len));
+API(API_BLE_ATTC_WRITE, int epic_ble_attc_write(uint8_t connId, uint16_t value_handle, const uint8_t *value, uint16_t value_len));
 #endif /* _EPICARDIUM_H */
 
diff --git a/pycardium/modules/modbluetooth_card10.c b/pycardium/modules/modbluetooth_card10.c
index 4a659f93d42fa430af82e18d5ed8bc2c7b08248d..91e2e399ee464b7b32f82ff7fcba7a18223c8b5c 100644
--- a/pycardium/modules/modbluetooth_card10.c
+++ b/pycardium/modules/modbluetooth_card10.c
@@ -1,6 +1,7 @@
 #include "modbluetooth_card10.h"
 #include "extmod/modbluetooth.h"
 #include "py/runtime.h"
+#include "py/mperrno.h"
 #include <stdint.h>
 #include <stdio.h>
 #include <string.h>
@@ -27,6 +28,7 @@ typedef struct {
 } gatts_status_entry_t;
 
 static bool active = false;
+static mp_obj_bluetooth_uuid_t uuid_filter;
 
 static void gatts_status_create_entry(mp_gatts_db_t db, uint16_t handle)
 {
@@ -75,12 +77,195 @@ static void handle_att_event(struct epic_att_event *att_event)
 			e->notification_status = NOTIFICATION_STATUS_OVERFLOW;
 		}
 	}
+#if MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE
+	if (att_event->hdr.event == ATTC_READ_BY_GROUP_TYPE_RSP) {
+		// TODO: can we get the connection id from the event?
+
+		uint8_t *v         = att_event->pValue;
+		uint8_t entry_size = v[0];
+
+		if (att_event->hdr.status == ATT_SUCCESS) {
+			for (size_t i = 1; i < att_event->valueLen;
+			     i += entry_size) {
+				uint16_t start_handle = (v[i + 1] << 8) | v[i];
+				uint16_t end_handle =
+					(v[i + 3] << 8) | v[i + 2];
+				mp_obj_bluetooth_uuid_t service_uuid;
+				if (entry_size == 6) {
+					memcpy(service_uuid.data, v + i + 4, 2);
+					service_uuid.type =
+						MP_BLUETOOTH_UUID_TYPE_16;
+				} else if (entry_size == 20) {
+					memcpy(service_uuid.data,
+					       v + i + 4,
+					       16);
+					service_uuid.type =
+						MP_BLUETOOTH_UUID_TYPE_128;
+				}
+				mp_bluetooth_gattc_on_primary_service_result(
+					1,
+					start_handle,
+					end_handle,
+					&service_uuid
+				);
+			}
+		}
+
+		if (att_event->hdr.status != ATT_SUCCESS ||
+		    att_event->continuing == 0) {
+			uint16_t status = 0;
+			mp_bluetooth_gattc_on_discover_complete(
+				MP_BLUETOOTH_IRQ_GATTC_SERVICE_DONE, 1, status
+			);
+		}
+	}
+
+	if (att_event->hdr.event == ATTC_READ_BY_TYPE_RSP) {
+		/* if read by type successful */
+		if (att_event->hdr.status == ATT_SUCCESS) {
+			// TODO: can we get the connection id from the event?
+			uint8_t *v         = att_event->pValue;
+			uint8_t entry_size = v[0];
+
+			for (size_t i = 1; i < att_event->valueLen;
+			     i += entry_size) {
+				uint16_t def_handle = (v[i + 1] << 8) | v[i];
+				uint8_t properties  = v[i + 2];
+				uint16_t value_handle =
+					(v[i + 4] << 8) | v[i + 3];
+				mp_obj_bluetooth_uuid_t characteristic_uuid;
+				if (entry_size == 2 + 1 + 2 + 2) {
+					memcpy(characteristic_uuid.data,
+					       v + i + 5,
+					       2);
+					characteristic_uuid.type =
+						MP_BLUETOOTH_UUID_TYPE_16;
+				} else if (entry_size == 2 + 1 + 2 + 16) {
+					memcpy(characteristic_uuid.data,
+					       v + i + 5,
+					       16);
+					characteristic_uuid.type =
+						MP_BLUETOOTH_UUID_TYPE_128;
+				}
+
+				// TODO: uuid_filter is set: compare against uuid
+				mp_bluetooth_gattc_on_characteristic_result(
+					1,
+					def_handle,
+					value_handle,
+					properties,
+					&characteristic_uuid
+				);
+			}
+		}
+
+		if (att_event->hdr.status != ATT_SUCCESS ||
+		    att_event->continuing == 0) {
+			uint16_t status = 0;
+			mp_bluetooth_gattc_on_discover_complete(
+				MP_BLUETOOTH_IRQ_GATTC_CHARACTERISTIC_DONE,
+				1,
+				status
+			);
+		}
+	}
+
+	if (att_event->hdr.event == ATTC_FIND_INFO_RSP) {
+		if (att_event->hdr.status == ATT_SUCCESS) {
+			// TODO: can we get the connection id from the event?
+			uint8_t *v         = att_event->pValue;
+			uint8_t entry_size = v[0] == 1 ? 4 : 18;
+
+			for (size_t i = 1; i < att_event->valueLen;
+			     i += entry_size) {
+				uint16_t descriptor_handle =
+					(v[i + 1] << 8) | v[i];
+				mp_obj_bluetooth_uuid_t descriptor_uuid;
+				if (entry_size == 2 + 2) {
+					memcpy(descriptor_uuid.data,
+					       v + i + 2,
+					       2);
+					descriptor_uuid.type =
+						MP_BLUETOOTH_UUID_TYPE_16;
+				} else if (entry_size == 2 + 16) {
+					memcpy(descriptor_uuid.data,
+					       v + i + 2,
+					       16);
+					descriptor_uuid.type =
+						MP_BLUETOOTH_UUID_TYPE_128;
+				}
+
+				mp_bluetooth_gattc_on_descriptor_result(
+					1, descriptor_handle, &descriptor_uuid
+				);
+			}
+		}
+
+		if (att_event->hdr.status != ATT_SUCCESS ||
+		    att_event->continuing == 0) {
+			uint16_t status = 0;
+			mp_bluetooth_gattc_on_discover_complete(
+				MP_BLUETOOTH_IRQ_GATTC_DESCRIPTOR_DONE,
+				1,
+				status
+			);
+		}
+	}
+
+	if (att_event->hdr.event == ATTC_READ_RSP) {
+		mp_uint_t atomic_state;
+		size_t len = mp_bluetooth_gattc_on_data_available_start(
+			MP_BLUETOOTH_IRQ_GATTC_READ_RESULT,
+			1,
+			att_event->handle,
+			att_event->valueLen,
+			&atomic_state
+		);
+		mp_bluetooth_gattc_on_data_available_chunk(
+			att_event->pValue, len
+		);
+		mp_bluetooth_gattc_on_data_available_end(atomic_state);
+		mp_bluetooth_gattc_on_read_write_status(
+			MP_BLUETOOTH_IRQ_GATTC_READ_DONE,
+			1,
+			att_event->handle,
+			att_event->hdr.status
+		);
+	}
+
+	if (att_event->hdr.event == ATTC_HANDLE_VALUE_NTF ||
+	    att_event->hdr.event == ATTC_HANDLE_VALUE_IND) {
+		uint16_t ev = att_event->hdr.event == ATTC_HANDLE_VALUE_NTF ?
+				      MP_BLUETOOTH_IRQ_GATTC_NOTIFY :
+				      MP_BLUETOOTH_IRQ_GATTC_INDICATE;
+		mp_uint_t atomic_state;
+		size_t len = mp_bluetooth_gattc_on_data_available_start(
+			ev,
+			1,
+			att_event->handle,
+			att_event->valueLen,
+			&atomic_state
+		);
+		mp_bluetooth_gattc_on_data_available_chunk(
+			att_event->pValue, len
+		);
+		mp_bluetooth_gattc_on_data_available_end(atomic_state);
+	}
+
+	if (att_event->hdr.event == ATTC_WRITE_RSP) {
+		mp_bluetooth_gattc_on_read_write_status(
+			MP_BLUETOOTH_IRQ_GATTC_WRITE_DONE,
+			1,
+			att_event->handle,
+			att_event->hdr.status
+		);
+	}
+#endif
 }
 
 static void handle_dm_event(struct epic_dm_event *dm_event)
 {
 	struct epic_wsf_header *hdr = (struct epic_wsf_header *)dm_event;
-
 	if (hdr->event == DM_CONN_OPEN_IND) {
 		struct epic_hciLeConnCmpl_event *e =
 			(struct epic_hciLeConnCmpl_event *)dm_event;
@@ -508,7 +693,10 @@ static void raise(void)
 
 // Start a discovery (scan). Set duration to zero to run continuously.
 int mp_bluetooth_gap_scan_start(
-	int32_t duration_ms, int32_t interval_us, int32_t window_us
+	int32_t duration_ms,
+	int32_t interval_us,
+	int32_t window_us,
+	bool active_scan
 ) {
 	raise();
 	return 0;
@@ -530,33 +718,52 @@ int mp_bluetooth_gap_peripheral_connect(
 }
 
 // Find all primary services on the connected peripheral.
-int mp_bluetooth_gattc_discover_primary_services(uint16_t conn_handle)
-{
-	raise();
-	return 0;
+int mp_bluetooth_gattc_discover_primary_services(
+	uint16_t conn_handle, const mp_obj_bluetooth_uuid_t *uuid
+) {
+	if (uuid) {
+		uint8_t uuid_len =
+			uuid->type == MP_BLUETOOTH_UUID_TYPE_16 ? 2 : 16;
+		return epic_ble_attc_discover_primary_services(
+			conn_handle, uuid->data, uuid_len
+		);
+	} else {
+		return epic_ble_attc_discover_primary_services(
+			conn_handle, NULL, 0
+		);
+	}
 }
 
 // Find all characteristics on the specified service on a connected peripheral.
 int mp_bluetooth_gattc_discover_characteristics(
-	uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle
+	uint16_t conn_handle,
+	uint16_t start_handle,
+	uint16_t end_handle,
+	const mp_obj_bluetooth_uuid_t *uuid
 ) {
-	raise();
-	return 0;
+	if (uuid) {
+		uuid_filter = *uuid;
+	} else {
+		uuid_filter.type = 0;
+	}
+	return epic_ble_attc_discover_characteristics(
+		conn_handle, start_handle, end_handle
+	);
 }
 
 // Find all descriptors on the specified characteristic on a connected peripheral.
 int mp_bluetooth_gattc_discover_descriptors(
 	uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle
 ) {
-	raise();
-	return 0;
+	return epic_ble_attc_discover_descriptors(
+		conn_handle, start_handle, end_handle
+	);
 }
 
 // Initiate read of a value from the remote peripheral.
 int mp_bluetooth_gattc_read(uint16_t conn_handle, uint16_t value_handle)
 {
-	raise();
-	return 0;
+	return epic_ble_attc_read(conn_handle, value_handle);
 }
 
 // Write the value to the remote peripheral.
@@ -567,7 +774,18 @@ int mp_bluetooth_gattc_write(
 	size_t *value_len,
 	unsigned int mode
 ) {
-	raise();
-	return 0;
+	int err;
+	if (mode == MP_BLUETOOTH_WRITE_MODE_NO_RESPONSE) {
+		err = epic_ble_attc_write_no_rsp(
+			conn_handle, value_handle, value, *value_len
+		);
+	} else if (mode == MP_BLUETOOTH_WRITE_MODE_WITH_RESPONSE) {
+		err = epic_ble_attc_write(
+			conn_handle, value_handle, value, *value_len
+		);
+	} else {
+		err = MP_EINVAL;
+	}
+	return err;
 }
 #endif
diff --git a/pycardium/modules/modbluetooth_card10.h b/pycardium/modules/modbluetooth_card10.h
index 0e2296a7e99fcfa862e2721f5c947dd139afb923..880cc37244ec4cf26d59929555daa8085cad1f26 100644
--- a/pycardium/modules/modbluetooth_card10.h
+++ b/pycardium/modules/modbluetooth_card10.h
@@ -126,6 +126,39 @@ enum { DM_RESET_CMPL_IND = DM_CBACK_START, /*!< \brief Reset complete */
        DM_VENDOR_SPEC_IND        /*!< \brief Vendor specific event */
 };
 
-#define ATTS_HANDLE_VALUE_CNF 15
+/** \name ATT Callback Events
+ * Events related to ATT transactions.
+ */
+/**@{*/
+#define ATT_CBACK_START                  0x02    /*!< \brief ATT callback event starting value */
+
+/*! \brief ATT client callback events */
+enum                                        /*!< \brief Internal note: event values match method values */
+{
+  ATTC_FIND_INFO_RSP = ATT_CBACK_START,     /*!< \brief Find information response */
+  ATTC_FIND_BY_TYPE_VALUE_RSP,              /*!< \brief Find by type value response */
+  ATTC_READ_BY_TYPE_RSP,                    /*!< \brief Read by type value response */
+  ATTC_READ_RSP,                            /*!< \brief Read response */
+  ATTC_READ_LONG_RSP,                       /*!< \brief Read long response */
+  ATTC_READ_MULTIPLE_RSP,                   /*!< \brief Read multiple response */
+  ATTC_READ_BY_GROUP_TYPE_RSP,              /*!< \brief Read group type response */
+  ATTC_WRITE_RSP,                           /*!< \brief Write response */
+  ATTC_WRITE_CMD_RSP,                       /*!< \brief Write command response */
+  ATTC_PREPARE_WRITE_RSP,                   /*!< \brief Prepare write response */
+  ATTC_EXECUTE_WRITE_RSP,                   /*!< \brief Execute write response */
+  ATTC_HANDLE_VALUE_NTF,                    /*!< \brief Handle value notification */
+  ATTC_HANDLE_VALUE_IND,                    /*!< \brief Handle value indication */
+  /* ATT server callback events */
+  ATTS_HANDLE_VALUE_CNF,                    /*!< \brief Handle value confirmation */
+  ATTS_CCC_STATE_IND,                       /*!< \brief Client chracteristic configuration state change */
+  ATTS_DB_HASH_CALC_CMPL_IND,               /*!< \brief Database hash calculation complete */
+  /* ATT common callback events */
+  ATT_MTU_UPDATE_IND                        /*!< \brief Negotiated MTU value */
+};
+
+/*! \brief ATT callback events */
+#define ATT_CBACK_END                    ATT_MTU_UPDATE_IND  /*!< \brief ATT callback event ending value */
+/**@}*/
+
 
 
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 1bcb79a1804deb525996192b5ec822d2df42a034..7e2a56ec31356d50f45d5511560a8188daa19a87 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -21,6 +21,7 @@
 #define MICROPY_LONGINT_IMPL                (MICROPY_LONGINT_IMPL_MPZ)
 
 #define MICROPY_ENABLE_SCHEDULER            (1)
+#define MICROPY_SCHEDULER_DEPTH             (8)
 
 #define MICROPY_ENABLE_SOURCE_LINE          (1)
 
@@ -54,6 +55,7 @@ int mp_hal_csprng_read_int(void);
 #define MICROPY_PY_FRAMEBUF                 (1)
 #define MICROPY_PY_BLUETOOTH                (1)
 #define MICROPY_PY_BUILTINS_MEMORYVIEW      (1)
+#define MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE (1)
 
 /* Modules */
 #define MODULE_BHI160_ENABLED               (1)