Listen to KEY_DOWN_EVENT and KEY_UP_EVENT in the HIDHelper and actually send. Working keyboard! ⌨

This commit is contained in:
Josh Klar 2018-09-16 23:20:16 -07:00
parent 6933d9c484
commit 3e99f0c8e3
8 changed files with 308 additions and 245 deletions

View File

@ -2,8 +2,10 @@ from logging import DEBUG
import machine
from kmk.common.consts import KC, DiodeOrientation
from kmk.common.consts import DiodeOrientation
from kmk.common.keycodes import KC
from kmk.firmware import Firmware
from kmk.micropython.pyb_hid import HIDHelper
def main():
@ -25,6 +27,7 @@ def main():
row_pins=rows,
col_pins=cols,
diode_orientation=diode_orientation,
hid=HIDHelper,
log_level=DEBUG,
)

View File

@ -1,7 +1,3 @@
from kmk.common.types import AttrDict
from kmk.common.util import flatten_dict
class DiodeOrientation:
'''
Orientation of diodes on handwired boards. You can think of:
@ -11,226 +7,3 @@ class DiodeOrientation:
COLUMNS = 0
ROWS = 1
class KeycodeCategory(type):
@classmethod
def to_dict(cls):
'''
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).
'''
hidden = ('to_dict', 'recursive_dict', 'contains')
return AttrDict({
key: getattr(cls, key)
for key in dir(cls)
if not key.startswith('_') and key not in hidden
})
@classmethod
def recursive_dict(cls):
'''
to_dict() executed recursively all the way down a tree
'''
ret = cls.to_dict()
for key, val in ret.items():
try:
nested_ret = val.recursive_dict()
except (AttributeError, NameError):
continue
ret[key] = nested_ret
return ret
@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 Keycodes.Modifiers.contains(requested_key)
```
This is not bulletproof due to how HID codes are defined (there is
overlap). Keycodes.Common.KC_A, for example, is equal in value to
Keycodes.Modifiers.KC_LALT, but it can still prevent silly mistakes
like trying to use, say, Keycodes.Common.KC_Q as a modifier.
This is recursive across subgroups, enabling stuff like:
```python
assert Keycodes.contains(requested_key)
```
To ensure that a valid keycode has been requested to begin with. Again,
not bulletproof, but adds at least some cushion to stuff that would
otherwise cause AttributeErrors and crash the keyboard.
'''
subcategories = (
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.to_dict().items()
if name.startswith('KC_')
):
return True
return any(sc.contains(kc) for sc in subcategories)
class Keycodes(KeycodeCategory):
'''
A massive grouping of keycodes
'''
class Modifiers(KeycodeCategory):
KC_CTRL = KC_LEFT_CTRL = 0x01
KC_SHIFT = KC_LEFT_SHIFT = 0x02
KC_ALT = KC_LALT = 0x04
KC_GUI = KC_LGUI = 0x08
KC_RCTRL = 0x10
KC_RSHIFT = 0x20
KC_RALT = 0x40
KC_RGUI = 0x80
class Common(KeycodeCategory):
KC_A = 4
KC_B = 5
KC_C = 6
KC_D = 7
KC_E = 8
KC_F = 9
KC_G = 10
KC_H = 11
KC_I = 12
KC_J = 13
KC_K = 14
KC_L = 15
KC_M = 16
KC_N = 17
KC_O = 18
KC_P = 19
KC_Q = 20
KC_R = 21
KC_S = 22
KC_T = 23
KC_U = 24
KC_V = 25
KC_W = 26
KC_X = 27
KC_Y = 28
KC_Z = 29
# Aliases to play nicely with AttrDict, since KC.1 isn't a valid
# attribute key in Python, but KC.N1 is
KC_1 = KC_N1 = 30
KC_2 = KC_N2 = 31
KC_3 = KC_N3 = 32
KC_4 = KC_N4 = 33
KC_5 = KC_N5 = 34
KC_6 = KC_N6 = 35
KC_7 = KC_N7 = 36
KC_8 = KC_N8 = 37
KC_9 = KC_N9 = 38
KC_0 = KC_N0 = 39
KC_ENTER = 40
KC_ESC = 41
KC_BACKSPACE = 42
KC_TAB = 43
KC_SPACE = 44
KC_MINUS = 45
KC_EQUAL = 46
KC_LBRC = 47
KC_RBRC = 48
KC_BACKSLASH = 49
KC_NUMBER = 50
KC_SEMICOLON = 51
KC_QUOTE = 52
KC_TILDE = 53
KC_COMMA = 54
KC_PERIOD = 55
KC_SLASH = 56
KC_CAPS_LOCK = 57
class FunctionKeys(KeycodeCategory):
KC_F1 = 58
KC_F2 = 59
KC_F3 = 60
KC_F4 = 61
KC_F5 = 62
KC_F6 = 63
KC_F7 = 64
KC_F8 = 65
KC_F9 = 66
KC_F10 = 67
KC_F11 = 68
KC_F12 = 69
class NavAndLocks(KeycodeCategory):
KC_PRINTSCREEN = 70
KC_SCROLL_LOCK = 71
KC_PAUSE = 72
KC_INSERT = 73
KC_HOME = 74
KC_PGUP = 75
KC_DELETE = 76
KC_END = 77
KC_PGDN = 78
KC_RIGHT = 79
KC_LEFT = 80
KC_DOWN = 81
KC_UP = 82
class Numpad(KeycodeCategory):
KC_NUMLOCK = 83
KC_KP_SLASH = 84
KC_KP_ASTERIX = 85
KC_KP_MINUS = 86
KC_KP_PLUS = 87
KC_KP_ENTER = 88
KC_KP_1 = 89
KC_KP_2 = 90
KC_KP_3 = 91
KC_KP_4 = 92
KC_KP_5 = 93
KC_KP_6 = 94
KC_KP_7 = 95
KC_KP_8 = 96
KC_KP_9 = 97
KC_KP_0 = 98
KC_KP_PERIOD = 99
ALL_KEYS = KC = AttrDict({
k.replace('KC_', ''): v
for k, v in flatten_dict(Keycodes.recursive_dict()).items()
})
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,),
}

