HID: Support Consumer (media) keys

What a short title for such a massive diff.

This (heavily squashed) commit adds support for Consumer keys such as
volume keys, media play/pause/stop, etc. by exposing four HID devices
over a single USB lane (as opposed to just exposing a keyboard). This
heavily refactors how HIDHelper works due to the new reporting
structure.

Many of the media keys were changed (mostly Keycodes.Media section), but
many (especially anything regarding Application keys) haven't been
touched yet - thus many keycodes may still be wrong. Probably worth
updating those soon, but I didn't get around to it yet. The definitive
list I refered to was
http://www.freebsddiary.org/APC/usb_hid_usages.php, which is basically
copy-pasta from the official USB HID spec at
https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf
(warning: massive PDF, not light reading).

The only known regression this introduces is that instead of 6KRO as the
USB spec usually supports, we can now only have 5KRO (maybe even 4KRO),
for reasons I have yet to fully debug - this seems to be related to the
report having to include the device descriptor _and_ not supporting a
full 8 bytes as it used to. For now I'm willing to accept this, but it
definitely will be great to squash that bug.

This adds descriptor support for MOUSE and SYSCONTROL devices, as of yet
unimplemented.
This commit is contained in:
Josh Klar 2018-09-29 15:46:17 -07:00
parent a1bfaac691
commit b7b1866ac9
No known key found for this signature in database
GPG Key ID: 220F99BD7DB7A99E
6 changed files with 265 additions and 102 deletions

View File

@ -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: class DiodeOrientation:
''' '''
Orientation of diodes on handwired boards. You can think of: Orientation of diodes on handwired boards. You can think of:

View File

