Merge pull request #45 from KMKfw/topic-feather-express-m4

Add support for Adafruit Feather+ItsyBitsy M4 Express boards through CircuitPython
This commit is contained in:
Josh Klar 2018-10-07 19:00:04 -07:00 committed by GitHub
commit 914b305fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 999 additions and 553 deletions

View File

@ -2,19 +2,26 @@ version: 2
jobs: jobs:
lint: lint:
docker: docker:
- image: 'python:3.7' - image: 'ubuntu:bionic'
environment: environment:
KMK_TEST: 1 KMK_TEST: 1
PIPENV_VENV_IN_PROJECT: 1 PIPENV_VENV_IN_PROJECT: 1
steps: steps:
- run: apt-get update && apt-get install -y software-properties-common git ssh wget
- run: add-apt-repository ppa:deadsnakes/ppa
- run: apt-get update && apt-get install -y python3.7 python3.7-dev build-essential pkg-config libffi-dev
- run: wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py
- run: python3.7 /tmp/get-pip.py
- run: python3.7 -m pip install pip==18.0 # downgrade pip to work around https://github.com/pypa/pipenv/issues/2924
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v1-kmk-venv-{{ checksum "Pipfile.lock" }} - v1-kmk-venv-{{ checksum "Pipfile.lock" }}
- run: pip install pipenv==2018.7.1 - run: python3.7 -m pip install pipenv==2018.7.1
- run: make lint - run: make lint
- save_cache: - save_cache:
@ -24,60 +31,107 @@ jobs:
test: test:
docker: docker:
- image: 'python:3.7' - image: 'ubuntu:bionic'
environment: environment:
KMK_TEST: 1 KMK_TEST: 1
PIPENV_VENV_IN_PROJECT: 1 PIPENV_VENV_IN_PROJECT: 1
steps: steps:
- run: apt-get update && apt-get install -y software-properties-common git ssh
- run: add-apt-repository ppa:team-gcc-arm-embedded/ppa
- run: add-apt-repository ppa:deadsnakes/ppa
- run: apt-get update && apt-get install -y gcc-arm-embedded gettext wget unzip rsync python3.7 python3.7-dev build-essential pkg-config libffi-dev
- run: wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py
- run: python3.7 /tmp/get-pip.py
- run: python3.7 -m pip install pip==18.0 # downgrade pip to work around https://github.com/pypa/pipenv/issues/2924
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v1-kmk-venv-{{ checksum "Pipfile.lock" }} - v1-kmk-venv-{{ checksum "Pipfile.lock" }}
- run: pip install pipenv==2018.7.1 - run: python3.7 -m pip install pipenv==2018.7.1
- run: apt-get update && apt-get install -y gcc-arm-none-eabi gettext wget unzip rsync
- run: make test - run: make test
build_feather_m4_express:
docker:
- image: 'ubuntu:bionic'
environment:
KMK_TEST: 1
PIPENV_VENV_IN_PROJECT: 1
steps:
- run: apt-get update && apt-get install -y software-properties-common git ssh
- run: add-apt-repository ppa:team-gcc-arm-embedded/ppa
- run: add-apt-repository ppa:deadsnakes/ppa
- run: apt-get update && apt-get install -y gcc-arm-embedded gettext wget unzip rsync python3.7 python3.7-dev build-essential pkg-config libffi-dev
- run: wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py
- run: python3.7 /tmp/get-pip.py
- run: python3.7 -m pip install pip==18.0 # downgrade pip to work around https://github.com/pypa/pipenv/issues/2924
- checkout
- restore_cache:
keys:
- v1-kmk-venv-{{ checksum "Pipfile.lock" }}
- run: python3.7 -m pip install pipenv==2018.7.1
- run: make SKIP_KEYMAP_VALIDATION=1 USER_KEYMAP=user_keymaps/noop.py build-feather-m4-express
build_itsybitsy_m4_express:
docker:
- image: 'ubuntu:bionic'
environment:
KMK_TEST: 1
PIPENV_VENV_IN_PROJECT: 1
steps:
- run: apt-get update && apt-get install -y software-properties-common git ssh
- run: add-apt-repository ppa:team-gcc-arm-embedded/ppa
- run: add-apt-repository ppa:deadsnakes/ppa
- run: apt-get update && apt-get install -y gcc-arm-embedded gettext wget unzip rsync python3.7 python3.7-dev build-essential pkg-config libffi-dev
- run: wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py
- run: python3.7 /tmp/get-pip.py
- run: python3.7 -m pip install pip==18.0 # downgrade pip to work around https://github.com/pypa/pipenv/issues/2924
- checkout
- restore_cache:
keys:
- v1-kmk-venv-{{ checksum "Pipfile.lock" }}
- run: python3.7 -m pip install pipenv==2018.7.1
- run: make SKIP_KEYMAP_VALIDATION=1 USER_KEYMAP=user_keymaps/noop.py build-itsybitsy-m4-express
build_pyboard: build_pyboard:
docker: docker:
- image: 'python:3.7' - image: 'ubuntu:bionic'
environment: environment:
KMK_TEST: 1 KMK_TEST: 1
PIPENV_VENV_IN_PROJECT: 1 PIPENV_VENV_IN_PROJECT: 1
steps: steps:
- run: apt-get update && apt-get install -y software-properties-common git ssh
- run: add-apt-repository ppa:team-gcc-arm-embedded/ppa
- run: add-apt-repository ppa:deadsnakes/ppa
- run: apt-get update && apt-get install -y gcc-arm-embedded gettext wget unzip rsync python3.7 python3.7-dev build-essential pkg-config libffi-dev
- run: wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py
- run: python3.7 /tmp/get-pip.py
- run: python3.7 -m pip install pip==18.0 # downgrade pip to work around https://github.com/pypa/pipenv/issues/2924
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v1-kmk-venv-{{ checksum "Pipfile.lock" }} - v1-kmk-venv-{{ checksum "Pipfile.lock" }}
- run: pip install pipenv==2018.7.1 - run: python3.7 -m pip install pipenv==2018.7.1
- run: apt-get update && apt-get install -y gcc-arm-none-eabi gettext wget unzip rsync
- run: make SKIP_KEYMAP_VALIDATION=1 USER_KEYMAP=user_keymaps/noop.py build-pyboard - run: make SKIP_KEYMAP_VALIDATION=1 USER_KEYMAP=user_keymaps/noop.py build-pyboard
build_teensy_31:
docker:
- image: 'python:3.7'
environment:
KMK_TEST: 1
PIPENV_VENV_IN_PROJECT: 1
steps:
- checkout
- restore_cache:
keys:
- v1-kmk-venv-{{ checksum "Pipfile.lock" }}
- run: pip install pipenv==2018.7.1
- run: apt-get update && apt-get install -y gcc-arm-none-eabi gettext wget unzip rsync
- run: make USER_KEYMAP=user_keymaps/noop.py build-teensy-3.1
workflows: workflows:
version: 2 version: 2
build-deploy: build-deploy:
@ -96,6 +150,22 @@ workflows:
only: /.*/ only: /.*/
requires: requires:
- lint - lint
- build_feather_m4_express:
filters:
branches:
only: /.*/
tags:
only: /.*/
requires:
- test
- build_itsybitsy_m4_express:
filters:
branches:
only: /.*/
tags:
only: /.*/
requires:
- test
- build_pyboard: - build_pyboard:
filters: filters:
branches: branches:
@ -104,11 +174,3 @@ workflows:
only: /.*/ only: /.*/
requires: requires:
- test - test
- build_teensy_31:
filters:
branches:
only: /.*/
tags:
only: /.*/
requires:
- test

1
.gitignore vendored
View File

@ -107,6 +107,7 @@ venv.bak/
.submodules .submodules
.circuitpy-deps .circuitpy-deps
.micropython-deps .micropython-deps
.devdeps
# Pycharms cruft # Pycharms cruft
.idea .idea

2
.gitmodules vendored
View File

