diff --git a/epicardium/ble/hid_work.c b/epicardium/ble/hid_work.c
index 4fdd0c1deb2ba0a46638e48a1896fe93b9d7e158..406f8bd0a9c37d05c6cb8b1134f1b10f4d5b8b63 100644
--- a/epicardium/ble/hid_work.c
+++ b/epicardium/ble/hid_work.c
@@ -114,7 +114,7 @@ static bool hid_dequeue_data(dmConnId_t connId)
 	return true;
 }
 
-int epic_hid_send_report(uint8_t reportId, uint8_t *data, uint8_t len)
+int epic_ble_hid_send_report(uint8_t reportId, uint8_t *data, uint8_t len)
 {
 	dmConnId_t connId = AppConnIsOpen();
 	if (connId == DM_CONN_ID_NONE) {
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index db5369ecb7b162c49a591442bbe07d6cad1ce92d..3a00a64783697be953fb5cedfd7d7ef19d0252a9 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -158,7 +158,7 @@ typedef _Bool bool;
 #define API_BLE_GET_LAST_PAIRING_NAME 0x145
 #define API_BLE_GET_PEER_DEVICE_NAME  0x146
 
-#define API_HID_SEND_REPORT        0x150
+#define API_BLE_HID_SEND_REPORT        0x150
 
 /* clang-format on */
 
@@ -2343,7 +2343,7 @@ API(API_BLE_GET_SCAN_REPORT, int epic_ble_get_scan_report(struct epic_scan_repor
  *    - ``-EAGAIN``: There is no space in the queue available. Try again later.
  *
  */
-API(API_HID_SEND_REPORT, int epic_hid_send_report(uint8_t reportId, uint8_t *data, uint8_t len));
+API(API_BLE_HID_SEND_REPORT, int epic_ble_hid_send_report(uint8_t reportId, uint8_t *data, uint8_t len));
 
 #endif /* _EPICARDIUM_H */
 
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..56d2baf5702dcbc6ee49a74297ad08caa8272cc7
--- /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.py b/preload/apps/hid.py
index 86c4a84238f1f65fb5c074fe89e37235722f572a..25a134ba56d9b84b7d77736cf539af2abdb73d93 100644
--- a/preload/apps/hid.py
+++ b/preload/apps/hid.py
@@ -1,11 +1,10 @@
-import hid
 import buttons
 import color
 import display
+import ble_hid
+from adafruit_hid.consumer_control_code import ConsumerControlCode
+from adafruit_hid.consumer_control import ConsumerControl
 
-VOLUME_UP = 0xE9
-VOLUME_DOWN = 0xEA
-PLAY_PAUSE = 0xCD
 
 disp = display.open()
 disp.clear()
@@ -16,6 +15,8 @@ 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()
@@ -23,11 +24,8 @@ while True:
         print(b_new)
         b_old = b_new
         if b_new == buttons.TOP_RIGHT:
-            hid.set_control(VOLUME_UP)
-            hid.set_control(0)
+            cc.send(ConsumerControlCode.VOLUME_INCREMENT)
         elif b_new == buttons.BOTTOM_RIGHT:
-            hid.set_control(VOLUME_DOWN)
-            hid.set_control(0)
+            cc.send(ConsumerControlCode.VOLUME_DECREMENT)
         elif b_new == buttons.BOTTOM_LEFT:
-            hid.set_control(PLAY_PAUSE)
-            hid.set_control(0)
+            cc.send(ConsumerControlCode.PLAY_PAUSE)
diff --git a/pycardium/meson.build b/pycardium/meson.build
index 758c565bf4887f0f22479e0c7f5ed2f3d7bd40e7..8621e66128ca22cecdd3d53b021f5ee03d1fab1d 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -7,7 +7,6 @@ modsrc = files(
   'modules/fat_file.c',
   'modules/fat_reader_import.c',
   'modules/gpio.c',
-  'modules/hid.c',
   'modules/interrupt.c',
   'modules/light_sensor.c',
   'modules/max30001-sys.c',
@@ -18,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/hid.c b/pycardium/modules/hid.c
deleted file mode 100644
index 62e198fd6a468dab687967f7fbdb95400fd60ad4..0000000000000000000000000000000000000000
--- a/pycardium/modules/hid.c
+++ /dev/null
@@ -1,28 +0,0 @@
-#include "epicardium.h"
-
-#include "py/builtin.h"
-#include "py/obj.h"
-#include "py/runtime.h"
-
-static mp_obj_t mp_hid_set_control(mp_obj_t control_code)
-{
-	int code       = mp_obj_get_int(control_code);
-	uint8_t data[] = { code, code >> 8 };
-	int ret        = epic_hid_send_report(3, data, sizeof(data));
-	return mp_obj_new_int(ret);
-}
-static MP_DEFINE_CONST_FUN_OBJ_1(hid_set_control_obj, mp_hid_set_control);
-
-static const mp_rom_map_elem_t hid_module_globals_table[] = {
-	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_hid) },
-	{ MP_ROM_QSTR(MP_QSTR_set_control), MP_ROM_PTR(&hid_set_control_obj) },
-};
-static MP_DEFINE_CONST_DICT(hid_module_globals, hid_module_globals_table);
-
-const mp_obj_module_t hid_module = {
-	.base    = { &mp_type_module },
-	.globals = (mp_obj_dict_t *)&hid_module_globals,
-};
-
-/* clang-format off */
-MP_REGISTER_MODULE(MP_QSTR_hid, hid_module, MODULE_HID_ENABLED);
diff --git a/pycardium/modules/py/ble_hid.py b/pycardium/modules/py/ble_hid.py
new file mode 100644
index 0000000000000000000000000000000000000000..9869f017b4b8a9b0abe556870301160502f29a9a
--- /dev/null
+++ b/pycardium/modules/py/ble_hid.py
@@ -0,0 +1,19 @@
+import sys_ble_hid
+
+
+class Report:
+    def __init__(self, report_id, usage_page, usage):
+        self.report_id = report_id
+        self.usage_page = usage_page
+        self.usage = usage
+
+    def send_report(self, data):
+        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, usage_page=0x01, usage=0x06),
+    Report(report_id=2, usage_page=0x01, usage=0x02),
+    Report(report_id=3, 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 fd33985e04a083572459c0be961051dd10031717..f804089f3c66b8abf03e408b0b7deeedaf5228e0 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -211,8 +211,8 @@ Q(EVENT_HANDLE_NUMERIC_COMPARISON)
 Q(EVENT_PAIRING_COMPLETE)
 Q(EVENT_PAIRING_FAILED)
 Q(EVENT_SCAN_REPORT)
-Q(hid)
-Q(set_control)
+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..47e0228ab7594075c0ca934857dd16999e8dc812
--- /dev/null
+++ b/pycardium/modules/sys_ble_hid.c
@@ -0,0 +1,36 @@
+#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
+	);
+	return mp_obj_new_int(ret);
+}
+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);