diff --git a/boards/klardotsh/threethree_matrix_pyboard.py b/boards/klardotsh/threethree_matrix_pyboard.py index 54817e5..c3b9c20 100644 --- a/boards/klardotsh/threethree_matrix_pyboard.py +++ b/boards/klardotsh/threethree_matrix_pyboard.py @@ -17,9 +17,21 @@ def main(): diode_orientation = DiodeOrientation.COLUMNS keymap = [ - [KC.DF(1), KC.H, KC.RESET], - [KC.MO(2), KC.I, KC.ENTER], - [KC.CTRL, KC.SPACE, KC.SHIFT], + [ + [KC.MO(1), KC.H, KC.RESET], + [KC.MO(2), KC.I, KC.ENTER], + [KC.CTRL, KC.SPACE, KC.SHIFT], + ], + [ + [KC.TRNS, KC.B, KC.C], + [KC.NO, KC.D, KC.E], + [KC.F, KC.G, KC.H], + ], + [ + [KC.X, KC.Y, KC.Z], + [KC.TRNS, KC.N, KC.O], + [KC.R, KC.P, KC.Q], + ], ] firmware = Firmware( diff --git a/kmk/common/event_defs.py b/kmk/common/event_defs.py index 04a1f7a..69bfd8a 100644 --- a/kmk/common/event_defs.py +++ b/kmk/common/event_defs.py @@ -1,8 +1,16 @@ +import logging + from micropython import const +from kmk.common.keycodes import Keycodes + KEY_UP_EVENT = const(1) KEY_DOWN_EVENT = const(2) INIT_FIRMWARE_EVENT = const(3) +NEW_MATRIX_EVENT = const(4) +HID_REPORT_EVENT = const(5) + +logger = logging.getLogger(__name__) def init_firmware(keymap, row_pins, col_pins, diode_orientation): @@ -15,19 +23,75 @@ def init_firmware(keymap, row_pins, col_pins, diode_orientation): } -def key_up_event(keycode, row, col): +def key_up_event(row, col): return { 'type': KEY_UP_EVENT, - 'keycode': keycode, 'row': row, 'col': col, } -def key_down_event(keycode, row, col): +def key_down_event(row, col): return { 'type': KEY_DOWN_EVENT, - 'keycode': keycode, 'row': row, 'col': col, } + + +def new_matrix_event(matrix): + return { + 'type': NEW_MATRIX_EVENT, + 'matrix': matrix, + } + + +def hid_report_event(): + return { + 'type': HID_REPORT_EVENT, + } + + +def matrix_changed(new_matrix): + def _key_pressed(dispatch, get_state): + 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)) + + 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, + )) + + with get_state() as new_state: + if old_keys_pressed != new_state.keys_pressed: + dispatch(hid_report_event()) + + 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?') + + return _key_pressed diff --git a/kmk/common/internal_keycodes.py b/kmk/common/internal_keycodes.py index eb2c3dc..2371699 100644 --- a/kmk/common/internal_keycodes.py +++ b/kmk/common/internal_keycodes.py @@ -4,53 +4,41 @@ from kmk.common.event_defs import KEY_DOWN_EVENT, KEY_UP_EVENT from kmk.common.keycodes import Keycodes -def process(state, action, logger=None): - if action['keycode'].code < 1000: - return state - +def process_internal_key_event(state, action, changed_key, logger=None): if logger is None: logger = logging.getLogger(__name__) - logger.warning(action['keycode']) - if action['keycode'] == Keycodes.KMK.KC_RESET: - return reset(state, action, logger=logger) - elif action['keycode'].code == Keycodes.Layers._KC_DF: - return df(state, action, logger=logger) - elif action['keycode'].code == Keycodes.Layers._KC_MO: - return tilde(state, action, logger=logger) - elif action['keycode'].code == Keycodes.Layers.KC_TILDE: + if changed_key.code == Keycodes.Layers._KC_DF: + return df(state, action, changed_key, logger=logger) + elif changed_key.code == Keycodes.Layers._KC_MO: + return mo(state, action, changed_key, logger=logger) + elif changed_key.code == Keycodes.Layers.KC_TILDE: pass else: return state -def tilde(state, action, logger): +def tilde(state, action, changed_key, logger): # TODO Actually process keycodes return state -def reset(state, action, logger): - logger.debug('Rebooting to bootloader') - import machine - machine.bootloader() - - -def df(state, action, logger): +def df(state, action, changed_key, logger): """Switches the default layer""" - state.active_layers[0] = action['keycode'].layer + state.active_layers[0] = changed_key.layer return state -def mo(state, action, logger): +def mo(state, action, changed_key, logger): """Momentarily activates layer, switches off when you let go""" if action['type'] == KEY_UP_EVENT: state.active_layers = [ layer for layer in state.active_layers - if layer != action['keycode'].layer + if layer != changed_key.layer ] elif action['type'] == KEY_DOWN_EVENT: - state.active_layers.append(action['keycode'].layer) + state.active_layers.append(changed_key.layer) return state diff --git a/kmk/common/internal_state.py b/kmk/common/internal_state.py index 362d748..ce5962b 100644 --- a/kmk/common/internal_state.py +++ b/kmk/common/internal_state.py @@ -2,9 +2,11 @@ import logging import sys from kmk.common.consts import DiodeOrientation -from kmk.common.event_defs import (INIT_FIRMWARE_EVENT, KEY_DOWN_EVENT, - KEY_UP_EVENT) -from kmk.common.internal_keycodes import process as process_internal +from kmk.common.event_defs import (HID_REPORT_EVENT, INIT_FIRMWARE_EVENT, + KEY_DOWN_EVENT, KEY_UP_EVENT, + NEW_MATRIX_EVENT) +from kmk.common.internal_keycodes import process_internal_key_event +from kmk.common.keycodes import FIRST_KMK_INTERNAL_KEYCODE, Keycodes class ReduxStore: @@ -16,9 +18,15 @@ class ReduxStore: self.callbacks = [] def dispatch(self, action): - self.logger.debug('Dispatching action: {}'.format(action)) - self.state = self.reducer(self.state, action) - self.logger.debug('Dispatching complete: {}'.format(action)) + if callable(action): + self.logger.debug('Received thunk') + action(self.dispatch, self.get_state) + self.logger.debug('Finished thunk') + return None + + self.logger.debug('Dispatching action: Type {} >> {}'.format(action['type'], action)) + self.state = self.reducer(self.state, action, logger=self.logger) + self.logger.debug('Dispatching complete: Type {}'.format(action['type'])) self.logger.debug('New state: {}'.format(self.state)) @@ -55,6 +63,12 @@ class InternalState: def __init__(self, preserve_intermediate_states=False): self.preserve_intermediate_states = preserve_intermediate_states + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass + def to_dict(self, verbose=False): ret = { 'keys_pressed': self.keys_pressed, @@ -86,6 +100,21 @@ class InternalState: return self +def find_key_in_map(state, row, col): + # Later-added layers have priority. Sift through the layers + # in reverse order until we find a valid keycode object + for layer in reversed(state.active_layers): + layer_key = state.keymap[layer][row][col] + + if not layer_key or layer_key == Keycodes.KMK.KC_TRNS: + continue + + if layer_key == Keycodes.KMK.KC_NO: + break + + return layer_key + + def kmk_reducer(state=None, action=None, logger=None): if state is None: state = InternalState() @@ -99,41 +128,52 @@ def kmk_reducer(state=None, action=None, logger=None): return state - if action['type'] == KEY_UP_EVENT: - newstate = state.update( - keys_pressed=frozenset( - key for key in state.keys_pressed if key != action['keycode'] - ), - matrix=[ - r if ridx != action['row'] else [ - c if cidx != action['col'] else False - for cidx, c in enumerate(r) - ] - for ridx, r in enumerate(state.matrix) - ], + if action['type'] == NEW_MATRIX_EVENT: + return state.update( + matrix=action['matrix'], ) - if action['keycode'].code >= 1000: - return process_internal(newstate, action, logger=logger) + if action['type'] == KEY_UP_EVENT: + row = action['row'] + col = action['col'] + + changed_key = find_key_in_map(state, row, col) + + logger.debug('Detected change to key: {}'.format(changed_key)) + + if not changed_key: + 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['type'] == KEY_DOWN_EVENT: + row = action['row'] + col = action['col'] + + changed_key = find_key_in_map(state, row, col) + + logger.debug('Detected change to key: {}'.format(changed_key)) + + if not changed_key: + return state + newstate = state.update( keys_pressed=( - state.keys_pressed | {action['keycode']} + state.keys_pressed | {changed_key} ), - matrix=[ - r if ridx != action['row'] else [ - c if cidx != action['col'] else True - for cidx, c in enumerate(r) - ] - for ridx, r in enumerate(state.matrix) - ], ) - if action['keycode'].code >= 1000: - return process_internal(newstate, action, logger=logger) + if changed_key.code >= FIRST_KMK_INTERNAL_KEYCODE: + return process_internal_key_event(newstate, action, changed_key, logger=logger) return newstate @@ -148,3 +188,14 @@ def kmk_reducer(state=None, action=None, logger=None): for r in action['row_pins'] ], ) + + # HID events are non-mutating, used exclusively for listeners to know + # they should be doing things. This could/should arguably be folded back + # into KEY_UP_EVENT and KEY_DOWN_EVENT, but for now it's nice to separate + # this out for debugging's sake. + if action['type'] == HID_REPORT_EVENT: + return state + + # On unhandled events, log and do not mutate state + logger.warning('Unhandled event! Returning state unmodified.') + return state diff --git a/kmk/common/keycodes.py b/kmk/common/keycodes.py index fe4d7d6..09e226a 100644 --- a/kmk/common/keycodes.py +++ b/kmk/common/keycodes.py @@ -8,6 +8,8 @@ except ImportError: from kmk.common.types import AttrDict from kmk.common.util import flatten_dict +FIRST_KMK_INTERNAL_KEYCODE = 1000 + Keycode = namedtuple('Keycode', ('code', 'is_modifier')) LayerKeycode = namedtuple('LayerKeycode', ('code', 'layer')) @@ -321,6 +323,8 @@ class Keycodes(KeycodeCategory): KC_RSPC = Keycode(1004, False) KC_LEAD = Keycode(1005, False) KC_LOCK = Keycode(1006, False) + KC_NO = Keycode(1100, False) + KC_TRNS = Keycode(1101, False) class Layers(KeycodeCategory): _KC_DF = 1050 diff --git a/kmk/common/keymap.py b/kmk/common/keymap.py deleted file mode 100644 index c135552..0000000 --- a/kmk/common/keymap.py +++ /dev/null @@ -1,25 +0,0 @@ -from kmk.common.event_defs import key_down_event, key_up_event - - -class Keymap: - def __init__(self, map): - self.map = map - - def parse(self, matrix, store): - state = store.get_state() - - for ridx, row in enumerate(matrix): - for cidx, col in enumerate(row): - if col != state.matrix[ridx][cidx]: - if col: - store.dispatch(key_down_event( - row=ridx, - col=cidx, - keycode=self.map[ridx][cidx], - )) - else: - store.dispatch(key_up_event( - row=ridx, - col=cidx, - keycode=self.map[ridx][cidx], - )) diff --git a/kmk/firmware.py b/kmk/firmware.py index 87885e7..2156beb 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -2,7 +2,6 @@ import logging from kmk.common.event_defs import init_firmware from kmk.common.internal_state import ReduxStore, kmk_reducer -from kmk.common.keymap import Keymap try: from kmk.circuitpython.matrix import MatrixScanner @@ -40,9 +39,6 @@ class Firmware: )) def _subscription(self, state, action): - if self.cached_state is None or self.cached_state.keymap != state.keymap: - self.keymap = Keymap(state.keymap) - if self.cached_state is None or any( getattr(self.cached_state, k) != getattr(state, k) for k in state.__dict__.keys() @@ -55,4 +51,8 @@ class Firmware: def go(self): while True: - self.keymap.parse(self.matrix.raw_scan(), store=self.store) + state = self.store.get_state() + update = self.matrix.scan_for_changes(state.matrix) + + if update: + self.store.dispatch(update) diff --git a/kmk/micropython/matrix.py b/kmk/micropython/matrix.py index 584ea97..3677201 100644 --- a/kmk/micropython/matrix.py +++ b/kmk/micropython/matrix.py @@ -2,6 +2,7 @@ import machine from kmk.common.abstract.matrix_scanner import AbstractMatrixScanner from kmk.common.consts import DiodeOrientation +from kmk.common.event_defs import matrix_changed class MatrixScanner(AbstractMatrixScanner): @@ -52,3 +53,17 @@ class MatrixScanner(AbstractMatrixScanner): 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) + + return None # The default, but for explicitness diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py index 88a4e19..d2a60c8 100644 --- a/kmk/micropython/pyb_hid.py +++ b/kmk/micropython/pyb_hid.py @@ -3,8 +3,9 @@ import string from pyb import USB_HID, delay -from kmk.common.event_defs import KEY_DOWN_EVENT, KEY_UP_EVENT -from kmk.common.keycodes import Keycodes, char_lookup +from kmk.common.event_defs import HID_REPORT_EVENT +from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, Keycodes, + char_lookup) class HIDHelper: @@ -48,26 +49,19 @@ class HIDHelper: self.clear_all() def _subscription(self, state, action): - if action['type'] == KEY_DOWN_EVENT: - if action['keycode'].code >= 1000: - return + if action['type'] == HID_REPORT_EVENT: + self.clear_all() - 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'].code >= 1000: - return + for key in state.keys_pressed: + if key.code >= FIRST_KMK_INTERNAL_KEYCODE: + continue - if action['keycode'].is_modifier: - self.remove_modifier(action['keycode']) - self.send() - else: - self.remove_key(action['keycode']) - self.send() + if key.is_modifier: + self.add_modifier(key) + else: + self.add_key(key) + + self.send() def send(self): self.logger.debug('Sending HID report: {}'.format(self._evt))