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

View File

@ -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)

View File

@ -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())

View File

@ -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,12 +34,52 @@ 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 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:
if key.code >= FIRST_KMK_INTERNAL_KEYCODE:
continue
@ -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:

View File

@ -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

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