diff --git a/kmk/common/event_defs.py b/kmk/common/event_defs.py index 69bfd8a..e87a628 100644 --- a/kmk/common/event_defs.py +++ b/kmk/common/event_defs.py @@ -9,6 +9,9 @@ KEY_DOWN_EVENT = const(2) INIT_FIRMWARE_EVENT = const(3) NEW_MATRIX_EVENT = const(4) HID_REPORT_EVENT = const(5) +KEYCODE_UP_EVENT = const(6) +KEYCODE_DOWN_EVENT = const(7) +MACRO_COMPLETE_EVENT = const(8) logger = logging.getLogger(__name__) @@ -39,6 +42,28 @@ def key_down_event(row, col): } +def keycode_up_event(keycode): + ''' + Press a key by Keycode object, bypassing the keymap. Used mostly for + macros. + ''' + return { + 'type': KEYCODE_UP_EVENT, + 'keycode': keycode, + } + + +def keycode_down_event(keycode): + ''' + Release a key by Keycode object, bypassing the keymap. Used mostly for + macros. + ''' + return { + 'type': KEYCODE_DOWN_EVENT, + 'keycode': keycode, + } + + def new_matrix_event(matrix): return { 'type': NEW_MATRIX_EVENT, @@ -52,6 +77,13 @@ def hid_report_event(): } +def macro_complete_event(macro): + return { + 'type': MACRO_COMPLETE_EVENT, + 'macro': macro, + } + + def matrix_changed(new_matrix): def _key_pressed(dispatch, get_state): state = get_state() @@ -94,4 +126,12 @@ def matrix_changed(new_matrix): except ImportError: logger.warning('Tried to reset to bootloader, but not supported on this chip?') + while get_state().macros_pending: + macro = get_state().macros_pending[0] + + for event in macro(): + dispatch(event) + + dispatch(macro_complete_event(macro)) + return _key_pressed diff --git a/kmk/common/internal_state.py b/kmk/common/internal_state.py index ce5962b..2d47807 100644 --- a/kmk/common/internal_state.py +++ b/kmk/common/internal_state.py @@ -4,9 +4,11 @@ import sys from kmk.common.consts import DiodeOrientation from kmk.common.event_defs import (HID_REPORT_EVENT, INIT_FIRMWARE_EVENT, KEY_DOWN_EVENT, KEY_UP_EVENT, - NEW_MATRIX_EVENT) + KEYCODE_DOWN_EVENT, KEYCODE_UP_EVENT, + MACRO_COMPLETE_EVENT, NEW_MATRIX_EVENT) from kmk.common.internal_keycodes import process_internal_key_event from kmk.common.keycodes import FIRST_KMK_INTERNAL_KEYCODE, Keycodes +from kmk.macros import KMKMacro class ReduxStore: @@ -52,6 +54,7 @@ class ReduxStore: class InternalState: modifiers_pressed = frozenset() keys_pressed = frozenset() + macros_pending = [] keymap = [] row_pins = [] col_pins = [] @@ -133,6 +136,20 @@ def kmk_reducer(state=None, action=None, logger=None): matrix=action['matrix'], ) + if action['type'] == KEYCODE_UP_EVENT: + return state.update( + keys_pressed=frozenset( + key for key in state.keys_pressed if key != action['keycode'] + ), + ) + + if action['type'] == KEYCODE_DOWN_EVENT: + return state.update( + keys_pressed=( + state.keys_pressed | {action['keycode']} + ), + ) + if action['type'] == KEY_UP_EVENT: row = action['row'] col = action['col'] @@ -144,6 +161,14 @@ def kmk_reducer(state=None, action=None, logger=None): if not changed_key: return state + if isinstance(changed_key, KMKMacro): + if changed_key.keyup: + return state.update( + macros_pending=state.macros_pending + [changed_key.keyup], + ) + + return state + newstate = state.update( keys_pressed=frozenset( key for key in state.keys_pressed if key != changed_key @@ -166,6 +191,14 @@ def kmk_reducer(state=None, action=None, logger=None): if not changed_key: return state + if isinstance(changed_key, KMKMacro): + if changed_key.keydown: + return state.update( + macros_pending=state.macros_pending + [changed_key.keydown], + ) + + return state + newstate = state.update( keys_pressed=( state.keys_pressed | {changed_key} @@ -196,6 +229,14 @@ def kmk_reducer(state=None, action=None, logger=None): if action['type'] == HID_REPORT_EVENT: return state + if action['type'] == MACRO_COMPLETE_EVENT: + return state.update( + macros_pending=[ + m for m in state.macros_pending + if m != action['macro'] + ], + ) + # On unhandled events, log and do not mutate state logger.warning('Unhandled event! Returning state unmodified.') return state diff --git a/kmk/macros/__init__.py b/kmk/macros/__init__.py new file mode 100644 index 0000000..a6cc1d9 --- /dev/null +++ b/kmk/macros/__init__.py @@ -0,0 +1,10 @@ +class KMKMacro: + def __init__(self, keydown=None, keyup=None): + self.keydown = keydown + self.keyup = keyup + + def on_keydown(self): + return self.keydown() if self.keydown else None + + def on_keyup(self): + return self.keyup() if self.keyup else None diff --git a/kmk/macros/simple.py b/kmk/macros/simple.py new file mode 100644 index 0000000..62c3cff --- /dev/null +++ b/kmk/macros/simple.py @@ -0,0 +1,14 @@ +from kmk.common.event_defs import (hid_report_event, keycode_down_event, + keycode_up_event) +from kmk.macros import KMKMacro + + +def simple_key_sequence(seq): + def _simple_key_sequence(): + for key in seq: + yield keycode_down_event(key) + yield hid_report_event() + yield keycode_up_event(key) + yield hid_report_event() + + return KMKMacro(keydown=_simple_key_sequence) diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py index a4a9526..1d6eb72 100644 --- a/kmk/micropython/pyb_hid.py +++ b/kmk/micropython/pyb_hid.py @@ -73,7 +73,6 @@ class HIDHelper: # device, or keys will get stuck (mostly when releasing # media/consumer keys) self.send() - delay(10) self.report_device[0] = needed_reporting_device @@ -99,6 +98,17 @@ class HIDHelper: self.logger.debug('Sending HID report: {}'.format(self._evt)) self._hid.send(self._evt) + # 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. + # + # It'd be real awesome if pyb.USB_HID.send/recv would support + # uselect.poll or uselect.select to more safely determine when + # it is safe to write to the host again... + delay(10) + return self def send_string(self, message): @@ -128,13 +138,6 @@ class HIDHelper: 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 keyadd was self.clear_all() self.send() diff --git a/user_keymaps/klardotsh/threethree_matrix_pyboard.py b/user_keymaps/klardotsh/threethree_matrix_pyboard.py index 324b10c..dd62898 100644 --- a/user_keymaps/klardotsh/threethree_matrix_pyboard.py +++ b/user_keymaps/klardotsh/threethree_matrix_pyboard.py @@ -3,6 +3,7 @@ import machine from kmk.common.consts import DiodeOrientation from kmk.common.keycodes import KC from kmk.entrypoints.handwire.pyboard import main +from kmk.macros.simple import simple_key_sequence p = machine.Pin.board cols = (p.X10, p.X11, p.X12) @@ -10,6 +11,21 @@ rows = (p.X1, p.X2, p.X3) diode_orientation = DiodeOrientation.COLUMNS +MACRO_TEST_STRING = simple_key_sequence([ + KC.LSHIFT(KC.H), + KC.E, + KC.L, + KC.L, + KC.O, + + KC.SPACE, + + KC.LSHIFT(KC.K), + KC.LSHIFT(KC.M), + KC.LSHIFT(KC.K), + KC.EXCLAIM, +]) + keymap = [ [ [KC.MO(1), KC.GESC, KC.RESET], @@ -24,6 +40,6 @@ keymap = [ [ [KC.VOLU, KC.MUTE, KC.Z], [KC.TRNS, KC.PIPE, KC.MEDIA_PLAY_PAUSE], - [KC.VOLD, KC.P, KC.Q], + [KC.VOLD, KC.P, MACRO_TEST_STRING], ], ]