diff --git a/Documentation/card10-cfg.rst b/Documentation/card10-cfg.rst
index c96a9a0dd865299776f9640c02d44f76884075c9..d37bedca43a49eec53be6a23e2383127a485c903 100644
--- a/Documentation/card10-cfg.rst
+++ b/Documentation/card10-cfg.rst
@@ -47,6 +47,8 @@ Option name        Type       Description
 ------------------ ---------- -----------
 ``ble_mac``        Boolean    MAC address used for BLE. Format: ``ca:4d:10:xx:xx:xx``.
 ------------------ ---------- -----------
+``ble_hid_enable`` Boolean    Enable the Human Interface Device (HID) characteristics on BLE.
+------------------ ---------- -----------
 ``ble_log_enable`` Boolean    Activate HCI level logging of BLE data. Creates a new btsnoop compatible log file named ``ble.log`` in the ``logs`` folder after each boot if BLE is activated. Keeps the last 10 files.
 ------------------ ---------- -----------
 ``right_scroll``   Boolean    Use both right buttons to scroll up and down. Lower left button is SELECT.
diff --git a/Documentation/conf.py b/Documentation/conf.py
index 10886a9fb03ead2822cafc09b08c717c2cc23b84..a09ee94ed0c473d97e1e1f216f6338df00294105 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -117,6 +117,7 @@ html_context = {
 autodoc_mock_imports = [
     "buttons",
     "interrupt",
+    "sys_ble_hid",
     "sys_bme680",
     "sys_bhi160",
     "sys_display",
diff --git a/Documentation/index.rst b/Documentation/index.rst
index 1090ad48cb9d2fce0e4cc5ed58512cd4b133c737..2816dcd17ed6f00868c18b0b8b6ef83c21c9cf9f 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -23,6 +23,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    pycardium/overview
    pycardium/stdlib
    pycardium/bhi160
+   pycardium/ble_hid
    pycardium/bme680
    pycardium/max30001
    pycardium/max86150
diff --git a/Documentation/pycardium/ble_hid.rst b/Documentation/pycardium/ble_hid.rst
new file mode 100644
index 0000000000000000000000000000000000000000..98f86b975e2afc9b04adc1ba91901dc2831685f3
--- /dev/null
+++ b/Documentation/pycardium/ble_hid.rst
@@ -0,0 +1,88 @@
+``ble_hid`` - BLE HID
+===============
+The ``ble_hid`` module provides access to the BLE Human Interface Device functionality.
+
+.. note::
+    Make sure to enable the BLE HID functionality in ``card10.cfg`` and reboot your card10
+    if you want to use BLE HID.
+
+    Also make sure that the ``adafruit_hid`` folder from the card10 release archive is placed
+    on the file system of your card10.
+
+
+.. warning::
+    At least Ubuntu Linux will keep auto connecting to BLE HID devices once they are
+    paired to the host computer. If you want to connect your card10 to a phone again,
+    you might have to temporarily turn off Bluetooth on your computer.
+
+An example application can be found in the preload directory (named ``HID Demo``). It provides
+examples how to use the card10 as keyboard, mouse or volume control.
+
+Please refer to the Adafruit CircuitPython HID library for examples how to use the HID service.
+The card10 implements the same HID descriptors as the Adafruit CircuitPython BLE library and
+should be compatible with all uses of the Adafruit CircuitPython HID library.
+
+**Example emulating a keyboard**:
+
+Adapted from https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/master/CPB_Keybutton_BLE/cpb_keybutton_ble.py
+
+A more complete version of this example can be found in the HID Demo app on your card10.
+
+.. code-block:: python
+
+    import ble_hid
+    from adafruit_hid.keyboard import Keyboard
+    from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
+    from adafruit_hid.keycode import Keycode
+
+    k = Keyboard(ble_hid.devices)
+    kl = KeyboardLayoutUS(k)
+
+    k.send(Keycode.BACKSPACE)
+
+    # use keyboard_layout for words
+    kl.write("Demo with a long text to show how fast a card10 can type!")
+
+    # add shift modifier
+    k.send(Keycode.SHIFT, Keycode.F19)
+
+
+**Example emulating a mouse**:
+
+.. code-block:: python
+
+    import ble_hid
+    import bhi160
+    import buttons
+    from adafruit_hid.mouse import Mouse
+
+    m = Mouse(ble_hid.devices)
+
+    def send_report(samples):
+        if len(samples) > 0:
+            x = -int(samples[0].z)
+            y = -int(samples[0].y)
+            m.move(x, y)
+
+    sensor = bhi160.BHI160Orientation(sample_rate=10, callback=send_report)
+
+    b_old = buttons.read()
+    while True:
+        b_new = buttons.read()
+        if not b_old == b_new:
+            print(b_new)
+            b_old = b_new
+            if b_new == buttons.TOP_RIGHT:
+                m.click(Mouse.MIDDLE_BUTTON)
+            elif b_new == buttons.BOTTOM_RIGHT:
+                m.click(Mouse.RIGHT_BUTTON)
+            elif b_new == buttons.BOTTOM_LEFT:
+                m.click(Mouse.LEFT_BUTTON)
+
+.. note::
+    Make sure to catch ``OSError`` exceptions in real applications. The exception will be thrown if
+    there is connection to the host (or if it is lost) and you want to send an event.
+
+
+.. automodule:: ble_hid
+   :members:
diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c
index 431b4310a59cd2b811d1ec39670d8a08023423f4..2e7a5cef4472d5688ae6e7a454eae686c1daf409 100644
--- a/epicardium/ble/ble_main.c
+++ b/epicardium/ble/ble_main.c
@@ -34,6 +34,7 @@
 #include "svc_hrs.h"
 #include "svc_dis.h"
 #include "svc_batt.h"
+#include "svc_hid.h"
 #include "svc_rscs.h"
 #include "bas/bas_api.h"
 #include "hrps/hrps_api.h"
@@ -41,11 +42,13 @@
 #include "profiles/gap_api.h"
 #include "cccd.h"
 #include "ess.h"
+#include "hid.h"
 
 #include "ble_api.h"
 #include "epicardium.h"
 #include "api/interrupt-sender.h"
 #include "modules/log.h"
+#include "modules/config.h"
 
 #define SCAN_REPORTS_NUM	16
 
@@ -187,6 +190,29 @@ static const uint8_t bleAdvDataDisc[] =
   0,                                      /*! tx power */
 };
 
+/*! advertising data, discoverable mode with HID service*/
+static const uint8_t bleAdvDataDiscHID[] =
+{
+  /*! flags */
+  2,                                      /*! length */
+  DM_ADV_TYPE_FLAGS,                      /*! AD type */
+  DM_FLAG_LE_LIMITED_DISC |               /*! flags */
+  DM_FLAG_LE_BREDR_NOT_SUP,
+
+  3,
+  DM_ADV_TYPE_APPEARANCE,
+  UINT16_TO_BYTES(CH_APPEAR_WATCH),
+
+  /*! service UUID list */
+  17,
+  DM_ADV_TYPE_128_UUID_PART,
+  CARD10_UUID_SUFFIX, 0x0, CARD10_UUID_PREFIX,
+
+  3,                                      /*! length */
+  DM_ADV_TYPE_16_UUID_PART,               /*! AD type */
+  UINT16_TO_BYTES(ATT_UUID_HID_SERVICE)
+};
+
 /*! scan data, discoverable mode */
 uint8_t bleScanDataDisc[] =
 {
@@ -232,6 +258,11 @@ static const attsCccSet_t bleCccSet[BLE_NUM_CCC_IDX] =
   {ESS_TEMP_CH_CCC_HDL,   ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_TEMP_CCC_IDX */
   {ESS_HUMI_CH_CCC_HDL,   ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_HUMI_CCC_IDX */
   {ESS_PRES_CH_CCC_HDL,   ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},   /* BLE_ESS_PRES_CCC_IDX */
+  {HID_MOUSE_BOOT_IN_CH_CCC_HDL,        ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_MBI_CCC_HDL */
+  {HID_KEYBOARD_BOOT_IN_CH_CCC_HDL,     ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_KBI_CCC_HDL */
+  {HID_INPUT_REPORT_1_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_KEYBOARD_CCC_HDL */
+  {HID_INPUT_REPORT_2_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_MOUSE_CCC_HDL */
+  {HID_INPUT_REPORT_3_CH_CCC_HDL,       ATT_CLIENT_CFG_NOTIFY,    DM_SEC_LEVEL_NONE},    /* HIDAPP_IN_CONSUMER_CCC_HDL */
 };
 
 /**************************************************************************************************
@@ -506,7 +537,12 @@ 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);
+  if(config_get_boolean_with_default("ble_hid_enable", false)) {
+    AppAdvSetData(APP_ADV_DATA_DISCOVERABLE, sizeof(bleAdvDataDiscHID), (uint8_t *) bleAdvDataDiscHID);
+  } else {
+    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 */
@@ -737,6 +773,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
 
     case ATTS_HANDLE_VALUE_CNF:
       BasProcMsg(&pMsg->hdr);
+      HidProcMsg(&pMsg->hdr);
       break;
 
     case ATTS_CCC_STATE_IND:
@@ -771,6 +808,7 @@ static void bleProcMsg(bleMsg_t *pMsg)
                connOpen->peerAddr[1], connOpen->peerAddr[0]);
       BasProcMsg(&pMsg->hdr);
       bleESS_ccc_update();
+      HidProcMsg(&pMsg->hdr);
       break;
 
     case DM_CONN_CLOSE_IND:
@@ -992,6 +1030,9 @@ void BleStart(void)
   SvcBattCbackRegister(BasReadCback, NULL);
   SvcBattAddGroup();
 
+  if(config_get_boolean_with_default("ble_hid_enable", false)) {
+    hid_init();
+  }
   /* Reset the device */
   DmDevReset();
 }
diff --git a/epicardium/ble/cccd.h b/epicardium/ble/cccd.h
index 2715f6ebb6252e4995b73bb7980795b4c0354083..ea91c630617a13ad159146cf782d6860dd50a88e 100644
--- a/epicardium/ble/cccd.h
+++ b/epicardium/ble/cccd.h
@@ -7,6 +7,11 @@ enum
   BLE_ESS_TEMP_CCC_IDX,                   /*! Environmental sensing service, temperature characteristic */
   BLE_ESS_HUMI_CCC_IDX,                   /*! Environmental sensing service, humidity characteristic */
   BLE_ESS_PRES_CCC_IDX,                   /*! Environmental sensing service, pressure characteristic */
+  HIDAPP_MBI_CCC_HDL,                   /*! HID Boot Mouse Input characteristic */
+  HIDAPP_KBI_CCC_HDL,                   /*! HID Boot Keyboard Input characteristic */
+  HIDAPP_IN_KEYBOARD_CCC_HDL,           /*! HID Input Report characteristic for keyboard inputs */
+  HIDAPP_IN_MOUSE_CCC_HDL,              /*! HID Input Report characteristic for mouse inputs */
+  HIDAPP_IN_CONSUMER_CCC_HDL,             /*! HID Input Report characteristic for consumer control inputs */
   BLE_NUM_CCC_IDX
 };
 
diff --git a/epicardium/ble/hid.c b/epicardium/ble/hid.c
new file mode 100644
index 0000000000000000000000000000000000000000..4386f5b1dd0cbbce6d1e59b33d4fb55a14315033
--- /dev/null
+++ b/epicardium/ble/hid.c
@@ -0,0 +1,229 @@
+/*
+ * Based on ble-profiles/sources/apps/hidapp/hidapp_main.c
+ */
+#include "cccd.h"
+#include "hid.h"
+
+#include "wsf_types.h"
+#include "dm_api.h"
+#include "att_api.h"
+#include "svc_hid.h"
+#include "hid/hid_api.h"
+
+#include "modules/log.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <stdint.h>
+
+/*! HidApp Report Map (Descriptor) */
+/* clang-format off */
+
+/* Based on https://github.com/adafruit/Adafruit_CircuitPython_BLE/blob/master/adafruit_ble/services/standard/hid.py */
+const uint8_t hidReportMap[] =
+{
+	0x05, 0x01,  /* Usage Page (Generic Desktop Ctrls) */
+	0x09, 0x06,  /* Usage (Keyboard) */
+	0xA1, 0x01,  /* Collection (Application) */
+	0x85, HIDAPP_KEYBOARD_REPORT_ID,  /*   Report ID (1) */
+	0x05, 0x07,  /*   Usage Page (Kbrd/Keypad) */
+	0x19, 0xE0,  /*   Usage Minimum (\xE0) */
+	0x29, 0xE7,  /*   Usage Maximum (\xE7) */
+	0x15, 0x00,  /*   Logical Minimum (0) */
+	0x25, 0x01,  /*   Logical Maximum (1) */
+	0x75, 0x01,  /*   Report Size (1) */
+	0x95, 0x08,  /*   Report Count (8) */
+	0x81, 0x02,  /*   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0x81, 0x01,  /*   Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0x19, 0x00,  /*   Usage Minimum (\x00) */
+	0x29, 0x89,  /*   Usage Maximum (\x89) */
+	0x15, 0x00,  /*   Logical Minimum (0) */
+	0x25, 0x89,  /*   Logical Maximum (137) */
+	0x75, 0x08,  /*   Report Size (8) */
+	0x95, 0x06,  /*   Report Count (6) */
+	0x81, 0x00,  /*   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0x05, 0x08,  /*   Usage Page (LEDs) */
+	0x19, 0x01,  /*   Usage Minimum (Num Lock) */
+	0x29, 0x05,  /*   Usage Maximum (Kana) */
+	0x15, 0x00,  /*   Logical Minimum (0) */
+	0x25, 0x01,  /*   Logical Maximum (1) */
+	0x75, 0x01,  /*   Report Size (1) */
+	0x95, 0x05,  /*   Report Count (5) */
+	0x91, 0x02,  /*   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
+	0x95, 0x03,  /*   Report Count (3) */
+	0x91, 0x01,  /*   Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */
+	0xC0,        /* End Collection */
+	0x05, 0x01,  /* Usage Page (Generic Desktop Ctrls) */
+	0x09, 0x02,  /* Usage (Mouse) */
+	0xA1, 0x01,  /* Collection (Application) */
+	0x09, 0x01,  /*   Usage (Pointer) */
+	0xA1, 0x00,  /*   Collection (Physical) */
+	0x85, HIDAPP_MOUSE_REPORT_ID,  /*     Report ID (2) */
+	0x05, 0x09,  /*     Usage Page (Button) */
+	0x19, 0x01,  /*     Usage Minimum (\x01) */
+	0x29, 0x05,  /*     Usage Maximum (\x05) */
+	0x15, 0x00,  /*     Logical Minimum (0) */
+	0x25, 0x01,  /*     Logical Maximum (1) */
+	0x95, 0x05,  /*     Report Count (5) */
+	0x75, 0x01,  /*     Report Size (1) */
+	0x81, 0x02,  /*     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0x95, 0x01,  /*     Report Count (1) */
+	0x75, 0x03,  /*     Report Size (3) */
+	0x81, 0x01,  /*     Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0x05, 0x01,  /*     Usage Page (Generic Desktop Ctrls) */
+	0x09, 0x30,  /*     Usage (X) */
+	0x09, 0x31,  /*     Usage (Y) */
+	0x15, 0x81,  /*     Logical Minimum (-127) */
+	0x25, 0x7F,  /*     Logical Maximum (127) */
+	0x75, 0x08,  /*     Report Size (8) */
+	0x95, 0x02,  /*     Report Count (2) */
+	0x81, 0x06,  /*     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) */
+	0x09, 0x38,  /*     Usage (Wheel) */
+	0x15, 0x81,  /*     Logical Minimum (-127) */
+	0x25, 0x7F,  /*     Logical Maximum (127) */
+	0x75, 0x08,  /*     Report Size (8) */
+	0x95, 0x01,  /*     Report Count (1) */
+	0x81, 0x06,  /*     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) */
+	0xC0,        /*   End Collection */
+	0xC0,        /* End Collection */
+	0x05, 0x0C,        /* Usage Page (Consumer) */
+	0x09, 0x01,        /* Usage (Consumer Control) */
+	0xA1, 0x01,        /* Collection (Application) */
+	0x85, HIDAPP_CONSUMER_REPORT_ID,        /*   Report ID (3) */
+	0x75, 0x10,        /*   Report Size (16) */
+	0x95, 0x01,        /*   Report Count (1) */
+	0x15, 0x01,        /*   Logical Minimum (1) */
+	0x26, 0x8C, 0x02,  /*   Logical Maximum (652) */
+	0x19, 0x01,        /*   Usage Minimum (Consumer Control) */
+	0x2A, 0x8C, 0x02,  /*   Usage Maximum (AC Send) */
+	0x81, 0x00,        /*   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0xC0,              /* End Collection */
+#if 0
+	0x05, 0x01,        /* Usage Page (Generic Desktop Ctrls) */
+	0x09, 0x05,        /* Usage (Game Pad) */
+	0xA1, 0x01,        /* Collection (Application) */
+	0x85, 0x05,        /*   Report ID (5) */
+	0x05, 0x09,        /*   Usage Page (Button) */
+	0x19, 0x01,        /*   Usage Minimum (\x01) */
+	0x29, 0x10,        /*   Usage Maximum (\x10) */
+	0x15, 0x00,        /*   Logical Minimum (0) */
+	0x25, 0x01,        /*   Logical Maximum (1) */
+	0x75, 0x01,        /*   Report Size (1) */
+	0x95, 0x10,        /*   Report Count (16) */
+	0x81, 0x02,        /*   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0x05, 0x01,        /*   Usage Page (Generic Desktop Ctrls) */
+	0x15, 0x81,        /*   Logical Minimum (-127) */
+	0x25, 0x7F,        /*   Logical Maximum (127) */
+	0x09, 0x30,        /*   Usage (X) */
+	0x09, 0x31,        /*   Usage (Y) */
+	0x09, 0x32,        /*   Usage (Z) */
+	0x09, 0x35,        /*   Usage (Rz) */
+	0x75, 0x08,        /*   Report Size (8) */
+	0x95, 0x04,        /*   Report Count (4) */
+	0x81, 0x02,        /*   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */
+	0xC0,              /* End Collection */
+#endif
+};
+/* clang-format on */
+
+const uint16_t hidReportMapLen = sizeof(hidReportMap);
+
+/*! HID Report Type/ID and attribute handle map */
+/* clang-format off */
+static const hidReportIdMap_t hidAppReportIdSet[] =
+{
+  /* type                       ID                            handle */
+  {HID_REPORT_TYPE_INPUT,       HIDAPP_KEYBOARD_REPORT_ID,    HID_INPUT_REPORT_1_HDL},     /* Keyboard Input Report */
+  {HID_REPORT_TYPE_OUTPUT,      HIDAPP_KEYBOARD_REPORT_ID,    HID_OUTPUT_REPORT_HDL},      /* Keyboard Output Report */
+  {HID_REPORT_TYPE_FEATURE,     HIDAPP_KEYBOARD_REPORT_ID,    HID_FEATURE_REPORT_HDL},     /* Keyboard Feature Report */
+  {HID_REPORT_TYPE_INPUT,       HIDAPP_MOUSE_REPORT_ID,       HID_INPUT_REPORT_2_HDL},     /* Mouse Input Report */
+  {HID_REPORT_TYPE_INPUT,       HIDAPP_CONSUMER_REPORT_ID,    HID_INPUT_REPORT_3_HDL},     /* Consumer Control Input Report */
+  {HID_REPORT_TYPE_INPUT,       HID_KEYBOARD_BOOT_ID,         HID_KEYBOARD_BOOT_IN_HDL},   /* Boot Keyboard Input Report */
+  {HID_REPORT_TYPE_OUTPUT,      HID_KEYBOARD_BOOT_ID,         HID_KEYBOARD_BOOT_OUT_HDL},  /* Boot Keyboard Output Report */
+  {HID_REPORT_TYPE_INPUT,       HID_MOUSE_BOOT_ID,            HID_MOUSE_BOOT_IN_HDL},      /* Boot Mouse Input Report */
+};
+/* clang-format on */
+
+void hidAppOutputCback(
+	dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport
+);
+void hidAppFeatureCback(
+	dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport
+);
+void hidAppInfoCback(dmConnId_t connId, uint8_t type, uint8_t value);
+
+/*! HID Profile Configuration */
+/* clang-format off */
+static const hidConfig_t hidAppHidConfig =
+{
+  (hidReportIdMap_t*) hidAppReportIdSet,                  /* Report ID to Attribute Handle map */
+  sizeof(hidAppReportIdSet)/sizeof(hidReportIdMap_t),     /* Size of Report ID to Attribute Handle map */
+  &hidAppOutputCback,                                     /* Output Report Callback */
+  &hidAppFeatureCback,                                    /* Feature Report Callback */
+  &hidAppInfoCback                                        /* Info Callback */
+};
+/* clang-format on */
+
+static void hidAppReportInit(void)
+{
+	uint8_t iKeyboardBuffer[HIDAPP_KEYBOARD_INPUT_REPORT_LEN];
+	uint8_t iMouseBuffer[HIDAPP_MOUSE_INPUT_REPORT_LEN];
+	uint8_t iConsumerBuffer[HIDAPP_CONSUMER_INPUT_REPORT_LEN];
+	uint8_t oBuffer[HIDAPP_OUTPUT_REPORT_LEN];
+	uint8_t fBuffer[HIDAPP_FEATURE_REPORT_LEN];
+
+	/* Keyboard Input report */
+	memset(iKeyboardBuffer, 0, HIDAPP_KEYBOARD_INPUT_REPORT_LEN);
+	AttsSetAttr(
+		HID_INPUT_REPORT_1_HDL,
+		HIDAPP_KEYBOARD_INPUT_REPORT_LEN,
+		iKeyboardBuffer
+	);
+
+	/* Mouse Input report */
+	memset(iMouseBuffer, 0, HIDAPP_MOUSE_INPUT_REPORT_LEN);
+	AttsSetAttr(
+		HID_INPUT_REPORT_2_HDL,
+		HIDAPP_MOUSE_INPUT_REPORT_LEN,
+		iMouseBuffer
+	);
+
+	/* Consumer Control Input report */
+	memset(iConsumerBuffer, 0, HIDAPP_CONSUMER_INPUT_REPORT_LEN);
+	AttsSetAttr(
+		HID_INPUT_REPORT_3_HDL,
+		HIDAPP_CONSUMER_INPUT_REPORT_LEN,
+		iConsumerBuffer
+	);
+
+	/* Output report */
+	memset(oBuffer, 0, HIDAPP_OUTPUT_REPORT_LEN);
+	AttsSetAttr(HID_OUTPUT_REPORT_HDL, HIDAPP_OUTPUT_REPORT_LEN, oBuffer);
+
+	/* Feature report */
+	memset(fBuffer, 0, HIDAPP_FEATURE_REPORT_LEN);
+	AttsSetAttr(HID_FEATURE_REPORT_HDL, HIDAPP_FEATURE_REPORT_LEN, fBuffer);
+}
+
+void hid_work_init(void);
+void hid_init(void)
+{
+#ifdef HID_ATT_DYNAMIC
+	/* Initialize the dynamic service system */
+	AttsDynInit();
+	/* Add the HID service dynamically */
+	pSHdl = SvcHidAddGroupDyn();
+	AttsDynRegister(pSHdl, NULL, HidAttsWriteCback);
+#else
+	/* Add the HID service statically */
+	SvcHidAddGroup();
+	SvcHidRegister(HidAttsWriteCback, NULL);
+#endif /* HID_ATT_DYNAMIC */
+	/* Initialize the HID profile */
+	HidInit(&hidAppHidConfig);
+
+	/* Initialize the report attributes */
+	hidAppReportInit();
+
+	hid_work_init();
+}
diff --git a/epicardium/ble/hid.h b/epicardium/ble/hid.h
new file mode 100644
index 0000000000000000000000000000000000000000..82620b7333e25d5c423109227466c3b37154330d
--- /dev/null
+++ b/epicardium/ble/hid.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "wsf_types.h"
+#include "wsf_os.h"
+
+/* The input report fits in one byte */
+#define HIDAPP_KEYBOARD_INPUT_REPORT_LEN  8
+#define HIDAPP_MOUSE_INPUT_REPORT_LEN     4
+#define HIDAPP_CONSUMER_INPUT_REPORT_LEN  2
+#define HIDAPP_OUTPUT_REPORT_LEN          1
+#define HIDAPP_FEATURE_REPORT_LEN         1
+
+
+/* HID Report IDs */
+#define HIDAPP_KEYBOARD_REPORT_ID         1
+#define HIDAPP_MOUSE_REPORT_ID            2
+#define HIDAPP_CONSUMER_REPORT_ID         3
+
+void hid_init(void);
+void HidProcMsg(wsfMsgHdr_t *pMsg);
diff --git a/epicardium/ble/hid_work.c b/epicardium/ble/hid_work.c
new file mode 100644
index 0000000000000000000000000000000000000000..f4eeb2e79881dfdaa86432e482c36a85beb65812
--- /dev/null
+++ b/epicardium/ble/hid_work.c
@@ -0,0 +1,241 @@
+#include "hid.h"
+#include "cccd.h"
+
+#include "epicardium.h"
+#include "modules/log.h"
+
+#include "dm_api.h"
+#include "att_api.h"
+#include "app_api.h"
+#include "hid/hid_api.h"
+#include "svc_hid.h"
+
+#include "FreeRTOS.h"
+#include "queue.h"
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+/* HidApp TX path flags */
+#define HIDAPP_TX_FLAGS_READY 0x01
+
+/*! application control block */
+/* clang-format off */
+struct
+{
+  uint8_t txFlags;                      /* transmit flags */
+  uint8_t protocolMode;                 /* current protocol mode */
+  uint8_t hostSuspended;                /* TRUE if host suspended */
+} hidAppCb;
+/* clang-format on */
+
+struct report {
+	uint8_t report_id;
+	uint8_t data[8];
+	uint8_t len;
+};
+
+#define QUEUE_SIZE 10
+
+static QueueHandle_t queue;
+static uint8_t buffer[sizeof(struct report) * QUEUE_SIZE];
+static StaticQueue_t queue_data;
+
+static int hid_queue_data(uint8_t report_id, uint8_t *data, uint8_t len)
+{
+	struct report report;
+
+	if (report_id < 1 || report_id > 3) {
+		return -EINVAL;
+	}
+
+	report.report_id = report_id;
+
+	if (len > sizeof(report.data)) {
+		return -EINVAL;
+	}
+
+	memcpy(report.data, data, len);
+	report.len = len;
+
+	if (xQueueSend(queue, &report, 0) != pdTRUE) {
+		/* Likely full */
+		return -EAGAIN;
+	}
+
+	return 0;
+}
+
+static bool hid_dequeue_data(dmConnId_t connId)
+{
+	uint8_t cccHandle;
+	struct report report;
+
+	//lock();
+
+	if (!(hidAppCb.txFlags & HIDAPP_TX_FLAGS_READY)) {
+		//unlock();
+		return false;
+	}
+
+	// Loop until a CCC is enabled or the queue is empty
+	while (true) {
+		if (xQueueReceive(queue, &report, 0) != pdTRUE) {
+			break;
+		}
+
+		if (HidGetProtocolMode() == HID_PROTOCOL_MODE_BOOT) {
+			if (report.report_id == HIDAPP_KEYBOARD_REPORT_ID) {
+				report.report_id = HID_KEYBOARD_BOOT_ID;
+				cccHandle        = HIDAPP_KBI_CCC_HDL;
+			} else if (report.report_id == HIDAPP_MOUSE_REPORT_ID) {
+				report.report_id = HID_MOUSE_BOOT_ID;
+				cccHandle        = HIDAPP_MBI_CCC_HDL;
+			} else {
+				break;
+			}
+		} else {
+			if (report.report_id == HIDAPP_KEYBOARD_REPORT_ID) {
+				cccHandle = HIDAPP_IN_KEYBOARD_CCC_HDL;
+			} else if (report.report_id == HIDAPP_MOUSE_REPORT_ID) {
+				cccHandle = HIDAPP_IN_MOUSE_CCC_HDL;
+			} else if (report.report_id == HIDAPP_CONSUMER_REPORT_ID) {
+				cccHandle = HIDAPP_IN_CONSUMER_CCC_HDL;
+			} else {
+				break;
+			};
+		}
+
+		if (AttsCccEnabled(connId, cccHandle) || 1) {
+			hidAppCb.txFlags &= ~(HIDAPP_TX_FLAGS_READY);
+			/* Send the message */
+			HidSendInputReport(
+				connId,
+				report.report_id,
+				report.len,
+				report.data
+			);
+			break;
+		}
+	}
+
+	//unlock();
+	return true;
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Callback to handle an output report from the host.
+ *
+ *  \param  connId    The connection identifier.
+ *  \param  id        The ID of the report.
+ *  \param  len       The length of the report data in pReport.
+ *  \param  pReport   A buffer containing the report.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void hidAppOutputCback(
+	dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport
+) {
+	/* TODO: process output reports */
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Callback to handle a feature report from the host.
+ *
+ *  \param  connId    The connection identifier.
+ *  \param  id        The ID of the report.
+ *  \param  len       The length of the report data in pReport.
+ *  \param  pReport   A buffer containing the report.
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void hidAppFeatureCback(
+	dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport
+) {
+	/* TODO: process feature reports */
+}
+
+/*************************************************************************************************/
+/*!
+ *  \brief  Callback to handle a change in protocol mode or control point from the host.
+ *
+ *  \param  connId    The connection identifier.
+ *  \param  mode      The type of information (HID_INFO_CONTROL_POINT or HID_INFO_PROTOCOL_MODE)
+ *  \param  value     The value of the information
+ *
+ *  \return None.
+ */
+/*************************************************************************************************/
+void hidAppInfoCback(dmConnId_t connId, uint8_t type, uint8_t value)
+{
+	if (type == HID_INFO_PROTOCOL_MODE) {
+		LOG_INFO("hid", "protocol mode: %u\n", value);
+		hidAppCb.protocolMode = value;
+	} else if (type == HID_INFO_CONTROL_POINT) {
+		LOG_INFO("hid", "host suspended: %u\n", value);
+		hidAppCb.hostSuspended =
+			(value == HID_CONTROL_POINT_SUSPEND) ? TRUE : FALSE;
+	}
+}
+
+void HidProcMsg(wsfMsgHdr_t *pMsg)
+{
+	if (!queue) {
+		/* Not initialized (yet). */
+		return;
+	}
+	if (pMsg->event == ATTS_HANDLE_VALUE_CNF) {
+		if (pMsg->status == ATT_SUCCESS) {
+			hidAppCb.txFlags |= HIDAPP_TX_FLAGS_READY;
+			hid_dequeue_data((dmConnId_t)pMsg->param);
+		}
+	}
+	if (pMsg->event == DM_CONN_OPEN_IND) {
+		hidAppCb.txFlags = HIDAPP_TX_FLAGS_READY;
+
+		struct report report;
+		while (xQueueReceive(queue, &report, 0) == pdTRUE)
+			;
+
+		/* Todo: At this point the CCC descriptors are not set up yet
+		 * and things which get sent until then are discarded. */
+	}
+}
+
+int epic_ble_hid_send_report(uint8_t report_id, uint8_t *data, uint8_t len)
+{
+	dmConnId_t connId = AppConnIsOpen();
+	if (connId == DM_CONN_ID_NONE) {
+		return -EIO;
+	}
+
+	if (!queue) {
+		return -EIO;
+	}
+
+	int ret;
+	ret = hid_queue_data(report_id, data, len);
+
+	if (ret < 0) {
+		return ret;
+	}
+
+	if (hid_dequeue_data(connId)) {
+		return 0;
+	} else {
+		return 1;
+	}
+}
+
+void hid_work_init(void)
+{
+	queue = xQueueCreateStatic(
+		QUEUE_SIZE, sizeof(struct report), buffer, &queue_data
+	);
+	hidAppCb.txFlags = HIDAPP_TX_FLAGS_READY;
+}
diff --git a/epicardium/ble/meson.build b/epicardium/ble/meson.build
index c2f10a12a8976cec3f049d9d8ca4617ef0cb70f9..341ad22571c4d5bc94c9bcf997c320682659142e 100644
--- a/epicardium/ble/meson.build
+++ b/epicardium/ble/meson.build
@@ -12,4 +12,6 @@ ble_sources = files(
   'card10.c',
   'ess.c',
   'filetransfer.c',
+  'hid.c',
+  'hid_work.c',
 )
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 842714028be6bdf0a9a86b31a052fbcf7fa1ec22..3a2fea1a04e31e3e5c3b5a6d84d1cbbdb7445d88 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -158,6 +158,7 @@ typedef _Bool bool;
 #define API_BLE_GET_LAST_PAIRING_NAME 0x145
 #define API_BLE_GET_PEER_DEVICE_NAME  0x146
 
+#define API_BLE_HID_SEND_REPORT        0x150
 
 /* clang-format on */
 
@@ -2320,5 +2321,23 @@ API(API_BLE_SET_MODE, void epic_ble_set_mode(bool bondable, bool scanner));
  *
  */
 API(API_BLE_GET_SCAN_REPORT, int epic_ble_get_scan_report(struct epic_scan_report *rpt));
+
+/**
+ * Send an input report to the host.
+ *
+ * :param uint8_t report_id: The id of the report to use. 1: keyboard, 2: mouse, 3: consumer control
+ * :param uint8_t *data: Data to be reported.
+ * :param uint8_t len: Length in bytes of the data to be reported. Maximum length is 8 bytes.
+ *
+ * :return: `0` on success, `1` if the report is queued or a negative value if an error occured. Possible
+ *    errors:
+ *
+ *    - ``-EIO``: There is no host device connected or BLE HID is not enabled.
+ *    - ``-EAGAIN``: There is no space in the queue available. Try again later.
+ *    - ``-EINVAL``: Either the report_id is out of range or the data is too long.
+ *
+ */
+API(API_BLE_HID_SEND_REPORT, int epic_ble_hid_send_report(uint8_t report_id, uint8_t *data, uint8_t len));
+
 #endif /* _EPICARDIUM_H */
 
diff --git a/lib/sdk/Libraries/BTLE/meson.build b/lib/sdk/Libraries/BTLE/meson.build
index 1314ad583ea2e6239b81daa630f4ed454079c7df..2bf03dbb6af2f7d34b6e6c376f9c632a28006d09 100644
--- a/lib/sdk/Libraries/BTLE/meson.build
+++ b/lib/sdk/Libraries/BTLE/meson.build
@@ -48,7 +48,7 @@ sources = files(
 'stack/ble-profiles/sources/apps/meds/meds_htp.c',
 'stack/ble-profiles/sources/apps/meds/meds_plx.c',
 'stack/ble-profiles/sources/apps/meds/meds_glp.c',
-'stack/ble-profiles/sources/apps/hidapp/hidapp_main.c',
+#'stack/ble-profiles/sources/apps/hidapp/hidapp_main.c',
 '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',
diff --git a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h
index fcd657fe391a6e9c9f610db71275a8deeae887dc..ce424a2bcf019a86a928f8ce94d51e61e624c4e0 100644
--- a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h
+++ b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h
@@ -84,7 +84,7 @@ Macros
  *
  */
 /**@{*/
-#define HID_START_HDL                 0x100             /*!< \brief Start handle. */
+#define HID_START_HDL                 0x80             /*!< \brief Start handle. */
 #define HID_END_HDL                   (HID_MAX_HDL - 1) /*!< \brief End handle. */
 
 /**************************************************************************************************
diff --git a/preload/adafruit_hid/__init__.py b/preload/adafruit_hid/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5323dd3ed0396e74bdc9603f5e0d50211cc15ca6
--- /dev/null
+++ b/preload/adafruit_hid/__init__.py
@@ -0,0 +1,56 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2017 Scott Shawcroft for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""
+`adafruit_hid`
+====================================================
+
+This driver simulates USB HID devices.
+
+* Author(s): Scott Shawcroft, Dan Halbert
+
+Implementation Notes
+--------------------
+**Software and Dependencies:**
+* Adafruit CircuitPython firmware for the supported boards:
+  https://github.com/adafruit/circuitpython/releases
+"""
+
+# imports
+
+__version__ = "0.0.0-auto.0"
+__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HID.git"
+
+
+def find_device(devices, *, usage_page, usage):
+    """Search through the provided list of devices to find the one with the matching usage_page and
+       usage."""
+    if hasattr(devices, "send_report"):
+        devices = [devices]
+    for device in devices:
+        if (
+            device.usage_page == usage_page
+            and device.usage == usage
+            and hasattr(device, "send_report")
+        ):
+            return device
+    raise ValueError("Could not find matching HID device.")
diff --git a/preload/adafruit_hid/consumer_control.py b/preload/adafruit_hid/consumer_control.py
new file mode 100644
index 0000000000000000000000000000000000000000..70176bb4204004a12fd0a8c05d5dbe71bbbefa4b
--- /dev/null
+++ b/preload/adafruit_hid/consumer_control.py
@@ -0,0 +1,87 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2018 Dan Halbert for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.consumer_control.ConsumerControl`
+====================================================
+
+* Author(s): Dan Halbert
+"""
+
+import sys
+
+# if sys.implementation.version[0] < 3:
+#     raise ImportError(
+#         "{0} is not supported in CircuitPython 2.x or lower".format(__name__)
+#     )
+
+# pylint: disable=wrong-import-position
+import struct
+import time
+from . import find_device
+
+
+class ConsumerControl:
+    """Send ConsumerControl code reports, used by multimedia keyboards, remote controls, etc.
+    """
+
+    def __init__(self, devices):
+        """Create a ConsumerControl object that will send Consumer Control Device HID reports.
+
+        Devices can be a list of devices that includes a Consumer Control device or a CC device
+        itself. A device is any object that implements ``send_report()``, ``usage_page`` and
+        ``usage``.
+        """
+        self._consumer_device = find_device(devices, usage_page=0x0C, usage=0x01)
+
+        # Reuse this bytearray to send consumer reports.
+        self._report = bytearray(2)
+
+        # Do a no-op to test if HID device is ready.
+        # If not, wait a bit and try once more.
+        try:
+            self.send(0x0)
+        except OSError:
+            time.sleep(1)
+            self.send(0x0)
+
+    def send(self, consumer_code):
+        """Send a report to do the specified consumer control action,
+        and then stop the action (so it will not repeat).
+
+        :param consumer_code: a 16-bit consumer control code.
+
+        Examples::
+
+            from adafruit_hid.consumer_control_code import ConsumerControlCode
+
+            # Raise volume.
+            consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT)
+
+            # Advance to next track (song).
+            consumer_control.send(ConsumerControlCode.SCAN_NEXT_TRACK)
+        """
+        struct.pack_into("<H", self._report, 0, consumer_code)
+        self._consumer_device.send_report(self._report)
+        self._report[0] = self._report[1] = 0x0
+        self._consumer_device.send_report(self._report)
diff --git a/preload/adafruit_hid/consumer_control_code.py b/preload/adafruit_hid/consumer_control_code.py
new file mode 100644
index 0000000000000000000000000000000000000000..735f771b3c8883a6fc57a7292c4203f091ae2e43
--- /dev/null
+++ b/preload/adafruit_hid/consumer_control_code.py
@@ -0,0 +1,64 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2018 Dan Halbert for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.consumer_control_code.ConsumerControlCode`
+========================================================
+
+* Author(s): Dan Halbert
+"""
+
+
+class ConsumerControlCode:
+    """USB HID Consumer Control Device constants.
+
+    This list includes a few common consumer control codes from
+    https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf#page=75.
+
+    *New in CircuitPython 3.0.*
+    """
+
+    # pylint: disable-msg=too-few-public-methods
+
+    RECORD = 0xB2
+    """Record"""
+    FAST_FORWARD = 0xB3
+    """Fast Forward"""
+    REWIND = 0xB4
+    """Rewind"""
+    SCAN_NEXT_TRACK = 0xB5
+    """Skip to next track"""
+    SCAN_PREVIOUS_TRACK = 0xB6
+    """Go back to previous track"""
+    STOP = 0xB7
+    """Stop"""
+    EJECT = 0xB8
+    """Eject"""
+    PLAY_PAUSE = 0xCD
+    """Play/Pause toggle"""
+    MUTE = 0xE2
+    """Mute"""
+    VOLUME_DECREMENT = 0xEA
+    """Decrease volume"""
+    VOLUME_INCREMENT = 0xE9
+    """Increase volume"""
diff --git a/preload/adafruit_hid/gamepad.py b/preload/adafruit_hid/gamepad.py
new file mode 100644
index 0000000000000000000000000000000000000000..468c0d6b04c8030bb6a13372d71ec0edb16775d0
--- /dev/null
+++ b/preload/adafruit_hid/gamepad.py
@@ -0,0 +1,177 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2018 Dan Halbert for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.gamepad.Gamepad`
+====================================================
+
+* Author(s): Dan Halbert
+"""
+
+import struct
+import time
+
+from . import find_device
+
+
+class Gamepad:
+    """Emulate a generic gamepad controller with 16 buttons,
+    numbered 1-16, and two joysticks, one controlling
+    ``x` and ``y`` values, and the other controlling ``z`` and
+    ``r_z`` (z rotation or ``Rz``) values.
+
+    The joystick values could be interpreted
+    differently by the receiving program: those are just the names used here.
+    The joystick values are in the range -127 to 127.
+"""
+
+    def __init__(self, devices):
+        """Create a Gamepad object that will send USB gamepad HID reports.
+
+        Devices can be a list of devices that includes a gamepad device or a gamepad device
+        itself. A device is any object that implements ``send_report()``, ``usage_page`` and
+        ``usage``.
+        """
+        self._gamepad_device = find_device(devices, usage_page=0x1, usage=0x05)
+
+        # Reuse this bytearray to send mouse reports.
+        # Typically controllers start numbering buttons at 1 rather than 0.
+        # report[0] buttons 1-8 (LSB is button 1)
+        # report[1] buttons 9-16
+        # report[2] joystick 0 x: -127 to 127
+        # report[3] joystick 0 y: -127 to 127
+        # report[4] joystick 1 x: -127 to 127
+        # report[5] joystick 1 y: -127 to 127
+        self._report = bytearray(6)
+
+        # Remember the last report as well, so we can avoid sending
+        # duplicate reports.
+        self._last_report = bytearray(6)
+
+        # Store settings separately before putting into report. Saves code
+        # especially for buttons.
+        self._buttons_state = 0
+        self._joy_x = 0
+        self._joy_y = 0
+        self._joy_z = 0
+        self._joy_r_z = 0
+
+        # Send an initial report to test if HID device is ready.
+        # If not, wait a bit and try once more.
+        try:
+            self.reset_all()
+        except OSError:
+            time.sleep(1)
+            self.reset_all()
+
+    def press_buttons(self, *buttons):
+        """Press and hold the given buttons. """
+        for button in buttons:
+            self._buttons_state |= 1 << self._validate_button_number(button) - 1
+        self._send()
+
+    def release_buttons(self, *buttons):
+        """Release the given buttons. """
+        for button in buttons:
+            self._buttons_state &= ~(1 << self._validate_button_number(button) - 1)
+        self._send()
+
+    def release_all_buttons(self):
+        """Release all the buttons."""
+
+        self._buttons_state = 0
+        self._send()
+
+    def click_buttons(self, *buttons):
+        """Press and release the given buttons."""
+        self.press_buttons(*buttons)
+        self.release_buttons(*buttons)
+
+    def move_joysticks(self, x=None, y=None, z=None, r_z=None):
+        """Set and send the given joystick values.
+        The joysticks will remain set with the given values until changed
+
+        One joystick provides ``x`` and ``y`` values,
+        and the other provides ``z`` and ``r_z`` (z rotation).
+        Any values left as ``None`` will not be changed.
+
+        All values must be in the range -127 to 127 inclusive.
+
+        Examples::
+
+            # Change x and y values only.
+            gp.move_joysticks(x=100, y=-50)
+
+            # Reset all joystick values to center position.
+            gp.move_joysticks(0, 0, 0, 0)
+        """
+        if x is not None:
+            self._joy_x = self._validate_joystick_value(x)
+        if y is not None:
+            self._joy_y = self._validate_joystick_value(y)
+        if z is not None:
+            self._joy_z = self._validate_joystick_value(z)
+        if r_z is not None:
+            self._joy_r_z = self._validate_joystick_value(r_z)
+        self._send()
+
+    def reset_all(self):
+        """Release all buttons and set joysticks to zero."""
+        self._buttons_state = 0
+        self._joy_x = 0
+        self._joy_y = 0
+        self._joy_z = 0
+        self._joy_r_z = 0
+        self._send(always=True)
+
+    def _send(self, always=False):
+        """Send a report with all the existing settings.
+        If ``always`` is ``False`` (the default), send only if there have been changes.
+        """
+        struct.pack_into(
+            "<Hbbbb",
+            self._report,
+            0,
+            self._buttons_state,
+            self._joy_x,
+            self._joy_y,
+            self._joy_z,
+            self._joy_r_z,
+        )
+
+        if always or self._last_report != self._report:
+            self._gamepad_device.send_report(self._report)
+            # Remember what we sent, without allocating new storage.
+            self._last_report[:] = self._report
+
+    @staticmethod
+    def _validate_button_number(button):
+        if not 1 <= button <= 16:
+            raise ValueError("Button number must in range 1 to 16")
+        return button
+
+    @staticmethod
+    def _validate_joystick_value(value):
+        if not -127 <= value <= 127:
+            raise ValueError("Joystick value must be in range -127 to 127")
+        return value
diff --git a/preload/adafruit_hid/keyboard.py b/preload/adafruit_hid/keyboard.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c0e11deb73168f07e8ac5c65103bc96ea30a170
--- /dev/null
+++ b/preload/adafruit_hid/keyboard.py
@@ -0,0 +1,164 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2017 Dan Halbert
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.keyboard.Keyboard`
+====================================================
+
+* Author(s): Scott Shawcroft, Dan Halbert
+"""
+
+import time
+from micropython import const
+
+from .keycode import Keycode
+
+from . import find_device
+
+_MAX_KEYPRESSES = const(6)
+
+
+class Keyboard:
+    """Send HID keyboard reports."""
+
+    # No more than _MAX_KEYPRESSES regular keys may be pressed at once.
+
+    def __init__(self, devices):
+        """Create a Keyboard object that will send keyboard HID reports.
+
+        Devices can be a list of devices that includes a keyboard device or a keyboard device
+        itself. A device is any object that implements ``send_report()``, ``usage_page`` and
+        ``usage``.
+        """
+        self._keyboard_device = find_device(devices, usage_page=0x1, usage=0x06)
+
+        # Reuse this bytearray to send keyboard reports.
+        self.report = bytearray(8)
+
+        # report[0] modifiers
+        # report[1] unused
+        # report[2:8] regular key presses
+
+        # View onto byte 0 in report.
+        self.report_modifier = memoryview(self.report)[0:1]
+
+        # List of regular keys currently pressed.
+        # View onto bytes 2-7 in report.
+        self.report_keys = memoryview(self.report)[2:]
+
+        # Do a no-op to test if HID device is ready.
+        # If not, wait a bit and try once more.
+        try:
+            self.release_all()
+        except OSError:
+            time.sleep(1)
+            self.release_all()
+
+    def press(self, *keycodes):
+        """Send a report indicating that the given keys have been pressed.
+
+        :param keycodes: Press these keycodes all at once.
+        :raises ValueError: if more than six regular keys are pressed.
+
+        Keycodes may be modifiers or regular keys.
+        No more than six regular keys may be pressed simultaneously.
+
+        Examples::
+
+            from adafruit_hid.keycode import Keycode
+
+            # Press ctrl-x.
+            kbd.press(Keycode.LEFT_CONTROL, Keycode.X)
+
+            # Or, more conveniently, use the CONTROL alias for LEFT_CONTROL:
+            kbd.press(Keycode.CONTROL, Keycode.X)
+
+            # Press a, b, c keys all at once.
+            kbd.press(Keycode.A, Keycode.B, Keycode.C)
+        """
+        for keycode in keycodes:
+            self._add_keycode_to_report(keycode)
+        self._keyboard_device.send_report(self.report)
+
+    def release(self, *keycodes):
+        """Send a USB HID report indicating that the given keys have been released.
+
+        :param keycodes: Release these keycodes all at once.
+
+        If a keycode to be released was not pressed, it is ignored.
+
+        Example::
+
+            # release SHIFT key
+            kbd.release(Keycode.SHIFT)
+        """
+        for keycode in keycodes:
+            self._remove_keycode_from_report(keycode)
+        self._keyboard_device.send_report(self.report)
+
+    def release_all(self):
+        """Release all pressed keys."""
+        for i in range(8):
+            self.report[i] = 0
+        self._keyboard_device.send_report(self.report)
+
+    def send(self, *keycodes):
+        """Press the given keycodes and then release all pressed keys.
+
+        :param keycodes: keycodes to send together
+        """
+        self.press(*keycodes)
+        self.release_all()
+
+    def _add_keycode_to_report(self, keycode):
+        """Add a single keycode to the USB HID report."""
+        modifier = Keycode.modifier_bit(keycode)
+        if modifier:
+            # Set bit for this modifier.
+            self.report_modifier[0] |= modifier
+        else:
+            # Don't press twice.
+            # (I'd like to use 'not in self.report_keys' here, but that's not implemented.)
+            for i in range(_MAX_KEYPRESSES):
+                if self.report_keys[i] == keycode:
+                    # Already pressed.
+                    return
+            # Put keycode in first empty slot.
+            for i in range(_MAX_KEYPRESSES):
+                if self.report_keys[i] == 0:
+                    self.report_keys[i] = keycode
+                    return
+            # All slots are filled.
+            raise ValueError("Trying to press more than six keys at once.")
+
+    def _remove_keycode_from_report(self, keycode):
+        """Remove a single keycode from the report."""
+        modifier = Keycode.modifier_bit(keycode)
+        if modifier:
+            # Turn off the bit for this modifier.
+            self.report_modifier[0] &= ~modifier
+        else:
+            # Check all the slots, just in case there's a duplicate. (There should not be.)
+            for i in range(_MAX_KEYPRESSES):
+                if self.report_keys[i] == keycode:
+                    self.report_keys[i] = 0
diff --git a/preload/adafruit_hid/keyboard_layout_us.py b/preload/adafruit_hid/keyboard_layout_us.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c8137f1f67e878df01ac99bb91120d79719048b
--- /dev/null
+++ b/preload/adafruit_hid/keyboard_layout_us.py
@@ -0,0 +1,256 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2017 Dan Halbert
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.keyboard_layout_us.KeyboardLayoutUS`
+=======================================================
+
+* Author(s): Dan Halbert
+"""
+
+from .keycode import Keycode
+
+
+class KeyboardLayoutUS:
+    """Map ASCII characters to appropriate keypresses on a standard US PC keyboard.
+
+    Non-ASCII characters and most control characters will raise an exception.
+    """
+
+    # The ASCII_TO_KEYCODE bytes object is used as a table to maps ASCII 0-127
+    # to the corresponding # keycode on a US 104-key keyboard.
+    # The user should not normally need to use this table,
+    # but it is not marked as private.
+    #
+    # Because the table only goes to 127, we use the top bit of each byte (ox80) to indicate
+    # that the shift key should be pressed. So any values 0x{8,9,a,b}* are shifted characters.
+    #
+    # The Python compiler will concatenate all these bytes literals into a single bytes object.
+    # Micropython/CircuitPython will store the resulting bytes constant in flash memory
+    # if it's in a .mpy file, so it doesn't use up valuable RAM.
+    #
+    # \x00 entries have no keyboard key and so won't be sent.
+    SHIFT_FLAG = 0x80
+    ASCII_TO_KEYCODE = (
+        b"\x00"  # NUL
+        b"\x00"  # SOH
+        b"\x00"  # STX
+        b"\x00"  # ETX
+        b"\x00"  # EOT
+        b"\x00"  # ENQ
+        b"\x00"  # ACK
+        b"\x00"  # BEL \a
+        b"\x2a"  # BS BACKSPACE \b (called DELETE in the usb.org document)
+        b"\x2b"  # TAB \t
+        b"\x28"  # LF \n (called Return or ENTER in the usb.org document)
+        b"\x00"  # VT \v
+        b"\x00"  # FF \f
+        b"\x00"  # CR \r
+        b"\x00"  # SO
+        b"\x00"  # SI
+        b"\x00"  # DLE
+        b"\x00"  # DC1
+        b"\x00"  # DC2
+        b"\x00"  # DC3
+        b"\x00"  # DC4
+        b"\x00"  # NAK
+        b"\x00"  # SYN
+        b"\x00"  # ETB
+        b"\x00"  # CAN
+        b"\x00"  # EM
+        b"\x00"  # SUB
+        b"\x29"  # ESC
+        b"\x00"  # FS
+        b"\x00"  # GS
+        b"\x00"  # RS
+        b"\x00"  # US
+        b"\x2c"  # SPACE
+        b"\x9e"  # ! x1e|SHIFT_FLAG (shift 1)
+        b"\xb4"  # " x34|SHIFT_FLAG (shift ')
+        b"\xa0"  # # x20|SHIFT_FLAG (shift 3)
+        b"\xa1"  # $ x21|SHIFT_FLAG (shift 4)
+        b"\xa2"  # % x22|SHIFT_FLAG (shift 5)
+        b"\xa4"  # & x24|SHIFT_FLAG (shift 7)
+        b"\x34"  # '
+        b"\xa6"  # ( x26|SHIFT_FLAG (shift 9)
+        b"\xa7"  # ) x27|SHIFT_FLAG (shift 0)
+        b"\xa5"  # * x25|SHIFT_FLAG (shift 8)
+        b"\xae"  # + x2e|SHIFT_FLAG (shift =)
+        b"\x36"  # ,
+        b"\x2d"  # -
+        b"\x37"  # .
+        b"\x38"  # /
+        b"\x27"  # 0
+        b"\x1e"  # 1
+        b"\x1f"  # 2
+        b"\x20"  # 3
+        b"\x21"  # 4
+        b"\x22"  # 5
+        b"\x23"  # 6
+        b"\x24"  # 7
+        b"\x25"  # 8
+        b"\x26"  # 9
+        b"\xb3"  # : x33|SHIFT_FLAG (shift ;)
+        b"\x33"  # ;
+        b"\xb6"  # < x36|SHIFT_FLAG (shift ,)
+        b"\x2e"  # =
+        b"\xb7"  # > x37|SHIFT_FLAG (shift .)
+        b"\xb8"  # ? x38|SHIFT_FLAG (shift /)
+        b"\x9f"  # @ x1f|SHIFT_FLAG (shift 2)
+        b"\x84"  # A x04|SHIFT_FLAG (shift a)
+        b"\x85"  # B x05|SHIFT_FLAG (etc.)
+        b"\x86"  # C x06|SHIFT_FLAG
+        b"\x87"  # D x07|SHIFT_FLAG
+        b"\x88"  # E x08|SHIFT_FLAG
+        b"\x89"  # F x09|SHIFT_FLAG
+        b"\x8a"  # G x0a|SHIFT_FLAG
+        b"\x8b"  # H x0b|SHIFT_FLAG
+        b"\x8c"  # I x0c|SHIFT_FLAG
+        b"\x8d"  # J x0d|SHIFT_FLAG
+        b"\x8e"  # K x0e|SHIFT_FLAG
+        b"\x8f"  # L x0f|SHIFT_FLAG
+        b"\x90"  # M x10|SHIFT_FLAG
+        b"\x91"  # N x11|SHIFT_FLAG
+        b"\x92"  # O x12|SHIFT_FLAG
+        b"\x93"  # P x13|SHIFT_FLAG
+        b"\x94"  # Q x14|SHIFT_FLAG
+        b"\x95"  # R x15|SHIFT_FLAG
+        b"\x96"  # S x16|SHIFT_FLAG
+        b"\x97"  # T x17|SHIFT_FLAG
+        b"\x98"  # U x18|SHIFT_FLAG
+        b"\x99"  # V x19|SHIFT_FLAG
+        b"\x9a"  # W x1a|SHIFT_FLAG
+        b"\x9b"  # X x1b|SHIFT_FLAG
+        b"\x9c"  # Y x1c|SHIFT_FLAG
+        b"\x9d"  # Z x1d|SHIFT_FLAG
+        b"\x2f"  # [
+        b"\x31"  # \ backslash
+        b"\x30"  # ]
+        b"\xa3"  # ^ x23|SHIFT_FLAG (shift 6)
+        b"\xad"  # _ x2d|SHIFT_FLAG (shift -)
+        b"\x35"  # `
+        b"\x04"  # a
+        b"\x05"  # b
+        b"\x06"  # c
+        b"\x07"  # d
+        b"\x08"  # e
+        b"\x09"  # f
+        b"\x0a"  # g
+        b"\x0b"  # h
+        b"\x0c"  # i
+        b"\x0d"  # j
+        b"\x0e"  # k
+        b"\x0f"  # l
+        b"\x10"  # m
+        b"\x11"  # n
+        b"\x12"  # o
+        b"\x13"  # p
+        b"\x14"  # q
+        b"\x15"  # r
+        b"\x16"  # s
+        b"\x17"  # t
+        b"\x18"  # u
+        b"\x19"  # v
+        b"\x1a"  # w
+        b"\x1b"  # x
+        b"\x1c"  # y
+        b"\x1d"  # z
+        b"\xaf"  # { x2f|SHIFT_FLAG (shift [)
+        b"\xb1"  # | x31|SHIFT_FLAG (shift \)
+        b"\xb0"  # } x30|SHIFT_FLAG (shift ])
+        b"\xb5"  # ~ x35|SHIFT_FLAG (shift `)
+        b"\x4c"  # DEL DELETE (called Forward Delete in usb.org document)
+    )
+
+    def __init__(self, keyboard):
+        """Specify the layout for the given keyboard.
+
+        :param keyboard: a Keyboard object. Write characters to this keyboard when requested.
+
+        Example::
+
+            kbd = Keyboard(usb_hid.devices)
+            layout = KeyboardLayoutUS(kbd)
+        """
+
+        self.keyboard = keyboard
+
+    def write(self, string):
+        """Type the string by pressing and releasing keys on my keyboard.
+
+        :param string: A string of ASCII characters.
+        :raises ValueError: if any of the characters are not ASCII or have no keycode
+            (such as some control characters).
+
+        Example::
+
+            # Write abc followed by Enter to the keyboard
+            layout.write('abc\\n')
+        """
+        for char in string:
+            keycode = self._char_to_keycode(char)
+            # If this is a shifted char, clear the SHIFT flag and press the SHIFT key.
+            if keycode & self.SHIFT_FLAG:
+                keycode &= ~self.SHIFT_FLAG
+                self.keyboard.press(Keycode.SHIFT)
+            self.keyboard.press(keycode)
+            self.keyboard.release_all()
+
+    def keycodes(self, char):
+        """Return a tuple of keycodes needed to type the given character.
+
+        :param char: A single ASCII character in a string.
+        :type char: str of length one.
+        :returns: tuple of Keycode keycodes.
+        :raises ValueError: if ``char`` is not ASCII or there is no keycode for it.
+
+        Examples::
+
+            # Returns (Keycode.TAB,)
+            keycodes('\t')
+            # Returns (Keycode.A,)
+            keycode('a')
+            # Returns (Keycode.SHIFT, Keycode.A)
+            keycode('A')
+            # Raises ValueError because it's a accented e and is not ASCII
+            keycode('é')
+        """
+        keycode = self._char_to_keycode(char)
+        if keycode & self.SHIFT_FLAG:
+            return (Keycode.SHIFT, keycode & ~self.SHIFT_FLAG)
+
+        return (keycode,)
+
+    def _char_to_keycode(self, char):
+        """Return the HID keycode for the given ASCII character, with the SHIFT_FLAG possibly set.
+
+        If the character requires pressing the Shift key, the SHIFT_FLAG bit is set.
+        You must clear this bit before passing the keycode in a USB report.
+        """
+        char_val = ord(char)
+        if char_val > 128:
+            raise ValueError("Not an ASCII character.")
+        keycode = self.ASCII_TO_KEYCODE[char_val]
+        if keycode == 0:
+            raise ValueError("No keycode available for character.")
+        return keycode
diff --git a/preload/adafruit_hid/keycode.py b/preload/adafruit_hid/keycode.py
new file mode 100644
index 0000000000000000000000000000000000000000..b6fd7ad3a4fbee42a082870b9c66a14e38510158
--- /dev/null
+++ b/preload/adafruit_hid/keycode.py
@@ -0,0 +1,315 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2017 Scott Shawcroft for Adafruit Industries
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.keycode.Keycode`
+====================================================
+
+* Author(s): Scott Shawcroft, Dan Halbert
+"""
+
+
+class Keycode:
+    """USB HID Keycode constants.
+
+    This list is modeled after the names for USB keycodes defined in
+    https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf#page=53.
+    This list does not include every single code, but does include all the keys on
+    a regular PC or Mac keyboard.
+
+    Remember that keycodes are the names for key *positions* on a US keyboard, and may
+    not correspond to the character that you mean to send if you want to emulate non-US keyboard.
+    For instance, on a French keyboard (AZERTY instead of QWERTY),
+    the keycode for 'q' is used to indicate an 'a'. Likewise, 'y' represents 'z' on
+    a German keyboard. This is historical: the idea was that the keycaps could be changed
+    without changing the keycodes sent, so that different firmware was not needed for
+    different variations of a keyboard.
+    """
+
+    # pylint: disable-msg=invalid-name
+    A = 0x04
+    """``a`` and ``A``"""
+    B = 0x05
+    """``b`` and ``B``"""
+    C = 0x06
+    """``c`` and ``C``"""
+    D = 0x07
+    """``d`` and ``D``"""
+    E = 0x08
+    """``e`` and ``E``"""
+    F = 0x09
+    """``f`` and ``F``"""
+    G = 0x0A
+    """``g`` and ``G``"""
+    H = 0x0B
+    """``h`` and ``H``"""
+    I = 0x0C
+    """``i`` and ``I``"""
+    J = 0x0D
+    """``j`` and ``J``"""
+    K = 0x0E
+    """``k`` and ``K``"""
+    L = 0x0F
+    """``l`` and ``L``"""
+    M = 0x10
+    """``m`` and ``M``"""
+    N = 0x11
+    """``n`` and ``N``"""
+    O = 0x12
+    """``o`` and ``O``"""
+    P = 0x13
+    """``p`` and ``P``"""
+    Q = 0x14
+    """``q`` and ``Q``"""
+    R = 0x15
+    """``r`` and ``R``"""
+    S = 0x16
+    """``s`` and ``S``"""
+    T = 0x17
+    """``t`` and ``T``"""
+    U = 0x18
+    """``u`` and ``U``"""
+    V = 0x19
+    """``v`` and ``V``"""
+    W = 0x1A
+    """``w`` and ``W``"""
+    X = 0x1B
+    """``x`` and ``X``"""
+    Y = 0x1C
+    """``y`` and ``Y``"""
+    Z = 0x1D
+    """``z`` and ``Z``"""
+
+    ONE = 0x1E
+    """``1`` and ``!``"""
+    TWO = 0x1F
+    """``2`` and ``@``"""
+    THREE = 0x20
+    """``3`` and ``#``"""
+    FOUR = 0x21
+    """``4`` and ``$``"""
+    FIVE = 0x22
+    """``5`` and ``%``"""
+    SIX = 0x23
+    """``6`` and ``^``"""
+    SEVEN = 0x24
+    """``7`` and ``&``"""
+    EIGHT = 0x25
+    """``8`` and ``*``"""
+    NINE = 0x26
+    """``9`` and ``(``"""
+    ZERO = 0x27
+    """``0`` and ``)``"""
+    ENTER = 0x28
+    """Enter (Return)"""
+    RETURN = ENTER
+    """Alias for ``ENTER``"""
+    ESCAPE = 0x29
+    """Escape"""
+    BACKSPACE = 0x2A
+    """Delete backward (Backspace)"""
+    TAB = 0x2B
+    """Tab and Backtab"""
+    SPACEBAR = 0x2C
+    """Spacebar"""
+    SPACE = SPACEBAR
+    """Alias for SPACEBAR"""
+    MINUS = 0x2D
+    """``-` and ``_``"""
+    EQUALS = 0x2E
+    """``=` and ``+``"""
+    LEFT_BRACKET = 0x2F
+    """``[`` and ``{``"""
+    RIGHT_BRACKET = 0x30
+    """``]`` and ``}``"""
+    BACKSLASH = 0x31
+    r"""``\`` and ``|``"""
+    POUND = 0x32
+    """``#`` and ``~`` (Non-US keyboard)"""
+    SEMICOLON = 0x33
+    """``;`` and ``:``"""
+    QUOTE = 0x34
+    """``'`` and ``"``"""
+    GRAVE_ACCENT = 0x35
+    r""":literal:`\`` and ``~``"""
+    COMMA = 0x36
+    """``,`` and ``<``"""
+    PERIOD = 0x37
+    """``.`` and ``>``"""
+    FORWARD_SLASH = 0x38
+    """``/`` and ``?``"""
+
+    CAPS_LOCK = 0x39
+    """Caps Lock"""
+
+    F1 = 0x3A
+    """Function key F1"""
+    F2 = 0x3B
+    """Function key F2"""
+    F3 = 0x3C
+    """Function key F3"""
+    F4 = 0x3D
+    """Function key F4"""
+    F5 = 0x3E
+    """Function key F5"""
+    F6 = 0x3F
+    """Function key F6"""
+    F7 = 0x40
+    """Function key F7"""
+    F8 = 0x41
+    """Function key F8"""
+    F9 = 0x42
+    """Function key F9"""
+    F10 = 0x43
+    """Function key F10"""
+    F11 = 0x44
+    """Function key F11"""
+    F12 = 0x45
+    """Function key F12"""
+
+    PRINT_SCREEN = 0x46
+    """Print Screen (SysRq)"""
+    SCROLL_LOCK = 0x47
+    """Scroll Lock"""
+    PAUSE = 0x48
+    """Pause (Break)"""
+
+    INSERT = 0x49
+    """Insert"""
+    HOME = 0x4A
+    """Home (often moves to beginning of line)"""
+    PAGE_UP = 0x4B
+    """Go back one page"""
+    DELETE = 0x4C
+    """Delete forward"""
+    END = 0x4D
+    """End (often moves to end of line)"""
+    PAGE_DOWN = 0x4E
+    """Go forward one page"""
+
+    RIGHT_ARROW = 0x4F
+    """Move the cursor right"""
+    LEFT_ARROW = 0x50
+    """Move the cursor left"""
+    DOWN_ARROW = 0x51
+    """Move the cursor down"""
+    UP_ARROW = 0x52
+    """Move the cursor up"""
+
+    KEYPAD_NUMLOCK = 0x53
+    """Num Lock (Clear on Mac)"""
+    KEYPAD_FORWARD_SLASH = 0x54
+    """Keypad ``/``"""
+    KEYPAD_ASTERISK = 0x55
+    """Keypad ``*``"""
+    KEYPAD_MINUS = 0x56
+    """Keyapd ``-``"""
+    KEYPAD_PLUS = 0x57
+    """Keypad ``+``"""
+    KEYPAD_ENTER = 0x58
+    """Keypad Enter"""
+    KEYPAD_ONE = 0x59
+    """Keypad ``1`` and End"""
+    KEYPAD_TWO = 0x5A
+    """Keypad ``2`` and Down Arrow"""
+    KEYPAD_THREE = 0x5B
+    """Keypad ``3`` and PgDn"""
+    KEYPAD_FOUR = 0x5C
+    """Keypad ``4`` and Left Arrow"""
+    KEYPAD_FIVE = 0x5D
+    """Keypad ``5``"""
+    KEYPAD_SIX = 0x5E
+    """Keypad ``6`` and Right Arrow"""
+    KEYPAD_SEVEN = 0x5F
+    """Keypad ``7`` and Home"""
+    KEYPAD_EIGHT = 0x60
+    """Keypad ``8`` and Up Arrow"""
+    KEYPAD_NINE = 0x61
+    """Keypad ``9`` and PgUp"""
+    KEYPAD_ZERO = 0x62
+    """Keypad ``0`` and Ins"""
+    KEYPAD_PERIOD = 0x63
+    """Keypad ``.`` and Del"""
+    KEYPAD_BACKSLASH = 0x64
+    """Keypad ``\\`` and ``|`` (Non-US)"""
+
+    APPLICATION = 0x65
+    """Application: also known as the Menu key (Windows)"""
+    POWER = 0x66
+    """Power (Mac)"""
+    KEYPAD_EQUALS = 0x67
+    """Keypad ``=`` (Mac)"""
+    F13 = 0x68
+    """Function key F13 (Mac)"""
+    F14 = 0x69
+    """Function key F14 (Mac)"""
+    F15 = 0x6A
+    """Function key F15 (Mac)"""
+    F16 = 0x6B
+    """Function key F16 (Mac)"""
+    F17 = 0x6C
+    """Function key F17 (Mac)"""
+    F18 = 0x6D
+    """Function key F18 (Mac)"""
+    F19 = 0x6E
+    """Function key F19 (Mac)"""
+
+    LEFT_CONTROL = 0xE0
+    """Control modifier left of the spacebar"""
+    CONTROL = LEFT_CONTROL
+    """Alias for LEFT_CONTROL"""
+    LEFT_SHIFT = 0xE1
+    """Shift modifier left of the spacebar"""
+    SHIFT = LEFT_SHIFT
+    """Alias for LEFT_SHIFT"""
+    LEFT_ALT = 0xE2
+    """Alt modifier left of the spacebar"""
+    ALT = LEFT_ALT
+    """Alias for LEFT_ALT; Alt is also known as Option (Mac)"""
+    OPTION = ALT
+    """Labeled as Option on some Mac keyboards"""
+    LEFT_GUI = 0xE3
+    """GUI modifier left of the spacebar"""
+    GUI = LEFT_GUI
+    """Alias for LEFT_GUI; GUI is also known as the Windows key, Command (Mac), or Meta"""
+    WINDOWS = GUI
+    """Labeled with a Windows logo on Windows keyboards"""
+    COMMAND = GUI
+    """Labeled as Command on Mac keyboards, with a clover glyph"""
+    RIGHT_CONTROL = 0xE4
+    """Control modifier right of the spacebar"""
+    RIGHT_SHIFT = 0xE5
+    """Shift modifier right of the spacebar"""
+    RIGHT_ALT = 0xE6
+    """Alt modifier right of the spacebar"""
+    RIGHT_GUI = 0xE7
+    """GUI modifier right of the spacebar"""
+
+    # pylint: enable-msg=invalid-name
+    @classmethod
+    def modifier_bit(cls, keycode):
+        """Return the modifer bit to be set in an HID keycode report if this is a
+        modifier key; otherwise return 0."""
+        return (
+            1 << (keycode - 0xE0) if cls.LEFT_CONTROL <= keycode <= cls.RIGHT_GUI else 0
+        )
diff --git a/preload/adafruit_hid/mouse.py b/preload/adafruit_hid/mouse.py
new file mode 100644
index 0000000000000000000000000000000000000000..404d7e19c729cc463febc72396002f4dfda2d190
--- /dev/null
+++ b/preload/adafruit_hid/mouse.py
@@ -0,0 +1,165 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2017 Dan Halbert
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+`adafruit_hid.mouse.Mouse`
+====================================================
+
+* Author(s): Dan Halbert
+"""
+import time
+
+from . import find_device
+
+
+class Mouse:
+    """Send USB HID mouse reports."""
+
+    LEFT_BUTTON = 1
+    """Left mouse button."""
+    RIGHT_BUTTON = 2
+    """Right mouse button."""
+    MIDDLE_BUTTON = 4
+    """Middle mouse button."""
+
+    def __init__(self, devices):
+        """Create a Mouse object that will send USB mouse HID reports.
+
+        Devices can be a list of devices that includes a keyboard device or a keyboard device
+        itself. A device is any object that implements ``send_report()``, ``usage_page`` and
+        ``usage``.
+        """
+        self._mouse_device = find_device(devices, usage_page=0x1, usage=0x02)
+
+        # Reuse this bytearray to send mouse reports.
+        # report[0] buttons pressed (LEFT, MIDDLE, RIGHT)
+        # report[1] x movement
+        # report[2] y movement
+        # report[3] wheel movement
+        self.report = bytearray(4)
+
+        # Do a no-op to test if HID device is ready.
+        # If not, wait a bit and try once more.
+        try:
+            self._send_no_move()
+        except OSError:
+            time.sleep(1)
+            self._send_no_move()
+
+    def press(self, buttons):
+        """Press the given mouse buttons.
+
+        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
+            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
+
+        Examples::
+
+            # Press the left button.
+            m.press(Mouse.LEFT_BUTTON)
+
+            # Press the left and right buttons simultaneously.
+            m.press(Mouse.LEFT_BUTTON | Mouse.RIGHT_BUTTON)
+        """
+        self.report[0] |= buttons
+        self._send_no_move()
+
+    def release(self, buttons):
+        """Release the given mouse buttons.
+
+        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
+            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
+        """
+        self.report[0] &= ~buttons
+        self._send_no_move()
+
+    def release_all(self):
+        """Release all the mouse buttons."""
+        self.report[0] = 0
+        self._send_no_move()
+
+    def click(self, buttons):
+        """Press and release the given mouse buttons.
+
+        :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``,
+            ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``.
+
+        Examples::
+
+            # Click the left button.
+            m.click(Mouse.LEFT_BUTTON)
+
+            # Double-click the left button.
+            m.click(Mouse.LEFT_BUTTON)
+            m.click(Mouse.LEFT_BUTTON)
+        """
+        self.press(buttons)
+        self.release(buttons)
+
+    def move(self, x=0, y=0, wheel=0):
+        """Move the mouse and turn the wheel as directed.
+
+        :param x: Move the mouse along the x axis. Negative is to the left, positive
+            is to the right.
+        :param y: Move the mouse along the y axis. Negative is upwards on the display,
+            positive is downwards.
+        :param wheel: Rotate the wheel this amount. Negative is toward the user, positive
+            is away from the user. The scrolling effect depends on the host.
+
+        Examples::
+
+            # Move 100 to the left. Do not move up and down. Do not roll the scroll wheel.
+            m.move(-100, 0, 0)
+            # Same, with keyword arguments.
+            m.move(x=-100)
+
+            # Move diagonally to the upper right.
+            m.move(50, 20)
+            # Same.
+            m.move(x=50, y=-20)
+
+            # Roll the mouse wheel away from the user.
+            m.move(wheel=1)
+        """
+        # Send multiple reports if necessary to move or scroll requested amounts.
+        while x != 0 or y != 0 or wheel != 0:
+            partial_x = self._limit(x)
+            partial_y = self._limit(y)
+            partial_wheel = self._limit(wheel)
+            self.report[1] = partial_x & 0xFF
+            self.report[2] = partial_y & 0xFF
+            self.report[3] = partial_wheel & 0xFF
+            self._mouse_device.send_report(self.report)
+            x -= partial_x
+            y -= partial_y
+            wheel -= partial_wheel
+
+    def _send_no_move(self):
+        """Send a button-only report."""
+        self.report[1] = 0
+        self.report[2] = 0
+        self.report[3] = 0
+        self._mouse_device.send_report(self.report)
+
+    @staticmethod
+    def _limit(dist):
+        return min(127, max(-127, dist))
diff --git a/preload/apps/hid/__init__.py b/preload/apps/hid/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9aaa8c76a0626aebe5aaec3a1ffa846b46af61a1
--- /dev/null
+++ b/preload/apps/hid/__init__.py
@@ -0,0 +1,191 @@
+import buttons
+import color
+import display
+import ble_hid
+import bhi160
+import config
+
+from adafruit_hid.keyboard import Keyboard
+from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
+from adafruit_hid.keycode import Keycode
+
+from adafruit_hid.mouse import Mouse
+
+from adafruit_hid.consumer_control_code import ConsumerControlCode
+from adafruit_hid.consumer_control import ConsumerControl
+
+
+import time
+import os
+
+
+disp = display.open()
+
+
+def keyboard_demo():
+    disp.clear()
+    disp.print("  card10", posy=0)
+    disp.print(" Keyboard", posy=20)
+    disp.print("F19", posy=60, fg=color.BLUE)
+    disp.print("Backspace", posy=40, posx=20, fg=color.RED)
+    disp.print("Type", posy=60, posx=100, fg=color.GREEN)
+    disp.update()
+
+    k = Keyboard(ble_hid.devices)
+    kl = KeyboardLayoutUS(k)
+
+    b_old = buttons.read()
+    while True:
+        b_new = buttons.read()
+        if not b_old == b_new:
+            print(b_new)
+            b_old = b_new
+            if b_new == buttons.TOP_RIGHT:
+                # print("back")  # for debug in REPL
+                k.send(Keycode.BACKSPACE)
+
+            elif b_new == buttons.BOTTOM_RIGHT:
+                # use keyboard_layout for words
+                kl.write("Demo with a long text to show how fast a card10 can type!")
+
+            elif b_new == buttons.BOTTOM_LEFT:
+                # add shift modifier
+                k.send(Keycode.SHIFT, Keycode.F19)
+
+
+def mouse_demo():
+    disp.clear()
+    disp.print("  card10", posy=0)
+    disp.print("  Mouse", posy=20)
+    disp.print("Left", posy=60, fg=color.BLUE)
+    disp.print("Midd", posy=40, posx=100, fg=color.RED)
+    disp.print("Right", posy=60, posx=80, fg=color.GREEN)
+    disp.update()
+
+    m = Mouse(ble_hid.devices)
+
+    def send_report(samples):
+        if len(samples) > 0:
+            x = -int(samples[0].z)
+            y = -int(samples[0].y)
+            print("Reporting", x, y)
+
+            m.move(x, y)
+
+    sensor = bhi160.BHI160Orientation(sample_rate=10, callback=send_report)
+
+    b_old = buttons.read()
+    while True:
+        b_new = buttons.read()
+        if not b_old == b_new:
+            print(b_new)
+            b_old = b_new
+            if b_new == buttons.TOP_RIGHT:
+                m.click(Mouse.MIDDLE_BUTTON)
+            elif b_new == buttons.BOTTOM_RIGHT:
+                m.click(Mouse.RIGHT_BUTTON)
+            elif b_new == buttons.BOTTOM_LEFT:
+                m.click(Mouse.LEFT_BUTTON)
+
+
+def control_demo():
+    disp.clear()
+    disp.print("  card10", posy=0)
+    disp.print("  Control", posy=20)
+    disp.print("Play", posy=60, fg=color.BLUE)
+    disp.print("Vol+", posy=40, posx=100, fg=color.RED)
+    disp.print("Vol-", posy=60, posx=100, fg=color.GREEN)
+    disp.update()
+
+    cc = ConsumerControl(ble_hid.devices)
+
+    b_old = buttons.read()
+    while True:
+        b_new = buttons.read()
+        if not b_old == b_new:
+            print(b_new)
+            b_old = b_new
+            if b_new == buttons.TOP_RIGHT:
+                cc.send(ConsumerControlCode.VOLUME_INCREMENT)
+            elif b_new == buttons.BOTTOM_RIGHT:
+                cc.send(ConsumerControlCode.VOLUME_DECREMENT)
+            elif b_new == buttons.BOTTOM_LEFT:
+                cc.send(ConsumerControlCode.PLAY_PAUSE)
+
+
+def selection_screen():
+    disp.clear()
+    disp.print("card10 HID", posy=0)
+    disp.print("   Demo", posy=20)
+    disp.print("KBD", posy=60, fg=color.BLUE)
+    disp.print("Mouse", posy=40, posx=80, fg=color.RED)
+    disp.print("Control", posy=60, posx=60, fg=color.GREEN)
+    disp.update()
+
+    b_old = buttons.read()
+    while True:
+        b_new = buttons.read()
+        if not b_old == b_new:
+            print(b_new)
+            b_old = b_new
+            if b_new == buttons.TOP_RIGHT:
+                mouse_demo()
+            elif b_new == buttons.BOTTOM_RIGHT:
+                control_demo()
+            elif b_new == buttons.BOTTOM_LEFT:
+                keyboard_demo()
+
+
+def set_config(enable):
+    if enable:
+        config.set_string("ble_hid_enable", "true")
+    else:
+        config.set_string("ble_hid_enable", "false")
+
+    disp.clear()
+    disp.print("resetting", posy=0, fg=[0, 255, 255])
+    disp.print("to toggle", posy=20, fg=[0, 255, 255])
+    disp.print("HID state", posy=40, fg=[0, 255, 255])
+    disp.update()
+    os.reset()
+
+
+def welcome_screen(is_enabled):
+    disp.clear()
+    disp.print("card10 HID", posy=0)
+    disp.print("   Demo", posy=20)
+
+    if is_enabled:
+        disp.print("Start ->", posy=40, posx=40, fg=color.GREEN)
+        disp.print("<- Disable", posy=60, posx=0, fg=color.RED)
+    else:
+        disp.print("Enable ->", posy=40, posx=30, fg=color.GREEN)
+
+    disp.update()
+
+    b_old = buttons.read()
+    while True:
+        b_new = buttons.read()
+        if not b_old == b_new:
+            print(b_new)
+            b_old = b_new
+            if b_new == buttons.TOP_RIGHT:
+                if is_enabled:
+                    # while buttons.read(): pass
+                    selection_screen()
+                else:
+                    set_config(True)
+            elif b_new == buttons.BOTTOM_LEFT:
+                if is_enabled:
+                    set_config(False)
+
+
+is_enabled = False
+try:
+    enabled = config.get_string("ble_hid_enable")
+    if enabled.lower() == "true" or enabled == "1":
+        is_enabled = True
+except OSError:
+    pass
+
+welcome_screen(is_enabled)
diff --git a/preload/apps/hid/metadata.json b/preload/apps/hid/metadata.json
new file mode 100644
index 0000000000000000000000000000000000000000..a4d27e2c24bfe066d8357d26e8fe5f190862c516
--- /dev/null
+++ b/preload/apps/hid/metadata.json
@@ -0,0 +1 @@
+{"author": "card10 contributors", "name": "HID Demo", "description": "HID Keyboard/Mouse/Control", "category": "Hardware", "revision": -1, "source":"preload"}
diff --git a/pycardium/meson.build b/pycardium/meson.build
index 2bfb09d31b1c3578ed0da629cf3f47c1131bee35..8621e66128ca22cecdd3d53b021f5ee03d1fab1d 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -17,6 +17,7 @@ modsrc = files(
   'modules/power.c',
   'modules/spo2_algo.c',
   'modules/sys_ble.c',
+  'modules/sys_ble_hid.c',
   'modules/sys_bme680.c',
   'modules/sys_display.c',
   'modules/sys_leds.c',
diff --git a/pycardium/modules/py/ble_hid.py b/pycardium/modules/py/ble_hid.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a0d9715b2396224815aa45f9fdd18349c057a42
--- /dev/null
+++ b/pycardium/modules/py/ble_hid.py
@@ -0,0 +1,85 @@
+import sys_ble_hid
+import time
+
+
+class Report:
+    """
+    The Report class provides an interface to the Adafruit CircuitPython HID library
+    (https://github.com/adafruit/Adafruit_CircuitPython_HID/).
+
+    ``ble_hid.devices`` exposes a list of reports for use with the CircuitPython HID
+    classes. You usually do not have to interact with a report yourself but you can make
+    use of its ``send_report`` method to send raw HID reports to the host.
+
+    **Example using Adafruit CircuitPython HID library**:
+
+    .. code-block:: python
+
+        import ble_hid
+        from adafruit_hid.mouse import Mouse
+
+        m = Mouse(ble_hid.devices)
+        m.click(Mouse.MIDDLE_BUTTON)
+
+    **Example using raw non blocking access**:
+    .. code-block:: python
+
+        import ble_hid
+        report = ble_hid.Report(report_id=3, blocking=False) # Consumer Control
+        report.send_report(b'\\xe9\\x00') # 0x00E9: Volume Increase
+        report.send_report(b'\\x00\\x00') # 0x0000: Release button
+
+
+    .. versionadded:: 1.17
+    """
+
+    def __init__(self, report_id, blocking=False, usage_page=None, usage=None):
+        """
+        Initializes a report.
+
+        All parameters are available as properties of the resulting object and can be modified
+        during runtime.
+
+        :param report_id: The id of the report. Currently supported: 1: Keyboard, 2: Mouse, 3: Consumer control
+        :param blocking: If True :py:func`send_report()` will try sending the report until
+                         there is space in the queue (unless the host is not connected).
+        :param usage_page: Used by Adafruit CircuitPython HID library to identify report
+        :param usage: Used by Adafruit CircuitPython HID library to identify report
+        """
+
+        self.report_id = report_id
+        self.usage_page = usage_page
+        self.usage = usage
+        self.blocking = True
+
+    def send_report(self, data):
+        """
+        Tries to send a report to the host.
+
+        :param data: The data to be sent. Must not exceed the configured length of the report.
+        :rtype: bool
+        :returns: `True` if the report was sent, `False` if the report was queued for sending.
+        :raises OSError: if there is no connection to a host or BLE HID is not enabled.
+        :raises MemoryError: if there was no space in the queue (only raised if ``blocking`` was set to `False`).
+        """
+
+        if self.blocking:
+            # Loop until we are able to place the report in the queue
+            # Forward all other exceptions to the caller
+            while True:
+                try:
+                    return sys_ble_hid.send_report(self.report_id, data)
+                    break
+                except MemoryError:
+                    time.sleep(0.1)
+        else:
+            # Forward all exceptions to the caller
+            return sys_ble_hid.send_report(self.report_id, data)
+
+
+# Reports as defined in the HID report map in epicardium/ble/hid.c
+devices = [
+    Report(report_id=1, blocking=True, usage_page=0x01, usage=0x06),
+    Report(report_id=2, blocking=True, usage_page=0x01, usage=0x02),
+    Report(report_id=3, blocking=True, usage_page=0x0C, usage=0x01),
+]
diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build
index b8d3310f053c8a46febe1935b15686cbcff86c6a..0130c75b8407fb77535b33bff087ab6160c3b5ff 100644
--- a/pycardium/modules/py/meson.build
+++ b/pycardium/modules/py/meson.build
@@ -1,6 +1,7 @@
 python_modules = files(
   'config.py',
   'bhi160.py',
+  'ble_hid.py',
   'bme680.py',
   'color.py',
   'display.py',
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index f842f5344616ab139c988be27c73f7f893e88310..f804089f3c66b8abf03e408b0b7deeedaf5228e0 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -211,6 +211,8 @@ Q(EVENT_HANDLE_NUMERIC_COMPARISON)
 Q(EVENT_PAIRING_COMPLETE)
 Q(EVENT_PAIRING_FAILED)
 Q(EVENT_SCAN_REPORT)
+Q(sys_ble_hid)
+Q(send_report)
 
 /* SpO2 */
 Q(spo2_algo)
diff --git a/pycardium/modules/sys_ble_hid.c b/pycardium/modules/sys_ble_hid.c
new file mode 100644
index 0000000000000000000000000000000000000000..7bc725babaf50cf47a53337fe6ca4e25b6521dea
--- /dev/null
+++ b/pycardium/modules/sys_ble_hid.c
@@ -0,0 +1,45 @@
+#include "epicardium.h"
+
+#include "py/builtin.h"
+#include "py/obj.h"
+#include "py/runtime.h"
+
+static mp_obj_t mp_sys_ble_hid_send_report(mp_obj_t report_id, mp_obj_t data)
+{
+	mp_buffer_info_t bufinfo;
+	mp_get_buffer_raise(data, &bufinfo, MP_BUFFER_READ);
+
+	int ret = epic_ble_hid_send_report(
+		mp_obj_get_int(report_id), bufinfo.buf, bufinfo.len
+	);
+
+	if (ret == -EAGAIN) {
+		mp_raise_msg(&mp_type_MemoryError, NULL);
+	}
+
+	if (ret < 0) {
+		mp_raise_OSError(-ret);
+	}
+
+	return ret == 0 ? mp_const_true : mp_const_false;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(
+	sys_ble_hid_send_report_obj, mp_sys_ble_hid_send_report
+);
+
+static const mp_rom_map_elem_t sys_ble_hid_module_globals_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_ble_hid) },
+	{ MP_ROM_QSTR(MP_QSTR_send_report),
+	  MP_ROM_PTR(&sys_ble_hid_send_report_obj) },
+};
+static MP_DEFINE_CONST_DICT(
+	sys_ble_hid_module_globals, sys_ble_hid_module_globals_table
+);
+
+const mp_obj_module_t sys_ble_hid_module = {
+	.base    = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&sys_ble_hid_module_globals,
+};
+
+/* clang-format off */
+MP_REGISTER_MODULE(MP_QSTR_sys_ble_hid, sys_ble_hid_module, MODULE_HID_ENABLED);
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 647b159f2886d1497f31858430c99d53e8a1c751..8749da69403d4eeadd7e799f612c94662bc1d5fd 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -53,6 +53,7 @@ int mp_hal_csprng_read_int(void);
 #define MICROPY_PY_UERRNO                   (1)
 #define MICROPY_PY_FRAMEBUF                 (1)
 #define MICROPY_PY_BLUETOOTH                (1)
+#define MICROPY_PY_BUILTINS_MEMORYVIEW      (1)
 
 /* Modules */
 #define MODULE_BHI160_ENABLED               (1)
@@ -61,6 +62,7 @@ int mp_hal_csprng_read_int(void);
 #define MODULE_BUTTONS_ENABLED              (1)
 #define MODULE_DISPLAY_ENABLED              (1)
 #define MODULE_GPIO_ENABLED                 (1)
+#define MODULE_HID_ENABLED                  (1)
 #define MODULE_INTERRUPT_ENABLED            (1)
 #define MODULE_LEDS_ENABLED                 (1)
 #define MODULE_LIGHT_SENSOR_ENABLED         (1)