View File

@ -19,7 +19,7 @@ class ReduxStore:
self.state = self.reducer(self.state, action)
self.logger.debug('Dispatching complete: {}'.format(action))
self.logger.debug('Calling subscriptions')
self.logger.debug('New state: {}'.format(self.state))
for cb in self.callbacks:
if cb is not None:
@ -29,8 +29,6 @@ class ReduxStore:
self.logger.error('Callback failed, moving on')
print(sys.print_exception(e), file=sys.stderr)
self.logger.debug('Callbacks complete')
def get_state(self):
return self.state

234
kmk/common/keycodes.py Normal file
View File

@ -0,0 +1,234 @@
try:
from collections import namedtuple
except ImportError:
# This is handled by micropython-lib/collections, but on local runs of
# MicroPython, it doesn't exist
from ucollections import namedtuple
from kmk.common.types import AttrDict
from kmk.common.util import flatten_dict
Keycode = namedtuple('Keycode', ('code', 'is_modifier'))
class KeycodeCategory(type):
@classmethod
def to_dict(cls):
'''
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).
'''
hidden = ('to_dict', 'recursive_dict', 'contains')
return AttrDict({
key: getattr(cls, key)
for key in dir(cls)
if not key.startswith('_') and key not in hidden
})
@classmethod
def recursive_dict(cls):
'''
to_dict() executed recursively all the way down a tree
'''
ret = cls.to_dict()
for key, val in ret.items():
try:
nested_ret = val.recursive_dict()
except (AttributeError, NameError):
continue
ret[key] = nested_ret
return ret
@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 Keycodes.Modifiers.contains(requested_key)
```
This is not bulletproof due to how HID codes are defined (there is
overlap). Keycodes.Common.KC_A, for example, is equal in value to
Keycodes.Modifiers.KC_LALT, but it can still prevent silly mistakes
like trying to use, say, Keycodes.Common.KC_Q as a modifier.
This is recursive across subgroups, enabling stuff like:
```python
assert Keycodes.contains(requested_key)
```
To ensure that a valid keycode has been requested to begin with. Again,
not bulletproof, but adds at least some cushion to stuff that would
otherwise cause AttributeErrors and crash the keyboard.
'''
subcategories = (
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.to_dict().items()
if name.startswith('KC_')
):
return True
return any(sc.contains(kc) for sc in subcategories)
class Keycodes(KeycodeCategory):
'''
A massive grouping of keycodes
'''
class Modifiers(KeycodeCategory):
KC_CTRL = KC_LEFT_CTRL = Keycode(0x01, True)
KC_SHIFT = KC_LEFT_SHIFT = Keycode(0x02, True)
KC_ALT = KC_LALT = Keycode(0x04, True)
KC_GUI = KC_LGUI = Keycode(0x08, True)
KC_RCTRL = Keycode(0x10, True)
KC_RSHIFT = Keycode(0x20, True)
KC_RALT = Keycode(0x40, True)
KC_RGUI = Keycode(0x80, True)
class Common(KeycodeCategory):
KC_A = Keycode(4, False)
KC_B = Keycode(5, False)
KC_C = Keycode(6, False)
KC_D = Keycode(7, False)
KC_E = Keycode(8, False)
KC_F = Keycode(9, False)
KC_G = Keycode(10, False)
KC_H = Keycode(11, False)
KC_I = Keycode(12, False)
KC_J = Keycode(13, False)
KC_K = Keycode(14, False)
KC_L = Keycode(15, False)
KC_M = Keycode(16, False)
KC_N = Keycode(17, False)
KC_O = Keycode(18, False)
KC_P = Keycode(19, False)
KC_Q = Keycode(20, False)
KC_R = Keycode(21, False)
KC_S = Keycode(22, False)
KC_T = Keycode(23, False)
KC_U = Keycode(24, False)
KC_V = Keycode(25, False)
KC_W = Keycode(26, False)
KC_X = Keycode(27, False)
KC_Y = Keycode(28, False)
KC_Z = Keycode(29, False)
# Aliases to play nicely with AttrDict, since KC.1 isn't a valid
# attribute key in Python, but KC.N1 is
KC_1 = KC_N1 = Keycode(30, False)
KC_2 = KC_N2 = Keycode(31, False)
KC_3 = KC_N3 = Keycode(32, False)
KC_4 = KC_N4 = Keycode(33, False)
KC_5 = KC_N5 = Keycode(34, False)
KC_6 = KC_N6 = Keycode(35, False)
KC_7 = KC_N7 = Keycode(36, False)
KC_8 = KC_N8 = Keycode(37, False)
KC_9 = KC_N9 = Keycode(38, False)
KC_0 = KC_N0 = Keycode(39, False)
KC_ENTER = Keycode(40, False)
KC_ESC = Keycode(41, False)
KC_BACKSPACE = Keycode(42, False)
KC_TAB = Keycode(43, False)
KC_SPACE = Keycode(44, False)
KC_MINUS = Keycode(45, False)
KC_EQUAL = Keycode(46, False)
KC_LBRC = Keycode(47, False)
KC_RBRC = Keycode(48, False)
KC_BACKSLASH = Keycode(49, False)
KC_NUMBER = Keycode(50, False)
KC_SEMICOLON = Keycode(51, False)
KC_QUOTE = Keycode(52, False)
KC_TILDE = Keycode(53, False)
KC_COMMA = Keycode(54, False)
KC_PERIOD = Keycode(55, False)
KC_SLASH = Keycode(56, False)
KC_CAPS_LOCK = Keycode(57, False)
class FunctionKeys(KeycodeCategory):
KC_F1 = Keycode(58, False)
KC_F2 = Keycode(59, False)
KC_F3 = Keycode(60, False)
KC_F4 = Keycode(61, False)
KC_F5 = Keycode(62, False)
KC_F6 = Keycode(63, False)
KC_F7 = Keycode(64, False)
KC_F8 = Keycode(65, False)
KC_F9 = Keycode(66, False)
KC_F10 = Keycode(67, False)
KC_F11 = Keycode(68, False)
KC_F12 = Keycode(69, False)
class NavAndLocks(KeycodeCategory):
KC_PRINTSCREEN = Keycode(70, False)
KC_SCROLL_LOCK = Keycode(71, False)
KC_PAUSE = Keycode(72, False)
KC_INSERT = Keycode(73, False)
KC_HOME = Keycode(74, False)
KC_PGUP = Keycode(75, False)
KC_DELETE = Keycode(76, False)
KC_END = Keycode(77, False)
KC_PGDN = Keycode(78, False)
KC_RIGHT = Keycode(79, False)
KC_LEFT = Keycode(80, False)
KC_DOWN = Keycode(81, False)
KC_UP = Keycode(82, False)
class Numpad(KeycodeCategory):
KC_NUMLOCK = Keycode(83, False)
KC_KP_SLASH = Keycode(84, False)
KC_KP_ASTERIX = Keycode(85, False)
KC_KP_MINUS = Keycode(86, False)
KC_KP_PLUS = Keycode(87, False)
KC_KP_ENTER = Keycode(88, False)
KC_KP_1 = Keycode(89, False)
KC_KP_2 = Keycode(90, False)
KC_KP_3 = Keycode(91, False)
KC_KP_4 = Keycode(92, False)
KC_KP_5 = Keycode(93, False)
KC_KP_6 = Keycode(94, False)
KC_KP_7 = Keycode(95, False)
KC_KP_8 = Keycode(96, False)
KC_KP_9 = Keycode(97, False)
KC_KP_0 = Keycode(98, False)
KC_KP_PERIOD = Keycode(99, False)
ALL_KEYS = KC = AttrDict({
k.replace('KC_', ''): v
for k, v in flatten_dict(Keycodes.recursive_dict()).items()
})
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,),
}

View File

@ -8,5 +8,3 @@ class AttrDict(dict):
'''
def __getattr__(self, key):
return self[key]

View File

@ -13,13 +13,25 @@ except ImportError:
class Firmware:
def __init__(
self, keymap, row_pins, col_pins, diode_orientation,
log_level=logging.NOTSET,
hid=None, log_level=logging.NOTSET,
):
logger = logging.getLogger(__name__)
logger.setLevel(log_level)
self.cached_state = None
self.store = ReduxStore(kmk_reducer, log_level=log_level)
self.store.subscribe(
lambda state, action: self._subscription(state, action),
)
if not hid:
logger.warning(
"Must provide a HIDHelper (arg: hid), disabling HID\n"
"Board will run in debug mode",
)
self.hid = hid(store=self.store, log_level=log_level)
self.store.dispatch(init_firmware(
keymap=keymap,
row_pins=row_pins,

View File

@ -3,7 +3,8 @@ import string
from pyb import USB_HID, delay
from kmk.common.consts import Keycodes, char_lookup
from kmk.common.event_defs import KEY_DOWN_EVENT, KEY_UP_EVENT
from kmk.common.keycodes import Keycodes, char_lookup
class HIDHelper:
@ -34,13 +35,34 @@ class HIDHelper:
myhid.send_string('testing').send_string(' ... and testing again')
```
'''
def __init__(self, log_level=logging.NOTSET):
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._hid = USB_HID()
self.clear_all()
def _subscription(self, state, action):
if action['type'] == KEY_DOWN_EVENT:
if action['keycode'].is_modifier:
self.add_modifier(action['keycode'])
self.send()
else:
self.add_key(action['keycode'])
self.send()
elif action['type'] == KEY_UP_EVENT:
if action['keycode'].is_modifier:
self.remove_modifier(action['keycode'])
self.send()
else:
self.remove_key(action['keycode'])
self.send()
def send(self):
self.logger.debug('Sending HID report: {}'.format(self._evt))
self._hid.send(self._evt)
@ -50,8 +72,8 @@ class HIDHelper:
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
All keys will be removed at the completion of the string. Modifiers
are not really supported here, though Shift will be added if
necessary to output the key.
'''
@ -69,7 +91,7 @@ class HIDHelper:
modifier = Keycodes.Modifiers.KC_SHIFT if char.isupper() else None
if modifier:
self.enable_modifier(modifier)
self.add_modifier(modifier)
self.add_key(kc)
self.send()
@ -81,7 +103,7 @@ class HIDHelper:
# on non-Pyboards.
delay(10)
# Release all keys or we'll forever hold whatever the last keypress was
# Release all keys or we'll forever hold whatever the last keyadd was
self.clear_all()
self.send()
@ -97,9 +119,16 @@ class HIDHelper:
return self
def enable_modifier(self, modifier):
if Keycodes.Modifiers.contains(modifier):
self._evt[0] |= modifier
def add_modifier(self, modifier):
if modifier.is_modifier and Keycodes.Modifiers.contains(modifier):
self._evt[0] |= modifier.code
return self
raise ValueError('Attempted to use non-modifier as a modifier')
def remove_modifier(self, modifier):
if modifier.is_modifier and Keycodes.Modifiers.contains(modifier):
self._evt[0] ^= modifier.code
return self
raise ValueError('Attempted to use non-modifier as a modifier')
@ -110,12 +139,27 @@ class HIDHelper:
placed = False
for pos in range(2, 8):
if self._evt[pos] == 0x00:
self._evt[pos] = key
self._evt[pos] = key.code
placed = True
break
if not placed:
raise ValueError('Out of space in HID report, could not add key')
self.logger.warning('Out of space in HID report, could not add key')
return self
raise ValueError('Invalid keycode?')
def remove_key(self, key):
if key and Keycodes.contains(key):
removed = False
for pos in range(2, 8):
if self._evt[pos] == key.code:
self._evt[pos] = 0x00
removed = True
if not removed:
self.logger.warning('Tried to remove key that was not added')
return self

View File

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