CircuitPython NKRO Keyboard

This information is slightly out of date! The format of boot.py was modified in 7.0.0-rc.0 and newer

This is not a boot-compatible keyboard, but it does allow full NRKO. Based on my light testing, it works on Linux (Debian 11 & Android). Presumably it does not work as a boot keyboard device.

You'll need CircuitPython 7 (including the latest alpha release).

The descriptor was designed based on one from the qmk documentation.

In boot.py, set up a custom keyboard descriptor. After putting boot.py on CIRCUITPY, reset the device and make sure that boot-out.txt contains the line "enabled HID with custom keyboard device".

import usb_hid

BITMAP_KEYBOARD_DESCRIPTOR_REPORT_ID = 7
REPORT_BYTES = 16
bitmap_keyboard_descriptor = bytes((
        0x05, 0x01,                     # Usage Page (Generic Desktop),
        0x09, 0x06,                     # Usage (Keyboard),
        0xA1, 0x01,                     # Collection (Application),
        0x85, 0xFF,                     #   6,7 Report ID  [SET AT RUNTIME]
        # bitmap of modifiers
        0x75, 0x01,                     #   Report Size (1),
        0x95, 0x08,                     #   Report Count (8),
        0x05, 0x07,                     #   Usage Page (Key Codes),
        0x19, 0xE0,                     #   Usage Minimum (224),
        0x29, 0xE7,                     #   Usage Maximum (231),
        0x15, 0x00,                     #   Logical Minimum (0),
        0x25, 0x01,                     #   Logical Maximum (1),
        0x81, 0x02,                     #   Input (Data, Variable, Absolute), ;Modifier byte
        # LED output report
        0x95, 0x05,                     #   Report Count (5),
        0x75, 0x01,                     #   Report Size (1),
        0x05, 0x08,                     #   Usage Page (LEDs),
        0x19, 0x01,                     #   Usage Minimum (1),
        0x29, 0x05,                     #   Usage Maximum (5),
        0x91, 0x02,                     #   Output (Data, Variable, Absolute),
        0x95, 0x01,                     #   Report Count (1),
        0x75, 0x03,                     #   Report Size (3),
        0x91, 0x03,                     #   Output (Constant),
        # bitmap of keys
        0x95, (REPORT_BYTES-1)*8,       #   Report Count (),
        0x75, 0x01,                     #   Report Size (1),
        0x15, 0x00,                     #   Logical Minimum (0),
        0x25, 0x01,                     #   Logical Maximum(1),
        0x05, 0x07,                     #   Usage Page (Key Codes),
        0x19, 0x00,                     #   Usage Minimum (0),
        0x29, (REPORT_BYTES-1)*8-1,     #   Usage Maximum (),
        0x81, 0x02,                     #   Input (Data, Variable, Absolute),
        0xc0                            # End Collection
))

bitmap_keyboard = usb_hid.Device(
    report_descriptor = bitmap_keyboard_descriptor,
    usage_page = 0x1,
    usage = 0x6,
    in_report_length = 16,
    out_report_length = 1,
    report_id_index = BITMAP_KEYBOARD_DESCRIPTOR_REPORT_ID,
)

print(bitmap_keyboard)
devices = [
    bitmap_keyboard
    usb_hid.Device.CONSUMER_CONTROL,
]
usb_hid.enable(devices)
print("enabled HID with custom keyboard device") 

In your code.py, use this subclass of adafruit_hid.keyboard.Keyboard:

class BitmapKeyboard(Keyboard):
    def __init__(self, devices):
        for device in devices:
            if device.usage == 6 and device.usage_page == 1:
                try:
                    device.send_report(b'\0' * 16)
                except ValueError:
                    print("found device but could not send report")
                    continue
                self._keyboard_device = device
                break
        else:
            raise IOError("Could not find an HID keyboard device.")

        # report[0] modifiers
        # report[1:16] regular key presses bitmask
        self.report = bytearray(16)

        self.report_modifier = memoryview(self.report)[0:1]
        self.report_keys = memoryview(self.report)[1:]

    def _add_keycode_to_report(self, keycode):
        modifier = Keycode.modifier_bit(keycode)
        print (f"{keycode:02x} {modifier:02x}")
        if modifier:
            # Set bit for this modifier.
            self.report_modifier[0] |= modifier
        else:
            self.report_keys[keycode >> 3] |= 1 << (keycode & 0x7)

    def _remove_keycode_from_report(self, keycode):
        modifier = Keycode.modifier_bit(keycode)
        if modifier:
            # Set bit for this modifier.
            self.report_modifier[0] &= ~modifier
        else:
            self.report_keys[keycode >> 3] &= ~(1 << (keycode & 0x7))

    def release_all(self):
        for i in range(len(self.report)):
            self.report[i] = 0
        self._keyboard_device.send_report(self.report)


Entry first conceived on 10 July 2021, 19:12 UTC, last modified on 8 September 2021, 17:35 UTC
Website Copyright © 2004-2021 Jeff Epler