@ -24,6 +24,11 @@ class ModifierKeycode:
self.code = code self.code = code
class ConsumerKeycode:
def __init__(self, code):
self.code = code
class KeycodeCategory(type): class KeycodeCategory(type):
@classmethod @classmethod
def to_dict(cls): def to_dict(cls):
@ -121,6 +126,9 @@ CODE_RGUI = CODE_RCMD = CODE_RWIN = 0x80
class Keycodes(KeycodeCategory): class Keycodes(KeycodeCategory):
''' '''
A massive grouping of keycodes 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): class Modifiers(KeycodeCategory):
KC_LCTRL = KC_LCTL = ModifierKeycode(CODE_LCTRL) KC_LCTRL = KC_LCTL = ModifierKeycode(CODE_LCTRL)
@ -280,60 +288,64 @@ class Keycodes(KeycodeCategory):
KC_LANG9 = Keycode(152) KC_LANG9 = Keycode(152)
class Misc(KeycodeCategory): class Misc(KeycodeCategory):
KC_APPLICATION = KC_APP = Keycode(101) KC_APPLICATION = KC_APP = ConsumerKeycode(101)
KC_POWER = Keycode(102) KC_POWER = ConsumerKeycode(102)
KC_EXECUTE = KC_EXEC = Keycode(116) KC_EXECUTE = KC_EXEC = ConsumerKeycode(116)
KC_SYSTEM_POWER = KC_PWR = Keycode(165) KC_SYSTEM_POWER = KC_PWR = ConsumerKeycode(165)
KC_SYSTEM_SLEEP = KC_SLEP = Keycode(166) KC_SYSTEM_SLEEP = KC_SLEP = ConsumerKeycode(166)
KC_SYSTEM_WAKE = KC_WAKE = Keycode(167) KC_SYSTEM_WAKE = KC_WAKE = ConsumerKeycode(167)
KC_HELP = Keycode(117) KC_HELP = ConsumerKeycode(117)
KC_MENU = Keycode(118) KC_MENU = ConsumerKeycode(118)
KC_SELECT = KC_SLCT = Keycode(119) KC_SELECT = KC_SLCT = ConsumerKeycode(119)
KC_STOP = Keycode(120) KC_STOP = ConsumerKeycode(120)
KC_AGAIN = KC_AGIN = Keycode(121) KC_AGAIN = KC_AGIN = ConsumerKeycode(121)
KC_UNDO = Keycode(122) KC_UNDO = ConsumerKeycode(122)
KC_CUT = Keycode(123) KC_CUT = ConsumerKeycode(123)
KC_COPY = Keycode(124) KC_COPY = ConsumerKeycode(124)
KC_PASTE = KC_PSTE = Keycode(125) KC_PASTE = KC_PSTE = ConsumerKeycode(125)
KC_FIND = Keycode(126) KC_FIND = ConsumerKeycode(126)
KC_ALT_ERASE = KC_ERAS = Keycode(153) KC_ALT_ERASE = KC_ERAS = ConsumerKeycode(153)
KC_SYSREQ = Keycode(154) KC_SYSREQ = ConsumerKeycode(154)
KC_CANCEL = Keycode(155) KC_CANCEL = ConsumerKeycode(155)
KC_CLEAR = KC_CLR = Keycode(156) KC_CLEAR = KC_CLR = ConsumerKeycode(156)
KC_PRIOR = Keycode(157) KC_PRIOR = ConsumerKeycode(157)
KC_RETURN = Keycode(158) KC_RETURN = ConsumerKeycode(158)
KC_SEPERATOR = Keycode(159) KC_SEPERATOR = ConsumerKeycode(159)
KC_OUT = Keycode(160) KC_OUT = ConsumerKeycode(160)
KC_OPER = Keycode(161) KC_OPER = ConsumerKeycode(161)
KC_CLEAR_AGAIN = Keycode(162) KC_CLEAR_AGAIN = ConsumerKeycode(162)
KC_CRSEL = Keycode(163) KC_CRSEL = ConsumerKeycode(163)
KC_EXSEL = Keycode(164) KC_EXSEL = ConsumerKeycode(164)
KC_MAIL = Keycode(177) KC_MAIL = ConsumerKeycode(177)
KC_CALCULATOR = KC_CALC = Keycode(178) KC_CALCULATOR = KC_CALC = ConsumerKeycode(178)
KC_MY_COMPUTER = KC_MYCM = Keycode(179) KC_MY_COMPUTER = KC_MYCM = ConsumerKeycode(179)
KC_WWW_SEARCH = KC_WSCH = Keycode(180) KC_WWW_SEARCH = KC_WSCH = ConsumerKeycode(180)
KC_WWW_HOME = KC_WHOM = Keycode(181) KC_WWW_HOME = KC_WHOM = ConsumerKeycode(181)
KC_WWW_BACK = KC_WBAK = Keycode(182) KC_WWW_BACK = KC_WBAK = ConsumerKeycode(182)
KC_WWW_FORWARD = KC_WFWD = Keycode(183) KC_WWW_FORWARD = KC_WFWD = ConsumerKeycode(183)
KC_WWW_STOP = KC_WSTP = Keycode(184) KC_WWW_STOP = KC_WSTP = ConsumerKeycode(184)
KC_WWW_REFRESH = KC_WREF = Keycode(185) KC_WWW_REFRESH = KC_WREF = ConsumerKeycode(185)
KC_WWW_FAVORITES = KC_WFAV = Keycode(186) KC_WWW_FAVORITES = KC_WFAV = ConsumerKeycode(186)
class Media(KeycodeCategory): class Media(KeycodeCategory):
KC__MUTE = Keycode(127) # I believe QMK used these double-underscore codes for MacOS
KC__VOLUP = Keycode(128) # support or something. I have no idea, but modern MacOS supports
KC__VOLDOWN = Keycode(129) # PC volume keys so I really don't care that these codes are the
KC_AUDIO_MUTE = KC_MUTE = Keycode(168) # same as below. If bugs arise, these codes may need to change.
KC_AUDIO_VOL_UP = KC_VOLU = Keycode(169) KC__MUTE = ConsumerKeycode(226)
KC_AUDIO_VOL_DOWN = KC_VOLD = Keycode(170) KC__VOLUP = ConsumerKeycode(233)
KC_MEDIA_NEXT_TRACK = KC_MNXT = Keycode(171) KC__VOLDOWN = ConsumerKeycode(234)
KC_MEDIA_PREV_TRACK = KC_MPRV = Keycode(172)
KC_MEDIA_STOP = KC_MSTP = Keycode(173) KC_AUDIO_MUTE = KC_MUTE = ConsumerKeycode(226) # 0xE2
KC_MEDIA_PLAY_PAUSE = KC_MPLY = Keycode(174) KC_AUDIO_VOL_UP = KC_VOLU = ConsumerKeycode(233) # 0xE9
KC_MEDIA_SELECT = KC_MSEL = Keycode(175) KC_AUDIO_VOL_DOWN = KC_VOLD = ConsumerKeycode(234) # 0xEA
KC_MEDIA_EJECT = KC_EJCT = Keycode(176) KC_MEDIA_NEXT_TRACK = KC_MNXT = ConsumerKeycode(181) # 0xB5
KC_MEDIA_FAST_FORWARD = KC_MFFD = Keycode(187) KC_MEDIA_PREV_TRACK = KC_MPRV = ConsumerKeycode(182) # 0xB6
KC_MEDIA_REWIND = KC_MRWD = Keycode(189) 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): class KMK(KeycodeCategory):
KC_RESET = Keycode(1000) KC_RESET = Keycode(1000)

