Support KEYBOARD and CONSUMER modes of HID on Feather M4 Express

This commit is contained in:
Josh Klar 2018-10-06 05:59:03 -07:00
parent 0b11f42cc2
commit 472b08d77b
No known key found for this signature in database
GPG Key ID: 220F99BD7DB7A99E
7 changed files with 240 additions and 151 deletions

39
kmk/circuitpython/hid.py Normal file
View File

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

155
kmk/common/abstract/hid.py Normal file
View File

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

View File

@ -5,10 +5,33 @@ class HIDReportTypes:
SYSCONTROL = 4 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([ HID_REPORT_STRUCTURE = bytes([
# Regular keyboard # Regular keyboard
0x05, 0x01, # Usage Page (Generic Desktop) 0x05, HIDUsagePage.KEYBOARD, # Usage Page (Generic Desktop)
0x09, 0x06, # Usage (Keyboard) 0x09, HIDUsage.KEYBOARD, # Usage (Keyboard)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x85, HIDReportTypes.KEYBOARD, # Report ID (1) 0x85, HIDReportTypes.KEYBOARD, # Report ID (1)
0x05, 0x07, # Usage Page (Keyboard) 0x05, 0x07, # Usage Page (Keyboard)
@ -39,8 +62,8 @@ HID_REPORT_STRUCTURE = bytes([
0x91, 0x01, # Output (Constant) 0x91, 0x01, # Output (Constant)
0xC0, # End Collection 0xC0, # End Collection
# Regular mouse # Regular mouse
0x05, 0x01, # Usage Page (Generic Desktop) 0x05, HIDUsagePage.MOUSE, # Usage Page (Generic Desktop)
0x09, 0x02, # Usage (Mouse) 0x09, HIDUsage.MOUSE, # Usage (Mouse)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x09, 0x01, # Usage (Pointer) 0x09, 0x01, # Usage (Pointer)
0xA1, 0x00, # Collection (Physical) 0xA1, 0x00, # Collection (Physical)
@ -73,8 +96,8 @@ HID_REPORT_STRUCTURE = bytes([
0xC0, # End Collection 0xC0, # End Collection
0xC0, # End Collection 0xC0, # End Collection
# Consumer ("multimedia") keys # Consumer ("multimedia") keys
0x05, 0x0C, # Usage Page (Consumer) 0x05, HIDUsagePage.CONSUMER, # Usage Page (Consumer)
0x09, 0x01, # Usage (Consumer Control) 0x09, HIDUsage.CONSUMER, # Usage (Consumer Control)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x85, HIDReportTypes.CONSUMER, # Report ID (n) 0x85, HIDReportTypes.CONSUMER, # Report ID (n)
0x75, 0x10, # Report Size (16) 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) 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection 0xC0, # End Collection
# Power controls # Power controls
0x05, 0x01, # Usage Page (Generic Desktop Ctrls) 0x05, HIDUsagePage.SYSCONTROL, # Usage Page (Generic Desktop Ctrls)
0x09, 0x80, # Usage (Sys Control) 0x09, HIDUsage.SYSCONTROL, # Usage (Sys Control)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x85, HIDReportTypes.SYSCONTROL, # Report ID (n) 0x85, HIDReportTypes.SYSCONTROL, # Report ID (n)
0x75, 0x02, # Report Size (2) 0x75, 0x02, # Report Size (2)

View File

@ -52,7 +52,7 @@ class Store:
cb(self.state, action) cb(self.state, action)
except Exception as e: except Exception as e:
self.logger.error('Callback failed, moving on') self.logger.error('Callback failed, moving on')
print(sys.print_exception(e), file=sys.stderr) sys.print_exception(e)
def get_state(self): def get_state(self):
return self.state return self.state

View File

@ -1,6 +1,7 @@
import sys import sys
from logging import DEBUG from logging import DEBUG
from kmk.circuitpython.hid import HIDHelper
from kmk.circuitpython.matrix import MatrixScanner from kmk.circuitpython.matrix import MatrixScanner
from kmk.common.consts import UnicodeModes from kmk.common.consts import UnicodeModes
from kmk.firmware import Firmware from kmk.firmware import Firmware
@ -23,6 +24,7 @@ def main():
unicode_mode=unicode_mode, unicode_mode=unicode_mode,
log_level=DEBUG, log_level=DEBUG,
matrix_scanner=MatrixScanner, matrix_scanner=MatrixScanner,
hid=HIDHelper,
) )
firmware.go() firmware.go()

View File

@ -1,11 +1,7 @@
import logging
from pyb import USB_HID, delay, hid_keyboard from pyb import USB_HID, delay, hid_keyboard
from kmk.common.consts import HID_REPORT_STRUCTURE, HIDReportTypes from kmk.common.abstract.hid import AbstractHidHelper
from kmk.common.event_defs import HID_REPORT_EVENT from kmk.common.consts import HID_REPORT_STRUCTURE
from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode,
ModifierKeycode)
def generate_pyb_hid_descriptor(): def generate_pyb_hid_descriptor():
@ -14,80 +10,20 @@ def generate_pyb_hid_descriptor():
return tuple(existing_keyboard) return tuple(existing_keyboard)
class HIDHelper: class HIDHelper(AbstractHidHelper):
def __init__(self, store, log_level=logging.NOTSET): # For some bizarre reason this can no longer be 8, it'll just fail to send
self.logger = logging.getLogger(__name__) # anything. This is almost certainly a bug in the report descriptor sent
self.logger.setLevel(log_level) # 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.store = store REPORT_BYTES = 7
self.store.subscribe(
lambda state, action: self._subscription(state, action),
)
def post_init(self):
self._hid = USB_HID() self._hid = USB_HID()
self.hid_send = self._hid.send
# 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()
def send(self): def send(self):
self.logger.debug('Sending HID report: {}'.format(self._evt)) 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 # Without this delay, events get clobbered and you'll likely end up with
# a string like `heloooooooooooooooo` rather than `hello`. This number # a string like `heloooooooooooooooo` rather than `hello`. This number
@ -101,69 +37,3 @@ class HIDHelper:
delay(5) delay(5)
return self 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

View File

@ -49,7 +49,7 @@ ANGRY_TABLE_FLIP = unicode_sequence([
keymap = [ keymap = [
[ [
[KC.GESC, KC.A, KC.RESET], [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.LT(2, KC.EXCLAIM), KC.HASH, KC.ENTER],
[KC.TT(3), KC.SPACE, KC.LSHIFT], [KC.TT(3), KC.SPACE, KC.LSHIFT],
], ],