diff --git a/card10-cross.ini b/card10-cross.ini
index ca8d05402ad57e3585656580a40b26748ede7e8d..8ccc99632335e4d8585fd3241c53ee912498250b 100644
--- a/card10-cross.ini
+++ b/card10-cross.ini
@@ -4,9 +4,9 @@ ar = 'arm-none-eabi-ar'
 strip = 'arm-none-eabi-strip'
 
 [properties]
-c_args      = ['-mthumb', '-mcpu=cortex-m4', '-mfloat-abi=hard', '-mfpu=fpv4-sp-d16', '-Wa,-mimplicit-it=thumb', '-ffunction-sections', '-fdata-sections', '-fsingle-precision-constant', '-fno-isolate-erroneous-paths-dereference']
+c_args      = ['-mthumb', '-mcpu=cortex-m4', '-mfloat-abi=softfp', '-mfpu=fpv4-sp-d16', '-Wa,-mimplicit-it=thumb', '-ffunction-sections', '-fdata-sections', '-fsingle-precision-constant', '-fno-isolate-erroneous-paths-dereference']
 
-c_link_args = ['-mthumb', '-mcpu=cortex-m4', '-mfloat-abi=hard', '-mfpu=fpv4-sp-d16', '-Wl,--start-group', '-lc', '-lnosys', '-Wl,--end-group', '--specs=nano.specs']
+c_link_args = ['-mthumb', '-mcpu=cortex-m4', '-mfloat-abi=softfp', '-mfpu=fpv4-sp-d16', '-Wl,--start-group', '-lc', '-lnosys', '-Wl,--end-group', '--specs=nano.specs']
 
 target_defs = ['-DTARGET=32665', '-DTARGET_REV=0x4131', '-DBOARD_CARD10=1']
 