View File

@ -1,3 +1,6 @@
import pyb 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())

View File

@ -1,34 +1,22 @@
import logging import logging
import string 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.event_defs import HID_REPORT_EVENT
from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, Keycodes, from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode,
ModifierKeycode, char_lookup) 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: 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: Most methods here return `self` upon completion, allowing chaining:
```python ```python
@ -46,24 +34,64 @@ class HIDHelper:
) )
self._hid = USB_HID() 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): def _subscription(self, state, action):
if action['type'] == HID_REPORT_EVENT: if action['type'] == HID_REPORT_EVENT:
self.clear_all() self.clear_all()
consumer_key = None
for key in state.keys_pressed: for key in state.keys_pressed:
if key.code >= FIRST_KMK_INTERNAL_KEYCODE: if isinstance(key, ConsumerKeycode):
continue consumer_key = key
break
if isinstance(key, ModifierKeycode): reporting_device = self.report_device[0]
self.add_modifier(key) needed_reporting_device = HIDReportTypes.KEYBOARD
else:
self.add_key(key)
if key.has_modifiers: if consumer_key:
for mod in key.has_modifiers: needed_reporting_device = HIDReportTypes.CONSUMER
self.add_modifier(mod)
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() self.send()
@ -114,37 +142,45 @@ class HIDHelper:
return self return self
def clear_all(self): def clear_all(self):
self._evt = bytearray(8) for idx, _ in enumerate(self.report_keys):
self.report_keys[idx] = 0x00
return self return self
def clear_non_modifiers(self): def clear_non_modifiers(self):
for pos in range(2, 8): for idx, _ in enumerate(self.report_non_mods):
self._evt[pos] = 0x00 self.report_non_mods[idx] = 0x00
return self return self
def add_modifier(self, modifier): def add_modifier(self, modifier):
if isinstance(modifier, ModifierKeycode): if isinstance(modifier, ModifierKeycode):
self._evt[0] |= modifier.code self.report_mods[0] |= modifier.code
else: else:
self._evt[0] |= modifier self.report_mods[0] |= modifier
return self return self
def remove_modifier(self, modifier): def remove_modifier(self, modifier):
if isinstance(modifier, ModifierKeycode): if isinstance(modifier, ModifierKeycode):
self._evt[0] ^= modifier.code self.report_mods[0] ^= modifier.code
else: else:
self._evt[0] ^= modifier self.report_mods[0] ^= modifier
return self return self
def add_key(self, key): def add_key(self, key):
# Try to find the first empty slot in the key report, and fill it # Try to find the first empty slot in the key report, and fill it
placed = False placed = False
for pos in range(2, 8):
if self._evt[pos] == 0x00: where_to_place = self.report_non_mods
self._evt[pos] = key.code
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 placed = True
break break
@ -155,9 +191,15 @@ class HIDHelper:
def remove_key(self, key): def remove_key(self, key):
removed = False removed = False
for pos in range(2, 8):
if self._evt[pos] == key.code: where_to_place = self.report_non_mods
self._evt[pos] = 0x00
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 removed = True
if not removed: if not removed:

View File

@ -1,7 +1,7 @@
[flake8] [flake8]
exclude = .git,__pycache__,vendor,.venv exclude = .git,__pycache__,vendor,.venv
max_line_length = 99 max_line_length = 99
ignore = X100 ignore = X100, E262
per-file-ignores = per-file-ignores =
user_keymaps/**/*.py: F401,E501 user_keymaps/**/*.py: F401,E501
tests/test_data/keymaps/**/*.py: F401,E501 tests/test_data/keymaps/**/*.py: F401,E501

View File

@ -22,8 +22,8 @@ keymap = [
[KC.F, KC.G, KC.H], [KC.F, KC.G, KC.H],
], ],
[ [
[KC.X, KC.Y, KC.Z], [KC.VOLU, KC.MUTE, KC.Z],
[KC.TRNS, KC.PIPE, KC.O], [KC.TRNS, KC.PIPE, KC.MEDIA_PLAY_PAUSE],
[KC.R, KC.P, KC.Q], [KC.VOLD, KC.P, KC.Q],
], ],
] ]