diff --git a/kmk/circuitpython/hid.py b/kmk/circuitpython/hid.py new file mode 100644 index 0000000..c86b0b5 --- /dev/null +++ b/kmk/circuitpython/hid.py @@ -0,0 +1,39 @@ +import usb_hid +from kmk.common.abstract.hid import AbstractHidHelper +from kmk.common.consts import (HID_REPORT_SIZES, HIDReportTypes, HIDUsage, + HIDUsagePage) + + +class HIDHelper(AbstractHidHelper): + REPORT_BYTES = 9 + + def post_init(self): + self.devices = {} + + for device in usb_hid.devices: + if device.usage_page == HIDUsagePage.CONSUMER and device.usage == HIDUsage.CONSUMER: + self.devices[HIDReportTypes.CONSUMER] = device + continue + + if device.usage_page == HIDUsagePage.KEYBOARD and device.usage == HIDUsage.KEYBOARD: + self.devices[HIDReportTypes.KEYBOARD] = device + continue + + if device.usage_page == HIDUsagePage.MOUSE and device.usage == HIDUsage.MOUSE: + self.devices[HIDReportTypes.MOUSE] = device + continue + + if ( + device.usage_page == HIDUsagePage.SYSCONTROL and + device.usage == HIDUsage.SYSCONTROL + ): + self.devices[HIDReportTypes.SYSCONTROL] = device + continue + + def hid_send(self, evt): + # int, can be looked up in HIDReportTypes + reporting_device_const = self.report_device[0] + + return self.devices[reporting_device_const].send_report( + evt[1:HID_REPORT_SIZES[reporting_device_const] + 1], + ) diff --git a/kmk/common/abstract/hid.py b/kmk/common/abstract/hid.py new file mode 100644 index 0000000..ef77831 --- /dev/null +++ b/kmk/common/abstract/hid.py @@ -0,0 +1,155 @@ +import logging + +from kmk.common.consts import HIDReportTypes +from kmk.common.event_defs import HID_REPORT_EVENT +from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode, + ModifierKeycode) + + +class AbstractHidHelper: + REPORT_BYTES = 8 + + def __init__(self, store, log_level=logging.NOTSET): + self.logger = logging.getLogger(__name__) + self.logger.setLevel(log_level) + + self.store = store + self.store.subscribe( + lambda state, action: self._subscription(state, action), + ) + + self._evt = bytearray(self.REPORT_BYTES) + self.report_device = memoryview(self._evt)[0:1] + self.report_device[0] = HIDReportTypes.KEYBOARD + + # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view + # is "reserved" and evidently (mostly?) unused. However, other modes (or + # at least consumer, so far) will use this byte, which is the main reason + # this view exists. For KEYBOARD, use report_mods and report_non_mods + self.report_keys = memoryview(self._evt)[1:] + + self.report_mods = memoryview(self._evt)[1:2] + self.report_non_mods = memoryview(self._evt)[3:] + + self.post_init() + + def post_init(self): + pass + + def _subscription(self, state, action): + if action.type == HID_REPORT_EVENT: + self.clear_all() + + consumer_key = None + for key in state.keys_pressed: + if isinstance(key, ConsumerKeycode): + consumer_key = key + break + + reporting_device = self.report_device[0] + needed_reporting_device = HIDReportTypes.KEYBOARD + + if consumer_key: + needed_reporting_device = HIDReportTypes.CONSUMER + + if reporting_device != needed_reporting_device: + # If we are about to change reporting devices, release + # all keys and close our proverbial tab on the existing + # device, or keys will get stuck (mostly when releasing + # media/consumer keys) + self.send() + + self.report_device[0] = needed_reporting_device + + if consumer_key: + self.add_key(consumer_key) + else: + for key in state.keys_pressed: + if key.code >= FIRST_KMK_INTERNAL_KEYCODE: + continue + + if isinstance(key, ModifierKeycode): + self.add_modifier(key) + else: + self.add_key(key) + + if key.has_modifiers: + for mod in key.has_modifiers: + self.add_modifier(mod) + + self.send() + + def hid_send(self, evt): + raise NotImplementedError('hid_send(evt) must be implemented') + + def send(self): + self.logger.debug('Sending HID report: {}'.format(self._evt)) + self.hid_send(self._evt) + + return self + + def clear_all(self): + for idx, _ in enumerate(self.report_keys): + self.report_keys[idx] = 0x00 + + return self + + def clear_non_modifiers(self): + for idx, _ in enumerate(self.report_non_mods): + self.report_non_mods[idx] = 0x00 + + return self + + def add_modifier(self, modifier): + if isinstance(modifier, ModifierKeycode): + self.report_mods[0] |= modifier.code + else: + self.report_mods[0] |= modifier + + return self + + def remove_modifier(self, modifier): + if isinstance(modifier, ModifierKeycode): + self.report_mods[0] ^= modifier.code + else: + self.report_mods[0] ^= modifier + + return self + + def add_key(self, key): + # Try to find the first empty slot in the key report, and fill it + placed = False + + where_to_place = self.report_non_mods + + if self.report_device[0] == HIDReportTypes.CONSUMER: + where_to_place = self.report_keys + + for idx, _ in enumerate(where_to_place): + if where_to_place[idx] == 0x00: + where_to_place[idx] = key.code + placed = True + break + + if not placed: + self.logger.warning('Out of space in HID report, could not add key') + + return self + + def remove_key(self, key): + removed = False + + where_to_place = self.report_non_mods + + if self.report_device[0] == HIDReportTypes.CONSUMER: + where_to_place = self.report_keys + + for idx, _ in enumerate(where_to_place): + if where_to_place[idx] == key.code: + where_to_place[idx] = 0x00 + removed = True + + if not removed: + self.logger.warning('Tried to remove key that was not added') + + return self diff --git a/kmk/common/consts.py b/kmk/common/consts.py index ae681b5..064a9b0 100644 --- a/kmk/common/consts.py +++ b/kmk/common/consts.py @@ -5,10 +5,33 @@ class HIDReportTypes: SYSCONTROL = 4 +class HIDUsage: + KEYBOARD = 0x06 + MOUSE = 0x02 + CONSUMER = 0x01 + SYSCONTROL = 0x80 + + +class HIDUsagePage: + CONSUMER = 0x0C + KEYBOARD = MOUSE = SYSCONTROL = 0x01 + + +# Currently only used by the CircuitPython HIDHelper because CircuitPython +# actually enforces these limits with a ValueError. Unused on PyBoard because +# we can happily send full reports there and it magically works. +HID_REPORT_SIZES = { + HIDReportTypes.KEYBOARD: 8, + HIDReportTypes.MOUSE: 4, + HIDReportTypes.CONSUMER: 2, + HIDReportTypes.SYSCONTROL: 8, # TODO find the correct value for this +} + + HID_REPORT_STRUCTURE = bytes([ # Regular keyboard - 0x05, 0x01, # Usage Page (Generic Desktop) - 0x09, 0x06, # Usage (Keyboard) + 0x05, HIDUsagePage.KEYBOARD, # Usage Page (Generic Desktop) + 0x09, HIDUsage.KEYBOARD, # Usage (Keyboard) 0xA1, 0x01, # Collection (Application) 0x85, HIDReportTypes.KEYBOARD, # Report ID (1) 0x05, 0x07, # Usage Page (Keyboard) @@ -39,8 +62,8 @@ HID_REPORT_STRUCTURE = bytes([ 0x91, 0x01, # Output (Constant) 0xC0, # End Collection # Regular mouse - 0x05, 0x01, # Usage Page (Generic Desktop) - 0x09, 0x02, # Usage (Mouse) + 0x05, HIDUsagePage.MOUSE, # Usage Page (Generic Desktop) + 0x09, HIDUsage.MOUSE, # Usage (Mouse) 0xA1, 0x01, # Collection (Application) 0x09, 0x01, # Usage (Pointer) 0xA1, 0x00, # Collection (Physical) @@ -73,8 +96,8 @@ HID_REPORT_STRUCTURE = bytes([ 0xC0, # End Collection 0xC0, # End Collection # Consumer ("multimedia") keys - 0x05, 0x0C, # Usage Page (Consumer) - 0x09, 0x01, # Usage (Consumer Control) + 0x05, HIDUsagePage.CONSUMER, # Usage Page (Consumer) + 0x09, HIDUsage.CONSUMER, # Usage (Consumer Control) 0xA1, 0x01, # Collection (Application) 0x85, HIDReportTypes.CONSUMER, # Report ID (n) 0x75, 0x10, # Report Size (16) @@ -86,8 +109,8 @@ HID_REPORT_STRUCTURE = bytes([ 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, # End Collection # Power controls - 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) - 0x09, 0x80, # Usage (Sys Control) + 0x05, HIDUsagePage.SYSCONTROL, # Usage Page (Generic Desktop Ctrls) + 0x09, HIDUsage.SYSCONTROL, # Usage (Sys Control) 0xA1, 0x01, # Collection (Application) 0x85, HIDReportTypes.SYSCONTROL, # Report ID (n) 0x75, 0x02, # Report Size (2) diff --git a/kmk/common/internal_state.py b/kmk/common/internal_state.py index b503ecf..861cb13 100644 --- a/kmk/common/internal_state.py +++ b/kmk/common/internal_state.py @@ -52,7 +52,7 @@ class Store: cb(self.state, action) except Exception as e: self.logger.error('Callback failed, moving on') - print(sys.print_exception(e), file=sys.stderr) + sys.print_exception(e) def get_state(self): return self.state diff --git a/kmk/entrypoints/handwire/feather_m4_express.py b/kmk/entrypoints/handwire/feather_m4_express.py index acbd4dd..8ce7d06 100644 --- a/kmk/entrypoints/handwire/feather_m4_express.py +++ b/kmk/entrypoints/handwire/feather_m4_express.py @@ -1,6 +1,7 @@ import sys from logging import DEBUG +from kmk.circuitpython.hid import HIDHelper from kmk.circuitpython.matrix import MatrixScanner from kmk.common.consts import UnicodeModes from kmk.firmware import Firmware @@ -23,6 +24,7 @@ def main(): unicode_mode=unicode_mode, log_level=DEBUG, matrix_scanner=MatrixScanner, + hid=HIDHelper, ) firmware.go() diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py index 1dfa807..97edc60 100644 --- a/kmk/micropython/pyb_hid.py +++ b/kmk/micropython/pyb_hid.py @@ -1,11 +1,7 @@ -import logging - from pyb import USB_HID, delay, hid_keyboard -from kmk.common.consts import HID_REPORT_STRUCTURE, HIDReportTypes -from kmk.common.event_defs import HID_REPORT_EVENT -from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode, - ModifierKeycode) +from kmk.common.abstract.hid import AbstractHidHelper +from kmk.common.consts import HID_REPORT_STRUCTURE def generate_pyb_hid_descriptor(): @@ -14,80 +10,20 @@ def generate_pyb_hid_descriptor(): return tuple(existing_keyboard) -class HIDHelper: - def __init__(self, store, log_level=logging.NOTSET): - self.logger = logging.getLogger(__name__) - self.logger.setLevel(log_level) - - self.store = store - self.store.subscribe( - lambda state, action: self._subscription(state, action), - ) +class HIDHelper(AbstractHidHelper): + # For some bizarre reason this can no longer be 8, it'll just fail to send + # anything. This is almost certainly a bug in the report descriptor sent + # over in the boot process. For now the sacrifice is that we only support + # 5KRO until I figure this out, rather than the 6KRO HID defines. + REPORT_BYTES = 7 + def post_init(self): self._hid = USB_HID() - - # For some bizarre reason this can no longer be 8, it'll just fail to - # send anything. This is almost certainly a bug in the report descriptor - # sent over in the boot process. For now the sacrifice is that we only - # support 5KRO until I figure this out, rather than the 6KRO HID defines. - self._evt = bytearray(7) - self.report_device = memoryview(self._evt)[0:1] - - # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view - # is "reserved" and evidently (mostly?) unused. However, other modes (or - # at least consumer, so far) will use this byte, which is the main reason - # this view exists. For KEYBOARD, use report_mods and report_non_mods - self.report_keys = memoryview(self._evt)[1:] - - self.report_mods = memoryview(self._evt)[1:2] - self.report_non_mods = memoryview(self._evt)[3:] - - def _subscription(self, state, action): - if action.type == HID_REPORT_EVENT: - self.clear_all() - - consumer_key = None - for key in state.keys_pressed: - if isinstance(key, ConsumerKeycode): - consumer_key = key - break - - reporting_device = self.report_device[0] - needed_reporting_device = HIDReportTypes.KEYBOARD - - if consumer_key: - needed_reporting_device = HIDReportTypes.CONSUMER - - if reporting_device != needed_reporting_device: - # If we are about to change reporting devices, release - # all keys and close our proverbial tab on the existing - # device, or keys will get stuck (mostly when releasing - # media/consumer keys) - self.send() - - self.report_device[0] = needed_reporting_device - - if consumer_key: - self.add_key(consumer_key) - else: - for key in state.keys_pressed: - if key.code >= FIRST_KMK_INTERNAL_KEYCODE: - continue - - if isinstance(key, ModifierKeycode): - self.add_modifier(key) - else: - self.add_key(key) - - if key.has_modifiers: - for mod in key.has_modifiers: - self.add_modifier(mod) - - self.send() + self.hid_send = self._hid.send def send(self): self.logger.debug('Sending HID report: {}'.format(self._evt)) - self._hid.send(self._evt) + self.hid_send(self._evt) # Without this delay, events get clobbered and you'll likely end up with # a string like `heloooooooooooooooo` rather than `hello`. This number @@ -101,69 +37,3 @@ class HIDHelper: delay(5) return self - - def clear_all(self): - for idx, _ in enumerate(self.report_keys): - self.report_keys[idx] = 0x00 - - return self - - def clear_non_modifiers(self): - for idx, _ in enumerate(self.report_non_mods): - self.report_non_mods[idx] = 0x00 - - return self - - def add_modifier(self, modifier): - if isinstance(modifier, ModifierKeycode): - self.report_mods[0] |= modifier.code - else: - self.report_mods[0] |= modifier - - return self - - def remove_modifier(self, modifier): - if isinstance(modifier, ModifierKeycode): - self.report_mods[0] ^= modifier.code - else: - self.report_mods[0] ^= modifier - - return self - - def add_key(self, key): - # Try to find the first empty slot in the key report, and fill it - placed = False - - where_to_place = self.report_non_mods - - if self.report_device[0] == HIDReportTypes.CONSUMER: - where_to_place = self.report_keys - - for idx, _ in enumerate(where_to_place): - if where_to_place[idx] == 0x00: - where_to_place[idx] = key.code - placed = True - break - - if not placed: - self.logger.warning('Out of space in HID report, could not add key') - - return self - - def remove_key(self, key): - removed = False - - where_to_place = self.report_non_mods - - if self.report_device[0] == HIDReportTypes.CONSUMER: - where_to_place = self.report_keys - - for idx, _ in enumerate(where_to_place): - if where_to_place[idx] == key.code: - where_to_place[idx] = 0x00 - removed = True - - if not removed: - self.logger.warning('Tried to remove key that was not added') - - return self diff --git a/user_keymaps/klardotsh/feather_m4_express/fourfour.py b/user_keymaps/klardotsh/feather_m4_express/fourfour.py index 661a69d..5a31835 100644 --- a/user_keymaps/klardotsh/feather_m4_express/fourfour.py +++ b/user_keymaps/klardotsh/feather_m4_express/fourfour.py @@ -49,7 +49,7 @@ ANGRY_TABLE_FLIP = unicode_sequence([ keymap = [ [ [KC.GESC, KC.A, KC.RESET], - [KC.MO(1), KC.B, KC.C], + [KC.MO(1), KC.B, KC.MUTE], [KC.LT(2, KC.EXCLAIM), KC.HASH, KC.ENTER], [KC.TT(3), KC.SPACE, KC.LSHIFT], ],