diff --git a/epicardium/ble/app/app_main.c b/epicardium/ble/app/app_main.c
new file mode 100644
index 0000000000000000000000000000000000000000..f795c4e7222adfeb7cbe5b19e15f3c1659b7827c
--- /dev/null
+++ b/epicardium/ble/app/app_main.c
@@ -0,0 +1,423 @@
+/*************************************************************************************************/
+/*!
+ *  \file
+ *
+ *  \brief  Application framework main module.
+ *
+ *  Copyright (c) 2011-2018 Arm Ltd. All Rights Reserved.
+ *  ARM Ltd. confidential and proprietary.
+ *
+ *  IMPORTANT.  Your use of this file is governed by a Software License Agreement
+ *  ("Agreement") that must be accepted in order to download or otherwise receive a
+ *  copy of this file.  You may not use or copy this file for any purpose other than
+ *  as described in the Agreement.  If you do not agree to all of the terms of the
+ *  Agreement do not use this file and delete all copies in your possession or control;
+ *  if you do not have a copy of the Agreement, you must contact ARM Ltd. prior
+ *  to any use, copying or further distribution of this software.
+ */
+/*************************************************************************************************/
+
+/* card10:
+ * copied from: lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/apps/app/app_main.c
+ *
+ * Reason: we need to correctly implement AppHandleNumericComparison
+ */
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+#include <string.h>
+#include "wsf_types.h"
+#include "wsf_msg.h"
+#include "sec_api.h"
+#include "wsf_trace.h"
+#include "wsf_timer.h"
+#include "wsf_assert.h"
+#include "util/bstream.h"
+#include "dm_api.h"
+#include "app_api.h"
+#include "app_main.h"
+#include "app_ui.h"
+
+/**************************************************************************************************
+  Global Variables
+**************************************************************************************************/
+
+/*! Configuration pointer for advertising */
+appAdvCfg_t *pAppAdvCfg;
+
+/*! Configuration pointer for extended and periodic advertising */
+appExtAdvCfg_t *pAppExtAdvCfg;
+
+/*! Configuration pointer for slave */
+appSlaveCfg_t *pAppSlaveCfg;
+
+/*! Configuration pointer for master */
+appMasterCfg_t *pAppMasterCfg;
+
+/*! Configuration pointer for extended master */
+appExtMasterCfg_t *pAppExtMasterCfg;
+
+/*! Configuration pointer for security */
+appSecCfg_t *pAppSecCfg;
+
+/*! Configuration pointer for connection parameter update */
+appUpdateCfg_t *pAppUpdateCfg;
+
+/*! Configuration pointer for discovery */
+appDiscCfg_t *pAppDiscCfg;
+
+/*! Configuration pointer for application */
+appCfg_t *pAppCfg;
+
+/*! Connection control block array */
+appConnCb_t appConnCb[DM_CONN_MAX];
+
+/*! WSF handler ID */
+wsfHandlerId_t appHandlerId;
+
+/*! Main control block */
+appCb_t appCb;
+
+/*! Configuration structure for incoming request actions */
+const appReqActCfg_t appReqActCfg =
+{
+  APP_ACT_ACCEPT        /*! Action for the remote connection parameter request */
+};
+
+/*! Configuration pointer for incoming request actions on master */
+appReqActCfg_t *pAppMasterReqActCfg = (appReqActCfg_t *) &appReqActCfg;
+
+/*! Configurable pointer for incoming request actions on slave */
+appReqActCfg_t *pAppSlaveReqActCfg = (appReqActCfg_t *) &appReqActCfg;
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Process messages from the event handler.
+ *
+ *  \param  pMsg    Pointer to message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void appProcMsg(wsfMsgHdr_t *pMsg)
+{
+  switch(pMsg->event)
+  {
+    case APP_BTN_POLL_IND:
+      appUiBtnPoll();
+      break;
+
+    case APP_UI_TIMER_IND:
+      appUiTimerExpired(pMsg);
+      break;
+
+    default:
+      break;
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Check the bonded state of a connection.
+ *
+ *  \param  connId      DM connection ID.
+ *
+ *  \return Bonded state.
+ */
+/*************************************************************************************************/
+bool_t appCheckBonded(dmConnId_t connId)
+{
+  WSF_ASSERT((connId > 0) && (connId <= DM_CONN_MAX));
+
+  return appConnCb[connId - 1].bonded;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Check the bond-by-LTK state of a connection.
+ *
+ *  \param  connId      DM connection ID.
+ *
+ *  \return Bond-by-LTK state.
+ */
+/*************************************************************************************************/
+bool_t appCheckBondByLtk(dmConnId_t connId)
+{
+  WSF_ASSERT((connId > 0) && (connId <= DM_CONN_MAX));
+
+  return appConnCb[connId - 1].bondByLtk;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Return the number of existing connections of the given role.
+ *
+ *  \param  role      Connection role
+ *
+ *  \return Number of connections.
+ */
+/*************************************************************************************************/
+uint8_t appNumConns(uint8_t role)
+{
+  appConnCb_t   *pCcb = appConnCb;
+  uint8_t       i, j;
+
+  for (i = DM_CONN_MAX, j = 0; i > 0; i--, pCcb++)
+  {
+    if ((pCcb->connId != DM_CONN_ID_NONE) && (DmConnRole(pCcb->connId) == role))
+    {
+      j++;
+    }
+  }
+
+  return j;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  App framework handler init function called during system initialization.
+ *
+ *  \param  handlerID  WSF handler ID for App.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppInit(void)
+{
+  appHandlerId = WsfOsSetNextHandler(AppHandler);
+
+  AppDbInit();
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  WSF event handler for app framework.
+ *
+ *  \param  event   WSF event mask.
+ *  \param  pMsg    WSF message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg)
+{
+  if (pMsg != NULL)
+  {
+    APP_TRACE_INFO1("App got evt %d", pMsg->event);
+
+    if (pMsg->event >= APP_MASTER_MSG_START)
+    {
+      /* pass event to master handler */
+      (*appCb.masterCback)(pMsg);
+    }
+    else if (pMsg->event >= APP_SLAVE_MSG_START)
+    {
+      /* pass event to slave handler */
+      (*appCb.slaveCback)(pMsg);
+    }
+    else
+    {
+      appProcMsg(pMsg);
+    }
+  }
+  else
+  {
+    if (event & APP_BTN_DOWN_EVT)
+    {
+      AppUiBtnPressed();
+    }
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Handle a passkey request during pairing.  If the passkey is to displayed, a
+ *          random passkey is generated and displayed.  If the passkey is to be entered
+ *          the user is prompted to enter the passkey.
+ *
+ *  \param  pAuthReq  DM authentication requested event structure.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppHandlePasskey(dmSecAuthReqIndEvt_t *pAuthReq)
+{
+  uint32_t passkey;
+  uint8_t  buf[SMP_PIN_LEN];
+
+  if (pAuthReq->display)
+  {
+    /* generate random passkey, limit to 6 digit max */
+    SecRand((uint8_t *) &passkey, sizeof(uint32_t));
+    passkey %= 1000000;
+
+    /* convert to byte buffer */
+    buf[0] = UINT32_TO_BYTE0(passkey);
+    buf[1] = UINT32_TO_BYTE1(passkey);
+    buf[2] = UINT32_TO_BYTE2(passkey);
+
+    /* send authentication response to DM */
+    DmSecAuthRsp((dmConnId_t) pAuthReq->hdr.param, SMP_PIN_LEN, buf);
+
+    /* display passkey */
+    AppUiDisplayPasskey(passkey);
+  }
+  else
+  {
+    /* prompt user to enter passkey */
+    AppUiAction(APP_UI_PASSKEY_PROMPT);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+*  \brief  Handle a numeric comparison indication during pairing.  The confirmation value is
+*          displayed and the user is prompted to verify that the local and peer confirmation
+*          values match.
+*
+*  \param  pCnfInd  DM confirmation indication event structure.
+*
+*  \return None.
+*/
+/*************************************************************************************************/
+void AppHandleNumericComparison(dmSecCnfIndEvt_t *pCnfInd)
+{
+  uint32_t confirm = DmSecGetCompareValue(pCnfInd->confirm);
+
+  /* display confirmation value */
+  AppUiDisplayConfirmValue(confirm);
+
+  /* TODO: Verify that local and peer confirmation values match */
+  DmSecCompareRsp((dmConnId_t)pCnfInd->hdr.param, TRUE);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Close the connection with the give connection identifier.
+ *
+ *  \param  connId    Connection identifier.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppConnClose(dmConnId_t connId)
+{
+  DmConnClose(DM_CLIENT_ID_APP, connId, HCI_ERR_REMOTE_TERMINATED);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get a list of connection identifiers of open connections.
+ *
+ *  \param  pConnIdList    Buffer to hold connection IDs (must be DM_CONN_MAX bytes).
+ *
+ *  \return Number of open connections.
+ *
+ */
+/*************************************************************************************************/
+uint8_t AppConnOpenList(dmConnId_t *pConnIdList)
+{
+  appConnCb_t   *pCcb = appConnCb;
+  uint8_t       i;
+  uint8_t       pos = 0;
+
+  memset(pConnIdList, DM_CONN_ID_NONE, DM_CONN_MAX);
+
+  for (i = DM_CONN_MAX; i > 0; i--, pCcb++)
+  {
+    if (pCcb->connId != DM_CONN_ID_NONE)
+    {
+      pConnIdList[pos++] = pCcb->connId;
+    }
+  }
+
+  return pos;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Check if a connection is open.
+ *
+ *  \return Connection ID of open connection or DM_CONN_ID_NONE if no open connections.
+ */
+/*************************************************************************************************/
+dmConnId_t AppConnIsOpen(void)
+{
+  appConnCb_t   *pCcb = appConnCb;
+  uint8_t       i;
+
+  for (i = DM_CONN_MAX; i > 0; i--, pCcb++)
+  {
+    if (pCcb->connId != DM_CONN_ID_NONE)
+    {
+      return pCcb->connId;
+    }
+  }
+
+  return DM_CONN_ID_NONE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the device database record handle associated with an open connection.
+ *
+ *  \param  connId    Connection identifier.
+ *
+ *  \return Database record handle or APP_DB_HDL_NONE.
+ */
+/*************************************************************************************************/
+appDbHdl_t AppDbGetHdl(dmConnId_t connId)
+{
+  return appConnCb[connId-1].dbHdl;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Add device to resolving list.
+ *
+ *  \param  pMsg    Pointer to DM callback event message.
+ *  \param  connId  Connection identifier.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppAddDevToResList(dmEvt_t *pMsg, dmConnId_t connId)
+{
+  dmSecKey_t *pPeerKey;
+  appDbHdl_t hdl = appConnCb[connId - 1].dbHdl;
+
+  /* if LL Privacy is supported and the peer device has distributed its IRK */
+  if (HciLlPrivacySupported() && ((pPeerKey = AppDbGetKey(hdl, DM_KEY_IRK, NULL))!= NULL))
+  {
+    /* add peer device to resolving list. If all-zero local or peer IRK is used then
+       LL will only use or accept local or peer identity address respectively. */
+    DmPrivAddDevToResList(pPeerKey->irk.addrType, pPeerKey->irk.bdAddr, pPeerKey->irk.key,
+                          DmSecGetLocalIrk(), TRUE, pMsg->hdr.param);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Update privacy mode for a given peer device.
+ *
+ *  \param  hdl     Database record handle.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUpdatePrivacyMode(appDbHdl_t hdl)
+{
+  /* if peer device's been added to resolving list but RPA Only attribute not found on peer device */
+  if ((hdl != APP_DB_HDL_NONE) && AppDbGetPeerAddedToRl(hdl) && !AppDbGetPeerRpao(hdl))
+  {
+    dmSecKey_t *pPeerKey = AppDbGetKey(hdl, DM_KEY_IRK, NULL);
+    if (pPeerKey != NULL)
+    {
+      /* set device privacy mode for this peer device */
+      DmPrivSetPrivacyMode(pPeerKey->irk.addrType, pPeerKey->irk.bdAddr, DM_PRIV_MODE_DEVICE);
+
+      /* make sure resolving list flag cleared */
+      AppDbSetPeerAddedToRl(hdl, FALSE);
+    }
+  }
+}
+/* clang-format on */
diff --git a/epicardium/ble/app/common/app_db.c b/epicardium/ble/app/common/app_db.c
new file mode 100644
index 0000000000000000000000000000000000000000..e4428f50751fd4407b3397c7111381314fd5ff45
--- /dev/null
+++ b/epicardium/ble/app/common/app_db.c
@@ -0,0 +1,713 @@
+/*************************************************************************************************/
+/*!
+ *  \file
+ *
+ *  \brief  Application framework device database example, using simple RAM-based storage.
+ *
+ *  Copyright (c) 2011-2018 Arm Ltd. All Rights Reserved.
+ *  ARM Ltd. confidential and proprietary.
+ *
+ *  IMPORTANT.  Your use of this file is governed by a Software License Agreement
+ *  ("Agreement") that must be accepted in order to download or otherwise receive a
+ *  copy of this file.  You may not use or copy this file for any purpose other than
+ *  as described in the Agreement.  If you do not agree to all of the terms of the
+ *  Agreement do not use this file and delete all copies in your possession or control;
+ *  if you do not have a copy of the Agreement, you must contact ARM Ltd. prior
+ *  to any use, copying or further distribution of this software.
+ */
+/*************************************************************************************************/
+
+/* card10:
+ * copied from: lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/apps/app/common/app_db.c
+ *
+ * Reason: we need to implement persistent storage for pairings
+ */
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+#include <string.h>
+#include "wsf_types.h"
+#include "wsf_assert.h"
+#include "util/bda.h"
+#include "app_api.h"
+#include "app_main.h"
+#include "app_db.h"
+#include "app_cfg.h"
+
+/**************************************************************************************************
+  Data Types
+**************************************************************************************************/
+
+/*! Database record */
+typedef struct
+{
+  /*! Common for all roles */
+  bdAddr_t    peerAddr;                     /*! Peer address */
+  uint8_t     addrType;                     /*! Peer address type */
+  dmSecIrk_t  peerIrk;                      /*! Peer IRK */
+  dmSecCsrk_t peerCsrk;                     /*! Peer CSRK */
+  uint8_t     keyValidMask;                 /*! Valid keys in this record */
+  bool_t      inUse;                        /*! TRUE if record in use */
+  bool_t      valid;                        /*! TRUE if record is valid */
+  bool_t      peerAddedToRl;                /*! TRUE if peer device's been added to resolving list */
+  bool_t      peerRpao;                     /*! TRUE if RPA Only attribute's present on peer device */
+
+  /*! For slave local device */
+  dmSecLtk_t  localLtk;                     /*! Local LTK */
+  uint8_t     localLtkSecLevel;             /*! Local LTK security level */
+  bool_t      peerAddrRes;                  /*! TRUE if address resolution's supported on peer device (master) */
+
+  /*! For master local device */
+  dmSecLtk_t  peerLtk;                      /*! Peer LTK */
+  uint8_t     peerLtkSecLevel;              /*! Peer LTK security level */
+
+  /*! for ATT server local device */
+  uint16_t    cccTbl[APP_DB_NUM_CCCD];      /*! Client characteristic configuration descriptors */
+  uint32_t    peerSignCounter;              /*! Peer Sign Counter */
+
+  /*! for ATT client */
+  uint16_t    hdlList[APP_DB_HDL_LIST_LEN]; /*! Cached handle list */
+  uint8_t     discStatus;                   /*! Service discovery and configuration status */
+} appDbRec_t;
+
+/*! Database type */
+typedef struct
+{
+  appDbRec_t  rec[APP_DB_NUM_RECS];               /*! Device database records */
+  char        devName[ATT_DEFAULT_PAYLOAD_LEN];   /*! Device name */
+  uint8_t     devNameLen;                         /*! Device name length */
+} appDb_t;
+
+/**************************************************************************************************
+  Local Variables
+**************************************************************************************************/
+
+/*! Database */
+static appDb_t appDb;
+
+/*! When all records are allocated use this index to determine which to overwrite */
+static appDbRec_t *pAppDbNewRec = appDb.rec;
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Initialize the device database.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbInit(void)
+{
+  return;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Create a new device database record.
+ *
+ *  \param  addrType  Address type.
+ *  \param  pAddr     Peer device address.
+ *
+ *  \return Database record handle.
+ */
+/*************************************************************************************************/
+appDbHdl_t AppDbNewRecord(uint8_t addrType, uint8_t *pAddr)
+{
+  appDbRec_t  *pRec = appDb.rec;
+  uint8_t     i;
+
+  /* find a free record */
+  for (i = APP_DB_NUM_RECS; i > 0; i--, pRec++)
+  {
+    if (!pRec->inUse)
+    {
+      break;
+    }
+  }
+
+  /* if all records were allocated */
+  if (i == 0)
+  {
+    /* overwrite a record */
+    pRec = pAppDbNewRec;
+
+    /* get next record to overwrite */
+    pAppDbNewRec++;
+    if (pAppDbNewRec == &appDb.rec[APP_DB_NUM_RECS])
+    {
+      pAppDbNewRec = appDb.rec;
+    }
+  }
+
+  /* initialize record */
+  memset(pRec, 0, sizeof(appDbRec_t));
+  pRec->inUse = TRUE;
+  pRec->addrType = addrType;
+  BdaCpy(pRec->peerAddr, pAddr);
+  pRec->peerAddedToRl = FALSE;
+  pRec->peerRpao = FALSE;
+
+  return (appDbHdl_t) pRec;
+}
+
+/*************************************************************************************************/
+/*!
+*  \brief  Get the next database record for a given record. For the first record, the function
+*          should be called with 'hdl' set to 'APP_DB_HDL_NONE'.
+*
+*  \param  hdl  Database record handle.
+*
+*  \return Next record handle found. APP_DB_HDL_NONE, otherwise.
+*/
+/*************************************************************************************************/
+appDbHdl_t AppDbGetNextRecord(appDbHdl_t hdl)
+{
+  appDbRec_t  *pRec;
+
+  /* if first record is requested */
+  if (hdl == APP_DB_HDL_NONE)
+  {
+    pRec = appDb.rec;
+  }
+  /* if valid record passed in */
+  else if (AppDbRecordInUse(hdl))
+  {
+    pRec = (appDbRec_t *)hdl;
+    pRec++;
+  }
+  /* invalid record passed in */
+  else
+  {
+    return APP_DB_HDL_NONE;
+  }
+
+  /* look for next valid record */
+  while (pRec < &appDb.rec[APP_DB_NUM_RECS])
+  {
+    /* if record is in use */
+    if (pRec->inUse && pRec->valid)
+    {
+      /* record found */
+      return (appDbHdl_t)pRec;
+    }
+
+    /* look for next record */
+    pRec++;
+  }
+
+  /* end of records */
+  return APP_DB_HDL_NONE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Delete a new device database record.
+ *
+ *  \param  hdl       Database record handle.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbDeleteRecord(appDbHdl_t hdl)
+{
+  ((appDbRec_t *) hdl)->inUse = FALSE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Validate a new device database record.  This function is called when pairing is
+ *          successful and the devices are bonded.
+ *
+ *  \param  hdl       Database record handle.
+ *  \param  keyMask   Bitmask of keys to validate.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbValidateRecord(appDbHdl_t hdl, uint8_t keyMask)
+{
+  ((appDbRec_t *) hdl)->valid = TRUE;
+  ((appDbRec_t *) hdl)->keyValidMask = keyMask;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Check if a record has been validated.  If it has not, delete it.  This function
+ *          is typically called when the connection is closed.
+ *
+ *  \param  hdl       Database record handle.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbCheckValidRecord(appDbHdl_t hdl)
+{
+  if (((appDbRec_t *) hdl)->valid == FALSE)
+  {
+    AppDbDeleteRecord(hdl);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+*  \brief  Check if a database record is in use.
+
+*  \param  hdl       Database record handle.
+*
+*  \return TURE if record in use. FALSE, otherwise.
+*/
+/*************************************************************************************************/
+bool_t AppDbRecordInUse(appDbHdl_t hdl)
+{
+  appDbRec_t  *pRec = appDb.rec;
+  uint8_t     i;
+
+  /* see if record is in database record list */
+  for (i = APP_DB_NUM_RECS; i > 0; i--, pRec++)
+  {
+    if (pRec->inUse && pRec->valid && (pRec == ((appDbRec_t *)hdl)))
+    {
+      return TRUE;
+    }
+  }
+
+  return FALSE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Check if there is a stored bond with any device.
+ *
+ *  \param  hdl       Database record handle.
+ *
+ *  \return TRUE if a bonded device is found, FALSE otherwise.
+ */
+/*************************************************************************************************/
+bool_t AppDbCheckBonded(void)
+{
+  appDbRec_t  *pRec = appDb.rec;
+  uint8_t     i;
+
+  /* find a record */
+  for (i = APP_DB_NUM_RECS; i > 0; i--, pRec++)
+  {
+    if (pRec->inUse)
+    {
+      return TRUE;
+    }
+  }
+
+  return FALSE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Delete all database records.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbDeleteAllRecords(void)
+{
+  appDbRec_t  *pRec = appDb.rec;
+  uint8_t     i;
+
+  /* set in use to false for all records */
+  for (i = APP_DB_NUM_RECS; i > 0; i--, pRec++)
+  {
+    pRec->inUse = FALSE;
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Find a device database record by peer address.
+ *
+ *  \param  addrType  Address type.
+ *  \param  pAddr     Peer device address.
+ *
+ *  \return Database record handle or APP_DB_HDL_NONE if not found.
+ */
+/*************************************************************************************************/
+appDbHdl_t AppDbFindByAddr(uint8_t addrType, uint8_t *pAddr)
+{
+  appDbRec_t  *pRec = appDb.rec;
+  uint8_t     peerAddrType = DmHostAddrType(addrType);
+  uint8_t     i;
+
+  /* find matching record */
+  for (i = APP_DB_NUM_RECS; i > 0; i--, pRec++)
+  {
+    if (pRec->inUse && (pRec->addrType == peerAddrType) && BdaCmp(pRec->peerAddr, pAddr))
+    {
+      return (appDbHdl_t) pRec;
+    }
+  }
+
+  return APP_DB_HDL_NONE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Find a device database record by data in an LTK request.
+ *
+ *  \param  encDiversifier  Encryption diversifier associated with key.
+ *  \param  pRandNum        Pointer to random number associated with key.
+ *
+ *  \return Database record handle or APP_DB_HDL_NONE if not found.
+ */
+/*************************************************************************************************/
+appDbHdl_t AppDbFindByLtkReq(uint16_t encDiversifier, uint8_t *pRandNum)
+{
+  appDbRec_t  *pRec = appDb.rec;
+  uint8_t     i;
+
+  /* find matching record */
+  for (i = APP_DB_NUM_RECS; i > 0; i--, pRec++)
+  {
+    if (pRec->inUse && (pRec->localLtk.ediv == encDiversifier) &&
+        (memcmp(pRec->localLtk.rand, pRandNum, SMP_RAND8_LEN) == 0))
+    {
+      return (appDbHdl_t) pRec;
+    }
+  }
+
+  return APP_DB_HDL_NONE;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get a key from a device database record.
+ *
+ *  \param  hdl       Database record handle.
+ *  \param  type      Type of key to get.
+ *  \param  pSecLevel If the key is valid, the security level of the key.
+ *
+ *  \return Pointer to key if key is valid or NULL if not valid.
+ */
+/*************************************************************************************************/
+dmSecKey_t *AppDbGetKey(appDbHdl_t hdl, uint8_t type, uint8_t *pSecLevel)
+{
+  dmSecKey_t *pKey = NULL;
+
+  /* if key valid */
+  if ((type & ((appDbRec_t *) hdl)->keyValidMask) != 0)
+  {
+    switch(type)
+    {
+      case DM_KEY_LOCAL_LTK:
+        *pSecLevel = ((appDbRec_t *) hdl)->localLtkSecLevel;
+        pKey = (dmSecKey_t *) &((appDbRec_t *) hdl)->localLtk;
+        break;
+
+      case DM_KEY_PEER_LTK:
+        *pSecLevel = ((appDbRec_t *) hdl)->peerLtkSecLevel;
+        pKey = (dmSecKey_t *) &((appDbRec_t *) hdl)->peerLtk;
+        break;
+
+      case DM_KEY_IRK:
+        pKey = (dmSecKey_t *)&((appDbRec_t *)hdl)->peerIrk;
+        break;
+
+      case DM_KEY_CSRK:
+        pKey = (dmSecKey_t *)&((appDbRec_t *)hdl)->peerCsrk;
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  return pKey;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set a key in a device database record.
+ *
+ *  \param  hdl       Database record handle.
+ *  \param  pKey      Key data.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetKey(appDbHdl_t hdl, dmSecKeyIndEvt_t *pKey)
+{
+  switch(pKey->type)
+  {
+    case DM_KEY_LOCAL_LTK:
+      ((appDbRec_t *) hdl)->localLtkSecLevel = pKey->secLevel;
+      ((appDbRec_t *) hdl)->localLtk = pKey->keyData.ltk;
+      break;
+
+    case DM_KEY_PEER_LTK:
+      ((appDbRec_t *) hdl)->peerLtkSecLevel = pKey->secLevel;
+      ((appDbRec_t *) hdl)->peerLtk = pKey->keyData.ltk;
+      break;
+
+    case DM_KEY_IRK:
+      ((appDbRec_t *)hdl)->peerIrk = pKey->keyData.irk;
+
+      /* make sure peer record is stored using its identity address */
+      ((appDbRec_t *)hdl)->addrType = pKey->keyData.irk.addrType;
+      BdaCpy(((appDbRec_t *)hdl)->peerAddr, pKey->keyData.irk.bdAddr);
+      break;
+
+    case DM_KEY_CSRK:
+      ((appDbRec_t *)hdl)->peerCsrk = pKey->keyData.csrk;
+
+      /* sign counter must be initialized to zero when CSRK is generated */
+      ((appDbRec_t *)hdl)->peerSignCounter = 0;
+      break;
+
+    default:
+      break;
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the client characteristic configuration descriptor table.
+ *
+ *  \param  hdl       Database record handle.
+ *
+ *  \return Pointer to client characteristic configuration descriptor table.
+ */
+/*************************************************************************************************/
+uint16_t *AppDbGetCccTbl(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *) hdl)->cccTbl;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set a value in the client characteristic configuration table.
+ *
+ *  \param  hdl       Database record handle.
+ *  \param  idx       Table index.
+ *  \param  value     client characteristic configuration value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetCccTblValue(appDbHdl_t hdl, uint16_t idx, uint16_t value)
+{
+  WSF_ASSERT(idx < APP_DB_NUM_CCCD);
+
+  ((appDbRec_t *) hdl)->cccTbl[idx] = value;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the discovery status.
+ *
+ *  \param  hdl       Database record handle.
+ *
+ *  \return Discovery status.
+ */
+/*************************************************************************************************/
+uint8_t AppDbGetDiscStatus(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *) hdl)->discStatus;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set the discovery status.
+ *
+ *  \param  hdl       Database record handle.
+ *  \param  state     Discovery status.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetDiscStatus(appDbHdl_t hdl, uint8_t status)
+{
+  ((appDbRec_t *) hdl)->discStatus = status;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the cached handle list.
+ *
+ *  \param  hdl       Database record handle.
+ *
+ *  \return Pointer to handle list.
+ */
+/*************************************************************************************************/
+uint16_t *AppDbGetHdlList(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *) hdl)->hdlList;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set the cached handle list.
+ *
+ *  \param  hdl       Database record handle.
+ *  \param  pHdlList  Pointer to handle list.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetHdlList(appDbHdl_t hdl, uint16_t *pHdlList)
+{
+  memcpy(((appDbRec_t *) hdl)->hdlList, pHdlList, sizeof(((appDbRec_t *) hdl)->hdlList));
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the device name.
+ *
+ *  \param  pLen      Returned device name length.
+ *
+ *  \return Pointer to UTF-8 string containing device name or NULL if not set.
+ */
+/*************************************************************************************************/
+char *AppDbGetDevName(uint8_t *pLen)
+{
+  /* if first character of name is NULL assume it is uninitialized */
+  if (appDb.devName[0] == 0)
+  {
+    *pLen = 0;
+    return NULL;
+  }
+  else
+  {
+    *pLen = appDb.devNameLen;
+    return appDb.devName;
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set the device name.
+ *
+ *  \param  len       Device name length.
+ *  \param  pStr      UTF-8 string containing device name.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetDevName(uint8_t len, char *pStr)
+{
+  /* check for maximum device length */
+  len = (len <= sizeof(appDb.devName)) ? len : sizeof(appDb.devName);
+
+  memcpy(appDb.devName, pStr, len);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get address resolution attribute value read from a peer device.
+ *
+ *  \param  hdl        Database record handle.
+ *
+ *  \return TRUE if address resolution is supported in peer device. FALSE, otherwise.
+ */
+/*************************************************************************************************/
+bool_t AppDbGetPeerAddrRes(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *)hdl)->peerAddrRes;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set address resolution attribute value for a peer device.
+ *
+ *  \param  hdl        Database record handle.
+ *  \param  addrRes    Address resolution attribue value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetPeerAddrRes(appDbHdl_t hdl, uint8_t addrRes)
+{
+  ((appDbRec_t *)hdl)->peerAddrRes = addrRes;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get sign counter for a peer device.
+ *
+ *  \param  hdl        Database record handle.
+ *
+ *  \return Sign counter for peer device.
+ */
+/*************************************************************************************************/
+uint32_t AppDbGetPeerSignCounter(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *)hdl)->peerSignCounter;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set sign counter for a peer device.
+ *
+ *  \param  hdl          Database record handle.
+ *  \param  signCounter  Sign counter for peer device.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetPeerSignCounter(appDbHdl_t hdl, uint32_t signCounter)
+{
+  ((appDbRec_t *)hdl)->peerSignCounter = signCounter;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the peer device added to resolving list flag value.
+ *
+ *  \param  hdl        Database record handle.
+ *
+ *  \return TRUE if peer device's been added to resolving list. FALSE, otherwise.
+ */
+/*************************************************************************************************/
+bool_t AppDbGetPeerAddedToRl(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *)hdl)->peerAddedToRl;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set the peer device added to resolving list flag to a given value.
+ *
+ *  \param  hdl           Database record handle.
+ *  \param  peerAddedToRl Peer device added to resolving list flag value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetPeerAddedToRl(appDbHdl_t hdl, bool_t peerAddedToRl)
+{
+  ((appDbRec_t *)hdl)->peerAddedToRl = peerAddedToRl;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Get the resolvable private address only attribute flag for a given peer device.
+ *
+ *  \param  hdl        Database record handle.
+ *
+ *  \return TRUE if RPA Only attribute is present on peer device. FALSE, otherwise.
+ */
+/*************************************************************************************************/
+bool_t AppDbGetPeerRpao(appDbHdl_t hdl)
+{
+  return ((appDbRec_t *)hdl)->peerRpao;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set the resolvable private address only attribute flag for a given peer device.
+ *
+ *  \param  hdl        Database record handle.
+ *  \param  peerRpao   Resolvable private address only attribute flag value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppDbSetPeerRpao(appDbHdl_t hdl, bool_t peerRpao)
+{
+  ((appDbRec_t *)hdl)->peerRpao = peerRpao;
+}
+/* clang-format on */
diff --git a/epicardium/ble/app/common/app_ui.c b/epicardium/ble/app/common/app_ui.c
new file mode 100644
index 0000000000000000000000000000000000000000..089e9dbbe615dc3787fec1a03712985b91537050
--- /dev/null
+++ b/epicardium/ble/app/common/app_ui.c
@@ -0,0 +1,335 @@
+/*************************************************************************************************/
+/*!
+ *  \file
+ *
+ *  \brief  Application framework user interface.
+ *
+ *  Copyright (c) 2011-2018 Arm Ltd. All Rights Reserved.
+ *  ARM Ltd. confidential and proprietary.
+ *
+ *  IMPORTANT.  Your use of this file is governed by a Software License Agreement
+ *  ("Agreement") that must be accepted in order to download or otherwise receive a
+ *  copy of this file.  You may not use or copy this file for any purpose other than
+ *  as described in the Agreement.  If you do not agree to all of the terms of the
+ *  Agreement do not use this file and delete all copies in your possession or control;
+ *  if you do not have a copy of the Agreement, you must contact ARM Ltd. prior
+ *  to any use, copying or further distribution of this software.
+ */
+/*************************************************************************************************/
+
+#include "wsf_types.h"
+#include "wsf_os.h"
+#include "wsf_trace.h"
+#include "app_ui.h"
+
+/* card10:
+ * copied from: lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/apps/app/common/app_ui.c
+ *
+ * Reason: has several user interactions which we likley have to implement
+ */
+
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+/**************************************************************************************************
+  Global Variables
+**************************************************************************************************/
+
+/*! \brief Callback struct */
+appUiCback_t appUiCbackTbl;
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Perform a user interface action based on the event value passed to the function.
+ *
+ *  \param  event   User interface event value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiAction(uint8_t event)
+{
+  switch (event)
+  {
+    case APP_UI_NONE:
+      /* no action */
+      break;
+
+    case APP_UI_RESET_CMPL:
+      APP_TRACE_INFO0(">>> Reset complete <<<");
+      break;
+
+    case APP_UI_ADV_START:
+      APP_TRACE_INFO0(">>> Advertising started <<<");
+      break;
+
+    case APP_UI_ADV_STOP:
+      APP_TRACE_INFO0(">>> Advertising stopped <<<");
+      break;
+
+    case APP_UI_SCAN_START:
+      APP_TRACE_INFO0(">>> Scanning started <<<");
+      break;
+
+    case APP_UI_SCAN_STOP:
+      APP_TRACE_INFO0(">>> Scanning stopped <<<");
+      break;
+
+    case APP_UI_SCAN_REPORT:
+      APP_TRACE_INFO0(">>> Scan data received from peer <<<");
+      break;
+
+    case APP_UI_CONN_OPEN:
+      APP_TRACE_INFO0(">>> Connection opened <<<");
+      break;
+
+    case APP_UI_CONN_CLOSE:
+      APP_TRACE_INFO0(">>> Connection closed <<<");
+      break;
+
+    case APP_UI_SEC_PAIR_CMPL:
+      APP_TRACE_INFO0(">>> Pairing completed successfully <<<");
+      break;
+
+    case APP_UI_SEC_PAIR_FAIL:
+      APP_TRACE_INFO0(">>> Pairing failed <<<");
+      break;
+
+    case APP_UI_SEC_ENCRYPT:
+      APP_TRACE_INFO0(">>> Connection encrypted <<<");
+      break;
+
+    case APP_UI_SEC_ENCRYPT_FAIL:
+      APP_TRACE_INFO0(">>> Encryption failed <<<");
+      break;
+
+    case APP_UI_PASSKEY_PROMPT:
+      APP_TRACE_INFO0(">>> Prompt user to enter passkey <<<");
+      break;
+
+    case APP_UI_ALERT_CANCEL:
+      APP_TRACE_INFO0(">>> Cancel a low or high alert <<<");
+      break;
+
+    case APP_UI_ALERT_LOW:
+      APP_TRACE_INFO0(">>> Low alert <<<");
+      break;
+
+    case APP_UI_ALERT_HIGH:
+      APP_TRACE_INFO0(">>> High alert <<<");
+      break;
+
+    case APP_UI_ADV_SET_START_IND:
+      APP_TRACE_INFO0(">>> Advertising set(s) started <<<");
+      break;
+
+    case APP_UI_ADV_SET_STOP_IND:
+      APP_TRACE_INFO0(">>> Advertising set(s) stopped <<<");
+      break;
+
+    case APP_UI_SCAN_REQ_RCVD_IND:
+      APP_TRACE_INFO0(">>> Scan request received <<<");
+      break;
+
+    case APP_UI_EXT_SCAN_START_IND:
+      APP_TRACE_INFO0(">>> Extended scanning started <<<");
+      break;
+
+    case APP_UI_EXT_SCAN_STOP_IND:
+      APP_TRACE_INFO0(">>> Extended scanning stopped <<<");
+      break;
+
+    case APP_UI_PER_ADV_SET_START_IND:
+      APP_TRACE_INFO0(">>> Periodic advertising set started <<<");
+      break;
+
+    case APP_UI_PER_ADV_SET_STOP_IND:
+      APP_TRACE_INFO0(">>> Periodic advertising set stopped <<<");
+      break;
+
+    case APP_UI_PER_ADV_SYNC_EST_IND:
+      APP_TRACE_INFO0(">>> Periodic advertising sync established <<<");
+      break;
+
+    case APP_UI_PER_ADV_SYNC_LOST_IND:
+      APP_TRACE_INFO0(">>> Periodic advertising sync lost <<<");
+      break;
+
+    default:
+      break;
+  }
+
+  if (appUiCbackTbl.actionCback)
+  {
+    (*appUiCbackTbl.actionCback)(event);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Display a passkey.
+ *
+ *  \param  passkey   Passkey to display.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiDisplayPasskey(uint32_t passkey)
+{
+  APP_TRACE_INFO1(">>> Passkey: %d <<<", passkey);
+}
+
+/*************************************************************************************************/
+/*!
+*  \brief  Display a confirmation value.
+*
+*  \param  confirm    Confirm value to display.
+*
+*  \return None.
+*/
+/*************************************************************************************************/
+void AppUiDisplayConfirmValue(uint32_t confirm)
+{
+  APP_TRACE_INFO1(">>> Confirm Value: %d <<<", confirm);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Display an RSSI value.
+ *
+ *  \param  rssi   Rssi value to display.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiDisplayRssi(int8_t rssi)
+{
+  APP_TRACE_INFO1(">>> RSSI: %d dBm<<<", rssi);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Handle a UI timer expiration event.
+ *
+ *  \param  pMsg    Pointer to message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void appUiTimerExpired(wsfMsgHdr_t *pMsg)
+{
+
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Perform button press polling.  This function is called to handle WSF
+ *          message APP_BTN_POLL_IND.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void appUiBtnPoll(void)
+{
+  if (appUiCbackTbl.btnPollCback)
+  {
+    (*appUiCbackTbl.btnPollCback)();
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Handle a hardware button press.  This function is called to handle WSF
+ *          event APP_BTN_DOWN_EVT.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiBtnPressed(void)
+{
+
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Register a callback function to receive application button press events.
+ *
+ *  \return None.
+ *
+ *  \note   Registered by application to receive button events
+ */
+/*************************************************************************************************/
+void AppUiBtnRegister(appUiBtnCback_t btnCback)
+{
+  appUiCbackTbl.btnCback = btnCback;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Register a callback function to receive action events.
+ *
+ *  \return None.
+ *
+ *  \note   Registered by platform
+ */
+/*************************************************************************************************/
+void AppUiActionRegister(appUiActionCback_t actionCback)
+{
+  appUiCbackTbl.actionCback = actionCback;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Register a callback function to receive APP_BTN_POLL_IND events.
+ *
+ *  \return None.
+ *
+ *  \note   Registered by platform
+ */
+/*************************************************************************************************/
+void AppUiBtnPollRegister(appUiBtnPollCback_t btnPollCback)
+{
+  appUiCbackTbl.btnPollCback = btnPollCback;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Play a sound.
+ *
+ *  \param  pSound   Pointer to sound tone/duration array.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiSoundPlay(const appUiSound_t *pSound)
+{
+
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Stop the sound that is currently playing.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiSoundStop(void)
+{
+
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Button test function-- for test purposes only.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void AppUiBtnTest(uint8_t btn)
+{
+  if (appUiCbackTbl.btnCback)
+  {
+    (*appUiCbackTbl.btnCback)(btn);
+  }
+}
+
+/* clang-format on */
diff --git a/epicardium/ble/ble.c b/epicardium/ble/ble.c
new file mode 100644
index 0000000000000000000000000000000000000000..9a2da1942627e02ca57bbdf518464c59ec3e37b4
--- /dev/null
+++ b/epicardium/ble/ble.c
@@ -0,0 +1,210 @@
+#include "modules/log.h"
+
+#include "fs_util.h"
+
+#include "wsf_types.h"
+#include "wsf_buf.h"
+#include "wsf_trace.h"
+#include "ble_api.h"
+#include "hci_vs.h"
+#include "att_api.h"
+
+#include "FreeRTOS.h"
+#include "timers.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+
+#define WSF_BUF_POOLS 6
+#define WSF_BUF_SIZE 0x1048
+
+uint32_t SystemHeapSize = WSF_BUF_SIZE;
+uint32_t SystemHeap[WSF_BUF_SIZE / 4];
+uint32_t SystemHeapStart;
+
+/* Task ID for the ble handler */
+static TaskHandle_t ble_task_id = NULL;
+
+/*! Default pool descriptor. */
+/* clang-format off */
+static wsfBufPoolDesc_t mainPoolDesc[WSF_BUF_POOLS] =
+{
+  {  16,  8 },
+  {  32,  4 },
+  {  64,  4 },
+  { 128,  4 },
+  { 256,  4 },
+  { 512,  4 }
+};
+/* clang-format on */
+
+static StaticTimer_t x;
+static TimerHandle_t timerWakeup = NULL;
+static int lasttick              = 0;
+
+/*! \brief  Stack initialization for app. */
+extern void StackInit(void);
+extern void AppInit(void);
+extern void bleuart_init(void);
+
+/*************************************************************************************************/
+void PalSysAssertTrap(void)
+{
+	while (1) {
+	}
+}
+/*************************************************************************************************/
+static bool_t myTrace(const uint8_t *pBuf, uint32_t len)
+{
+	extern uint8_t wsfCsNesting;
+
+	if (wsfCsNesting == 0) {
+		fwrite(pBuf, len, 1, stdout);
+		return TRUE;
+	}
+
+	return FALSE;
+}
+/*************************************************************************************************/
+static void WsfInit(void)
+{
+	uint32_t bytesUsed __attribute__((unused));
+	WsfTimerInit();
+
+	SystemHeapStart = (uint32_t)&SystemHeap;
+	memset(SystemHeap, 0, sizeof(SystemHeap));
+	//printf("SystemHeapStart = 0x%x\n", SystemHeapStart);
+	//printf("SystemHeapSize = 0x%x\n", SystemHeapSize);
+
+	WsfTraceRegisterHandler(myTrace);
+	WsfTraceEnable(TRUE);
+
+	bytesUsed = WsfBufInit(WSF_BUF_POOLS, mainPoolDesc);
+	APP_TRACE_INFO1("bytesUsed = %u", (unsigned int)bytesUsed);
+}
+/*************************************************************************************************/
+/* TODO: We need a source of MACs */
+static void setAddress(void)
+{
+	uint8_t bdAddr[6] = { 0x02, 0x02, 0x44, 0x8B, 0x05, 0x00 };
+	char buf[32];
+
+	fs_read_text_file("mac.txt", buf, sizeof(buf));
+	APP_TRACE_INFO1("mac file contents: %s", buf);
+	int a, b, c, d, e, f;
+	if (sscanf(buf, "%x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f) == 6) {
+		bdAddr[0] = f;
+		bdAddr[1] = e;
+		bdAddr[2] = d;
+		bdAddr[3] = c;
+		bdAddr[4] = b;
+		bdAddr[5] = a;
+	}
+
+	LOG_INFO(
+		"ble",
+		"Setting MAC address to %02X:%02X:%02X:%02X:%02X:%02X",
+		bdAddr[5],
+		bdAddr[4],
+		bdAddr[3],
+		bdAddr[2],
+		bdAddr[1],
+		bdAddr[0]
+	);
+	HciVsSetBdAddr(bdAddr);
+}
+/*************************************************************************************************/
+static void vTimerCallback(xTimerHandle pxTimer)
+{
+	//printf("wake\n");
+	int tick = xTaskGetTickCount();
+	//printf("WsfTimerUpdate(%d)\n", tick - lasttick);
+	WsfTimerUpdate(tick - lasttick);
+	lasttick = tick;
+	//printf("done\n");
+}
+/*************************************************************************************************/
+static void notify(void)
+{
+	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
+	if (xPortIsInsideInterrupt()) {
+		vTaskNotifyGiveFromISR(ble_task_id, &xHigherPriorityTaskWoken);
+		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
+	} else {
+		xTaskNotifyGive(ble_task_id);
+	}
+}
+/*************************************************************************************************/
+void WsfTimerNotify(void)
+{
+	//printf("WsfTimerNotify\n");
+	// TODO: Can we do this without waking up the task?
+	// xTimerChangePeriodFromISR exists
+	notify();
+}
+/*************************************************************************************************/
+void wsf_ble_signal_event(void)
+{
+	//printf("wsf_ble_signal_event\n");
+	notify();
+}
+/*************************************************************************************************/
+static void scheduleTimer(void)
+{
+	bool_t timerRunning;
+	wsfTimerTicks_t time_to_next_expire;
+
+	vTimerCallback(NULL);
+	time_to_next_expire = WsfTimerNextExpiration(&timerRunning);
+
+	if (timerRunning) {
+		//printf("time_to_next_expire = %d\n", time_to_next_expire);
+		//printf("change period\n");
+		if (timerWakeup != NULL) {
+			xTimerChangePeriod(
+				timerWakeup,
+				pdMS_TO_TICKS(time_to_next_expire),
+				0
+			);
+			//printf("insert done\n");
+		} else {
+			LOG_ERR("ble", "Could not create timer");
+		}
+	} else {
+		APP_TRACE_INFO0("No timer running");
+	}
+}
+/*************************************************************************************************/
+void vBleTask(void *pvParameters)
+{
+	ble_task_id = xTaskGetCurrentTaskHandle();
+	WsfInit();
+	StackInit();
+	setAddress();
+
+	NVIC_SetPriority(BTLE_SFD_TO_IRQn, 2);
+	NVIC_SetPriority(BTLE_TX_DONE_IRQn, 2);
+	NVIC_SetPriority(BTLE_RX_RCVD_IRQn, 2);
+	AppInit();
+	BleStart();
+	AttsDynInit();
+
+	bleuart_init();
+
+	lasttick = xTaskGetTickCount();
+
+	timerWakeup = xTimerCreateStatic(
+		"timerWakeup",    /* name */
+		pdMS_TO_TICKS(1), /* period/time */
+		pdFALSE,          /* auto reload */
+		NULL,             /* timer ID */
+		vTimerCallback,
+		&x); /* callback */
+
+	while (1) {
+		ulTaskNotifyTake(pdTRUE, portTICK_PERIOD_MS * 1000);
+		wsfOsDispatcher();
+		scheduleTimer();
+	}
+}
diff --git a/epicardium/ble/ble_api.h b/epicardium/ble/ble_api.h
new file mode 100644
index 0000000000000000000000000000000000000000..9040f8e436100660b2bbb7c1ef6079cdde07082a
--- /dev/null
+++ b/epicardium/ble/ble_api.h
@@ -0,0 +1,13 @@
+#pragma once
+/**************************************************************************************************
+  Function Declarations
+**************************************************************************************************/
+/*************************************************************************************************/
+/*!
+ *  \brief  Start the application.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void BleStart(void);
+
diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c
new file mode 100644
index 0000000000000000000000000000000000000000..4c0971e7b4964781f473b6d0ba1e461f3fd99561
--- /dev/null
+++ b/epicardium/ble/ble_main.c
@@ -0,0 +1,513 @@
+/* card10:
+ * Copied from lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/apps/fit/fit_main.c
+ *
+ * Also have a look at lib/sdk/Applications/EvKitExamples/BLE_fit/fit_main.c which has some changes
+ * to this file regarding handling of OOB paring data
+ *
+ * This file contains some application logic taken from the "fit" example.
+ *
+ * Things have been renamed:
+ * fit -> ble
+ * Fit -> Ble
+ * FIT -> BLE
+ */
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+#include <string.h>
+#include "wsf_types.h"
+#include "util/bstream.h"
+#include "wsf_msg.h"
+#include "wsf_trace.h"
+#include "hci_api.h"
+#include "dm_api.h"
+#include "att_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_rscs.h"
+#include "bas/bas_api.h"
+#include "hrps/hrps_api.h"
+#include "rscp/rscp_api.h"
+
+/**************************************************************************************************
+  Macros
+**************************************************************************************************/
+
+/*! WSF message event starting value */
+#define BLE_MSG_START               0xA0
+
+/*! WSF message event enumeration */
+enum
+{
+  BLE_BATT_TIMER_IND = BLE_MSG_START,                     /*! Battery measurement timer expired */
+};
+
+/**************************************************************************************************
+  Data Types
+**************************************************************************************************/
+
+/*! Application message type */
+typedef union
+{
+  wsfMsgHdr_t     hdr;
+  dmEvt_t         dm;
+  attsCccEvt_t    ccc;
+  attEvt_t        att;
+} bleMsg_t;
+
+/**************************************************************************************************
+  Configurable Parameters
+**************************************************************************************************/
+
+/*! configurable parameters for advertising */
+static const appAdvCfg_t bleAdvCfg =
+{
+  {60000,     0,     0},                  /*! Advertising durations in ms */
+  {500/0.625,     4000/0.625,     0}                   /*! Advertising intervals in 0.625 ms units */
+};
+
+/*! configurable parameters for slave */
+static const appSlaveCfg_t bleSlaveCfg =
+{
+  1,                                      /*! Maximum connections */
+};
+
+/*! configurable parameters for security */
+static const appSecCfg_t bleSecCfg =
+{
+  DM_AUTH_BOND_FLAG | DM_AUTH_SC_FLAG,    /*! Authentication and bonding flags */
+  0,                                      /*! Initiator key distribution flags */
+  DM_KEY_DIST_LTK,                        /*! Responder key distribution flags */
+  FALSE,                                  /*! TRUE if Out-of-band pairing data is present */
+  TRUE                                    /*! TRUE to initiate security upon connection */
+};
+
+/*! configurable parameters for connection parameter update */
+static const appUpdateCfg_t bleUpdateCfg =
+{
+  6000,                                   /*! Connection idle period in ms before attempting
+                                              connection parameter update; set to zero to disable */
+  800/1.25,                               /*! Minimum connection interval in 1.25ms units */
+  1000/1.25,                              /*! Maximum connection interval in 1.25ms units */
+  0,                                      /*! Connection latency */
+  9000/10,                                /*! Supervision timeout in 10ms units */
+  5                                       /*! Number of update attempts before giving up */
+};
+
+/*! battery measurement configuration */
+static const basCfg_t bleBasCfg =
+{
+  30,       /*! Battery measurement timer expiration period in seconds */
+  1,        /*! Perform battery measurement after this many timer periods */
+  100       /*! Send battery level notification to peer when below this level. */
+};
+
+/*! SMP security parameter configuration */
+static const smpCfg_t bleSmpCfg =
+{
+  3000,                                   /*! 'Repeated attempts' timeout in msec */
+  SMP_IO_NO_IN_NO_OUT,                    /*! I/O Capability */
+  7,                                      /*! Minimum encryption key length */
+  16,                                     /*! Maximum encryption key length */
+  3,                                      /*! Attempts to trigger 'repeated attempts' timeout */
+  0,                                      /*! Device authentication requirements */
+};
+
+/**************************************************************************************************
+  Advertising Data
+**************************************************************************************************/
+
+/*! advertising data, discoverable mode */
+static const uint8_t bleAdvDataDisc[] =
+{
+  /*! flags */
+  2,                                      /*! length */
+  DM_ADV_TYPE_FLAGS,                      /*! AD type */
+  DM_FLAG_LE_GENERAL_DISC |               /*! flags */
+  DM_FLAG_LE_BREDR_NOT_SUP,
+
+  /*! tx power */
+  2,                                      /*! length */
+  DM_ADV_TYPE_TX_POWER,                   /*! AD type */
+  0,                                      /*! tx power */
+
+  /*! service UUID list */
+  5,                                      /*! length */
+  DM_ADV_TYPE_16_UUID,                    /*! AD type */
+  UINT16_TO_BYTES(ATT_UUID_DEVICE_INFO_SERVICE),
+  UINT16_TO_BYTES(ATT_UUID_BATTERY_SERVICE)
+};
+
+/*! scan data, discoverable mode */
+static const uint8_t bleScanDataDisc[] =
+{
+  /*! device name */
+  7,                                      /*! length */
+  DM_ADV_TYPE_LOCAL_NAME,                 /*! AD type */
+  'c','a','r','d','1','0'
+};
+
+/**************************************************************************************************
+  Client Characteristic Configuration Descriptors
+**************************************************************************************************/
+
+/*! enumeration of client characteristic configuration descriptors */
+enum
+{
+  BLE_GATT_SC_CCC_IDX,                    /*! GATT service, service changed characteristic */
+  BLE_BATT_LVL_CCC_IDX,                   /*! Battery service, battery level characteristic */
+  BLE_NUM_CCC_IDX
+};
+
+/*! client characteristic configuration descriptors settings, indexed by above enumeration */
+static const attsCccSet_t bleCccSet[BLE_NUM_CCC_IDX] =
+{
+  /* cccd handle          value range               security level */
+  {GATT_SC_CH_CCC_HDL,    ATT_CLIENT_CFG_INDICATE,  DM_SEC_LEVEL_NONE},   /* BLE_GATT_SC_CCC_IDX */
+  {BATT_LVL_CH_CCC_HDL,   ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_BATT_LVL_CCC_IDX */
+};
+
+/**************************************************************************************************
+  Global Variables
+**************************************************************************************************/
+
+/*! WSF handler ID */
+wsfHandlerId_t bleHandlerId;
+
+
+static void BleHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg);
+/*************************************************************************************************/
+/*!
+ *  \brief  Application DM callback.
+ *
+ *  \param  pDmEvt  DM callback event
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleDmCback(dmEvt_t *pDmEvt)
+{
+  dmEvt_t *pMsg;
+  uint16_t len;
+
+  len = DmSizeOfEvt(pDmEvt);
+
+  if ((pMsg = WsfMsgAlloc(len)) != NULL)
+  {
+    memcpy(pMsg, pDmEvt, len);
+    WsfMsgSend(bleHandlerId, pMsg);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Application ATT callback.
+ *
+ *  \param  pEvt    ATT callback event
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleAttCback(attEvt_t *pEvt)
+{
+  attEvt_t *pMsg;
+
+  if ((pMsg = WsfMsgAlloc(sizeof(attEvt_t) + pEvt->valueLen)) != NULL)
+  {
+    memcpy(pMsg, pEvt, sizeof(attEvt_t));
+    pMsg->pValue = (uint8_t *) (pMsg + 1);
+    memcpy(pMsg->pValue, pEvt->pValue, pEvt->valueLen);
+    WsfMsgSend(bleHandlerId, pMsg);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Application ATTS client characteristic configuration callback.
+ *
+ *  \param  pDmEvt  DM callback event
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleCccCback(attsCccEvt_t *pEvt)
+{
+  attsCccEvt_t  *pMsg;
+  appDbHdl_t    dbHdl;
+
+  /* if CCC not set from initialization and there's a device record */
+  if ((pEvt->handle != ATT_HANDLE_NONE) &&
+      ((dbHdl = AppDbGetHdl((dmConnId_t) pEvt->hdr.param)) != APP_DB_HDL_NONE))
+  {
+    /* store value in device database */
+    AppDbSetCccTblValue(dbHdl, pEvt->idx, pEvt->value);
+  }
+
+  if ((pMsg = WsfMsgAlloc(sizeof(attsCccEvt_t))) != NULL)
+  {
+    memcpy(pMsg, pEvt, sizeof(attsCccEvt_t));
+    WsfMsgSend(bleHandlerId, pMsg);
+  }
+}
+
+
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Process CCC state change.
+ *
+ *  \param  pMsg    Pointer to message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleProcCccState(bleMsg_t *pMsg)
+{
+  APP_TRACE_INFO3("ccc state ind value:%d handle:%d idx:%d", pMsg->ccc.value, pMsg->ccc.handle, pMsg->ccc.idx);
+
+  /* handle battery level CCC */
+  if (pMsg->ccc.idx == BLE_BATT_LVL_CCC_IDX)
+  {
+    if (pMsg->ccc.value == ATT_CLIENT_CFG_NOTIFY)
+    {
+      BasMeasBattStart((dmConnId_t) pMsg->ccc.hdr.param, BLE_BATT_TIMER_IND, BLE_BATT_LVL_CCC_IDX);
+    }
+    else
+    {
+      BasMeasBattStop((dmConnId_t) pMsg->ccc.hdr.param);
+    }
+    return;
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Perform UI actions on connection close.
+ *
+ *  \param  pMsg    Pointer to message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleClose(bleMsg_t *pMsg)
+{
+  /* stop battery measurement */
+  BasMeasBattStop((dmConnId_t) pMsg->hdr.param);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Set up advertising and other procedures that need to be performed after
+ *          device reset.
+ *
+ *  \param  pMsg    Pointer to message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleSetup(bleMsg_t *pMsg)
+{
+  /* set advertising and scan response data for discoverable mode */
+  AppAdvSetData(APP_ADV_DATA_DISCOVERABLE, sizeof(bleAdvDataDisc), (uint8_t *) bleAdvDataDisc);
+  AppAdvSetData(APP_SCAN_DATA_DISCOVERABLE, sizeof(bleScanDataDisc), (uint8_t *) bleScanDataDisc);
+
+  /* set advertising and scan response data for connectable mode */
+  AppAdvSetData(APP_ADV_DATA_CONNECTABLE, 0, NULL);
+  AppAdvSetData(APP_SCAN_DATA_CONNECTABLE, 0, NULL);
+
+  /* start advertising; automatically set connectable/discoverable mode and bondable mode */
+  AppAdvStart(APP_MODE_AUTO_INIT);
+}
+
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Process messages from the event handler.
+ *
+ *  \param  pMsg    Pointer to message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void bleProcMsg(bleMsg_t *pMsg)
+{
+  uint8_t uiEvent = APP_UI_NONE;
+
+  switch(pMsg->hdr.event)
+  {
+    case BLE_BATT_TIMER_IND:
+      BasProcMsg(&pMsg->hdr);
+      break;
+
+    case ATTS_HANDLE_VALUE_CNF:
+      BasProcMsg(&pMsg->hdr);
+      break;
+
+    case ATTS_CCC_STATE_IND:
+      bleProcCccState(pMsg);
+      break;
+
+    case DM_RESET_CMPL_IND:
+      DmSecGenerateEccKeyReq();
+      bleSetup(pMsg);
+      uiEvent = APP_UI_RESET_CMPL;
+      break;
+
+    case DM_ADV_START_IND:
+      uiEvent = APP_UI_ADV_START;
+      break;
+
+    case DM_ADV_STOP_IND:
+      uiEvent = APP_UI_ADV_STOP;
+      break;
+
+    case DM_CONN_OPEN_IND:
+      BasProcMsg(&pMsg->hdr);
+      uiEvent = APP_UI_CONN_OPEN;
+      break;
+
+    case DM_CONN_CLOSE_IND:
+      bleClose(pMsg);
+      uiEvent = APP_UI_CONN_CLOSE;
+      break;
+
+    case DM_SEC_PAIR_CMPL_IND:
+      uiEvent = APP_UI_SEC_PAIR_CMPL;
+      break;
+
+    case DM_SEC_PAIR_FAIL_IND:
+      uiEvent = APP_UI_SEC_PAIR_FAIL;
+      break;
+
+    case DM_SEC_ENCRYPT_IND:
+      uiEvent = APP_UI_SEC_ENCRYPT;
+      break;
+
+    case DM_SEC_ENCRYPT_FAIL_IND:
+      uiEvent = APP_UI_SEC_ENCRYPT_FAIL;
+      break;
+
+    case DM_SEC_AUTH_REQ_IND:
+      AppHandlePasskey(&pMsg->dm.authReq);
+      break;
+
+    case DM_SEC_ECC_KEY_IND:
+      DmSecSetEccKey(&pMsg->dm.eccMsg.data.key);
+      break;
+
+    case DM_SEC_COMPARE_IND:
+      AppHandleNumericComparison(&pMsg->dm.cnfInd);
+      break;
+
+    case DM_HW_ERROR_IND:
+      uiEvent = APP_UI_HW_ERROR;
+      break;
+
+    default:
+      break;
+  }
+
+  if (uiEvent != APP_UI_NONE)
+  {
+    AppUiAction(uiEvent);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Application handler init function called during system initialization.
+ *
+ *  \param  handlerID  WSF handler ID.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void BleHandlerInit(void)
+{
+  APP_TRACE_INFO0("BleHandlerInit");
+
+  /* store handler ID */
+  bleHandlerId =WsfOsSetNextHandler(BleHandler);
+
+  /* Set configuration pointers */
+  pAppAdvCfg = (appAdvCfg_t *) &bleAdvCfg;
+  pAppSlaveCfg = (appSlaveCfg_t *) &bleSlaveCfg;
+  pAppSecCfg = (appSecCfg_t *) &bleSecCfg;
+  pAppUpdateCfg = (appUpdateCfg_t *) &bleUpdateCfg;
+
+  /* Initialize application framework */
+  AppSlaveInit();
+
+  /* Set stack configuration pointers */
+  pSmpCfg = (smpCfg_t *) &bleSmpCfg;
+
+  /* initialize battery service server */
+  BasInit(bleHandlerId, (basCfg_t *) &bleBasCfg);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  WSF event handler for application.
+ *
+ *  \param  event   WSF event mask.
+ *  \param  pMsg    WSF message.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+static void BleHandler(wsfEventMask_t event, wsfMsgHdr_t *pMsg)
+{
+  if (pMsg != NULL)
+  {
+    APP_TRACE_INFO1("Ble got evt %d", pMsg->event);
+
+    if (pMsg->event >= DM_CBACK_START && pMsg->event <= DM_CBACK_END)
+    {
+      /* process advertising and connection-related messages */
+      AppSlaveProcDmMsg((dmEvt_t *) pMsg);
+
+      /* process security-related messages */
+      AppSlaveSecProcDmMsg((dmEvt_t *) pMsg);
+    }
+
+    /* perform profile and user interface-related operations */
+    bleProcMsg((bleMsg_t *) pMsg);
+  }
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Start the application.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void BleStart(void)
+{
+
+  BleHandlerInit();
+
+  /* Register for stack callbacks */
+  DmRegister(bleDmCback);
+  DmConnRegister(DM_CLIENT_ID_APP, bleDmCback);
+  AttRegister(bleAttCback);
+  AttConnRegister(AppServerConnCback);
+  AttsCccRegister(BLE_NUM_CCC_IDX, (attsCccSet_t *) bleCccSet, bleCccCback);
+
+  /* Initialize attribute server database */
+  SvcCoreAddGroup();
+  SvcDisAddGroup(); // Device Information Service
+  SvcBattCbackRegister(BasReadCback, NULL);
+  SvcBattAddGroup();
+
+  /* Reset the device */
+  DmDevReset();
+}
+/* clang-format on */
diff --git a/epicardium/ble/meson.build b/epicardium/ble/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..02f2eeb58e44eb2840f3bb8bc0a1cf979822bad1
--- /dev/null
+++ b/epicardium/ble/meson.build
@@ -0,0 +1,11 @@
+ble_sources = files(
+  'ble.c',
+  'stack.c',
+  'ble_main.c',
+  'svc_dis.c',
+  'svc_core.c',
+  'app/app_main.c',
+  'app/common/app_db.c',
+  'app/common/app_ui.c',
+  'uart.c'
+)
diff --git a/epicardium/ble/stack.c b/epicardium/ble/stack.c
new file mode 100644
index 0000000000000000000000000000000000000000..690d1cb1447c05e841f157b4edb494ba2241460e
--- /dev/null
+++ b/epicardium/ble/stack.c
@@ -0,0 +1,189 @@
+/*************************************************************************************************/
+/*!
+ *  \file
+ *
+ *  \brief  Stack initialization
+ *
+ *  Copyright (c) 2016-2017 ARM Ltd. All Rights Reserved.
+ *  ARM Ltd. confidential and proprietary.
+ *
+ *  IMPORTANT.  Your use of this file is governed by a Software License Agreement
+ *  ("Agreement") that must be accepted in order to download or otherwise receive a
+ *  copy of this file.  You may not use or copy this file for any purpose other than
+ *  as described in the Agreement.  If you do not agree to all of the terms of the
+ *  Agreement do not use this file and delete all copies in your possession or control;
+ *  if you do not have a copy of the Agreement, you must contact ARM Ltd. prior
+ *  to any use, copying or further distribution of this software.
+ */
+/*************************************************************************************************/
+
+/*
+ * This file initializes the different components of the whole BLE stack. This inlucdes link level,
+ * HCI, security, etc...
+ *
+ * This file has been copied from lib/sdk/Applications/EvKitExamples/BLE_fit/stack_fit.c
+ *
+ * NOTE: Different stack_*.c files in the SDK initialize different components. We have to
+ * be very carefull to intitialize all needed components here. Think e.g. SecRandInit() ...
+ *
+ * Many components are related to the role of the device. Different components need to be
+ * initialized for central and peripheral roles.
+ */
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+#include <stdio.h>
+#include <string.h>
+#include "wsf_types.h"
+#include "wsf_os.h"
+#include "util/bstream.h"
+#include "ble_api.h"
+#include "hci_handler.h"
+#include "dm_handler.h"
+#include "l2c_handler.h"
+#include "att_handler.h"
+#include "smp_handler.h"
+#include "l2c_api.h"
+#include "att_api.h"
+#include "smp_api.h"
+#include "app_api.h"
+#include "svc_dis.h"
+#include "svc_core.h"
+#include "sec_api.h"
+#include "ll_init_api.h"
+
+/* TODO: card10: Where does this number come from? Is there any documentation? */
+#define LL_IMPL_REV             0x2303
+
+/* TODO: card10: Where does this number come from? Is there any documentation? */
+#define LL_MEMORY_FOOTPRINT     0xC152
+
+uint8_t LlMem[LL_MEMORY_FOOTPRINT];
+
+const LlRtCfg_t _ll_cfg = {
+    /* Device */
+    /*compId*/                  LL_COMP_ID_ARM,
+    /*implRev*/                 LL_IMPL_REV,
+    /*btVer*/                   LL_VER_BT_CORE_SPEC_5_0,
+    /*_align32 */               0, // padding for alignment
+
+    /* Advertiser */
+    /*maxAdvSets*/              4, // 4 Extended Advertising Sets
+    /*maxAdvReports*/           8,
+    /*maxExtAdvDataLen*/        LL_MAX_ADV_DATA_LEN,
+    /*defExtAdvDataFrag*/       64,
+    /*auxDelayUsec*/            0,
+
+    /* Scanner */
+    /*maxScanReqRcvdEvt*/       4,
+    /*maxExtScanDataLen*/       LL_MAX_ADV_DATA_LEN,
+
+    /* Connection */
+    /*maxConn*/                 2,
+    /*numTxBufs*/               16,
+    /*numRxBufs*/               16,
+    /*maxAclLen*/               512,
+    /*defTxPwrLvl*/             0,
+    /*ceJitterUsec*/            0,
+
+    /* DTM */
+    /*dtmRxSyncMs*/             10000,
+
+    /* PHY */
+    /*phy2mSup*/                TRUE,
+    /*phyCodedSup*/             TRUE,
+    /*stableModIdxTxSup*/       FALSE,
+    /*stableModIdxRxSup*/       FALSE
+};
+
+const BbRtCfg_t _bb_cfg = {
+    /*clkPpm*/                  20,
+    /*rfSetupDelayUsec*/        BB_RF_SETUP_DELAY_US,
+    /*maxScanPeriodMsec*/       BB_MAX_SCAN_PERIOD_MS,
+    /*schSetupDelayUsec*/       BB_SCH_SETUP_DELAY_US
+};
+
+/*************************************************************************************************/
+/*!
+ *  \brief      Initialize stack.
+ *
+ *  \return     None.
+ */
+/*************************************************************************************************/
+void StackInit(void)
+{
+  wsfHandlerId_t handlerId;
+
+/* card10: We do not use the SDMA HCI at the moment. The block below is not compiled. */
+#ifndef ENABLE_SDMA
+  uint32_t memUsed;
+
+  /* Initialize link layer. */
+  LlInitRtCfg_t ll_init_cfg =
+  {
+      .pBbRtCfg     = &_bb_cfg,
+      .wlSizeCfg    = 4,
+      .rlSizeCfg    = 4,
+      .plSizeCfg    = 4,
+      .pLlRtCfg     = &_ll_cfg,
+      .pFreeMem     = LlMem,
+      .freeMemAvail = LL_MEMORY_FOOTPRINT
+  };
+
+#ifdef DATS_APP_USE_LEGACY_API
+  memUsed = LlInitControllerExtInit(&ll_init_cfg);
+#else /* DATS_APP_USE_LEGACY_API */
+  memUsed = LlInitControllerExtInit(&ll_init_cfg);
+#endif /* DATS_APP_USE_LEGACY_API */
+  if(memUsed != LL_MEMORY_FOOTPRINT)
+  {
+      printf("Controller memory mismatch 0x%x != 0x%x\n", (unsigned int)memUsed, 
+          (unsigned int)LL_MEMORY_FOOTPRINT);
+  }
+#endif
+
+
+  /* card10:
+   * These calls register a queue for callbacks in the OS abstraction
+   * and then pass a handle down to modules which uses them to
+   * internally handle callbacks.
+   *
+   * No idea why the modules don't call WsfOsSetNextHandler()
+   * internally ... */
+  handlerId = WsfOsSetNextHandler(HciHandler);
+  HciHandlerInit(handlerId);
+
+  SecInit();
+  SecAesInit();
+  SecCmacInit();
+  SecEccInit();
+
+  handlerId = WsfOsSetNextHandler(DmHandler);
+  DmDevVsInit(0);
+  DmAdvInit();
+  DmConnInit();
+  DmConnSlaveInit();
+  DmSecInit();
+  DmSecLescInit();
+  DmPrivInit();
+  DmPhyInit();
+  DmHandlerInit(handlerId);
+
+  handlerId = WsfOsSetNextHandler(L2cSlaveHandler);
+  L2cSlaveHandlerInit(handlerId);
+  L2cInit();
+  L2cSlaveInit();
+
+  handlerId = WsfOsSetNextHandler(AttHandler);
+  AttHandlerInit(handlerId);
+  AttsInit();
+  AttsIndInit();
+
+  handlerId = WsfOsSetNextHandler(SmpHandler);
+  SmpHandlerInit(handlerId);
+  SmprInit();
+  SmprScInit();
+
+  /*TODO card10: Probably want to adjust this */
+  HciSetMaxRxAclLen(100);
+}
+/* clang-format off */
diff --git a/epicardium/ble/svc_core.c b/epicardium/ble/svc_core.c
new file mode 100644
index 0000000000000000000000000000000000000000..a8618dc6b2e5aa682265b7af7de74f4eb4fa4334
--- /dev/null
+++ b/epicardium/ble/svc_core.c
@@ -0,0 +1,349 @@
+/*************************************************************************************************/
+/*!
+ *  \file
+ *
+ *  \brief  Example GATT and GAP service implementations.
+ *
+ *  Copyright (c) 2009-2018 Arm Ltd. All Rights Reserved.
+ *  ARM Ltd. confidential and proprietary.
+ *
+ *  IMPORTANT.  Your use of this file is governed by a Software License Agreement
+ *  ("Agreement") that must be accepted in order to download or otherwise receive a
+ *  copy of this file.  You may not use or copy this file for any purpose other than
+ *  as described in the Agreement.  If you do not agree to all of the terms of the
+ *  Agreement do not use this file and delete all copies in your possession or control;
+ *  if you do not have a copy of the Agreement, you must contact ARM Ltd. prior
+ *  to any use, copying or further distribution of this software.
+ */
+/*************************************************************************************************/
+
+/* card10:
+ * copied from lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_core.c
+ */
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+#include "wsf_types.h"
+#include "att_api.h"
+#include "att_uuid.h"
+#include "util/bstream.h"
+#include "svc_core.h"
+#include "svc_ch.h"
+#include "svc_cfg.h"
+#include "wsf_assert.h"
+
+/**************************************************************************************************
+  Macros
+**************************************************************************************************/
+
+/*! Characteristic read permissions */
+#ifndef CORE_SEC_PERMIT_READ
+#define CORE_SEC_PERMIT_READ SVC_SEC_PERMIT_READ
+#endif
+
+/*! Characteristic write permissions */
+#ifndef CORE_SEC_PERMIT_WRITE
+#define CORE_SEC_PERMIT_WRITE SVC_SEC_PERMIT_WRITE
+#endif
+
+/*! Default device name */
+#define CORE_DEFAULT_DEV_NAME       "card10"
+
+/*! Length of default device name */
+#define CORE_DEFAULT_DEV_NAME_LEN   6
+
+/**************************************************************************************************
+ GAP group
+**************************************************************************************************/
+
+/* service */
+static const uint8_t gapValSvc[] = {UINT16_TO_BYTES(ATT_UUID_GAP_SERVICE)};
+static const uint16_t gapLenSvc = sizeof(gapValSvc);
+
+/* device name characteristic */
+static const uint8_t gapValDnCh[] = {ATT_PROP_READ | ATT_PROP_WRITE, UINT16_TO_BYTES(GAP_DN_HDL), UINT16_TO_BYTES(ATT_UUID_DEVICE_NAME)};
+static const uint16_t gapLenDnCh = sizeof(gapValDnCh);
+
+/* device name */
+static uint8_t gapValDn[ATT_DEFAULT_PAYLOAD_LEN] = CORE_DEFAULT_DEV_NAME;
+static uint16_t gapLenDn = CORE_DEFAULT_DEV_NAME_LEN;
+
+/* appearance characteristic */
+static const uint8_t gapValApCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(GAP_AP_HDL), UINT16_TO_BYTES(ATT_UUID_APPEARANCE)};
+static const uint16_t gapLenApCh = sizeof(gapValApCh);
+
+/* appearance */
+static uint8_t gapValAp[] = {UINT16_TO_BYTES(CH_APPEAR_UNKNOWN)};
+static const uint16_t gapLenAp = sizeof(gapValAp);
+
+/* central address resolution characteristic */
+static const uint8_t gapValCarCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(GAP_CAR_HDL), UINT16_TO_BYTES(ATT_UUID_CAR)};
+static const uint16_t gapLenCarCh = sizeof(gapValCarCh);
+
+/* central address resolution */
+static uint8_t gapValCar[] = {FALSE};
+static const uint16_t gapLenCar = sizeof(gapValCar);
+#if 0
+/* TODO card10:
+ * Enable these if "privacy" is enabled. See svc_core.h lien 38 */
+
+/* resolvable private address only characteristic */
+static const uint8_t gapValRpaoCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(GAP_RPAO_HDL), UINT16_TO_BYTES(ATT_UUID_RPAO)};
+static const uint16_t gapLenRpaoCh = sizeof(gapValRpaoCh);
+
+/* resolvable private address only */
+static uint8_t gapValRpao[] = {0};
+static const uint16_t gapLenRpao = sizeof(gapValRpao);
+#endif
+
+/* Attribute list for GAP group */
+static const attsAttr_t gapList[] =
+{
+  {
+    attPrimSvcUuid,
+    (uint8_t *) gapValSvc,
+    (uint16_t *) &gapLenSvc,
+    sizeof(gapValSvc),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) gapValDnCh,
+    (uint16_t *) &gapLenDnCh,
+    sizeof(gapValDnCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attDnChUuid,
+    gapValDn,
+    &gapLenDn,
+    sizeof(gapValDn),
+    (ATTS_SET_VARIABLE_LEN | ATTS_SET_WRITE_CBACK),
+    (ATTS_PERMIT_READ | CORE_SEC_PERMIT_WRITE)
+  },
+  {
+    attChUuid,
+    (uint8_t *) gapValApCh,
+    (uint16_t *) &gapLenApCh,
+    sizeof(gapValApCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attApChUuid,
+    gapValAp,
+    (uint16_t *) &gapLenAp,
+    sizeof(gapValAp),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) gapValCarCh,
+    (uint16_t *) &gapLenCarCh,
+    sizeof(gapValCarCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attCarChUuid,
+    gapValCar,
+    (uint16_t *) &gapLenCar,
+    sizeof(gapValCar),
+    0,
+    ATTS_PERMIT_READ
+  },
+#if 0
+/* TODO card10:
+ * Enable these if "privacy" is enabled. See svc_core.h lien 38 */
+  {
+    attChUuid,
+    (uint8_t *) gapValRpaoCh,
+    (uint16_t *) &gapLenRpaoCh,
+    sizeof(gapValRpaoCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attRpaoChUuid,
+    gapValRpao,
+    (uint16_t *) &gapLenRpao,
+    sizeof(gapValRpao),
+    0,
+    ATTS_PERMIT_READ
+  }
+#endif
+};
+
+/* GAP group structure */
+static attsGroup_t svcGapGroup =
+{
+  NULL,
+  (attsAttr_t *) gapList,
+  NULL,
+  NULL,
+  GAP_START_HDL,
+  GAP_END_HDL
+};
+
+WSF_CT_ASSERT(((sizeof(gapList) / sizeof(gapList[0])) == GAP_END_HDL - GAP_START_HDL + 1));
+/**************************************************************************************************
+ GATT group
+**************************************************************************************************/
+
+/* service */
+static const uint8_t gattValSvc[] = {UINT16_TO_BYTES(ATT_UUID_GATT_SERVICE)};
+static const uint16_t gattLenSvc = sizeof(gattValSvc);
+
+/* service changed characteristic */
+static const uint8_t gattValScCh[] = {ATT_PROP_INDICATE, UINT16_TO_BYTES(GATT_SC_HDL), UINT16_TO_BYTES(ATT_UUID_SERVICE_CHANGED)};
+static const uint16_t gattLenScCh = sizeof(gattValScCh);
+
+/* service changed */
+static const uint8_t gattValSc[] = {UINT16_TO_BYTES(0x0001), UINT16_TO_BYTES(0xFFFF)};
+static const uint16_t gattLenSc = sizeof(gattValSc);
+
+/* service changed client characteristic configuration */
+static uint8_t gattValScChCcc[] = {UINT16_TO_BYTES(0x0000)};
+static const uint16_t gattLenScChCcc = sizeof(gattValScChCcc);
+
+/* Attribute list for GATT group */
+static const attsAttr_t gattList[] =
+{
+  {
+    attPrimSvcUuid,
+    (uint8_t *) gattValSvc,
+    (uint16_t *) &gattLenSvc,
+    sizeof(gattValSvc),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) gattValScCh,
+    (uint16_t *) &gattLenScCh,
+    sizeof(gattValScCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attScChUuid,
+    (uint8_t *) gattValSc,
+    (uint16_t *) &gattLenSc,
+    sizeof(gattValSc),
+    0,
+    0
+  },
+  {
+    attCliChCfgUuid,
+    gattValScChCcc,
+    (uint16_t *) &gattLenScChCcc,
+    sizeof(gattValScChCcc),
+    ATTS_SET_CCC,
+    (ATTS_PERMIT_READ | CORE_SEC_PERMIT_WRITE)
+  },
+};
+
+/* GATT group structure */
+static attsGroup_t svcGattGroup =
+{
+  NULL,
+  (attsAttr_t *) gattList,
+  NULL,
+  NULL,
+  GATT_START_HDL,
+  GATT_END_HDL
+};
+
+WSF_CT_ASSERT(((sizeof(gattList) / sizeof(gattList[0])) == GATT_END_HDL - GATT_START_HDL + 1));
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Add the services to the attribute server.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcCoreAddGroup(void)
+{
+  AttsAddGroup(&svcGapGroup);
+  AttsAddGroup(&svcGattGroup);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Remove the services from the attribute server.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcCoreRemoveGroup(void)
+{
+  AttsRemoveGroup(GAP_START_HDL);
+  AttsRemoveGroup(GATT_START_HDL);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Register callbacks for the service.
+ *
+ *  \param  readCback   Read callback function.
+ *  \param  writeCback  Write callback function.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcCoreGapCbackRegister(attsReadCback_t readCback, attsWriteCback_t writeCback)
+{
+  svcGapGroup.readCback = readCback;
+  svcGapGroup.writeCback = writeCback;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Register callbacks for the service.
+ *
+ *  \param  readCback   Read callback function.
+ *  \param  writeCback  Write callback function.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcCoreGattCbackRegister(attsReadCback_t readCback, attsWriteCback_t writeCback)
+{
+  svcGattGroup.readCback = readCback;
+  svcGattGroup.writeCback = writeCback;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Update the central address resolution attribute value.
+ *
+ *  \param  value   New value.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcCoreGapCentAddrResUpdate(bool_t value)
+{
+  gapValCar[0] = value;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Add the Resolvable Private Address Only (RPAO) characteristic to the GAP service.
+ *          The RPAO characteristic should be added only when DM Privacy is enabled.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcCoreGapAddRpaoCh(void)
+{
+  /* if RPAO characteristic not already in GAP service */
+  if (svcGapGroup.endHandle < GAP_RPAO_HDL)
+  {
+    svcGapGroup.endHandle = GAP_RPAO_HDL;
+  }
+}
+/* clang-format on */
diff --git a/epicardium/ble/svc_dis.c b/epicardium/ble/svc_dis.c
new file mode 100644
index 0000000000000000000000000000000000000000..8ce2786486458369498b744207a61c8dfa8d0644
--- /dev/null
+++ b/epicardium/ble/svc_dis.c
@@ -0,0 +1,336 @@
+/*************************************************************************************************/
+/*!
+ *  \file
+ *
+ *  \brief  Example Device Information Service implementation.
+ *
+ *  Copyright (c) 2011-2018 Arm Ltd. All Rights Reserved.
+ *  ARM Ltd. confidential and proprietary.
+ *
+ *  IMPORTANT.  Your use of this file is governed by a Software License Agreement
+ *  ("Agreement") that must be accepted in order to download or otherwise receive a
+ *  copy of this file.  You may not use or copy this file for any purpose other than
+ *  as described in the Agreement.  If you do not agree to all of the terms of the
+ *  Agreement do not use this file and delete all copies in your possession or control;
+ *  if you do not have a copy of the Agreement, you must contact ARM Ltd. prior
+ *  to any use, copying or further distribution of this software.
+ */
+/*************************************************************************************************/
+
+/* card10:
+ * Copied from lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_dis.c
+ *
+ * Contains adaptions for the card10 (e.g. manufacturer name)
+ */
+/* clang-format off */
+/* clang-formet turned off for easier diffing against orginal file */
+#include "wsf_types.h"
+#include "att_api.h"
+#include "wsf_assert.h"
+#include "wsf_trace.h"
+#include "util/bstream.h"
+#include "svc_dis.h"
+#include "svc_cfg.h"
+
+/**************************************************************************************************
+  Macros
+**************************************************************************************************/
+
+/*! Characteristic read permissions */
+#ifndef DIS_SEC_PERMIT_READ
+#define DIS_SEC_PERMIT_READ SVC_SEC_PERMIT_READ
+#endif
+
+/*! Default manufacturer name */
+#define DIS_DEFAULT_MFR_NAME        "CCC"
+
+/*! Length of default manufacturer name */
+#define DIS_DEFAULT_MFR_NAME_LEN    3
+
+/*! Default model number */
+#define DIS_DEFAULT_MODEL_NUM       "1"
+
+/*! Length of default model number */
+#define DIS_DEFAULT_MODEL_NUM_LEN   1
+
+/*! Default serial number */
+#define DIS_DEFAULT_SERIAL_NUM      "1"
+
+/*! Length of default serial number */
+#define DIS_DEFAULT_SERIAL_NUM_LEN  1
+
+/*! Default firmware revision */
+#define DIS_DEFAULT_FW_REV          "<git hash>"
+
+/*! Length of default firmware revision */
+#define DIS_DEFAULT_FW_REV_LEN      10
+
+/*! Default hardware revision */
+#define DIS_DEFAULT_HW_REV          "1"
+
+/*! Length of default hardware revision */
+#define DIS_DEFAULT_HW_REV_LEN      1
+
+/*! Default software revision */
+#define DIS_DEFAULT_SW_REV          "1"
+
+/*! Length of default software revision */
+#define DIS_DEFAULT_SW_REV_LEN      1
+
+/**************************************************************************************************
+ Service variables
+**************************************************************************************************/
+
+/* Device information service declaration */
+static const uint8_t disValSvc[] = {UINT16_TO_BYTES(ATT_UUID_DEVICE_INFO_SERVICE)};
+static const uint16_t disLenSvc = sizeof(disValSvc);
+
+/* Manufacturer name string characteristic */
+static const uint8_t disValMfrCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_MFR_HDL), UINT16_TO_BYTES(ATT_UUID_MANUFACTURER_NAME)};
+static const uint16_t disLenMfrCh = sizeof(disValMfrCh);
+
+/* Manufacturer name string */
+static const uint8_t disUuMfr[] = {UINT16_TO_BYTES(ATT_UUID_MANUFACTURER_NAME)};
+static uint8_t disValMfr[DIS_MAXSIZE_MFR_ATT] = DIS_DEFAULT_MFR_NAME;
+static uint16_t disLenMfr = DIS_DEFAULT_MFR_NAME_LEN;
+
+/* System ID characteristic */
+static const uint8_t disValSidCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_SID_HDL), UINT16_TO_BYTES(ATT_UUID_SYSTEM_ID)};
+static const uint16_t disLenSidCh = sizeof(disValSidCh);
+
+/* System ID */
+static const uint8_t disUuSid[] = {UINT16_TO_BYTES(ATT_UUID_SYSTEM_ID)};
+static uint8_t disValSid[DIS_SIZE_SID_ATT] = {0x01, 0x02, 0x03, 0x04, 0x05, UINT16_TO_BYTE0(HCI_ID_ARM), UINT16_TO_BYTE1(HCI_ID_ARM), 0x00};
+static const uint16_t disLenSid = sizeof(disValSid);
+
+/* Model number string characteristic */
+static const uint8_t disValMnCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_MN_HDL), UINT16_TO_BYTES(ATT_UUID_MODEL_NUMBER)};
+static const uint16_t disLenMnCh = sizeof(disValMnCh);
+
+/* Model number string */
+static const uint8_t disUuMn[] = {UINT16_TO_BYTES(ATT_UUID_MODEL_NUMBER)};
+static uint8_t disValMn[DIS_MAXSIZE_MN_ATT] = DIS_DEFAULT_MODEL_NUM;
+static uint16_t disLenMn = DIS_DEFAULT_MODEL_NUM_LEN;
+
+/* Serial number string characteristic */
+static const uint8_t disValSnCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_SN_HDL), UINT16_TO_BYTES(ATT_UUID_SERIAL_NUMBER)};
+static const uint16_t disLenSnCh = sizeof(disValSnCh);
+
+/* Serial number string */
+static const uint8_t disUuSn[] = {UINT16_TO_BYTES(ATT_UUID_SERIAL_NUMBER)};
+static uint8_t disValSn[DIS_MAXSIZE_SN_ATT] = DIS_DEFAULT_SERIAL_NUM;
+static uint16_t disLenSn = DIS_DEFAULT_SERIAL_NUM_LEN;
+
+/* Firmware revision string characteristic */
+static const uint8_t disValFwrCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_FWR_HDL), UINT16_TO_BYTES(ATT_UUID_FIRMWARE_REV)};
+static const uint16_t disLenFwrCh = sizeof(disValFwrCh);
+
+/* Firmware revision string */
+static const uint8_t disUuFwr[] = {UINT16_TO_BYTES(ATT_UUID_FIRMWARE_REV)};
+static uint8_t disValFwr[DIS_MAXSIZE_FWR_ATT] = DIS_DEFAULT_FW_REV;
+static uint16_t disLenFwr = DIS_DEFAULT_FW_REV_LEN;
+
+/* Hardware revision string characteristic */
+static const uint8_t disValHwrCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_HWR_HDL), UINT16_TO_BYTES(ATT_UUID_HARDWARE_REV)};
+static const uint16_t disLenHwrCh = sizeof(disValHwrCh);
+
+/* Hardware revision string */
+static const uint8_t disUuHwr[] = {UINT16_TO_BYTES(ATT_UUID_HARDWARE_REV)};
+static uint8_t disValHwr[DIS_MAXSIZE_HWR_ATT] = DIS_DEFAULT_HW_REV;
+static uint16_t disLenHwr = DIS_DEFAULT_HW_REV_LEN;
+
+/* Software revision string characteristic */
+static const uint8_t disValSwrCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_SWR_HDL), UINT16_TO_BYTES(ATT_UUID_SOFTWARE_REV)};
+static const uint16_t disLenSwrCh = sizeof(disValSwrCh);
+
+/* Software revision string */
+static const uint8_t disUuSwr[] = {UINT16_TO_BYTES(ATT_UUID_SOFTWARE_REV)};
+static uint8_t disValSwr[DIS_MAXSIZE_SWR_ATT] = DIS_DEFAULT_SW_REV;
+static uint16_t disLenSwr = DIS_DEFAULT_SW_REV_LEN;
+
+/* Registration certificate data characteristic */
+static const uint8_t disValRcdCh[] = {ATT_PROP_READ, UINT16_TO_BYTES(DIS_RCD_HDL), UINT16_TO_BYTES(ATT_UUID_11073_CERT_DATA)};
+static const uint16_t disLenRcdCh = sizeof(disValRcdCh);
+
+/* Registration certificate data */
+static const uint8_t disUuRcd[] = {UINT16_TO_BYTES(ATT_UUID_11073_CERT_DATA)};
+static uint8_t disValRcd[DIS_SIZE_RCD_ATT] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+static const uint16_t disLenRcd = sizeof(disValRcd);
+
+/* Attribute list for dis group */
+static const attsAttr_t disList[] =
+{
+  {
+    attPrimSvcUuid,
+    (uint8_t *) disValSvc,
+    (uint16_t *) &disLenSvc,
+    sizeof(disValSvc),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValMfrCh,
+    (uint16_t *) &disLenMfrCh,
+    sizeof(disValMfrCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuMfr,
+    (uint8_t *) disValMfr,
+    (uint16_t *) &disLenMfr,
+    sizeof(disValMfr),
+    ATTS_SET_VARIABLE_LEN,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValSidCh,
+    (uint16_t *) &disLenSidCh,
+    sizeof(disValSidCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuSid,
+    disValSid,
+    (uint16_t *) &disLenSid,
+    sizeof(disValSid),
+    0,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValMnCh,
+    (uint16_t *) &disLenMnCh,
+    sizeof(disValMnCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuMn,
+    (uint8_t *) disValMn,
+    (uint16_t *) &disLenMn,
+    sizeof(disValMn),
+    ATTS_SET_VARIABLE_LEN,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValSnCh,
+    (uint16_t *) &disLenSnCh,
+    sizeof(disValSnCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuSn,
+    (uint8_t *) disValSn,
+    (uint16_t *) &disLenSn,
+    sizeof(disValSn),
+    ATTS_SET_VARIABLE_LEN,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValFwrCh,
+    (uint16_t *) &disLenFwrCh,
+    sizeof(disValFwrCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuFwr,
+    (uint8_t *) disValFwr,
+    (uint16_t *) &disLenFwr,
+    sizeof(disValFwr),
+    ATTS_SET_VARIABLE_LEN,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValHwrCh,
+    (uint16_t *) &disLenHwrCh,
+    sizeof(disValHwrCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuHwr,
+    (uint8_t *) disValHwr,
+    (uint16_t *) &disLenHwr,
+    sizeof(disValHwr),
+    ATTS_SET_VARIABLE_LEN,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValSwrCh,
+    (uint16_t *) &disLenSwrCh,
+    sizeof(disValSwrCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuSwr,
+    (uint8_t *) disValSwr,
+    (uint16_t *) &disLenSwr,
+    sizeof(disValSwr),
+    ATTS_SET_VARIABLE_LEN,
+    DIS_SEC_PERMIT_READ
+  },
+  {
+    attChUuid,
+    (uint8_t *) disValRcdCh,
+    (uint16_t *) &disLenRcdCh,
+    sizeof(disValRcdCh),
+    0,
+    ATTS_PERMIT_READ
+  },
+  {
+    disUuRcd,
+    (uint8_t *) disValRcd,
+    (uint16_t *) &disLenRcd,
+    sizeof(disValRcd),
+    0,
+    DIS_SEC_PERMIT_READ
+  },
+};
+
+/* DIS group structure */
+static attsGroup_t svcDisGroup =
+{
+  NULL,
+  (attsAttr_t *) disList,
+  NULL,
+  NULL,
+  DIS_START_HDL,
+  DIS_END_HDL
+};
+
+WSF_CT_ASSERT(((sizeof(disList) / sizeof(disList[0])) == DIS_END_HDL - DIS_START_HDL + 1));
+/*************************************************************************************************/
+/*!
+ *  \brief  Add the services to the attribute server.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcDisAddGroup(void)
+{
+  AttsAddGroup(&svcDisGroup);
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Remove the services from the attribute server.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void SvcDisRemoveGroup(void)
+{
+  AttsRemoveGroup(DIS_START_HDL);
+}
+/* clang-format on */
diff --git a/epicardium/ble/uart.c b/epicardium/ble/uart.c
new file mode 100644
index 0000000000000000000000000000000000000000..d902e3426f1adb4a5e92538beb897a2e2bcc2ef5
--- /dev/null
+++ b/epicardium/ble/uart.c
@@ -0,0 +1,175 @@
+#include "modules/modules.h"
+
+#include "wsf_types.h"
+#include "util/bstream.h"
+#include "att_api.h"
+
+#include "FreeRTOS.h"
+#include "timers.h"
+
+#include <stdio.h>
+#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 uint8_t uartRxCh[] = {ATT_PROP_WRITE, UINT16_TO_BYTES(UART_RX_HDL), 0x9E,0xCA,0xDC,0x24,0x0E,0xE5,0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x02,0x00,0x40,0x6E};
+const uint8_t attUartRxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x02,0x00,0x40,0x6E};
+
+static const uint8_t uartTxCh[] = {ATT_PROP_READ | ATT_PROP_NOTIFY, UINT16_TO_BYTES(UART_TX_HDL), 0x9E,0xCA,0xDC,0x24,0x0E,0xE5,0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x03,0x00,0x40,0x6E};
+const uint8_t attUartTxChUuid[] = {0x9E,0xCA,0xDC,0x24,0x0E,0xE5, 0xA9,0xE0,0x93,0xF3,0xA3,0xB5,0x03,0x00,0x40,0x6E};
+/* clang-format on */
+
+static void *SvcUARTAddGroupDyn(void)
+{
+	void *pSHdl;
+	uint8_t initCcc[] = { UINT16_TO_BYTES(0x0000) };
+
+	/* Create the service */
+	pSHdl = AttsDynCreateGroup(UART_START_HDL, UART_END_HDL);
+
+	if (pSHdl != NULL) {
+		/* clang-format off */
+		/* Primary service */
+		AttsDynAddAttrConst( pSHdl, attPrimSvcUuid, UARTSvc, sizeof(UARTSvc),
+			0, ATTS_PERMIT_READ);
+
+		/* UART rx characteristic */
+		AttsDynAddAttrConst( pSHdl, attChUuid, uartRxCh, sizeof(uartRxCh),
+			0, ATTS_PERMIT_READ);
+		// XXX: attUartRxChUuid is 16 bytes but nothing says so....
+		/* UART rx value */
+		// XXX: not sure if max value of 128 is fine...
+		AttsDynAddAttr( pSHdl, attUartRxChUuid, NULL, 0, 128,
+			ATTS_SET_WRITE_CBACK | ATTS_SET_VARIABLE_LEN, ATTS_PERMIT_WRITE);
+
+		/* UART tx characteristic */
+		AttsDynAddAttrConst( pSHdl, attChUuid, uartTxCh, sizeof(uartTxCh),
+			0, ATTS_PERMIT_READ);
+		/* UART tx value */
+		/* TODO: do we need ATTS_SET_READ_CBACK ? */
+		AttsDynAddAttr( pSHdl, attUartTxChUuid, NULL, 0, sizeof(uint8_t),
+			ATTS_SET_READ_CBACK, ATTS_PERMIT_READ);
+		/* UART tx CCC descriptor */
+		AttsDynAddAttr( pSHdl, attCliChCfgUuid, initCcc, sizeof(uint16_t), sizeof(uint16_t),
+			ATTS_SET_CCC, ATTS_PERMIT_READ | ATTS_PERMIT_WRITE);
+		/* clang-format on */
+	}
+
+	return pSHdl;
+}
+
+dmConnId_t active_connection = 0;
+
+static uint8_t UARTReadCback(
+	dmConnId_t connId,
+	uint16_t handle,
+	uint8_t operation,
+	uint16_t offset,
+	attsAttr_t *pAttr
+) {
+	printf("read callback\n");
+	return ATT_SUCCESS;
+}
+
+static uint8_t UARTWriteCback(
+	dmConnId_t connId,
+	uint16_t handle,
+	uint8_t operation,
+	uint16_t offset,
+	uint16_t len,
+	uint8_t *pValue,
+	attsAttr_t *pAttr
+) {
+	active_connection = connId;
+
+	//printf("UARTWriteCback %d: ", len);
+	int i;
+	for (i = 0; i < len; i++) {
+		//printf("%c", pValue[i]);
+		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;
+}
+
+uint8_t ble_uart_tx_buf[129];
+uint8_t ble_uart_buf_tx_fill;
+int ble_uart_lasttick = 0;
+
+void ble_uart_write(uint8_t *pValue, uint8_t len)
+{
+	int i;
+	for (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++;
+		} else if (pValue[i] == '\r' || pValue[i] == '\n') {
+			if (ble_uart_buf_tx_fill > 0) {
+				AttsSetAttr(
+					UART_TX_HDL,
+					ble_uart_buf_tx_fill,
+					ble_uart_tx_buf
+				);
+				if (active_connection) {
+					int x = xTaskGetTickCount() -
+						ble_uart_lasttick;
+					if (x < 100) {
+						// Ugly hack if we already send something recently.
+						// TODO: figure out how fast we can send or use indications
+						vTaskDelay(100 - x);
+					}
+					//printf("notify: ");
+					//int j;
+					//for(j=0;j<ble_uart_buf_tx_fill;j++) {
+					//    printf("%02x ", ble_uart_tx_buf[j]);
+					//}
+					//printf("\n");
+					AttsHandleValueNtf(
+						active_connection,
+						UART_TX_HDL,
+						ble_uart_buf_tx_fill,
+						ble_uart_tx_buf
+					);
+					ble_uart_lasttick = xTaskGetTickCount();
+				}
+				ble_uart_buf_tx_fill = 0;
+			}
+		}
+	}
+}
+
+void bleuart_init(void)
+{
+	/* Add the UART service dynamically */
+	void *pSHdl;
+	pSHdl = SvcUARTAddGroupDyn();
+	AttsDynRegister(pSHdl, UARTReadCback, UARTWriteCback);
+	//AttsDynRegister(pSHdl, NULL, UARTWriteCback);
+}
diff --git a/epicardium/main.c b/epicardium/main.c
index c90d008248e0562c184701099be9ff59d7639b60..ebadaf954765295f48d33fe1e703d60ccbaaab05 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -25,6 +25,8 @@
 
 TaskHandle_t dispatcher_task_id;
 
+void vBleTask(void *pvParameters);
+
 /*
  * API dispatcher task.  This task will sleep until an API call is issued and
  * then wake up to dispatch it.
@@ -103,6 +105,18 @@ int main(void)
 		abort();
 	}
 
+	/* BLE */
+	if (xTaskCreate(
+		    vBleTask,
+		    (const char *)"BLE",
+		    configMINIMAL_STACK_SIZE * 10,
+		    NULL,
+		    tskIDLE_PRIORITY + 1,
+		    NULL) != pdPASS) {
+		LOG_CRIT("startup", "Failed to create %s task!", "BLE");
+		abort();
+	}
+
 	LOG_INFO("startup", "Initializing dispatcher ...");
 	api_dispatcher_init();
 
diff --git a/epicardium/meson.build b/epicardium/meson.build
index c5dc701d6164c79246e9a6aa8bc761c01626b32e..9b3880a81e8dcf3ea17ef76411328cc54a2f15f1 100644
--- a/epicardium/meson.build
+++ b/epicardium/meson.build
@@ -65,6 +65,7 @@ freertos = static_library(
 ##########################################################################
 
 subdir('modules/')
+subdir('ble/')
 
 subdir('l0der/')
 
@@ -75,7 +76,8 @@ elf = executable(
   'support.c',
   module_sources,
   l0der_sources,
-  dependencies: [libcard10, max32665_startup_core0, maxusb, libff13],
+  ble_sources,
+  dependencies: [libcard10, max32665_startup_core0, maxusb, libff13, ble],
   link_with: [api_dispatcher_lib, freertos],
   link_whole: [max32665_startup_core0_lib, board_card10_lib, newlib_heap_lib],
   include_directories: [freertos_includes],
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 113e3cb8ecaebc932a65ce9e6124cc2ff3be60ce..d02549569f0ae789435567c1115864a1fa2f860f 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -9,5 +9,5 @@ module_sources = files(
   'stream.c',
   'vibra.c',
   'light_sensor.c',
-  'rtc.c'
+  'rtc.c',
 )
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 182fdae7e1bee27bce1b3d56d94f35bce9eb979e..5dbda13c1af2b98a863077e8813fda66c3fbede2 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -1,6 +1,7 @@
 #ifndef MODULES_H
 #define MODULES_H
 
+#include <stdint.h>
 /* ---------- FAT fs ------------------------------------------------------ */
 /* Number of bits to use for indexing into our internal pool of files/directories
  * This indirectly specifies the size of the pool as 2^EPIC_FAT_FD_INDEX_BITS
@@ -15,13 +16,17 @@ void fatfs_init(void);
 #define SERIAL_READ_BUFFER_SIZE 128
 void vSerialTask(void *pvParameters);
 
+void serial_enqueue_char(char chr);
+
 /* ---------- PMIC --------------------------------------------------------- */
 /* In 1/10s */
 #define PMIC_PRESS_SLEEP           20
 #define PMIC_PRESS_POWEROFF        40
 void vPmicTask(void *pvParameters);
 
+/* ---------- BLE ---------------------------------------------------------- */
+void ble_uart_write(uint8_t *pValue, uint8_t len);
+
 // Forces an unlock of the display. Only to be used in epicardium
 void disp_forcelock();
-
 #endif /* MODULES_H */
diff --git a/epicardium/modules/serial.c b/epicardium/modules/serial.c
index 8cffa3ac40f8a66ddcd7dc0c5491cc4b5d8b59f8..db188ebe4474e8ac03cf0dbe773bf00e28e49e6e 100644
--- a/epicardium/modules/serial.c
+++ b/epicardium/modules/serial.c
@@ -29,6 +29,7 @@ void epic_uart_write_str(const char *str, intptr_t length)
 {
 	UART_Write(ConsoleUart, (uint8_t *)str, length);
 	cdcacm_write((uint8_t *)str, length);
+	ble_uart_write((uint8_t *)str, length);
 }
 
 /*
@@ -72,7 +73,7 @@ static void uart_callback(uart_req_t *req, int error)
 	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
 }
 
-static void enqueue_char(char chr)
+void serial_enqueue_char(char chr)
 {
 	if (chr == 0x3) {
 		/* Control-C */
@@ -122,15 +123,15 @@ void vSerialTask(void *pvParameters)
 		ulTaskNotifyTake(pdTRUE, portTICK_PERIOD_MS * 1000);
 
 		if (read_req.num > 0) {
-			enqueue_char(*read_req.data);
+			serial_enqueue_char(*read_req.data);
 		}
 
 		while (UART_NumReadAvail(ConsoleUart) > 0) {
-			enqueue_char(UART_ReadByte(ConsoleUart));
+			serial_enqueue_char(UART_ReadByte(ConsoleUart));
 		}
 
 		while (cdcacm_num_read_avail() > 0) {
-			enqueue_char(cdcacm_read());
+			serial_enqueue_char(cdcacm_read());
 		}
 	}
 }
diff --git a/lib/sdk/Libraries/BTLE/meson.build b/lib/sdk/Libraries/BTLE/meson.build
index b80f44af67413181f8e89fd75159b4f861a1f354..ab0db720687c87a7cc2763511090fbcfd2370104 100644
--- a/lib/sdk/Libraries/BTLE/meson.build
+++ b/lib/sdk/Libraries/BTLE/meson.build
@@ -1,3 +1,8 @@
+# cordio-phy.a
+cc = meson.get_compiler('c')
+libcordiophy = cc.find_library('cordio-phy', dirs: meson.current_source_dir())
+
+
 includes = include_directories(
   'wsf/include',
   'stack/ble-host/include',
@@ -45,7 +50,7 @@ sources = files(
 'stack/ble-profiles/sources/apps/watch/watch_main.c',
 'stack/ble-profiles/sources/apps/datc/datc_main.c',
 'stack/ble-profiles/sources/apps/gluc/gluc_main.c',
-'stack/ble-profiles/sources/apps/fit/fit_main.c',
+#'stack/ble-profiles/sources/apps/fit/fit_main.c',
 'stack/ble-profiles/sources/apps/cycling/cycling_main.c',
 'stack/ble-profiles/sources/apps/sensor/sensor_main.c',
 'stack/ble-profiles/sources/apps/app/app_disc.c',
@@ -54,11 +59,11 @@ sources = files(
 'stack/ble-profiles/sources/apps/app/app_master_ae.c',
 'stack/ble-profiles/sources/apps/app/app_slave_leg.c',
 'stack/ble-profiles/sources/apps/app/app_slave_ae.c',
-'stack/ble-profiles/sources/apps/app/app_main.c',
+#'stack/ble-profiles/sources/apps/app/app_main.c',
 'stack/ble-profiles/sources/apps/app/app_server.c',
 'stack/ble-profiles/sources/apps/app/app_master_leg.c',
-'stack/ble-profiles/sources/apps/app/common/app_db.c',
-'stack/ble-profiles/sources/apps/app/common/app_ui.c',
+#'stack/ble-profiles/sources/apps/app/common/app_db.c',
+#'stack/ble-profiles/sources/apps/app/common/app_ui.c',
 'stack/ble-profiles/sources/apps/app/common/app_hw.c',
 'stack/ble-profiles/sources/apps/app/app_terminal.c',
 'stack/ble-profiles/sources/apps/wdxs/wdxs_oad.c',
@@ -111,7 +116,7 @@ sources = files(
 'stack/ble-profiles/sources/services/svc_rscs.c',
 'stack/ble-profiles/sources/services/svc_hts.c',
 'stack/ble-profiles/sources/services/svc_temp.c',
-'stack/ble-profiles/sources/services/svc_core.c',
+#'stack/ble-profiles/sources/services/svc_core.c',
 'stack/ble-profiles/sources/services/svc_hid.c',
 'stack/ble-profiles/sources/services/svc_scpss.c',
 'stack/ble-profiles/sources/services/svc_hrs.c',
@@ -120,7 +125,7 @@ sources = files(
 'stack/ble-profiles/sources/services/svc_ipss.c',
 'stack/ble-profiles/sources/services/svc_wdxs.c',
 'stack/ble-profiles/sources/services/svc_uricfg.c',
-'stack/ble-profiles/sources/services/svc_dis.c',
+#'stack/ble-profiles/sources/services/svc_dis.c',
 'stack/ble-profiles/sources/services/svc_px.c',
 'stack/ble-profiles/sources/services/svc_batt.c',
 'stack/ble-profiles/sources/services/svc_wss.c',
@@ -409,22 +414,40 @@ sources = files(
 'link_layer/controller/sources/common/sch/sch_list.c',
 )
 
+
+ble_compileargs = [
+  '-DWSF_ASSERT_ENABLED=TRUE',
+  '-DWSF_MS_PER_TICK=1',
+  '-DINIT_BROADCASTER',
+  '-DINIT_PERIPHERAL',
+  '-DINIT_ENCRYPTED',
+]
+
+if get_option('ble_trace')
+  ble_compileargs += [
+    '-DWSF_TRACE_ENABLED=TRUE',
+  ]
+endif
+
 lib = static_library(
   'ble',
   sources,
   include_directories: includes,
-  dependencies: periphdriver,
-  c_args: ['-w',
-            '-DLHCI_ENABLE_VS=TRUE',
-            '-DBB_CLK_RATE_HZ=1600000',
-            '-DLCTR_CONN_NO_TIFS_REASSEMBLY=1',
-            '-DBB_ENABLE_INLINE_ENC_TX=1',
-            '-DBB_ENABLE_INLINE_DEC_RX=1',
-            '-DFORCE_PMU_WAKEUP=1']
+  dependencies: [periphdriver, libcordiophy],
+  c_args: [
+    '-w',
+    '-DLHCI_ENABLE_VS=TRUE',
+    '-DBB_CLK_RATE_HZ=1600000',
+    '-DLCTR_CONN_NO_TIFS_REASSEMBLY=1',
+    '-DBB_ENABLE_INLINE_ENC_TX=1',
+    '-DBB_ENABLE_INLINE_DEC_RX=1',
+    '-DFORCE_PMU_WAKEUP=1',
+  ] + ble_compileargs,
 )
 
 ble = declare_dependency(
   include_directories: includes,
   link_with: lib,
-  dependencies: periphdriver,
+  compile_args: ble_compileargs,
+  dependencies: [periphdriver, libcordiophy],
 )
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-host/include/svc_core.h b/lib/sdk/Libraries/BTLE/stack/ble-host/include/svc_core.h
deleted file mode 100644
index 3555b57143c98c147c3ba88abfee9574ce6656bf..0000000000000000000000000000000000000000
--- a/lib/sdk/Libraries/BTLE/stack/ble-host/include/svc_core.h
+++ /dev/null
@@ -1,171 +0,0 @@
-/* Copyright (c) 2009-2019 Arm Limited
- * SPDX-License-Identifier: Apache-2.0
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*************************************************************************************************/
-/*!
- *  \brief Example GATT and GAP service implementations.
- */
-/*************************************************************************************************/
-
-#ifndef SVC_CORE_H
-#define SVC_CORE_H
-
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/*! \addtogroup GATT_AND_GAP_SERVICE
- *  \{ */
-
-
-/**************************************************************************************************
- Handle Ranges
-**************************************************************************************************/
-/** \name GAP Service Handles
- * \note GAP -- RPAO characterstic added only when DM Privacy enabled
- */
-/**@{*/
-#define GAP_START_HDL               0x01               /*!< \brief GAP start handle */
-#define GAP_END_HDL                 (GAP_MAX_HDL - 3)  /*!< \brief GAP end handle */
-/**@}*/
-
-/** \name GATT Service Handles
- *
- */
-/**@{*/
-#define GATT_START_HDL              0x10                /*!< \brief GATT start handle */
-#define GATT_END_HDL                (GATT_MAX_HDL - 1)  /*!< \brief GATT end handle */
-/**@}*/
-
-/**************************************************************************************************
- Handles
-**************************************************************************************************/
-
-/** \name GAP Service Handles
- *
- */
-/**@{*/
-/*! \brief GAP service handle */
-enum
-{
-  GAP_SVC_HDL = GAP_START_HDL,      /*!< \brief GAP service declaration */
-  GAP_DN_CH_HDL,                    /*!< \brief Device name characteristic */
-  GAP_DN_HDL,                       /*!< \brief Device name */
-  GAP_AP_CH_HDL,                    /*!< \brief Appearance characteristic */
-  GAP_AP_HDL,                       /*!< \brief Appearance */
-  GAP_CAR_CH_HDL,                   /*!< \brief Central address resolution characteristic */
-  GAP_CAR_HDL,                      /*!< \brief Central address resolution */
-  GAP_RPAO_CH_HDL,                  /*!< \brief Resolvable private address only characteristic */
-  GAP_RPAO_HDL,                     /*!< \brief Resolvable private address only */
-  GAP_MAX_HDL                       /*!< \brief GAP maximum handle */
-};
-/**@}*/
-
-/** \name GATT Service Handles
- *
- */
-/**@{*/
-/*! \brief GATT service handles */
-enum
-{
-  GATT_SVC_HDL = GATT_START_HDL,    /*!< \brief GATT service declaration */
-  GATT_SC_CH_HDL,                   /*!< \brief Service changed characteristic */
-  GATT_SC_HDL,                      /*!< \brief Service changed */
-  GATT_SC_CH_CCC_HDL,               /*!< \brief Service changed client characteristic configuration descriptor */
-  GATT_CSF_CH_HDL,                  /*!< \brief Client supported features characteristic */
-  GATT_CSF_HDL,                     /*!< \brief Client supported features */
-  GATT_DBH_CH_HDL,                  /*!< \brief Database hash characteristic */
-  GATT_DBH_HDL,                     /*!< \brief Database hash */
-  GATT_MAX_HDL                      /*!< \brief GATT maximum handle */
-};
-/**@}*/
-
-/**************************************************************************************************
-  Function Declarations
-**************************************************************************************************/
-
-/*************************************************************************************************/
-/*!
- *  \brief  Add the services to the attribute server.
- *
- *  \return None.
- */
-/*************************************************************************************************/
-void SvcCoreAddGroup(void);
-
-/*************************************************************************************************/
-/*!
- *  \brief  Remove the services from the attribute server.
- *
- *  \return None.
- */
-/*************************************************************************************************/
-void SvcCoreRemoveGroup(void);
-
-/*************************************************************************************************/
-/*!
- *  \brief  Register callbacks for the service.
- *
- *  \param  readCback   Read callback function.
- *  \param  writeCback  Write callback function.
- *
- *  \return None.
- */
-/*************************************************************************************************/
-void SvcCoreGattCbackRegister(attsReadCback_t readCback, attsWriteCback_t writeCback);
-
-/*************************************************************************************************/
-/*!
- *  \brief  Register callbacks for the service.
- *
- *  \param  readCback   Read callback function.
- *  \param  writeCback  Write callback function.
- *
- *  \return None.
- */
-/*************************************************************************************************/
-void SvcCoreGapCbackRegister(attsReadCback_t readCback, attsWriteCback_t writeCback);
-
-/*************************************************************************************************/
-/*!
- *  \brief  Update the central address resolution attribute value.
- *
- *  \param  value   New value.
- *
- *  \return None.
- */
-/*************************************************************************************************/
-void SvcCoreGapCentAddrResUpdate(bool_t value);
-
-/*************************************************************************************************/
-/*!
- *  \brief  Add the Resolvable Private Address Only (RPAO) characteristic to the GAP service.
- *          The RPAO characteristic should be added only when DM Privacy is enabled.
- *
- *  \return None.
- */
-/*************************************************************************************************/
-void SvcCoreGapAddRpaoCh(void);
-
-/*! \} */    /* GATT_AND_GAP_SERVICE */
-
-#ifdef __cplusplus
-};
-#endif
-
-#endif /* SVC_CORE_H */
-
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_core.h b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_core.h
index 7f246ad711b25e8c6083dc1d05f44e1689dfa003..05dbe5e79d65dd6f45acb23d57e8a5e1e6bb91db 100644
--- a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_core.h
+++ b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_core.h
@@ -33,6 +33,8 @@ extern "C" {
  Handle Ranges
 **************************************************************************************************/
 /** \name GAP Service Handles
+ * TODO card10:
+ * WTF!
  * \note GAP -- RPAO characterstic added only when DM Privacy enabled
  */
 /**@{*/
diff --git a/lib/sdk/Libraries/BTLE/stack/platform/max32665/wsf_os.c b/lib/sdk/Libraries/BTLE/stack/platform/max32665/wsf_os.c
index d4e0c647b0d4be27ee15b89e2b368ff8a72916bf..62012a7d4bb84a58ac72aff1bd1eff0c3047ff8b 100644
--- a/lib/sdk/Libraries/BTLE/stack/platform/max32665/wsf_os.c
+++ b/lib/sdk/Libraries/BTLE/stack/platform/max32665/wsf_os.c
@@ -132,7 +132,7 @@ void WsfSetEvent(wsfHandlerId_t handlerId, wsfEventMask_t event)
   WSF_CS_EXIT(cs);
 
   /* set event in OS */
-  // wsf_mbed_ble_signal_event();
+  wsf_ble_signal_event();
 }
 
 /*************************************************************************************************/
@@ -157,7 +157,7 @@ void WsfTaskSetReady(wsfHandlerId_t handlerId, wsfTaskEvent_t event)
   WSF_CS_EXIT(cs);
 
   /* set event in OS */
-  // wsf_mbed_ble_signal_event();
+  wsf_ble_signal_event();
 }
 
 /*************************************************************************************************/
diff --git a/lib/sdk/Libraries/BTLE/wsf/include/wsf_os.h b/lib/sdk/Libraries/BTLE/wsf/include/wsf_os.h
index 674ffb73d1f572074448d48066ab141b0c337304..a9ffd502c8009414b4501723a6667bb79c24f950 100644
--- a/lib/sdk/Libraries/BTLE/wsf/include/wsf_os.h
+++ b/lib/sdk/Libraries/BTLE/wsf/include/wsf_os.h
@@ -216,6 +216,7 @@ void wsfOsDispatcher(void);
 /*************************************************************************************************/
 void WsfOsInit(void);
 
+void wsf_ble_signal_event(void);
 /*! \} */    /* WSF_OS_API */
 
 #ifdef __cplusplus
diff --git a/lib/sdk/Libraries/BTLE/wsf/include/wsf_timer.h b/lib/sdk/Libraries/BTLE/wsf/include/wsf_timer.h
index 41259e38c80ec073d008931f4d7f6896ef64bbf1..0ba11f66625fc3c719cf18209f637166f607add9 100644
--- a/lib/sdk/Libraries/BTLE/wsf/include/wsf_timer.h
+++ b/lib/sdk/Libraries/BTLE/wsf/include/wsf_timer.h
@@ -164,6 +164,8 @@ void WsfTimerSleep(void);
 /*************************************************************************************************/
 void WsfTimerSleepUpdate(void);
 
+void WsfTimerNotify(void);
+
 /*! \} */    /* WSF_TIMER_API */
 
 #ifdef __cplusplus
diff --git a/lib/sdk/Libraries/BTLE/wsf/sources/port/baremetal/wsf_timer.c b/lib/sdk/Libraries/BTLE/wsf/sources/port/baremetal/wsf_timer.c
index 3ed48a4360efaee01858a83c15e31c64b8afd888..4b0134315c96f2e025f85413830ff80240654c1b 100644
--- a/lib/sdk/Libraries/BTLE/wsf/sources/port/baremetal/wsf_timer.c
+++ b/lib/sdk/Libraries/BTLE/wsf/sources/port/baremetal/wsf_timer.c
@@ -111,7 +111,9 @@ static void wsfTimerRemove(wsfTimer_t *pTimer)
 {
   wsfTimer_t  *pElem;
   wsfTimer_t  *pPrev = NULL;
+  bool_t      newHead = FALSE;
 
+  /* TODO: why is there no lock here? */
   pElem = (wsfTimer_t *) wsfTimerTimerQueue.pHead;
 
   /* find timer in queue */
@@ -128,10 +130,21 @@ static void wsfTimerRemove(wsfTimer_t *pTimer)
   /* if timer found remove from queue */
   if (pElem != NULL)
   {
+    if (pElem == wsfTimerTimerQueue.pHead)
+    {
+      newHead = TRUE;
+    }
     WsfQueueRemove(&wsfTimerTimerQueue, pTimer, pPrev);
 
     pTimer->isStarted = FALSE;
   }
+
+  if (newHead)
+  {
+    /* We have a new head. Notify the OS. */
+    /* TODO: Not sure if this should be inside a lock */
+    WsfTimerNotify();
+  }
 }
 
 /*************************************************************************************************/
@@ -179,6 +192,13 @@ static void wsfTimerInsert(wsfTimer_t *pTimer, wsfTimerTicks_t ticks)
 
   /* task schedule unlock */
   WsfTaskUnlock();
+
+  if(wsfTimerTimerQueue.pHead == pTimer)
+  {
+    /* The timer is new head. Notify the OS. */
+    /* TODO: Not sure if this should be inside the lock */
+    WsfTimerNotify();
+  }
 }
 
 /*************************************************************************************************/
@@ -380,6 +400,10 @@ wsfTimer_t *WsfTimerServiceExpired(wsfTaskId_t taskId)
 
     WSF_TRACE_INFO1("Timer expired pTimer:0x%x", pElem);
 
+    /* We have a new head. Notify the OS. */
+    /* TODO: Not sure if this should be inside the lock */
+    WsfTimerNotify();
+
     /* return timer */
     return pElem;
   }
diff --git a/meson_options.txt b/meson_options.txt
index e0a79c540133acc94716e895ed5e8697cfe2847b..bb96608f53ba3363e9e5a3b96dfb6eb6e11086df 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -5,3 +5,11 @@ option(
 
   description: 'Whether to print debug messages on the serial console'
 )
+
+option(
+  'ble_trace',
+  type: 'boolean',
+  value: false,
+
+  description: 'Whether to enable WSF TRACE prints',
+)