Merge pull request #30 from KMKfw/topic-consumer-device

HID: Support Consumer (media) keys
This commit is contained in:
Josh Klar 2018-09-30 16:37:58 -07:00 committed by GitHub
commit 85cdf03572
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 279 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,12 +34,52 @@ 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:
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()
delay(10)
self.report_device[0] = needed_reporting_device
if consumer_key:
self.add_key(consumer_key)
else:
for key in state.keys_pressed: for key in state.keys_pressed:
if key.code >= FIRST_KMK_INTERNAL_KEYCODE: if key.code >= FIRST_KMK_INTERNAL_KEYCODE:
continue continue
@ -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

@ -0,0 +1,14 @@
try:
from collections import namedtuple
except ImportError:
from ucollections import namedtuple
HIDMode = namedtuple('HIDMode', (
'subclass',
'protocol',
'max_packet_length',
'polling_interval',
'report_descriptor',
))
hid_keyboard = HIDMode(0, 0, 0, 0, bytearray(0))

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],
], ],
] ]