Implement basic HID keyboard support (send_string works great!)

This commit is contained in:
Josh Klar 2018-09-16 20:49:14 -07:00
parent 27f1e971b0
commit 7f88f4f415
4 changed files with 175 additions and 16 deletions

View File

@ -1,3 +1,3 @@
import pyb
pyb.usb_mode('VCP+HID') # act as a serial device and a mouse
pyb.usb_mode('VCP+HID', hid=pyb.hid_keyboard) # act as a serial device and a mouse

View File

@ -10,13 +10,31 @@ class DiodeOrientation:
class KeycodeCategory(type):
def __contains__(cls, kc):
@classmethod
def to_dict(cls):
'''
Enables the 'in' operator for keycode groupings. Not super useful in
most cases, but does allow for sanity checks like
MicroPython, for whatever reason (probably performance/memory) makes
__dict__ optional for ports. Unfortunately, at least the STM32
(Pyboard) port is one such port. This reimplements a subset of
__dict__, limited to just keys we're likely to care about (though this
could be opened up further later).
'''
return {
key: getattr(cls, key)
for key in dir(cls)
if not key.startswith('_')
}
@classmethod
def contains(cls, kc):
'''
Emulates the 'in' operator for keycode groupings, given MicroPython's
lack of support for metaclasses (meaning implementing 'in' for
uninstantiated classes, such as these, is largely not possible). Not
super useful in most cases, but does allow for sanity checks like
```python
assert requested_key in Keycodes.Modifiers
assert Keycodes.Modifiers.contains(requested_key)
```
This is not bulletproof due to how HID codes are defined (there is
@ -27,7 +45,7 @@ class KeycodeCategory(type):
This is recursive across subgroups, enabling stuff like:
```python
assert requested_key in Keycodes
assert Keycodes.contains(requested_key)
```
To ensure that a valid keycode has been requested to begin with. Again,
@ -35,25 +53,32 @@ class KeycodeCategory(type):
otherwise cause AttributeErrors and crash the keyboard.
'''
subcategories = (
category for category in cls.__dict__.values()
if isinstance(category, KeycodeCategory)
category for category in cls.to_dict().values()
# Disgusting, but since `cls.__bases__` isn't implemented in MicroPython,
# I resort to a less foolproof inheritance check that should still ignore
# strings and other stupid stuff (we don't want to iterate over __doc__,
# for example), but include nested classes.
#
# One huge lesson in this project is that uninstantiated classes are hard...
# and four times harder when the implementation of Python is half-baked.
if isinstance(category, type)
)
if any(
kc == _kc
for name, _kc in cls.__dict__.items()
for name, _kc in cls.to_dict().items()
if name.startswith('KC_')
):
return True
return any(kc in sc for sc in subcategories)
return any(sc.contains(kc) for sc in subcategories)
class Keycodes(metaclass=KeycodeCategory):
class Keycodes(KeycodeCategory):
'''
A massive grouping of keycodes
'''
class Modifiers(metaclass=KeycodeCategory):
class Modifiers(KeycodeCategory):
KC_CTRL = KC_LEFT_CTRL = 0x01
KC_SHIFT = KC_LEFT_SHIFT = 0x02
KC_ALT = KC_LALT = 0x04
@ -63,7 +88,7 @@ class Keycodes(metaclass=KeycodeCategory):
KC_RALT = 0x40
KC_RGUI = 0x80
class Common(metaclass=KeycodeCategory):
class Common(KeycodeCategory):
KC_A = 4
KC_B = 5
KC_C = 6
@ -120,7 +145,7 @@ class Keycodes(metaclass=KeycodeCategory):
KC_SLASH = 56
KC_CAPS_LOCK = 57
class FunctionKeys(metaclass=KeycodeCategory):
class FunctionKeys(KeycodeCategory):
KC_F1 = 58
KC_F2 = 59
KC_F3 = 60
@ -134,7 +159,7 @@ class Keycodes(metaclass=KeycodeCategory):
KC_F11 = 68
KC_F12 = 69
class NavAndLocks(metaclass=KeycodeCategory):
class NavAndLocks(KeycodeCategory):
KC_PRINTSCREEN = 70
KC_SCROLL_LOCK = 71
KC_PAUSE = 72
@ -149,7 +174,7 @@ class Keycodes(metaclass=KeycodeCategory):
KC_DOWN = 81
KC_UP = 82
class Numpad(metaclass=KeycodeCategory):
class Numpad(KeycodeCategory):
KC_NUMLOCK = 83
KC_KP_SLASH = 84
KC_KP_ASTERIX = 85
@ -167,3 +192,14 @@ class Keycodes(metaclass=KeycodeCategory):
KC_KP_9 = 97
KC_KP_0 = 98
KC_KP_PERIOD = 99
char_lookup = {
"\n": (Keycodes.Common.KC_ENTER,),
"\t": (Keycodes.Common.KC_TAB,),
' ': (Keycodes.Common.KC_SPACE,),
'-': (Keycodes.Common.KC_MINUS,),
'=': (Keycodes.Common.KC_EQUAL,),
'+': (Keycodes.Common.KC_EQUAL, Keycodes.Modifiers.KC_SHIFT),
'~': (Keycodes.Common.KC_TILDE,),
}

122
kmk/micropython/pyb_hid.py Normal file
View File

@ -0,0 +1,122 @@
import logging
import string
from pyb import USB_HID, delay
from kmk.common.consts import Keycodes, char_lookup
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
myhid = HIDHelper()
myhid.send_string('testing').send_string(' ... and testing again')
```
'''
def __init__(self, log_level=logging.NOTSET):
self.logger = logging.getLogger(__name__)
self.logger.setLevel(log_level)
self._hid = USB_HID()
self.clear_all()
def send(self):
self.logger.debug('Sending HID report: {}'.format(self._evt))
self._hid.send(self._evt)
return self
def send_string(self, message):
'''
Clears the HID report, and sends along a string of arbitrary length.
All keys will be released at the completion of the string. Modifiers
are not really supported here, though Shift will be pressed if
necessary to output the key.
'''
self.clear_all()
self.send()
for char in message:
kc = None
modifier = None
if char in char_lookup:
kc, modifier = char_lookup[char]
elif char in string.ascii_letters + string.digits:
kc = getattr(Keycodes.Common, 'KC_{}'.format(char.upper()))
modifier = Keycodes.Modifiers.KC_SHIFT if char.isupper() else None
if modifier:
self.enable_modifier(modifier)
self.add_key(kc)
self.send()
# Without this delay, events get clobbered and you'll likely end up with
# a string like `heloooooooooooooooo` rather than `hello`. This number
# may be able to be shrunken down. It may also make sense to use
# time.sleep_us or time.sleep_ms or time.sleep (platform dependent)
# on non-Pyboards.
delay(10)
# Release all keys or we'll forever hold whatever the last keypress was
self.clear_all()
self.send()
return self
def clear_all(self):
self._evt = bytearray(8)
return self
def clear_non_modifiers(self):
for pos in range(2, 8):
self._evt[pos] = 0x00
return self
def enable_modifier(self, modifier):
if Keycodes.Modifiers.contains(modifier):
self._evt[0] |= modifier
return self
raise ValueError('Attempted to use non-modifier as a modifier')
def add_key(self, key):
if key and Keycodes.contains(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
placed = True
break
if not placed:
raise ValueError('Out of space in HID report, could not add key')
return self
raise ValueError('Invalid keycode?')

View File

@ -1 +1,2 @@
vendor/upy-lib/logging/logging.py
vendor/upy-lib/string/string.py