From b5457534bff9798c221b86a0569f23594f4aaf59 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Mon, 1 Oct 2018 00:31:45 -0700 Subject: [PATCH] Completely overhaul the entire MatrixScanner and KEY_UP/DOWN_EVENT system for efficiency. Less delay() on HID sends. Speed is only BARELY slower than QMK now. --- kmk/common/abstract/matrix_scanner.py | 18 +--- kmk/common/event_defs.py | 59 ++++-------- kmk/common/internal_keycodes.py | 79 +++++++--------- kmk/common/internal_state.py | 129 +++++++++++--------------- kmk/firmware.py | 12 +-- kmk/micropython/matrix.py | 35 +++---- kmk/micropython/pyb_hid.py | 5 +- 7 files changed, 126 insertions(+), 211 deletions(-) diff --git a/kmk/common/abstract/matrix_scanner.py b/kmk/common/abstract/matrix_scanner.py index 28561c9..6f324b7 100644 --- a/kmk/common/abstract/matrix_scanner.py +++ b/kmk/common/abstract/matrix_scanner.py @@ -5,21 +5,5 @@ class AbstractMatrixScanner(): def __init__(self, cols, rows, active_layers, diode_orientation=DiodeOrientation.COLUMNS): raise NotImplementedError('Abstract implementation') - def _normalize_matrix(self, matrix): - ''' - We always want to internally look at a keyboard as a list of rows, - where a "row" is a list of keycodes (columns). - - This will convert DiodeOrientation.COLUMNS matrix scans into a - ROWS scan, so we never have to think about these things again. - ''' - if self.diode_orientation == DiodeOrientation.ROWS: - return matrix - - return [ - [col[col_entry] for col in matrix] - for col_entry in range(max(len(col) for col in matrix)) - ] - - def raw_scan(self): + def scan_for_pressed(self): raise NotImplementedError('Abstract implementation') diff --git a/kmk/common/event_defs.py b/kmk/common/event_defs.py index f061f39..e7102f9 100644 --- a/kmk/common/event_defs.py +++ b/kmk/common/event_defs.py @@ -100,55 +100,28 @@ def macro_complete_event(): ) -def matrix_changed(new_matrix): +def matrix_changed(new_pressed): def _key_pressed(dispatch, get_state): + dispatch(new_matrix_event(new_pressed)) + state = get_state() - # Temporarily preserve a reference to the old event - # We do fake Redux around here because microcontrollers - # aren't exactly RAM or CPU powerhouses - the state does - # mutate in place. Unfortunately this makes reasoning - # about code a bit messier and really hurts one of the - # selling points of Redux. Former development versions - # of KMK created new InternalState copies every single - # time the state changed, but it was sometimes slow. - old_matrix = state.matrix - old_keys_pressed = state.keys_pressed - dispatch(new_matrix_event(new_matrix)) + if state.hid_pending: + dispatch(hid_report_event()) - with get_state() as new_state: - for ridx, row in enumerate(new_state.matrix): - for cidx, col in enumerate(row): - if col != old_matrix[ridx][cidx]: - if col: - dispatch(key_down_event( - row=ridx, - col=cidx, - )) - else: - dispatch(key_up_event( - row=ridx, - col=cidx, - )) + if Keycodes.KMK.KC_RESET in state.keys_pressed: + try: + import machine + machine.bootloader() + except ImportError: + logger.warning('Tried to reset to bootloader, but not supported on this chip?') - with get_state() as new_state: - if old_keys_pressed != new_state.keys_pressed: - dispatch(hid_report_event()) + if state.macro_pending: + macro = state.macro_pending - if Keycodes.KMK.KC_RESET in new_state.keys_pressed: - try: - import machine - machine.bootloader() - except ImportError: - logger.warning('Tried to reset to bootloader, but not supported on this chip?') + for event in macro(state): + dispatch(event) - with get_state() as new_state: - if new_state.macro_pending: - macro = new_state.macro_pending - - for event in macro(new_state): - dispatch(event) - - dispatch(macro_complete_event()) + dispatch(macro_complete_event()) return _key_pressed diff --git a/kmk/common/internal_keycodes.py b/kmk/common/internal_keycodes.py index 2dd903e..54ef00f 100644 --- a/kmk/common/internal_keycodes.py +++ b/kmk/common/internal_keycodes.py @@ -4,7 +4,7 @@ from kmk.common.event_defs import KEY_DOWN_EVENT, KEY_UP_EVENT from kmk.common.keycodes import Keycodes, RawKeycodes -def process_internal_key_event(state, action, changed_key, logger=None): +def process_internal_key_event(state, action_type, changed_key, logger=None): if logger is None: logger = logging.getLogger(__name__) @@ -14,71 +14,58 @@ def process_internal_key_event(state, action, changed_key, logger=None): # objects if changed_key.code == RawKeycodes.KC_DF: - return df(state, action, changed_key, logger=logger) + return df(state, action_type, changed_key, logger=logger) elif changed_key.code == RawKeycodes.KC_MO: - return mo(state, action, changed_key, logger=logger) + return mo(state, action_type, changed_key, logger=logger) elif changed_key.code == RawKeycodes.KC_TG: - return tg(state, action, changed_key, logger=logger) + return tg(state, action_type, changed_key, logger=logger) elif changed_key.code == RawKeycodes.KC_TO: - return to(state, action, changed_key, logger=logger) + return to(state, action_type, changed_key, logger=logger) elif changed_key.code == Keycodes.KMK.KC_GESC.code: - return grave_escape(state, action, logger=logger) + return grave_escape(state, action_type, logger=logger) elif changed_key.code == RawKeycodes.KC_UC_MODE: - return unicode_mode(state, action, changed_key, logger=logger) + return unicode_mode(state, action_type, changed_key, logger=logger) else: return state -def grave_escape(state, action, logger): - if action[0] == KEY_DOWN_EVENT: +def grave_escape(state, action_type, logger): + if action_type == KEY_DOWN_EVENT: for key in state.keys_pressed: - if key in {Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT}: - # if Shift is held, return KC_GRAVE which will become KC_TILDE on OS level - return state.update( - keys_pressed=( - state.keys_pressed | {Keycodes.Common.KC_GRAVE} - ), - ) - elif key in {Keycodes.Modifiers.KC_LGUI, Keycodes.Modifiers.KC_RGUI}: - # if GUI is held, return KC_GRAVE - return state.update( - keys_pressed=( - state.keys_pressed | {Keycodes.Common.KC_GRAVE} - ), - ) + if key in { + Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT, + Keycodes.Modifiers.KC_LGUI, Keycodes.Modifiers.KC_RGUI, + }: + # if Shift is held, KC_GRAVE will become KC_TILDE on OS level + state.keys_pressed.add(Keycodes.Common.KC_GRAVE) + return state # else return KC_ESC - return state.update( - keys_pressed=( - state.keys_pressed | {Keycodes.Common.KC_ESCAPE} - ), - ) + state.keys_pressed.add(Keycodes.Common.KC_ESCAPE) + return state - elif action[0] == KEY_UP_EVENT: - return state.update( - keys_pressed=frozenset( - key for key in state.keys_pressed - if key not in {Keycodes.Common.KC_ESCAPE, Keycodes.Common.KC_GRAVE} - ), - ) + elif action_type == KEY_UP_EVENT: + state.keys_pressed.discard(Keycodes.Common.KC_ESCAPE) + state.keys_pressed.discard(Keycodes.Common.KC_GRAVE) + return state -def df(state, action, changed_key, logger): +def df(state, action_type, changed_key, logger): """Switches the default layer""" - if action[0] == KEY_DOWN_EVENT: + if action_type == KEY_DOWN_EVENT: state.active_layers[0] = changed_key.layer return state -def mo(state, action, changed_key, logger): +def mo(state, action_type, changed_key, logger): """Momentarily activates layer, switches off when you let go""" - if action[0] == KEY_UP_EVENT: + if action_type == KEY_UP_EVENT: state.active_layers = [ layer for layer in state.active_layers if layer != changed_key.layer ] - elif action[0] == KEY_DOWN_EVENT: + elif action_type == KEY_DOWN_EVENT: state.active_layers.append(changed_key.layer) return state @@ -92,9 +79,9 @@ def lt(layer, kc): """Momentarily activates layer if held, sends kc if tapped""" -def tg(state, action, changed_key, logger): +def tg(state, action_type, changed_key, logger): """Toggles the layer (enables it if not active, and vise versa)""" - if action[0] == KEY_DOWN_EVENT: + if action_type == KEY_DOWN_EVENT: if changed_key.layer in state.active_layers: state.active_layers = [ layer for layer in state.active_layers @@ -106,9 +93,9 @@ def tg(state, action, changed_key, logger): return state -def to(state, action, changed_key, logger): +def to(state, action_type, changed_key, logger): """Activates layer and deactivates all other layers""" - if action[0] == KEY_DOWN_EVENT: + if action_type == KEY_DOWN_EVENT: state.active_layers = [changed_key.layer] return state @@ -118,8 +105,8 @@ def tt(layer): """Momentarily activates layer if held, toggles it if tapped repeatedly""" -def unicode_mode(state, action, changed_key, logger): - if action[0] == KEY_DOWN_EVENT: +def unicode_mode(state, action_type, changed_key, logger): + if action_type == KEY_DOWN_EVENT: state.unicode_mode = changed_key.mode return state diff --git a/kmk/common/internal_state.py b/kmk/common/internal_state.py index 935fa55..36c5601 100644 --- a/kmk/common/internal_state.py +++ b/kmk/common/internal_state.py @@ -52,9 +52,9 @@ class ReduxStore: class InternalState: - modifiers_pressed = frozenset() - keys_pressed = frozenset() + keys_pressed = set() macro_pending = None + hid_pending = False unicode_mode = UnicodeModes.NOOP keymap = [] row_pins = [] @@ -76,7 +76,6 @@ class InternalState: def to_dict(self, verbose=False): ret = { 'keys_pressed': self.keys_pressed, - 'modifiers_pressed': self.modifiers_pressed, 'active_layers': self.active_layers, 'unicode_mode': self.unicode_mode, } @@ -134,83 +133,60 @@ def kmk_reducer(state=None, action=None, logger=None): return state if action[0] == NEW_MATRIX_EVENT: - return state.update( - matrix=action[1], - ) + matrix_keys_pressed = { + find_key_in_map(state, row, col) + for row, col in action[1] + } + + pressed = matrix_keys_pressed - state.keys_pressed + released = state.keys_pressed - matrix_keys_pressed + + if not pressed and not released: + return state + + for changed_key in released: + if not changed_key: + continue + + elif isinstance(changed_key, KMKMacro): + if changed_key.keyup: + state.macro_pending = changed_key.keyup + + elif changed_key.code >= FIRST_KMK_INTERNAL_KEYCODE: + state = process_internal_key_event(state, KEY_UP_EVENT, changed_key, logger=logger) + + for changed_key in pressed: + if not changed_key: + continue + + elif isinstance(changed_key, KMKMacro): + if changed_key.keydown: + state.macro_pending = changed_key.keydown + + elif changed_key.code >= FIRST_KMK_INTERNAL_KEYCODE: + state = process_internal_key_event( + state, + KEY_DOWN_EVENT, + changed_key, + logger=logger, + ) + + state.matrix = action[1] + state.keys_pressed |= pressed + state.keys_pressed -= released + state.hid_pending = True + + return state if action[0] == KEYCODE_UP_EVENT: - return state.update( - keys_pressed=frozenset( - key for key in state.keys_pressed if key != action[1] - ), - ) + state.keys_pressed.discard(action[1]) + state.hid_pending = True + return state if action[0] == KEYCODE_DOWN_EVENT: - return state.update( - keys_pressed=( - state.keys_pressed | {action[1]} - ), - ) - - if action[0] == KEY_UP_EVENT: - row = action[1] - col = action[2] - - changed_key = find_key_in_map(state, row, col) - - logger.debug('Detected change to key: {}'.format(changed_key)) - - if not changed_key: - return state - - if isinstance(changed_key, KMKMacro): - if changed_key.keyup: - return state.update( - macro_pending=changed_key.keyup, - ) - - return state - - newstate = state.update( - keys_pressed=frozenset( - key for key in state.keys_pressed if key != changed_key - ), - ) - - if changed_key.code >= FIRST_KMK_INTERNAL_KEYCODE: - return process_internal_key_event(newstate, action, changed_key, logger=logger) - - return newstate - - if action[0] == KEY_DOWN_EVENT: - row = action[1] - col = action[2] - - changed_key = find_key_in_map(state, row, col) - - logger.debug('Detected change to key: {}'.format(changed_key)) - - if not changed_key: - return state - - if isinstance(changed_key, KMKMacro): - if changed_key.keydown: - return state.update( - macro_pending=changed_key.keydown, - ) - - return state - - newstate = state.update( - keys_pressed=( - state.keys_pressed | {changed_key} - ), - ) - - if changed_key.code >= FIRST_KMK_INTERNAL_KEYCODE: - return process_internal_key_event(newstate, action, changed_key, logger=logger) - - return newstate + state.keys_pressed.add(action[1]) + state.hid_pending = True + return state if action[0] == INIT_FIRMWARE_EVENT: return state.update( @@ -230,6 +206,7 @@ def kmk_reducer(state=None, action=None, logger=None): # into KEY_UP_EVENT and KEY_DOWN_EVENT, but for now it's nice to separate # this out for debugging's sake. if action[0] == HID_REPORT_EVENT: + state.hid_pending = False return state if action[0] == MACRO_COMPLETE_EVENT: diff --git a/kmk/firmware.py b/kmk/firmware.py index bff6c08..b97c1b0 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -18,7 +18,8 @@ class Firmware: logger = logging.getLogger(__name__) logger.setLevel(log_level) - self.cached_state = None + self.hydrated = False + self.store = ReduxStore(kmk_reducer, log_level=log_level) self.store.subscribe( lambda state, action: self._subscription(state, action), @@ -41,20 +42,17 @@ class Firmware: )) def _subscription(self, state, action): - if self.cached_state is None or any( - getattr(self.cached_state, k) != getattr(state, k) - for k in state.__dict__.keys() - ): + if not self.hydrated: self.matrix = MatrixScanner( state.col_pins, state.row_pins, state.diode_orientation, ) + self.hydrated = True def go(self): while True: - state = self.store.get_state() - update = self.matrix.scan_for_changes(state.matrix) + update = self.matrix.scan_for_pressed() if update: self.store.dispatch(update) diff --git a/kmk/micropython/matrix.py b/kmk/micropython/matrix.py index 3677201..6f69105 100644 --- a/kmk/micropython/matrix.py +++ b/kmk/micropython/matrix.py @@ -21,6 +21,7 @@ class MatrixScanner(AbstractMatrixScanner): self.rows = [machine.Pin(pin) for pin in rows] self.diode_orientation = diode_orientation self.active_layers = active_layers + self.last_pressed_len = 0 if self.diode_orientation == DiodeOrientation.COLUMNS: self.outputs = self.cols @@ -41,29 +42,23 @@ class MatrixScanner(AbstractMatrixScanner): pin.init(machine.Pin.IN, machine.Pin.PULL_DOWN) pin.off() - def _normalize_matrix(self, matrix): - return super()._normalize_matrix(matrix) + def scan_for_pressed(self): + pressed = [] - def raw_scan(self): - matrix = [] - - for opin in self.outputs: + for oidx, opin in enumerate(self.outputs): opin.value(1) - matrix.append([bool(ipin.value()) for ipin in self.inputs]) + + for iidx, ipin in enumerate(self.inputs): + if ipin.value(): + pressed.append( + (oidx, iidx) if self.diode_orientation == DiodeOrientation.ROWS else (iidx, oidx) # noqa + ) + opin.value(0) - return self._normalize_matrix(matrix) - - def scan_for_changes(self, old_matrix): - matrix = self.raw_scan() - - if any( - any( - col != old_matrix[ridx][cidx] - for cidx, col in enumerate(row) - ) - for ridx, row in enumerate(matrix) - ): - return matrix_changed(matrix) + if len(pressed) != self.last_pressed_len: + print('LEN PRESSED {} LEN LAST PRESSED {}'.format(len(pressed), self.last_pressed_len)) + self.last_pressed_len = len(pressed) + return matrix_changed(pressed) return None # The default, but for explicitness diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py index 98d733c..4299814 100644 --- a/kmk/micropython/pyb_hid.py +++ b/kmk/micropython/pyb_hid.py @@ -6,6 +6,7 @@ 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, ConsumerKeycode, ModifierKeycode) +from kmk.common.macros import KMKMacro def generate_pyb_hid_descriptor(): @@ -71,7 +72,7 @@ class HIDHelper: self.add_key(consumer_key) else: for key in state.keys_pressed: - if key.code >= FIRST_KMK_INTERNAL_KEYCODE: + if isinstance(key, KMKMacro) or key.code >= FIRST_KMK_INTERNAL_KEYCODE: continue if isinstance(key, ModifierKeycode): @@ -98,7 +99,7 @@ class HIDHelper: # 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) + delay(5) return self