diff --git a/.gitmodules b/.gitmodules index ce7fb1a..2f10435 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,10 +2,7 @@ path = vendor/circuitpython url = https://github.com/adafruit/circuitpython.git branch = "9b98ad779468676c3d5f1efdc06b454aaed7c407" -[submodule "pydux"] - path = vendor/pydux - url = https://github.com/usrlocalben/pydux.git - branch = "943ca1c75357b9289f55f17ff2d997a66a3313a4" + ignore = dirty [submodule "upy-lib"] path = vendor/upy-lib url = https://github.com/micropython/micropython-lib.git diff --git a/Makefile b/Makefile index fb9f393..69ade93 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,10 @@ freeze-nrf-vendor-deps \ lint +NRF_DFU_PORT ?= /dev/ttyUSB0 +NRF_DFU_BAUD ?= 115200 +NRF_DFU_DELAY ?= 1.5 + devdeps: Pipfile.lock @pipenv install --dev --ignore-pipfile @@ -39,26 +43,26 @@ circuitpy-freeze-kmk-nrf: freeze-nrf-vendor-deps @rm -rf vendor/circuitpython/ports/nrf/kmk* @cp -av kmk vendor/circuitpython/ports/nrf/freeze/ -circuitpy-flash-nrf: circuitpy-freeze-kmk-nrf +circuitpy-flash-nrf: @echo "===> Building and flashing CircuitPython with KMK and your keymap" - @make -C vendor/circuitpython/ports/nrf BOARD=feather_nrf52832 SERIAL=/dev/ttyUSB0 SD=s132 FROZEN_MPY_DIR=freeze clean dfu-gen dfu-flash + @make -C vendor/circuitpython/ports/nrf BOARD=feather_nrf52832 SERIAL=${NRF_DFU_PORT} SD=s132 FROZEN_MPY_DIR=freeze clean dfu-gen dfu-flash circuitpy-flash-nrf-entrypoint: @echo "===> Flashing entrypoint if it doesn't already exist" @sleep 2 - @-timeout -k 5s 10s pipenv run ampy rm main.py 2>/dev/null - @-timeout -k 5s 10s pipenv run ampy put entrypoints/feather_nrf52832.py main.py + @-timeout -k 5s 10s pipenv run ampy -p ${NRF_DFU_PORT} -d ${NRF_DFU_DELAY} -b ${NRF_DFU_BAUD} rm main.py 2>/dev/null + @-timeout -k 5s 10s pipenv run ampy -p ${NRF_DFU_PORT} -d ${NRF_DFU_DELAY} -b ${NRF_DFU_BAUD} put entrypoints/feather_nrf52832.py main.py @echo "===> Flashed keyboard successfully!" -build-feather-test: lint devdeps circuitpy-deps circuitpy-freeze-kmk-nrf +ifndef BOARD +build-feather-nrf52832: + @echo "===> Must provide a board (usually from boards/...) to build!" +else +build-feather-nrf52832: lint devdeps circuitpy-deps circuitpy-freeze-kmk-nrf @echo "===> Preparing keyboard script for bundling into CircuitPython" - @cp -av boards/klardotsh/twotwo_matrix_feather.py vendor/circuitpython/ports/nrf/freeze/kmk_keyboard_user.py - @$(MAKE) circuitpy-flash-nrf circuitpy-flash-nrf-entrypoint - -build-feather-noop: lint devdeps circuitpy-deps circuitpy-freeze-kmk-nrf - @echo "===> Preparing keyboard script for bundling into CircuitPython" - @cp -av boards/noop.py vendor/circuitpython/ports/nrf/freeze/kmk_keyboard_user.py + @cp -av ${BOARD} vendor/circuitpython/ports/nrf/freeze/kmk_keyboard_user.py @$(MAKE) circuitpy-flash-nrf circuitpy-flash-nrf-entrypoint +endif # Fully wipe the board with only stock CircuitPython burn-it-all-with-fire: lint devdeps @@ -77,5 +81,5 @@ burn-it-all-with-fire: lint devdeps @$(MAKE) circuitpy-flash-nrf @echo "===> Wiping keyboard config" @sleep 2 - @-pipenv run ampy rm main.py 2>/dev/null + @-timeout -k 5s 10s pipenv run ampy -p ${NRF_DFU_PORT} -d ${NRF_DFU_DELAY} -b ${NRF_DFU_BAUD} rm main.py 2>/dev/null @echo "===> Wiped! Probably safe to flash keyboard, try Python serial REPL to verify?" diff --git a/Pipfile b/Pipfile index dfdfabf..45f1914 100644 --- a/Pipfile +++ b/Pipfile @@ -4,11 +4,18 @@ verify_ssl = true name = "pypi" [packages] +pydux = "*" [dev-packages] adafruit-ampy = "*" "flake8" = "*" "flake8-comprehensions" = "*" +ipython = "*" +ipdb = "*" +"flake8-commas" = "*" +isort = "*" +"flake8-isort" = "*" +neovim = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 09ac140..e92f6e8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e246783475c5feb51445494a3f30c397ad6ddaed35c45decc9846970d001ad50" + "sha256": "44e8c37b94a71b7f47fc43f2c98bf17d546f4a5ef7ad1cad5076d4a47fc4515a" }, "pipfile-spec": 6, "requires": { @@ -15,7 +15,16 @@ } ] }, - "default": {}, + "default": { + "pydux": { + "hashes": [ + "sha256:5cb9217be9d8c7ff79b028f6f02597bbb055b107ce8eecbe5f631f3fc76d793f", + "sha256:bed123b5255d566f792b9ceebad87e3f9c1d2d85abed4b9a9475ffc831035879" + ], + "index": "pypi", + "version": "==0.2.2" + } + }, "develop": { "adafruit-ampy": { "hashes": [ @@ -24,6 +33,13 @@ "index": "pypi", "version": "==1.0.5" }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, "click": { "hashes": [ "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", @@ -31,6 +47,13 @@ ], "version": "==6.7" }, + "decorator": { + "hashes": [ + "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", + "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + ], + "version": "==4.3.0" + }, "flake8": { "hashes": [ "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", @@ -39,6 +62,14 @@ "index": "pypi", "version": "==3.5.0" }, + "flake8-commas": { + "hashes": [ + "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7", + "sha256:ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e" + ], + "index": "pypi", + "version": "==2.0.0" + }, "flake8-comprehensions": { "hashes": [ "sha256:b83891fec0e680b07aa1fd92e53eb6993be29a0f3673a09badbe8da307c445e0", @@ -47,6 +78,76 @@ "index": "pypi", "version": "==1.4.1" }, + "flake8-isort": { + "hashes": [ + "sha256:298d7904ac3a46274edf4ce66fd7e272c2a60c34c3cc999dea000608d64e5e6e", + "sha256:5992850626ce96547b1f1c7e8a7f0ef49ab2be44eca2177934566437b636fa3c" + ], + "index": "pypi", + "version": "==2.5" + }, + "greenlet": { + "hashes": [ + "sha256:0411b5bf0de5ec11060925fd811ad49073fa19f995bcf408839eb619b59bb9f7", + "sha256:131f4ed14f0fd28d2a9fa50f79a57d5ed1c8f742d3ccac3d773fee09ef6fe217", + "sha256:13510d32f8db72a0b3e1720dbf8cba5c4eecdf07abc4cb631982f51256c453d1", + "sha256:31dc4d77ef04ab0460d024786f51466dbbc274fda7c8aad0885a6df5ff8d642e", + "sha256:35021d9fecea53b21e4defec0ff3ad69a8e2b75aca1ceddd444a5ba71216547e", + "sha256:426a8ef9e3b97c27e841648241c2862442c13c91ec4a48c4a72b262ccf30add9", + "sha256:58217698193fb94f3e6ff57eed0ae20381a8d06c2bc10151f76c06bb449a3a19", + "sha256:5f45adbbb69281845981bb4e0a4efb8a405f10f3cd6c349cb4a5db3357c6bf93", + "sha256:5fdb524767288f7ad161d2182f7ed6cafc0a283363728dcd04b9485f6411547c", + "sha256:71fbee1f7ef3fb42efa3761a8faefc796e7e425f528de536cfb4c9de03bde885", + "sha256:80bd314157851d06f7db7ca527082dbb0ee97afefb529cdcd59f7a5950927ba0", + "sha256:b843c9ef6aed54a2649887f55959da0031595ccfaf7e7a0ba7aa681ffeaa0aa1", + "sha256:c6a05ef8125503d2d282ccf1448e3599b8a6bd805c3cdee79760fa3da0ea090e", + "sha256:deeda2769a52db840efe5bf7bdf7cefa0ae17b43a844a3259d39fb9465c8b008", + "sha256:e66f8b09eec1afdcab947d3a1d65b87b25fde39e9172ae1bec562488335633b4", + "sha256:e8db93045414980dbada8908d49dbbc0aa134277da3ff613b3e548cb275bdd37", + "sha256:f1cc268a15ade58d9a0c04569fe6613e19b8b0345b64453064e2c3c6d79051af", + "sha256:fe3001b6a4f3f3582a865b9e5081cc548b973ec20320f297f5e2d46860e9c703", + "sha256:fe85bf7adb26eb47ad53a1bae5d35a28df16b2b93b89042a3a28746617a4738d" + ], + "version": "==0.4.14" + }, + "ipdb": { + "hashes": [ + "sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a" + ], + "index": "pypi", + "version": "==0.11" + }, + "ipython": { + "hashes": [ + "sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62", + "sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4" + ], + "index": "pypi", + "version": "==6.5.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "index": "pypi", + "version": "==4.3.4" + }, + "jedi": { + "hashes": [ + "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", + "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f" + ], + "version": "==0.12.1" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -54,6 +155,70 @@ ], "version": "==0.6.1" }, + "msgpack": { + "hashes": [ + "sha256:0b3b1773d2693c70598585a34ca2715873ba899565f0a7c9a1545baef7e7fbdc", + "sha256:0bae5d1538c5c6a75642f75a1781f3ac2275d744a92af1a453c150da3446138b", + "sha256:0ee8c8c85aa651be3aa0cd005b5931769eaa658c948ce79428766f1bd46ae2c3", + "sha256:1369f9edba9500c7a6489b70fdfac773e925342f4531f1e3d4c20ac3173b1ae0", + "sha256:22d9c929d1d539f37da3d1b0e16270fa9d46107beab8c0d4d2bddffffe895cee", + "sha256:2ff43e3247a1e11d544017bb26f580a68306cec7a6257d8818893c1fda665f42", + "sha256:31a98047355d34d047fcdb55b09cb19f633cf214c705a765bd745456c142130c", + "sha256:8767eb0032732c3a0da92cbec5ac186ef89a3258c6edca09161472ca0206c45f", + "sha256:8acc8910218555044e23826980b950e96685dc48124a290c86f6f41a296ea172", + "sha256:ab189a6365be1860a5ecf8159c248f12d33f79ea799ae9695fa6a29896dcf1d4", + "sha256:cfd6535feb0f1cf1c7cdb25773e965cc9f92928244a8c3ef6f8f8a8e1f7ae5c4", + "sha256:e274cd4480d8c76ec467a85a9c6635bbf2258f0649040560382ab58cabb44bcf", + "sha256:f86642d60dca13e93260187d56c2bef2487aa4d574a669e8ceefcf9f4c26fd00", + "sha256:f8a57cbda46a94ed0db55b73e6ab0c15e78b4ede8690fa491a0e55128d552bb0", + "sha256:fcea97a352416afcbccd7af9625159d80704a25c519c251c734527329bb20d0e" + ], + "version": "==0.5.6" + }, + "neovim": { + "hashes": [ + "sha256:6ce58a742e0427491c0e1c8108556ee72ba33844209bd9e226b8da9538299276" + ], + "index": "pypi", + "version": "==0.2.6" + }, + "parso": { + "hashes": [ + "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", + "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + ], + "version": "==0.3.1" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", + "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5" + ], + "version": "==0.7.4" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", + "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", + "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" + ], + "version": "==1.0.15" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, "pycodestyle": { "hashes": [ "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", @@ -68,6 +233,13 @@ ], "version": "==1.6.0" }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, "pyserial": { "hashes": [ "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", @@ -81,6 +253,40 @@ "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77" ], "version": "==0.9.1" + }, + "simplegeneric": { + "hashes": [ + "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" + ], + "version": "==0.8.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "testfixtures": { + "hashes": [ + "sha256:7e4df89a8bf8b8905464160f08aff131a36f0b33654fe4f9e4387afe546eae25", + "sha256:bcadbad77526cc5fc38bfb2ab80da810d7bde56ffe4c7fdb8e2bba122ded9620" + ], + "version": "==6.2.0" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" } } } diff --git a/boards/klardotsh/twotwo_matrix_feather.py b/boards/klardotsh/twotwo_matrix_feather.py index e8b23eb..9a814c8 100644 --- a/boards/klardotsh/twotwo_matrix_feather.py +++ b/boards/klardotsh/twotwo_matrix_feather.py @@ -1,23 +1,28 @@ +from logging import DEBUG + import board -from kmk.circuitpython.matrix import MatrixScanner from kmk.common.consts import DiodeOrientation -from kmk.common.keymap import Keymap +from kmk.firmware import Firmware def main(): cols = (board.A4, board.A5) rows = (board.D27, board.A6) - matrix = MatrixScanner( - cols=cols, rows=rows, - diode_orientation=DiodeOrientation.COLUMNS, - ) + diode_orientation = DiodeOrientation.COLUMNS - keymap = Keymap([ + keymap = [ ['A', 'B'], ['C', 'D'], - ]) + ] - while True: - keymap.parse(matrix.raw_scan()) + firmware = Firmware( + keymap=keymap, + row_pins=rows, + col_pins=cols, + diode_orientation=diode_orientation, + log_level=DEBUG, + ) + + firmware.go() diff --git a/entrypoints/feather_nrf52832.py b/entrypoints/feather_nrf52832.py index 1f52211..a21b146 100644 --- a/entrypoints/feather_nrf52832.py +++ b/entrypoints/feather_nrf52832.py @@ -1,5 +1,12 @@ +import sys + +from kmk.circuitpython.util import feather_red_led_flash from kmk_keyboard_user import main - if __name__ == '__main__': - main() + try: + main() + except Exception as e: + sys.print_exception(e) + feather_red_led_flash(duration=10, rate=0.5) + sys.exit(1) diff --git a/kmk/circuitpython/matrix.py b/kmk/circuitpython/matrix.py index 5203cc2..666a2ca 100644 --- a/kmk/circuitpython/matrix.py +++ b/kmk/circuitpython/matrix.py @@ -1,9 +1,10 @@ import digitalio +from kmk.common.abstract.matrix_scanner import AbstractMatrixScanner from kmk.common.consts import DiodeOrientation -class MatrixScanner: +class MatrixScanner(AbstractMatrixScanner): def __init__(self, cols, rows, diode_orientation=DiodeOrientation.COLUMNS): # 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 @@ -35,20 +36,7 @@ class MatrixScanner: pin.switch_to_input(pull=digitalio.Pull.DOWN) def _normalize_matrix(self, matrix): - ''' - We always want to internally look at a keyboard as a list of rows, - where a "row" is a list of keycodes (columns). - - This will convert DiodeOrientation.COLUMNS matrix scans into a - ROWS scan, so we never have to think about these things again. - ''' - if self.diode_orientation == DiodeOrientation.ROWS: - return matrix - - return [ - [col[col_entry] for col in matrix] - for col_entry in range(max(len(col) for col in matrix)) - ] + return super()._normalize_matrix(matrix) def raw_scan(self): matrix = [] diff --git a/kmk/circuitpython/util.py b/kmk/circuitpython/util.py index 01d63d5..a0575f7 100644 --- a/kmk/circuitpython/util.py +++ b/kmk/circuitpython/util.py @@ -1,22 +1,17 @@ +import time + import board import digitalio -import time -import sys -def feather_signal_error_with_led_flash(rate=0.5): +def feather_red_led_flash(duration=10, rate=0.5): ''' - Flash the red LED for 10 seconds, alternating every $rate - Could be useful as an uncaught exception handler later on, - but is for now unused + Flash the red LED for $duration seconds, alternating every $rate ''' rled = digitalio.DigitalInOut(board.LED1) rled.direction = digitalio.Direction.OUTPUT - # blink for 5 seconds and exit - for cycle in range(10): + for cycle in range(duration / rate): rled.value = cycle % 2 time.sleep(rate) - - sys.exit(1) diff --git a/kmk/common/abstract/__init__.py b/kmk/common/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmk/common/abstract/matrix_scanner.py b/kmk/common/abstract/matrix_scanner.py new file mode 100644 index 0000000..4ef71dc --- /dev/null +++ b/kmk/common/abstract/matrix_scanner.py @@ -0,0 +1,25 @@ +from kmk.common.consts import DiodeOrientation + + +class AbstractMatrixScanner(): + def __init__(self, cols, rows, diode_orientation=DiodeOrientation.COLUMNS): + raise NotImplementedError('Abstract implementation') + + def _normalize_matrix(self, matrix): + ''' + We always want to internally look at a keyboard as a list of rows, + where a "row" is a list of keycodes (columns). + + This will convert DiodeOrientation.COLUMNS matrix scans into a + ROWS scan, so we never have to think about these things again. + ''' + if self.diode_orientation == DiodeOrientation.ROWS: + return matrix + + return [ + [col[col_entry] for col in matrix] + for col_entry in range(max(len(col) for col in matrix)) + ] + + def raw_scan(self): + raise NotImplementedError('Abstract implementation') diff --git a/kmk/common/event_defs.py b/kmk/common/event_defs.py new file mode 100644 index 0000000..04a1f7a --- /dev/null +++ b/kmk/common/event_defs.py @@ -0,0 +1,33 @@ +from micropython import const + +KEY_UP_EVENT = const(1) +KEY_DOWN_EVENT = const(2) +INIT_FIRMWARE_EVENT = const(3) + + +def init_firmware(keymap, row_pins, col_pins, diode_orientation): + return { + 'type': INIT_FIRMWARE_EVENT, + 'keymap': keymap, + 'row_pins': row_pins, + 'col_pins': col_pins, + 'diode_orientation': diode_orientation, + } + + +def key_up_event(keycode, row, col): + return { + 'type': KEY_UP_EVENT, + 'keycode': keycode, + 'row': row, + 'col': col, + } + + +def key_down_event(keycode, row, col): + return { + 'type': KEY_DOWN_EVENT, + 'keycode': keycode, + 'row': row, + 'col': col, + } diff --git a/kmk/common/internal_state.py b/kmk/common/internal_state.py new file mode 100644 index 0000000..a7790b0 --- /dev/null +++ b/kmk/common/internal_state.py @@ -0,0 +1,133 @@ +import logging +import sys + +from kmk.common.consts import DiodeOrientation +from kmk.common.event_defs import (INIT_FIRMWARE_EVENT, KEY_DOWN_EVENT, + KEY_UP_EVENT) + + +class ReduxStore: + 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): + self.logger.debug('Dispatching action: {}'.format(action)) + self.state = self.reducer(self.state, action) + self.logger.debug('Dispatching complete: {}'.format(action)) + + self.logger.debug('Calling subscriptions') + + 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') + print(sys.print_exception(e), file=sys.stderr) + + self.logger.debug('Callbacks complete') + + 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: + modifiers_pressed = frozenset() + keys_pressed = frozenset() + keymap = [] + row_pins = [] + col_pins = [] + matrix = [] + diode_orientation = DiodeOrientation.COLUMNS + + @property + def __dict__(self): + return { + 'keys_pressed': self.keys_pressed, + 'modifiers_pressed': self.modifiers_pressed, + 'keymap': self.keymap, + 'col_pins': self.col_pins, + 'row_pins': self.row_pins, + 'diode_orientation': self.diode_orientation, + } + + def __repr__(self): + return 'InternalState({})'.format(self.__dict__) + + def copy(self, **kwargs): + new_state = InternalState() + + for k, v in self.__dict__.items(): + if hasattr(new_state, k): + setattr(new_state, k, v) + + for k, v in kwargs.items(): + if hasattr(new_state, k): + setattr(new_state, k, v) + + return new_state + + +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'] == KEY_UP_EVENT: + return state.copy( + keys_pressed=frozenset( + key for key in state.keys_pressed if key != action['keycode'] + ), + matrix=[ + r if ridx != action['row'] else [ + c if cidx != action['col'] else False + for cidx, c in enumerate(r) + ] + for ridx, r in enumerate(state.matrix) + ], + ) + + if action['type'] == KEY_DOWN_EVENT: + return state.copy( + keys_pressed=( + state.keys_pressed | {action['keycode']} + ), + matrix=[ + r if ridx != action['row'] else [ + c if cidx != action['col'] else True + for cidx, c in enumerate(r) + ] + for ridx, r in enumerate(state.matrix) + ], + ) + + if action['type'] == INIT_FIRMWARE_EVENT: + return state.copy( + keymap=action['keymap'], + row_pins=action['row_pins'], + col_pins=action['col_pins'], + diode_orientation=action['diode_orientation'], + matrix=[ + [False for c in action['col_pins']] + for r in action['row_pins'] + ], + ) diff --git a/kmk/common/keymap.py b/kmk/common/keymap.py index cdcfc9c..c135552 100644 --- a/kmk/common/keymap.py +++ b/kmk/common/keymap.py @@ -1,18 +1,25 @@ +from kmk.common.event_defs import key_down_event, key_up_event + + class Keymap: def __init__(self, map): self.map = map - self.state = [ - [False for _ in row] - for row in self.map - ] - def parse(self, matrix): + def parse(self, matrix, store): + state = store.get_state() + for ridx, row in enumerate(matrix): for cidx, col in enumerate(row): - if col != self.state[ridx][cidx]: - print('{}: {}'.format( - 'KEYDOWN' if col else 'KEYUP', - self.map[ridx][cidx], - )) - - self.state = matrix + if col != state.matrix[ridx][cidx]: + if col: + store.dispatch(key_down_event( + row=ridx, + col=cidx, + keycode=self.map[ridx][cidx], + )) + else: + store.dispatch(key_up_event( + row=ridx, + col=cidx, + keycode=self.map[ridx][cidx], + )) diff --git a/kmk/firmware.py b/kmk/firmware.py new file mode 100644 index 0000000..907919c --- /dev/null +++ b/kmk/firmware.py @@ -0,0 +1,46 @@ +import logging + +from kmk.common.event_defs import init_firmware +from kmk.common.internal_state import ReduxStore, kmk_reducer +from kmk.common.keymap import Keymap + +try: + from kmk.circuitpython.matrix import MatrixScanner +except ImportError: + from kmk.micropython.matrix import MatrixScanner + + +class Firmware: + def __init__( + self, keymap, row_pins, col_pins, diode_orientation, + log_level=logging.NOTSET, + ): + self.cached_state = None + self.store = ReduxStore(kmk_reducer, log_level=log_level) + self.store.subscribe( + lambda state, action: self._subscription(state, action), + ) + 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 self.cached_state is None or self.cached_state.keymap != state.keymap: + self.keymap = Keymap(state.keymap) + + if self.cached_state is None or any( + getattr(self.cached_state, k) != getattr(state, k) + for k in state.__dict__.keys() + ): + self.matrix = MatrixScanner( + state.col_pins, + state.row_pins, + state.diode_orientation, + ) + + def go(self): + while True: + self.keymap.parse(self.matrix.raw_scan(), store=self.store) diff --git a/setup.cfg b/setup.cfg index 1d9a970..fc463b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [flake8] exclude = .git,__pycache__,vendor +max_line_length = 99 + +[isort] +known_third_party = analogio,bitbangio,bleio,board,busio,digitalio,framebuf,gamepad,gc,microcontroller,micropython,pulseio,pyb,pydux,uio,ubluepy diff --git a/upy-freeze.txt b/upy-freeze.txt index 97cb5b7..b06922b 100644 --- a/upy-freeze.txt +++ b/upy-freeze.txt @@ -1,4 +1 @@ -vendor/upy-lib/__future__/__future__.py -vendor/upy-lib/functools/functools.py -vendor/upy-lib/string/string.py -vendor/pydux/pydux +vendor/upy-lib/logging/logging.py diff --git a/vendor/pydux b/vendor/pydux deleted file mode 160000 index 943ca1c..0000000 --- a/vendor/pydux +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 943ca1c75357b9289f55f17ff2d997a66a3313a4