32 Commits

Author SHA1 Message Date
Kyle Brown
d7e43f7d0d Continue split refactor 2022-10-09 13:17:05 -07:00
xs5871
ec6bb9564b draft concept for ble-split to transport refactor 2022-10-09 10:46:06 -07:00
xs5871
17f2961c0b fix pystack exhaust during resume_process_key.
Instead of handling resumed key events in a deep stack, buffer them
until the next main loop iteration. New resume events that may be emitted
during handling of old resumes are prepended to that buffer, i.e. take
precedence over events that happen deeper into the buffer/event stack.
Logical replay order is thus preserved.
2022-10-08 13:36:00 -07:00
xs5871
0fbba96026 add type hinting for kmk.keys 2022-10-08 13:28:29 -07:00
Kyle Brown
2f0134eed3 Update docs/quickpin.md
Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com>

Update docs/quickpin.md

Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com>

Update docs/quickpin.md

Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com>
2022-10-08 19:48:30 +00:00
Kyle Brown
75f535c977 Initial port of zodiark with quickpin 2022-10-08 19:48:30 +00:00
Kyle Brown
af3febc469 Add image to make numbers easier to understand 2022-10-08 19:48:30 +00:00
Kyle Brown
9431ce5ec1 Quickpin initial support 2022-10-08 19:48:30 +00:00
Kyle Brown
03c8a61ed0 Boot.py docs better example 2022-10-05 18:22:31 +00:00
Kyle Brown
711114ad30 Passes tests 2022-10-04 21:29:50 +00:00
Brian Corteil
d965a5c3e8 added Raspberry Pi Pico pinouts
added Raspberry Pi Pico pinouts to keybow.py and auto selection.
2022-10-04 21:29:50 +00:00
xs5871
218c15e322 even more typehints 2022-10-03 18:12:48 -07:00
xs5871
565ec8353b continue/finish holdtap-repeat 2022-10-02 09:54:52 -07:00
Aldoo
5efd2688d7 Support for tap behavior autorepeat for modtap and layertap keys.
Faulty logic corrected

Possibly a bug could happen whenever entering ht_press with a non-empty key_state outside of repeat context (but can it really happen?)

Some formatting to make github tests happy

Documentation for repeat behavior.

Make the tests happy again!

same...
2022-10-02 09:54:52 -07:00
xs5871
91032e123f Howto RGB Layers
Very common question, very simple answer.
Comments and suggestions welcome
2022-10-01 12:36:08 -07:00
topicref
54a1d61a06 fixes small typo in media key keycodes table 2022-09-27 18:55:36 -07:00
xs5871
a97e655d08 fix typo 2022-09-26 00:17:23 -07:00
xs5871
f2fb4eecf4 add type hinting for kmk_keyboard 2022-09-22 15:35:14 -07:00
xs5871
63fd4d9574 add debug info for pixelbuffer to RGB extension 2022-09-22 08:22:22 -07:00
Kyle Brown
002a57b3b1 Replace tabs with spaces 2022-09-20 08:09:04 +00:00
Kyle Brown
dd488686c7 sequences update with new example 2022-09-20 08:09:04 +00:00
Anton K. (ai Doge)
891cd1a67f Add key coord combos
Added an option to use key coord in the matrix to make combos.
this allows to press / release combos regardless of layer and Key.
2022-09-20 08:04:57 +00:00
Ian Pearce
ea3f59340d Set sandbox.matrix_update
Previously only secondary_matrix_update was set on sandbox.
2022-09-19 07:51:45 +00:00
xs5871
85b5aba679 make rgb.val_limit apply globally 2022-09-16 17:59:38 -07:00
xs5871
44233bbb01 bring rgb docs somewhat up-to-date 2022-09-16 17:59:24 -07:00
Kyle Brown
0909c6767d dstore gitignore 2022-09-15 20:35:54 -07:00
Kyle Brown
a21bf761ba Update peg_rgb_matrix.py
Actually fix formatting
2022-09-15 20:24:45 -07:00
Kyle Brown
01cd9d4a7c Apply suggestions from code review
Fix formatting
2022-09-15 20:24:45 -07:00
Jan Lindblom
3ab11179e1 Updated and linted documentation for peg_rgb_matrix. 2022-09-15 20:24:45 -07:00
Jan Lindblom
352bf25c0d Decrease RGB brightness when in powersave. Also add more colours. 2022-09-15 20:24:45 -07:00
Jan Lindblom
9c85ea9b6b Performance improvements. 2022-09-15 20:24:45 -07:00
Jan Lindblom
5f09ba7c0a Add support for changing LED brightness. 2022-09-15 20:24:45 -07:00
40 changed files with 1998 additions and 681 deletions

2
.gitignore vendored
View File

@@ -180,3 +180,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
# Mac Finder
.DS_Store

View File

@@ -1,12 +1,12 @@
import board import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.quickpin.pro_micro.boardsource_blok import pinout as pins
from kmk.scanners import DiodeOrientation from kmk.scanners import DiodeOrientation
class KMKKeyboard(_KMKKeyboard): class KMKKeyboard(_KMKKeyboard):
row_pins = (board.P1_15, board.P0_02, board.P0_29) row_pins = (pins[16], pins[17], pins[18])
col_pins = (board.P0_09, board.P0_10, board.P1_11, board.P1_13) col_pins = (pins[12], pins[13], pins[14], pins[15])
diode_orientation = DiodeOrientation.COLUMNS diode_orientation = DiodeOrientation.COLUMNS
i2c = board.I2C i2c = board.I2C
powersave_pin = board.P0_13

View File

@@ -1,30 +1,35 @@
import board import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.quickpin.pro_micro.boardsource_blok import pinout as pins
from kmk.scanners import DiodeOrientation from kmk.scanners import DiodeOrientation
class KMKKeyboard(_KMKKeyboard): class KMKKeyboard(_KMKKeyboard):
col_pins = ( col_pins = (
board.P0_31, pins[19],
board.P0_29, pins[18],
board.P0_02, pins[17],
board.P1_15, pins[16],
board.P1_13, pins[15],
board.P1_11, pins[14],
)
row_pins = (
pins[6],
pins[7],
pins[8],
pins[9],
) )
row_pins = (board.P0_22, board.P0_24, board.P1_00, board.P0_11)
diode_orientation = DiodeOrientation.COLUMNS diode_orientation = DiodeOrientation.COLUMNS
data_pin = board.P0_08 data_pin = pins[1]
rgb_pixel_pin = board.P0_06 rgb_pixel_pin = pins[0]
i2c = board.I2C i2c = board.I2C
powersave_pin = board.P0_13
# flake8: noqa # flake8: noqa
# fmt: off
coord_mapping = [ coord_mapping = [
0, 1, 2, 3, 4, 5, 29, 28, 27, 26, 25, 24, 0, 1, 2, 3, 4, 5, 29, 28, 27, 26, 25, 24,
6, 7, 8, 9, 10, 11, 35, 34, 33, 32, 31, 30, 6, 7, 8, 9, 10, 11, 35, 34, 33, 32, 31, 30,
12, 13, 14, 15, 16, 17, 41, 40, 39, 38, 37, 36, 12, 13, 14, 15, 16, 17, 41, 40, 39, 38, 37, 36,
21, 22, 23, 47, 46, 45, 21, 22, 23, 47, 46, 45,
] ]

View File

