diff --git a/kmk/common/consts.py b/kmk/common/consts.py index e631d2d..61d4d1c 100644 --- a/kmk/common/consts.py +++ b/kmk/common/consts.py @@ -1,3 +1,109 @@ +class HIDReportTypes: + KEYBOARD = 1 + MOUSE = 2 + CONSUMER = 3 + SYSCONTROL = 4 + + +HID_REPORT_STRUCTURE = bytes([ + # Regular keyboard + 0x05, 0x01, # Usage Page (Generic Desktop) + 0x09, 0x06, # Usage (Keyboard) + 0xA1, 0x01, # Collection (Application) + 0x85, HIDReportTypes.KEYBOARD, # Report ID (1) + 0x05, 0x07, # Usage Page (Keyboard) + 0x19, 224, # Usage Minimum (224) + 0x29, 231, # Usage Maximum (231) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x75, 0x01, # Report Size (1) + 0x95, 0x08, # Report Count (8) + 0x81, 0x02, # Input (Data, Variable, Absolute) + 0x81, 0x01, # Input (Constant) + 0x19, 0x00, # Usage Minimum (0) + 0x29, 101, # Usage Maximum (101) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 101, # Logical Maximum (101) + 0x75, 0x08, # Report Size (8) + 0x95, 0x06, # Report Count (6) + 0x81, 0x00, # Input (Data, Array) + 0x05, 0x08, # Usage Page (LED) + 0x19, 0x01, # Usage Minimum (1) + 0x29, 0x05, # Usage Maximum (5) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x75, 0x01, # Report Size (1) + 0x95, 0x05, # Report Count (5) + 0x91, 0x02, # Output (Data, Variable, Absolute) + 0x95, 0x03, # Report Count (3) + 0x91, 0x01, # Output (Constant) + 0xC0, # End Collection + # Regular mouse + 0x05, 0x01, # Usage Page (Generic Desktop) + 0x09, 0x02, # Usage (Mouse) + 0xA1, 0x01, # Collection (Application) + 0x09, 0x01, # Usage (Pointer) + 0xA1, 0x00, # Collection (Physical) + 0x85, HIDReportTypes.MOUSE, # Report ID (n) + 0x05, 0x09, # Usage Page (Button) + 0x19, 0x01, # Usage Minimum (0x01) + 0x29, 0x05, # Usage Maximum (0x05) + 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 + # Consumer ("multimedia") keys + 0x05, 0x0C, # Usage Page (Consumer) + 0x09, 0x01, # Usage (Consumer Control) + 0xA1, 0x01, # Collection (Application) + 0x85, HIDReportTypes.CONSUMER, # Report ID (n) + 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 + # Power controls + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x80, # Usage (Sys Control) + 0xA1, 0x01, # Collection (Application) + 0x85, HIDReportTypes.SYSCONTROL, # Report ID (n) + 0x75, 0x02, # Report Size (2) + 0x95, 0x01, # Report Count (1) + 0x15, 0x01, # Logical Minimum (1) + 0x25, 0x03, # Logical Maximum (3) + 0x09, 0x82, # Usage (Sys Sleep) + 0x09, 0x81, # Usage (Sys Power Down) + 0x09, 0x83, # Usage (Sys Wake Up) + 0x81, 0x60, # Input (Data,Array,Abs,No Wrap,Linear,No Preferred State,Null State) + 0x75, 0x06, # Report Size (6) + 0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection +]) + + class DiodeOrientation: ''' Orientation of diodes on handwired boards. You can think of: diff --git a/kmk/common/keycodes.py b/kmk/common/keycodes.py index 41552a9..d0b0b36 100644 --- a/kmk/common/keycodes.py +++ b/kmk/common/keycodes.py @@ -24,6 +24,11 @@ class ModifierKeycode: self.code = code +class ConsumerKeycode: + def __init__(self, code): + self.code = code + + class KeycodeCategory(type): @classmethod def to_dict(cls): @@ -121,6 +126,9 @@ CODE_RGUI = CODE_RCMD = CODE_RWIN = 0x80 class Keycodes(KeycodeCategory): ''' A massive grouping of keycodes + + Some of these are from http://www.freebsddiary.org/APC/usb_hid_usages.php, + one of the most useful pages on the interwebs for HID stuff, apparently. ''' class Modifiers(KeycodeCategory): KC_LCTRL = KC_LCTL = ModifierKeycode(CODE_LCTRL) @@ -280,60 +288,64 @@ class Keycodes(KeycodeCategory): KC_LANG9 = Keycode(152) class Misc(KeycodeCategory): - KC_APPLICATION = KC_APP = Keycode(101) - KC_POWER = Keycode(102) - KC_EXECUTE = KC_EXEC = Keycode(116) - KC_SYSTEM_POWER = KC_PWR = Keycode(165) - KC_SYSTEM_SLEEP = KC_SLEP = Keycode(166) - KC_SYSTEM_WAKE = KC_WAKE = Keycode(167) - KC_HELP = Keycode(117) - KC_MENU = Keycode(118) - KC_SELECT = KC_SLCT = Keycode(119) - KC_STOP = Keycode(120) - KC_AGAIN = KC_AGIN = Keycode(121) - KC_UNDO = Keycode(122) - KC_CUT = Keycode(123) - KC_COPY = Keycode(124) - KC_PASTE = KC_PSTE = Keycode(125) - KC_FIND = Keycode(126) - KC_ALT_ERASE = KC_ERAS = Keycode(153) - KC_SYSREQ = Keycode(154) - KC_CANCEL = Keycode(155) - KC_CLEAR = KC_CLR = Keycode(156) - KC_PRIOR = Keycode(157) - KC_RETURN = Keycode(158) - KC_SEPERATOR = Keycode(159) - KC_OUT = Keycode(160) - KC_OPER = Keycode(161) - KC_CLEAR_AGAIN = Keycode(162) - KC_CRSEL = Keycode(163) - KC_EXSEL = Keycode(164) - KC_MAIL = Keycode(177) - KC_CALCULATOR = KC_CALC = Keycode(178) - KC_MY_COMPUTER = KC_MYCM = Keycode(179) - KC_WWW_SEARCH = KC_WSCH = Keycode(180) - KC_WWW_HOME = KC_WHOM = Keycode(181) - KC_WWW_BACK = KC_WBAK = Keycode(182) - KC_WWW_FORWARD = KC_WFWD = Keycode(183) - KC_WWW_STOP = KC_WSTP = Keycode(184) - KC_WWW_REFRESH = KC_WREF = Keycode(185) - KC_WWW_FAVORITES = KC_WFAV = Keycode(186) + KC_APPLICATION = KC_APP = ConsumerKeycode(101) + KC_POWER = ConsumerKeycode(102) + KC_EXECUTE = KC_EXEC = ConsumerKeycode(116) + KC_SYSTEM_POWER = KC_PWR = ConsumerKeycode(165) + KC_SYSTEM_SLEEP = KC_SLEP = ConsumerKeycode(166) + KC_SYSTEM_WAKE = KC_WAKE = ConsumerKeycode(167) + KC_HELP = ConsumerKeycode(117) + KC_MENU = ConsumerKeycode(118) + KC_SELECT = KC_SLCT = ConsumerKeycode(119) + KC_STOP = ConsumerKeycode(120) + KC_AGAIN = KC_AGIN = ConsumerKeycode(121) + KC_UNDO = ConsumerKeycode(122) + KC_CUT = ConsumerKeycode(123) + KC_COPY = ConsumerKeycode(124) + KC_PASTE = KC_PSTE = ConsumerKeycode(125) + KC_FIND = ConsumerKeycode(126) + KC_ALT_ERASE = KC_ERAS = ConsumerKeycode(153) + KC_SYSREQ = ConsumerKeycode(154) + KC_CANCEL = ConsumerKeycode(155) + KC_CLEAR = KC_CLR = ConsumerKeycode(156) + KC_PRIOR = ConsumerKeycode(157) + KC_RETURN = ConsumerKeycode(158) + KC_SEPERATOR = ConsumerKeycode(159) + KC_OUT = ConsumerKeycode(160) + KC_OPER = ConsumerKeycode(161) + KC_CLEAR_AGAIN = ConsumerKeycode(162) + KC_CRSEL = ConsumerKeycode(163) + KC_EXSEL = ConsumerKeycode(164) + KC_MAIL = ConsumerKeycode(177) + KC_CALCULATOR = KC_CALC = ConsumerKeycode(178) + KC_MY_COMPUTER = KC_MYCM = ConsumerKeycode(179) + KC_WWW_SEARCH = KC_WSCH = ConsumerKeycode(180) + KC_WWW_HOME = KC_WHOM = ConsumerKeycode(181) + KC_WWW_BACK = KC_WBAK = ConsumerKeycode(182) + KC_WWW_FORWARD = KC_WFWD = ConsumerKeycode(183) + KC_WWW_STOP = KC_WSTP = ConsumerKeycode(184) + KC_WWW_REFRESH = KC_WREF = ConsumerKeycode(185) + KC_WWW_FAVORITES = KC_WFAV = ConsumerKeycode(186) class Media(KeycodeCategory): - KC__MUTE = Keycode(127) - KC__VOLUP = Keycode(128) - KC__VOLDOWN = Keycode(129) - KC_AUDIO_MUTE = KC_MUTE = Keycode(168) - KC_AUDIO_VOL_UP = KC_VOLU = Keycode(169) - KC_AUDIO_VOL_DOWN = KC_VOLD = Keycode(170) - KC_MEDIA_NEXT_TRACK = KC_MNXT = Keycode(171) - KC_MEDIA_PREV_TRACK = KC_MPRV = Keycode(172) - KC_MEDIA_STOP = KC_MSTP = Keycode(173) - KC_MEDIA_PLAY_PAUSE = KC_MPLY = Keycode(174) - KC_MEDIA_SELECT = KC_MSEL = Keycode(175) - KC_MEDIA_EJECT = KC_EJCT = Keycode(176) - KC_MEDIA_FAST_FORWARD = KC_MFFD = Keycode(187) - KC_MEDIA_REWIND = KC_MRWD = Keycode(189) + # I believe QMK used these double-underscore codes for MacOS + # support or something. I have no idea, but modern MacOS supports + # PC volume keys so I really don't care that these codes are the + # same as below. If bugs arise, these codes may need to change. + KC__MUTE = ConsumerKeycode(226) + KC__VOLUP = ConsumerKeycode(233) + KC__VOLDOWN = ConsumerKeycode(234) + + KC_AUDIO_MUTE = KC_MUTE = ConsumerKeycode(226) # 0xE2 + KC_AUDIO_VOL_UP = KC_VOLU = ConsumerKeycode(233) # 0xE9 + KC_AUDIO_VOL_DOWN = KC_VOLD = ConsumerKeycode(234) # 0xEA + KC_MEDIA_NEXT_TRACK = KC_MNXT = ConsumerKeycode(181) # 0xB5 + KC_MEDIA_PREV_TRACK = KC_MPRV = ConsumerKeycode(182) # 0xB6 + KC_MEDIA_STOP = KC_MSTP = ConsumerKeycode(183) # 0xB7 + KC_MEDIA_PLAY_PAUSE = KC_MPLY = ConsumerKeycode(205) # 0xCD (this may not be right) + KC_MEDIA_EJECT = KC_EJCT = ConsumerKeycode(184) # 0xB8 + KC_MEDIA_FAST_FORWARD = KC_MFFD = ConsumerKeycode(179) # 0xB3 + KC_MEDIA_REWIND = KC_MRWD = ConsumerKeycode(180) # 0xB4 class KMK(KeycodeCategory): KC_RESET = Keycode(1000) diff --git a/kmk/entrypoints/handwire/pyboard_boot.py b/kmk/entrypoints/handwire/pyboard_boot.py index c537197..2472478 100644 --- a/kmk/entrypoints/handwire/pyboard_boot.py +++ b/kmk/entrypoints/handwire/pyboard_boot.py @@ -1,3 +1,6 @@ import pyb -pyb.usb_mode('VCP+HID', hid=pyb.hid_keyboard) # act as a serial device and a mouse +from kmk.micropython.pyb_hid import generate_pyb_hid_descriptor + +# act as a serial device and a KMK device +pyb.usb_mode('VCP+HID', hid=generate_pyb_hid_descriptor()) diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py index 0b4d90e..a4a9526 100644 --- a/kmk/micropython/pyb_hid.py +++ b/kmk/micropython/pyb_hid.py @@ -1,34 +1,22 @@ import logging import string -from pyb import USB_HID, delay +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, Keycodes, - ModifierKeycode, char_lookup) +from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode, + Keycodes, ModifierKeycode, char_lookup) + + +def generate_pyb_hid_descriptor(): + existing_keyboard = list(hid_keyboard) + existing_keyboard[-1] = HID_REPORT_STRUCTURE + return tuple(existing_keyboard) class HIDHelper: ''' - Wraps a HID reporting event. The structure of such events is (courtesy of - http://wiki.micropython.org/USB-HID-Keyboard-mode-example-a-password-dongle): - - >Byte 0 is for a modifier key, or combination thereof. It is used as a - >bitmap, each bit mapped to a modifier: - > bit 0: left control - > bit 1: left shift - > bit 2: left alt - > bit 3: left GUI (Win/Apple/Meta key) - > bit 4: right control - > bit 5: right shift - > bit 6: right alt - > bit 7: right GUI - > - > Examples: 0x02 for Shift, 0x05 for Control+Alt - > - >Byte 1 is "reserved" (unused, actually) - >Bytes 2-7 are for the actual key scancode(s) - up to 6 at a time ("chording"). - Most methods here return `self` upon completion, allowing chaining: ```python @@ -46,24 +34,64 @@ class HIDHelper: ) self._hid = USB_HID() - self.clear_all() + + # 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 key.code >= FIRST_KMK_INTERNAL_KEYCODE: - continue + if isinstance(key, ConsumerKeycode): + consumer_key = key + break - if isinstance(key, ModifierKeycode): - self.add_modifier(key) - else: - self.add_key(key) + reporting_device = self.report_device[0] + needed_reporting_device = HIDReportTypes.KEYBOARD - if key.has_modifiers: - for mod in key.has_modifiers: - self.add_modifier(mod) + 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() + delay(10) + + 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() @@ -114,37 +142,45 @@ class HIDHelper: return self def clear_all(self): - self._evt = bytearray(8) + for idx, _ in enumerate(self.report_keys): + self.report_keys[idx] = 0x00 + return self def clear_non_modifiers(self): - for pos in range(2, 8): - self._evt[pos] = 0x00 + 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._evt[0] |= modifier.code + self.report_mods[0] |= modifier.code else: - self._evt[0] |= modifier + self.report_mods[0] |= modifier return self def remove_modifier(self, modifier): if isinstance(modifier, ModifierKeycode): - self._evt[0] ^= modifier.code + self.report_mods[0] ^= modifier.code else: - self._evt[0] ^= modifier + 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 - for pos in range(2, 8): - if self._evt[pos] == 0x00: - self._evt[pos] = key.code + + 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 @@ -155,9 +191,15 @@ class HIDHelper: def remove_key(self, key): removed = False - for pos in range(2, 8): - if self._evt[pos] == key.code: - self._evt[pos] = 0x00 + + 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: diff --git a/setup.cfg b/setup.cfg index 48a6b8c..d518cba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [flake8] exclude = .git,__pycache__,vendor,.venv max_line_length = 99 -ignore = X100 +ignore = X100, E262 per-file-ignores = user_keymaps/**/*.py: F401,E501 tests/test_data/keymaps/**/*.py: F401,E501 diff --git a/user_keymaps/klardotsh/threethree_matrix_pyboard.py b/user_keymaps/klardotsh/threethree_matrix_pyboard.py index 9fe47a3..324b10c 100644 --- a/user_keymaps/klardotsh/threethree_matrix_pyboard.py +++ b/user_keymaps/klardotsh/threethree_matrix_pyboard.py @@ -22,8 +22,8 @@ keymap = [ [KC.F, KC.G, KC.H], ], [ - [KC.X, KC.Y, KC.Z], - [KC.TRNS, KC.PIPE, KC.O], - [KC.R, KC.P, KC.Q], + [KC.VOLU, KC.MUTE, KC.Z], + [KC.TRNS, KC.PIPE, KC.MEDIA_PLAY_PAUSE], + [KC.VOLD, KC.P, KC.Q], ], ]