From 0c72554773d49643cf66ceefc9f853fcb8152870 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 01:04:36 -0700 Subject: [PATCH 01/14] Add support for a Planck Rev 6 spidered to a Feather M4 Express --- kmk/matrix.py | 31 +++++++++++++-- .../klardotsh/feather_m4_express/klarank.py | 39 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 user_keymaps/klardotsh/feather_m4_express/klarank.py diff --git a/kmk/matrix.py b/kmk/matrix.py index 0ef14e7..938ef11 100644 --- a/kmk/matrix.py +++ b/kmk/matrix.py @@ -16,6 +16,9 @@ class MatrixScanner: self.cols = cols self.rows = rows + self.len_cols = len(cols) + self.len_rows = len(rows) + self.diode_orientation = diode_orientation self.last_pressed_len = 0 @@ -36,6 +39,18 @@ class MatrixScanner: for pin in self.inputs: pin.switch_to_input(pull=digitalio.Pull.DOWN) + import kmk_keyboard + + self.swap_indicies = getattr(kmk_keyboard, 'swap_indicies', {}) + self.rollover_cols_every_rows = getattr( + kmk_keyboard, + 'rollover_cols_every_rows', + self.len_rows, + ) + + for k, v in self.swap_indicies.items(): + self.swap_indicies[v] = k + def scan_for_pressed(self): pressed = [] @@ -44,9 +59,19 @@ class MatrixScanner: for iidx, ipin in enumerate(self.inputs): if ipin.value(): - pressed.append( - (oidx, iidx) if self.diode_orientation == DiodeOrientation.ROWS else (iidx, oidx) # noqa - ) + if self.diode_orientation == DiodeOrientation.ROWS: + report_tuple = (oidx, iidx) + else: + new_oidx = oidx + self.len_cols * (iidx // self.rollover_cols_every_rows) + new_iidx = iidx - self.rollover_cols_every_rows * ( + iidx // self.rollover_cols_every_rows + ) + report_tuple = (new_iidx, new_oidx) + + if report_tuple in self.swap_indicies: + report_tuple = self.swap_indicies[report_tuple] + + pressed.append(report_tuple) opin.value(False) diff --git a/user_keymaps/klardotsh/feather_m4_express/klarank.py b/user_keymaps/klardotsh/feather_m4_express/klarank.py new file mode 100644 index 0000000..ca479b6 --- /dev/null +++ b/user_keymaps/klardotsh/feather_m4_express/klarank.py @@ -0,0 +1,39 @@ +from kmk.consts import DiodeOrientation, UnicodeModes +from kmk.entrypoints.handwire.circuitpython_samd51 import main +from kmk.keycodes import KC +from kmk.macros.simple import send_string +from kmk.macros.unicode import unicode_string_sequence +from kmk.pins import Pin as P +from kmk.types import AttrDict + +# physical, visible cols (SCK, MO, MI, RX, TX, D4) +# physical, visible rows (10, 11, 12, 13) (9, 6, 5, SCL) +cols = (P.SCK, P.MOSI, P.MISO, P.RX, P.TX, P.D4) +rows = (P.D10, P.D11, P.D12, P.D13, P.D9, P.D6, P.D5, P.SCL) + +swap_indicies = { + (3, 3): (3, 9), + (3, 4): (3, 10), + (3, 5): (3, 11), +} + +rollover_cols_every_rows = 4 + +diode_orientation = DiodeOrientation.COLUMNS + + +# ------------------User level config variables --------------------------------------- +unicode_mode = UnicodeModes.LINUX +debug_enable = True + +keymap = [ + [ + [KC.A, KC.E, KC.I, KC.M, KC.Q, KC.U, KC.N1, KC.N5, KC.N9, KC.HASH, KC.AMPR, KC.UNDS], + [KC.B, KC.F, KC.J, KC.N, KC.R, KC.V, KC.N2, KC.N6, KC.N0, KC.DOLLAR, KC.ASTR, KC.LCBR], + [KC.C, KC.G, KC.K, KC.O, KC.S, KC.W, KC.N3, KC.N7, KC.EXCLAIM, KC.PERCENT, KC.LPRN, KC.RCBR], + [KC.D, KC.H, KC.L, KC.P, KC.T, KC.X, KC.N4, KC.N8, KC.AT, KC.CIRC, KC.RPRN, KC.PIPE], + ], +] + +if __name__ == '__main__': + main() From 16c82b1c0c842a814572be2cb2a1e84c6a3dab28 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 04:04:39 -0700 Subject: [PATCH 02/14] OMEGA REFACTOR! Perf grind basically complete. Resolves #70, Resolves #67 Still needs some regression testing in general, and a definite regression is that rotary encoders are no longer (for the immediate time being) supported. Moves to a much simpler internal state tracking system, and FAR lighter matrix scan. Removes MicroPython support entirely. --- Makefile | 73 +-- kmk/abstract/hid.py | 163 ----- kmk/{abstract => boards}/__init__.py | 0 kmk/boards/klarank.py | 18 + kmk/circuitpython/hid.py | 38 -- kmk/circuitpython/util.py | 17 - kmk/contrib/__init__.py | 0 kmk/entrypoints/__init__.py | 0 kmk/entrypoints/handwire/__init__.py | 0 .../handwire/circuitpython_samd51.py | 36 -- kmk/entrypoints/handwire/pyboard.py | 43 -- kmk/entrypoints/handwire/pyboard_boot.py | 6 - kmk/event_defs.py | 137 ---- kmk/firmware.py | 143 +++-- kmk/hid.py | 194 ++++++ kmk/internal_state.py | 600 ++++++++---------- kmk/keycodes.py | 41 +- kmk/kmktime.py | 8 +- kmk/leader_mode.py | 86 --- kmk/macros/simple.py | 33 +- kmk/macros/unicode.py | 60 +- kmk/matrix.py | 75 ++- kmk/{circuitpython => mcus}/__init__.py | 0 kmk/mcus/circuitpython_samd51.py | 6 + kmk/micropython/__init__.py | 0 kmk/micropython/pyb_hid.py | 39 -- setup.cfg | 2 + upy-freeze.txt | 1 - .../kdb424/handwire_planck_pyboard.py | 96 --- ...handwire_planck_featherm4.py => klanck.py} | 44 +- .../klardotsh/feather_m4_express/fourfour.py | 76 --- .../klardotsh/feather_m4_express/klarank.py | 39 -- .../itsybitsy_m4_express/threethree.py | 92 --- user_keymaps/klardotsh/klarank_featherm4.py | 89 +++ .../klardotsh/threethree_matrix_pyboard.py | 68 -- 35 files changed, 797 insertions(+), 1526 deletions(-) delete mode 100644 kmk/abstract/hid.py rename kmk/{abstract => boards}/__init__.py (100%) create mode 100644 kmk/boards/klarank.py delete mode 100644 kmk/circuitpython/hid.py delete mode 100644 kmk/circuitpython/util.py delete mode 100644 kmk/contrib/__init__.py delete mode 100644 kmk/entrypoints/__init__.py delete mode 100644 kmk/entrypoints/handwire/__init__.py delete mode 100644 kmk/entrypoints/handwire/circuitpython_samd51.py delete mode 100644 kmk/entrypoints/handwire/pyboard.py delete mode 100644 kmk/entrypoints/handwire/pyboard_boot.py delete mode 100644 kmk/event_defs.py create mode 100644 kmk/hid.py delete mode 100644 kmk/leader_mode.py rename kmk/{circuitpython => mcus}/__init__.py (100%) create mode 100644 kmk/mcus/circuitpython_samd51.py delete mode 100644 kmk/micropython/__init__.py delete mode 100644 kmk/micropython/pyb_hid.py delete mode 100644 user_keymaps/kdb424/handwire_planck_pyboard.py rename user_keymaps/kdb424/{handwire_planck_featherm4.py => klanck.py} (79%) delete mode 100644 user_keymaps/klardotsh/feather_m4_express/fourfour.py delete mode 100644 user_keymaps/klardotsh/feather_m4_express/klarank.py delete mode 100644 user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py create mode 100644 user_keymaps/klardotsh/klarank_featherm4.py delete mode 100644 user_keymaps/klardotsh/threethree_matrix_pyboard.py diff --git a/Makefile b/Makefile index 85c4fb2..c543154 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ AMPY_DELAY ?= 1.5 ARDUINO ?= /usr/share/arduino PIPENV ?= $(shell which pipenv) -all: copy-kmk copy-keymap copy-main.py +all: copy-kmk copy-keymap .docker_base: Dockerfile_base @echo "===> Building Docker base image kmkfw/base:${DOCKER_BASE_TAG}" @@ -99,57 +99,6 @@ build/micropython/ports/unix/modules/.kmk_frozen: upy-freeze.txt submodules.toml xargs -I '{}' rsync -ah {} build/micropython/ports/unix/modules/ @touch $@ -freeze-stm32-build-deps: build/micropython/ports/stm32/freeze/.kmk_frozen - -build/micropython/ports/stm32/freeze/.kmk_frozen: upy-freeze.txt submodules.toml - @echo "===> Preparing vendored dependencies for bundling into MicroPython for STM32" - @echo "===> Preparing vendored dependencies for bundling into MicroPython for STM32" >> .build.log - @mkdir -p build/micropython/ports/stm32/freeze/ - @rm -rf build/micropython/ports/stm32/freeze/* - @cat upy-freeze.txt | egrep -v '(^#|^\s*$|^\s*\t*#)' | grep MICROPY | cut -d'|' -f2- | \ - xargs -I '{}' rsync -ah {} build/micropython/ports/stm32/freeze/ - @touch $@ - -micropython-freeze-kmk-stm32: freeze-stm32-build-deps - @echo "===> Preparing KMK source for bundling into MicroPython for STM32" - @echo "===> Preparing KMK source for bundling into MicroPython for STM32" >> .build.log - @rm -rf build/micropython/ports/stm32/freeze/kmk* - @rsync -ah kmk build/micropython/ports/stm32/freeze/ --exclude kmk/circuitpython - -micropython-build-pyboard: - @echo "===> Building MicroPython for STM32 - PYBV11" - @echo "===> Building MicroPython for STM32 - PYBV11" >> .build.log - @pipenv run $(MAKE) -j4 -C build/micropython/ports/stm32/ BOARD=PYBV11 FROZEN_MPY_DIR=freeze all 2>&1 >> .build.log - -micropython-flash-pyboard: micropython-build-pyboard - @echo "===> Flashing MicroPython with KMK and your keymap" - @echo "===> Flashing MicroPython with KMK and your keymap" >> .build.log - @pipenv run $(MAKE) -j4 -C build/micropython/ports/stm32/ BOARD=PYBV11 FROZEN_MPY_DIR=freeze deploy 2>&1 >> .build.log - -ifndef USER_KEYMAP -build-pyboard: - @echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1 - -flash-pyboard: - @echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1 -else -ifndef SKIP_KEYMAP_VALIDATION -build-pyboard: clean-build-log lint micropython-deps micropython-freeze-kmk-stm32 micropython-build-unix -else -build-pyboard: clean-build-log lint micropython-deps micropython-freeze-kmk-stm32 -endif - @echo "===> Preparing keyboard script for bundling into MicroPython for STM32" -ifndef SKIP_KEYMAP_VALIDATION - @MICROPYPATH=./ ./bin/micropython.sh bin/keymap_sanity_check.py ${USER_KEYMAP} -endif - @rsync -ah ${USER_KEYMAP} build/micropython/ports/stm32/freeze/main.py - @rsync -ah main.py build/micropython/ports/stm32/freeze/_main.py - @rsync -ah kmk/entrypoints/handwire/pyboard_boot.py build/micropython/ports/stm32/freeze/_boot.py - @$(MAKE) AMPY_PORT=/dev/ttyACM0 AMPY_BAUD=115200 micropython-build-pyboard - -flash-pyboard: build-pyboard micropython-flash-pyboard -endif - reset-bootloader: @echo "===> Rebooting your board to bootloader (safe to ignore file not found errors)" @-timeout -k 5s 10s $(PIPENV) run ampy -p /dev/ttyACM0 -d ${AMPY_DELAY} -b ${AMPY_BAUD} run util/bootloader.py @@ -173,30 +122,18 @@ copy-kmk: echo "**** MOUNTPOINT must be defined (wherever your CIRCUITPY drive is mounted) ****" && exit 1 endif -ifdef MOUNTPOINT -$(MOUNTPOINT)/main.py: main.py - @echo "===> Copying a basic main.py" - @rsync -rh main.py $@ - @sync - -copy-main.py: $(MOUNTPOINT)/main.py -else -copy-main.py: - echo "**** MOUNTPOINT must be defined (wherever your CIRCUITPY drive is mounted) ****" && exit 1 -endif - ifdef MOUNTPOINT ifndef USER_KEYMAP -$(MOUNTPOINT)/kmk_keyboard.py: +$(MOUNTPOINT)/main.py: @echo "**** USER_KEYMAP must be defined (ex. USER_KEYMAP=user_keymaps/noop.py) ****" && exit 1 else -$(MOUNTPOINT)/kmk_keyboard.py: $(USER_KEYMAP) - @echo "===> Copying your keymap to kmk_keyboard.py" +$(MOUNTPOINT)/main.py: $(USER_KEYMAP) + @echo "===> Copying your keymap to main.py" @rsync -rh $(USER_KEYMAP) $@ @sync endif # USER_KEYMAP -copy-keymap: $(MOUNTPOINT)/kmk_keyboard.py +copy-keymap: $(MOUNTPOINT)/main.py else copy-keymap: echo "**** MOUNTPOINT must be defined (wherever your CIRCUITPY drive is mounted) ****" && exit 1 diff --git a/kmk/abstract/hid.py b/kmk/abstract/hid.py deleted file mode 100644 index 3c92136..0000000 --- a/kmk/abstract/hid.py +++ /dev/null @@ -1,163 +0,0 @@ -import logging - -from kmk.consts import HIDReportTypes -from kmk.event_defs import HID_REPORT_EVENT -from kmk.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode, - ModifierKeycode) - - -class AbstractHidHelper: - REPORT_BYTES = 8 - - 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._evt = bytearray(self.REPORT_BYTES) - self.report_device = memoryview(self._evt)[0:1] - self.report_device[0] = HIDReportTypes.KEYBOARD - - # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view - # is "reserved" and evidently (mostly?) unused. However, other modes (or - # at least consumer, so far) will use this byte, which is the main reason - # this view exists. For KEYBOARD, use report_mods and report_non_mods - self.report_keys = memoryview(self._evt)[1:] - - self.report_mods = memoryview(self._evt)[1:2] - self.report_non_mods = memoryview(self._evt)[3:] - - self.post_init() - - def post_init(self): - pass - - def _subscription(self, state, action): - if action.type == HID_REPORT_EVENT: - self.clear_all() - - consumer_key = None - for key in state.keys_pressed: - if isinstance(key, ConsumerKeycode): - consumer_key = key - break - - reporting_device = self.report_device[0] - needed_reporting_device = HIDReportTypes.KEYBOARD - - if consumer_key: - needed_reporting_device = HIDReportTypes.CONSUMER - - if reporting_device != needed_reporting_device: - # If we are about to change reporting devices, release - # all keys and close our proverbial tab on the existing - # device, or keys will get stuck (mostly when releasing - # media/consumer keys) - self.send() - - self.report_device[0] = needed_reporting_device - - if consumer_key: - self.add_key(consumer_key) - else: - for key in state.keys_pressed: - if key.code >= FIRST_KMK_INTERNAL_KEYCODE: - continue - - if isinstance(key, ModifierKeycode): - self.add_modifier(key) - else: - self.add_key(key) - - if key.has_modifiers: - for mod in key.has_modifiers: - self.add_modifier(mod) - - self.send() - - def hid_send(self, evt): - raise NotImplementedError('hid_send(evt) must be implemented') - - def send(self): - self.logger.debug('Sending HID report: {}'.format(self._evt)) - self.hid_send(self._evt) - - return self - - def clear_all(self): - for idx, _ in enumerate(self.report_keys): - self.report_keys[idx] = 0x00 - - return self - - def clear_non_modifiers(self): - for idx, _ in enumerate(self.report_non_mods): - self.report_non_mods[idx] = 0x00 - - return self - - def add_modifier(self, modifier): - if isinstance(modifier, ModifierKeycode): - if modifier.code == ModifierKeycode.FAKE_CODE: - for mod in modifier.has_modifiers: - self.report_mods[0] |= mod - else: - self.report_mods[0] |= modifier.code - else: - self.report_mods[0] |= modifier - - return self - - def remove_modifier(self, modifier): - if isinstance(modifier, ModifierKeycode): - if modifier.code == ModifierKeycode.FAKE_CODE: - for mod in modifier.has_modifiers: - self.report_mods[0] ^= mod - else: - self.report_mods[0] ^= modifier.code - else: - self.report_mods[0] ^= modifier - - return self - - def add_key(self, key): - # Try to find the first empty slot in the key report, and fill it - placed = False - - where_to_place = self.report_non_mods - - if self.report_device[0] == HIDReportTypes.CONSUMER: - where_to_place = self.report_keys - - for idx, _ in enumerate(where_to_place): - if where_to_place[idx] == 0x00: - where_to_place[idx] = key.code - placed = True - break - - if not placed: - self.logger.warning('Out of space in HID report, could not add key') - - return self - - def remove_key(self, key): - removed = False - - where_to_place = self.report_non_mods - - if self.report_device[0] == HIDReportTypes.CONSUMER: - where_to_place = self.report_keys - - for idx, _ in enumerate(where_to_place): - if where_to_place[idx] == key.code: - where_to_place[idx] = 0x00 - removed = True - - if not removed: - self.logger.warning('Tried to remove key that was not added') - - return self diff --git a/kmk/abstract/__init__.py b/kmk/boards/__init__.py similarity index 100% rename from kmk/abstract/__init__.py rename to kmk/boards/__init__.py diff --git a/kmk/boards/klarank.py b/kmk/boards/klarank.py new file mode 100644 index 0000000..c7b84dc --- /dev/null +++ b/kmk/boards/klarank.py @@ -0,0 +1,18 @@ +from kmk.consts import DiodeOrientation +from kmk.mcus.circuitpython_samd51 import Firmware as _Firmware +from kmk.pins import Pin as P + + +class Firmware(_Firmware): + # physical, visible cols (SCK, MO, MI, RX, TX, D4) + # physical, visible rows (10, 11, 12, 13) (9, 6, 5, SCL) + col_pins = (P.SCK, P.MOSI, P.MISO, P.RX, P.TX, P.D4) + row_pins = (P.D10, P.D11, P.D12, P.D13, P.D9, P.D6, P.D5, P.SCL) + rollover_cols_every_rows = 4 + diode_orientation = DiodeOrientation.COLUMNS + + swap_indicies = { + (3, 3): (3, 9), + (3, 4): (3, 10), + (3, 5): (3, 11), + } diff --git a/kmk/circuitpython/hid.py b/kmk/circuitpython/hid.py deleted file mode 100644 index 281521d..0000000 --- a/kmk/circuitpython/hid.py +++ /dev/null @@ -1,38 +0,0 @@ -import usb_hid -from kmk.abstract.hid import AbstractHidHelper -from kmk.consts import HID_REPORT_SIZES, HIDReportTypes, HIDUsage, HIDUsagePage - - -class HIDHelper(AbstractHidHelper): - REPORT_BYTES = 9 - - def post_init(self): - self.devices = {} - - for device in usb_hid.devices: - if device.usage_page == HIDUsagePage.CONSUMER and device.usage == HIDUsage.CONSUMER: - self.devices[HIDReportTypes.CONSUMER] = device - continue - - if device.usage_page == HIDUsagePage.KEYBOARD and device.usage == HIDUsage.KEYBOARD: - self.devices[HIDReportTypes.KEYBOARD] = device - continue - - if device.usage_page == HIDUsagePage.MOUSE and device.usage == HIDUsage.MOUSE: - self.devices[HIDReportTypes.MOUSE] = device - continue - - if ( - device.usage_page == HIDUsagePage.SYSCONTROL and - device.usage == HIDUsage.SYSCONTROL - ): - self.devices[HIDReportTypes.SYSCONTROL] = device - continue - - def hid_send(self, evt): - # int, can be looked up in HIDReportTypes - reporting_device_const = self.report_device[0] - - return self.devices[reporting_device_const].send_report( - evt[1:HID_REPORT_SIZES[reporting_device_const] + 1], - ) diff --git a/kmk/circuitpython/util.py b/kmk/circuitpython/util.py deleted file mode 100644 index a0575f7..0000000 --- a/kmk/circuitpython/util.py +++ /dev/null @@ -1,17 +0,0 @@ -import time - -import board -import digitalio - - -def feather_red_led_flash(duration=10, rate=0.5): - ''' - Flash the red LED for $duration seconds, alternating every $rate - ''' - - rled = digitalio.DigitalInOut(board.LED1) - rled.direction = digitalio.Direction.OUTPUT - - for cycle in range(duration / rate): - rled.value = cycle % 2 - time.sleep(rate) diff --git a/kmk/contrib/__init__.py b/kmk/contrib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kmk/entrypoints/__init__.py b/kmk/entrypoints/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kmk/entrypoints/handwire/__init__.py b/kmk/entrypoints/handwire/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kmk/entrypoints/handwire/circuitpython_samd51.py b/kmk/entrypoints/handwire/circuitpython_samd51.py deleted file mode 100644 index 3fc33aa..0000000 --- a/kmk/entrypoints/handwire/circuitpython_samd51.py +++ /dev/null @@ -1,36 +0,0 @@ -def main(): - import sys - - from kmk.circuitpython.hid import HIDHelper - from kmk.firmware import Firmware - from kmk.matrix import MatrixScanner - - import kmk_keyboard - - cols = getattr(kmk_keyboard, 'cols') - diode_orientation = getattr(kmk_keyboard, 'diode_orientation') - keymap = getattr(kmk_keyboard, 'keymap') - rows = getattr(kmk_keyboard, 'rows') - - debug_enable = getattr(kmk_keyboard, 'debug_enable', False) - - if debug_enable: - from logging import DEBUG as log_level - else: - from logging import ERROR as log_level - - try: - firmware = Firmware( - keymap=keymap, - row_pins=rows, - col_pins=cols, - diode_orientation=diode_orientation, - log_level=log_level, - matrix_scanner=MatrixScanner, - hid=HIDHelper, - ) - - firmware.go() - except Exception as e: - sys.print_exception(e) - sys.exit(1) diff --git a/kmk/entrypoints/handwire/pyboard.py b/kmk/entrypoints/handwire/pyboard.py deleted file mode 100644 index 3bc80ec..0000000 --- a/kmk/entrypoints/handwire/pyboard.py +++ /dev/null @@ -1,43 +0,0 @@ -import sys - -import gc - -from kmk.firmware import Firmware -from kmk.matrix import MatrixScanner -from kmk.micropython.pyb_hid import HIDHelper - - -def main(): - import kmk_keyboard - cols = getattr(kmk_keyboard, 'cols') - diode_orientation = getattr(kmk_keyboard, 'diode_orientation') - keymap = getattr(kmk_keyboard, 'keymap') - rows = getattr(kmk_keyboard, 'rows') - - debug_enable = getattr(kmk_keyboard, 'debug_enable', False) - - if debug_enable: - from logging import DEBUG as log_level - else: - from logging import ERROR as log_level - - # This will run out of ram at this point unless you manually GC - gc.collect() - - try: - firmware = Firmware( - keymap=keymap, - row_pins=rows, - col_pins=cols, - diode_orientation=diode_orientation, - hid=HIDHelper, - log_level=log_level, - matrix_scanner=MatrixScanner, - ) - # This will run out of ram at this point unless you manually GC - gc.collect() - - firmware.go() - except Exception as e: - sys.print_exception(e) - sys.exit(1) diff --git a/kmk/entrypoints/handwire/pyboard_boot.py b/kmk/entrypoints/handwire/pyboard_boot.py deleted file mode 100644 index 2472478..0000000 --- a/kmk/entrypoints/handwire/pyboard_boot.py +++ /dev/null @@ -1,6 +0,0 @@ -import pyb - -from kmk.micropython.pyb_hid import generate_pyb_hid_descriptor - -# act as a serial device and a KMK device -pyb.usb_mode('VCP+HID', hid=generate_pyb_hid_descriptor()) diff --git a/kmk/event_defs.py b/kmk/event_defs.py deleted file mode 100644 index 5c2678a..0000000 --- a/kmk/event_defs.py +++ /dev/null @@ -1,137 +0,0 @@ -import logging -from collections import namedtuple - -from micropython import const - -from kmk.keycodes import Keycodes -from kmk.util import reset_bootloader - -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) -KEYCODE_UP_EVENT = const(6) -KEYCODE_DOWN_EVENT = const(7) -MACRO_COMPLETE_EVENT = const(8) -PENDING_KEYCODE_POP_EVENT = const(9) - -logger = logging.getLogger(__name__) - - -InitFirmware = namedtuple('InitFirmware', ( - 'type', - 'keymap', - 'row_pins', - 'col_pins', - 'diode_orientation', -)) - -KeyUpDown = namedtuple('KeyUpDown', ('type', 'row', 'col')) -KeycodeUpDown = namedtuple('KeycodeUpDown', ('type', 'keycode')) -NewMatrix = namedtuple('NewMatrix', ('type', 'matrix')) -BareEvent = namedtuple('BareEvent', ('type',)) - -hid_report_event = BareEvent( - type=HID_REPORT_EVENT, -) - - -macro_complete_event = BareEvent( - type=MACRO_COMPLETE_EVENT, -) - - -pending_keycode_pop_event = BareEvent( - type=PENDING_KEYCODE_POP_EVENT, -) - - -def init_firmware(keymap, row_pins, col_pins, diode_orientation): - return InitFirmware( - type=INIT_FIRMWARE_EVENT, - keymap=keymap, - row_pins=row_pins, - col_pins=col_pins, - diode_orientation=diode_orientation, - ) - - -def key_up_event(row, col): - return KeyUpDown( - type=KEY_UP_EVENT, - row=row, - col=col, - ) - - -def key_down_event(row, col): - return KeyUpDown( - type=KEY_DOWN_EVENT, - row=row, - col=col, - ) - - -def keycode_up_event(keycode): - ''' - Press a key by Keycode object, bypassing the keymap. Used mostly for - macros. - ''' - return KeycodeUpDown( - 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 KeycodeUpDown( - type=KEYCODE_DOWN_EVENT, - keycode=keycode, - ) - - -def new_matrix_event(matrix): - return NewMatrix( - type=NEW_MATRIX_EVENT, - matrix=matrix, - ) - - -def matrix_changed(new_pressed): - def _key_pressed(dispatch, get_state): - dispatch(new_matrix_event(new_pressed)) - - state = get_state() - - if state.hid_pending: - dispatch(hid_report_event) - - if Keycodes.KMK.KC_RESET in state.keys_pressed: - reset_bootloader() - - if state.pending_keys: - for key in state.pending_keys: - if not key.no_press: - dispatch(keycode_down_event(key)) - dispatch(hid_report_event) - - if not key.no_release: - dispatch(keycode_up_event(key)) - dispatch(hid_report_event) - - dispatch(pending_keycode_pop_event) - - if state.macro_pending: - macro = state.macro_pending - - for event in macro(state): - dispatch(event) - - dispatch(macro_complete_event) - - return _key_pressed diff --git a/kmk/firmware.py b/kmk/firmware.py index 5e3ed19..52d4298 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -1,70 +1,111 @@ -import logging +# Welcome to RAM and stack size hacks central, I'm your host, klardotsh! +# We really get stuck between a rock and a hard place on CircuitPython +# sometimes: our import structure is deeply nested enough that stuff +# breaks in some truly bizarre ways, including: +# - explicit RuntimeError exceptions, complaining that our +# stack depth is too deep +# +# - silent hard locks of the device (basically unrecoverable without +# UF2 flash if done in main.py, fixable with a reboot if done +# in REPL) +# +# However, there's a hackaround that works for us! Because sys.modules +# caches everything it sees (and future imports will use that cached +# copy of the module), let's take this opportunity _way_ up the import +# chain to import _every single thing_ KMK eventually uses in a normal +# workflow, in order from fewest to least nested dependencies. -from kmk.event_defs import init_firmware -from kmk.internal_state import Store, kmk_reducer -from kmk.leader_mode import LeaderHelper +# First, stuff that has no dependencies, or only C/MPY deps +import collections +import kmk.consts +import kmk.kmktime +import kmk.types + +# Now stuff that depends on the above (and so on) +import kmk.keycodes +import kmk.matrix + +import kmk.hid +import kmk.internal_state + +# GC runs automatically after CircuitPython imports. If we ever go back to +# supporting MicroPython, we'll need a GC here (and probably after each +# chunk of the above) + +# Thanks for sticking around. Now let's do real work, starting below + +import gc +from kmk.consts import LeaderMode, UnicodeModes +from kmk.hid import USB_HID +from kmk.internal_state import InternalState +from kmk.matrix import MatrixScanner class Firmware: - def __init__( - self, keymap, row_pins, col_pins, - diode_orientation, - hid=None, - log_level=logging.NOTSET, - matrix_scanner=None, - ): - assert matrix_scanner is not None - self.matrix_scanner = matrix_scanner + debug_enabled = False - logger = logging.getLogger(__name__) - logger.setLevel(log_level) + keymap = None - import kmk_keyboard - self.encoders = getattr(kmk_keyboard, 'encoders', []) + row_pins = None + col_pins = None + diode_orientation = None - self.hydrated = False + unicode_mode = UnicodeModes.NOOP + tap_time = 300 + leader_mode = LeaderMode.ENTER + leader_dictionary = {} - self.store = Store(kmk_reducer, log_level=log_level) - self.store.subscribe( - lambda state, action: self._subscription(state, action), + hid_helper = USB_HID + + def __init__(self): + self.matrix = MatrixScanner( + cols=self.col_pins, + rows=self.row_pins, + diode_orientation=self.diode_orientation, + rollover_cols_every_rows=getattr(self, 'rollover_cols_every_rows', None), + swap_indicies=getattr(self, 'swap_indicies', None), ) - if hid: - self.hid = hid(store=self.store, log_level=log_level) - else: - logger.warning( - "Must provide a HIDHelper (arg: hid), disabling HID\n" - "Board will run in debug mode", - ) + self._hid_helper_inst = self.hid_helper() - self.leader_helper = LeaderHelper(store=self.store, log_level=log_level) + self._state = InternalState(self) - self.store.dispatch(init_firmware( - keymap=keymap, - row_pins=row_pins, - col_pins=col_pins, - diode_orientation=diode_orientation, - )) - - def _subscription(self, state, action): - if not self.hydrated: - self.matrix = self.matrix_scanner( - state.col_pins, - state.row_pins, - state.diode_orientation, - ) - self.hydrated = True + def _send_hid(self): + self._hid_helper_inst.create_report(self._state.keys_pressed).send() + self._state.resolve_hid() def go(self): + assert self.keymap, 'must define a keymap with at least one row' + assert self.row_pins, 'no GPIO pins defined for matrix rows' + assert self.col_pins, 'no GPIO pins defined for matrix columns' + assert self.diode_orientation is not None, 'diode orientation must be defined' + + if self.debug_enabled: + print("Firin' lazers. Keyboard is booted.") + while True: update = self.matrix.scan_for_pressed() - if update: - self.store.dispatch(update) + if update is not None: + self._state.matrix_changed( + update[0], + update[1], + update[2], + ) - for encoder in self.encoders: - eupdate = encoder.scan() + if self._state.hid_pending: + self._send_hid() - if eupdate: - for event in eupdate: - self.store.dispatch(event) + if self._state.macro_pending: + for key in self._state.macro_pending(self): + if not getattr(key, 'no_press', None): + self._state.force_keycode_down(key) + self._send_hid() + + if not getattr(key, 'no_release', None): + self._state.force_keycode_up(key) + self._send_hid() + + self._state.resolve_macro() + + gc.collect() diff --git a/kmk/hid.py b/kmk/hid.py new file mode 100644 index 0000000..4be6ec6 --- /dev/null +++ b/kmk/hid.py @@ -0,0 +1,194 @@ +from kmk.consts import HID_REPORT_SIZES, HIDReportTypes, HIDUsage, HIDUsagePage +from kmk.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode, + ModifierKeycode) + + +class USB_HID: + REPORT_BYTES = 8 + + def __init__(self): + self._evt = bytearray(self.REPORT_BYTES) + self.report_device = memoryview(self._evt)[0:1] + self.report_device[0] = HIDReportTypes.KEYBOARD + + # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view + # is "reserved" and evidently (mostly?) unused. However, other modes (or + # at least consumer, so far) will use this byte, which is the main reason + # this view exists. For KEYBOARD, use report_mods and report_non_mods + self.report_keys = memoryview(self._evt)[1:] + + self.report_mods = memoryview(self._evt)[1:2] + self.report_non_mods = memoryview(self._evt)[3:] + + self.post_init() + + def post_init(self): + pass + + def create_report(self, keys_pressed): + self.clear_all() + + consumer_key = None + for key in keys_pressed: + if isinstance(key, ConsumerKeycode): + consumer_key = key + break + + reporting_device = self.report_device[0] + needed_reporting_device = HIDReportTypes.KEYBOARD + + if consumer_key: + needed_reporting_device = HIDReportTypes.CONSUMER + + if reporting_device != needed_reporting_device: + # If we are about to change reporting devices, release + # all keys and close our proverbial tab on the existing + # device, or keys will get stuck (mostly when releasing + # media/consumer keys) + self.send() + + self.report_device[0] = needed_reporting_device + + if consumer_key: + self.add_key(consumer_key) + else: + for key in keys_pressed: + if key.code >= FIRST_KMK_INTERNAL_KEYCODE: + continue + + if isinstance(key, ModifierKeycode): + self.add_modifier(key) + else: + self.add_key(key) + + if key.has_modifiers: + for mod in key.has_modifiers: + self.add_modifier(mod) + + return self + + def hid_send(self, evt): + # Don't raise a NotImplementedError so this can serve as our "dummy" HID + # when MCU/board doesn't define one to use (which should almost always be + # the CircuitPython-targeting one, except when unit testing or doing + # something truly bizarre. This will likely change eventually when Bluetooth + # is added) + pass + + def send(self): + self.hid_send(self._evt) + + return self + + def clear_all(self): + for idx, _ in enumerate(self.report_keys): + self.report_keys[idx] = 0x00 + + return self + + def clear_non_modifiers(self): + for idx, _ in enumerate(self.report_non_mods): + self.report_non_mods[idx] = 0x00 + + return self + + def add_modifier(self, modifier): + if isinstance(modifier, ModifierKeycode): + if modifier.code == ModifierKeycode.FAKE_CODE: + for mod in modifier.has_modifiers: + self.report_mods[0] |= mod + else: + self.report_mods[0] |= modifier.code + else: + self.report_mods[0] |= modifier + + return self + + def remove_modifier(self, modifier): + if isinstance(modifier, ModifierKeycode): + if modifier.code == ModifierKeycode.FAKE_CODE: + for mod in modifier.has_modifiers: + self.report_mods[0] ^= mod + else: + self.report_mods[0] ^= modifier.code + else: + self.report_mods[0] ^= modifier + + return self + + def add_key(self, key): + # Try to find the first empty slot in the key report, and fill it + placed = False + + where_to_place = self.report_non_mods + + if self.report_device[0] == HIDReportTypes.CONSUMER: + where_to_place = self.report_keys + + for idx, _ in enumerate(where_to_place): + if where_to_place[idx] == 0x00: + where_to_place[idx] = key.code + placed = True + break + + if not placed: + # TODO what do we do here?...... + pass + + return self + + def remove_key(self, key): + where_to_place = self.report_non_mods + + if self.report_device[0] == HIDReportTypes.CONSUMER: + where_to_place = self.report_keys + + for idx, _ in enumerate(where_to_place): + if where_to_place[idx] == key.code: + where_to_place[idx] = 0x00 + + return self + + +try: + import usb_hid + PLATFORM_CIRCUITPYTHON = True +except ImportError: + PLATFORM_CIRCUITPYTHON = False +else: + class CircuitPythonUSB_HID(USB_HID): + REPORT_BYTES = 9 + + def post_init(self): + self.devices = {} + + for device in usb_hid.devices: + us = device.usage + up = device.usage_page + + if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER: + self.devices[HIDReportTypes.CONSUMER] = device + continue + + if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD: + self.devices[HIDReportTypes.KEYBOARD] = device + continue + + if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE: + self.devices[HIDReportTypes.MOUSE] = device + continue + + if ( + up == HIDUsagePage.SYSCONTROL and + us == HIDUsage.SYSCONTROL + ): + self.devices[HIDReportTypes.SYSCONTROL] = device + continue + + def hid_send(self, evt): + # int, can be looked up in HIDReportTypes + reporting_device_const = self.report_device[0] + + return self.devices[reporting_device_const].send_report( + evt[1:HID_REPORT_SIZES[reporting_device_const] + 1], + ) diff --git a/kmk/internal_state.py b/kmk/internal_state.py index 4effd4c..8e6add2 100644 --- a/kmk/internal_state.py +++ b/kmk/internal_state.py @@ -1,13 +1,5 @@ -import logging -import sys - -from kmk import kmktime -from kmk.consts import DiodeOrientation, LeaderMode, UnicodeModes -from kmk.event_defs import (HID_REPORT_EVENT, INIT_FIRMWARE_EVENT, - KEY_DOWN_EVENT, KEY_UP_EVENT, KEYCODE_DOWN_EVENT, - KEYCODE_UP_EVENT, MACRO_COMPLETE_EVENT, - NEW_MATRIX_EVENT, PENDING_KEYCODE_POP_EVENT) from kmk.keycodes import FIRST_KMK_INTERNAL_KEYCODE, Keycodes, RawKeycodes +from kmk.kmktime import sleep_ms, ticks_diff, ticks_ms GESC_TRIGGERS = { Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT, @@ -15,57 +7,6 @@ GESC_TRIGGERS = { } -class Store: - ''' - A data store very loosely inspired by Redux, but with most of the fancy - functional and immutable abilities unavailable because microcontrollers. - This serves as the event dispatcher at the heart of KMK. All changes to the - state of the keyboard should be triggered by events (see event_defs.py) - dispatched through this store, and listened to (for side-effects or other - handling) by subscription functions. - ''' - def __init__(self, reducer, log_level=logging.NOTSET): - self.reducer = reducer - self.logger = logging.getLogger(__name__) - self.logger.setLevel(log_level) - self.state = self.reducer(logger=self.logger) - self.callbacks = [] - - def dispatch(self, action): - if self.state.preserve_intermediate_states: - self.state._oldstates.append(repr(self.state.to_dict(verbose=True))) - - 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)) - - for cb in self.callbacks: - if cb is not None: - try: - cb(self.state, action) - except Exception as e: - self.logger.error('Callback failed, moving on') - sys.print_exception(e) - - def get_state(self): - return self.state - - def subscribe(self, callback): - self.callbacks.append(callback) - return len(self.callbacks) - 1 - - def unsubscribe(self, idx): - self.callbacks[idx] = None - - class InternalState: keys_pressed = set() pending_keys = set() @@ -73,13 +14,9 @@ class InternalState: leader_pending = None leader_last_len = 0 hid_pending = False - keymap = [] - row_pins = [] - col_pins = [] - matrix = [] - diode_orientation = DiodeOrientation.COLUMNS leader_mode_history = [] active_layers = [0] + reversed_active_layers = list(reversed(active_layers)) start_time = { 'lt': None, 'tg': None, @@ -87,23 +24,31 @@ class InternalState: 'lm': None, 'leader': None, } - _oldstates = [] - def __init__(self, preserve_intermediate_states=False): - import kmk_keyboard - self.unicode_mode = getattr(kmk_keyboard, 'unicode_mode', UnicodeModes.NOOP) - self.tap_time = getattr(kmk_keyboard, 'tap_time', 300) - self.leader_mode = getattr(kmk_keyboard, 'leader_mode', LeaderMode.ENTER) - self.leader_dictionary = getattr(kmk_keyboard, 'leader_dictionary', {}) - self.preserve_intermediate_states = preserve_intermediate_states + def __init__(self, config): + self.config = config - def __enter__(self): - return self + self.leader_mode = config.leader_mode - def __exit__(self, type, value, traceback): - pass + self.internal_key_handlers = { + RawKeycodes.KC_DF: self._layer_df, + RawKeycodes.KC_MO: self._layer_mo, + RawKeycodes.KC_LM: self._layer_lm, + RawKeycodes.KC_LT: self._layer_lt, + RawKeycodes.KC_TG: self._layer_tg, + RawKeycodes.KC_TO: self._layer_to, + RawKeycodes.KC_TT: self._layer_tt, + Keycodes.KMK.KC_GESC.code: self._kc_gesc, + RawKeycodes.KC_UC_MODE: self._kc_uc_mode, + RawKeycodes.KC_MACRO: self._kc_macro, + Keycodes.KMK.KC_LEAD.code: self._kc_lead, + Keycodes.KMK.KC_NO.code: self._kc_no, + } - def to_dict(self, verbose=False): + def __repr__(self): + return 'InternalState({})'.format(self._to_dict()) + + def _to_dict(self): ret = { 'keys_pressed': self.keys_pressed, 'active_layers': self.active_layers, @@ -113,307 +58,280 @@ class InternalState: 'start_time': self.start_time, } - if verbose: - ret.update({ - 'keymap': self.keymap, - 'matrix': self.matrix, - 'col_pins': self.col_pins, - 'row_pins': self.row_pins, - 'diode_orientation': self.diode_orientation, - }) - return ret - def __repr__(self): - return 'InternalState({})'.format(self.to_dict()) + def _find_key_in_map(self, row, col): + # Later-added layers have priority. Sift through the layers + # in reverse order until we find a valid keycode object + for layer in self.reversed_active_layers: + layer_key = self.config.keymap[layer][row][col] - -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() - - if logger is not None: - logger.debug('Reducer received state of None, creating new') - - if action is None: - if logger is not None: - logger.debug('No action received, returning state unmodified') - - return state - - if action.type == NEW_MATRIX_EVENT: - matrix_keys_pressed = { - find_key_in_map(state, row, col) - for row, col, in action.matrix - } - - 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: + if not layer_key or layer_key == Keycodes.KMK.KC_TRNS: continue - 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 changed_key.code >= FIRST_KMK_INTERNAL_KEYCODE: - state = process_internal_key_event( - state, - KEY_DOWN_EVENT, - changed_key, - logger=logger, - ) + if layer_key == Keycodes.KMK.KC_NO: + return layer_key - state.matrix = action.matrix - state.keys_pressed |= pressed - state.keys_pressed -= released - if state.leader_mode % 2 == 1: - state.hid_pending = False + return layer_key + + def matrix_changed(self, row, col, is_pressed): + if self.config.debug_enabled: + print('Matrix changed (col, row, pressed?): {}, {}, {}'.format( + row, col, is_pressed, + )) + + kc_changed = self._find_key_in_map(row, col) + + if kc_changed is None: + print('No key accessible for col, row: {}, {}'.format(row, col)) + return self + + if kc_changed.code >= FIRST_KMK_INTERNAL_KEYCODE: + self._process_internal_key_event( + kc_changed, + is_pressed, + ) else: - state.hid_pending = True + if is_pressed: + self.keys_pressed.add(kc_changed) + else: + self.keys_pressed.discard(kc_changed) - return state + self.hid_pending = True - if action.type == KEYCODE_UP_EVENT: - state.keys_pressed.discard(action.keycode) - return state + if self.leader_mode % 2 == 1: + self._process_leader_mode() - if action.type == KEYCODE_DOWN_EVENT: - state.keys_pressed.add(action.keycode) - return state + return self - if action.type == INIT_FIRMWARE_EVENT: - state.keymap = action.keymap - state.row_pins = action.row_pins - state.col_pins = action.col_pins - state.diode_orientation = action.diode_orientation - return state + def force_keycode_up(self, keycode): + self.keys_pressed.discard(keycode) + self.hid_pending = True + return self - # 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 + def force_keycode_down(self, keycode): + if keycode.code == Keycodes.KMK.KC_MACRO_SLEEP_MS: + sleep_ms(keycode.ms) + else: + self.keys_pressed.add(keycode) + self.hid_pending = True + return self - if action.type == MACRO_COMPLETE_EVENT: - state.macro_pending = None - return state + def pending_key_handled(self): + popped = self.pending_keys.pop() - if action.type == PENDING_KEYCODE_POP_EVENT: - state.pending_keys.pop() - return state + if self.config.debug_enabled: + print('Popped pending key: {}'.format(popped)) - # On unhandled events, log and do not mutate state - logger.warning('Unhandled event! Returning state unmodified.') - return state + return self + def resolve_hid(self): + self.hid_pending = False + return self -def process_internal_key_event(state, action_type, changed_key, logger=None): - if logger is None: - logger = logging.getLogger(__name__) + def resolve_macro(self): + if self.config.debug_enabled: + print('Macro complete!') - # Since the key objects can be chained into new objects - # with, for example, no_press set, always check against - # the underlying code rather than comparing Keycode - # objects + self.macro_pending = None + return self - if changed_key.code == RawKeycodes.KC_DF: - return df(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_MO: - return mo(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_LM: - return lm(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_LT: - return lt(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_TG: - return tg(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_TO: - return to(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_TT: - return tt(state, action_type, changed_key, logger=logger) - elif changed_key.code == Keycodes.KMK.KC_GESC.code: - return grave_escape(state, action_type, logger=logger) - elif changed_key.code == RawKeycodes.KC_UC_MODE: - return unicode_mode(state, action_type, changed_key, logger=logger) - elif changed_key.code == RawKeycodes.KC_MACRO: - return macro(state, action_type, changed_key, logger=logger) - elif changed_key.code == Keycodes.KMK.KC_LEAD.code: - return leader(state) - else: - return state + def _process_internal_key_event(self, changed_key, is_pressed): + # Since the key objects can be chained into new objects + # with, for example, no_press set, always check against + # the underlying code rather than comparing Keycode + # objects + return self.internal_key_handlers[changed_key.code]( + changed_key, is_pressed, + ) -def grave_escape(state, action_type, logger): - if action_type == KEY_DOWN_EVENT: - if any(key in GESC_TRIGGERS for key in state.keys_pressed): - # if Shift is held, KC_GRAVE will become KC_TILDE on OS level - state.keys_pressed.add(Keycodes.Common.KC_GRAVE) - return state + def _layer_df(self, changed_key, is_pressed): + """Switches the default layer""" + if is_pressed: + self.active_layers[0] = changed_key.layer + self.reversed_active_layers = list(reversed(self.active_layers)) - # else return KC_ESC - state.keys_pressed.add(Keycodes.Common.KC_ESCAPE) - return state + return self - elif action_type == KEY_UP_EVENT: - state.keys_pressed.discard(Keycodes.Common.KC_ESCAPE) - state.keys_pressed.discard(Keycodes.Common.KC_GRAVE) - return state - - return state - - -def df(state, action_type, changed_key, logger): - """Switches the default layer""" - if action_type == KEY_DOWN_EVENT: - state.active_layers[0] = changed_key.layer - - return state - - -def mo(state, action_type, 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 != changed_key.layer - ] - elif action_type == KEY_DOWN_EVENT: - state.active_layers.append(changed_key.layer) - - return state - - -def lm(state, action_type, changed_key, logger): - """As MO(layer) but with mod active""" - if action_type == KEY_DOWN_EVENT: - # Sets the timer start and acts like MO otherwise - state.start_time['lm'] = kmktime.ticks_ms() - state.keys_pressed.add(changed_key.kc) - state = mo(state, action_type, changed_key, logger) - elif action_type == KEY_UP_EVENT: - state.keys_pressed.discard(changed_key.kc) - state.start_time['lm'] = None - state = mo(state, action_type, changed_key) - - return state - - -def lt(state, action_type, changed_key, logger): - """Momentarily activates layer if held, sends kc if tapped""" - if action_type == KEY_DOWN_EVENT: - # Sets the timer start and acts like MO otherwise - state.start_time['lt'] = kmktime.ticks_ms() - state = mo(state, action_type, changed_key, logger) - elif action_type == KEY_UP_EVENT: - # On keyup, check timer, and press key if needed. - if state.start_time['lt'] and ( - kmktime.ticks_diff(kmktime.ticks_ms(), state.start_time['lt']) < state.tap_time - ): - state.pending_keys.add(changed_key.kc) - - state.start_time['lt'] = None - state = mo(state, action_type, changed_key, logger) - - return state - - -def tg(state, action_type, changed_key, logger): - """Toggles the layer (enables it if not active, and vise versa)""" - if action_type == KEY_DOWN_EVENT: - if changed_key.layer in state.active_layers: - state.active_layers = [ - layer for layer in state.active_layers + def _layer_mo(self, changed_key, is_pressed): + """Momentarily activates layer, switches off when you let go""" + if is_pressed: + self.active_layers.append(changed_key.layer) + else: + self.active_layers = [ + layer for layer in self.active_layers if layer != changed_key.layer ] - else: - state.active_layers.append(changed_key.layer) - return state + self.reversed_active_layers = list(reversed(self.active_layers)) + return self -def to(state, action_type, changed_key, logger): - """Activates layer and deactivates all other layers""" - if action_type == KEY_DOWN_EVENT: - state.active_layers = [changed_key.layer] + def _layer_lm(self, changed_key, is_pressed): + """As MO(layer) but with mod active""" + self.hid_pending = True - return state - - -def tt(state, action_type, changed_key, logger): - """Momentarily activates layer if held, toggles it if tapped repeatedly""" - # TODO Make this work with tap dance to function more correctly, but technically works. - if action_type == KEY_DOWN_EVENT: - if state.start_time['tt'] is None: + if is_pressed: # Sets the timer start and acts like MO otherwise - state.start_time['tt'] = kmktime.ticks_ms() - state = mo(state, action_type, changed_key, logger) - elif kmktime.ticks_diff(kmktime.ticks_ms(), state.start_time['tt']) < state.tap_time: - state.start_time['tt'] = None - state = tg(state, action_type, changed_key, logger) - elif action_type == KEY_UP_EVENT and ( - state.start_time['tt'] is None or - kmktime.ticks_diff(kmktime.ticks_ms(), state.start_time['tt']) >= state.tap_time - ): - # On first press, works like MO. On second press, does nothing unless let up within - # time window, then acts like TG. - state.start_time['tt'] = None - state = mo(state, action_type, changed_key, logger) + self.start_time['lm'] = ticks_ms() + self.keys_pressed.add(changed_key.kc) + return self.mo(changed_key, is_pressed) - return state + self.keys_pressed.discard(changed_key.kc) + self.start_time['lm'] = None + return self.mo(changed_key, is_pressed) + def _layer_lt(self, changed_key, is_pressed): + """Momentarily activates layer if held, sends kc if tapped""" + if is_pressed: + # Sets the timer start and acts like MO otherwise + self.start_time['lt'] = ticks_ms() + return self.mo(changed_key, is_pressed) -def unicode_mode(state, action_type, changed_key, logger): - if action_type == KEY_DOWN_EVENT: - state.unicode_mode = changed_key.mode + # On keyup, check timer, and press key if needed. + if self.start_time['lt'] and ( + ticks_diff(ticks_ms(), self.start_time['lt']) < self.tap_time + ): + self.hid_pending = True + self.pending_keys.add(changed_key.kc) - return state + self.start_time['lt'] = None + return self.mo(changed_key, is_pressed) + def _layer_tg(self, changed_key, is_pressed): + """Toggles the layer (enables it if not active, and vise versa)""" + if is_pressed: + if changed_key.layer in self.active_layers: + self.active_layers = [ + layer for layer in self.active_layers + if layer != changed_key.layer + ] + else: + self.active_layers.append(changed_key.layer) -def macro(state, action_type, changed_key, logger): - if action_type == KEY_UP_EVENT: - if changed_key.keyup: - state.macro_pending = changed_key.keyup - return state + self.reversed_active_layers = list(reversed(self.active_layers)) - elif action_type == KEY_DOWN_EVENT: - if changed_key.keydown: - state.macro_pending = changed_key.keydown - return state + return self - return state + def _layer_to(self, changed_key, is_pressed): + """Activates layer and deactivates all other layers""" + if is_pressed: + self.active_layers = [changed_key.layer] + self.reversed_active_layers = list(reversed(self.active_layers)) + return self -def leader(state): - if state.leader_mode % 2 == 0: - state.keys_pressed.discard(Keycodes.KMK.KC_LEAD) - # All leader modes are one number higher when activating - state.leader_mode += 1 + def _layer_tt(self, changed_key, is_pressed): + """Momentarily activates layer if held, toggles it if tapped repeatedly""" + # TODO Make this work with tap dance to function more correctly, but technically works. + if is_pressed: + if self.start_time['tt'] is None: + # Sets the timer start and acts like MO otherwise + self.start_time['tt'] = ticks_ms() + return self.mo(changed_key, is_pressed) + elif ticks_diff(ticks_ms(), self.start_time['tt']) < self.tap_time: + self.start_time['tt'] = None + return self.tg(changed_key, is_pressed) + elif ( + self.start_time['tt'] is None or + ticks_diff(ticks_ms(), self.start_time['tt']) >= self.tap_time + ): + # On first press, works like MO. On second press, does nothing unless let up within + # time window, then acts like TG. + self.start_time['tt'] = None + return self.mo(changed_key, is_pressed) - return state + return self + + def _kc_uc_mode(self, changed_key, is_pressed): + if is_pressed: + self.config.unicode_mode = changed_key.mode + + return self + + def _kc_macro(self, changed_key, is_pressed): + if is_pressed: + if changed_key.keyup: + self.macro_pending = changed_key.keyup + else: + if changed_key.keydown: + self.macro_pending = changed_key.keydown + + return self + + def _kc_lead(self, changed_key, is_pressed): + if is_pressed: + self._begin_leader_mode() + + return self + + def _kc_gesc(self, changed_key, is_pressed): + self.hid_pending = True + + if is_pressed: + if GESC_TRIGGERS.intersection(self.keys_pressed): + # if Shift is held, KC_GRAVE will become KC_TILDE on OS level + self.keys_pressed.add(Keycodes.Common.KC_GRAVE) + return self + + # else return KC_ESC + self.keys_pressed.add(Keycodes.Common.KC_ESCAPE) + return self + + self.keys_pressed.discard(Keycodes.Common.KC_ESCAPE) + self.keys_pressed.discard(Keycodes.Common.KC_GRAVE) + return self + + def _kc_no(self, changed_key, is_pressed): + return self + + def _begin_leader_mode(self): + if self.leader_mode % 2 == 0: + self.keys_pressed.discard(Keycodes.KMK.KC_LEAD) + # All leader modes are one number higher when activating + self.leader_mode += 1 + + return self + + def _process_leader_mode(self): + keys_pressed = self.keys_pressed + + if self.leader_last_len and self.leader_mode_history: + history_set = set(self.leader_mode_history) + + keys_pressed = keys_pressed - history_set + + self.leader_last_len = len(self.keys_pressed) + + for key in keys_pressed: + if key == Keycodes.Common.KC_ENT: + # Process the action and remove the extra KC.ENT that was added to get here + + lmh = tuple(self.leader_mode_history) + + if lmh in self.config.leader_dictionary: + self.macro_pending = self.config.leader_dictionary[lmh].keydown + + self._exit_leader_mode() + break + elif key == Keycodes.Common.KC_ESC or key == Keycodes.KMK.KC_GESC: + # Clean self and turn leader mode off. + self._exit_leader_mode() + break + elif key == Keycodes.KMK.KC_LEAD: + break + else: + # Add key if not needing to escape + # This needs replaced later with a proper debounce + self.leader_mode_history.append(key) + + self.hid_pending = False + return self + + def _exit_leader_mode(self): + self.leader_mode_history.clear() + self.leader_mode -= 1 + self.leader_last_len = 0 + self.keys_pressed.clear() + return self diff --git a/kmk/keycodes.py b/kmk/keycodes.py index 6f558f3..a18c029 100644 --- a/kmk/keycodes.py +++ b/kmk/keycodes.py @@ -10,6 +10,45 @@ from kmk.types import AttrDict FIRST_KMK_INTERNAL_KEYCODE = 1000 +kc_lookup_cache = {} + + +def lookup_kc_with_cache(char): + found_code = kc_lookup_cache.get( + char, + getattr(Common, 'KC_{}'.format(char.upper())), + ) + + kc_lookup_cache[char] = found_code + kc_lookup_cache[char.upper()] = found_code + kc_lookup_cache[char.lower()] = found_code + + return found_code + + +def generate_codepoint_keysym_seq(codepoint, expected_length=4): + # To make MacOS and Windows happy, always try to send + # sequences that are of length 4 at a minimum + # On Linux systems, we can happily send longer strings. + # They will almost certainly break on MacOS and Windows, + # but this is a documentation problem more than anything. + # Not sure how to send emojis on Mac/Windows like that, + # though, since (for example) the Canadian flag is assembled + # from two five-character codepoints, 1f1e8 and 1f1e6 + # + # As a bonus, this function can be pretty useful for + # leader dictionary keys as strings. + seq = [Common.KC_0 for _ in range(max(len(codepoint), expected_length))] + + for idx, codepoint_fragment in enumerate(reversed(codepoint)): + seq[-(idx + 1)] = lookup_kc_with_cache(codepoint_fragment) + + return seq + + +def generate_leader_dictionary_seq(string): + return tuple(generate_codepoint_keysym_seq(string, 1)) + class RawKeycodes: ''' @@ -286,7 +325,7 @@ class Common(KeycodeCategory): KC_ENTER = KC_ENT = Keycode(40) KC_ESCAPE = KC_ESC = Keycode(41) - KC_BACKSPACE = KC_BKSP = Keycode(42) + KC_BACKSPACE = KC_BSPC = KC_BKSP = Keycode(42) KC_TAB = Keycode(43) KC_SPACE = KC_SPC = Keycode(44) KC_MINUS = KC_MINS = Keycode(45) diff --git a/kmk/kmktime.py b/kmk/kmktime.py index ea97a9e..1b52a56 100644 --- a/kmk/kmktime.py +++ b/kmk/kmktime.py @@ -1,11 +1,7 @@ import math -try: - import utime as time - USE_UTIME = True -except ImportError: - import time - USE_UTIME = False +import time +USE_UTIME = False def sleep_ms(ms): diff --git a/kmk/leader_mode.py b/kmk/leader_mode.py deleted file mode 100644 index 5b28416..0000000 --- a/kmk/leader_mode.py +++ /dev/null @@ -1,86 +0,0 @@ -import logging - -from kmk.keycodes import Keycodes - - -class LeaderHelper: - """ - Acts as a hid to absorb keypress, and perform macros when a timer - or enter key is pressed depending on the mode set. - """ - 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), - ) - - def _subscription(self, state, action): - """ - Subscribes to the state machine, and dispatches actions based - based on incoming keypresses, or when a timer runs out depending - on the mode. - :param state: - :param action: - :return state: - """ - if state.leader_mode % 2 == 1: - keys_pressed = state.keys_pressed - - if state.leader_last_len and state.leader_mode_history: - history_set = set(state.leader_mode_history) - - keys_pressed = keys_pressed - history_set - - state.leader_last_len = len(state.keys_pressed) - - for key in keys_pressed: - if key == Keycodes.Common.KC_ENT: - # Process the action and remove the extra KC.ENT that was added to get here - state = process(state) - return clean_exit(state) - elif key == Keycodes.Common.KC_ESC or key == Keycodes.KMK.KC_GESC: - # Clean state and turn leader mode off. - return clean_exit(state) - elif key == Keycodes.KMK.KC_LEAD: - return state - else: - # Add key if not needing to escape - # This needs replaced later with a proper debounce - state.leader_mode_history.append(key) - return state - - return state - - -def clean_exit(state): - """ - Cleans up the state and hands the HID control back. - :param state: - :return state: - """ - state.leader_mode_history = [] - state.leader_mode -= 1 - state.leader_last_len = 0 - state.keys_pressed.clear() - return state - - -def process(state): - """ - Checks if there are iny matching sequences of keys, and - performs the macro specified by the user. - :param state: - :param leader_dictionary: - :return state: - """ - lmh = tuple(state.leader_mode_history) - - if lmh in state.leader_dictionary: - state.macro_pending = state.leader_dictionary[lmh].keydown - - state.keys_pressed.clear() - - return state diff --git a/kmk/macros/simple.py b/kmk/macros/simple.py index 69b1a1a..00e9bba 100644 --- a/kmk/macros/simple.py +++ b/kmk/macros/simple.py @@ -1,40 +1,13 @@ import string -from kmk.event_defs import (hid_report_event, keycode_down_event, - keycode_up_event) -from kmk.keycodes import Keycodes, Macro, RawKeycodes, char_lookup +from kmk.keycodes import (Keycodes, Macro, RawKeycodes, char_lookup, + lookup_kc_with_cache) from kmk.kmktime import sleep_ms -kc_lookup_cache = {} - - -def lookup_kc_with_cache(char): - found_code = kc_lookup_cache.get( - char, - getattr(Keycodes.Common, 'KC_{}'.format(char.upper())), - ) - - kc_lookup_cache[char] = found_code - kc_lookup_cache[char.upper()] = found_code - kc_lookup_cache[char.lower()] = found_code - - return found_code - def simple_key_sequence(seq): def _simple_key_sequence(state): - for key in seq: - if key.code == RawKeycodes.KC_MACRO_SLEEP_MS: - sleep_ms(key.ms) - continue - - if not getattr(key, 'no_press', None): - yield keycode_down_event(key) - yield hid_report_event - - if not getattr(key, 'no_release', None): - yield keycode_up_event(key) - yield hid_report_event + return seq return Macro(keydown=_simple_key_sequence) diff --git a/kmk/macros/unicode.py b/kmk/macros/unicode.py index 434cba6..345f264 100644 --- a/kmk/macros/unicode.py +++ b/kmk/macros/unicode.py @@ -1,38 +1,22 @@ from kmk.consts import UnicodeModes -from kmk.event_defs import (hid_report_event, keycode_down_event, - keycode_up_event) -from kmk.keycodes import Common, Macro, Modifiers +from kmk.keycodes import Common, Macro, Modifiers, generate_codepoint_keysym_seq from kmk.macros.simple import lookup_kc_with_cache, simple_key_sequence +from kmk.types import AttrDict from kmk.util import get_wide_ordinal IBUS_KEY_COMBO = Modifiers.KC_LCTRL(Modifiers.KC_LSHIFT(Common.KC_U)) -IBUS_KEY_DOWN = keycode_down_event(IBUS_KEY_COMBO) -IBUS_KEY_UP = keycode_up_event(IBUS_KEY_COMBO) -RALT_DOWN = keycode_down_event(Modifiers.KC_RALT) -RALT_UP = keycode_up_event(Modifiers.KC_RALT) -U_DOWN = keycode_down_event(Common.KC_U) -U_UP = keycode_up_event(Common.KC_U) -ENTER_DOWN = keycode_down_event(Common.KC_ENTER) -ENTER_UP = keycode_up_event(Common.KC_ENTER) -RALT_DOWN_NO_RELEASE = keycode_down_event(Modifiers.KC_RALT(no_release=True)) -RALT_UP_NO_PRESS = keycode_up_event(Modifiers.KC_RALT(no_press=True)) +RALT_KEY = Modifiers.KC_RALT +U_KEY = Common.KC_U +ENTER_KEY = Common.KC_ENTER +RALT_DOWN_NO_RELEASE = Modifiers.KC_RALT(no_release=True) +RALT_UP_NO_PRESS = Modifiers.KC_RALT(no_press=True) -def generate_codepoint_keysym_seq(codepoint): - # To make MacOS and Windows happy, always try to send - # sequences that are of length 4 at a minimum - # On Linux systems, we can happily send longer strings. - # They will almost certainly break on MacOS and Windows, - # but this is a documentation problem more than anything. - # Not sure how to send emojis on Mac/Windows like that, - # though, since (for example) the Canadian flag is assembled - # from two five-character codepoints, 1f1e8 and 1f1e6 - seq = [Common.KC_0 for _ in range(max(len(codepoint), 4))] +def compile_unicode_string_sequences(string_table): + for k, v in string_table.items(): + string_table[k] = unicode_string_sequence(v) - for idx, codepoint_fragment in enumerate(reversed(codepoint)): - seq[-(idx + 1)] = lookup_kc_with_cache(codepoint_fragment) - - return seq + return AttrDict(string_table) def unicode_string_sequence(unistring): @@ -71,23 +55,15 @@ def unicode_codepoint_sequence(codepoints): def _ralt_unicode_sequence(kc_macros, state): for kc_macro in kc_macros: yield RALT_DOWN_NO_RELEASE - yield hid_report_event yield from kc_macro.keydown(state) yield RALT_UP_NO_PRESS - yield hid_report_event def _ibus_unicode_sequence(kc_macros, state): for kc_macro in kc_macros: - yield IBUS_KEY_DOWN - yield hid_report_event - yield IBUS_KEY_UP - yield hid_report_event + yield IBUS_KEY_COMBO yield from kc_macro.keydown(state) - yield ENTER_DOWN - yield hid_report_event - yield ENTER_UP - yield hid_report_event + yield ENTER_KEY def _winc_unicode_sequence(kc_macros, state): @@ -98,12 +74,6 @@ def _winc_unicode_sequence(kc_macros, state): https://github.com/SamHocevar/wincompose ''' for kc_macro in kc_macros: - yield RALT_DOWN - yield hid_report_event - yield RALT_UP - yield hid_report_event - yield U_DOWN - yield hid_report_event - yield U_UP - yield hid_report_event + yield RALT_KEY + yield U_KEY yield from kc_macro.keydown(state) diff --git a/kmk/matrix.py b/kmk/matrix.py index 938ef11..4c16d63 100644 --- a/kmk/matrix.py +++ b/kmk/matrix.py @@ -1,11 +1,15 @@ import digitalio from kmk.consts import DiodeOrientation -from kmk.event_defs import matrix_changed class MatrixScanner: - def __init__(self, cols, rows, diode_orientation=DiodeOrientation.COLUMNS): + def __init__( + self, cols, rows, + diode_orientation=DiodeOrientation.COLUMNS, + rollover_cols_every_rows=None, + swap_indicies=None, + ): # A pin cannot be both a row and column, detect this by combining the # two tuples into a set and validating that the length did not drop # @@ -20,14 +24,15 @@ class MatrixScanner: self.len_rows = len(rows) self.diode_orientation = diode_orientation - self.last_pressed_len = 0 if self.diode_orientation == DiodeOrientation.COLUMNS: self.outputs = self.cols self.inputs = self.rows + self.translate_coords = True elif self.diode_orientation == DiodeOrientation.ROWS: self.outputs = self.rows self.inputs = self.cols + self.translate_coords = False else: raise ValueError('Invalid DiodeOrientation: {}'.format( self.diode_orientation, @@ -39,44 +44,64 @@ class MatrixScanner: for pin in self.inputs: pin.switch_to_input(pull=digitalio.Pull.DOWN) - import kmk_keyboard + self.swap_indicies = {} + if swap_indicies is not None: + for k, v in swap_indicies.items(): + self.swap_indicies[self._intify_coordinate(*k)] = v + self.swap_indicies[self._intify_coordinate(*v)] = k - self.swap_indicies = getattr(kmk_keyboard, 'swap_indicies', {}) - self.rollover_cols_every_rows = getattr( - kmk_keyboard, - 'rollover_cols_every_rows', - self.len_rows, - ) + self.rollover_cols_every_rows = rollover_cols_every_rows + if self.rollover_cols_every_rows is None: + self.rollover_cols_every_rows = self.len_rows - for k, v in self.swap_indicies.items(): - self.swap_indicies[v] = k + self.len_state_arrays = self.len_cols * self.len_rows + self.state = bytearray(self.len_state_arrays) + self.report = bytearray(3) + + def _intify_coordinate(self, row, col): + return row << 8 | col def scan_for_pressed(self): - pressed = [] + ba_idx = 0 + any_changed = False for oidx, opin in enumerate(self.outputs): opin.value(True) for iidx, ipin in enumerate(self.inputs): - if ipin.value(): - if self.diode_orientation == DiodeOrientation.ROWS: - report_tuple = (oidx, iidx) - else: + old_val = self.state[ba_idx] + new_val = ipin.value() + + if old_val != new_val: + if self.translate_coords: new_oidx = oidx + self.len_cols * (iidx // self.rollover_cols_every_rows) new_iidx = iidx - self.rollover_cols_every_rows * ( iidx // self.rollover_cols_every_rows ) - report_tuple = (new_iidx, new_oidx) - if report_tuple in self.swap_indicies: - report_tuple = self.swap_indicies[report_tuple] + self.report[0] = new_iidx + self.report[1] = new_oidx + else: + self.report[0] = oidx + self.report[1] = iidx - pressed.append(report_tuple) + swap_src = self._intify_coordinate(self.report[0], self.report[1]) + if swap_src in self.swap_indicies: + tgt_row, tgt_col = self.swap_indicies[swap_src] + self.report[0] = tgt_row + self.report[1] = tgt_col + + self.report[2] = new_val + self.state[ba_idx] = new_val + any_changed = True + break + + ba_idx += 1 opin.value(False) - if len(pressed) != self.last_pressed_len: - self.last_pressed_len = len(pressed) - return matrix_changed(pressed) + if any_changed: + break - return None # The default, but for explicitness + if any_changed: + return self.report diff --git a/kmk/circuitpython/__init__.py b/kmk/mcus/__init__.py similarity index 100% rename from kmk/circuitpython/__init__.py rename to kmk/mcus/__init__.py diff --git a/kmk/mcus/circuitpython_samd51.py b/kmk/mcus/circuitpython_samd51.py new file mode 100644 index 0000000..7957f26 --- /dev/null +++ b/kmk/mcus/circuitpython_samd51.py @@ -0,0 +1,6 @@ +from kmk.firmware import Firmware as _Firmware +from kmk.hid import CircuitPythonUSB_HID + + +class Firmware(_Firmware): + hid_helper = CircuitPythonUSB_HID diff --git a/kmk/micropython/__init__.py b/kmk/micropython/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py deleted file mode 100644 index 2764277..0000000 --- a/kmk/micropython/pyb_hid.py +++ /dev/null @@ -1,39 +0,0 @@ -from pyb import USB_HID, delay, hid_keyboard - -from kmk.abstract.hid import AbstractHidHelper -from kmk.consts import HID_REPORT_STRUCTURE - - -def generate_pyb_hid_descriptor(): - existing_keyboard = list(hid_keyboard) - existing_keyboard[-1] = HID_REPORT_STRUCTURE - return tuple(existing_keyboard) - - -class HIDHelper(AbstractHidHelper): - # For some bizarre reason this can no longer be 8, it'll just fail to send - # anything. This is almost certainly a bug in the report descriptor sent - # over in the boot process. For now the sacrifice is that we only support - # 5KRO until I figure this out, rather than the 6KRO HID defines. - REPORT_BYTES = 7 - - def post_init(self): - self._hid = USB_HID() - self.hid_send = self._hid.send - - def send(self): - 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(1) - - return self diff --git a/setup.cfg b/setup.cfg index e4505e2..cc95d53 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,8 @@ per-file-ignores = # Allow crazy line lengths, unused variables, and multiple spaces after commas in lists (for grid alignment) user_keymaps/**/*.py: F401,E501,E241 tests/test_data/keymaps/**/*.py: F401,E501 +# Forgive me for my RAM hack sins + kmk/firmware.py: I001,I003,I004,F401 [isort] known_third_party = analogio,bitbangio,bleio,board,busio,digitalio,framebuf,gamepad,gc,microcontroller,micropython,pulseio,pyb,pydux,uio,ubluepy,machine,pyb,uos diff --git a/upy-freeze.txt b/upy-freeze.txt index 2e2157a..1e1b44a 100644 --- a/upy-freeze.txt +++ b/upy-freeze.txt @@ -1,5 +1,4 @@ # CircuitPython provides collections, don't overwrite it MICROPY|vendor/upy-lib/collections/collections -MICROPYCIRCUITPY|vendor/upy-lib/logging/logging.py MICROPYCIRCUITPY|vendor/upy-lib/string/string.py diff --git a/user_keymaps/kdb424/handwire_planck_pyboard.py b/user_keymaps/kdb424/handwire_planck_pyboard.py deleted file mode 100644 index f268306..0000000 --- a/user_keymaps/kdb424/handwire_planck_pyboard.py +++ /dev/null @@ -1,96 +0,0 @@ -import gc - -from kmk.consts import DiodeOrientation, UnicodeModes -from kmk.entrypoints.handwire.pyboard import main -from kmk.keycodes import KC -from kmk.macros.simple import send_string -from kmk.macros.unicode import unicode_string_sequence -from kmk.pins import Pin as P -from kmk.types import AttrDict - -cols = (P.Y12, P.Y11, P.Y10, P.Y9, P.X8, P.X7, P.X6, P.X5, P.X4, P.X3, P.X2, P.X1) -rows = (P.Y1, P.Y2, P.Y3, P.Y4) - -diode_orientation = DiodeOrientation.COLUMNS - - -# ------------------User level config variables --------------------------------------- -unicode_mode = UnicodeModes.LINUX -tap_time = 150 -leader_timeout = 2000 -debug_enable = False - -# -------------------------------Macros ----------------------------------------------- - -gc.collect() -emoticons = AttrDict({ - # Emoticons, but fancier - 'ANGRY_TABLE_FLIP': r'(ノಠ痊ಠ)ノ彡┻━┻', - 'CHEER': r'+。:.゚ヽ(´∀。)ノ゚.:。+゚゚+。:.゚ヽ(*´∀)ノ゚.:。+゚', - 'TABLE_FLIP': r'(╯°□°)╯︵ ┻━┻', - 'WAT': r'⊙.☉', - 'FF': r'凸(゚Д゚#)', - 'F': r'( ̄^ ̄)凸', - 'MEH': r'╮( ̄_ ̄)╭', - 'YAY': r'o(^▽^)o', -}) - -for k, v in emoticons.items(): - emoticons[k] = unicode_string_sequence(v) - -# ---------------------- Leader Key Macros -------------------------------------------- - -gc.collect() -leader_dictionary = { - (KC.F, KC.L, KC.I, KC.P): emoticons.ANGRY_TABLE_FLIP, - (KC.C, KC.H, KC.E, KC.E, KC.R): emoticons.CHEER, - (KC.W, KC.A, KC.T): emoticons.WAT, - (KC.F, KC.F): emoticons.FF, - (KC.F,): emoticons.F, - (KC.M, KC.E, KC.H): emoticons.MEH, - (KC.Y, KC.A, KC.Y): emoticons.YAY, -} - -gc.collect() -WPM = send_string("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum arcu vitae elementum curabitur vitae nunc sed. Facilisis sed odio morbi quis.") - -# ---------------------- Keymap --------------------------------------------------------- - -gc.collect() -keymap = [ - [ - # Default - [KC.GESC, KC.QUOTE, KC.COMMA, KC.DOT, KC.P, KC.Y, KC.F, KC.G, KC.C, KC.R, KC.L, KC.BKSP], - [KC.TAB, KC.A, KC.O, KC.E, KC.U, KC.I, KC.D, KC.H, KC.T, KC.N, KC.S, KC.ENT], - [KC.LSFT, KC.SCLN, KC.Q, KC.J, KC.K, KC.X, KC.B, KC.M, KC.W, KC.V, KC.Z, KC.SLSH], - [KC.LCTRL, KC.LGUI, KC.LALT, KC.LEAD, KC.MO(2), KC.LT(3, KC.SPC), KC.LT(3, KC.SPC), KC.MO(4), KC.LEFT, KC.DOWN, KC.UP, KC.RIGHT], - ], - [ - # Gaming - [KC.TAB, KC.QUOT, KC.COMM, KC.DOT, KC.P, KC.Y, KC.F, KC.G, KC.C, KC.R, KC.L, KC.BKSP], - [KC.ESC, KC.A, KC.O, KC.E, KC.U, KC.I, KC.D, KC.H, KC.T, KC.N, KC.S, KC.ENT], - [KC.LSFT, KC.SCLN, KC.Q, KC.J, KC.K, KC.X, KC.B, KC.M, KC.W, KC.V, KC.Z, KC.SLSH], - [KC.LCTRL, KC.LGUI, KC.LALT, KC.F1, KC.F2, KC.SPC, KC.SPC, KC.MO(4), KC.LEFT, KC.DOWN, KC.UP, KC.RIGHT], - ], - [ - # Raise1 - [KC.TILD, KC.EXLM, KC.AT, KC.HASH, KC.DLR, KC.PERC, KC.CIRC, KC.AMPR, KC.ASTR, KC.LPRN, KC.RPRN, KC.DEL], - [KC.TRNS, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.LBRC, KC.RBRC, KC.BSLS], - [KC.TRNS, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.NO, KC.INS, KC.PGDN, KC.PGUP, KC.MINS], - [KC.RESET, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.NO, KC.NO, KC.EQL, KC.HOME, KC.VOLD, KC.VOLU, KC.END], - ], - [ - # Raise2 - [KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.N7, KC.N8, KC.N9, KC.BKSP], - [KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.N4, KC.N5, KC.N6, KC.NO], - [KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.N1, KC.N2, KC.N3, KC.NO], - [KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.N0, KC.N0, KC.PDOT, KC.ENT], - ], - [ - # Raise3 - [WPM, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.F10, KC.F11, KC.F12, KC.LSHIFT(KC.INS)], - [KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.F7, KC.F8, KC.F9, KC.NO], - [KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.F4, KC.F5, KC.F6, KC.NO], - [KC.DF(0), KC.DF(1), KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.F1, KC.F2, KC.F3, KC.NO], - ], -] diff --git a/user_keymaps/kdb424/handwire_planck_featherm4.py b/user_keymaps/kdb424/klanck.py similarity index 79% rename from user_keymaps/kdb424/handwire_planck_featherm4.py rename to user_keymaps/kdb424/klanck.py index 184661e..5b80640 100644 --- a/user_keymaps/kdb424/handwire_planck_featherm4.py +++ b/user_keymaps/kdb424/klanck.py @@ -1,24 +1,27 @@ from kmk.consts import DiodeOrientation, UnicodeModes from kmk.entrypoints.handwire.circuitpython_samd51 import main from kmk.keycodes import KC +from kmk.keycodes import generate_leader_dictionary_seq as glds from kmk.macros.simple import send_string -from kmk.macros.unicode import unicode_string_sequence +from kmk.macros.unicode import compile_unicode_string_sequences +from kmk.mcus.circuitpython_samd51 import Firmware from kmk.pins import Pin as P from kmk.types import AttrDict -cols = (P.A0, P.A1, P.A2, P.A3, P.A4, P.A5, P.SCK, P.MOSI, P.MISO, P.RX, P.TX, P.D4) -rows = (P.D10, P.D11, P.D12, P.D13) +keyboard = Firmware() -diode_orientation = DiodeOrientation.COLUMNS +keyboard.col_pins = (P.A0, P.A1, P.A2, P.A3, P.A4, P.A5, P.SCK, P.MOSI, P.MISO, P.RX, P.TX, P.D4) +keyboard.row_pins = (P.D10, P.D11, P.D12, P.D13) +keyboard.diode_orientation = DiodeOrientation.COLUMNS # ------------------User level config variables --------------------------------------- -unicode_mode = UnicodeModes.LINUX -tap_time = 200 -leader_timeout = 2000 -debug_enable = True +keyboard.unicode_mode = UnicodeModes.LINUX +keyboard.tap_time = 200 +keyboard.leader_timeout = 2000 +keyboard.debug_enabled = True -emoticons = AttrDict({ +emoticons = compile_unicode_string_sequences({ # Emoticons, but fancier 'ANGRY_TABLE_FLIP': r'(ノಠ痊ಠ)ノ彡┻━┻', 'CHEER': r'+。:.゚ヽ(´∀。)ノ゚.:。+゚゚+。:.゚ヽ(*´∀)ノ゚.:。+゚', @@ -30,26 +33,23 @@ emoticons = AttrDict({ 'YAY': r'o(^▽^)o', }) -for k, v in emoticons.items(): - emoticons[k] = unicode_string_sequence(v) - # ---------------------- Leader Key Macros -------------------------------------------- -leader_dictionary = { - (KC.F, KC.L, KC.I, KC.P): emoticons.ANGRY_TABLE_FLIP, - (KC.C, KC.H, KC.E, KC.E, KC.R): emoticons.CHEER, - (KC.W, KC.A, KC.T): emoticons.WAT, - (KC.F, KC.F): emoticons.FF, - (KC.F,): emoticons.F, - (KC.M, KC.E, KC.H): emoticons.MEH, - (KC.Y, KC.A, KC.Y): emoticons.YAY, +keyboard.leader_dictionary = { + glds('flip'): emoticons.ANGRY_TABLE_FLIP, + glds('cheer'): emoticons.CHEER, + glds('wat'): emoticons.WAT, + glds('ff'): emoticons.FF, + glds('f'): emoticons.F, + glds('meh'): emoticons.MEH, + glds('yay'): emoticons.YAY, } WPM = send_string("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum arcu vitae elementum curabitur vitae nunc sed. Facilisis sed odio morbi quis.") # ---------------------- Keymap --------------------------------------------------------- -keymap = [ +keyboard.keymap = [ [ # Default [KC.GESC, KC.QUOTE, KC.COMMA, KC.DOT, KC.P, KC.Y, KC.F, KC.G, KC.C, KC.R, KC.L, KC.BKSP], @@ -88,4 +88,4 @@ keymap = [ ] if __name__ == '__main__': - main() + keyboard.go() diff --git a/user_keymaps/klardotsh/feather_m4_express/fourfour.py b/user_keymaps/klardotsh/feather_m4_express/fourfour.py deleted file mode 100644 index a50fa83..0000000 --- a/user_keymaps/klardotsh/feather_m4_express/fourfour.py +++ /dev/null @@ -1,76 +0,0 @@ -from kmk.consts import DiodeOrientation, UnicodeModes -from kmk.entrypoints.handwire.circuitpython_samd51 import main -from kmk.firmware import Firmware -from kmk.keycodes import KC -from kmk.macros.simple import send_string, simple_key_sequence -from kmk.macros.unicode import unicode_codepoint_sequence -from kmk.pins import Pin as P - -cols = (P.D11, P.D10, P.D9) -rows = (P.A2, P.A3, P.A4, P.A5) - -diode_orientation = DiodeOrientation.COLUMNS -unicode_mode = UnicodeModes.LINUX - -MACRO_TEST_SIMPLE = simple_key_sequence([ - KC.LSHIFT(KC.H), - KC.E, - KC.L, - KC.L, - KC.O, - - KC.SPACE, - - KC.MACRO_SLEEP_MS(500), - - KC.LSHIFT(KC.K), - KC.LSHIFT(KC.M), - KC.LSHIFT(KC.K), - KC.EXCLAIM, -]) - -MACRO_TEST_STRING = send_string("Hello! from, uhhhh, send_string | and some other WEIRD STUFF` \\ like this' \"\t[]") - -ANGRY_TABLE_FLIP = unicode_codepoint_sequence([ - "28", - "30ce", - "ca0", - "75ca", - "ca0", - "29", - "30ce", - "5f61", - "253b", - "2501", - "253b", -]) - -keymap = [ - [ - [KC.GESC, KC.A, KC.RESET], - [KC.MO(1), KC.B, KC.MUTE], - [KC.LT(2, KC.EXCLAIM), KC.HASH, KC.ENTER], - [KC.TT(3), KC.SPACE, ANGRY_TABLE_FLIP], - ], - [ - [KC.TRNS, KC.B, KC.C], - [KC.NO, KC.D, KC.E], - [KC.F, KC.G, KC.H], - [KC.I, KC.J, KC.K], - ], - [ - [KC.VOLU, KC.MUTE, ANGRY_TABLE_FLIP], - [KC.TRNS, KC.PIPE, MACRO_TEST_SIMPLE], - [KC.VOLD, KC.P, MACRO_TEST_STRING], - [KC.L, KC.M, KC.N], - ], - [ - [KC.NO, KC.UC_MODE_NOOP, KC.C], - [KC.NO, KC.UC_MODE_LINUX, KC.E], - [KC.TRNS, KC.UC_MODE_MACOS, KC.H], - [KC.O, KC.P, KC.Q], - ], -] - -if __name__ == '__main__': - main() diff --git a/user_keymaps/klardotsh/feather_m4_express/klarank.py b/user_keymaps/klardotsh/feather_m4_express/klarank.py deleted file mode 100644 index ca479b6..0000000 --- a/user_keymaps/klardotsh/feather_m4_express/klarank.py +++ /dev/null @@ -1,39 +0,0 @@ -from kmk.consts import DiodeOrientation, UnicodeModes -from kmk.entrypoints.handwire.circuitpython_samd51 import main -from kmk.keycodes import KC -from kmk.macros.simple import send_string -from kmk.macros.unicode import unicode_string_sequence -from kmk.pins import Pin as P -from kmk.types import AttrDict - -# physical, visible cols (SCK, MO, MI, RX, TX, D4) -# physical, visible rows (10, 11, 12, 13) (9, 6, 5, SCL) -cols = (P.SCK, P.MOSI, P.MISO, P.RX, P.TX, P.D4) -rows = (P.D10, P.D11, P.D12, P.D13, P.D9, P.D6, P.D5, P.SCL) - -swap_indicies = { - (3, 3): (3, 9), - (3, 4): (3, 10), - (3, 5): (3, 11), -} - -rollover_cols_every_rows = 4 - -diode_orientation = DiodeOrientation.COLUMNS - - -# ------------------User level config variables --------------------------------------- -unicode_mode = UnicodeModes.LINUX -debug_enable = True - -keymap = [ - [ - [KC.A, KC.E, KC.I, KC.M, KC.Q, KC.U, KC.N1, KC.N5, KC.N9, KC.HASH, KC.AMPR, KC.UNDS], - [KC.B, KC.F, KC.J, KC.N, KC.R, KC.V, KC.N2, KC.N6, KC.N0, KC.DOLLAR, KC.ASTR, KC.LCBR], - [KC.C, KC.G, KC.K, KC.O, KC.S, KC.W, KC.N3, KC.N7, KC.EXCLAIM, KC.PERCENT, KC.LPRN, KC.RCBR], - [KC.D, KC.H, KC.L, KC.P, KC.T, KC.X, KC.N4, KC.N8, KC.AT, KC.CIRC, KC.RPRN, KC.PIPE], - ], -] - -if __name__ == '__main__': - main() diff --git a/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py b/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py deleted file mode 100644 index 47219c9..0000000 --- a/user_keymaps/klardotsh/itsybitsy_m4_express/threethree.py +++ /dev/null @@ -1,92 +0,0 @@ -from kmk.consts import DiodeOrientation, UnicodeModes -from kmk.entrypoints.handwire.circuitpython_samd51 import main -from kmk.firmware import Firmware -from kmk.keycodes import KC -from kmk.macros.rotary_encoder import VolumeRotaryEncoder -from kmk.macros.simple import send_string, simple_key_sequence -from kmk.macros.unicode import unicode_string_sequence -from kmk.pins import Pin as P -from kmk.types import AttrDict - -DEBUG_ENABLE = True - -cols = (P.A4, P.A5, P.D7) -rows = (P.D12, P.D11, P.D10) - -diode_orientation = DiodeOrientation.COLUMNS -unicode_mode = UnicodeModes.LINUX - -encoders = [ - VolumeRotaryEncoder(P.A3, P.A2, 6, 0.6), -] - -emoticons = AttrDict({ - # Emojis - 'BEER': r'🍺', - 'BEER_TOAST': r'🍻', - 'FACE_CUTE_SMILE': r'😊', - 'FACE_HEART_EYES': r'😍', - 'FACE_JOY': r'😂', - 'FACE_SWEAT_SMILE': r'😅', - 'FACE_THINKING': r'🤔', - 'FIRE': r'🔥', - 'FLAG_CA': r'🇨🇦', - 'FLAG_US': r'🇺🇸', - 'HAND_CLAP': r'👏', - 'HAND_HORNS': r'🤘', - 'HAND_OK': r'👌', - 'HAND_THUMB_DOWN': r'👎', - 'HAND_THUMB_UP': r'👍', - 'HAND_WAVE': r'👋', - 'HEART': r'❤️', - 'MAPLE_LEAF': r'🍁', - 'POOP': r'💩', - 'TADA': r'🎉', - - # Emoticons, but fancier - 'ANGRY_TABLE_FLIP': r'(ノಠ痊ಠ)ノ彡┻━┻', - 'CELEBRATORY_GLITTER': r'+。:.゚ヽ(´∀。)ノ゚.:。+゚゚+。:.゚ヽ(*´∀)ノ゚.:。+゚', - 'SHRUGGIE': r'¯\_(ツ)_/¯', - 'TABLE_FLIP': r'(╯°□°)╯︵ ┻━┻', -}) - -for k, v in emoticons.items(): - emoticons[k] = unicode_string_sequence(v) - -MACRO_HELLO_WORLD = simple_key_sequence([ - KC.LSHIFT(KC.H), - KC.E, - KC.L, - KC.L, - KC.O, - - KC.SPACE, - - KC.MACRO_SLEEP_MS(500), - - KC.LSHIFT(KC.K), - KC.LSHIFT(KC.M), - KC.LSHIFT(KC.K), - KC.EXCLAIM, -]) - -keymap = [ - [ - [KC.GESC, KC.HYPR, KC.RESET], - [KC.MO(1), KC.B, KC.MUTE], - [KC.LT(2, KC.EXCLAIM), KC.HASH, KC.ENTER], - ], - [ - [KC.MUTE, KC.B, KC.C], - [KC.TRNS, KC.D, KC.E], - [KC.F, KC.G, KC.H], - ], - [ - [emoticons.CELEBRATORY_GLITTER, emoticons.SHRUGGIE, emoticons.ANGRY_TABLE_FLIP], - [emoticons.BEER, emoticons.FLAG_CA, emoticons.FLAG_US], - [KC.TRNS, KC.P, MACRO_HELLO_WORLD], - ], -] - -if __name__ == '__main__': - main() diff --git a/user_keymaps/klardotsh/klarank_featherm4.py b/user_keymaps/klardotsh/klarank_featherm4.py new file mode 100644 index 0000000..ecf90bd --- /dev/null +++ b/user_keymaps/klardotsh/klarank_featherm4.py @@ -0,0 +1,89 @@ +from kmk.boards.klarank import Firmware +from kmk.consts import UnicodeModes +from kmk.keycodes import KC +from kmk.keycodes import generate_leader_dictionary_seq as glds +from kmk.macros.simple import send_string +from kmk.macros.unicode import compile_unicode_string_sequences as cuss + +keyboard = Firmware() + +keyboard.debug_enabled = True +keyboard.unicode_mode = UnicodeModes.LINUX + +_______ = KC.TRNS +xxxxxxx = KC.NO + +emoticons = cuss({ + # Emojis + 'BEER': r'🍺', + 'BEER_TOAST': r'🍻', + 'FACE_CUTE_SMILE': r'😊', + 'FACE_HEART_EYES': r'😍', + 'FACE_JOY': r'😂', + 'FACE_SWEAT_SMILE': r'😅', + 'FACE_THINKING': r'🤔', + 'FIRE': r'🔥', + 'FLAG_CA': r'🇨🇦', + 'FLAG_US': r'🇺🇸', + 'HAND_CLAP': r'👏', + 'HAND_HORNS': r'🤘', + 'HAND_OK': r'👌', + 'HAND_THUMB_DOWN': r'👎', + 'HAND_THUMB_UP': r'👍', + 'HAND_WAVE': r'👋', + 'HEART': r'❤️', + 'MAPLE_LEAF': r'🍁', + 'POOP': r'💩', + 'TADA': r'🎉', + + # Emoticons, but fancier + 'ANGRY_TABLE_FLIP': r'(ノಠ痊ಠ)ノ彡┻━┻', + 'CELEBRATORY_GLITTER': r'+。:.゚ヽ(´∀。)ノ゚.:。+゚゚+。:.゚ヽ(*´∀)ノ゚.:。+゚', + 'SHRUGGIE': r'¯\_(ツ)_/¯', + 'TABLE_FLIP': r'(╯°□°)╯︵ ┻━┻', +}) + +WPM = send_string("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum arcu vitae elementum curabitur vitae nunc sed. Facilisis sed odio morbi quis.") + +keyboard.leader_dictionary = { + glds('hello'): send_string('hello world from kmk macros'), + glds('wpm'): WPM, + glds('atf'): emoticons.ANGRY_TABLE_FLIP, + glds('tf'): emoticons.TABLE_FLIP, + glds('fca'): emoticons.FLAG_CA, + glds('fus'): emoticons.FLAG_US, + glds('cel'): emoticons.CELEBRATORY_GLITTER, +} + +keyboard.keymap = [ + [ + [KC.GESC, KC.QUOT, KC.COMM, KC.DOT, KC.P, KC.Y, KC.F, KC.G, KC.C, KC.R, KC.L, KC.BSPC], + [KC.TAB, KC.A, KC.O, KC.E, KC.U, KC.I, KC.D, KC.H, KC.T, KC.N, KC.S, KC.ENT], + [KC.LGUI, KC.SCLN, KC.Q, KC.J, KC.K, KC.X, KC.B, KC.M, KC.W, KC.V, KC.Z, KC.LALT], + [KC.LCTL, KC.LEAD, KC.LSHIFT(KC.LGUI), KC.MO(2), KC.MO(3), KC.LSFT, KC.SPC, KC.MO(1), KC.LEFT, KC.DOWN, KC.UP, KC.RGHT], + ], + + [ + [KC.GESC, xxxxxxx, xxxxxxx, KC.F10, KC.F11, KC.F12, xxxxxxx, KC.PSLS, KC.N7, KC.N8, KC.N9, KC.BSPC], + [KC.TAB, xxxxxxx, xxxxxxx, KC.F7, KC.F8, KC.F9, xxxxxxx, KC.PAST, KC.N4, KC.N5, KC.N6, _______], + [KC.LGUI, xxxxxxx, xxxxxxx, KC.F4, KC.F5, KC.F6, xxxxxxx, KC.PMNS, KC.N1, KC.N2, KC.N3, _______], + [KC.LCTL, xxxxxxx, _______, KC.F1, KC.F2, KC.F3, KC.SPC, _______, KC.N0, KC.DOT, xxxxxxx, KC.EQL], + ], + + [ + [KC.GESC, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.BSLS, KC.LBRC, KC.RBRC, KC.DEL], + [KC.TAB, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.MINS], + [KC.LGUI, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.LBRC, xxxxxxx, xxxxxxx, KC.INS], + [KC.LCTL, xxxxxxx, _______, _______, xxxxxxx, _______, xxxxxxx, xxxxxxx, KC.HOME, KC.PGDN, KC.PGUP, KC.END], + ], + + [ + [KC.GRV, KC.EXLM, KC.AT, KC.HASH, KC.DLR, KC.PERC, KC.CIRC, KC.AMPR, KC.ASTR, KC.LPRN, KC.RPRN, KC.SLSH], + [KC.TAB, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.MINS], + [KC.LGUI, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx], + [KC.LCTL, xxxxxxx, xxxxxxx, xxxxxxx, _______, _______, xxxxxxx, xxxxxxx, KC.MUTE, KC.VOLD, KC.VOLU, xxxxxxx], + ], +] + +if __name__ == '__main__': + keyboard.go() diff --git a/user_keymaps/klardotsh/threethree_matrix_pyboard.py b/user_keymaps/klardotsh/threethree_matrix_pyboard.py deleted file mode 100644 index 2a6f389..0000000 --- a/user_keymaps/klardotsh/threethree_matrix_pyboard.py +++ /dev/null @@ -1,68 +0,0 @@ -from kmk.consts import DiodeOrientation, UnicodeModes -from kmk.entrypoints.handwire.pyboard import main -from kmk.keycodes import KC -from kmk.macros.simple import send_string, simple_key_sequence -from kmk.macros.unicode import unicode_codepoint_sequence -from kmk.pins import Pin as P - -cols = (P.X10, P.X11, P.X12) -rows = (P.X1, P.X2, P.X3) - -diode_orientation = DiodeOrientation.COLUMNS -unicode_mode = UnicodeModes.LINUX - -MACRO_TEST_SIMPLE = simple_key_sequence([ - KC.LSHIFT(KC.H), - KC.E, - KC.L, - KC.L, - KC.O, - - KC.SPACE, - - KC.MACRO_SLEEP_MS(500), - - KC.LSHIFT(KC.K), - KC.LSHIFT(KC.M), - KC.LSHIFT(KC.K), - KC.EXCLAIM, -]) - -MACRO_TEST_STRING = send_string("Hello! from, uhhhh, send_string | and some other WEIRD STUFF` \\ like this' \"\t[]") - -ANGRY_TABLE_FLIP = unicode_codepoint_sequence([ - "28", - "30ce", - "ca0", - "75ca", - "ca0", - "29", - "30ce", - "5f61", - "253b", - "2501", - "253b", -]) - -keymap = [ - [ - [KC.MO(1), KC.GESC, KC.RESET], - [KC.LT(2, KC.EXCLAIM), KC.HASH, KC.ENTER], - [KC.TT(3), KC.SPACE, KC.LSHIFT], - ], - [ - [KC.TRNS, KC.B, KC.C], - [KC.NO, KC.D, KC.E], - [KC.F, KC.G, KC.H], - ], - [ - [KC.VOLU, KC.MUTE, ANGRY_TABLE_FLIP], - [KC.TRNS, KC.PIPE, MACRO_TEST_SIMPLE], - [KC.VOLD, KC.P, MACRO_TEST_STRING], - ], - [ - [KC.NO, KC.UC_MODE_NOOP, KC.C], - [KC.NO, KC.UC_MODE_LINUX, KC.E], - [KC.TRNS, KC.UC_MODE_MACOS, KC.H], - ], -] From 0d94bf4c06c39fc3bcf0fd9118b55e333f1eb151 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 22:30:33 -0700 Subject: [PATCH 03/14] Maybe surface errors in the right order --- kmk/firmware.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/kmk/firmware.py b/kmk/firmware.py index 52d4298..c29f5c5 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -58,16 +58,6 @@ class Firmware: hid_helper = USB_HID def __init__(self): - self.matrix = MatrixScanner( - cols=self.col_pins, - rows=self.row_pins, - diode_orientation=self.diode_orientation, - rollover_cols_every_rows=getattr(self, 'rollover_cols_every_rows', None), - swap_indicies=getattr(self, 'swap_indicies', None), - ) - - self._hid_helper_inst = self.hid_helper() - self._state = InternalState(self) def _send_hid(self): @@ -80,6 +70,16 @@ class Firmware: assert self.col_pins, 'no GPIO pins defined for matrix columns' assert self.diode_orientation is not None, 'diode orientation must be defined' + self.matrix = MatrixScanner( + cols=self.col_pins, + rows=self.row_pins, + diode_orientation=self.diode_orientation, + rollover_cols_every_rows=getattr(self, 'rollover_cols_every_rows', None), + swap_indicies=getattr(self, 'swap_indicies', None), + ) + + self._hid_helper_inst = self.hid_helper() + if self.debug_enabled: print("Firin' lazers. Keyboard is booted.") From d042b458f00e0135e50c996510956b3c238a13af Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 22:36:01 -0700 Subject: [PATCH 04/14] Fix Kyle board, and unbreak MO-dependent layers --- kmk/internal_state.py | 12 ++++++------ user_keymaps/kdb424/klanck.py | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/kmk/internal_state.py b/kmk/internal_state.py index 8e6add2..8746b00 100644 --- a/kmk/internal_state.py +++ b/kmk/internal_state.py @@ -176,18 +176,18 @@ class InternalState: # Sets the timer start and acts like MO otherwise self.start_time['lm'] = ticks_ms() self.keys_pressed.add(changed_key.kc) - return self.mo(changed_key, is_pressed) + return self._layer_mo(changed_key, is_pressed) self.keys_pressed.discard(changed_key.kc) self.start_time['lm'] = None - return self.mo(changed_key, is_pressed) + return self._layer_mo(changed_key, is_pressed) def _layer_lt(self, changed_key, is_pressed): """Momentarily activates layer if held, sends kc if tapped""" if is_pressed: # Sets the timer start and acts like MO otherwise self.start_time['lt'] = ticks_ms() - return self.mo(changed_key, is_pressed) + return self._layer_mo(changed_key, is_pressed) # On keyup, check timer, and press key if needed. if self.start_time['lt'] and ( @@ -197,7 +197,7 @@ class InternalState: self.pending_keys.add(changed_key.kc) self.start_time['lt'] = None - return self.mo(changed_key, is_pressed) + return self._layer_mo(changed_key, is_pressed) def _layer_tg(self, changed_key, is_pressed): """Toggles the layer (enables it if not active, and vise versa)""" @@ -229,7 +229,7 @@ class InternalState: if self.start_time['tt'] is None: # Sets the timer start and acts like MO otherwise self.start_time['tt'] = ticks_ms() - return self.mo(changed_key, is_pressed) + return self._layer_mo(changed_key, is_pressed) elif ticks_diff(ticks_ms(), self.start_time['tt']) < self.tap_time: self.start_time['tt'] = None return self.tg(changed_key, is_pressed) @@ -240,7 +240,7 @@ class InternalState: # On first press, works like MO. On second press, does nothing unless let up within # time window, then acts like TG. self.start_time['tt'] = None - return self.mo(changed_key, is_pressed) + return self._layer_mo(changed_key, is_pressed) return self diff --git a/user_keymaps/kdb424/klanck.py b/user_keymaps/kdb424/klanck.py index 5b80640..8d3c9f4 100644 --- a/user_keymaps/kdb424/klanck.py +++ b/user_keymaps/kdb424/klanck.py @@ -1,5 +1,4 @@ from kmk.consts import DiodeOrientation, UnicodeModes -from kmk.entrypoints.handwire.circuitpython_samd51 import main from kmk.keycodes import KC from kmk.keycodes import generate_leader_dictionary_seq as glds from kmk.macros.simple import send_string @@ -17,7 +16,7 @@ keyboard.diode_orientation = DiodeOrientation.COLUMNS # ------------------User level config variables --------------------------------------- keyboard.unicode_mode = UnicodeModes.LINUX -keyboard.tap_time = 200 +keyboard.tap_time = 900 keyboard.leader_timeout = 2000 keyboard.debug_enabled = True From bf500d37ff7edfb00d5880cbe260249a20f9dd9b Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 22:39:17 -0700 Subject: [PATCH 05/14] unbreak tap time --- kmk/internal_state.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kmk/internal_state.py b/kmk/internal_state.py index 8746b00..17dbe42 100644 --- a/kmk/internal_state.py +++ b/kmk/internal_state.py @@ -53,7 +53,7 @@ class InternalState: 'keys_pressed': self.keys_pressed, 'active_layers': self.active_layers, 'unicode_mode': self.unicode_mode, - 'tap_time': self.tap_time, + 'tap_time': self.config.tap_time, 'leader_mode_history': self.leader_mode_history, 'start_time': self.start_time, } @@ -191,7 +191,7 @@ class InternalState: # On keyup, check timer, and press key if needed. if self.start_time['lt'] and ( - ticks_diff(ticks_ms(), self.start_time['lt']) < self.tap_time + ticks_diff(ticks_ms(), self.start_time['lt']) < self.config.tap_time ): self.hid_pending = True self.pending_keys.add(changed_key.kc) @@ -230,12 +230,12 @@ class InternalState: # Sets the timer start and acts like MO otherwise self.start_time['tt'] = ticks_ms() return self._layer_mo(changed_key, is_pressed) - elif ticks_diff(ticks_ms(), self.start_time['tt']) < self.tap_time: + elif ticks_diff(ticks_ms(), self.start_time['tt']) < self.config.tap_time: self.start_time['tt'] = None return self.tg(changed_key, is_pressed) elif ( self.start_time['tt'] is None or - ticks_diff(ticks_ms(), self.start_time['tt']) >= self.tap_time + ticks_diff(ticks_ms(), self.start_time['tt']) >= self.config.tap_time ): # On first press, works like MO. On second press, does nothing unless let up within # time window, then acts like TG. From e413392826da8c5d9c13d2cedf60d8c7d03bc1bf Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 22:40:36 -0700 Subject: [PATCH 06/14] Remove pyboard from circle --- .circleci/config.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1b078d6..a84d61b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,22 +16,6 @@ jobs: - run: make test - build_pyboard: - docker: - - image: 'kmkfw/base' - - environment: - KMK_TEST: 1 - PIPENV_VENV_IN_PROJECT: 1 - - steps: - - checkout - - restore_cache: - keys: - - v2-kmk-venv-{{ checksum "Pipfile.lock" }} - - - run: make SKIP_KEYMAP_VALIDATION=1 USER_KEYMAP=user_keymaps/noop.py build-pyboard - workflows: version: 2 build-deploy: @@ -42,11 +26,3 @@ workflows: only: /.*/ tags: only: /.*/ - - build_pyboard: - filters: - branches: - only: /.*/ - tags: - only: /.*/ - requires: - - test From b92aceb68233bf4dcd790ecc77fc4eee3ea09b97 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 22:43:47 -0700 Subject: [PATCH 07/14] Fix linting --- kmk/firmware.py | 25 ++++++++++++------------- kmk/kmktime.py | 2 +- kmk/macros/simple.py | 4 +--- kmk/macros/unicode.py | 5 +++-- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/kmk/firmware.py b/kmk/firmware.py index c29f5c5..e73ba22 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -17,16 +17,21 @@ # First, stuff that has no dependencies, or only C/MPY deps import collections + +import gc + import kmk.consts -import kmk.kmktime -import kmk.types - -# Now stuff that depends on the above (and so on) -import kmk.keycodes -import kmk.matrix - import kmk.hid import kmk.internal_state +# Now stuff that depends on the above (and so on) +import kmk.keycodes +import kmk.kmktime +import kmk.matrix +import kmk.types +from kmk.consts import LeaderMode, UnicodeModes +from kmk.hid import USB_HID +from kmk.internal_state import InternalState +from kmk.matrix import MatrixScanner # GC runs automatically after CircuitPython imports. If we ever go back to # supporting MicroPython, we'll need a GC here (and probably after each @@ -34,12 +39,6 @@ import kmk.internal_state # Thanks for sticking around. Now let's do real work, starting below -import gc -from kmk.consts import LeaderMode, UnicodeModes -from kmk.hid import USB_HID -from kmk.internal_state import InternalState -from kmk.matrix import MatrixScanner - class Firmware: debug_enabled = False diff --git a/kmk/kmktime.py b/kmk/kmktime.py index 1b52a56..39381c8 100644 --- a/kmk/kmktime.py +++ b/kmk/kmktime.py @@ -1,6 +1,6 @@ import math - import time + USE_UTIME = False diff --git a/kmk/macros/simple.py b/kmk/macros/simple.py index 00e9bba..9fb1734 100644 --- a/kmk/macros/simple.py +++ b/kmk/macros/simple.py @@ -1,8 +1,6 @@ import string -from kmk.keycodes import (Keycodes, Macro, RawKeycodes, char_lookup, - lookup_kc_with_cache) -from kmk.kmktime import sleep_ms +from kmk.keycodes import Keycodes, Macro, char_lookup, lookup_kc_with_cache def simple_key_sequence(seq): diff --git a/kmk/macros/unicode.py b/kmk/macros/unicode.py index 345f264..2c5e280 100644 --- a/kmk/macros/unicode.py +++ b/kmk/macros/unicode.py @@ -1,6 +1,7 @@ from kmk.consts import UnicodeModes -from kmk.keycodes import Common, Macro, Modifiers, generate_codepoint_keysym_seq -from kmk.macros.simple import lookup_kc_with_cache, simple_key_sequence +from kmk.keycodes import (Common, Macro, Modifiers, + generate_codepoint_keysym_seq) +from kmk.macros.simple import simple_key_sequence from kmk.types import AttrDict from kmk.util import get_wide_ordinal From e2ed95556a138afcb5ef5bdd82ab32d5f876de50 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 22:50:41 -0700 Subject: [PATCH 08/14] Try to fix various advanced layers --- kmk/firmware.py | 21 ++++++++++++++------- kmk/internal_state.py | 29 +++++++++++++++-------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/kmk/firmware.py b/kmk/firmware.py index e73ba22..a642936 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -63,6 +63,15 @@ class Firmware: self._hid_helper_inst.create_report(self._state.keys_pressed).send() self._state.resolve_hid() + def _send_key(self, key): + if not getattr(key, 'no_press', None): + self._state.force_keycode_down(key) + self._send_hid() + + if not getattr(key, 'no_release', None): + self._state.force_keycode_up(key) + self._send_hid() + def go(self): assert self.keymap, 'must define a keymap with at least one row' assert self.row_pins, 'no GPIO pins defined for matrix rows' @@ -95,15 +104,13 @@ class Firmware: if self._state.hid_pending: self._send_hid() + for key in self._state.pending_keys: + self._send_key(key) + self._state.pending_key_handled() + if self._state.macro_pending: for key in self._state.macro_pending(self): - if not getattr(key, 'no_press', None): - self._state.force_keycode_down(key) - self._send_hid() - - if not getattr(key, 'no_release', None): - self._state.force_keycode_up(key) - self._send_hid() + self._send_key(key) self._state.resolve_macro() diff --git a/kmk/internal_state.py b/kmk/internal_state.py index 17dbe42..c2309cc 100644 --- a/kmk/internal_state.py +++ b/kmk/internal_state.py @@ -9,7 +9,7 @@ GESC_TRIGGERS = { class InternalState: keys_pressed = set() - pending_keys = set() + pending_keys = [] macro_pending = None leader_pending = None leader_last_len = 0 @@ -176,10 +176,10 @@ class InternalState: # Sets the timer start and acts like MO otherwise self.start_time['lm'] = ticks_ms() self.keys_pressed.add(changed_key.kc) - return self._layer_mo(changed_key, is_pressed) + else: + self.keys_pressed.discard(changed_key.kc) + self.start_time['lm'] = None - self.keys_pressed.discard(changed_key.kc) - self.start_time['lm'] = None return self._layer_mo(changed_key, is_pressed) def _layer_lt(self, changed_key, is_pressed): @@ -187,17 +187,18 @@ class InternalState: if is_pressed: # Sets the timer start and acts like MO otherwise self.start_time['lt'] = ticks_ms() - return self._layer_mo(changed_key, is_pressed) + self._layer_mo(changed_key, is_pressed) + else: + # On keyup, check timer, and press key if needed. + if self.start_time['lt'] and ( + ticks_diff(ticks_ms(), self.start_time['lt']) < self.config.tap_time + ): + self.hid_pending = True + self.pending_keys.append(changed_key.kc) - # On keyup, check timer, and press key if needed. - if self.start_time['lt'] and ( - ticks_diff(ticks_ms(), self.start_time['lt']) < self.config.tap_time - ): - self.hid_pending = True - self.pending_keys.add(changed_key.kc) - - self.start_time['lt'] = None - return self._layer_mo(changed_key, is_pressed) + self._layer_mo(changed_key, is_pressed) + self.start_time['lt'] = None + return self def _layer_tg(self, changed_key, is_pressed): """Toggles the layer (enables it if not active, and vise versa)""" From ed64b1e79e513dbcde2db448414b22fc9ee9d3a7 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 16 Oct 2018 23:11:23 -0700 Subject: [PATCH 09/14] Remove the sanity checker, it is unused and out of date --- Makefile | 12 +------ bin/keymap_sanity_check.py | 68 -------------------------------------- 2 files changed, 1 insertion(+), 79 deletions(-) delete mode 100755 bin/keymap_sanity_check.py diff --git a/Makefile b/Makefile index c543154..9af964a 100644 --- a/Makefile +++ b/Makefile @@ -55,17 +55,7 @@ powerwash: clean @echo "===> Removing pipenv-managed virtual environment" @$(PIPENV) --rm || true -test: lint micropython-build-unix - @echo "===> Testing keymap_sanity_check.py script" - @echo " --> Known good layout should pass..." - @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/known_good.py - @echo " --> Layer with ghosted MO should fail..." - @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/ghosted_layer_mo.py 2>/dev/null && exit 1 || exit 0 - @echo " --> Sharing a pin between rows/cols should fail..." - @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/duplicated_pins_between_row_col.py 2>/dev/null && exit 1 || exit 0 - @echo " --> Sharing a pin between two rows should fail..." - @MICROPYPATH=tests/test_data:./ ./bin/micropython.sh bin/keymap_sanity_check.py keymaps/duplicate_row_pins.py 2>/dev/null && exit 1 || exit 0 - @echo "===> The sanity checker is sane, unlike klardotsh" +test: lint .submodules: .gitmodules submodules.toml @echo "===> Pulling dependencies, this may take several minutes" diff --git a/bin/keymap_sanity_check.py b/bin/keymap_sanity_check.py deleted file mode 100755 index 6663a3b..0000000 --- a/bin/keymap_sanity_check.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env micropython - -import sys - -import uos - -from kmk.keycodes import Keycodes, RawKeycodes - -if len(sys.argv) < 2: - print('Must provide a keymap to test as first argument', file=sys.stderr) - sys.exit(200) - -user_keymap_file = sys.argv[1] - -if user_keymap_file.endswith('.py'): - user_keymap_file = user_keymap_file[:-3] - -# Before we can import the user's keymap, we need to wrangle sys.path to -# add our stub modules. Before we can do THAT, we have to figure out where -# we actually are, and that's not the most trivial thing in MicroPython! -# -# The hack here is to see if we can find ourselves in whatever uPy thinks -# the current directory is. If we can, we need to head up a level. Obviously, -# if the layout of the KMK repo ever changes, this script will need updated -# or all hell will break loose. - -# First, hack around https://github.com/micropython/micropython/issues/2322, -# where frozen modules aren't available if MicroPython is running a script -# rather than via REPL -sys.path.insert(0, '') - -if any(fname == 'keymap_sanity_check.py' for fname, _, _ in uos.ilistdir()): - sys.path.extend(('../', '../upy-unix-stubs/')) -else: - sys.path.extend(('./', './upy-unix-stubs')) - -user_keymap = __import__(user_keymap_file) - -if hasattr(user_keymap, 'cols') or hasattr(user_keymap, 'rows'): - assert hasattr(user_keymap, 'cols'), 'Handwired keyboards must have both rows and cols defined' - assert hasattr(user_keymap, 'rows'), 'Handwired keyboards must have both rows and cols defined' - - # Ensure that no pins are duplicated in a handwire config - # This is the same check done in the MatrixScanners, relying - # on the __repr__ of the objects to be unique (because generally, - # Pin objects themselves are not hashable) - assert len(user_keymap.cols) == len({p for p in user_keymap.cols}), \ - 'Cannot use a single pin for multiple columns' - assert len(user_keymap.rows) == len({p for p in user_keymap.rows}), \ - 'Cannot use a single pin for multiple rows' - - unique_pins = {repr(c) for c in user_keymap.cols} | {repr(r) for r in user_keymap.rows} - assert len(unique_pins) == len(user_keymap.cols) + len(user_keymap.rows), \ - 'Cannot use a pin as both a column and row' - -assert hasattr(user_keymap, 'keymap'), 'Must define a keymap array' -assert len(user_keymap.keymap), 'Keymap must contain at least one layer' - -for lidx, layer in enumerate(user_keymap.keymap): - assert len(layer), 'Layer {} must contain at least one row'.format(lidx) - assert all(len(row) for row in layer), 'Layer {} must not contain empty rows'.format(lidx) - - for ridx, row in enumerate(layer): - for cidx, key in enumerate(row): - if key.code == RawKeycodes.KC_MO: - assert user_keymap.keymap[key.layer][ridx][cidx] == Keycodes.KMK.KC_TRNS, \ - ('The physical key used for MO layer switching must be KC_TRNS on the ' - 'target layer or you will get stuck on that layer.') From 6c5a111d65090f89228bc054b5d16e0969891a7a Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 18 Oct 2018 12:55:06 -0700 Subject: [PATCH 10/14] Import hacks MUST be in non-isort order --- kmk/firmware.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/kmk/firmware.py b/kmk/firmware.py index a642936..f33f6e6 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -16,22 +16,17 @@ # workflow, in order from fewest to least nested dependencies. # First, stuff that has no dependencies, or only C/MPY deps -import collections +import collections # isort:skip +import kmk.consts # isort:skip +import kmk.kmktime # isort:skip +import kmk.types # isort:skip -import gc - -import kmk.consts -import kmk.hid -import kmk.internal_state # Now stuff that depends on the above (and so on) -import kmk.keycodes -import kmk.kmktime -import kmk.matrix -import kmk.types -from kmk.consts import LeaderMode, UnicodeModes -from kmk.hid import USB_HID -from kmk.internal_state import InternalState -from kmk.matrix import MatrixScanner +import kmk.keycodes # isort:skip +import kmk.matrix # isort:skip + +import kmk.hid # isort:skip +import kmk.internal_state # isort:skip # GC runs automatically after CircuitPython imports. If we ever go back to # supporting MicroPython, we'll need a GC here (and probably after each @@ -39,6 +34,13 @@ from kmk.matrix import MatrixScanner # Thanks for sticking around. Now let's do real work, starting below +import gc + +from kmk.consts import LeaderMode, UnicodeModes +from kmk.hid import USB_HID +from kmk.internal_state import InternalState +from kmk.matrix import MatrixScanner + class Firmware: debug_enabled = False From 90891063161c0d6231a2e622dacc7c7534b7eb15 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 18 Oct 2018 23:24:19 -0700 Subject: [PATCH 11/14] Work out some bugs where I could get stuck on layers. Add more debugging output --- kmk/firmware.py | 36 +++++++++++++++++++----------------- kmk/internal_state.py | 14 +++++++++----- kmk/matrix.py | 20 ++++++++------------ kmk/util.py | 4 ++++ 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/kmk/firmware.py b/kmk/firmware.py index f33f6e6..6ba2399 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -94,26 +94,28 @@ class Firmware: print("Firin' lazers. Keyboard is booted.") while True: - update = self.matrix.scan_for_pressed() + for update in self.matrix.scan_for_pressed(): + if update is not None: + self._state.matrix_changed( + update[0], + update[1], + update[2], + ) - if update is not None: - self._state.matrix_changed( - update[0], - update[1], - update[2], - ) + if self._state.hid_pending: + self._send_hid() - if self._state.hid_pending: - self._send_hid() - - for key in self._state.pending_keys: - self._send_key(key) - self._state.pending_key_handled() - - if self._state.macro_pending: - for key in self._state.macro_pending(self): + for key in self._state.pending_keys: self._send_key(key) + self._state.pending_key_handled() - self._state.resolve_macro() + if self._state.macro_pending: + for key in self._state.macro_pending(self): + self._send_key(key) + + self._state.resolve_macro() + + if self.debug_enabled: + print('New State: {}'.format(self._state._to_dict())) gc.collect() diff --git a/kmk/internal_state.py b/kmk/internal_state.py index c2309cc..054bd9c 100644 --- a/kmk/internal_state.py +++ b/kmk/internal_state.py @@ -1,5 +1,6 @@ from kmk.keycodes import FIRST_KMK_INTERNAL_KEYCODE, Keycodes, RawKeycodes from kmk.kmktime import sleep_ms, ticks_diff, ticks_ms +from kmk.util import intify_coordinate GESC_TRIGGERS = { Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT, @@ -9,6 +10,7 @@ GESC_TRIGGERS = { class InternalState: keys_pressed = set() + coord_keys_pressed = {} pending_keys = [] macro_pending = None leader_pending = None @@ -52,8 +54,6 @@ class InternalState: ret = { 'keys_pressed': self.keys_pressed, 'active_layers': self.active_layers, - 'unicode_mode': self.unicode_mode, - 'tap_time': self.config.tap_time, 'leader_mode_history': self.leader_mode_history, 'start_time': self.start_time, } @@ -69,17 +69,18 @@ class InternalState: if not layer_key or layer_key == Keycodes.KMK.KC_TRNS: continue - if layer_key == Keycodes.KMK.KC_NO: - return layer_key + if self.config.debug_enabled: + print('Resolved key: {}'.format(layer_key)) return layer_key def matrix_changed(self, row, col, is_pressed): if self.config.debug_enabled: print('Matrix changed (col, row, pressed?): {}, {}, {}'.format( - row, col, is_pressed, + col, row, is_pressed, )) + int_coord = intify_coordinate(row, col) kc_changed = self._find_key_in_map(row, col) if kc_changed is None: @@ -94,8 +95,11 @@ class InternalState: else: if is_pressed: self.keys_pressed.add(kc_changed) + self.coord_keys_pressed[int_coord] = kc_changed else: self.keys_pressed.discard(kc_changed) + self.keys_pressed.discard(self.coord_keys_pressed[int_coord]) + self.coord_keys_pressed[int_coord] = None self.hid_pending = True diff --git a/kmk/matrix.py b/kmk/matrix.py index 4c16d63..448ca73 100644 --- a/kmk/matrix.py +++ b/kmk/matrix.py @@ -1,6 +1,7 @@ import digitalio from kmk.consts import DiodeOrientation +from kmk.util import intify_coordinate class MatrixScanner: @@ -47,8 +48,8 @@ class MatrixScanner: self.swap_indicies = {} if swap_indicies is not None: for k, v in swap_indicies.items(): - self.swap_indicies[self._intify_coordinate(*k)] = v - self.swap_indicies[self._intify_coordinate(*v)] = k + self.swap_indicies[intify_coordinate(*k)] = v + self.swap_indicies[intify_coordinate(*v)] = k self.rollover_cols_every_rows = rollover_cols_every_rows if self.rollover_cols_every_rows is None: @@ -58,9 +59,6 @@ class MatrixScanner: self.state = bytearray(self.len_state_arrays) self.report = bytearray(3) - def _intify_coordinate(self, row, col): - return row << 8 | col - def scan_for_pressed(self): ba_idx = 0 any_changed = False @@ -85,7 +83,7 @@ class MatrixScanner: self.report[0] = oidx self.report[1] = iidx - swap_src = self._intify_coordinate(self.report[0], self.report[1]) + swap_src = intify_coordinate(self.report[0], self.report[1]) if swap_src in self.swap_indicies: tgt_row, tgt_col = self.swap_indicies[swap_src] self.report[0] = tgt_row @@ -94,14 +92,12 @@ class MatrixScanner: self.report[2] = new_val self.state[ba_idx] = new_val any_changed = True - break + + yield self.report ba_idx += 1 opin.value(False) - if any_changed: - break - - if any_changed: - return self.report + if not any_changed: + yield None diff --git a/kmk/util.py b/kmk/util.py index fa7f608..c53c505 100644 --- a/kmk/util.py +++ b/kmk/util.py @@ -1,3 +1,7 @@ +def intify_coordinate(row, col): + return row << 8 | col + + def get_wide_ordinal(char): if len(char) != 2: return ord(char) From 85ab403d223aac1a8ddf48aa6572680dffc81aea Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 18 Oct 2018 23:33:04 -0700 Subject: [PATCH 12/14] More clear matrix scan function --- kmk/firmware.py | 2 +- kmk/matrix.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/kmk/firmware.py b/kmk/firmware.py index 6ba2399..32af927 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -94,7 +94,7 @@ class Firmware: print("Firin' lazers. Keyboard is booted.") while True: - for update in self.matrix.scan_for_pressed(): + for update in self.matrix.scan_for_changes(): if update is not None: self._state.matrix_changed( update[0], diff --git a/kmk/matrix.py b/kmk/matrix.py index 448ca73..0b428e6 100644 --- a/kmk/matrix.py +++ b/kmk/matrix.py @@ -59,7 +59,13 @@ class MatrixScanner: self.state = bytearray(self.len_state_arrays) self.report = bytearray(3) - def scan_for_pressed(self): + def scan_for_changes(self): + ''' + Poll the matrix for changes and return either None (if nothing updated) + or a bytearray (reused in later runs so copy this if you need the raw + array itself for some crazy reason) consisting of (row, col, pressed) + which are (int, int, bool) + ''' ba_idx = 0 any_changed = False From 9646c89d3a2584e65c209bd312b1a0419a251252 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 18 Oct 2018 23:54:36 -0700 Subject: [PATCH 13/14] Update soooo much documentation --- docs/keymap.md | 202 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 160 insertions(+), 42 deletions(-) diff --git a/docs/keymap.md b/docs/keymap.md index 2ce2242..6d3e764 100644 --- a/docs/keymap.md +++ b/docs/keymap.md @@ -1,51 +1,169 @@ # Keymap -The keymap is a very simple set of instruction for your keyboard. -An example keymap for a 4x3 macro pad + +Keymaps in KMK are simple Python class objects with various attributes assigned +(some by default, however all are overridable). + +The basics of what you'll need to get started are: + +- Import the `Firmware` object for your keyboard from `kmk.boards` (or, if + handwiring your keyboard, import `Firmware` from the appropriate MCU for your + board from `kmk.mcus`. See `hardware.md` for the list of supported boards and + map this to the correct Python module under either of those paths. + +- Add a file to `user_keymaps/your_username` called whatever you'd like + +- Assign a `Firmware` instance to a variable (ex. `keyboard = Firmware()` - note + the parentheses) + +- Assign pins and your diode orientation (only necessary on handwire keyboards), + for example: + ```python -keymap = [ +col_pins = (P.SCK, P.MOSI, P.MISO, P.RX, P.TX, P.D4) +row_pins = (P.D10, P.D11, P.D12, P.D13, P.D9, P.D6, P.D5, P.SCL) +rollover_cols_every_rows = 4 +diode_orientation = DiodeOrientation.COLUMNS + +swap_indicies = { + (3, 3): (3, 9), + (3, 4): (3, 10), + (3, 5): (3, 11), +} +``` + +The pins should be based on whatever CircuitPython calls pins on your particular +board. You can find these in the REPL on your CircuitPython device: + +```python +import board +print(dir(board)) +``` + +> Note: `rollover_cols_every_rows` is only supported with +> `DiodeOrientation.COLUMNS`, not `DiodeOrientation.ROWS`. It is used for boards +> such as the Planck Rev6 which reuse column pins to simulate a 4x12 matrix in +> the form of an 8x6 matrix + +> Note: `swap_indicies` is used to literally flip two keys' positions in the +> matrix. This is pretty rarely needed, but for example the Planck Rev6 in full +> 1u Grid mode swaps the bottom three right keys on each "half", thus the +> example above + +You can further define a bunch of other stuff: + +- `debug_enabled` which will spew a ton of debugging information to the serial + console. This is very rarely needed, but can provide very valuable information + if you need to open an issue. + +- `unicode_mode` from `kmk.consts.UnicodeModes`, which defines the default + operating system implementation to use for unicode sequences (see examples + below, or `unicode.md`. This can be changed after boot with a key (see + `keycodes.md`) + +- `tap_time` which defines how long `KC.TT` and `KC.LT` will wait before + considering a key "held" (see `keycodes.md`) + +- `leader_dictionary`, which defines leader sequences (see `leader.md`), defined + as tuples of keycode objects (or you can use + `kmk.keycodes.generate_leader_dictionary_seq` with a string) + +We also support unicode sequences (emojis, emoticons, umlauted letters, +whatever) if your operating system and system setup do! See `unicode.md` for +details. + +Here's a giant example of all the above. This is my personal 4x12 matrix layout +running on a Planck Rev6 PCB, with a Feather M4 Express wired up to the outer +matrix pins (in somewhat of a "spider" setup), utilizing most of the above +features: + +```python +from kmk.boards.klarank import Firmware +from kmk.consts import UnicodeModes +from kmk.keycodes import KC +from kmk.keycodes import generate_leader_dictionary_seq as glds +from kmk.macros.simple import send_string +from kmk.macros.unicode import compile_unicode_string_sequences as cuss + +keyboard = Firmware() + +keyboard.debug_enabled = True +keyboard.unicode_mode = UnicodeModes.LINUX + +_______ = KC.TRNS +xxxxxxx = KC.NO + +emoticons = cuss({ + # Emojis + 'BEER': r'🍺', + 'BEER_TOAST': r'🍻', + 'FACE_CUTE_SMILE': r'😊', + 'FACE_HEART_EYES': r'😍', + 'FACE_JOY': r'😂', + 'FACE_SWEAT_SMILE': r'😅', + 'FACE_THINKING': r'🤔', + 'FIRE': r'🔥', + 'FLAG_CA': r'🇨🇦', + 'FLAG_US': r'🇺🇸', + 'HAND_CLAP': r'👏', + 'HAND_HORNS': r'🤘', + 'HAND_OK': r'👌', + 'HAND_THUMB_DOWN': r'👎', + 'HAND_THUMB_UP': r'👍', + 'HAND_WAVE': r'👋', + 'HEART': r'❤️', + 'MAPLE_LEAF': r'🍁', + 'POOP': r'💩', + 'TADA': r'🎉', + + # Emoticons, but fancier + 'ANGRY_TABLE_FLIP': r'(ノಠ痊ಠ)ノ彡┻━┻', + 'CELEBRATORY_GLITTER': r'+。:.゚ヽ(´∀。)ノ゚.:。+゚゚+。:.゚ヽ(*´∀)ノ゚.:。+゚', + 'SHRUGGIE': r'¯\_(ツ)_/¯', + 'TABLE_FLIP': r'(╯°□°)╯︵ ┻━┻', +}) + +WPM = send_string("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum arcu vitae elementum curabitur vitae nunc sed. Facilisis sed odio morbi quis.") + +keyboard.leader_dictionary = { + glds('hello'): send_string('hello world from kmk macros'), + glds('wpm'): WPM, + glds('atf'): emoticons.ANGRY_TABLE_FLIP, + glds('tf'): emoticons.TABLE_FLIP, + glds('fca'): emoticons.FLAG_CA, + glds('fus'): emoticons.FLAG_US, + glds('cel'): emoticons.CELEBRATORY_GLITTER, +} + +keyboard.keymap = [ [ - [KC.GESC, KC.A, KC.B], - [KC.C, KC.D, KC.E], - [KC.F, KC.G, KC.H], - [KC.MO(1), KC.I, KC.J], + [KC.GESC, KC.QUOT, KC.COMM, KC.DOT, KC.P, KC.Y, KC.F, KC.G, KC.C, KC.R, KC.L, KC.BSPC], + [KC.TAB, KC.A, KC.O, KC.E, KC.U, KC.I, KC.D, KC.H, KC.T, KC.N, KC.S, KC.ENT], + [KC.LGUI, KC.SCLN, KC.Q, KC.J, KC.K, KC.X, KC.B, KC.M, KC.W, KC.V, KC.Z, KC.LALT], + [KC.LCTL, KC.LEAD, KC.LSHIFT(KC.LGUI), KC.MO(2), KC.MO(3), KC.LSFT, KC.SPC, KC.MO(1), KC.LEFT, KC.DOWN, KC.UP, KC.RGHT], ], + [ - [KC.TRNS, KC.K, KC.RESET], - [KC.L, KC.M, KC.N], - [KC.O, KC.P, KC.Q], - [KC.TRNS, KC.R, KC.S], + [KC.GESC, xxxxxxx, xxxxxxx, KC.F10, KC.F11, KC.F12, xxxxxxx, KC.PSLS, KC.N7, KC.N8, KC.N9, KC.BSPC], + [KC.TAB, xxxxxxx, xxxxxxx, KC.F7, KC.F8, KC.F9, xxxxxxx, KC.PAST, KC.N4, KC.N5, KC.N6, _______], + [KC.LGUI, xxxxxxx, xxxxxxx, KC.F4, KC.F5, KC.F6, xxxxxxx, KC.PMNS, KC.N1, KC.N2, KC.N3, _______], + [KC.LCTL, xxxxxxx, _______, KC.F1, KC.F2, KC.F3, KC.SPC, _______, KC.N0, KC.DOT, xxxxxxx, KC.EQL], + ], + + [ + [KC.GESC, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.BSLS, KC.LBRC, KC.RBRC, KC.DEL], + [KC.TAB, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.MINS], + [KC.LGUI, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.LBRC, xxxxxxx, xxxxxxx, KC.INS], + [KC.LCTL, xxxxxxx, _______, _______, xxxxxxx, _______, xxxxxxx, xxxxxxx, KC.HOME, KC.PGDN, KC.PGUP, KC.END], + ], + + [ + [KC.GRV, KC.EXLM, KC.AT, KC.HASH, KC.DLR, KC.PERC, KC.CIRC, KC.AMPR, KC.ASTR, KC.LPRN, KC.RPRN, KC.SLSH], + [KC.TAB, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, KC.MINS], + [KC.LGUI, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx, xxxxxxx], + [KC.LCTL, xxxxxxx, xxxxxxx, xxxxxxx, _______, _______, xxxxxxx, xxxxxxx, KC.MUTE, KC.VOLD, KC.VOLU, xxxxxxx], ], ] -``` -# Handwire -When hand wiring custom keyboards, you will have to set the board up so KMK -knows where things are. This is the basic SETUP. - -```python -from kmk.consts import DiodeOrientation -from kmk.keycodes import KC -from kmk.pins import Pin as P -from kmk.entrypoints.circuitpython_samd51 import main -from kmk.firmware import Firmware - -cols = (P.D11, P.D10, P.D9) -rows = (P.A2, P.A3, P.A4, P.A5) - -diode_orientation = DiodeOrientation.COLUMNS -``` - -The main things that need set up are: -1. Your microcontroller will need to be set here. -```python -from kmk.entrypoints.circuitpython_samd51 import main -``` -2. Set your row and column pins up -```python -cols = (P.D11, P.D10, P.D9) -rows = (P.A2, P.A3, P.A4, P.A5) -``` -3. Set your diode orientation -```python -diode_orientation = DiodeOrientation.COLUMNS +if __name__ == '__main__': + keyboard.go() ``` From 758e4de82b0dc76a5d725c36074c94469bb0d979 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 18 Oct 2018 23:59:26 -0700 Subject: [PATCH 14/14] Add a bit of important documentation --- docs/keymap.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/keymap.md b/docs/keymap.md index 6d3e764..3914104 100644 --- a/docs/keymap.md +++ b/docs/keymap.md @@ -15,6 +15,14 @@ The basics of what you'll need to get started are: - Assign a `Firmware` instance to a variable (ex. `keyboard = Firmware()` - note the parentheses) +- Make sure this `Firmware` instance is actually run at the end of the file with + a block such as the following: + +```python +if __name__ == '__main__': + keyboard.go() +``` + - Assign pins and your diode orientation (only necessary on handwire keyboards), for example: @@ -25,9 +33,9 @@ rollover_cols_every_rows = 4 diode_orientation = DiodeOrientation.COLUMNS swap_indicies = { - (3, 3): (3, 9), - (3, 4): (3, 10), - (3, 5): (3, 11), + (3, 3): (3, 9), + (3, 4): (3, 10), + (3, 5): (3, 11), } ```