@@ -1,28 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.scanners import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.A3,
board.A2,
board.A1,
board.A0,
board.SCK,
board.MISO,
)
row_pins = (board.D4, board.D5, board.D6, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
data_pin = board.RX
rgb_pixel_pin = board.D0
i2c = board.I2C
# flake8: noqa
coord_mapping = [
0, 1, 2, 3, 4, 5, 29, 28, 27, 26, 25, 24,
6, 7, 8, 9, 10, 11, 35, 34, 33, 32, 31, 30,
12, 13, 14, 15, 16, 17, 41, 40, 39, 38, 37, 36,
21, 22, 23, 47, 46, 45,
]

View File

@@ -41,8 +41,9 @@ from kmk.extensions.rgb import RGB, AnimationModes
from kmk.kmk_keyboard import KMKKeyboard from kmk.kmk_keyboard import KMKKeyboard
from kmk.scanners.keypad import KeysScanner from kmk.scanners.keypad import KeysScanner
# fmt: off # fmt: off
def raspi_pins(): def raspi_pins():
return [ return [
board.D20, board.D16, board.D26, board.D20, board.D16, board.D26,
@@ -52,6 +53,15 @@ def raspi_pins():
] ]
def rp2040_pins():
return [
board.GP7, board.GP8, board.GP27,
board.GP9, board.GP26, board.GP10,
board.GP11, board.GP18, board.GP12,
board.GP16, board.GP17, board.GP14
]
def itsybitsy_pins(): def itsybitsy_pins():
return [ return [
board.D11, board.D12, board.D2, board.D11, board.D12, board.D2,
@@ -59,6 +69,7 @@ def itsybitsy_pins():
board.A5, board.A4, board.A3, board.A5, board.A4, board.A3,
board.A2, board.A1, board.A0, board.A2, board.A1, board.A0,
] ]
# fmt: on # fmt: on
@@ -66,9 +77,17 @@ def isPi():
return sys.platform == 'BROADCOM' return sys.platform == 'BROADCOM'
def isRP2040():
return sys.platform == 'RP2040'
if isPi(): if isPi():
_KEY_CFG = raspi_pins() _KEY_CFG = raspi_pins()
_LED_PINS = (board.SCK, board.MOSI) _LED_PINS = (board.SCK, board.MOSI)
elif isRP2040():
_KEY_CFG = rp2040_pins()
_LED_PINS = (board.GP2, board.GP3)
else: else:
_KEY_CFG = itsybitsy_pins() _KEY_CFG = itsybitsy_pins()
_LED_PINS = (board.SCK, board.MOSI) _LED_PINS = (board.SCK, board.MOSI)

15
boards/zodiark/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Zodiark
![Zodiark](https://camo.githubusercontent.com/b5283aea8fe39b0646a405fd358aa0c2c0f5896fc0fb80a92b5e761149759214/68747470733a2f2f692e696d6775722e636f6d2f34394f38616f776c2e6a7067)
A split keyboard with 5x7 including a thumbcluster, encoders on each side, per
key RGB, and 2x I2C headers per side, supporting 1.3"/.96" 128x64 OLEDs (the
1.3" is an SSH1106 OLED, .91" 128x32 OLEDs.
Hardware Availability: Pending Group Buy - [Discord Link](https://discord.gg/BCSbXwskVt)
Extensions enabled by default
- [Split](/docs/split.md) Connects halves using a wire.
- [Layers](/docs/layers.md) Need more keys than switches? Use layers.
- [PEG_RGB](/docs/peg_rgb_matrix.md) Light it up!
- [PEG_OLED](/docs/peg_oled_display.md) Screens to see things on of course.

68
boards/zodiark/kb.py Normal file
View File

@@ -0,0 +1,68 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.scanners import DiodeOrientation
from kmk.scanners.keypad import MatrixScanner
from kmk.quickpin.pro_micro.boardsource_blok import pinout as pins
from kmk.quickpin.pro_Micro.avr_promicro import avr
class KMKKeyboard(_KMKKeyboard):
def __init__(self):
# create and register the scanner
self.matrix = [
MatrixScanner(
# required arguments:
column_pins=self.col_pins,
row_pins=self.row_pins,
# optional arguments with defaults:
columns_to_anodes=DiodeOrientation.COL2ROW,
interval=0.02,
max_events=64,
),
]
col_pins = (
pins[avr['F5']],
pins[avr['F6']],
pins[avr['F7']],
pins[avr['B1']],
pins[avr['B3']],
pins[avr['B2']],
pins[avr['B6']],
)
row_pins = (
pins[avr['C6']],
pins[avr['D7']],
pins[avr['E6']],
pins[avr['B4']],
pins[avr['F4']],
)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = pins[avr['B5']]
data_pin = pins[avr['D3']]
i2c = board.I2C
SCL = board.SCL
SDA = board.SDA
# NOQA
# flake8: noqa
# fmt: off
led_key_pos =[
5, 4, 3, 2, 01, 00, 34, 35, 36, 37, 38, 39,
6, 7, 8, 9, 10, 11, 12, 46, 45, 44, 43, 42, 41, 40,
19, 18, 17, 16, 15, 14, 13, 47, 48, 49, 50, 51, 52, 53,
20, 21, 22, 23, 24, 25, 26, 60, 59, 58, 57, 56, 55, 54,
33, 32, 31, 30, 29, 28, 27, 61, 62, 63, 64, 65, 66, 67
]
brightness_limit = 0.5
num_pixels = 62
# NOQA
# flake8: noqa
coord_mapping = [
0, 1, 2, 3, 4, 5, 40, 39, 38, 37, 36, 35,
7, 8, 9, 10, 11, 12, 06, 41, 47, 46, 45, 44, 43, 42,
14, 15, 16, 17, 18, 19, 13, 48, 54, 53, 52, 51, 50, 49,
21, 22, 23, 24, 25, 26, 20, 27, 62, 55, 61, 60, 59, 58, 57, 56,
28, 29, 30, 31, 32, 33, 34, 69, 68, 67, 66, 65, 64, 63
]

709
boards/zodiark/main.py Normal file
View File

@@ -0,0 +1,709 @@
import supervisor
from kb import KMKKeyboard
from kmk.extensions.peg_oled_Display import (
Oled,
OledData,
OledDisplayMode,
OledReactionType,
)
from kmk.extensions.peg_rgb_matrix import Rgb_matrix
from kmk.handlers.sequences import send_string
from kmk.hid import HIDModes
from kmk.keys import KC
from kmk.modules.layers import Layers
from kmk.modules.modtap import ModTap
from kmk.modules.split import Split, SplitSide, SplitType
keyboard = KMKKeyboard()
modtap = ModTap()
layers_ext = Layers()
keyboard.modules.append(layers_ext)
keyboard.modules.append(modtap)
oled_ext = Oled(
OledData(
corner_one={0: OledReactionType.STATIC, 1: ['qwertyzzzz']},
corner_two={
0: OledReactionType.LAYER,
1: ['1', '2', '3', '4', '5', '6', '7', '8'],
},
corner_three={
0: OledReactionType.LAYER,
1: ['base', 'raise', 'lower', 'adjust', '5', '6', '7', '8'],
},
corner_four={
0: OledReactionType.LAYER,
1: ['qwertyzzz', 'nums', 'shifted', 'leds', '5', '6', '7', '8'],
},
),
toDisplay=OledDisplayMode.TXT,
flip=False,
)
keyboard.extensions.append(oled_ext)
# Default RGB matrix colours
rgb_ext = Rgb_matrix(
ledDisplay=[
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
[80, 0, 80],
],
split=True,
rightSide=False,
disable_auto_write=True,
)
keyboard.extensions.append(rgb_ext)
# TODO Comment one of these on each side
split_side = SplitSide.LEFT
# split_side = SplitSide.RIGHT
split = Split(data_pin=keyboard.data_pin)
keyboard.modules.append(split)
keyboard.keymap = [
[
KC.F1,
KC.F2,
KC.F3,
KC.F4,
KC.F5,
KC.F6,
KC.F7,
KC.F8,
KC.F9,
KC.F10,
KC.F11,
KC.F12,
KC.ESC,
KC.N1,
KC.N2,
KC.N3,
KC.N4,
KC.N5,
KC.N6,
KC.N7,
KC.N8,
KC.N9,
KC.N0,
KC.GRV,
KC.TAB,
KC.Q,
KC.W,
KC.E,
KC.R,
KC.T,
KC.NO,
KC.NO,
KC.Y,
KC.U,
KC.I,
KC.O,
KC.P,
KC.MINS,
KC.LCTL,
KC.A,
KC.S,
KC.D,
KC.F,
KC.G,
KC.NO,
KC.NO,
KC.H,
KC.J,
KC.K,
KC.L,
KC.SCLN,
KC.QUOT,
KC.LSFT,
KC.Z,
KC.X,
KC.C,
KC.V,
KC.B,
KC.LBRC,
KC.RBRC,
KC.N,
KC.M,
KC.COMMA,
KC.DOT,
KC.SLSH,
KC.RSFT,
KC.NO,
KC.NO,
KC.NO,
KC.LGUI,
KC.MO(1),
KC.LCTL,
KC.SPC,
KC.ENT,
KC.MO(2),
KC.BSPC,
KC.RGUI,
KC.NO,
KC.NO,
KC.NO,
],
[
KC.F1,
KC.F2,
KC.F3,
KC.F4,
KC.F5,
KC.F6,
KC.F7,
KC.F8,
KC.F9,
KC.F10,
KC.F11,
KC.F12,
KC.TRNS,
KC.TRNS,
KC.UP,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.EQL,
KC.TRNS,
KC.TRNS,
KC.LEFT,
KC.DOWN,
KC.RGHT,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.LEFT,
KC.DOWN,
KC.RGHT,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.DEL,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
],
[
KC.N2,
KC.EXLM,
KC.AT,
KC.HASH,
KC.DLR,
KC.PERC,
KC.CIRC,
KC.AMPR,
KC.ASTR,
KC.LPRN,
KC.RPRN,
KC.TILD,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.PLUS,
KC.UNDS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.LCBR,
KC.RCBR,
KC.TRNS,
KC.TRNS,
KC.LABK,
KC.RABK,
KC.QUES,
KC.TRNS,
],
[
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
],
[
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
],
[
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
],
[
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
],
[
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
KC.TRNS,
],
]
if __name__ == '__main__':
keyboard.go(hid_type=HIDModes.USB)

View File

@@ -38,18 +38,21 @@ import storage
import usb_cdc import usb_cdc
import usb_hid import usb_hid
# This is from the base kmk boot.py from kb import KMKKeyboard
supervisor.set_next_stack_limit(4096 + 4096) from kmk.scanners import DiodeOrientation
# If this key is held during boot, don't run the code which hides the storage and disables serial # If this key is held during boot, don't run the code which hides the storage and disables serial
# To use another key just count its row and column and use those pins # This will use the first row/col pin. Feel free to change it if you want it to be another pin
# You can also use any other pins not already used in the matrix and make a button just for accesing your storage col = digitalio.DigitalInOut(KMKKeyboard.col_pins[0])
col = digitalio.DigitalInOut(board.GP2) row = digitalio.DigitalInOut(KMKKeyboard.row_pins[0])
row = digitalio.DigitalInOut(board.GP13)
# TODO: If your diode orientation is ROW2COL, then make row the output and col the input if KMKKeyboard.diode_orientation == DiodeOrientation.COLUMNS:
col.switch_to_output(value=True) col.switch_to_output(value=True)
row.switch_to_input(pull=digitalio.Pull.DOWN) row.switch_to_input(pull=digitalio.Pull.DOWN)
else:
col.switch_to_input(pull=digitalio.Pull.DOWN)
row.switch_to_output(value=True)
if not row.value: if not row.value:
storage.disable_usb_drive() storage.disable_usb_drive()

View File

@@ -27,6 +27,7 @@ Optional arguments that customize individual combos:
* `per_key_timeout`: If True, reset timeout on every key press (default for * `per_key_timeout`: If True, reset timeout on every key press (default for
sequences). sequences).
* `timeout`: Set the time window within which the match has to happen in ms. * `timeout`: Set the time window within which the match has to happen in ms.
* `match_coord`: If True, matches key position in the matrix.
## Example Code ## Example Code
```python ```python
@@ -43,6 +44,8 @@ make_key(
combos.combos = [ combos.combos = [
Chord((KC.A, KC.B), KC.LSFT), Chord((KC.A, KC.B), KC.LSFT),
Chord((KC.A, KC.B, KC.C), KC.LALT), Chord((KC.A, KC.B, KC.C), KC.LALT),
Chord((0, 1), KC.ESC, match_coord=True),
Chord((8, 9, 10), KC.MO(4), match_coord=True),
Sequence((KC.LEADER, KC.A, KC.B), KC.C), Sequence((KC.LEADER, KC.A, KC.B), KC.C),
Sequence((KC.E, KC.F), KC.MYKEY, timeout=500, per_key_timeout=False, fast_reset=False) Sequence((KC.E, KC.F), KC.MYKEY, timeout=500, per_key_timeout=False, fast_reset=False)
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

View File

@@ -68,3 +68,26 @@ keyboard.keymap = [
], ],
] ]
``` ```
## Advanced Example
A common question is: "How do I change RGB background based on my active layer?"
Here is _one_ (simple) way of many to go about it.
```python
from kmk.modules.layers import Layers as _Layers
from kmk.extensions.rgb import RGB
rgb = RGB(...) # your RGB configuration goes here
keyboard.extensions.append(rgb)
class Layers(_Layers):
last_top_layer = 0
hues = (4, 20, 69)
def after_hid_send(keyboard):
if keyboard.active_layers[0] != self.last_top_layer:
self.last_top_layer = keyboard.active_layers[0]
rgb.set_hsv_fill(self.hues[self.last_top_layer], 255, 255)
keyboard.modules.append(Layers())
```

View File

@@ -14,7 +14,7 @@ keyboard.extensions.append(MediaKeys())
|`KC.AUDIO_MUTE` |`KC.MUTE` |Mute | |`KC.AUDIO_MUTE` |`KC.MUTE` |Mute |
|`KC.AUDIO_VOL_UP` |`KC.VOLU` |Volume Up | |`KC.AUDIO_VOL_UP` |`KC.VOLU` |Volume Up |
|`KC.AUDIO_VOL_DOWN` |`KC.VOLD` |Volume Down | |`KC.AUDIO_VOL_DOWN` |`KC.VOLD` |Volume Down |
|`KC.BRIGHTESS_UP` |`KC.BRIU` |Brightness Up | |`KC.BRIGHTNESS_UP` |`KC.BRIU` |Brightness Up |
|`KC.BRIGHTNESS_DOWN` |`KC.BRID` |Brightness Down | |`KC.BRIGHTNESS_DOWN` |`KC.BRID` |Brightness Down |
|`KC.MEDIA_NEXT_TRACK` |`KC.MNXT` |Next Track (Windows) | |`KC.MEDIA_NEXT_TRACK` |`KC.MNXT` |Next Track (Windows) |
|`KC.MEDIA_PREV_TRACK` |`KC.MPRV` |Previous Track (Windows) | |`KC.MEDIA_PREV_TRACK` |`KC.MPRV` |Previous Track (Windows) |

View File

@@ -31,7 +31,7 @@ keyboard.modules.append(modtap)
## Custom HoldTap Behavior ## Custom HoldTap Behavior
The full ModTap signature is as follows: The full ModTap signature is as follows:
```python ```python
KC.MT(KC.TAP, KC.HOLD, prefer_hold=True, tap_interrupted=False, tap_time=None) KC.MT(KC.TAP, KC.HOLD, prefer_hold=True, tap_interrupted=False, tap_time=None, repeat=False)
``` ```
* `prefer_hold`: decides which keycode the ModTap key resolves to when another * `prefer_hold`: decides which keycode the ModTap key resolves to when another
key is pressed before the timeout finishes. When `True` the hold keycode is key is pressed before the timeout finishes. When `True` the hold keycode is
@@ -40,5 +40,11 @@ KC.MT(KC.TAP, KC.HOLD, prefer_hold=True, tap_interrupted=False, tap_time=None)
key press/down, or after the first other key up/release. Set to `True` for key press/down, or after the first other key up/release. Set to `True` for
interrupt on release. interrupt on release.
* `tap_time`: length of the tap timeout in milliseconds. * `tap_time`: length of the tap timeout in milliseconds.
* `repeat`: decides how to interpret repeated presses if they happen within
`tap_time` after a release.
When `True` the repeated press sends the previous keycode, no matter
how long the key remains pressed the second time.
This allows to hold the tap keycode, or repeatedly tap the hold keycode.
When `False` repeated presses are independent.
Each of these parameters can be set for every ModTap key individually. Each of these parameters can be set for every ModTap key individually.

View File

@@ -1,35 +1,47 @@
# Peg RGB Matrix # Peg RGB Matrix
### What you can and cannot do with this extension: ## What you can and cannot do with this extension:
### Can Do
#### Can Do
* Set any key's LED to be any color in a syntax very similar to your keymap * Set any key's LED to be any color in a syntax very similar to your keymap
* Allows specific keys to be set to OFF * Allows specific keys to be set to OFF
* Allows underglow LEDs to be a different color than per-key LEDs * Allows underglow LEDs to be a different color than per-key LEDs
* Allows modifier keys to be set to a different color than alpha keys * Allows modifier keys to be set to a different color than alpha keys
* Full split keyboard support * Full split keyboard support
* Change brightness of LEDs from code or using keycodes
### Cannot Do (currently in progress)
#### Cannot Do (currently in progress)
* Adjust color at runtime. Currently the extension requires changes to main.py in order to make changes to your LEDs. * Adjust color at runtime. Currently the extension requires changes to main.py in order to make changes to your LEDs.
* Animations * Animations
* Change LED color based on current layer * Change LED color based on current layer
### Keycodes ## Keycodes
Currently this extension does not support changing LEDs at runtime, as a result there is only a single keycode available to interact with this extension and that is KC.RGB_TOG. This keycode simply toggles all your LEDs on and off.
Currently this extension does not support changing LEDs at runtime, as a result there are only three keycodes available to interact with this extension,those are:
* `KC.RGB_TOG`. This keycode simply toggles all your LEDs on and off.
* `KC.RGB_BRI`. This keycode increases the brightness of the LEDs.
* `KC.RGB_BRD`. This keycode decreases the brightness of the LEDs.
## Required Libraries ## Required Libraries
The following libraries must be frozen in your CircuitPython distribution or in a 'lib' folder at the root of your drive. The following libraries must be frozen in your CircuitPython distribution or in a 'lib' folder at the root of your drive.
* [Adafruit_CircuitPython_NeoPixel](https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel) * [Adafruit_CircuitPython_NeoPixel](https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel)
* [Download .mpy versions from here](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/download/20220415/adafruit-circuitpython-bundle-7.x-mpy-20220415.zip) * [Download .mpy versions from here](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/download/20220415/adafruit-circuitpython-bundle-7.x-mpy-20220415.zip)
# Required Changes to main.py and kb.py ## Required Changes to main.py and kb.py
In order to use this extension the user must make changes to both their kb.py and main.py files. Below you will find a more comprehensive list of changes required in order to use this extension. In order to use this extension the user must make changes to both their kb.py and main.py files. Below you will find a more comprehensive list of changes required in order to use this extension.
## kb.py ### kb.py
It is possible your chosen board may already have these changes made, if not you will need to make these additions: It is possible your chosen board may already have these changes made, if not you will need to make these additions:
The board's kb.py needs 3 fields: The board's kb.py needs 3 fields:
* LED Key Position `led_key_pos` * LED Key Position `led_key_pos`
* Much like `coord_mapping` this tells the extension where the LEDs are on your board. * Much like `coord_mapping` this tells the extension where the LEDs are on your board.
* Brightness Limit `brightness_limit` * Brightness Limit `brightness_limit`
@@ -37,8 +49,7 @@ The board's kb.py needs 3 fields:
* Number of LEDs `num_pixels` * Number of LEDs `num_pixels`
* Used for calculations in order to ensure the LEDs map to the correct keys. * Used for calculations in order to ensure the LEDs map to the correct keys.
#### Non-split Example:
### Non-split Example:
Below shows a simple non-split example for a board containing 48 LEDs total and 38 keys with per-key LEDs. Below shows a simple non-split example for a board containing 48 LEDs total and 38 keys with per-key LEDs.
This means we will have 10 underglow LEDs and 38 per-key LEDs. This means we will have 10 underglow LEDs and 38 per-key LEDs.
@@ -62,8 +73,7 @@ Underglow LEDs always appear at the end of the `led_key_pos` array, because the
num_pixels = 48 num_pixels = 48
``` ```
#### Split Example:
### Split Example:
Below shows a 58 key split keyboard's `led_key_pos` array for a board containing 70 LEDs in total. Below shows a 58 key split keyboard's `led_key_pos` array for a board containing 70 LEDs in total.
The board has 58 keys, meaning we are left with 12 underglow LEDs total. The board has 58 keys, meaning we are left with 12 underglow LEDs total.
@@ -95,8 +105,8 @@ Underglow LEDs always appear at the end of the `led_key_pos` array, because the
``` ```
### main.py
## main.py
It is possible your chosen board may already have these changes made, if not you will need to make these additions: It is possible your chosen board may already have these changes made, if not you will need to make these additions:
```python ```python
@@ -125,10 +135,12 @@ Rgb_matrix:
* This is optional and only serves to make all your LEDs turn on at once instead of animate to their on state. * This is optional and only serves to make all your LEDs turn on at once instead of animate to their on state.
### Colors ### Colors
Colors are RGB and can be provided in one of two ways. Colors are RGB and can be provided in one of two ways.
Colors can be defined as an array of three numbers (0-255) or you can use the `Color` class with its default colors, see example below. Colors can be defined as an array of three numbers (0-255) or you can use the `Color` class with its default colors, see example below.
### Passing RGB Codes #### Passing RGB Codes
```python ```python
Rgb_matrix_data( Rgb_matrix_data(
keys=[[255,55,55],[55,55,55],[55,55,55],[55,55,55],[55,55,55],[55,55,55],"""... rest of colors""" ], keys=[[255,55,55],[55,55,55],[55,55,55],[55,55,55],[55,55,55],[55,55,55],"""... rest of colors""" ],
@@ -136,7 +148,8 @@ Rgb_matrix_data(
) )
``` ```
### Using `Color` Class #### Using `Color` Class
```python ```python
Rgb_matrix_data( Rgb_matrix_data(
keys=[Color.RED, Color.GREEN, Color.BLUE, Color.WHITE, Color.YELLOW, Color.ORANGE,"""... rest of colors""" ], keys=[Color.RED, Color.GREEN, Color.BLUE, Color.WHITE, Color.YELLOW, Color.ORANGE,"""... rest of colors""" ],
@@ -163,7 +176,8 @@ rgb_ext = Rgb_matrix(ledDisplay=Rgb_matrix_data(
disable_auto_write=True) disable_auto_write=True)
``` ```
### Bonus #### Bonus
Because creating `ledDisplay` can be time consuming, there is a utility avaiable that will generate a basic framework for you. Because creating `ledDisplay` can be time consuming, there is a utility avaiable that will generate a basic framework for you.
```python ```python
@@ -173,6 +187,7 @@ Rgb_matrix_data.generate_led_map(58,10,Color.WHITE,Color.BLUE)
Call `Rgb_matrix_data.generate_led_map` before you do any configuration beyond imports and it will print an `Rgb_matrix_data` class to your CircuitPython REPL which you can view by using a tool like "screen" or "PUTTY". Call `Rgb_matrix_data.generate_led_map` before you do any configuration beyond imports and it will print an `Rgb_matrix_data` class to your CircuitPython REPL which you can view by using a tool like "screen" or "PUTTY".
Generate LED Map Arguments: Generate LED Map Arguments:
* Number of Keys * Number of Keys
* Number of Underglow * Number of Underglow
* Key Color * Key Color
@@ -184,6 +199,5 @@ Example Using Above Arguments:
Rgb_matrix_data(keys=[[249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249]], Rgb_matrix_data(keys=[[249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249], [249, 249, 249]],
underglow=[[0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255]]) underglow=[[0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255]])
``` ```
[Connecting to the Serial Console](https://learn.adafruit.com/welcome-to-circuitpython/kattni-connecting-to-the-serial-console) [Connecting to the Serial Console](https://learn.adafruit.com/welcome-to-circuitpython/kattni-connecting-to-the-serial-console)

103
docs/quickpin.md Normal file
View File

@@ -0,0 +1,103 @@
# Quickpin
Quickpin helps devs quickly translate pinouts between boards of similar footprint.
This lets you write a single `kb.py` that can be swapped between
microcontrollers with only a single line change and less mistakes.
## Supported footprints/boards
- Pro micro footprint
- Sparkfun Pro micro RP2040
- Boardsource Blok
- Nice!nano
## Pro micro footprint pinout
![pro micro footprint pins](./img/pro_micro_pinout.png)
## Example
In this example, we are converting a Boardsource 3x4 from a hard pinned
nice!nano to a controller agnostic pinout.
```python
row_pins = (board.P1_15, board.P0_02, board.P0_29)
col_pins = (board.P0_09, board.P0_10, board.P1_11, board.P1_13)
```
Converts to the following. Notice that `nice_nano` can be subbed for
`boardsource_blok` or `sparkfun_promicro_rp2040`, or any other board sharing
this pinout.
```python
from kmk.quickpin.pro_micro.nice_nano import pinout as pins
row_pins = (pins[16], pins[17], pins[18])
col_pins = (pins[12], pins[13], pins[14], pins[15])
```
## Porting from AVR pro micro
An additional added convenience for translating from other firmwares with AVR
pro micros has also been added to speed up porting.
```python
from kmk.quickpin.pro_micro.nice_nano import pinout as pins
from kmk.quickpin.pro_Micro.avr_promicro import avr
row_pins = (
pins[avr['F7']],
pins[avr['F6']],
pins[avr['F5']],
)
col_pins = (
pins[avr['B6']],
pins[avr['B2']],
pins[avr['B3']],
pins[avr['B1']],
)
```
## Adding boards to quickpin support
Quickpin format is simply a list of pins in order of all through hole pins,
going anticlockwise starting at the top left. The orientation should be with the
chips facing toward you, with USB facing the top. If this isn't appliable, or
otherwise is not true, it should be stated in a comment in the file. Any pin
that is not addressable in software should be left as `None` to fill the space,
and align pins correctly for all boards. All boards should be stored in
`kmk/quickpin/<footprint>/boardname.py`.
Pro Micro RP2040 shown as an example:
```python
import board
pinout = [
board.TX,
board.RX,
None, # GND
None, # GND
board.D2,
board.D3,
board.D4,
board.D5,
board.D6,
board.D7,
board.D8,
board.D9,
board.D21,
board.MOSI,
board.MISO,
board.SCK,
board.D26,
board.D27,
board.D28,
board.D29,
None, # 3.3v
None, # RST
None, # GND
None, # RAW
]
```

View File

@@ -4,7 +4,7 @@ Want your keyboard to shine? Add some lights!
## CircuitPython ## CircuitPython
This does require the [NeoPixel library from Adafruit](https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel/blob/main/neopixel.py). This does require the [NeoPixel library from Adafruit](https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel/blob/main/neopixel.py).
It is part of the [Adafruit CircuitPython Bundle](https://github.com/adafruit/Adafruit_CircuitPython_Bundle). It is part of the [Adafruit CircuitPython Bundle](https://github.com/adafruit/Adafruit_CircuitPython_Bundle).
Simply put this in the "root" of your CircuitPython device. If unsure, it's the folder with main.py in it, and should be the first folder you see when you open the device. Simply put this in the "root" of your CircuitPython device. If unsure, it's the folder with `main.py` in it, and should be the first folder you see when you open the device.
Currently we support the following addressable LEDs: Currently we support the following addressable LEDs:
@@ -20,13 +20,13 @@ Changing the **Saturation** moves between the inner and outer sections of the wh
Changing the **Value** sets the overall brightness. Changing the **Value** sets the overall brightness.
## Enabling the extension ## Enabling the extension
The only required values that you need to give the RGB extension would be the pixel pin, and the number of pixels/LED's. If using a split keyboard, this number is per side, and not the total of both sides. The only required values that you need to give the RGB extension would be the board pin for the data line, and the number of pixels/LED's. If using a split keyboard, this number is per side, and not the total of both sides.
```python ```python
import board
from kmk.extensions.RGB import RGB from kmk.extensions.RGB import RGB
from kb import rgb_pixel_pin # This can be imported or defined manually
rgb_ext = RGB(pixel_pin=rgb_pixel_pin, num_pixels=27) rgb = RGB(pixel_pin=board.GP14, num_pixels=27)
keyboard.extensions.append(rgb_ext) keyboard.extensions.append(rgb)
``` ```
## [Keycodes] ## [Keycodes]
@@ -52,22 +52,21 @@ keyboard.extensions.append(rgb_ext)
## Configuration ## Configuration
|Define |Default |Description | |Define |Default |Description |
|-------------------------------------|-------------|-----------------------------------------------------------------------------| |-------------------------------------|-------------|-----------------------------------------------------------------------------|
|`keyboard.pixel_pin` | |The pin connected to the data pin of the LEDs| |`rgb.num_pixels`| |The number of LEDs connected |
|`keyboard.num_pixels`| |The number of LEDs connected | |`rgb.rgb_order` |`(1, 0, 2)` |The order of the pixels R G B, and optionally white. Example(1, 0, 2, 3) |
|`keyboard.rgb_config['rgb_order']` |`(1, 0, 2)` |The order of the pixels R G B, and optionally white. Example(1, 0, 2, 3) | |`rgb.hue_step` |`10` |The number of steps to cycle through the hue by |
|`keyboard.rgb_config['hue_step']` |`10` |The number of steps to cycle through the hue by | |`rgb.sat_step` |`17` |The number of steps to change the saturation by |
|`keyboard.rgb_config['sat_step']` |`17` |The number of steps to change the saturation by | |`rgb.val_step` |`17` |The number of steps to change the brightness by |
|`keyboard.rgb_config['val_step']` |`17` |The number of steps to change the brightness by | |`rgb.hue_default` |`0` |The default hue when the keyboard boots |
|`keyboard.rgb_config['hue_default']` |`0` |The default hue when the keyboard boots | |`rgb.sat_default` |`255` |The default saturation when the keyboard boots |
|`keyboard.rgb_config['sat_default']` |`100` |The default saturation when the keyboard boots | |`rgb.val_default` |`255` |The default value (brightness) when the keyboard boots |
|`keyboard.rgb_config['val_default']` |`100` |The default value (brightness) when the keyboard boots | |`rgb.val_limit` |`255` |The maximum brightness level |
|`keyboard.rgb_config['val_limit']` |`255` |The maximum brightness level |
## Built-in Animation Configuration ## Built-in Animation Configuration
|Define |Default |Description | |Define |Default |Description |
|----------------------------------------------|-------------|-------------------------------------------------------------------------------------| |----------------------------------------------|-------------|-------------------------------------------------------------------------------------|
|`keyboard.rgb_config['breathe_center']` |`1.5` |Used to calculate the curve for the breathing animation. Anywhere from 1.0 - 2.7 is valid| |`rgb.breathe_center` |`1.5` |Used to calculate the curve for the breathing animation. Anywhere from 1.0 - 2.7 is valid|
|`keyboard.rgb_config['knight_effect_length']` |`4` |The number of LEDs to light up for the "Knight" animation | |`rgb.knight_effect_length` |`4` |The number of LEDs to light up for the "Knight" animation |
## Functions ## Functions
@@ -75,38 +74,36 @@ If you want to create your own animations, or for example, change the lighting i
|Function |Description | |Function |Description |
|--------------------------------------------------|--------------------------------------------------------------------------------------------| |--------------------------------------------------|--------------------------------------------------------------------------------------------|
|`keyboard.pixels.set_hsv_fill(hue, sat, val)` |Fills all LED's with HSV values | |`rgb.set_hsv_fill(hue, sat, val)` |Fills all LED's with HSV values |
|`keyboard.pixels.set_hsv(hue, sat, val, index)` |Sets a single LED with HSV value | |`rgb.set_hsv(hue, sat, val, index)` |Sets a single LED with HSV value |
|`keyboard.pixels.set_rgb_fill((r, g, b))` |Fills all LED's with RGB(W) values | |`rgb.set_rgb_fill((r, g, b))` |Fills all LED's with RGB(W) values |
|`keyboard.pixels.set_rgb((r, g, b), index)` |Set's a single LED with RGB(W) values | |`rgb.set_rgb((r, g, b), index)` |Set's a single LED with RGB(W) values |
|`keyboard.pixels.disable_auto_write(bool)` |When True, disables showing changes. Good for setting multiple LED's before a visible update| |`rgb.disable_auto_write(bool)` |When True, disables showing changes. Good for setting multiple LED's before a visible update|
|`keyboard.pixels.increase_hue(step)` |Increases hue by a given step | |`rgb.increase_hue(step)` |Increases hue by a given step |
|`keyboard.pixels.decrease_hue(step)` |Decreases hue by a given step | |`rgb.decrease_hue(step)` |Decreases hue by a given step |
|`keyboard.pixels.increase_sat(step)` |Increases saturation by a given step | |`rgb.increase_sat(step)` |Increases saturation by a given step |
|`keyboard.pixels.decrease_sat(step)` |Decreases saturation by a given step | |`rgb.decrease_sat(step)` |Decreases saturation by a given step |
|`keyboard.pixels.increase_val(step)` |Increases value (brightness) by a given step | |`rgb.increase_val(step)` |Increases value (brightness) by a given step |
|`keyboard.pixels.decrease_val(step)` |Decreases value (brightness) by a given step | |`rgb.decrease_val(step)` |Decreases value (brightness) by a given step |
|`keyboard.pixels.increase_ani()` |Increases animation speed by 1. Maximum 10 | |`rgb.increase_ani()` |Increases animation speed by 1. Maximum 10 |
|`keyboard.pixels.decrease_ani()` |Decreases animation speed by 1. Minimum 10 | |`rgb.decrease_ani()` |Decreases animation speed by 1. Minimum 10 |
|`keyboard.pixels.off()` |Turns all LED's off | |`rgb.off()` |Turns all LED's off |
|`keyboard.pixels.show()` |Displays all stored configuration for LED's. Useful with disable_auto_write explained above | |`rgb.show()` |Displays all stored configuration for LED's. Useful with disable_auto_write explained above |
|`keyboard.pixels.time_ms()` |Returns a time in ms since the board has booted. Useful for start/stop timers |
## Direct variable access ## Direct variable access
|Define |Default |Description | |Define |Default |Description |
|-----------------------------------|-----------|-----------------------------------------------------------------------------------------------------------| |-----------------------------------|-----------|-----------------------------------------------------------------------------------------------------------|
|`keyboard.pixels.hue` |`0` |Sets the hue from 0-360 | |`rgb.hue` |`0` |Sets the hue from 0-255 |
|`keyboard.pixels.sat` |`100` |Sets the saturation from 0-100 | |`rgb.sat` |`255` |Sets the saturation from 0-255 |
|`keyboard.pixels.val` |`80` |Sets the brightness from 1-255 | |`rgb.val` |`255` |Sets the brightness from 0-255 |
|`keyboard.pixels.reverse_animation`|`False` |If true, some animations will run in reverse. Can be safely used in user animations | |`rgb.reverse_animation`|`False` |If true, some animations will run in reverse. Can be safely used in user animations |
|`keyboard.pixels.animation_mode` |`static` |This can be changed to any modes included, or to something custom for user animations. Any string is valid | |`rgb.animation_mode` |`static` |This can be changed to any modes included, or to something custom for user animations. Any string is valid |
|`keyboard.pixels.animation_speed` |`1` |Increases animation speed of most animations. Recommended 1-5, Maximum 10. | |`rgb.animation_speed` |`1` |Increases animation speed of most animations. Recommended 1-5, Maximum 10. |
```python ```python
from kmk.extensions.rgb import AnimationModes from kmk.extensions.rgb import AnimationModes
rgb_ext = RGB(pixel_pin=rgb_pixel_pin, rgb = RGB(pixel_pin=rgb_pixel_pin,
num_pixels=27 num_pixels=27
num_pixels=0,
val_limit=100, val_limit=100,
hue_default=0, hue_default=0,
sat_default=100, sat_default=100,
@@ -169,8 +166,8 @@ from kb import rgb_pixel_pin # This can be imported or defined manually
_LED_COUNT=12 _LED_COUNT=12
pixels = adafruit_dotstar.DotStar(board.SCK, board.MOSI, _LED_COUNT) pixels = adafruit_dotstar.DotStar(board.SCK, board.MOSI, _LED_COUNT)
rgb_ext = RGB(pixel_pin=None, pixels=pixels) rgb = RGB(pixel_pin=None, pixels=pixels)
keyboard.extensions.append(rgb_ext) keyboard.extensions.append(rgb)
``` ```
### Multiple PixelBuffer ### Multiple PixelBuffer
@@ -185,6 +182,6 @@ pixels = (
CustomPixelBuf(...) CustomPixelBuf(...)
) )
rgb_ext = RGB(pixel_pin=None, pixels=pixels) rgb = RGB(pixel_pin=None, pixels=pixels)
keyboard.extensions.append(rgb_ext) keyboard.extensions.append(rgb)
``` ```

View File

@@ -81,6 +81,18 @@ COUNTDOWN_TO_PASTE = simple_key_sequence(
keyboard.keymap = [<other keycodes>, COUNTDOWN_TO_PASTE, <other keycodes>] keyboard.keymap = [<other keycodes>, COUNTDOWN_TO_PASTE, <other keycodes>]
``` ```
from kmk.handlers.sequences import simple_key_sequence
NEXT = simple_key_sequence(
(
KC.LALT(no_release=True),
KC.MACRO_SLEEP_MS(30),
KC.TAB,
KC.MACRO_SLEEP_MS(30),
KC.LALT(no_press=True),
)
)
This example will type out the following, waiting one second (1000 ms) between numbers: This example will type out the following, waiting one second (1000 ms) between numbers:
3 3
@@ -89,6 +101,24 @@ This example will type out the following, waiting one second (1000 ms) between n
and then paste the contents of your clipboard. and then paste the contents of your clipboard.
### Alt Tab with delay
If alt tab isn't working because it requires a delay, adding a delay and triggering
down and up on ALT manually may fix the issue.
``` python
from kmk.handlers.sequences import simple_key_sequence
NEXT = simple_key_sequence(
(
KC.LALT(no_release=True),
KC.MACRO_SLEEP_MS(30),
KC.TAB,
KC.MACRO_SLEEP_MS(30),
KC.LALT(no_press=True),
)
)
```
## Unicode ## Unicode
Before trying to send Unicode sequences, make sure you set your `UnicodeMode`. Before trying to send Unicode sequences, make sure you set your `UnicodeMode`.

View File

@@ -61,7 +61,6 @@ class Oled(Extension):
def renderOledTextLayer(self, layer): def renderOledTextLayer(self, layer):
splash = displayio.Group() splash = displayio.Group()
self._display.show(splash)
splash.append( splash.append(
label.Label( label.Label(
terminalio.FONT, terminalio.FONT,
@@ -98,16 +97,17 @@ class Oled(Extension):
y=25, y=25,
) )
) )
self._display.show(splash)
gc.collect() gc.collect()
def renderOledImgLayer(self, layer): def renderOledImgLayer(self, layer):
splash = displayio.Group() splash = displayio.Group()
self._display.show(splash)
odb = displayio.OnDiskBitmap( odb = displayio.OnDiskBitmap(
'/' + self.returnCurrectRenderText(layer, self._views[0]) '/' + self.returnCurrectRenderText(layer, self._views[0])
) )
image = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader) image = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
splash.append(image) splash.append(image)
self._display.show(splash)
gc.collect() gc.collect()
def updateOLED(self, sandbox): def updateOLED(self, sandbox):

View File

@@ -9,11 +9,15 @@ from kmk.keys import make_key
class Color: class Color:
OFF = [0, 0, 0] OFF = [0, 0, 0]
BLACK = OFF
WHITE = [249, 249, 249] WHITE = [249, 249, 249]
RED = [255, 0, 0] RED = [255, 0, 0]
AZURE = [153, 245, 255]
BLUE = [0, 0, 255] BLUE = [0, 0, 255]
CYAN = [0, 255, 255]
GREEN = [0, 255, 0] GREEN = [0, 255, 0]
YELLOW = [255, 247, 0] YELLOW = [255, 247, 0]
MAGENTA = [255, 0, 255]
ORANGE = [255, 77, 0] ORANGE = [255, 77, 0]
PURPLE = [255, 0, 242] PURPLE = [255, 0, 242]
TEAL = [0, 128, 128] TEAL = [0, 128, 128]
@@ -53,6 +57,9 @@ class Rgb_matrix(Extension):
self.disable_auto_write = disable_auto_write self.disable_auto_write = disable_auto_write
self.split = split self.split = split
self.rightSide = rightSide self.rightSide = rightSide
self.brightness_step = 0.1
self.brightness = 0
if name.endswith('L'): if name.endswith('L'):
self.rightSide = False self.rightSide = False
elif name.endswith('R'): elif name.endswith('R'):
@@ -65,6 +72,12 @@ class Rgb_matrix(Extension):
make_key( make_key(
names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
) )
make_key(
names=('RGB_BRI',), on_press=self._rgb_bri, on_release=handler_passthrough
)
make_key(
names=('RGB_BRD',), on_press=self._rgb_brd, on_release=handler_passthrough
)
def _rgb_tog(self, *args, **kwargs): def _rgb_tog(self, *args, **kwargs):
if self.enable: if self.enable:
@@ -73,6 +86,12 @@ class Rgb_matrix(Extension):
self.on() self.on()
self.enable = not self.enable self.enable = not self.enable
def _rgb_bri(self, *args, **kwargs):
self.increase_brightness()
def _rgb_brd(self, *args, **kwargs):
self.decrease_brightness()
def on(self): def on(self):
if self.neopixel: if self.neopixel:
self.setBasedOffDisplay() self.setBasedOffDisplay()
@@ -88,6 +107,34 @@ class Rgb_matrix(Extension):
if self.disable_auto_write: if self.disable_auto_write:
self.neopixel.show() self.neopixel.show()
def set_brightness(self, brightness=None):
if brightness is None:
brightness = self.brightness
if self.neopixel:
self.neopixel.brightness = brightness
if self.disable_auto_write:
self.neopixel.show()
def increase_brightness(self, step=None):
if step is None:
step = self.brightness_step
self.brightness = (
self.brightness + step if self.brightness + step <= 1.0 else 1.0
)
self.set_brightness(self.brightness)
def decrease_brightness(self, step=None):
if step is None:
step = self.brightness_step
self.brightness = (
self.brightness - step if self.brightness - step >= 0.0 else 0.0
)
self.set_brightness(self.brightness)
def setBasedOffDisplay(self): def setBasedOffDisplay(self):
if self.split: if self.split:
for i, val in enumerate(self.ledDisplay): for i, val in enumerate(self.ledDisplay):
@@ -121,6 +168,7 @@ class Rgb_matrix(Extension):
) )
self.num_pixels = board.num_pixels self.num_pixels = board.num_pixels
self.keyPos = board.led_key_pos self.keyPos = board.led_key_pos
self.brightness = board.brightness_limit
self.on() self.on()
return return
@@ -137,7 +185,17 @@ class Rgb_matrix(Extension):
return return
def on_powersave_enable(self, sandbox): def on_powersave_enable(self, sandbox):
return if self.neopixel:
self.neopixel.brightness = (
self.neopixel.brightness / 2
if self.neopixel.brightness / 2 > 0
else 0.1
)
if self.disable_auto_write:
self.neopixel.show()
def on_powersave_disable(self, sandbox): def on_powersave_disable(self, sandbox):
return if self.neopixel:
self.neopixel.brightness = self.brightness
if self.disable_auto_write:
self.neopixel.show()

View File

@@ -5,7 +5,9 @@ from kmk.extensions import Extension
from kmk.handlers.stock import passthrough as handler_passthrough from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key from kmk.keys import make_key
from kmk.kmktime import PeriodicTimer from kmk.kmktime import PeriodicTimer
from kmk.utils import clamp from kmk.utils import Debug, clamp
debug = Debug(__name__)
rgb_config = {} rgb_config = {}
@@ -127,6 +129,10 @@ class RGB(Extension):
for pixels in self.pixels: for pixels in self.pixels:
self.num_pixels += len(pixels) self.num_pixels += len(pixels)
if debug.enabled:
for n, pixels in enumerate(self.pixels):
debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')
self.rgbw = bool(len(rgb_order) == 4) self.rgbw = bool(len(rgb_order) == 4)
self.hue_step = hue_step self.hue_step = hue_step
@@ -249,6 +255,9 @@ class RGB(Extension):
:param val: :param val:
:param index: Index of LED/Pixel :param index: Index of LED/Pixel
''' '''
val = clamp(val, 0, self.val_limit)
if self.rgbw: if self.rgbw:
self.set_rgb(hsv_to_rgbw(hue, sat, val), index) self.set_rgb(hsv_to_rgbw(hue, sat, val), index)
else: else:
@@ -261,6 +270,9 @@ class RGB(Extension):
:param sat: :param sat:
:param val: :param val:
''' '''
val = clamp(val, 0, self.val_limit)
if self.rgbw: if self.rgbw:
self.set_rgb_fill(hsv_to_rgbw(hue, sat, val)) self.set_rgb_fill(hsv_to_rgbw(hue, sat, val))
else: else:

View File

@@ -1,3 +1,8 @@
try:
from typing import Callable, Optional, Tuple
except ImportError:
pass
from micropython import const from micropython import const
import kmk.handlers.stock as handlers import kmk.handlers.stock as handlers
@@ -6,13 +11,20 @@ from kmk.key_validators import key_seq_sleep_validator, unicode_mode_key_validat
from kmk.types import UnicodeModeKeyMeta from kmk.types import UnicodeModeKeyMeta
from kmk.utils import Debug from kmk.utils import Debug
# Type aliases / forward declaration; can't use the proper types because of circular imports.
Keyboard = object
Key = object
class KeyType:
SIMPLE = const(0)
MODIFIER = const(1)
CONSUMER = const(2)
FIRST_KMK_INTERNAL_KEY = const(1000) FIRST_KMK_INTERNAL_KEY = const(1000)
NEXT_AVAILABLE_KEY = 1000 NEXT_AVAILABLE_KEY = 1000
KEY_SIMPLE = const(0)
KEY_MODIFIER = const(1)
KEY_CONSUMER = const(2)
ALL_ALPHAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ALL_ALPHAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALL_NUMBERS = '1234567890' ALL_NUMBERS = '1234567890'
# since KC.1 isn't valid Python, alias to KC.N1 # since KC.1 isn't valid Python, alias to KC.N1
@@ -21,7 +33,12 @@ ALL_NUMBER_ALIASES = tuple(f'N{x}' for x in ALL_NUMBERS)
debug = Debug(__name__) debug = Debug(__name__)
def maybe_make_key(code, names, *args, **kwargs): def maybe_make_key(
code: Optional[int],
names: Tuple[str, ...],
*args,
**kwargs,
) -> Callable[[str], Key]:
def closure(candidate): def closure(candidate):
if candidate in names: if candidate in names:
return make_key(code=code, names=names, *args, **kwargs) return make_key(code=code, names=names, *args, **kwargs)
@@ -31,10 +48,10 @@ def maybe_make_key(code, names, *args, **kwargs):
def maybe_make_argumented_key( def maybe_make_argumented_key(
validator=lambda *validator_args, **validator_kwargs: object(), validator=lambda *validator_args, **validator_kwargs: object(),
names=tuple(), # NOQA names: Tuple[str, ...] = tuple(), # NOQA
*constructor_args, *constructor_args,
**constructor_kwargs, **constructor_kwargs,
): ) -> Callable[[str], Key]:
def closure(candidate): def closure(candidate):
if candidate in names: if candidate in names:
return make_argumented_key( return make_argumented_key(
@@ -44,7 +61,7 @@ def maybe_make_argumented_key(
return closure return closure
def maybe_make_no_key(candidate): def maybe_make_no_key(candidate: str) -> Optional[Key]:
# NO and TRNS are functionally identical in how they (don't) mutate # NO and TRNS are functionally identical in how they (don't) mutate
# the state, but are tracked semantically separately, so create # the state, but are tracked semantically separately, so create
# two keys with the exact same functionality # two keys with the exact same functionality
@@ -62,7 +79,7 @@ def maybe_make_no_key(candidate):
) )
def maybe_make_alpha_key(candidate): def maybe_make_alpha_key(candidate: str) -> Optional[Key]:
if len(candidate) != 1: if len(candidate) != 1:
return return
@@ -74,7 +91,7 @@ def maybe_make_alpha_key(candidate):
) )
def maybe_make_numeric_key(candidate): def maybe_make_numeric_key(candidate: str) -> Optional[Key]:
if candidate in ALL_NUMBERS or candidate in ALL_NUMBER_ALIASES: if candidate in ALL_NUMBERS or candidate in ALL_NUMBER_ALIASES:
try: try:
offset = ALL_NUMBERS.index(candidate) offset = ALL_NUMBERS.index(candidate)
@@ -87,7 +104,7 @@ def maybe_make_numeric_key(candidate):
) )
def maybe_make_mod_key(candidate): def maybe_make_mod_key(candidate: str) -> Optional[Key]:
# MEH = LCTL | LALT | LSFT # MEH = LCTL | LALT | LSFT
# HYPR = LCTL | LALT | LSFT | LGUI # HYPR = LCTL | LALT | LSFT | LGUI
mods = ( mods = (
@@ -105,10 +122,10 @@ def maybe_make_mod_key(candidate):
for code, names in mods: for code, names in mods:
if candidate in names: if candidate in names:
return make_key(code=code, names=names, type=KEY_MODIFIER) return make_key(code=code, names=names, type=KeyType.MODIFIER)
def maybe_make_more_ascii(candidate): def maybe_make_more_ascii(candidate: str) -> Optional[Key]:
codes = ( codes = (
(40, ('ENTER', 'ENT', '\n')), (40, ('ENTER', 'ENT', '\n')),
(41, ('ESCAPE', 'ESC')), (41, ('ESCAPE', 'ESC')),
@@ -133,7 +150,7 @@ def maybe_make_more_ascii(candidate):
return make_key(code=code, names=names) return make_key(code=code, names=names)
def maybe_make_fn_key(candidate): def maybe_make_fn_key(candidate: str) -> Optional[Key]:
codes = ( codes = (
(58, ('F1',)), (58, ('F1',)),
(59, ('F2',)), (59, ('F2',)),
@@ -166,7 +183,7 @@ def maybe_make_fn_key(candidate):
return make_key(code=code, names=names) return make_key(code=code, names=names)
def maybe_make_navlock_key(candidate): def maybe_make_navlock_key(candidate: str) -> Optional[Key]:
codes = ( codes = (
(57, ('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')), (57, ('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')),
# FIXME: Investigate whether this key actually works, and # FIXME: Investigate whether this key actually works, and
@@ -195,7 +212,7 @@ def maybe_make_navlock_key(candidate):
return make_key(code=code, names=names) return make_key(code=code, names=names)
def maybe_make_numpad_key(candidate): def maybe_make_numpad_key(candidate: str) -> Optional[Key]:
codes = ( codes = (
(83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')), (83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')),
(84, ('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')), (84, ('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')),
@@ -224,7 +241,7 @@ def maybe_make_numpad_key(candidate):
return make_key(code=code, names=names) return make_key(code=code, names=names)
def maybe_make_shifted_key(candidate): def maybe_make_shifted_key(candidate: str) -> Optional[Key]:
codes = ( codes = (
(30, ('EXCLAIM', 'EXLM', '!')), (30, ('EXCLAIM', 'EXLM', '!')),
(31, ('AT', '@')), (31, ('AT', '@')),
@@ -254,7 +271,7 @@ def maybe_make_shifted_key(candidate):
return make_key(code=code, names=names, has_modifiers={KC.LSFT.code}) return make_key(code=code, names=names, has_modifiers={KC.LSFT.code})
def maybe_make_international_key(candidate): def maybe_make_international_key(candidate: str) -> Optional[Key]:
codes = ( codes = (
(50, ('NONUS_HASH', 'NUHS')), (50, ('NONUS_HASH', 'NUHS')),
(100, ('NONUS_BSLASH', 'NUBS')), (100, ('NONUS_BSLASH', 'NUBS')),
@@ -284,7 +301,7 @@ def maybe_make_international_key(candidate):
return make_key(code=code, names=names) return make_key(code=code, names=names)
def maybe_make_unicode_key(candidate): def maybe_make_unicode_key(candidate: str) -> Optional[Key]:
keys = ( keys = (
( (
('UC_MODE_NOOP', 'UC_DISABLE'), ('UC_MODE_NOOP', 'UC_DISABLE'),
@@ -320,7 +337,7 @@ def maybe_make_unicode_key(candidate):
) )
def maybe_make_firmware_key(candidate): def maybe_make_firmware_key(candidate: str) -> Optional[Key]:
keys = ( keys = (
((('BLE_REFRESH',), handlers.ble_refresh)), ((('BLE_REFRESH',), handlers.ble_refresh)),
((('BOOTLOADER',), handlers.bootloader)), ((('BOOTLOADER',), handlers.bootloader)),
@@ -388,13 +405,13 @@ class KeyAttrDict:
def __iter__(self): def __iter__(self):
return self.__cache.__iter__() return self.__cache.__iter__()
def __setitem__(self, key, value): def __setitem__(self, key: str, value: Key):
self.__cache.__setitem__(key, value) self.__cache.__setitem__(key, value)
def __getattr__(self, key): def __getattr__(self, key: Key):
return self.__getitem__(key) return self.__getitem__(key)
def get(self, key, default=None): def get(self, key: Key, default: Optional[Key] = None):
try: try:
return self.__getitem__(key) return self.__getitem__(key)
except Exception: except Exception:
@@ -403,7 +420,7 @@ class KeyAttrDict:
def clear(self): def clear(self):
self.__cache.clear() self.__cache.clear()
def __getitem__(self, key): def __getitem__(self, key: Key):
try: try:
return self.__cache[key] return self.__cache[key]
except KeyError: except KeyError:
@@ -430,13 +447,17 @@ KC = KeyAttrDict()
class Key: class Key:
def __init__( def __init__(
self, self,
code, code: int,
has_modifiers=None, has_modifiers: Optional[list[Key, ...]] = None,
no_press=False, no_press: bool = False,
no_release=False, no_release: bool = False,
on_press=handlers.default_pressed, on_press: Callable[
on_release=handlers.default_released, [object, Key, Keyboard, ...], None
meta=object(), ] = handlers.default_pressed,
on_release: Callable[
[object, Key, Keyboard, ...], None
] = handlers.default_released,
meta: object = object(),
): ):
self.code = code self.code = code
self.has_modifiers = has_modifiers self.has_modifiers = has_modifiers
@@ -448,7 +469,9 @@ class Key:
self._handle_release = on_release self._handle_release = on_release
self.meta = meta self.meta = meta
def __call__(self, no_press=None, no_release=None): def __call__(
self, no_press: Optional[bool] = None, no_release: Optional[bool] = None
) -> Key:
if no_press is None and no_release is None: if no_press is None and no_release is None:
return self return self
@@ -465,35 +488,31 @@ class Key:
def __repr__(self): def __repr__(self):
return f'Key(code={self.code}, has_modifiers={self.has_modifiers})' return f'Key(code={self.code}, has_modifiers={self.has_modifiers})'
def on_press(self, state, coord_int=None): def on_press(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
if hasattr(self, '_pre_press_handlers'): if hasattr(self, '_pre_press_handlers'):
for fn in self._pre_press_handlers: for fn in self._pre_press_handlers:
if not fn(self, state, KC, coord_int): if not fn(self, keyboard, KC, coord_int):
return None return
ret = self._handle_press(self, state, KC, coord_int) self._handle_press(self, keyboard, KC, coord_int)
if hasattr(self, '_post_press_handlers'): if hasattr(self, '_post_press_handlers'):
for fn in self._post_press_handlers: for fn in self._post_press_handlers:
fn(self, state, KC, coord_int) fn(self, keyboard, KC, coord_int)
return ret def on_release(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
def on_release(self, state, coord_int=None):
if hasattr(self, '_pre_release_handlers'): if hasattr(self, '_pre_release_handlers'):
for fn in self._pre_release_handlers: for fn in self._pre_release_handlers:
if not fn(self, state, KC, coord_int): if not fn(self, keyboard, KC, coord_int):
return None return
ret = self._handle_release(self, state, KC, coord_int) self._handle_release(self, keyboard, KC, coord_int)
if hasattr(self, '_post_release_handlers'): if hasattr(self, '_post_release_handlers'):
for fn in self._post_release_handlers: for fn in self._post_release_handlers:
fn(self, state, KC, coord_int) fn(self, keyboard, KC, coord_int)
return ret def clone(self) -> Key:
def clone(self):
''' '''
Return a shallow clone of the current key without any pre/post press/release Return a shallow clone of the current key without any pre/post press/release
handlers attached. Almost exclusively useful for creating non-colliding keys handlers attached. Almost exclusively useful for creating non-colliding keys
@@ -510,7 +529,7 @@ class Key:
meta=self.meta, meta=self.meta,
) )
def before_press_handler(self, fn): def before_press_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
''' '''
Attach a callback to be run prior to the on_press handler for this key. Attach a callback to be run prior to the on_press handler for this key.
Receives the following: Receives the following:
@@ -533,9 +552,8 @@ class Key:
if not hasattr(self, '_pre_press_handlers'): if not hasattr(self, '_pre_press_handlers'):
self._pre_press_handlers = [] self._pre_press_handlers = []
self._pre_press_handlers.append(fn) self._pre_press_handlers.append(fn)
return self
def after_press_handler(self, fn): def after_press_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
''' '''
Attach a callback to be run after the on_release handler for this key. Attach a callback to be run after the on_release handler for this key.
Receives the following: Receives the following:
@@ -557,9 +575,8 @@ class Key:
if not hasattr(self, '_post_press_handlers'): if not hasattr(self, '_post_press_handlers'):
self._post_press_handlers = [] self._post_press_handlers = []
self._post_press_handlers.append(fn) self._post_press_handlers.append(fn)
return self
def before_release_handler(self, fn): def before_release_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
''' '''
Attach a callback to be run prior to the on_release handler for this Attach a callback to be run prior to the on_release handler for this
key. Receives the following: key. Receives the following:
@@ -582,9 +599,8 @@ class Key:
if not hasattr(self, '_pre_release_handlers'): if not hasattr(self, '_pre_release_handlers'):
self._pre_release_handlers = [] self._pre_release_handlers = []
self._pre_release_handlers.append(fn) self._pre_release_handlers.append(fn)
return self
def after_release_handler(self, fn): def after_release_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
''' '''
Attach a callback to be run after the on_release handler for this key. Attach a callback to be run after the on_release handler for this key.
Receives the following: Receives the following:
@@ -606,13 +622,17 @@ class Key:
if not hasattr(self, '_post_release_handlers'): if not hasattr(self, '_post_release_handlers'):
self._post_release_handlers = [] self._post_release_handlers = []
self._post_release_handlers.append(fn) self._post_release_handlers.append(fn)
return self
class ModifierKey(Key): class ModifierKey(Key):
FAKE_CODE = const(-1) FAKE_CODE = const(-1)
def __call__(self, modified_key=None, no_press=None, no_release=None): def __call__(
self,
modified_key: Optional[Key] = None,
no_press: Optional[bool] = None,
no_release: Optional[bool] = None,
) -> Key:
if modified_key is None: if modified_key is None:
return super().__call__(no_press=no_press, no_release=no_release) return super().__call__(no_press=no_press, no_release=no_release)
@@ -649,7 +669,12 @@ class ConsumerKey(Key):
pass pass
def make_key(code=None, names=tuple(), type=KEY_SIMPLE, **kwargs): # NOQA def make_key(
code: Optional[int] = None,
names: Tuple[str, ...] = tuple(), # NOQA
type: KeyType = KeyType.SIMPLE,
**kwargs,
) -> Key:
''' '''
Create a new key, aliased by `names` in the KC lookup table. Create a new key, aliased by `names` in the KC lookup table.
@@ -668,11 +693,11 @@ def make_key(code=None, names=tuple(), type=KEY_SIMPLE, **kwargs): # NOQA
global NEXT_AVAILABLE_KEY global NEXT_AVAILABLE_KEY
if type == KEY_SIMPLE: if type == KeyType.SIMPLE:
constructor = Key constructor = Key
elif type == KEY_MODIFIER: elif type == KeyType.MODIFIER:
constructor = ModifierKey constructor = ModifierKey
elif type == KEY_CONSUMER: elif type == KeyType.CONSUMER:
constructor = ConsumerKey constructor = ConsumerKey
else: else:
raise ValueError('Unrecognized key type') raise ValueError('Unrecognized key type')
@@ -694,29 +719,29 @@ def make_key(code=None, names=tuple(), type=KEY_SIMPLE, **kwargs): # NOQA
return key return key
def make_mod_key(code, names, *args, **kwargs): def make_mod_key(code: int, names: Tuple[str, ...], *args, **kwargs) -> Key:
return make_key(code, names, *args, **kwargs, type=KEY_MODIFIER) return make_key(code, names, *args, **kwargs, type=KeyType.MODIFIER)
def make_shifted_key(code, names): def make_shifted_key(code: int, names: Tuple[str, ...]) -> Key:
return make_key(code, names, has_modifiers={KC.LSFT.code}) return make_key(code, names, has_modifiers={KC.LSFT.code})
def make_consumer_key(*args, **kwargs): def make_consumer_key(*args, **kwargs) -> Key:
return make_key(*args, **kwargs, type=KEY_CONSUMER) return make_key(*args, **kwargs, type=KeyType.CONSUMER)
# Argumented keys are implicitly internal, so auto-gen of code # Argumented keys are implicitly internal, so auto-gen of code
# is almost certainly the best plan here # is almost certainly the best plan here
def make_argumented_key( def make_argumented_key(
validator=lambda *validator_args, **validator_kwargs: object(), validator: object = lambda *validator_args, **validator_kwargs: object(),
names=tuple(), # NOQA names: Tuple[str, ...] = tuple(), # NOQA
*constructor_args, *constructor_args,
**constructor_kwargs, **constructor_kwargs,
): ) -> Key:
global NEXT_AVAILABLE_KEY global NEXT_AVAILABLE_KEY
def _argumented_key(*user_args, **user_kwargs): def _argumented_key(*user_args, **user_kwargs) -> Key:
global NEXT_AVAILABLE_KEY global NEXT_AVAILABLE_KEY
meta = validator(*user_args, **user_kwargs) meta = validator(*user_args, **user_kwargs)

View File

@@ -1,10 +1,13 @@
try: try:
from typing import Optional from typing import Callable, Optional, Tuple
except ImportError: except ImportError:
pass pass
from supervisor import ticks_ms from supervisor import ticks_ms
from collections import namedtuple
from keypad import Event as KeyEvent
from kmk.consts import UnicodeMode from kmk.consts import UnicodeMode
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
from kmk.keys import KC, Key from kmk.keys import KC, Key
@@ -15,6 +18,10 @@ from kmk.utils import Debug
debug = Debug(__name__) debug = Debug(__name__)
KeyBufferFrame = namedtuple(
'KeyBufferFrame', ('key', 'is_pressed', 'int_coord', 'index')
)
class Sandbox: class Sandbox:
matrix_update = None matrix_update = None
@@ -57,6 +64,8 @@ class KMKKeyboard:
i2c_deinit_count = 0 i2c_deinit_count = 0
_go_args = None _go_args = None
_processing_timeouts = False _processing_timeouts = False
_resume_buffer = []
_resume_buffer_x = []
# this should almost always be PREpended to, replaces # this should almost always be PREpended to, replaces
# former use of reversed_active_layers which had pointless # former use of reversed_active_layers which had pointless
@@ -69,7 +78,7 @@ class KMKKeyboard:
# 6.0rc1) this runs out of RAM every cycle and takes down the board. no # 6.0rc1) this runs out of RAM every cycle and takes down the board. no
# real known fix yet other than turning off debug, but M4s have always been # real known fix yet other than turning off debug, but M4s have always been
# tight on RAM so.... # tight on RAM so....
def __repr__(self): def __repr__(self) -> str:
return ''.join( return ''.join(
[ [
'KMKKeyboard(\n', 'KMKKeyboard(\n',
@@ -87,12 +96,12 @@ class KMKKeyboard:
] ]
) )
def _print_debug_cycle(self, init=False): def _print_debug_cycle(self, init: bool = False) -> None:
if debug.enabled: if debug.enabled:
debug(f'coordkeys_pressed={self._coordkeys_pressed}') debug(f'coordkeys_pressed={self._coordkeys_pressed}')
debug(f'keys_pressed={self.keys_pressed}') debug(f'keys_pressed={self.keys_pressed}')
def _send_hid(self): def _send_hid(self) -> None:
if self._hid_send_enabled: if self._hid_send_enabled:
hid_report = self._hid_helper.create_report(self.keys_pressed) hid_report = self._hid_helper.create_report(self.keys_pressed)
try: try:
@@ -102,12 +111,12 @@ class KMKKeyboard:
debug(f'HidNotFound(HIDReportType={e})') debug(f'HidNotFound(HIDReportType={e})')
self.hid_pending = False self.hid_pending = False
def _handle_matrix_report(self, update=None): def _handle_matrix_report(self, kevent: KeyEvent) -> None:
if update is not None: if kevent is not None:
self._on_matrix_changed(update) self._on_matrix_changed(kevent)
self.state_changed = True self.state_changed = True
def _find_key_in_map(self, int_coord): def _find_key_in_map(self, int_coord: int) -> Key:
try: try:
idx = self.coord_mapping.index(int_coord) idx = self.coord_mapping.index(int_coord)
except ValueError: except ValueError:
@@ -129,7 +138,7 @@ class KMKKeyboard:
return layer_key return layer_key
def _on_matrix_changed(self, kevent): def _on_matrix_changed(self, kevent: KeyEvent) -> None:
int_coord = kevent.key_number int_coord = kevent.key_number
is_pressed = kevent.pressed is_pressed = kevent.pressed
if debug.enabled: if debug.enabled:
@@ -156,15 +165,62 @@ class KMKKeyboard:
self.pre_process_key(key, is_pressed, int_coord) self.pre_process_key(key, is_pressed, int_coord)
def _process_resume_buffer(self):
'''
Resume the processing of buffered, delayed, deferred, etc. key events
emitted by modules.
We use a copy of the `_resume_buffer` as a working buffer. The working
buffer holds all key events in the correct order for processing. If
during processing new events are pushed to the `_resume_buffer`, they
are prepended to the working buffer (which may not be emptied), in
order to preserve key event order.
We also double-buffer `_resume_buffer` with `_resume_buffer_x`, only
copying the reference to hopefully safe some time on allocations.
'''
buffer, self._resume_buffer = self._resume_buffer, self._resume_buffer_x
while buffer:
ksf = buffer.pop(0)
key = ksf.key
# Handle any unaccounted-for layer shifts by looking up the key resolution again.
if ksf.int_coord in self._coordkeys_pressed.keys():
key = self._find_key_in_map(ksf.int_coord)
# Resume the processing of the key event and update the HID report
# when applicable.
self.pre_process_key(key, ksf.is_pressed, ksf.int_coord, ksf.index)
if self.hid_pending:
self._send_hid()
self.hid_pending = False
# Any newly buffered key events must be prepended to the working
# buffer.
if self._resume_buffer:
self._resume_buffer.extend(buffer)
buffer.clear()
buffer, self._resume_buffer = self._resume_buffer, buffer
self._resume_buffer_x = buffer
@property @property
def debug_enabled(self): def debug_enabled(self) -> bool:
return debug.enabled return debug.enabled
@debug_enabled.setter @debug_enabled.setter
def debug_enabled(self, enabled): def debug_enabled(self, enabled: bool):
debug.enabled = enabled debug.enabled = enabled
def pre_process_key(self, key, is_pressed, int_coord=None, index=0): def pre_process_key(
self,
key: Key,
is_pressed: bool,
int_coord: Optional[int] = None,
index: int = 0,
) -> None:
for module in self.modules[index:]: for module in self.modules[index:]:
try: try:
key = module.process_key(self, key, is_pressed, int_coord) key = module.process_key(self, key, is_pressed, int_coord)
@@ -187,16 +243,14 @@ class KMKKeyboard:
if key: if key:
self.process_key(key, is_pressed, int_coord) self.process_key(key, is_pressed, int_coord)
return self def process_key(
self, key: Key, is_pressed: bool, coord_int: Optional[int] = None
def process_key(self, key, is_pressed, coord_int=None): ) -> None:
if is_pressed: if is_pressed:
key.on_press(self, coord_int) key.on_press(self, coord_int)
else: else:
key.on_release(self, coord_int) key.on_release(self, coord_int)
return self
def resume_process_key( def resume_process_key(
self, self,
module: Module, module: Module,
@@ -205,24 +259,27 @@ class KMKKeyboard:
int_coord: Optional[int] = None, int_coord: Optional[int] = None,
) -> None: ) -> None:
index = self.modules.index(module) + 1 index = self.modules.index(module) + 1
self.pre_process_key(key, is_pressed, int_coord, index) ksf = KeyBufferFrame(
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
)
self._resume_buffer.append(ksf)
def remove_key(self, keycode): def remove_key(self, keycode: Key) -> None:
self.keys_pressed.discard(keycode) self.keys_pressed.discard(keycode)
return self.process_key(keycode, False) self.process_key(keycode, False)
def add_key(self, keycode): def add_key(self, keycode: Key) -> None:
self.keys_pressed.add(keycode) self.keys_pressed.add(keycode)
return self.process_key(keycode, True) self.process_key(keycode, True)
def tap_key(self, keycode): def tap_key(self, keycode: Key) -> None:
self.add_key(keycode) self.add_key(keycode)
# On the next cycle, we'll remove the key. # On the next cycle, we'll remove the key.
self.set_timeout(False, lambda: self.remove_key(keycode)) self.set_timeout(False, lambda: self.remove_key(keycode))
return self def set_timeout(
self, after_ticks: int, callback: Callable[[None], None]
def set_timeout(self, after_ticks, callback): ) -> Tuple[int, int]:
# We allow passing False as an implicit "run this on the next process timeouts cycle" # We allow passing False as an implicit "run this on the next process timeouts cycle"
if after_ticks is False: if after_ticks is False:
after_ticks = 0 after_ticks = 0
@@ -240,16 +297,16 @@ class KMKKeyboard:
return (timeout_key, idx) return (timeout_key, idx)
def cancel_timeout(self, timeout_key): def cancel_timeout(self, timeout_key: int) -> None:
try: try:
self._timeouts[timeout_key[0]][timeout_key[1]] = None self._timeouts[timeout_key[0]][timeout_key[1]] = None
except (KeyError, IndexError): except (KeyError, IndexError):
if debug.enabled: if debug.enabled:
debug(f'no such timeout: {timeout_key}') debug(f'no such timeout: {timeout_key}')
def _process_timeouts(self): def _process_timeouts(self) -> None:
if not self._timeouts: if not self._timeouts:
return self return
# Copy timeout keys to a temporary list to allow sorting. # Copy timeout keys to a temporary list to allow sorting.
# Prevent net timeouts set during handling from running on the current # Prevent net timeouts set during handling from running on the current
@@ -273,9 +330,7 @@ class KMKKeyboard:
self._processing_timeouts = False self._processing_timeouts = False
return self def _init_sanity_check(self) -> None:
def _init_sanity_check(self):
''' '''
Ensure the provided configuration is *probably* bootable Ensure the provided configuration is *probably* bootable
''' '''
@@ -290,9 +345,7 @@ class KMKKeyboard:
self.diode_orientation is not None self.diode_orientation is not None
), 'diode orientation must be defined' ), 'diode orientation must be defined'
return self def _init_coord_mapping(self) -> None:
def _init_coord_mapping(self):
''' '''
Attempt to sanely guess a coord_mapping if one is not provided. No-op Attempt to sanely guess a coord_mapping if one is not provided. No-op
if `kmk.extensions.split.Split` is used, it provides equivalent if `kmk.extensions.split.Split` is used, it provides equivalent
@@ -310,7 +363,7 @@ class KMKKeyboard:
cm.extend(m.coord_mapping) cm.extend(m.coord_mapping)
self.coord_mapping = tuple(cm) self.coord_mapping = tuple(cm)
def _init_hid(self): def _init_hid(self) -> None:
if self.hid_type == HIDModes.NOOP: if self.hid_type == HIDModes.NOOP:
self._hid_helper = AbstractHID self._hid_helper = AbstractHID
elif self.hid_type == HIDModes.USB: elif self.hid_type == HIDModes.USB:
@@ -322,7 +375,7 @@ class KMKKeyboard:
self._hid_helper = self._hid_helper(**self._go_args) self._hid_helper = self._hid_helper(**self._go_args)
self._hid_send_enabled = True self._hid_send_enabled = True
def _init_matrix(self): def _init_matrix(self) -> None:
if self.matrix is None: if self.matrix is None:
if debug.enabled: if debug.enabled:
debug('Initialising default matrix scanner.') debug('Initialising default matrix scanner.')
@@ -341,9 +394,7 @@ class KMKKeyboard:
except TypeError: except TypeError:
self.matrix = (self.matrix,) self.matrix = (self.matrix,)
return self def before_matrix_scan(self) -> None:
def before_matrix_scan(self):
for module in self.modules: for module in self.modules:
try: try:
module.before_matrix_scan(self) module.before_matrix_scan(self)
@@ -358,7 +409,7 @@ class KMKKeyboard:
if debug.enabled: if debug.enabled:
debug(f'Error in {ext}.before_matrix_scan: {err}') debug(f'Error in {ext}.before_matrix_scan: {err}')
def after_matrix_scan(self): def after_matrix_scan(self) -> None:
for module in self.modules: for module in self.modules:
try: try:
module.after_matrix_scan(self) module.after_matrix_scan(self)
@@ -373,7 +424,7 @@ class KMKKeyboard:
if debug.enabled: if debug.enabled:
debug(f'Error in {ext}.after_matrix_scan: {err}') debug(f'Error in {ext}.after_matrix_scan: {err}')
def before_hid_send(self): def before_hid_send(self) -> None:
for module in self.modules: for module in self.modules:
try: try:
module.before_hid_send(self) module.before_hid_send(self)
@@ -390,7 +441,7 @@ class KMKKeyboard:
f'Error in {ext}.before_hid_send: {err}', f'Error in {ext}.before_hid_send: {err}',
) )
def after_hid_send(self): def after_hid_send(self) -> None:
for module in self.modules: for module in self.modules:
try: try:
module.after_hid_send(self) module.after_hid_send(self)
@@ -405,7 +456,7 @@ class KMKKeyboard:
if debug.enabled: if debug.enabled:
debug(f'Error in {ext}.after_hid_send: {err}') debug(f'Error in {ext}.after_hid_send: {err}')
def powersave_enable(self): def powersave_enable(self) -> None:
for module in self.modules: for module in self.modules:
try: try:
module.on_powersave_enable(self) module.on_powersave_enable(self)
@@ -420,7 +471,7 @@ class KMKKeyboard:
if debug.enabled: if debug.enabled:
debug(f'Error in {ext}.powersave_enable: {err}') debug(f'Error in {ext}.powersave_enable: {err}')
def powersave_disable(self): def powersave_disable(self) -> None:
for module in self.modules: for module in self.modules:
try: try:
module.on_powersave_disable(self) module.on_powersave_disable(self)
@@ -434,12 +485,17 @@ class KMKKeyboard:
if debug.enabled: if debug.enabled:
debug(f'Error in {ext}.powersave_disable: {err}') debug(f'Error in {ext}.powersave_disable: {err}')
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs): def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs) -> None:
self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs) self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
while True: while True:
self._main_loop() self._main_loop()
def _init(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs): def _init(
self,
hid_type: HIDModes = HIDModes.USB,
secondary_hid_type: Optional[HIDModes] = None,
**kwargs,
) -> None:
self._go_args = kwargs self._go_args = kwargs
self.hid_type = hid_type self.hid_type = hid_type
self.secondary_hid_type = secondary_hid_type self.secondary_hid_type = secondary_hid_type
@@ -465,17 +521,20 @@ class KMKKeyboard:
if debug.enabled: if debug.enabled:
debug(f'init: {self}') debug(f'init: {self}')
def _main_loop(self): def _main_loop(self) -> None:
self.state_changed = False self.state_changed = False
self.sandbox.active_layers = self.active_layers.copy() self.sandbox.active_layers = self.active_layers.copy()
self.before_matrix_scan() self.before_matrix_scan()
self._process_resume_buffer()
for matrix in self.matrix: for matrix in self.matrix:
update = matrix.scan_for_changes() update = matrix.scan_for_changes()
if update: if update:
self.matrix_update = update self.matrix_update = update
break break
self.sandbox.matrix_update = self.matrix_update
self.sandbox.secondary_matrix_update = self.secondary_matrix_update self.sandbox.secondary_matrix_update = self.secondary_matrix_update
self.after_matrix_scan() self.after_matrix_scan()

View File

@@ -6,26 +6,26 @@ _TICKS_MAX = const(_TICKS_PERIOD - 1)
_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2) _TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
def ticks_diff(new, start): def ticks_diff(new: int, start: int) -> int:
diff = (new - start) & _TICKS_MAX diff = (new - start) & _TICKS_MAX
diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
return diff return diff
def ticks_add(ticks, delta): def ticks_add(ticks: int, delta: int) -> int:
return (ticks + delta) % _TICKS_PERIOD return (ticks + delta) % _TICKS_PERIOD
def check_deadline(new, start, ms): def check_deadline(new: int, start: int, ms: int) -> int:
return ticks_diff(new, start) < ms return ticks_diff(new, start) < ms
class PeriodicTimer: class PeriodicTimer:
def __init__(self, period): def __init__(self, period: int):
self.period = period self.period = period
self.last_tick = ticks_ms() self.last_tick = ticks_ms()
def tick(self): def tick(self) -> bool:
now = ticks_ms() now = ticks_ms()
if ticks_diff(now, self.last_tick) >= self.period: if ticks_diff(now, self.last_tick) >= self.period:
self.last_tick = now self.last_tick = now

View File

@@ -1,5 +1,5 @@
try: try:
from typing import Optional, Tuple from typing import Optional, Tuple, Union
except ImportError: except ImportError:
pass pass
from micropython import const from micropython import const
@@ -24,14 +24,16 @@ class Combo:
_remaining = [] _remaining = []
_timeout = None _timeout = None
_state = _ComboState.IDLE _state = _ComboState.IDLE
_match_coord = False
def __init__( def __init__(
self, self,
match: Tuple[Key, ...], match: Tuple[Union[Key, int], ...],
result: Key, result: Key,
fast_reset=None, fast_reset=None,
per_key_timeout=None, per_key_timeout=None,
timeout=None, timeout=None,
match_coord=None,
): ):
''' '''
match: tuple of keys (KC.A, KC.B) match: tuple of keys (KC.A, KC.B)
@@ -45,22 +47,36 @@ class Combo:
self.per_key_timeout = per_key_timeout self.per_key_timeout = per_key_timeout
if timeout is not None: if timeout is not None:
self.timeout = timeout self.timeout = timeout
if match_coord is not None:
self._match_coord = match_coord
def __repr__(self): def __repr__(self):
return f'{self.__class__.__name__}({[k.code for k in self.match]})' return f'{self.__class__.__name__}({[k.code for k in self.match]})'
def matches(self, key): def matches(self, key: Key, int_coord: int):
raise NotImplementedError raise NotImplementedError
def has_match(self, key: Key, int_coord: int):
return self._match_coord and int_coord in self.match or key in self.match
def insert(self, key: Key, int_coord: int):
if self._match_coord:
self._remaining.insert(0, int_coord)
else:
self._remaining.insert(0, key)
def reset(self): def reset(self):
self._remaining = list(self.match) self._remaining = list(self.match)
class Chord(Combo): class Chord(Combo):
def matches(self, key): def matches(self, key: Key, int_coord: int):
if key in self._remaining: if not self._match_coord and key in self._remaining:
self._remaining.remove(key) self._remaining.remove(key)
return True return True
elif self._match_coord and int_coord in self._remaining:
self._remaining.remove(int_coord)
return True
else: else:
return False return False
@@ -70,8 +86,12 @@ class Sequence(Combo):
per_key_timeout = True per_key_timeout = True
timeout = 1000 timeout = 1000
def matches(self, key): def matches(self, key: Key, int_coord: int):
if self._remaining and self._remaining[0] == key: if (
not self._match_coord and self._remaining and self._remaining[0] == key
) or (
self._match_coord and self._remaining and self._remaining[0] == int_coord
):
self._remaining.pop(0) self._remaining.pop(0)
return True return True
else: else:
@@ -127,7 +147,7 @@ class Combos(Module):
for combo in self.combos: for combo in self.combos:
if combo._state != _ComboState.MATCHING: if combo._state != _ComboState.MATCHING:
continue continue
if combo.matches(key): if combo.matches(key, int_coord):
continue continue
combo._state = _ComboState.IDLE combo._state = _ComboState.IDLE
if combo._timeout: if combo._timeout:
@@ -171,10 +191,11 @@ class Combos(Module):
) )
else: else:
# There's no matching combo: send and reset key buffer # There's no matching combo: send and reset key buffer
if self._key_buffer:
self._key_buffer.append((int_coord, key, True))
self.send_key_buffer(keyboard) self.send_key_buffer(keyboard)
self._key_buffer = [] self._key_buffer = []
if int_coord is not None: key = None
key = keyboard._find_key_in_map(int_coord)
return key return key
@@ -182,7 +203,7 @@ class Combos(Module):
for combo in self.combos: for combo in self.combos:
if combo._state != _ComboState.ACTIVE: if combo._state != _ComboState.ACTIVE:
continue continue
if key in combo.match: if combo.has_match(key, int_coord):
# Deactivate combo if it matches current key. # Deactivate combo if it matches current key.
self.deactivate(keyboard, combo) self.deactivate(keyboard, combo)
@@ -190,7 +211,7 @@ class Combos(Module):
self.reset_combo(keyboard, combo) self.reset_combo(keyboard, combo)
self._key_buffer = [] self._key_buffer = []
else: else:
combo._remaining.insert(0, key) combo.insert(key, int_coord)
combo._state = _ComboState.MATCHING combo._state = _ComboState.MATCHING
key = combo.result key = combo.result
@@ -203,7 +224,7 @@ class Combos(Module):
for combo in self.combos: for combo in self.combos:
if combo._state != _ComboState.MATCHING: if combo._state != _ComboState.MATCHING:
continue continue
if key not in combo.match: if not combo.has_match(key, int_coord):
continue continue
# Combo matches, but first key released before timeout. # Combo matches, but first key released before timeout.
@@ -216,7 +237,7 @@ class Combos(Module):
if combo.fast_reset: if combo.fast_reset:
self.reset_combo(keyboard, combo) self.reset_combo(keyboard, combo)
else: else:
combo._remaining.insert(0, key) combo.insert(key, int_coord)
combo._state = _ComboState.MATCHING combo._state = _ComboState.MATCHING
self.reset(keyboard) self.reset(keyboard)
@@ -231,12 +252,14 @@ class Combos(Module):
elif len(combo._remaining) == len(combo.match) - 1: elif len(combo._remaining) == len(combo.match) - 1:
self.reset_combo(keyboard, combo) self.reset_combo(keyboard, combo)
if not self.count_matching(): if not self.count_matching():
self._key_buffer.append((int_coord, key, False))
self.send_key_buffer(keyboard) self.send_key_buffer(keyboard)
self._key_buffer = [] self._key_buffer = []
key = None
# Anything between first and last key released. # Anything between first and last key released.
else: else:
combo._remaining.insert(0, key) combo.insert(key, int_coord)
# Don't propagate key-release events for keys that have been # Don't propagate key-release events for keys that have been
# buffered. Append release events only if corresponding press is in # buffered. Append release events only if corresponding press is in
@@ -275,17 +298,7 @@ class Combos(Module):
def send_key_buffer(self, keyboard): def send_key_buffer(self, keyboard):
for (int_coord, key, is_pressed) in self._key_buffer: for (int_coord, key, is_pressed) in self._key_buffer:
new_key = None keyboard.resume_process_key(self, key, is_pressed, int_coord)
if not is_pressed:
try:
new_key = keyboard._coordkeys_pressed[int_coord]
except KeyError:
new_key = None
if new_key is None:
new_key = keyboard._find_key_in_map(int_coord)
keyboard.resume_process_key(self, new_key, is_pressed, int_coord)
keyboard._send_hid()
def activate(self, keyboard, combo): def activate(self, keyboard, combo):
combo.result.on_press(keyboard) combo.result.on_press(keyboard)

View File

@@ -1,6 +1,6 @@
from micropython import const from micropython import const
from kmk.keys import make_argumented_key from kmk.keys import KC, make_argumented_key
from kmk.modules import Module from kmk.modules import Module
from kmk.utils import Debug from kmk.utils import Debug
@@ -12,6 +12,7 @@ class ActivationType:
RELEASED = const(1) RELEASED = const(1)
HOLD_TIMEOUT = const(2) HOLD_TIMEOUT = const(2)
INTERRUPTED = const(3) INTERRUPTED = const(3)
REPEAT = const(4)
class HoldTapKeyState: class HoldTapKeyState:
@@ -30,12 +31,14 @@ class HoldTapKeyMeta:
prefer_hold=True, prefer_hold=True,
tap_interrupted=False, tap_interrupted=False,
tap_time=None, tap_time=None,
repeat=False,
): ):
self.tap = tap self.tap = tap
self.hold = hold self.hold = hold
self.prefer_hold = prefer_hold self.prefer_hold = prefer_hold
self.tap_interrupted = tap_interrupted self.tap_interrupted = tap_interrupted
self.tap_time = tap_time self.tap_time = tap_time
self.repeat = repeat
class HoldTap(Module): class HoldTap(Module):
@@ -44,6 +47,7 @@ class HoldTap(Module):
def __init__(self): def __init__(self):
self.key_buffer = [] self.key_buffer = []
self.key_states = {} self.key_states = {}
if not KC.get('HT'):
make_argumented_key( make_argumented_key(
validator=HoldTapKeyMeta, validator=HoldTapKeyMeta,
names=('HT',), names=('HT',),
@@ -82,12 +86,8 @@ class HoldTap(Module):
self.ht_activate_on_interrupt( self.ht_activate_on_interrupt(
key, keyboard, *state.args, **state.kwargs key, keyboard, *state.args, **state.kwargs
) )
keyboard._send_hid()
send_buffer = True send_buffer = True
if state.activated == ActivationType.INTERRUPTED:
current_key = keyboard._find_key_in_map(int_coord)
# if interrupt on release: store interrupting keys until one of them # if interrupt on release: store interrupting keys until one of them
# is released. # is released.
if ( if (
@@ -100,10 +100,13 @@ class HoldTap(Module):
# apply changes with 'side-effects' on key_states or the loop behaviour # apply changes with 'side-effects' on key_states or the loop behaviour
# outside the loop. # outside the loop.
if append_buffer: if append_buffer:
self.key_buffer.append((int_coord, current_key)) self.key_buffer.append((int_coord, current_key, is_pressed))
current_key = None current_key = None
elif send_buffer: elif send_buffer:
self.send_key_buffer(keyboard) self.send_key_buffer(keyboard)
keyboard.resume_process_key(self, current_key, is_pressed, int_coord)
current_key = None
return current_key return current_key
@@ -120,7 +123,20 @@ class HoldTap(Module):
return return
def ht_pressed(self, key, keyboard, *args, **kwargs): def ht_pressed(self, key, keyboard, *args, **kwargs):
'''Do nothing yet, action resolves when key is released, timer expires or other key is pressed.''' '''Unless in repeat mode, do nothing yet, action resolves when key is released, timer expires or other key is pressed.'''
if key in self.key_states:
state = self.key_states[key]
keyboard.cancel_timeout(self.key_states[key].timeout_key)
if state.activated == ActivationType.RELEASED:
state.activated = ActivationType.REPEAT
self.ht_activate_tap(key, keyboard, *args, **kwargs)
elif state.activated == ActivationType.HOLD_TIMEOUT:
self.ht_activate_hold(key, keyboard, *args, **kwargs)
elif state.activated == ActivationType.INTERRUPTED:
self.ht_activate_on_interrupt(key, keyboard, *args, **kwargs)
return
if key.meta.tap_time is None: if key.meta.tap_time is None:
tap_time = self.tap_time tap_time = self.tap_time
else: else:
@@ -149,10 +165,24 @@ class HoldTap(Module):
elif state.activated == ActivationType.PRESSED: elif state.activated == ActivationType.PRESSED:
# press and release tap because key released within tap time # press and release tap because key released within tap time
self.ht_activate_tap(key, keyboard, *args, **kwargs) self.ht_activate_tap(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard)
self.ht_deactivate_tap(key, keyboard, *args, **kwargs) self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
state.activated = ActivationType.RELEASED state.activated = ActivationType.RELEASED
self.send_key_buffer(keyboard) self.send_key_buffer(keyboard)
del self.key_states[key] elif state.activated == ActivationType.REPEAT:
state.activated = ActivationType.RELEASED
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
# don't delete the key state right now in this case
tap_time = 0
if key.meta.repeat:
if key.meta.tap_time is None:
tap_time = self.tap_time
else:
tap_time = key.meta.tap_time
state.timeout_key = keyboard.set_timeout(
tap_time, lambda: self.key_states.pop(key)
)
return keyboard return keyboard
@@ -176,11 +206,12 @@ class HoldTap(Module):
del self.key_states[key] del self.key_states[key]
def send_key_buffer(self, keyboard): def send_key_buffer(self, keyboard):
for (int_coord, key) in self.key_buffer: if not self.key_buffer:
new_key = keyboard._find_key_in_map(int_coord) return
keyboard.resume_process_key(self, new_key, True, int_coord)
for (int_coord, key, is_pressed) in self.key_buffer:
keyboard.resume_process_key(self, key, is_pressed, int_coord)
keyboard._send_hid()
self.key_buffer.clear() self.key_buffer.clear()
def ht_activate_hold(self, key, keyboard, *args, **kwargs): def ht_activate_hold(self, key, keyboard, *args, **kwargs):
@@ -191,23 +222,16 @@ class HoldTap(Module):
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs): def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
if debug.enabled: if debug.enabled:
debug('ht_deactivate_hold') debug('ht_deactivate_hold')
keyboard.set_timeout( keyboard.resume_process_key(self, key.meta.hold, False)
False, lambda: keyboard.resume_process_key(self, key.meta.hold, False)
)
def ht_activate_tap(self, key, keyboard, *args, **kwargs): def ht_activate_tap(self, key, keyboard, *args, **kwargs):
if debug.enabled: if debug.enabled:
debug('ht_activate_tap') debug('ht_activate_tap')
keyboard.resume_process_key(self, key.meta.tap, True) keyboard.resume_process_key(self, key.meta.tap, True)
def ht_deactivate_tap(self, key, keyboard, *args, delayed=True, **kwargs): def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
if debug.enabled: if debug.enabled:
debug('ht_deactivate_tap') debug('ht_deactivate_tap')
if delayed:
keyboard.set_timeout(
False, lambda: keyboard.resume_process_key(self, key.meta.tap, False)
)
else:
keyboard.resume_process_key(self, key.meta.tap, False) keyboard.resume_process_key(self, key.meta.tap, False)
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs): def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
@@ -224,4 +248,4 @@ class HoldTap(Module):
if key.meta.prefer_hold: if key.meta.prefer_hold:
self.ht_deactivate_hold(key, keyboard, *args, **kwargs) self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
else: else:
self.ht_deactivate_tap(key, keyboard, *args, delayed=False, **kwargs) self.ht_deactivate_tap(key, keyboard, *args, **kwargs)

View File

@@ -1,6 +1,6 @@
'''One layer isn't enough. Adds keys to get to more of them''' '''One layer isn't enough. Adds keys to get to more of them'''
from kmk.keys import KC, make_argumented_key from kmk.keys import KC, make_argumented_key
from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKeyMeta from kmk.modules.holdtap import HoldTap, HoldTapKeyMeta
from kmk.utils import Debug from kmk.utils import Debug
debug = Debug(__name__) debug = Debug(__name__)
@@ -73,20 +73,6 @@ class Layers(HoldTap):
on_release=self.ht_released, on_release=self.ht_released,
) )
def process_key(self, keyboard, key, is_pressed, int_coord):
current_key = super().process_key(keyboard, key, is_pressed, int_coord)
for key, state in self.key_states.items():
if key == current_key:
continue
# on interrupt: key must be translated here, because it was asigned
# before the layer shift happend.
if state.activated == ActivationType.INTERRUPTED:
current_key = keyboard._find_key_in_map(int_coord)
return current_key
def _df_pressed(self, key, keyboard, *args, **kwargs): def _df_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Switches the default layer Switches the default layer

View File

@@ -29,6 +29,12 @@ class OneShot(HoldTap):
elif state.activated == ActivationType.RELEASED and is_pressed: elif state.activated == ActivationType.RELEASED and is_pressed:
state.activated = ActivationType.INTERRUPTED state.activated = ActivationType.INTERRUPTED
elif state.activated == ActivationType.INTERRUPTED: elif state.activated == ActivationType.INTERRUPTED:
if is_pressed:
keyboard.remove_key(key.meta.tap)
self.key_buffer.append((int_coord, current_key, is_pressed))
keyboard.set_timeout(False, lambda: self.send_key_buffer(keyboard))
current_key = None
else:
self.ht_released(key, keyboard) self.ht_released(key, keyboard)
return current_key return current_key
@@ -37,6 +43,7 @@ class OneShot(HoldTap):
'''Register HoldTap mechanism and activate os key.''' '''Register HoldTap mechanism and activate os key.'''
self.ht_pressed(key, keyboard, *args, **kwargs) self.ht_pressed(key, keyboard, *args, **kwargs)
self.ht_activate_tap(key, keyboard, *args, **kwargs) self.ht_activate_tap(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard)
return keyboard return keyboard
def osk_released(self, key, keyboard, *args, **kwargs): def osk_released(self, key, keyboard, *args, **kwargs):

View File

@@ -1,13 +1,10 @@
'''Enables splitting keyboards wirelessly or wired''' '''Enables splitting keyboards wirelessly or wired'''
import busio
from micropython import const from micropython import const
from supervisor import runtime, ticks_ms from supervisor import runtime
from keypad import Event as KeyEvent from keypad import Event as KeyEvent
from storage import getmount from storage import getmount
from kmk.hid import HIDModes
from kmk.kmktime import check_deadline
from kmk.modules import Module from kmk.modules import Module
@@ -21,6 +18,7 @@ class SplitType:
I2C = const(2) # unused I2C = const(2) # unused
ONEWIRE = const(3) # unused ONEWIRE = const(3) # unused
BLE = const(4) BLE = const(4)
PIO_UART = const(5)
class Split(Module): class Split(Module):
@@ -50,52 +48,79 @@ class Split(Module):
self.data_pin2 = data_pin2 self.data_pin2 = data_pin2
self.uart_flip = uart_flip self.uart_flip = uart_flip
self._use_pio = use_pio self._use_pio = use_pio
self._uart = None self._transport = None
self._uart_interval = uart_interval self._uart_interval = uart_interval
self._debug_enabled = debug_enabled self._debug_enabled = debug_enabled
self.uart_header = bytearray([0xB2]) # Any non-zero byte should work self.uart_header = bytearray([0xB2]) # Any non-zero byte should work
if self.split_type == SplitType.BLE: if split_type == SplitType.UART and use_pio:
try: split_type = SplitType.PIO_UART
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import (
ProvideServicesAdvertisement,
)
from adafruit_ble.services.nordic import UARTService
self.BLERadio = BLERadio
self.ProvideServicesAdvertisement = ProvideServicesAdvertisement
self.UARTService = UARTService
except ImportError:
print('BLE Import error')
return # BLE isn't supported on this platform
self._ble_last_scan = ticks_ms() - 5000
self._connection_count = 0
self._split_connected = False
self._uart_connection = None
self._advertisment = None # Seems to not be used anywhere
self._advertising = False
self._psave_enable = False
if self._use_pio:
from kmk.transports.pio_uart import PIO_UART
self.PIO_UART = PIO_UART
def during_bootup(self, keyboard): def during_bootup(self, keyboard):
# Set up name for target side detection and BLE advertisment # Set up name for target side detection and BLE advertisment
name = str(getmount('/').label)
if self.split_type == SplitType.BLE:
if keyboard.hid_type == HIDModes.BLE:
self._ble = keyboard._hid_helper.ble
else:
self._ble = self.BLERadio()
self._ble.name = name
else:
# Try to guess data pins if not supplied
if not self.data_pin: if not self.data_pin:
self.data_pin = keyboard.data_pin self.data_pin = keyboard.data_pin
self._get_side()
if not self._is_target:
keyboard._hid_send_enabled = False
if self.split_offset is None:
self.split_offset = keyboard.matrix[-1].coord_mapping[-1] + 1
self._init_transport(keyboard)
# Attempt to sanely guess a coord_mapping if one is not provided.
if not keyboard.coord_mapping:
self._guess_coord_mapping()
if self.split_side == SplitSide.RIGHT:
offset = self.split_offset
for matrix in keyboard.matrix:
matrix.offset = offset
offset += matrix.key_count
def before_matrix_scan(self, keyboard):
if self._can_receive(keyboard):
keyboard.secondary_matrix_update = self._transport.receive(keyboard)
def after_matrix_scan(self, keyboard):
if keyboard.matrix_update:
self._transport.write(keyboard, keyboard.matrix_update)
def before_hid_send(self, keyboard):
if not self._is_target:
keyboard.hid_pending = False
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
self._transport.powersave(True)
def on_powersave_disable(self, keyboard):
self._transport.powersave(False)
def _serialize_update(self, update):
buffer = bytearray(2)
buffer[0] = update.key_number
buffer[1] = update.pressed
return buffer
def _deserialize_update(self, update):
kevent = KeyEvent(key_number=update[0], pressed=update[1])
return kevent
def _checksum(self, update):
checksum = bytes([sum(update) & 0xFF])
return checksum
def _get_side(self):
name = str(getmount('/').label)
# if split side was given, find target from split_side. # if split side was given, find target from split_side.
if self.split_side == SplitSide.LEFT: if self.split_side == SplitSide.LEFT:
self._is_target = bool(self.split_target_left) self._is_target = bool(self.split_target_left)
@@ -108,38 +133,44 @@ class Split(Module):
or self.split_type == SplitType.ONEWIRE or self.split_type == SplitType.ONEWIRE
): ):
self._is_target = runtime.usb_connected self._is_target = runtime.usb_connected
elif self.split_type == SplitType.BLE:
self._is_target = name.endswith('L') == self.split_target_left
if name.endswith('L'): if name.endswith('L'):
self.split_side = SplitSide.LEFT self.split_side = SplitSide.LEFT
elif name.endswith('R'): elif name.endswith('R'):
self.split_side = SplitSide.RIGHT self.split_side = SplitSide.RIGHT
if not self._is_target: def _init_transport(self, keyboard):
keyboard._hid_send_enabled = False if self.split_type == SplitType.UART:
from kmk.transports.uart import UART
elif self.split_type == SplitType.PIO_UART:
from kmk.transports.pio_uart import PIO_UART
elif self.split_type == SplitType.BLE:
from kmk.transports.ble import BLE_UART
if self.split_offset is None:
self.split_offset = keyboard.matrix[-1].coord_mapping[-1] + 1
if self.split_type == SplitType.UART and self.data_pin is not None:
if self._is_target or not self.uart_flip: if self._is_target or not self.uart_flip:
if self._use_pio: tx_pin = self.data_pin2
self._uart = self.PIO_UART(tx=self.data_pin2, rx=self.data_pin) rx_pin = self.data_pin
else: else:
self._uart = busio.UART( tx_pin = self.data_pin
tx=self.data_pin2, rx=self.data_pin, timeout=self._uart_interval rx_pin = self.data_pin2
)
else:
if self._use_pio:
self._uart = self.PIO_UART(tx=self.data_pin, rx=self.data_pin2)
else:
self._uart = busio.UART(
tx=self.data_pin, rx=self.data_pin2, timeout=self._uart_interval
)
# Attempt to sanely guess a coord_mapping if one is not provided. if self.split_type == SplitType.UART:
if not keyboard.coord_mapping: self._transport = UART(
tx=tx_pin,
rx=rx_pin,
target=self.is_target,
uart_interval=self._uart_interval,
)
elif self.split_type == SplitType.PIO_UART:
self._transport = PIO_UART(tx=tx_pin, rx=rx_pin)
elif self.split_type == SplitType.BLE:
self._transport = BLE_UART(
target=self._is_target, uart_interval=self.uart_interval
)
else:
raise NotImplementedError
def _guess_coord_mapping(self, keyboard):
cm = [] cm = []
rows_to_calc = len(keyboard.row_pins) rows_to_calc = len(keyboard.row_pins)
@@ -158,227 +189,13 @@ class Split(Module):
keyboard.coord_mapping = tuple(cm) keyboard.coord_mapping = tuple(cm)
if self.split_side == SplitSide.RIGHT: def _can_receive(self, keyboard) -> bool:
offset = self.split_offset
for matrix in keyboard.matrix:
matrix.offset = offset
offset += matrix.key_count
def before_matrix_scan(self, keyboard):
if self.split_type == SplitType.BLE: if self.split_type == SplitType.BLE:
self._check_all_connections(keyboard) self._transport.check_connection(keyboard)
self._receive_ble(keyboard) return True
elif self.split_type == SplitType.UART: elif self.split_type == SplitType.UART:
if self._is_target or self.data_pin2: if self._is_target or self.data_pin2:
self._receive_uart(keyboard)
elif self.split_type == SplitType.ONEWIRE:
pass # Protocol needs written
return
def after_matrix_scan(self, keyboard):
if keyboard.matrix_update:
if self.split_type == SplitType.UART:
if not self._is_target or self.data_pin2:
self._send_uart(keyboard.matrix_update)
else:
pass # explicit pass just for dev sanity...
elif self.split_type == SplitType.BLE:
self._send_ble(keyboard.matrix_update)
elif self.split_type == SplitType.ONEWIRE:
pass # Protocol needs written
else:
print('Unexpected case in after_matrix_scan')
return
def before_hid_send(self, keyboard):
if not self._is_target:
keyboard.hid_pending = False
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
if self.split_type == SplitType.BLE:
if self._uart_connection and not self._psave_enable:
self._uart_connection.connection_interval = self._uart_interval
self._psave_enable = True
def on_powersave_disable(self, keyboard):
if self.split_type == SplitType.BLE:
if self._uart_connection and self._psave_enable:
self._uart_connection.connection_interval = 11.25
self._psave_enable = False
def _check_all_connections(self, keyboard):
'''Validates the correct number of BLE connections'''
self._previous_connection_count = self._connection_count
self._connection_count = len(self._ble.connections)
if self._is_target:
if self._advertising or not self._check_if_split_connected():
self._target_advertise()
elif self._connection_count < 2 and keyboard.hid_type == HIDModes.BLE:
keyboard._hid_helper.start_advertising()
elif not self._is_target and self._connection_count < 1:
self._initiator_scan()
def _check_if_split_connected(self):
# I'm looking for a way how to recognize which connection is on and which one off
# For now, I found that service name relation to having other CP device
if self._connection_count == 0:
return False
if self._connection_count == 2:
self._split_connected = True
return True return True
# Polling this takes some time so I check only if connection_count changed
if self._previous_connection_count == self._connection_count:
return self._split_connected
bleio_connection = self._ble.connections[0]._bleio_connection
connection_services = bleio_connection.discover_remote_services()
for service in connection_services:
if str(service.uuid).startswith("UUID('adaf0001"):
self._split_connected = True
return True
return False return False
def _initiator_scan(self):
'''Scans for target device'''
self._uart = None
self._uart_connection = None
# See if any existing connections are providing UARTService.
self._connection_count = len(self._ble.connections)
if self._connection_count > 0 and not self._uart:
for connection in self._ble.connections:
if self.UARTService in connection:
self._uart_connection = connection
self._uart_connection.connection_interval = 11.25
self._uart = self._uart_connection[self.UARTService]
break
if not self._uart:
if self._debug_enabled:
print('Scanning')
self._ble.stop_scan()
for adv in self._ble.start_scan(
self.ProvideServicesAdvertisement, timeout=20
):
if self._debug_enabled:
print('Scanning')
if self.UARTService in adv.services and adv.rssi > -70:
self._uart_connection = self._ble.connect(adv)
self._uart_connection.connection_interval = 11.25
self._uart = self._uart_connection[self.UARTService]
self._ble.stop_scan()
if self._debug_enabled:
print('Scan complete')
break
self._ble.stop_scan()
def _target_advertise(self):
'''Advertises the target for the initiator to find'''
# Give previous advertising some time to complete
if self._advertising:
if self._check_if_split_connected():
if self._debug_enabled:
print('Advertising complete')
self._ble.stop_advertising()
self._advertising = False
return
if not self.ble_rescan_timer():
return
if self._debug_enabled:
print('Advertising not answered')
self._ble.stop_advertising()
if self._debug_enabled:
print('Advertising')
# Uart must not change on this connection if reconnecting
if not self._uart:
self._uart = self.UARTService()
advertisement = self.ProvideServicesAdvertisement(self._uart)
self._ble.start_advertising(advertisement)
self._advertising = True
self.ble_time_reset()
def ble_rescan_timer(self):
'''If true, the rescan timer is up'''
return not bool(check_deadline(ticks_ms(), self._ble_last_scan, 5000))
def ble_time_reset(self):
'''Resets the rescan timer'''
self._ble_last_scan = ticks_ms()
def _serialize_update(self, update):
buffer = bytearray(2)
buffer[0] = update.key_number
buffer[1] = update.pressed
return buffer
def _deserialize_update(self, update):
kevent = KeyEvent(key_number=update[0], pressed=update[1])
return kevent
def _send_ble(self, update):
if self._uart:
try:
self._uart.write(self._serialize_update(update))
except OSError:
try:
self._uart.disconnect()
except: # noqa: E722
if self._debug_enabled:
print('UART disconnect failed')
if self._debug_enabled:
print('Connection error')
self._uart_connection = None
self._uart = None
def _receive_ble(self, keyboard):
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
while self._uart.in_waiting >= 2:
update = self._deserialize_update(self._uart.read(2))
self._uart_buffer.append(update)
if self._uart_buffer:
keyboard.secondary_matrix_update = self._uart_buffer.pop(0)
def _checksum(self, update):
checksum = bytes([sum(update) & 0xFF])
return checksum
def _send_uart(self, update):
# Change offsets depending on where the data is going to match the correct
# matrix location of the receiever
if self._uart is not None:
update = self._serialize_update(update)
self._uart.write(self.uart_header)
self._uart.write(update)
self._uart.write(self._checksum(update))
def _receive_uart(self, keyboard):
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
if self._uart.in_waiting >= 60:
# This is a dirty hack to prevent crashes in unrealistic cases
import microcontroller
microcontroller.reset()
while self._uart.in_waiting >= 4:
# Check the header
if self._uart.read(1) == self.uart_header:
update = self._uart.read(2)
# check the checksum
if self._checksum(update) == self._uart.read(1):
self._uart_buffer.append(self._deserialize_update(update))
if self._uart_buffer:
keyboard.secondary_matrix_update = self._uart_buffer.pop(0)

View File

@@ -48,8 +48,11 @@ class TapDance(HoldTap):
if state.activated == ActivationType.RELEASED: if state.activated == ActivationType.RELEASED:
keyboard.cancel_timeout(state.timeout_key) keyboard.cancel_timeout(state.timeout_key)
self.ht_activate_tap(_key, keyboard) self.ht_activate_tap(_key, keyboard)
keyboard._send_hid() self.send_key_buffer(keyboard)
self.ht_deactivate_tap(_key, keyboard, delayed=False) self.ht_deactivate_tap(_key, keyboard)
keyboard.resume_process_key(self, key, is_pressed, int_coord)
key = None
del self.key_states[_key] del self.key_states[_key]
del self.td_counts[state.tap_dance] del self.td_counts[state.tap_dance]
@@ -114,6 +117,6 @@ class TapDance(HoldTap):
state = self.key_states[key] state = self.key_states[key]
if state.activated == ActivationType.RELEASED: if state.activated == ActivationType.RELEASED:
self.ht_activate_tap(key, keyboard, *args, **kwargs) self.ht_activate_tap(key, keyboard, *args, **kwargs)
keyboard._send_hid() self.send_key_buffer(keyboard)
del self.td_counts[state.tap_dance] del self.td_counts[state.tap_dance]
super().on_tap_time_expired(key, keyboard, *args, **kwargs) super().on_tap_time_expired(key, keyboard, *args, **kwargs)

View File

@@ -0,0 +1,20 @@
translate = {
'D3': 0,
'D2': 1,
'D1': 4,
'D0': 5,
'D4': 6,
'C6': 7,
'D7': 8,
'E6': 9,
'B4': 10,
'B5': 11,
'B6': 12,
'B2': 13,
'B3': 14,
'B1': 15,
'F7': 16,
'F6': 17,
'F5': 18,
'F4': 19,
}

View File

@@ -0,0 +1,28 @@
import board
pinout = [
board.TX,
board.RX,
None, # GND
None, # GND
board.SDA,
board.SCL,
board.GP04,
board.GP05,
board.GP06,
board.GP07,
board.GP08,
board.GP09,
board.GP21,
board.GP23,
board.GP20,
board.GP22,
board.GP26,
board.GP27,
board.GP28,
board.GP29,
None, # 3.3v
None, # RST
None, # GND
None, # RAW
]

View File

@@ -0,0 +1,28 @@
import board
pinout = [
board.TX,
board.RX,
None, # GND
None, # GND
board.SDA,
board.SCL,
board.P0_22,
board.P0_24,
board.P1_00,
board.P0_11,
board.P1_04,
board.P1_06,
board.P0_09,
board.P0_10,
board.P1_11,
board.P1_13,
board.P1_15,
board.P0_02,
board.P0_29,
board.P0_31,
None, # 3.3v
None, # RST
None, # GND
None, # Battery+
]

View File

@@ -0,0 +1,28 @@
import board
pinout = [
board.TX,
board.RX,
None, # GND
None, # GND
board.D2,
board.D3,
board.D4,
board.D5,
board.D6,
board.D7,
board.D8,
board.D9,
board.D21,
board.MOSI,
board.MISO,
board.SCK,
board.D26,
board.D27,
board.D28,
board.D29,
None, # 3.3v
None, # RST
None, # GND
None, # RAW
]

181
kmk/transports/ble.py Normal file
View File

@@ -0,0 +1,181 @@
from supervisor import ticks_ms
from kmk.hid import HIDModes
from kmk.kmktime import check_deadline
class BLE_UART:
def __init__(self, is_target=True, uart_interval=20):
try:
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
self.BLERadio = BLERadio
self.ProvideServicesAdvertisement = ProvideServicesAdvertisement
self.UARTService = UARTService
except ImportError:
print('BLE Import error')
return # BLE isn't supported on this platform
self._debug_enabled = True
self._ble_last_scan = ticks_ms() - 5000
self._connection_count = 0
self._split_connected = False
self._uart_connection = None
self._advertisment = None # Seems to not be used anywhere
self._advertising = False
self._psave_enabled = False
self._uart = None
self._uart_interval = uart_interval
self._is_target = is_target
@property
def in_waiting(self):
return self._uart.in_waiting
def read(self, n):
return self._uart.read(n)
def readinto(self, buf):
return self._uart.readinto(buf)
def write(self, buffer):
try:
self._uart.write(buffer)
except OSError:
try:
self._uart.disconnect()
except: # noqa: E722
if self._debug_enabled:
print('UART disconnect failed')
if self._debug_enabled:
print('Connection error')
self._uart_connection = None
self._uart = None
def check_connection(self, keyboard):
self._check_all_connections(keyboard)
def powersave(self, enable=True):
if enable:
if self._uart_connection and not self._psave_enable:
self._uart_connection.connection_interval = self._uart_interval
self._psave_enabled = True
else:
if self._uart_connection and self._psave_enable:
self._uart_connection.connection_interval = 11.25
self._psave_enable = False
def _check_all_connections(self, keyboard):
'''Validates the correct number of BLE connections'''
self._previous_connection_count = self._connection_count
self._connection_count = len(self._ble.connections)
if self._is_target:
if self._advertising or not self._check_if_split_connected():
self._target_advertise()
elif self._connection_count < 2 and keyboard.hid_type == HIDModes.BLE:
keyboard._hid_helper.start_advertising()
elif not self._is_target and self._connection_count < 1:
self._initiator_scan()
def _check_if_split_connected(self):
# I'm looking for a way how to recognize which connection is on and which one off
# For now, I found that service name relation to having other CP device
if self._connection_count == 0:
return False
if self._connection_count == 2:
self._split_connected = True
return True
# Polling this takes some time so I check only if connection_count changed
if self._previous_connection_count == self._connection_count:
return self._split_connected
bleio_connection = self._ble.connections[0]._bleio_connection
connection_services = bleio_connection.discover_remote_services()
for service in connection_services:
if str(service.uuid).startswith("UUID('adaf0001"):
self._split_connected = True
return True
return False
def _initiator_scan(self):
'''Scans for target device'''
self._uart = None
self._uart_connection = None
# See if any existing connections are providing UARTService.
self._connection_count = len(self._ble.connections)
if self._connection_count > 0 and not self._uart:
for connection in self._ble.connections:
if self.UARTService in connection:
self._uart_connection = connection
self._uart_connection.connection_interval = 11.25
self._uart = self._uart_connection[self.UARTService]
break
if not self._uart:
if self._debug_enabled:
print('Scanning')
self._ble.stop_scan()
for adv in self._ble.start_scan(
self.ProvideServicesAdvertisement, timeout=20
):
if self._debug_enabled:
print('Scanning')
if self.UARTService in adv.services and adv.rssi > -70:
self._uart_connection = self._ble.connect(adv)
self._uart_connection.connection_interval = 11.25
self._uart = self._uart_connection[self.UARTService]
self._ble.stop_scan()
if self._debug_enabled:
print('Scan complete')
break
self._ble.stop_scan()
def _target_advertise(self):
'''Advertises the target for the initiator to find'''
# Give previous advertising some time to complete
if self._advertising:
if self._check_if_split_connected():
if self._debug_enabled:
print('Advertising complete')
self._ble.stop_advertising()
self._advertising = False
return
if not self.ble_rescan_timer():
return
if self._debug_enabled:
print('Advertising not answered')
self._ble.stop_advertising()
if self._debug_enabled:
print('Advertising')
# Uart must not change on this connection if reconnecting
if not self._uart:
self._uart = self.UARTService()
advertisement = self.ProvideServicesAdvertisement(self._uart)
self._ble.start_advertising(advertisement)
self._advertising = True
self.ble_time_reset()
def ble_rescan_timer(self):
'''If true, the rescan timer is up'''
return not bool(check_deadline(ticks_ms(), self._ble_last_scan, 5000))
def ble_time_reset(self):
'''Resets the rescan timer'''
self._ble_last_scan = ticks_ms()
def receive(self, keyboard):
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
while self._uart.in_waiting >= 2:
update = self._deserialize_update(self._uart.read(2))
self._uart_buffer.append(update)
if self._uart_buffer:
keyboard.secondary_matrix_update = self._uart_buffer.pop(0)

51
kmk/transports/uart.py Normal file
View File

@@ -0,0 +1,51 @@
class UART:
def __init__(self, tx=None, rx=None, is_target=True, uart_interval=20):
self._debug_enabled = True
self._uart_connection = None
self._uart = None
self._uart_interval = uart_interval
self._is_target = is_target
@property
def in_waiting(self):
return self._uart.in_waiting
def read(self, n):
return self._uart.read(n)
def readinto(self, buf):
return self._uart.readinto(buf)
def powersave(enable: bool):
return
def write(self, buffer, update):
if self._uart is not None:
if not self._is_target or self.data_pin2:
update = self._serialize_update(update)
self._uart.write(self.uart_header)
self._uart.write(update)
self._uart.write(self._checksum(update))
def receive(self, keyboard):
if self._transport.in_waiting > 0 or self._uart_buffer:
if self._transport.in_waiting >= 60:
# This is a dirty hack to prevent crashes in unrealistic cases
# TODO See if this hack is needed with checksum, and if not, use
# that to fix it.
import microcontroller
microcontroller.reset()
while self._transport.in_waiting >= 4:
# Check the header
if self._transport.read(1) == self.uart_header:
update = self._transport.read(2)
# check the checksum
if self._checksum(update) == self._transport.read(1):
self._uart_buffer.append(self._deserialize_update(update))
if self._uart_buffer:
return self._uart_buffer.pop(0)
return None

View File

@@ -1,7 +1,7 @@
from supervisor import ticks_ms from supervisor import ticks_ms
def clamp(x, bottom=0, top=100): def clamp(x: int, bottom: int = 0, top: int = 100) -> int:
return min(max(bottom, x), top) return min(max(bottom, x), top)
@@ -13,18 +13,18 @@ class Debug:
debug = Debug(__name__) debug = Debug(__name__)
''' '''
def __init__(self, name=__name__): def __init__(self, name: str = __name__):
self.name = name self.name = name
def __call__(self, message): def __call__(self, message: str) -> None:
print(f'{ticks_ms()} {self.name}: {message}') print(f'{ticks_ms()} {self.name}: {message}')
@property @property
def enabled(self): def enabled(self) -> bool:
global _debug_enabled global _debug_enabled
return _debug_enabled return _debug_enabled
@enabled.setter @enabled.setter
def enabled(self, enabled): def enabled(self, enabled: bool):
global _debug_enabled global _debug_enabled
_debug_enabled = enabled _debug_enabled = enabled

View File

@@ -74,6 +74,7 @@ class KeyboardTest:
is_pressed = e[1] is_pressed = e[1]
self.pins[key_pos].value = is_pressed self.pins[key_pos].value = is_pressed
self.do_main_loop() self.do_main_loop()
self.keyboard._main_loop()
matching = True matching = True
for i in range(max(len(hid_reports), len(assert_reports))): for i in range(max(len(hid_reports), len(assert_reports))):

View File

@@ -192,7 +192,14 @@ class TestHoldTap(unittest.TestCase):
keyboard.test( keyboard.test(
'chained 4', 'chained 4',
[(1, True), (3, True), (0, True), (3, False), (1, False), (0, False)], [(1, True), (3, True), (0, True), (3, False), (1, False), (0, False)],
[{KC.LCTL}, {KC.LCTL, KC.N3, KC.N0}, {KC.LCTL, KC.N0}, {KC.N0}, {}], [
{KC.LCTL},
{KC.LCTL, KC.N3},
{KC.LCTL, KC.N0, KC.N3},
{KC.LCTL, KC.N0},
{KC.N0},
{},
],
) )
keyboard.test( keyboard.test(
@@ -293,25 +300,25 @@ class TestHoldTap(unittest.TestCase):
keyboard.test( keyboard.test(
'OS interrupt within tap time', 'OS interrupt within tap time',
[(4, True), (4, False), t_within, (3, True), (3, False)], [(4, True), (4, False), t_within, (3, True), (3, False)],
[{KC.E}, {KC.D, KC.E}, {}], [{KC.E}, {KC.D, KC.E}, {KC.E}, {}],
) )
keyboard.test( keyboard.test(
'OS interrupt, multiple within tap time', 'OS interrupt, multiple within tap time',
[(4, True), (4, False), (3, True), (3, False), (2, True), (2, False)], [(4, True), (4, False), (3, True), (3, False), (2, True), (2, False)],
[{KC.E}, {KC.D, KC.E}, {}, {KC.C}, {}], [{KC.E}, {KC.D, KC.E}, {KC.E}, {}, {KC.C}, {}],
) )
keyboard.test( keyboard.test(
'OS interrupt, multiple interleaved', 'OS interrupt, multiple interleaved',
[(4, True), (4, False), (3, True), (2, True), (2, False), (3, False)], [(4, True), (4, False), (3, True), (2, True), (2, False), (3, False)],
[{KC.E}, {KC.D, KC.E}, {KC.C, KC.D}, {KC.D}, {}], [{KC.E}, {KC.D, KC.E}, {KC.D}, {KC.C, KC.D}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'OS interrupt, multiple interleaved', 'OS interrupt, multiple interleaved',
[(4, True), (4, False), (3, True), (2, True), (3, False), (2, False)], [(4, True), (4, False), (3, True), (2, True), (3, False), (2, False)],
[{KC.E}, {KC.D, KC.E}, {KC.C, KC.D}, {KC.C}, {}], [{KC.E}, {KC.D, KC.E}, {KC.D}, {KC.C, KC.D}, {KC.C}, {}],
) )
keyboard.test( keyboard.test(

View File

@@ -71,7 +71,7 @@ class TestTapDance(unittest.TestCase):
keyboard.test( keyboard.test(
'Tap x1 interrupted', 'Tap x1 interrupted',
[(0, True), (0, False), (4, True), (4, False)], [(0, True), (0, False), (4, True), (4, False)],
[{KC.N0}, {KC.N4}, {}], [{KC.N0}, {}, {KC.N4}, {}],
) )
keyboard.test( keyboard.test(