@ -4,7 +4,7 @@
ignore = dirty ignore = dirty
[submodule "upy-lib"] [submodule "upy-lib"]
path = vendor/upy-lib path = vendor/upy-lib
url = https://github.com/micropython/micropython-lib.git url = https://github.com/kmkfw/micropython-lib.git
ignore = dirty ignore = dirty
[submodule "micropython"] [submodule "micropython"]
path = vendor/micropython path = vendor/micropython

183
Makefile
View File

@ -12,8 +12,11 @@ AMPY_DELAY ?= 1.5
ARDUINO ?= /usr/share/arduino ARDUINO ?= /usr/share/arduino
PIPENV ?= $(shell which pipenv) PIPENV ?= $(shell which pipenv)
devdeps: Pipfile.lock .devdeps: Pipfile.lock
@$(PIPENV) install --dev --ignore-pipfile @$(PIPENV) install --dev --ignore-pipfile
@touch .devdeps
devdeps: .devdeps
lint: devdeps lint: devdeps
@$(PIPENV) run flake8 @$(PIPENV) run flake8
@ -42,20 +45,21 @@ test: micropython-build-unix
.submodules: .gitmodules submodules.toml .submodules: .gitmodules submodules.toml
@echo "===> Pulling dependencies, this may take several minutes" @echo "===> Pulling dependencies, this may take several minutes"
@git submodule sync
@git submodule update --init --recursive @git submodule update --init --recursive
@rsync -avh vendor/ build/ @rsync -ah vendor/ build/
@touch .submodules @touch .submodules
.circuitpy-deps: .submodules .circuitpy-deps: .submodules
@echo "===> Building circuitpython/mpy-cross" @echo "===> Building circuitpython/mpy-cross"
@make -C build/circuitpython/mpy-cross @pipenv run $(MAKE) -C build/circuitpython/mpy-cross
@echo "===> Pulling Nordic BLE stack" @echo "===> Pulling Nordic BLE stack"
@cd build/circuitpython/ports/nrf && ./drivers/bluetooth/download_ble_stack.sh 2>/dev/null >/dev/null @cd build/circuitpython/ports/nrf && ./drivers/bluetooth/download_ble_stack.sh 2>/dev/null >/dev/null
@touch .circuitpy-deps @touch .circuitpy-deps
.micropython-deps: .submodules .micropython-deps: .submodules
@echo "===> Building micropython/mpy-cross" @echo "===> Building micropython/mpy-cross"
@make -C build/micropython/mpy-cross @pipenv run $(MAKE) -C build/micropython/mpy-cross
@touch .micropython-deps @touch .micropython-deps
submodules: .submodules submodules: .submodules
@ -63,74 +67,93 @@ circuitpy-deps: .circuitpy-deps
micropython-deps: .micropython-deps micropython-deps: .micropython-deps
build/micropython/ports/unix/micropython: micropython-deps build/micropython/ports/unix/modules/.kmk_frozen build/micropython/ports/unix/micropython: micropython-deps build/micropython/ports/unix/modules/.kmk_frozen
@make -j4 -C build/micropython/ports/unix @pipenv run $(MAKE) -j4 -C build/micropython/ports/unix
micropython-build-unix: build/micropython/ports/unix/micropython micropython-build-unix: build/micropython/ports/unix/micropython
freeze-atmel-samd-build-deps: build/circuitpython/ports/atmel-samd/modules/.kmk_frozen
freeze-nrf-build-deps: build/circuitpython/ports/nrf/freeze/.kmk_frozen freeze-nrf-build-deps: build/circuitpython/ports/nrf/freeze/.kmk_frozen
freeze-teensy3.1-build-deps: build/micropython/ports/teensy/freeze/.kmk_frozen
freeze-stm32-build-deps: build/micropython/ports/stm32/freeze/.kmk_frozen freeze-stm32-build-deps: build/micropython/ports/stm32/freeze/.kmk_frozen
build/micropython/ports/unix/modules/.kmk_frozen: upy-freeze.txt build/micropython/ports/unix/modules/.kmk_frozen: upy-freeze.txt submodules.toml
@echo "===> Preparing builded dependencies for local development" @echo "===> Preparing builded dependencies for local development"
@rm -rf build/micropython/ports/unix/freeze/* @rm -rf build/micropython/ports/unix/modules/*
@cat $< | xargs -I '{}' cp -a {} build/micropython/ports/unix/modules/ @cat upy-freeze.txt | egrep -v '(^#|^\s*$|^\s*\t*#)' | grep MICROPY | cut -d'|' -f2- | \
xargs -I '{}' cp -a {} build/micropython/ports/unix/modules/
@touch $@ @touch $@
build/circuitpython/ports/nrf/freeze/.kmk_frozen: upy-freeze.txt build/circuitpython/ports/atmel-samd/modules/.kmk_frozen: upy-freeze.txt submodules.toml
@echo "===> Preparing builded dependencies for bundling"
@rm -rf build/circuitpython/ports/atmel-samd/modules/*
@cat upy-freeze.txt | egrep -v '(^#|^\s*$|^\s*\t*#)' | grep CIRCUITPY | cut -d'|' -f2- | \
xargs -I '{}' cp -a {} build/circuitpython/ports/atmel-samd/modules/
@touch $@
build/circuitpython/ports/nrf/freeze/.kmk_frozen: upy-freeze.txt submodules.toml
@echo "===> Preparing builded dependencies for bundling" @echo "===> Preparing builded dependencies for bundling"
@rm -rf build/circuitpython/ports/nrf/freeze/* @rm -rf build/circuitpython/ports/nrf/freeze/*
@cat $< | xargs -I '{}' cp -a {} build/circuitpython/ports/nrf/freeze/ @cat upy-freeze.txt | egrep -v '(^#|^\s*$|^\s*\t*#)' | grep CIRCUITPY | cut -d'|' -f2- | \
xargs -I '{}' cp -a {} build/circuitpython/ports/nrf/freeze/
@touch $@ @touch $@
build/micropython/ports/teensy/freeze/.kmk_frozen: upy-freeze.txt build/micropython/ports/stm32/freeze/.kmk_frozen: upy-freeze.txt submodules.toml
@echo "===> Preparing builded dependencies for bundling"
@mkdir -p build/micropython/ports/teensy/freeze/
@rm -rf build/micropython/ports/teensy/freeze/*
@cat $< | xargs -I '{}' cp -a {} build/micropython/ports/teensy/freeze/
@touch $@
build/micropython/ports/stm32/freeze/.kmk_frozen: upy-freeze.txt
@echo "===> Preparing builded dependencies for bundling" @echo "===> Preparing builded dependencies for bundling"
@mkdir -p build/micropython/ports/stm32/freeze/ @mkdir -p build/micropython/ports/stm32/freeze/
@rm -rf build/micropython/ports/stm32/freeze/* @rm -rf build/micropython/ports/stm32/freeze/*
@cat $< | xargs -I '{}' cp -a {} build/micropython/ports/stm32/freeze/ @cat upy-freeze.txt | egrep -v '(^#|^\s*$|^\s*\t*#)' | grep MICROPY | cut -d'|' -f2- | \
xargs -I '{}' cp -a {} build/micropython/ports/stm32/freeze/
@touch $@ @touch $@
circuitpy-freeze-kmk-atmel-samd: freeze-atmel-samd-build-deps
@echo "===> Preparing KMK source for bundling into CircuitPython"
@rm -rf build/circuitpython/ports/atmel-samd/modules/kmk*
@rsync -ah kmk build/circuitpython/ports/atmel-samd/modules/
circuitpy-freeze-kmk-nrf: freeze-nrf-build-deps circuitpy-freeze-kmk-nrf: freeze-nrf-build-deps
@echo "===> Preparing KMK source for bundling into CircuitPython" @echo "===> Preparing KMK source for bundling into CircuitPython"
@rm -rf build/circuitpython/ports/nrf/kmk* @rm -rf build/circuitpython/ports/nrf/kmk*
@cp -av kmk build/circuitpython/ports/nrf/freeze/ @rsync -ah kmk build/circuitpython/ports/nrf/freeze/
micropython-freeze-kmk-teensy3.1: freeze-teensy3.1-build-deps
@echo "===> Preparing KMK source for bundling into MicroPython"
@rm -rf build/micropython/ports/teensy/kmk*
@cp -av kmk build/micropython/ports/teensy/memzip_files/
micropython-freeze-kmk-stm32: freeze-stm32-build-deps micropython-freeze-kmk-stm32: freeze-stm32-build-deps
@echo "===> Preparing KMK source for bundling into MicroPython" @echo "===> Preparing KMK source for bundling into MicroPython"
@rm -rf build/micropython/ports/stm32/freeze/kmk* @rm -rf build/micropython/ports/stm32/freeze/kmk*
@cp -av kmk build/micropython/ports/stm32/freeze/ @rsync -ah kmk build/micropython/ports/stm32/freeze/
circuitpy-build-feather-m4-express:
@echo "===> Building CircuitPython"
@pipenv run $(MAKE) -C build/circuitpython/ports/atmel-samd BOARD=feather_m4_express FROZEN_MPY_DIRS="modules" clean all
circuitpy-build-itsybitsy-m4-express:
@echo "===> Building CircuitPython"
@pipenv run $(MAKE) -C build/circuitpython/ports/atmel-samd BOARD=itsybitsy_m4_express FROZEN_MPY_DIRS="modules" clean all
circuitpy-build-nrf: circuitpy-build-nrf:
@echo "===> Building CircuitPython" @echo "===> Building CircuitPython"
@make -C build/circuitpython/ports/nrf BOARD=feather_nrf52832 SERIAL=${AMPY_PORT} SD=s132 FROZEN_MPY_DIR=freeze clean all @pipenv run $(MAKE) -C build/circuitpython/ports/nrf BOARD=feather_nrf52832 SERIAL=${AMPY_PORT} SD=s132 FROZEN_MPY_DIR=freeze clean all
circuitpy-flash-feather-m4-express:
@echo "Flashing not available for Feather M4 Express over bossa right now"
@echo "First, double tap the reset button on the Feather. You should see a red light near the USB port"
@echo "Then, find and (if necessary) mount the USB drive that will show up (should be about 4MB)"
@echo "Copy build/circuitpython/ports/atmel-samd/build-feather_m4_express/firmware.uf2 to this device"
@echo "The device will auto-reboot. You may need to forcibly unmount the drive on Linuxes, with umount -f path/to/mountpoint"
circuitpy-flash-itsybitsy-m4-express:
@echo "Flashing not available for ItsyBitsy M4 Express over bossa right now"
@echo "First, double tap the reset button on the ItsyBitsy. You should see a red light near the USB port"
@echo "Then, find and (if necessary) mount the USB drive that will show up (should be about 4MB)"
@echo "Copy build/circuitpython/ports/atmel-samd/build-itsybitsy_m4_express/firmware.uf2 to this device"
@echo "The device will auto-reboot. You may need to forcibly unmount the drive on Linuxes, with umount -f path/to/mountpoint"
circuitpy-flash-nrf: circuitpy-build-nrf circuitpy-flash-nrf: circuitpy-build-nrf
@echo "===> Flashing CircuitPython with KMK and your keymap" @echo "===> Flashing CircuitPython with KMK and your keymap"
@make -C build/circuitpython/ports/nrf BOARD=feather_nrf52832 SERIAL=${AMPY_PORT} SD=s132 FROZEN_MPY_DIR=freeze dfu-gen dfu-flash @pipenv run $(MAKE) -C build/circuitpython/ports/nrf BOARD=feather_nrf52832 SERIAL=${AMPY_PORT} SD=s132 FROZEN_MPY_DIR=freeze dfu-gen dfu-flash
micropython-build-teensy3.1:
@make -C build/micropython/ports/teensy/ BOARD=TEENSY_3.1 all
micropython-flash-teensy3.1: micropython-build-teensy3.1
@make -C build/micropython/ports/teensy/ BOARD=TEENSY_3.1 deploy
micropython-build-pyboard: micropython-build-pyboard:
@make -j4 -C build/micropython/ports/stm32/ BOARD=PYBV11 FROZEN_MPY_DIR=freeze all @pipenv run $(MAKE) -j4 -C build/micropython/ports/stm32/ BOARD=PYBV11 FROZEN_MPY_DIR=freeze all
micropython-flash-pyboard: micropython-build-pyboard micropython-flash-pyboard: micropython-build-pyboard
@make -j4 -C build/micropython/ports/stm32/ BOARD=PYBV11 FROZEN_MPY_DIR=freeze deploy @pipenv run $(MAKE) -j4 -C build/micropython/ports/stm32/ BOARD=PYBV11 FROZEN_MPY_DIR=freeze deploy
circuitpy-flash-nrf-entrypoint: circuitpy-flash-nrf-entrypoint:
@echo "===> Flashing entrypoint if it doesn't already exist" @echo "===> Flashing entrypoint if it doesn't already exist"
@ -139,6 +162,29 @@ circuitpy-flash-nrf-entrypoint:
@-timeout -k 5s 10s $(PIPENV) run ampy -p ${AMPY_PORT} -d ${AMPY_DELAY} -b ${AMPY_BAUD} put entrypoints/feather_nrf52832.py main.py @-timeout -k 5s 10s $(PIPENV) run ampy -p ${AMPY_PORT} -d ${AMPY_DELAY} -b ${AMPY_BAUD} put entrypoints/feather_nrf52832.py main.py
@echo "===> Flashed keyboard successfully!" @echo "===> Flashed keyboard successfully!"
ifndef USER_KEYMAP
build-feather-m4-express:
@echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1
flash-feather-m4-express:
@echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1
else
ifndef SKIP_KEYMAP_VALIDATION
build-feather-m4-express: lint devdeps circuitpy-deps circuitpy-freeze-kmk-atmel-samd
else
build-feather-m4-express: lint devdeps circuitpy-deps circuitpy-freeze-kmk-atmel-samd micropython-build-unix
endif
@echo "===> Preparing keyboard script for bundling into CircuitPython"
ifndef SKIP_KEYMAP_VALIDATION
@MICROPYPATH=./ ./bin/micropython.sh bin/keymap_sanity_check.py ${USER_KEYMAP}
endif
@rsync -ah ${USER_KEYMAP} build/circuitpython/ports/atmel-samd/modules/kmk_keyboard_user.py
@rsync -ah kmk/entrypoints/global.py build/circuitpython/ports/atmel-samd/modules/_main.py
@$(MAKE) circuitpy-build-feather-m4-express
flash-feather-m4-express: build-feather-m4-express circuitpy-flash-feather-m4-express
endif
ifndef USER_KEYMAP ifndef USER_KEYMAP
build-feather-nrf52832: build-feather-nrf52832:
@echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1 @echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1
@ -148,31 +194,33 @@ flash-feather-nrf52832:
else else
build-feather-nrf52832: lint devdeps circuitpy-deps circuitpy-freeze-kmk-nrf build-feather-nrf52832: lint devdeps circuitpy-deps circuitpy-freeze-kmk-nrf
@echo "===> Preparing keyboard script for bundling into CircuitPython" @echo "===> Preparing keyboard script for bundling into CircuitPython"
@cp -av ${USER_KEYMAP} build/circuitpython/ports/nrf/freeze/kmk_keyboard_user.py @rsync -ah ${USER_KEYMAP} build/circuitpython/ports/nrf/freeze/kmk_keyboard_user.py
@$(MAKE) circuitpy-build-nrf @$(MAKE) circuitpy-build-nrf
flash-feather-nrf52832: lint devdeps circuitpy-deps circuitpy-freeze-kmk-nrf flash-feather-nrf52832: build-feather-nrf52832 circuitpy-flash-nrf circuitpy-flash-nrf-endpoint
@echo "===> Preparing keyboard script for bundling into CircuitPython"
@cp -av ${USER_KEYMAP} build/circuitpython/ports/nrf/freeze/kmk_keyboard_user.py
@$(MAKE) circuitpy-flash-nrf circuitpy-flash-nrf-entrypoint
endif endif
ifndef USER_KEYMAP ifndef USER_KEYMAP
build-teensy-3.1: build-itsybitsy-m4-express:
@echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1 @echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1
flash-teensy-3.1: flash-itsybitsy-m4-express:
@echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1 @echo "===> Must provide a USER_KEYMAP (usually from user_keymaps/...) to build!" && exit 1
else else
build-teensy-3.1: lint devdeps micropython-deps micropython-freeze-kmk-teensy3.1 ifndef SKIP_KEYMAP_VALIDATION
@echo "===> Preparing keyboard script for bundling into MicroPython" build-itsybitsy-m4-express: lint devdeps circuitpy-deps circuitpy-freeze-kmk-atmel-samd
@cp -av ${USER_KEYMAP} build/micropython/ports/teensy/freeze/kmk_keyboard_user.py else
@$(MAKE) ARDUINO=${ARDUINO} micropython-build-teensy3.1 build-itsybitsy-m4-express: lint devdeps circuitpy-deps circuitpy-freeze-kmk-atmel-samd micropython-build-unix
endif
@echo "===> Preparing keyboard script for bundling into CircuitPython"
ifndef SKIP_KEYMAP_VALIDATION
@MICROPYPATH=./ ./bin/micropython.sh bin/keymap_sanity_check.py ${USER_KEYMAP}
endif
@rsync -ah ${USER_KEYMAP} build/circuitpython/ports/atmel-samd/modules/kmk_keyboard_user.py
@rsync -ah kmk/entrypoints/global.py build/circuitpython/ports/atmel-samd/modules/_main.py
@$(MAKE) circuitpy-build-itsybitsy-m4-express
flash-teensy-3.1: lint devdeps micropython-deps micropython-freeze-kmk-teensy3.1 flash-itsybitsy-m4-express: build-itsybitsy-m4-express circuitpy-flash-itsybitsy-m4-express
@echo "===> Preparing keyboard script for bundling into MicroPython"
@cp -av ${USER_KEYMAP} build/micropython/ports/teensy/freeze/kmk_keyboard_user.py
@$(MAKE) ARDUINO=${ARDUINO} micropython-flash-teensy3.1
endif endif
ifndef USER_KEYMAP ifndef USER_KEYMAP
@ -191,15 +239,12 @@ endif
ifndef SKIP_KEYMAP_VALIDATION ifndef SKIP_KEYMAP_VALIDATION
@MICROPYPATH=./ ./bin/micropython.sh bin/keymap_sanity_check.py ${USER_KEYMAP} @MICROPYPATH=./ ./bin/micropython.sh bin/keymap_sanity_check.py ${USER_KEYMAP}
endif endif
@cp -av ${USER_KEYMAP} build/micropython/ports/stm32/freeze/kmk_keyboard_user.py @rsync -ah ${USER_KEYMAP} build/micropython/ports/stm32/freeze/kmk_keyboard_user.py
@rsync -ah kmk/entrypoints/global.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 @$(MAKE) AMPY_PORT=/dev/ttyACM0 AMPY_BAUD=115200 micropython-build-pyboard
flash-pyboard: lint devdeps micropython-deps micropython-freeze-kmk-stm32 flash-pyboard: build-pyboard micropython-flash-pyboard
@echo "===> Preparing keyboard script for bundling into MicroPython"
@cp -av ${USER_KEYMAP} build/micropython/ports/stm32/freeze/kmk_keyboard_user.py
@cp -av kmk/entrypoints/global.py build/micropython/ports/stm32/freeze/_main.py
@cp -av kmk/entrypoints/handwire/pyboard_boot.py build/micropython/ports/stm32/freeze/_boot.py
@$(MAKE) micropython-flash-pyboard
endif endif
reset-bootloader: reset-bootloader:
@ -207,23 +252,3 @@ reset-bootloader:
reset-board: reset-board:
@-timeout -k 5s 10s $(PIPENV) run ampy -p /dev/ttyACM0 -d ${AMPY_DELAY} -b ${AMPY_BAUD} run util/reset.py @-timeout -k 5s 10s $(PIPENV) run ampy -p /dev/ttyACM0 -d ${AMPY_DELAY} -b ${AMPY_BAUD} run util/reset.py
# Fully wipe the board with only stock CircuitPython
burn-it-all-with-fire: lint devdeps
@echo "===> Flashing STOCK CircuitPython with no KMK or user keyboard scripts, and wiping entrypoint"
@echo "===> This is the nuclear option. Ctrl-C to cancel, or any key to continue"
@read
@echo "===> Pulling dependencies, this may take several minutes"
@git submodule update --init --recursive
@echo "===> Building circuitpython/mpy-cross"
@make -C build/circuitpython/mpy-cross
@echo "===> Pulling Nordic BLE stack"
@cd build/circuitpython/ports/nrf && ./drivers/bluetooth/download_ble_stack.sh 2>/dev/null >/dev/null
@echo "===> Preparing KMK source for bundling into CircuitPython"
@rm -rf build/circuitpython/ports/nrf/*
@echo "===> Building CircuitPython WITHOUT KMK or user keyboard script to induce ImportError"
@$(MAKE) circuitpy-flash-nrf
@echo "===> Wiping keyboard config"
@sleep 2
@-timeout -k 5s 10s $(PIPENV) run ampy -p ${AMPY_PORT} -d ${AMPY_DELAY} -b ${AMPY_BAUD} rm main.py 2>/dev/null
@echo "===> Wiped! Probably safe to flash keyboard, try Python serial REPL to verify?"

View File

@ -2,31 +2,54 @@
[![CircleCI](https://circleci.com/gh/KMKfw/kmk_firmware/tree/master.svg?style=svg)](https://circleci.com/gh/KMKfw/kmk_firmware/tree/master)[![CLA assistant](https://cla-assistant.io/readme/badge/KMKfw/kmk_firmware)](https://cla-assistant.io/KMKfw/kmk_firmware) [![CircleCI](https://circleci.com/gh/KMKfw/kmk_firmware/tree/master.svg?style=svg)](https://circleci.com/gh/KMKfw/kmk_firmware/tree/master)[![CLA assistant](https://cla-assistant.io/readme/badge/KMKfw/kmk_firmware)](https://cla-assistant.io/KMKfw/kmk_firmware)
KMK is a work-in-progress and proof-of-concept firmware for (usually mechanical) KMK is a firmware for (usually mechanical) keyboards, written in
keyboards, written in [MicroPython](https://micropython.org/) and [MicroPython](https://micropython.org/) and
[CircuitPython](https://github.com/adafruit/circuitpython). This allows for [CircuitPython](https://github.com/adafruit/circuitpython), heavily inspired by
high-level and expressive keyboard programming and creature comforts that C QMK (and with some additions of our own). Python may not be the fastest thing on
simply doesn't make easy. KMK was heavily inspired by QMK - in fact, KMK was the planet, but it's a joy to write, and bringing that ease of maintainership to
only created because QMK didn't correctly support some hardware I bought, and keyboard firmware (often a world of C and all the crazy error states C can
hacking support in was going to be a heavy uphill battle. provide) opens up custom keyboards to whole new demographics. KMK currently only
supports handwired keyboards (see "Supported Devices" below), but work has begun
on both ports to existing keyboards, as well as converter devices to allow
existing keyboards with Pro Micro pinouts to use KMK-supported microcontrollers.
As always in open-source, KMK is a work in progress, and help is welcome!
This project is currently written and maintained by: This project is currently written and maintained by:
- [Josh Klar](https://github.com/klardotsh) - [Josh Klar (@klardotsh)](https://github.com/klardotsh)
- [Kyle Brown](https://github.com/kdb424) - [Kyle Brown (@kdb424)](https://github.com/kdb424)
This project also owes a `$BEVERAGE_OF_CHOICE` to some wonderful people in the
ecosystem:
- [Jack Humbert (@jackhumbert)](https://jackhumbert.com/), for writing QMK.
Without QMK, I'd have never been exposed to the wonderful world of
programmable keyboards. He's also just an awesometastic human in general, if
you ever catch him on Discord/Reddit/etc.
- [Dan Halbert (@dhalbert)](https://danhalbert.org/), for his amazing and
unjudgemental support of two random dudes on Github asking all sorts of
bizzare (okay... and occasionally dumb) questions on the MicroPython and
CircuitPython Github projects and the Adafruit Discord. Dan, without your help
and pointers (even when those pointers are "Remember you're working with a
microcontroller with a few MHz of processing speed and a few KB of RAM"), this
project would have never gotten off the ground. Thank you, and an extended
thanks to Adafruit.
## Supported Devices ## Supported Devices
| Board | Chipset | Python Platform | Notes | | Board | Chipset | Python Platform | Notes |
| ----- | ------- | --------------- | ----- | | ----- | ------- | --------------- | ----- |
| [pyboard v1.1](https://www.adafruit.com/product/2390) | STM32F405RG (Cortex M4F) | MicroPython | A very basic keyboard has been written for this, see `boards/klardotsh/threethree_matrix_pyboard.py` | | [pyboard v1.1](https://www.adafruit.com/product/2390) | STM32F405RG (Cortex M4F) | MicroPython | Our reference board for basic USB keyboards |
| [Adafruit Feather M4 Express](https://www.adafruit.com/product/3857) | Atmel SAMD52 (Cortex M4F) | CircuitPython | A more economical solution for basic USB keyboards |
| [Adafruit ItsyBitsy M4 Express](https://www.adafruit.com/product/3800) | Atmel SAMD52 (Cortex M4F) | CircuitPython | An EVEN MORE economical solution for basic USB keyboards |
### Support Planned/WIP ### Support Planned/WIP
| Board | Chipset | Python Platform | Notes | | Board | Chipset | Python Platform | Notes |
| ----- | ------- | --------------- | ----- | | ----- | ------- | --------------- | ----- |
| [Seeed nRF52840 Micro Dev Kit](https://www.seeedstudio.com/nRF52840-Micro-Development-Kit-p-3079.html) | nRF52840 | [MicroPython](https://github.com/klardotsh/micropython/commit/4eac11a6d1ba2d269b4cdc663d4b5b788b288804) | This is basically as bleeding edge as it gets. Linked is my very unstable and somewhat broken uPy port to this device, WIP. Supports BLE HID and, allegedly, USB HID if I can figure it out. Another option is CircuitPython, since AdaFruit is working on a Feather Express with the same chipset, and commits have been made there to support USB HID. | | [Seeed nRF52840 Micro Dev Kit](https://www.seeedstudio.com/nRF52840-Micro-Development-Kit-p-3079.html) | nRF52840 | [CircuitPython](https://github.com/KMKfw/circuitpython/tree/topic-nrf52840-mdk) | This is basically as bleeding edge as it gets. Will support BLE HID to PC as well as BLE split boards |
| [Planck rev6 Keyboard](https://olkb.com/planck) | STM32 of some sort | Probably MicroPython? | I have one on the way! We'll see what happens. | | [Planck rev6 Keyboard](https://olkb.com/planck) | STM32 of some sort | MicroPython | Requires porting MicroPython to STM32F3, this work has begun but I'm pretty terrible at it. |
| [Proton C Controller?](https://www.reddit.com/r/MechanicalKeyboards/comments/87cw36/render_of_the_qmk_proton_c_qmkpowered_pro_micro/) | ??? | ??? | Does not exist yet, the controller from a Planck rev6 in a Pro Micro pin-compat controller chip | | [Proton C Controller?](https://www.reddit.com/r/MechanicalKeyboards/comments/87cw36/render_of_the_qmk_proton_c_qmkpowered_pro_micro/) | ??? | ??? | Does not exist yet, the controller from a Planck rev6 in a Pro Micro pin-compat controller chip |
@ -49,34 +72,6 @@ are currently not, due to some deficiency uncovered in development/testing:
| [Teensy 3.2 Controller](https://www.adafruit.com/product/2756) | | MicroPython | Lack of USB HID (SW - MP) | | [Teensy 3.2 Controller](https://www.adafruit.com/product/2756) | | MicroPython | Lack of USB HID (SW - MP) |
## The Great Hackaround
While it is required that at least the device talking over USB/BLE HID (the
"primary brain") be from the Supported Devices list and running the primary
component of KMK, it will soon be possible to build split keyboards with other,
otherwise unsupported devices (currently this means a Pro Micro), either to
reduce costs or to convert existing QMK boards to KMK. You'll need to flash
"dummy" firmware to each Pro Micro which simply scans a matrix and passes the
values over I2C to the "brain" device, which does the heavy lifting from there
(including actually sending HID events).
The obvious downsides of this method are increased number of moving parts,
increased number of things to flash (though the Pro Micros only need flashed
when matricies change, which should almost never happen once a board is built),
and all downsides that go with those points (increased power usage, etc.) The
upside is that it can be a _ton_ cheaper to build a split keyboard this way -
cheapo Pro Micro clones can be had for as little as $4 CAD at time of writing,
whereas a HUZZAH32, for example, is closer to $26 CAD, and to build the
"traditional" way, you'd need N of them (where N is the number of split sections
of your keyboard).
It is also possible to convert many QMK boards through this fashion - while
untested for now, just about anything with a TRRS jack should work (Ergodoxen,
just about anything from keeb.io, etc.)
This hackaround is almost certainly pointless for non-split boards.
## License, Copyright, and Legal ## License, Copyright, and Legal
This project, and all source code within (even if the file is missing headers), This project, and all source code within (even if the file is missing headers),

View File

@ -59,8 +59,6 @@ assert len(user_keymap.keymap), 'Keymap must contain at least one layer'
for lidx, layer in enumerate(user_keymap.keymap): for lidx, layer in enumerate(user_keymap.keymap):
assert len(layer), 'Layer {} must contain at least one row'.format(lidx) 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) assert all(len(row) for row in layer), 'Layer {} must not contain empty rows'.format(lidx)
assert all(len(row) == len(layer[0]) for row in user_keymap.keymap), \
'All rows in layer {} must be of the same length'.format(lidx)
for ridx, row in enumerate(layer): for ridx, row in enumerate(layer):
for cidx, key in enumerate(row): for cidx, key in enumerate(row):

39
kmk/circuitpython/hid.py Normal file
View File

@ -0,0 +1,39 @@
import usb_hid
from kmk.common.abstract.hid import AbstractHidHelper
from kmk.common.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],
)

View File

@ -2,6 +2,7 @@ import digitalio
from kmk.common.abstract.matrix_scanner import AbstractMatrixScanner from kmk.common.abstract.matrix_scanner import AbstractMatrixScanner
from kmk.common.consts import DiodeOrientation from kmk.common.consts import DiodeOrientation
from kmk.common.event_defs import matrix_changed
class MatrixScanner(AbstractMatrixScanner): class MatrixScanner(AbstractMatrixScanner):
@ -17,6 +18,7 @@ class MatrixScanner(AbstractMatrixScanner):
self.cols = [digitalio.DigitalInOut(pin) for pin in cols] self.cols = [digitalio.DigitalInOut(pin) for pin in cols]
self.rows = [digitalio.DigitalInOut(pin) for pin in rows] self.rows = [digitalio.DigitalInOut(pin) for pin in rows]
self.diode_orientation = diode_orientation self.diode_orientation = diode_orientation
self.last_pressed_len = 0
if self.diode_orientation == DiodeOrientation.COLUMNS: if self.diode_orientation == DiodeOrientation.COLUMNS:
self.outputs = self.cols self.outputs = self.cols
@ -35,15 +37,22 @@ class MatrixScanner(AbstractMatrixScanner):
for pin in self.inputs: for pin in self.inputs:
pin.switch_to_input(pull=digitalio.Pull.DOWN) pin.switch_to_input(pull=digitalio.Pull.DOWN)
def _normalize_matrix(self, matrix): def scan_for_pressed(self):
return super()._normalize_matrix(matrix) pressed = []
def raw_scan(self): for oidx, opin in enumerate(self.outputs):
matrix = []
for opin in self.outputs:
opin.value = True opin.value = True
matrix.append([ipin.value for ipin in self.inputs])
for iidx, ipin in enumerate(self.inputs):
if ipin.value:
pressed.append(
(oidx, iidx) if self.diode_orientation == DiodeOrientation.ROWS else (iidx, oidx) # noqa
)
opin.value = False opin.value = False
return self._normalize_matrix(matrix) if len(pressed) != self.last_pressed_len:
self.last_pressed_len = len(pressed)
return matrix_changed(pressed)
return None # The default, but for explicitness

155
kmk/common/abstract/hid.py Normal file
View File

@ -0,0 +1,155 @@
import logging
from kmk.common.consts import HIDReportTypes
from kmk.common.event_defs import HID_REPORT_EVENT
from kmk.common.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):
self.report_mods[0] |= modifier.code
else:
self.report_mods[0] |= modifier
return self
def remove_modifier(self, modifier):
if isinstance(modifier, ModifierKeycode):
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

View File

@ -5,10 +5,33 @@ class HIDReportTypes:
SYSCONTROL = 4 SYSCONTROL = 4
class HIDUsage:
KEYBOARD = 0x06
MOUSE = 0x02
CONSUMER = 0x01
SYSCONTROL = 0x80
class HIDUsagePage:
CONSUMER = 0x0C
KEYBOARD = MOUSE = SYSCONTROL = 0x01
# Currently only used by the CircuitPython HIDHelper because CircuitPython
# actually enforces these limits with a ValueError. Unused on PyBoard because
# we can happily send full reports there and it magically works.
HID_REPORT_SIZES = {
HIDReportTypes.KEYBOARD: 8,
HIDReportTypes.MOUSE: 4,
HIDReportTypes.CONSUMER: 2,
HIDReportTypes.SYSCONTROL: 8, # TODO find the correct value for this
}
HID_REPORT_STRUCTURE = bytes([ HID_REPORT_STRUCTURE = bytes([
# Regular keyboard # Regular keyboard
0x05, 0x01, # Usage Page (Generic Desktop) 0x05, HIDUsagePage.KEYBOARD, # Usage Page (Generic Desktop)
0x09, 0x06, # Usage (Keyboard) 0x09, HIDUsage.KEYBOARD, # Usage (Keyboard)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x85, HIDReportTypes.KEYBOARD, # Report ID (1) 0x85, HIDReportTypes.KEYBOARD, # Report ID (1)
0x05, 0x07, # Usage Page (Keyboard) 0x05, 0x07, # Usage Page (Keyboard)
@ -39,8 +62,8 @@ HID_REPORT_STRUCTURE = bytes([
0x91, 0x01, # Output (Constant) 0x91, 0x01, # Output (Constant)
0xC0, # End Collection 0xC0, # End Collection
# Regular mouse # Regular mouse
0x05, 0x01, # Usage Page (Generic Desktop) 0x05, HIDUsagePage.MOUSE, # Usage Page (Generic Desktop)
0x09, 0x02, # Usage (Mouse) 0x09, HIDUsage.MOUSE, # Usage (Mouse)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x09, 0x01, # Usage (Pointer) 0x09, 0x01, # Usage (Pointer)
0xA1, 0x00, # Collection (Physical) 0xA1, 0x00, # Collection (Physical)
@ -73,8 +96,8 @@ HID_REPORT_STRUCTURE = bytes([
0xC0, # End Collection 0xC0, # End Collection
0xC0, # End Collection 0xC0, # End Collection
# Consumer ("multimedia") keys # Consumer ("multimedia") keys
0x05, 0x0C, # Usage Page (Consumer) 0x05, HIDUsagePage.CONSUMER, # Usage Page (Consumer)
0x09, 0x01, # Usage (Consumer Control) 0x09, HIDUsage.CONSUMER, # Usage (Consumer Control)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x85, HIDReportTypes.CONSUMER, # Report ID (n) 0x85, HIDReportTypes.CONSUMER, # Report ID (n)
0x75, 0x10, # Report Size (16) 0x75, 0x10, # Report Size (16)
@ -86,8 +109,8 @@ HID_REPORT_STRUCTURE = bytes([
0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection 0xC0, # End Collection
# Power controls # Power controls
0x05, 0x01, # Usage Page (Generic Desktop Ctrls) 0x05, HIDUsagePage.SYSCONTROL, # Usage Page (Generic Desktop Ctrls)
0x09, 0x80, # Usage (Sys Control) 0x09, HIDUsage.SYSCONTROL, # Usage (Sys Control)
0xA1, 0x01, # Collection (Application) 0xA1, 0x01, # Collection (Application)
0x85, HIDReportTypes.SYSCONTROL, # Report ID (n) 0x85, HIDReportTypes.SYSCONTROL, # Report ID (n)
0x75, 0x02, # Report Size (2) 0x75, 0x02, # Report Size (2)

View File

@ -4,6 +4,7 @@ from collections import namedtuple
from micropython import const from micropython import const
from kmk.common.keycodes import Keycodes from kmk.common.keycodes import Keycodes
from kmk.common.util import reset_bootloader
KEY_UP_EVENT = const(1) KEY_UP_EVENT = const(1)
KEY_DOWN_EVENT = const(2) KEY_DOWN_EVENT = const(2)
@ -117,11 +118,7 @@ def matrix_changed(new_pressed):
dispatch(hid_report_event()) dispatch(hid_report_event())
if Keycodes.KMK.KC_RESET in state.keys_pressed: if Keycodes.KMK.KC_RESET in state.keys_pressed:
try: reset_bootloader()
import machine
machine.bootloader()
except ImportError:
logger.warning('Tried to reset to bootloader, but not supported on this chip?')
if state.pending_keys: if state.pending_keys:
for key in state.pending_keys: for key in state.pending_keys:

View File

@ -1,183 +0,0 @@
import logging
from kmk.common import kmktime
from kmk.common.event_defs import KEY_DOWN_EVENT, KEY_UP_EVENT
from kmk.common.keycodes import Keycodes, RawKeycodes
GESC_TRIGGERS = {
Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT,
Keycodes.Modifiers.KC_LGUI, Keycodes.Modifiers.KC_RGUI,
}
def process_internal_key_event(state, action_type, changed_key, logger=None):
if logger is None:
logger = logging.getLogger(__name__)
# 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
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)
else:
return state
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
# else return KC_ESC
state.keys_pressed.add(Keycodes.Common.KC_ESCAPE)
return state
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
if layer != changed_key.layer
]
else:
state.active_layers.append(changed_key.layer)
return state
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]
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:
# 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)
return state
def unicode_mode(state, action_type, changed_key, logger):
if action_type == KEY_DOWN_EVENT:
state.unicode_mode = changed_key.mode
return state
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
elif action_type == KEY_DOWN_EVENT:
if changed_key.keydown:
state.macro_pending = changed_key.keydown
return state
return state

View File

@ -1,17 +1,31 @@
import logging import logging
import sys import sys
from kmk.common import kmktime
from kmk.common.consts import DiodeOrientation, UnicodeModes from kmk.common.consts import DiodeOrientation, UnicodeModes
from kmk.common.event_defs import (HID_REPORT_EVENT, INIT_FIRMWARE_EVENT, from kmk.common.event_defs import (HID_REPORT_EVENT, INIT_FIRMWARE_EVENT,
KEY_DOWN_EVENT, KEY_UP_EVENT, KEY_DOWN_EVENT, KEY_UP_EVENT,
KEYCODE_DOWN_EVENT, KEYCODE_UP_EVENT, KEYCODE_DOWN_EVENT, KEYCODE_UP_EVENT,
MACRO_COMPLETE_EVENT, NEW_MATRIX_EVENT, MACRO_COMPLETE_EVENT, NEW_MATRIX_EVENT,
PENDING_KEYCODE_POP_EVENT) PENDING_KEYCODE_POP_EVENT)
from kmk.common.internal_keycodes import process_internal_key_event from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, Keycodes,
from kmk.common.keycodes import FIRST_KMK_INTERNAL_KEYCODE, Keycodes RawKeycodes)
GESC_TRIGGERS = {
Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT,
Keycodes.Modifiers.KC_LGUI, Keycodes.Modifiers.KC_RGUI,
}
class ReduxStore: 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): def __init__(self, reducer, log_level=logging.NOTSET):
self.reducer = reducer self.reducer = reducer
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@ -38,7 +52,7 @@ class ReduxStore:
cb(self.state, action) cb(self.state, action)
except Exception as e: except Exception as e:
self.logger.error('Callback failed, moving on') self.logger.error('Callback failed, moving on')
print(sys.print_exception(e), file=sys.stderr) sys.print_exception(e)
def get_state(self): def get_state(self):
return self.state return self.state
@ -223,3 +237,176 @@ def kmk_reducer(state=None, action=None, logger=None):
# On unhandled events, log and do not mutate state # On unhandled events, log and do not mutate state
logger.warning('Unhandled event! Returning state unmodified.') logger.warning('Unhandled event! Returning state unmodified.')
return state return state
def process_internal_key_event(state, action_type, changed_key, logger=None):
if logger is None:
logger = logging.getLogger(__name__)
# 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
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)
else:
return state
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
# else return KC_ESC
state.keys_pressed.add(Keycodes.Common.KC_ESCAPE)
return state
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
if layer != changed_key.layer
]
else:
state.active_layers.append(changed_key.layer)
return state
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]
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:
# 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)
return state
def unicode_mode(state, action_type, changed_key, logger):
if action_type == KEY_DOWN_EVENT:
state.unicode_mode = changed_key.mode
return state
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
elif action_type == KEY_DOWN_EVENT:
if changed_key.keydown:
state.macro_pending = changed_key.keydown
return state
return state

View File

@ -1,13 +1,33 @@
import math
try: try:
import utime import utime as time
USE_UTIME = True
except ImportError: except ImportError:
pass import time
USE_UTIME = False
def sleep_ms(ms):
'''
Tries to sleep for a number of milliseconds in a cross-implementation
way. Will raise an ImportError if time is not available on the platform.
'''
if USE_UTIME:
return time.sleep_ms(ms)
else:
return time.sleep(ms / 1000)
def ticks_ms(): def ticks_ms():
return utime.ticks_ms() if USE_UTIME:
return time.ticks_ms()
else:
return math.floor(time.monotonic() * 1000)
def ticks_diff(new, old): def ticks_diff(new, old):
return utime.ticks_diff(new, old) if USE_UTIME:
return time.ticks_diff(new, old)
else:
return new - old

View File

@ -3,7 +3,7 @@ import string
from kmk.common.event_defs import (hid_report_event, keycode_down_event, from kmk.common.event_defs import (hid_report_event, keycode_down_event,
keycode_up_event) keycode_up_event)
from kmk.common.keycodes import Keycodes, Macro, RawKeycodes, char_lookup from kmk.common.keycodes import Keycodes, Macro, RawKeycodes, char_lookup
from kmk.common.util import sleep_ms from kmk.common.kmktime import sleep_ms
def simple_key_sequence(seq): def simple_key_sequence(seq):

40
kmk/common/pins.py Normal file
View File

@ -0,0 +1,40 @@
try:
import board
PLATFORM = 'CircuitPython'
PIN_SOURCE = board
except ImportError:
import machine
PLATFORM = 'MicroPython'
PIN_SOURCE = machine.Pin.board
except ImportError:
from kmk.common.types import Passthrough
PLATFORM = 'Unit Testing'
PIN_SOURCE = Passthrough()
def get_pin(pin):
'''
Cross-platform method to find a pin by string.
The pin definitions are platform-dependent, but this provides
a way to say "I'm using pin D20" without rolling a D20 and
having to actually learn MicroPython/CircuitPython and the
differences in how they handle pinouts.
This also makes the keymap sanity checker actually work for
CircuitPython boards, since it's not possible in CPY to
define a module stub for `board` that uses Passthrough
natively (which is how the MicroPython stub worked originally)
'''
return getattr(PIN_SOURCE, pin)
class PinLookup:
def __getattr__(self, attr):
return get_pin(attr)
Pin = PinLookup()

View File

@ -8,3 +8,19 @@ class AttrDict(dict):
''' '''
def __getattr__(self, key): def __getattr__(self, key):
return self[key] return self[key]
class Anything:
'''
A stub class which will repr as a provided name
'''
def __init__(self, name):
self.name = name
def __repr__(self):
return 'Anything<{}>'.format(self.name)
class Passthrough:
def __getattr__(self, attr):
return Anything(attr)

View File

@ -29,16 +29,3 @@ def reset_bootloader():
import microcontroller import microcontroller
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER) microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset() microcontroller.reset()
def sleep_ms(ms):
'''
Tries to sleep for a number of milliseconds in a cross-implementation
way. Will raise an ImportError if time is not available on the platform.
'''
try:
import time
time.sleep_ms(ms)
except AttributeError:
import time
time.sleep(ms / 1000)

View File

@ -0,0 +1,33 @@
import sys
from logging import WARNING
from kmk.circuitpython.hid import HIDHelper
from kmk.circuitpython.matrix import MatrixScanner
from kmk.common.consts import UnicodeModes
from kmk.firmware import Firmware
def main():
from kmk_keyboard_user import cols, diode_orientation, keymap, rows
try:
from kmk_keyboard_user import unicode_mode
except Exception:
unicode_mode = UnicodeModes.NOOP
try:
firmware = Firmware(
keymap=keymap,
row_pins=rows,
col_pins=cols,
diode_orientation=diode_orientation,
unicode_mode=unicode_mode,
log_level=WARNING,
matrix_scanner=MatrixScanner,
hid=HIDHelper,
)
firmware.go()
except Exception as e:
sys.print_exception(e)
sys.exit(1)

View File

@ -0,0 +1,33 @@
import sys
from logging import WARNING
from kmk.circuitpython.hid import HIDHelper
from kmk.circuitpython.matrix import MatrixScanner
from kmk.common.consts import UnicodeModes
from kmk.firmware import Firmware
def main():
from kmk_keyboard_user import cols, diode_orientation, keymap, rows
try:
from kmk_keyboard_user import unicode_mode
except Exception:
unicode_mode = UnicodeModes.NOOP
try:
firmware = Firmware(
keymap=keymap,
row_pins=rows,
col_pins=cols,
diode_orientation=diode_orientation,
unicode_mode=unicode_mode,
log_level=WARNING,
matrix_scanner=MatrixScanner,
hid=HIDHelper,
)
firmware.go()
except Exception as e:
sys.print_exception(e)
sys.exit(1)

View File

@ -3,6 +3,7 @@ from logging import DEBUG
from kmk.common.consts import UnicodeModes from kmk.common.consts import UnicodeModes
from kmk.firmware import Firmware from kmk.firmware import Firmware
from kmk.micropython.matrix import MatrixScanner
from kmk.micropython.pyb_hid import HIDHelper from kmk.micropython.pyb_hid import HIDHelper
@ -23,6 +24,7 @@ def main():
unicode_mode=unicode_mode, unicode_mode=unicode_mode,
hid=HIDHelper, hid=HIDHelper,
log_level=DEBUG, log_level=DEBUG,
matrix_scanner=MatrixScanner,
) )
firmware.go() firmware.go()

View File

@ -1,12 +1,7 @@
import logging import logging
from kmk.common.event_defs import init_firmware from kmk.common.event_defs import init_firmware
from kmk.common.internal_state import ReduxStore, kmk_reducer from kmk.common.internal_state import Store, kmk_reducer
try:
from kmk.circuitpython.matrix import MatrixScanner
except ImportError:
from kmk.micropython.matrix import MatrixScanner
class Firmware: class Firmware:
@ -14,25 +9,29 @@ class Firmware:
self, keymap, row_pins, col_pins, self, keymap, row_pins, col_pins,
diode_orientation, unicode_mode=None, diode_orientation, unicode_mode=None,
hid=None, log_level=logging.NOTSET, hid=None, log_level=logging.NOTSET,
matrix_scanner=None,
): ):
assert matrix_scanner is not None
self.matrix_scanner = matrix_scanner
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(log_level) logger.setLevel(log_level)
self.hydrated = False self.hydrated = False
self.store = ReduxStore(kmk_reducer, log_level=log_level) self.store = Store(kmk_reducer, log_level=log_level)
self.store.subscribe( self.store.subscribe(
lambda state, action: self._subscription(state, action), lambda state, action: self._subscription(state, action),
) )
if not hid: if hid:
self.hid = hid(store=self.store, log_level=log_level)
else:
logger.warning( logger.warning(
"Must provide a HIDHelper (arg: hid), disabling HID\n" "Must provide a HIDHelper (arg: hid), disabling HID\n"
"Board will run in debug mode", "Board will run in debug mode",
) )
self.hid = hid(store=self.store, log_level=log_level)
self.store.dispatch(init_firmware( self.store.dispatch(init_firmware(
keymap=keymap, keymap=keymap,
row_pins=row_pins, row_pins=row_pins,
@ -43,7 +42,7 @@ class Firmware:
def _subscription(self, state, action): def _subscription(self, state, action):
if not self.hydrated: if not self.hydrated:
self.matrix = MatrixScanner( self.matrix = self.matrix_scanner(
state.col_pins, state.col_pins,
state.row_pins, state.row_pins,
state.diode_orientation, state.diode_orientation,

View File

@ -1,11 +1,7 @@
import logging
from pyb import USB_HID, delay, hid_keyboard from pyb import USB_HID, delay, hid_keyboard
from kmk.common.consts import HID_REPORT_STRUCTURE, HIDReportTypes from kmk.common.abstract.hid import AbstractHidHelper
from kmk.common.event_defs import HID_REPORT_EVENT from kmk.common.consts import HID_REPORT_STRUCTURE
from kmk.common.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, ConsumerKeycode,
ModifierKeycode)
def generate_pyb_hid_descriptor(): def generate_pyb_hid_descriptor():
@ -14,80 +10,20 @@ def generate_pyb_hid_descriptor():
return tuple(existing_keyboard) return tuple(existing_keyboard)
class HIDHelper: class HIDHelper(AbstractHidHelper):
def __init__(self, store, log_level=logging.NOTSET): # For some bizarre reason this can no longer be 8, it'll just fail to send
self.logger = logging.getLogger(__name__) # anything. This is almost certainly a bug in the report descriptor sent
self.logger.setLevel(log_level) # 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.
self.store = store REPORT_BYTES = 7
self.store.subscribe(
lambda state, action: self._subscription(state, action),
)
def post_init(self):
self._hid = USB_HID() self._hid = USB_HID()
self.hid_send = self._hid.send
# 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.
self._evt = bytearray(7)
self.report_device = memoryview(self._evt)[0:1]
# 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:]
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 send(self): def send(self):
self.logger.debug('Sending HID report: {}'.format(self._evt)) self.logger.debug('Sending HID report: {}'.format(self._evt))
self._hid.send(self._evt) self.hid_send(self._evt)
# Without this delay, events get clobbered and you'll likely end up with # Without this delay, events get clobbered and you'll likely end up with
# a string like `heloooooooooooooooo` rather than `hello`. This number # a string like `heloooooooooooooooo` rather than `hello`. This number
@ -101,69 +37,3 @@ class HIDHelper:
delay(5) delay(5)
return self 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):
self.report_mods[0] |= modifier.code
else:
self.report_mods[0] |= modifier
return self
def remove_modifier(self, modifier):
if isinstance(modifier, ModifierKeycode):
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

View File

@ -1,4 +1,4 @@
[submodules] [submodules]
"vendor/circuitpython" = "d751740" "vendor/circuitpython" = "5c606de"
"vendor/micropython" = "65a49fa" "vendor/micropython" = "65a49fa"
"vendor/upy-lib" = "f20d89c" "vendor/upy-lib" = "451b1c0"

View File

@ -1,3 +1,5 @@
vendor/upy-lib/collections/collections # CircuitPython provides collections, don't overwrite it
vendor/upy-lib/logging/logging.py MICROPY|vendor/upy-lib/collections/collections
vendor/upy-lib/string/string.py
MICROPYCIRCUITPY|vendor/upy-lib/logging/logging.py
MICROPYCIRCUITPY|vendor/upy-lib/string/string.py

View File

@ -0,0 +1,3 @@
class DigitalInOut:
def __init__(self, *args, **kwargs):
pass

View File

@ -0,0 +1 @@
devices = []

View File

@ -1,14 +1,11 @@
import machine
from kmk.common.consts import DiodeOrientation, UnicodeModes from kmk.common.consts import DiodeOrientation, UnicodeModes
from kmk.common.keycodes import KC from kmk.common.keycodes import KC
from kmk.common.macros.unicode import unicode_sequence from kmk.common.macros.unicode import unicode_sequence
from kmk.common.pins import Pin as P
from kmk.entrypoints.handwire.pyboard import main from kmk.entrypoints.handwire.pyboard import main
p = machine.Pin.board 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)
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 diode_orientation = DiodeOrientation.COLUMNS

View File

@ -0,0 +1,73 @@
from kmk.common.consts import DiodeOrientation, UnicodeModes
from kmk.common.keycodes import KC
from kmk.common.macros.simple import send_string, simple_key_sequence
from kmk.common.macros.unicode import unicode_sequence
from kmk.common.pins import Pin as P
from kmk.entrypoints.handwire.feather_m4_express 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
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_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],
],
]

View File

@ -0,0 +1,64 @@
from kmk.common.consts import DiodeOrientation, UnicodeModes
from kmk.common.keycodes import KC
from kmk.common.macros.simple import send_string, simple_key_sequence
from kmk.common.macros.unicode import unicode_sequence
from kmk.common.pins import Pin as P
from kmk.entrypoints.handwire.itsybitsy_m4_express import main
from kmk.firmware import Firmware
cols = (P.A4, P.A5, P.D7)
rows = (P.D12, P.D11, P.D10)
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_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.MUTE, KC.B, KC.C],
[KC.TRNS, KC.D, KC.E],
[KC.F, KC.G, KC.H],
],
[
[KC.VOLU, KC.MUTE, ANGRY_TABLE_FLIP],
[KC.NO, KC.PIPE, MACRO_TEST_SIMPLE],
[KC.TRNS, KC.P, MACRO_TEST_STRING],
],
]

View File

@ -1,14 +1,12 @@
import machine
from kmk.common.consts import DiodeOrientation, UnicodeModes from kmk.common.consts import DiodeOrientation, UnicodeModes
from kmk.common.keycodes import KC from kmk.common.keycodes import KC
from kmk.common.macros.simple import send_string, simple_key_sequence from kmk.common.macros.simple import send_string, simple_key_sequence
from kmk.common.macros.unicode import unicode_sequence from kmk.common.macros.unicode import unicode_sequence
from kmk.common.pins import Pin as P
from kmk.entrypoints.handwire.pyboard import main from kmk.entrypoints.handwire.pyboard import main
p = machine.Pin.board cols = (P.X10, P.X11, P.X12)
cols = (p.X10, p.X11, p.X12) rows = (P.X1, P.X2, P.X3)
rows = (p.X1, p.X2, p.X3)
diode_orientation = DiodeOrientation.COLUMNS diode_orientation = DiodeOrientation.COLUMNS
unicode_mode = UnicodeModes.LINUX unicode_mode = UnicodeModes.LINUX

@ -1 +1 @@
Subproject commit d7517409e93be2b098b463cf5430c8dbc76cef07 Subproject commit 5c606deecc93c573516c46787486cee846a6d599

2
vendor/upy-lib vendored

@ -1 +1 @@
Subproject commit f20d89c6aad9443a696561ca2a01f7ef0c8fb302 Subproject commit 451b1c07567de85062ef35b672b2101647285e9a