Compare commits

..

1 Commits

Author SHA1 Message Date
xs5871
20bcfcdbb9
Add boilerplate method for board configuration at boot time 2023-03-06 21:08:06 +00:00
36 changed files with 424 additions and 1145 deletions

View File

@ -1,27 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] Title"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
* Setup and configuration of the affected parts of the firmware (avoid copy-pasting the entire configuration if possible)
* Setup and configuration of peripherals
* Input: keys pressed, ...
(Choose which are applicable.)
**Expected behavior**
A clear and concise description of what you expected to happen.
**Debug output**
If applicable, add debug output from the serial console to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Enhancement] Title"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -9,15 +9,10 @@ KMK is a feature-rich and beginner-friendly firmware for computer keyboards
written and configured in written and configured in
[CircuitPython](https://github.com/adafruit/circuitpython). [CircuitPython](https://github.com/adafruit/circuitpython).
## Support
For asynchronous support and chatter about KMK, [join our Zulip For asynchronous support and chatter about KMK, [join our Zulip
community](https://kmkfw.zulipchat.com)! community](https://kmkfw.zulipchat.com)! In particular, swing by the Zulip chat
*before* opening a GitHub Issue about configuration, documentation, etc.
If you ask for help in chat or open a bug report, if possible concerns.
make sure your copy of KMK is up-to-date.
In particular, swing by the Zulip chat *before* opening a GitHub Issue about
configuration, documentation, etc. concerns.
> The former Matrix and Discord rooms once linked to in this README are no > The former Matrix and Discord rooms once linked to in this README are no
> longer officially supported, please do not use them! > longer officially supported, please do not use them!

View File

@ -20,6 +20,8 @@ encoder_handler.pins = ((board.D1, board.D2, board.D0),)
encoder_handler.map = (((KC.VOLD, KC.VOLU, KC.MUTE),),) encoder_handler.map = (((KC.VOLD, KC.VOLU, KC.MUTE),),)
knob.modules.append(encoder_handler) knob.modules.append(encoder_handler)
print('ANAVI Knob 1')
rgb_ext = RGB( rgb_ext = RGB(
pixel_pin=board.NEOPIXEL, pixel_pin=board.NEOPIXEL,
num_pixels=1, num_pixels=1,

View File

@ -1,98 +0,0 @@
## Combo Layers
Combo Layers is when you hold down 2 or more KC.MO() or KC.LM() keys at a time, and it goes to a defined layer.
By default combo layers is not activated. You can activate combo layers by adding this to your `main.py` file.
The combolayers NEEDS to be above the `keyboard.modules.append(Layers(combolayers))`
```python
combo_layers = {
(1, 2): 3,
}
keyboard.modules.append(Layers(combo_layers))
```
In the above code, when layer 1 and 2 are held, layer 3 will activate. If you release 1 or 2 it will go to whatever key is still being held, if both are released it goes to the default (0) layer.
You should also notice that if you already have the layers Module activated, you can just add combolayers into `(Layers())`
You can add more, and even add more than 2 layers at a time.
```python
combo_layers = {
(1, 2): 3,
(1, 2, 3): 4,
}
```
## Limitations
There can only be one combo layer active at a time and for overlapping matches
the first matching combo in `combo_layers` takes precedence.
Example:
```python
layers = Layers()
layers.combo_layers = {
(1, 2, 3): 8,
(1, 2): 9,
}
keyboard.modules.append(Layers(combo_layers))
```
* If you activate layers 1 then 2, your active layer will be layer number 9.
* If you activate layers 1 then 2, then 3, your active layer will be layer
number 3 (because the layer combo `(1,2)` has been activated, but layer 3
stacks on top).
* deactivate 1: you're on layer 3
* deactivate 2: you're on layer 3
* deactivate 3: you're on layer 8
* If you activate layers 3 then 1, then 2, your active layer will be layer
number 8. Deativate layer
* deactivate any of 1/2/3: you're on layer 0
## Fully Working Example code
Below is an example of a fully working keypad that uses combo layers.
```python
print("Starting")
import board
from kmk.kmk_keyboard import KMKKeyboard
from kmk.keys import KC
combo_layers = {
(1, 2): 3,
keyboard.modules.append(Layers(combo_layers))
keyboard = KMKKeyboard()
keyboard.keymap = [
[ #Default
KC.A, KC.B KC.C KC.D,
KC.E, KC.F KC.G KC.H,
KC.MO(1), KC.J, KC.K, KC.MO(2),
],
[ #Layer 1
KC.N1, KC.N2, KC.N3, KC.N4,
KC.N5, KC.N6, KC.N7, KC.8,
KC.MO(1), KC.N9, KC.N0, KC.MO(2),
],
[ #Layer 2
KC.EXLM, KC.AT, KC.HASH, KC.DLR,
KC.PERC, KC.CIRC, KC.AMPR, KC.ASTR,
KC.MO(1), KC.LPRN, KC.RPRN, KC.MO(2),
],
[ #Layer 3
KC.F1, KC.F2, KC.F3, KC.F4,
KC.F5, KC.F6, KC.F7, KC.F8,
KC.MO(1) KC.F9, KC.F10, KC.MO(2)
]
]
if __name__ == '__main__':
keyboard.go()
```

View File

@ -188,7 +188,6 @@
| `KC.RESET` | Restarts the keyboard | | `KC.RESET` | Restarts the keyboard |
| `KC.RELOAD`, `KC.RLD` | Reloads the keyboard software, preserving any serial connections | | `KC.RELOAD`, `KC.RLD` | Reloads the keyboard software, preserving any serial connections |
| `KC.DEBUG` | Toggle `debug_enabled`, which enables log spew to serial console | | `KC.DEBUG` | Toggle `debug_enabled`, which enables log spew to serial console |
| `KC.ANY` | Any key between `A and `/` |
| `KC.GESC` | Escape when tapped, <code>&#96;</code> when pressed with Shift or GUI | | `KC.GESC` | Escape when tapped, <code>&#96;</code> when pressed with Shift or GUI |
| `KC.BKDL` | Backspace when tapped, Delete when pressed with GUI | | `KC.BKDL` | Backspace when tapped, Delete when pressed with GUI |
| `KC.UC_MODE_NOOP` | Sets UnicodeMode to NOOP | | `KC.UC_MODE_NOOP` | Sets UnicodeMode to NOOP |

View File

@ -33,11 +33,6 @@ Some helpful guidelines to keep in mind as you design your layers:
- Only reference higher-numbered layers from a given layer - Only reference higher-numbered layers from a given layer
- Leave keys as `KC.TRNS` in higher layers when they would overlap with a layer-switch - Leave keys as `KC.TRNS` in higher layers when they would overlap with a layer-switch
## Using Combo Layers
Combo Layers allow you to activate a corresponding layer based on the activation of 2 or more other layers.
The advantage of using Combo layers is that when you release one of the layer keys, it stays on whatever layer is still being held.
See [combo layers documentation](combolayers.md) for more information on it's function and to see examples.
### Using Multiple Base Layers ### Using Multiple Base Layers
In some cases, you may want to have more than one base layer (for instance you want to use In some cases, you may want to have more than one base layer (for instance you want to use
both QWERTY and Dvorak layouts, or you have a custom gamepad that can switch between both QWERTY and Dvorak layouts, or you have a custom gamepad that can switch between
@ -45,7 +40,6 @@ different games). In this case, best practice is to have these layers be the low
defined first in your keymap. These layers are mutually-exclusive, so treat changing default defined first in your keymap. These layers are mutually-exclusive, so treat changing default
layers with `KC.DF()` the same way that you would treat using `KC.TO()` layers with `KC.DF()` the same way that you would treat using `KC.TO()`
## Example Code ## Example Code
For our example, let's take a simple 3x3 macropad with two layers as follows: For our example, let's take a simple 3x3 macropad with two layers as follows:

View File

@ -7,6 +7,3 @@ If you ask for help in chat or open a bug report, if possible
make sure your copy of KMK is up-to-date. make sure your copy of KMK is up-to-date.
In particular, swing by the Zulip chat *before* opening a GitHub Issue about In particular, swing by the Zulip chat *before* opening a GitHub Issue about
configuration, documentation, etc. concerns. configuration, documentation, etc. concerns.
> The former Matrix and Discord rooms once linked to in this README are no
> longer officially supported, please do not use them!

60
kmk/bootcfg.py Normal file
View File

@ -0,0 +1,60 @@
import digitalio
import microcontroller
def bootcfg(
sense,
source=None,
no_cdc=True,
no_hid=False,
no_midi=True,
no_storage=True,
usb_id=None,
):
if isinstance(sense, microcontroller.Pin):
sense = digitalio.DigitalInOut(sense)
sense.direction = digitalio.Direction.INPUT
sense.pull = digitalio.Pull.UP
if isinstance(source, microcontroller.Pin):
source = digitalio.DigitalInOut(source)
source.direction = digitalio.Direction.OUTPUT
source.value = False
else:
return False
# sense pulled low -> skip boot configuration
if not sense.value:
return False
if no_cdc:
import usb_cdc
usb_cdc.disable()
if no_hid:
import usb_hid
usb_hid.disable()
if no_midi:
import usb_midi
usb_midi.disable()
if isinstance(usb_id, tuple):
import supervisor
if hasattr(supervisor, 'set_usb_identification'):
supervisor.set_usb_identification(*usb_id)
# The no_storage entry is intentionally evaluated last to ensure the drive
# is mountable and rescueable, in case any of the previous code throws an
# exception.
if no_storage:
import storage
storage.disable_usb_drive()
return True

View File

@ -49,6 +49,3 @@ class Extension:
def on_powersave_disable(self, keyboard): def on_powersave_disable(self, keyboard):
raise NotImplementedError raise NotImplementedError
def deinit(self, keyboard):
pass

View File

@ -1,272 +0,0 @@
import busio
from supervisor import ticks_ms
import adafruit_displayio_ssd1306
import displayio
import terminalio
from adafruit_display_text import label
from kmk.extensions import Extension
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key
from kmk.kmktime import PeriodicTimer, ticks_diff
from kmk.modules.split import Split, SplitSide
from kmk.utils import clamp
displayio.release_displays()
class TextEntry:
def __init__(
self,
text='',
x=0,
y=0,
x_anchor='L',
y_anchor='T',
direction='LTR',
line_spacing=0.75,
inverted=False,
layer=None,
side=None,
):
self.text = text
self.direction = direction
self.line_spacing = line_spacing
self.inverted = inverted
self.layer = layer
self.color = 0xFFFFFF
self.background_color = 0x000000
self.x_anchor = 0.0
self.y_anchor = 0.0
if x_anchor == 'L':
self.x_anchor = 0.0
x = x + 1
if x_anchor == 'M':
self.x_anchor = 0.5
if x_anchor == 'R':
self.x_anchor = 1.0
if y_anchor == 'T':
self.y_anchor = 0.0
if y_anchor == 'M':
self.y_anchor = 0.5
if y_anchor == 'B':
self.y_anchor = 1.0
self.anchor_point = (self.x_anchor, self.y_anchor)
self.anchored_position = (x, y)
if inverted:
self.color = 0x000000
self.background_color = 0xFFFFFF
self.side = side
if side == 'L':
self.side = SplitSide.LEFT
if side == 'R':
self.side = SplitSide.RIGHT
class ImageEntry:
def __init__(self, x=0, y=0, image='', layer=None, side=None):
self.x = x
self.y = y
self.image = displayio.OnDiskBitmap(image)
self.layer = layer
self.side = side
if side == 'L':
self.side = SplitSide.LEFT
if side == 'R':
self.side = SplitSide.RIGHT
class Oled(Extension):
def __init__(
self,
i2c=None,
sda=None,
scl=None,
device_address=0x3C,
entries=[],
width=128,
height=32,
flip: bool = False,
flip_left: bool = False,
flip_right: bool = False,
brightness=0.8,
brightness_step=0.1,
dim_time=20,
dim_target=0.1,
off_time=60,
powersave_dim_time=10,
powersave_dim_target=0.1,
powersave_off_time=30,
):
self.device_address = device_address
self.flip = flip
self.flip_left = flip_left
self.flip_right = flip_right
self.entries = entries
self.width = width
self.height = height
self.prev_layer = None
self.brightness = brightness
self.brightness_step = brightness_step
self.timer_start = ticks_ms()
self.powersave = False
self.dim_time_ms = dim_time * 1000
self.dim_target = dim_target
self.off_time_ms = off_time * 1000
self.powersavedim_time_ms = powersave_dim_time * 1000
self.powersave_dim_target = powersave_dim_target
self.powersave_off_time_ms = powersave_off_time * 1000
self.dim_period = PeriodicTimer(50)
self.split_side = None
# i2c initialization
self.i2c = i2c
if self.i2c is None:
self.i2c = busio.I2C(scl, sda)
make_key(
names=('OLED_BRI',),
on_press=self.oled_brightness_increase,
on_release=handler_passthrough,
)
make_key(
names=('OLED_BRD',),
on_press=self.oled_brightness_decrease,
on_release=handler_passthrough,
)
def render(self, layer):
splash = displayio.Group()
for entry in self.entries:
if entry.layer != layer and entry.layer is not None:
continue
if isinstance(entry, TextEntry):
splash.append(
label.Label(
terminalio.FONT,
text=entry.text,
color=entry.color,
background_color=entry.background_color,
anchor_point=entry.anchor_point,
anchored_position=entry.anchored_position,
label_direction=entry.direction,
line_spacing=entry.line_spacing,
padding_left=1,
)
)
elif isinstance(entry, ImageEntry):
splash.append(
displayio.TileGrid(
entry.image,
pixel_shader=entry.image.pixel_shader,
x=entry.x,
y=entry.y,
)
)
self.display.show(splash)
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, keyboard):
for module in keyboard.modules:
if isinstance(module, Split):
self.split_side = module.split_side
if self.split_side == SplitSide.LEFT:
self.flip = self.flip_left
elif self.split_side == SplitSide.RIGHT:
self.flip = self.flip_right
for idx, entry in enumerate(self.entries):
if entry.side != self.split_side and entry.side is not None:
del self.entries[idx]
self.display = adafruit_displayio_ssd1306.SSD1306(
displayio.I2CDisplay(self.i2c, device_address=self.device_address),
width=self.width,
height=self.height,
rotation=180 if self.flip else 0,
brightness=self.brightness,
)
def before_matrix_scan(self, sandbox):
if self.dim_period.tick():
self.dim()
if sandbox.active_layers[0] != self.prev_layer:
self.prev_layer = sandbox.active_layers[0]
self.render(sandbox.active_layers[0])
def after_matrix_scan(self, sandbox):
if sandbox.matrix_update or sandbox.secondary_matrix_update:
self.timer_start = ticks_ms()
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
self.powersave = True
def on_powersave_disable(self, sandbox):
self.powersave = False
def deinit(self, sandbox):
displayio.release_displays()
self.i2c.deinit()
def oled_brightness_increase(self):
self.display.brightness = clamp(
self.display.brightness + self.brightness_step, 0, 1
)
self.brightness = self.display.brightness # Save current brightness
def oled_brightness_decrease(self):
self.display.brightness = clamp(
self.display.brightness - self.brightness_step, 0, 1
)
self.brightness = self.display.brightness # Save current brightness
def dim(self):
if self.powersave:
if (
self.powersave_off_time_ms
and ticks_diff(ticks_ms(), self.timer_start)
> self.powersave_off_time_ms
):
self.display.sleep()
elif (
self.powersave_dim_time_ms
and ticks_diff(ticks_ms(), self.timer_start)
> self.powersave_dim_time_ms
):
self.display.brightness = self.powersave_dim_target
else:
self.display.brightness = self.brightness
self.display.wake()
elif (
self.off_time_ms
and ticks_diff(ticks_ms(), self.timer_start) > self.off_time_ms
):
self.display.sleep()
elif (
self.dim_time_ms
and ticks_diff(ticks_ms(), self.timer_start) > self.dim_time_ms
):
self.display.brightness = self.dim_target
else:
self.display.brightness = self.brightness
self.display.wake()

View File

@ -4,7 +4,7 @@ from math import e, exp, pi, sin
from kmk.extensions import Extension 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.scheduler import create_task from kmk.kmktime import PeriodicTimer
from kmk.utils import Debug, clamp from kmk.utils import Debug, clamp
debug = Debug(__name__) debug = Debug(__name__)
@ -90,10 +90,10 @@ class RGB(Extension):
self, self,
pixel_pin, pixel_pin,
num_pixels=0, num_pixels=0,
rgb_order=(1, 0, 2), # GRB WS2812
val_limit=255, val_limit=255,
hue_default=0, hue_default=0,
sat_default=255, sat_default=255,
rgb_order=(1, 0, 2), # GRB WS2812
val_default=255, val_default=255,
hue_step=4, hue_step=4,
sat_step=13, sat_step=13,
@ -109,9 +109,32 @@ class RGB(Extension):
pixels=None, pixels=None,
refresh_rate=60, refresh_rate=60,
): ):
self.pixel_pin = pixel_pin if pixels is None:
import neopixel
pixels = neopixel.NeoPixel(
pixel_pin,
num_pixels,
pixel_order=rgb_order,
auto_write=not disable_auto_write,
)
self.pixels = pixels
self.num_pixels = num_pixels self.num_pixels = num_pixels
self.rgb_order = rgb_order
# PixelBuffer are already iterable, can't do the usual `try: iter(...)`
if issubclass(self.pixels.__class__, PixelBuf):
self.pixels = (self.pixels,)
if self.num_pixels == 0:
for pixels in self.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.hue_step = hue_step self.hue_step = hue_step
self.sat_step = sat_step self.sat_step = sat_step
self.val_step = val_step self.val_step = val_step
@ -130,11 +153,8 @@ class RGB(Extension):
self.reverse_animation = reverse_animation self.reverse_animation = reverse_animation
self.user_animation = user_animation self.user_animation = user_animation
self.disable_auto_write = disable_auto_write self.disable_auto_write = disable_auto_write
self.pixels = pixels
self.refresh_rate = refresh_rate self.refresh_rate = refresh_rate
self.rgbw = bool(len(rgb_order) == 4)
self._substep = 0 self._substep = 0
make_key( make_key(
@ -207,29 +227,7 @@ class RGB(Extension):
return return
def during_bootup(self, sandbox): def during_bootup(self, sandbox):
if self.pixels is None: self._timer = PeriodicTimer(1000 // self.refresh_rate)
import neopixel
self.pixels = neopixel.NeoPixel(
self.pixel_pin,
self.num_pixels,
pixel_order=self.rgb_order,
auto_write=not self.disable_auto_write,
)
# PixelBuffer are already iterable, can't do the usual `try: iter(...)`
if issubclass(self.pixels.__class__, PixelBuf):
self.pixels = (self.pixels,)
if self.num_pixels == 0:
for pixels in self.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._task = create_task(self.animate, period_ms=(1000 // self.refresh_rate))
def before_matrix_scan(self, sandbox): def before_matrix_scan(self, sandbox):
return return
@ -241,7 +239,7 @@ class RGB(Extension):
return return
def after_hid_send(self, sandbox): def after_hid_send(self, sandbox):
pass self.animate()
def on_powersave_enable(self, sandbox): def on_powersave_enable(self, sandbox):
return return
@ -249,10 +247,6 @@ class RGB(Extension):
def on_powersave_disable(self, sandbox): def on_powersave_disable(self, sandbox):
self._do_update() self._do_update()
def deinit(self, sandbox):
for pixel in self.pixels:
pixel.deinit()
def set_hsv(self, hue, sat, val, index): def set_hsv(self, hue, sat, val, index):
''' '''
Takes HSV values and displays it on a single LED/Neopixel Takes HSV values and displays it on a single LED/Neopixel
@ -435,7 +429,7 @@ class RGB(Extension):
if self.animation_mode is AnimationModes.STATIC_STANDBY: if self.animation_mode is AnimationModes.STATIC_STANDBY:
return return
if self.enable: if self.enable and self._timer.tick():
self._animation_step() self._animation_step()
if self.animation_mode == AnimationModes.BREATHING: if self.animation_mode == AnimationModes.BREATHING:
self.effect_breathing() self.effect_breathing()

View File

@ -137,10 +137,3 @@ def ble_disconnect(key, keyboard, *args, **kwargs):
keyboard._hid_helper.clear_bonds() keyboard._hid_helper.clear_bonds()
return keyboard return keyboard
def any_pressed(key, keyboard, *args, **kwargs):
from random import randint
key.code = randint(4, 56)
default_pressed(key, keyboard, *args, **kwargs)

View File

@ -370,7 +370,6 @@ def maybe_make_firmware_key(candidate: str) -> Optional[Key]:
((('HID_SWITCH', 'HID'), handlers.hid_switch)), ((('HID_SWITCH', 'HID'), handlers.hid_switch)),
((('RELOAD', 'RLD'), handlers.reload)), ((('RELOAD', 'RLD'), handlers.reload)),
((('RESET',), handlers.reset)), ((('RESET',), handlers.reset)),
((('ANY',), handlers.any_pressed)),
) )
for names, handler in keys: for names, handler in keys:
@ -476,9 +475,7 @@ class KeyAttrDict:
break break
if not maybe_key: if not maybe_key:
if debug.enabled: raise ValueError(f'Invalid key: {name}')
debug(f'Invalid key: {name}')
return KC.NO
if debug.enabled: if debug.enabled:
debug(f'{name}: {maybe_key}') debug(f'{name}: {maybe_key}')

View File

@ -1,33 +1,28 @@
try: try:
from typing import Callable, Optional from typing import Callable, Optional, Tuple
except ImportError: except ImportError:
pass pass
from supervisor import ticks_ms
from collections import namedtuple from collections import namedtuple
from keypad import Event as KeyEvent 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
from kmk.kmktime import ticks_add, ticks_diff
from kmk.modules import Module from kmk.modules import Module
from kmk.scanners.keypad import MatrixScanner from kmk.scanners.keypad import MatrixScanner
from kmk.scheduler import Task, cancel_task, create_task, get_due_task
from kmk.utils import Debug from kmk.utils import Debug
debug = Debug('kmk.keyboard') debug = Debug(__name__)
KeyBufferFrame = namedtuple( KeyBufferFrame = namedtuple(
'KeyBufferFrame', ('key', 'is_pressed', 'int_coord', 'index') 'KeyBufferFrame', ('key', 'is_pressed', 'int_coord', 'index')
) )
def debug_error(module, message: str, error: Exception):
if debug.enabled:
debug(
message, ': ', error.__class__.__name__, ': ', error, name=module.__module__
)
class Sandbox: class Sandbox:
matrix_update = None matrix_update = None
secondary_matrix_update = None secondary_matrix_update = None
@ -64,6 +59,7 @@ class KMKKeyboard:
matrix_update = None matrix_update = None
secondary_matrix_update = None secondary_matrix_update = None
matrix_update_queue = [] matrix_update_queue = []
state_changed = False
_trigger_powersave_enable = False _trigger_powersave_enable = False
_trigger_powersave_disable = False _trigger_powersave_disable = False
i2c_deinit_count = 0 i2c_deinit_count = 0
@ -79,24 +75,47 @@ class KMKKeyboard:
_timeouts = {} _timeouts = {}
# on some M4 setups (such as klardotsh/klarank_feather_m4, CircuitPython
# 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
# tight on RAM so....
def __repr__(self) -> str: def __repr__(self) -> str:
return self.__class__.__name__ return ''.join(
[
'KMKKeyboard(\n',
f' debug_enabled={self.debug_enabled}, ',
f'diode_orientation={self.diode_orientation}, ',
f'matrix={self.matrix},\n',
f' unicode_mode={self.unicode_mode}, ',
f'_hid_helper={self._hid_helper},\n',
f' keys_pressed={self.keys_pressed},\n',
f' axes={self.axes},\n',
f' _coordkeys_pressed={self._coordkeys_pressed},\n',
f' hid_pending={self.hid_pending}, ',
f'active_layers={self.active_layers}, ',
f'_timeouts={self._timeouts}\n',
')',
]
)
def _print_debug_cycle(self, init: bool = False) -> None:
if debug.enabled:
debug(f'coordkeys_pressed={self._coordkeys_pressed}')
debug(f'keys_pressed={self.keys_pressed}')
def _send_hid(self) -> None: def _send_hid(self) -> None:
if not self._hid_send_enabled: if not self._hid_send_enabled:
return return
if debug.enabled: if self.axes and debug.enabled:
if self.keys_pressed: debug(f'axes={self.axes}')
debug('keys_pressed=', self.keys_pressed)
if self.axes:
debug('axes=', self.axes)
self._hid_helper.create_report(self.keys_pressed, self.axes) self._hid_helper.create_report(self.keys_pressed, self.axes)
try: try:
self._hid_helper.send() self._hid_helper.send()
except Exception as err: except KeyError as e:
debug_error(self._hid_helper, 'send', err) if debug.enabled:
debug(f'HidNotFound(HIDReportType={e})')
self.hid_pending = False self.hid_pending = False
@ -106,32 +125,35 @@ class KMKKeyboard:
def _handle_matrix_report(self, kevent: KeyEvent) -> None: def _handle_matrix_report(self, kevent: KeyEvent) -> None:
if kevent is not None: if kevent is not None:
self._on_matrix_changed(kevent) self._on_matrix_changed(kevent)
self.state_changed = True
def _find_key_in_map(self, int_coord: int) -> Key: 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:
if debug.enabled: if debug.enabled:
debug('no such int_coord: ', int_coord) debug(f'CoordMappingNotFound(ic={int_coord})')
return None return None
for layer in self.active_layers: for layer in self.active_layers:
try: try:
key = self.keymap[layer][idx] layer_key = self.keymap[layer][idx]
except IndexError: except IndexError:
key = None layer_key = None
if debug.enabled: if debug.enabled:
debug('keymap IndexError: idx=', idx, ' layer=', layer) debug(f'KeymapIndexError(idx={idx}, layer={layer})')
if not key or key == KC.TRNS: if not layer_key or layer_key == KC.TRNS:
continue continue
return key return layer_key
def _on_matrix_changed(self, kevent: KeyEvent) -> None: 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:
debug(f'MatrixChange(ic={int_coord}, pressed={is_pressed})')
key = None key = None
if not is_pressed: if not is_pressed:
@ -139,16 +161,18 @@ class KMKKeyboard:
key = self._coordkeys_pressed[int_coord] key = self._coordkeys_pressed[int_coord]
except KeyError: except KeyError:
if debug.enabled: if debug.enabled:
debug('release w/o press: ', int_coord) debug(f'KeyNotPressed(ic={int_coord})')
if key is None: if key is None:
key = self._find_key_in_map(int_coord) key = self._find_key_in_map(int_coord)
if key is None: if key is None:
return if debug.enabled:
debug(f'MatrixUndefinedCoordinate(ic={int_coord})')
return self
if debug.enabled: if debug.enabled:
debug(kevent, ': ', key) debug(f'KeyResolution(key={key})')
self.pre_process_key(key, is_pressed, int_coord) self.pre_process_key(key, is_pressed, int_coord)
@ -173,7 +197,7 @@ class KMKKeyboard:
key = ksf.key key = ksf.key
# Handle any unaccounted-for layer shifts by looking up the key resolution again. # Handle any unaccounted-for layer shifts by looking up the key resolution again.
if ksf.int_coord is not None: if ksf.int_coord in self._coordkeys_pressed.keys():
key = self._find_key_in_map(ksf.int_coord) key = self._find_key_in_map(ksf.int_coord)
# Resume the processing of the key event and update the HID report # Resume the processing of the key event and update the HID report
@ -214,7 +238,8 @@ class KMKKeyboard:
if key is None: if key is None:
break break
except Exception as err: except Exception as err:
debug_error(module, 'process_key', err) if debug.enabled:
debug(f'Error in {module}.process_key: {err}')
if int_coord is not None: if int_coord is not None:
if is_pressed: if is_pressed:
@ -224,20 +249,18 @@ class KMKKeyboard:
del self._coordkeys_pressed[int_coord] del self._coordkeys_pressed[int_coord]
except KeyError: except KeyError:
if debug.enabled: if debug.enabled:
debug('release w/o press:', int_coord) debug(f'ReleaseKeyError(ic={int_coord})')
if debug.enabled:
debug('coordkeys_pressed=', self._coordkeys_pressed)
if key: if key:
self.process_key(key, is_pressed, int_coord) self.process_key(key, is_pressed, int_coord)
def process_key( def process_key(
self, key: Key, is_pressed: bool, int_coord: Optional[int] = None self, key: Key, is_pressed: bool, coord_int: Optional[int] = None
) -> None: ) -> None:
if is_pressed: if is_pressed:
key.on_press(self, int_coord) key.on_press(self, coord_int)
else: else:
key.on_release(self, int_coord) key.on_release(self, coord_int)
def resume_process_key( def resume_process_key(
self, self,
@ -245,9 +268,8 @@ class KMKKeyboard:
key: Key, key: Key,
is_pressed: bool, is_pressed: bool,
int_coord: Optional[int] = None, int_coord: Optional[int] = None,
reprocess: Optional[bool] = False,
) -> None: ) -> None:
index = self.modules.index(module) + (0 if reprocess else 1) index = self.modules.index(module) + 1
ksf = KeyBufferFrame( ksf = KeyBufferFrame(
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
) )
@ -264,17 +286,60 @@ class KMKKeyboard:
def tap_key(self, keycode: Key) -> None: 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(0, lambda: self.remove_key(keycode)) self.set_timeout(False, lambda: self.remove_key(keycode))
def set_timeout(self, after_ticks: int, callback: Callable[[None], None]) -> [Task]: def set_timeout(
return create_task(callback, after_ms=after_ticks) self, after_ticks: int, callback: Callable[[None], None]
) -> Tuple[int, int]:
# We allow passing False as an implicit "run this on the next process timeouts cycle"
if after_ticks is False:
after_ticks = 0
if after_ticks == 0 and self._processing_timeouts:
after_ticks += 1
timeout_key = ticks_add(ticks_ms(), after_ticks)
if timeout_key not in self._timeouts:
self._timeouts[timeout_key] = []
idx = len(self._timeouts[timeout_key])
self._timeouts[timeout_key].append(callback)
return (timeout_key, idx)
def cancel_timeout(self, timeout_key: int) -> None: def cancel_timeout(self, timeout_key: int) -> None:
cancel_task(timeout_key) try:
self._timeouts[timeout_key[0]][timeout_key[1]] = None
except (KeyError, IndexError):
if debug.enabled:
debug(f'no such timeout: {timeout_key}')
def _process_timeouts(self) -> None: def _process_timeouts(self) -> None:
for task in get_due_task(): if not self._timeouts:
task() return
# Copy timeout keys to a temporary list to allow sorting.
# Prevent net timeouts set during handling from running on the current
# cycle by setting a flag `_processing_timeouts`.
current_time = ticks_ms()
timeout_keys = []
self._processing_timeouts = True
for k in self._timeouts.keys():
if ticks_diff(k, current_time) <= 0:
timeout_keys.append(k)
if timeout_keys and debug.enabled:
debug('processing timeouts')
for k in sorted(timeout_keys):
for callback in self._timeouts[k]:
if callback:
callback()
del self._timeouts[k]
self._processing_timeouts = False
def _init_sanity_check(self) -> None: def _init_sanity_check(self) -> None:
''' '''
@ -321,15 +386,14 @@ 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
if debug.enabled:
debug('hid=', self._hid_helper)
def _deinit_hid(self) -> None: def _deinit_hid(self) -> None:
self._hid_helper.clear_all() self._hid_helper.clear_all()
self._hid_helper.send() self._hid_helper.send()
def _init_matrix(self) -> None: def _init_matrix(self) -> None:
if self.matrix is None: if self.matrix is None:
if debug.enabled:
debug('Initialising default matrix scanner.')
self.matrix = MatrixScanner( self.matrix = MatrixScanner(
column_pins=self.col_pins, column_pins=self.col_pins,
row_pins=self.row_pins, row_pins=self.row_pins,
@ -345,124 +409,96 @@ class KMKKeyboard:
except TypeError: except TypeError:
self.matrix = (self.matrix,) self.matrix = (self.matrix,)
if debug.enabled:
debug('matrix=', [_.__class__.__name__ for _ in self.matrix])
def during_bootup(self) -> None:
# Modules and extensions that fail `during_bootup` get removed from
# their respective lists. This serves as a self-check mechanism; any
# modules or extensions that initialize peripherals or data structures
# should do that in `during_bootup`.
for idx, module in enumerate(self.modules):
try:
module.during_bootup(self)
except Exception as err:
debug_error(module, 'during_bootup', err)
del self.modules[idx]
if debug.enabled:
debug('modules=', [_.__class__.__name__ for _ in self.modules])
for idx, ext in enumerate(self.extensions):
try:
ext.during_bootup(self)
except Exception as err:
debug_error(ext, 'during_bootup', err)
del self.extensions[idx]
if debug.enabled:
debug('extensions=', [_.__class__.__name__ for _ in self.extensions])
def before_matrix_scan(self) -> None: def before_matrix_scan(self) -> None:
for module in self.modules: for module in self.modules:
try: try:
module.before_matrix_scan(self) module.before_matrix_scan(self)
except Exception as err: except Exception as err:
debug_error(module, 'before_matrix_scan', err) if debug.enabled:
debug(f'Error in {module}.before_matrix_scan: {err}')
for ext in self.extensions: for ext in self.extensions:
try: try:
ext.before_matrix_scan(self.sandbox) ext.before_matrix_scan(self.sandbox)
except Exception as err: except Exception as err:
debug_error(ext, 'before_matrix_scan', err) if debug.enabled:
debug(f'Error in {ext}.before_matrix_scan: {err}')
def after_matrix_scan(self) -> None: 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)
except Exception as err: except Exception as err:
debug_error(module, 'after_matrix_scan', err) if debug.enabled:
debug(f'Error in {module}.after_matrix_scan: {err}')
for ext in self.extensions: for ext in self.extensions:
try: try:
ext.after_matrix_scan(self.sandbox) ext.after_matrix_scan(self.sandbox)
except Exception as err: except Exception as err:
debug_error(ext, 'after_matrix_scan', err) if debug.enabled:
debug(f'Error in {ext}.after_matrix_scan: {err}')
def before_hid_send(self) -> None: 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)
except Exception as err: except Exception as err:
debug_error(module, 'before_hid_send', err) if debug.enabled:
debug(f'Error in {module}.before_hid_send: {err}')
for ext in self.extensions: for ext in self.extensions:
try: try:
ext.before_hid_send(self.sandbox) ext.before_hid_send(self.sandbox)
except Exception as err: except Exception as err:
debug_error(ext, 'before_hid_send', err) if debug.enabled:
debug(
f'Error in {ext}.before_hid_send: {err}',
)
def after_hid_send(self) -> None: 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)
except Exception as err: except Exception as err:
debug_error(module, 'after_hid_send', err) if debug.enabled:
debug(f'Error in {module}.after_hid_send: {err}')
for ext in self.extensions: for ext in self.extensions:
try: try:
ext.after_hid_send(self.sandbox) ext.after_hid_send(self.sandbox)
except Exception as err: except Exception as err:
debug_error(ext, 'after_hid_send', err) if debug.enabled:
debug(f'Error in {ext}.after_hid_send: {err}')
def powersave_enable(self) -> None: 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)
except Exception as err: except Exception as err:
debug_error(module, 'powersave_enable', err) if debug.enabled:
debug(f'Error in {module}.on_powersave: {err}')
for ext in self.extensions: for ext in self.extensions:
try: try:
ext.on_powersave_enable(self.sandbox) ext.on_powersave_enable(self.sandbox)
except Exception as err: except Exception as err:
debug_error(ext, 'powersave_enable', err) if debug.enabled:
debug(f'Error in {ext}.powersave_enable: {err}')
def powersave_disable(self) -> None: 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)
except Exception as err: except Exception as err:
debug_error(module, 'powersave_disable', err) if debug.enabled:
debug(f'Error in {module}.powersave_disable: {err}')
for ext in self.extensions: for ext in self.extensions:
try: try:
ext.on_powersave_disable(self.sandbox) ext.on_powersave_disable(self.sandbox)
except Exception as err: except Exception as err:
debug_error(ext, 'powersave_disable', err) if debug.enabled:
debug(f'Error in {ext}.powersave_disable: {err}')
def deinit(self) -> None:
for module in self.modules:
try:
module.deinit(self)
except Exception as err:
debug_error(module, 'deinit', err)
for ext in self.extensions:
try:
ext.deinit(self.sandbox)
except Exception as err:
debug_error(ext, 'deinit', err)
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs) -> None: 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)
@ -472,7 +508,6 @@ class KMKKeyboard:
finally: finally:
debug('Unexpected error: cleaning up') debug('Unexpected error: cleaning up')
self._deinit_hid() self._deinit_hid()
self.deinit()
def _init( def _init(
self, self,
@ -484,22 +519,29 @@ class KMKKeyboard:
self.hid_type = hid_type self.hid_type = hid_type
self.secondary_hid_type = secondary_hid_type self.secondary_hid_type = secondary_hid_type
if debug.enabled: self._init_sanity_check()
debug('Initialising ', self)
debug('unicode_mode=', self.unicode_mode)
self._init_hid() self._init_hid()
self._init_matrix() self._init_matrix()
self._init_coord_mapping() self._init_coord_mapping()
self.during_bootup()
for module in self.modules:
try:
module.during_bootup(self)
except Exception as err:
if debug.enabled:
debug(f'Failed to load module {module}: {err}')
for ext in self.extensions:
try:
ext.during_bootup(self)
except Exception as err:
if debug.enabled:
debug(f'Failed to load extensions {module}: {err}')
if debug.enabled: if debug.enabled:
import gc debug(f'init: {self}')
gc.collect()
debug('mem_info used:', gc.mem_alloc(), ' free:', gc.mem_free())
def _main_loop(self) -> None: def _main_loop(self) -> None:
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()
@ -537,6 +579,7 @@ class KMKKeyboard:
if self.hid_pending: if self.hid_pending:
self._send_hid() self._send_hid()
self.state_changed = True
self.after_hid_send() self.after_hid_send()
@ -545,3 +588,6 @@ class KMKKeyboard:
if self._trigger_powersave_disable: if self._trigger_powersave_disable:
self.powersave_disable() self.powersave_disable()
if self.state_changed:
self._print_debug_cycle()

View File

@ -41,6 +41,3 @@ class Module:
def on_powersave_disable(self, keyboard): def on_powersave_disable(self, keyboard):
raise NotImplementedError raise NotImplementedError
def deinit(self, keyboard):
pass

View File

@ -8,9 +8,6 @@ import kmk.handlers.stock as handlers
from kmk.keys import Key, make_key from kmk.keys import Key, make_key
from kmk.kmk_keyboard import KMKKeyboard from kmk.kmk_keyboard import KMKKeyboard
from kmk.modules import Module from kmk.modules import Module
from kmk.utils import Debug
debug = Debug(__name__)
class _ComboState: class _ComboState:
@ -217,7 +214,7 @@ class Combos(Module):
combo.insert(key, int_coord) combo.insert(key, int_coord)
combo._state = _ComboState.MATCHING combo._state = _ComboState.MATCHING
key = None key = combo.result
break break
else: else:
@ -304,14 +301,10 @@ class Combos(Module):
keyboard.resume_process_key(self, key, is_pressed, int_coord) keyboard.resume_process_key(self, key, is_pressed, int_coord)
def activate(self, keyboard, combo): def activate(self, keyboard, combo):
if debug.enabled:
debug('activate', combo)
combo.result.on_press(keyboard) combo.result.on_press(keyboard)
combo._state = _ComboState.ACTIVE combo._state = _ComboState.ACTIVE
def deactivate(self, keyboard, combo): def deactivate(self, keyboard, combo):
if debug.enabled:
debug('deactivate', combo)
combo.result.on_release(keyboard) combo.result.on_release(keyboard)
combo._state = _ComboState.IDLE combo._state = _ComboState.IDLE

View File

@ -54,7 +54,7 @@ class HoldTap(Module):
def __init__(self): def __init__(self):
self.key_buffer = [] self.key_buffer = []
self.key_states = {} self.key_states = {}
if KC.get('HT') == KC.NO: if not KC.get('HT'):
make_argumented_key( make_argumented_key(
validator=HoldTapKeyMeta, validator=HoldTapKeyMeta,
names=('HT',), names=('HT',),
@ -83,11 +83,6 @@ class HoldTap(Module):
if state.activated != ActivationType.PRESSED: if state.activated != ActivationType.PRESSED:
continue continue
# holdtap isn't interruptable, resolves on ht_release or timeout.
if not key.meta.tap_interrupted and not key.meta.prefer_hold:
append_buffer = True
continue
# holdtap is interrupted by another key event. # holdtap is interrupted by another key event.
if (is_pressed and not key.meta.tap_interrupted) or ( if (is_pressed and not key.meta.tap_interrupted) or (
not is_pressed and key.meta.tap_interrupted and self.key_buffer not is_pressed and key.meta.tap_interrupted and self.key_buffer
@ -98,12 +93,15 @@ 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
) )
append_buffer = True
send_buffer = True send_buffer = True
# 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 key.meta.tap_interrupted and is_pressed: if (
key.meta.tap_interrupted
and is_pressed
and not isinstance(current_key.meta, HoldTapKeyMeta)
):
append_buffer = True append_buffer = True
# apply changes with 'side-effects' on key_states or the loop behaviour # apply changes with 'side-effects' on key_states or the loop behaviour
@ -112,8 +110,10 @@ class HoldTap(Module):
self.key_buffer.append((int_coord, current_key, is_pressed)) self.key_buffer.append((int_coord, current_key, is_pressed))
current_key = None current_key = None
if 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
@ -221,11 +221,8 @@ class HoldTap(Module):
if not self.key_buffer: if not self.key_buffer:
return return
reprocess = False
for (int_coord, key, is_pressed) in self.key_buffer: for (int_coord, key, is_pressed) in self.key_buffer:
keyboard.resume_process_key(self, key, is_pressed, int_coord, reprocess) keyboard.resume_process_key(self, key, is_pressed, int_coord)
if isinstance(key.meta, HoldTapKeyMeta):
reprocess = True
self.key_buffer.clear() self.key_buffer.clear()

View File

@ -36,15 +36,9 @@ class LayerKeyMeta:
class Layers(HoldTap): class Layers(HoldTap):
'''Gives access to the keys used to enable the layer system''' '''Gives access to the keys used to enable the layer system'''
_active_combo = None def __init__(self):
def __init__(
self,
combo_layers=None,
):
# Layers # Layers
super().__init__() super().__init__()
self.combo_layers = combo_layers
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
names=('MO',), names=('MO',),
@ -52,7 +46,9 @@ class Layers(HoldTap):
on_release=self._mo_released, on_release=self._mo_released,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, names=('DF',), on_press=self._df_pressed validator=layer_key_validator,
names=('DF',),
on_press=self._df_pressed,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, validator=layer_key_validator,
@ -61,10 +57,14 @@ class Layers(HoldTap):
on_release=self._lm_released, on_release=self._lm_released,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed validator=layer_key_validator,
names=('TG',),
on_press=self._tg_pressed,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator, names=('TO',), on_press=self._to_pressed validator=layer_key_validator,
names=('TO',),
on_press=self._to_pressed,
) )
make_argumented_key( make_argumented_key(
validator=layer_key_validator_lt, validator=layer_key_validator_lt,
@ -83,102 +83,67 @@ class Layers(HoldTap):
''' '''
Switches the default layer Switches the default layer
''' '''
self.activate_layer(keyboard, key.meta.layer, as_default=True) keyboard.active_layers[-1] = key.meta.layer
self._print_debug(keyboard)
def _mo_pressed(self, key, keyboard, *args, **kwargs): def _mo_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Momentarily activates layer, switches off when you let go Momentarily activates layer, switches off when you let go
''' '''
self.activate_layer(keyboard, key.meta.layer) keyboard.active_layers.insert(0, key.meta.layer)
self._print_debug(keyboard)
def _mo_released(self, key, keyboard, *args, **kwargs): @staticmethod
self.deactivate_layer(keyboard, key.meta.layer) def _mo_released(key, keyboard, *args, **kwargs):
# remove the first instance of the target layer
# from the active list
# under almost all normal use cases, this will
# disable the layer (but preserve it if it was triggered
# as a default layer, etc.)
# this also resolves an issue where using DF() on a layer
# triggered by MO() and then defaulting to the MO()'s layer
# would result in no layers active
try:
del_idx = keyboard.active_layers.index(key.meta.layer)
del keyboard.active_layers[del_idx]
except ValueError:
pass
__class__._print_debug(__class__, keyboard)
def _lm_pressed(self, key, keyboard, *args, **kwargs): def _lm_pressed(self, key, keyboard, *args, **kwargs):
''' '''
As MO(layer) but with mod active As MO(layer) but with mod active
''' '''
keyboard.hid_pending = True # Sets the timer start and acts like MO otherwise
keyboard.keys_pressed.add(key.meta.kc) keyboard.add_key(key.meta.kc)
self.activate_layer(keyboard, key.meta.layer) self._mo_pressed(key, keyboard, *args, **kwargs)
def _lm_released(self, key, keyboard, *args, **kwargs): def _lm_released(self, key, keyboard, *args, **kwargs):
''' '''
As MO(layer) but with mod active As MO(layer) but with mod active
''' '''
keyboard.hid_pending = True keyboard.remove_key(key.meta.kc)
keyboard.keys_pressed.discard(key.meta.kc) self._mo_released(key, keyboard, *args, **kwargs)
self.deactivate_layer(keyboard, key.meta.layer)
def _tg_pressed(self, key, keyboard, *args, **kwargs): def _tg_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Toggles the layer (enables it if not active, and vise versa) Toggles the layer (enables it if not active, and vise versa)
''' '''
# See mo_released for implementation details around this # See mo_released for implementation details around this
if key.meta.layer in keyboard.active_layers: try:
self.deactivate_layer(keyboard, key.meta.layer) del_idx = keyboard.active_layers.index(key.meta.layer)
else: del keyboard.active_layers[del_idx]
self.activate_layer(keyboard, key.meta.layer) except ValueError:
keyboard.active_layers.insert(0, key.meta.layer)
def _to_pressed(self, key, keyboard, *args, **kwargs): def _to_pressed(self, key, keyboard, *args, **kwargs):
''' '''
Activates layer and deactivates all other layers Activates layer and deactivates all other layers
''' '''
self._active_combo = None
keyboard.active_layers.clear() keyboard.active_layers.clear()
keyboard.active_layers.insert(0, key.meta.layer) keyboard.active_layers.insert(0, key.meta.layer)
def _print_debug(self, keyboard): def _print_debug(self, keyboard):
# debug(f'__getitem__ {key}')
if debug.enabled: if debug.enabled:
debug(f'active_layers={keyboard.active_layers}') debug(f'active_layers={keyboard.active_layers}')
def activate_layer(self, keyboard, layer, as_default=False):
if as_default:
keyboard.active_layers[-1] = layer
else:
keyboard.active_layers.insert(0, layer)
if self.combo_layers:
self._activate_combo_layer(keyboard)
self._print_debug(keyboard)
def deactivate_layer(self, keyboard, layer):
# Remove the first instance of the target layer from the active list
# under almost all normal use cases, this will disable the layer (but
# preserve it if it was triggered as a default layer, etc.).
# This also resolves an issue where using DF() on a layer
# triggered by MO() and then defaulting to the MO()'s layer
# would result in no layers active.
try:
del_idx = keyboard.active_layers.index(layer)
del keyboard.active_layers[del_idx]
except ValueError:
if debug.enabled:
debug(f'_mo_released: layer {layer} not active')
if self.combo_layers:
self._deactivate_combo_layer(keyboard, layer)
self._print_debug(keyboard)
def _activate_combo_layer(self, keyboard):
if self._active_combo:
return
for combo, result in self.combo_layers.items():
matching = True
for layer in combo:
if layer not in keyboard.active_layers:
matching = False
break
if matching:
self._active_combo = combo
keyboard.active_layers.insert(0, result)
break
def _deactivate_combo_layer(self, keyboard, layer):
if self._active_combo and layer in self._active_combo:
keyboard.active_layers.remove(self.combo_layers[self._active_combo])
self._active_combo = None

View File

@ -53,7 +53,7 @@ class OneShot(HoldTap):
elif state.activated == ActivationType.INTERRUPTED: elif state.activated == ActivationType.INTERRUPTED:
if is_pressed: if is_pressed:
send_buffer = True send_buffer = True
self.key_buffer.insert(0, (None, key, False)) self.key_buffer.insert(0, (0, key, False))
if send_buffer: if send_buffer:
self.key_buffer.append((int_coord, current_key, is_pressed)) self.key_buffer.append((int_coord, current_key, is_pressed))

View File

@ -42,11 +42,12 @@ class Phrase:
self._characters: list[Character] = [] self._characters: list[Character] = []
self._index: int = 0 self._index: int = 0
for char in string: for char in string:
try:
key_code = KC[char] key_code = KC[char]
if key_code == KC.NO:
raise ValueError(f'Invalid character in dictionary: {char}')
shifted = char.isupper() or key_code.has_modifiers == {2} shifted = char.isupper() or key_code.has_modifiers == {2}
self._characters.append(Character(key_code, shifted)) self._characters.append(Character(key_code, shifted))
except ValueError:
raise ValueError(f'Invalid character in dictionary: {char}')
def next_character(self) -> None: def next_character(self) -> None:
'''Increment the current index for this phrase''' '''Increment the current index for this phrase'''

View File

@ -17,7 +17,7 @@ class TapDanceKeyMeta:
ht_key = KC.HT( ht_key = KC.HT(
tap=key, tap=key,
hold=key, hold=key,
prefer_hold=True, prefer_hold=False,
tap_interrupted=False, tap_interrupted=False,
tap_time=self.tap_time, tap_time=self.tap_time,
) )

View File

@ -1,67 +0,0 @@
'''
Here we're abusing _asyncios TaskQueue to implement a very simple priority
queue task scheduler.
Despite documentation, Circuitpython doesn't usually ship with a min-heap
module; it does however implement a pairing-heap for `TaskQueue` in native code.
'''
try:
from typing import Callable
except ImportError:
pass
from supervisor import ticks_ms
from _asyncio import Task, TaskQueue
from kmk.kmktime import ticks_add, ticks_diff
_task_queue = TaskQueue()
class PeriodicTaskMeta:
def __init__(self, func: Callable[[None], None], period: int) -> None:
self._task = Task(self.call)
self._coro = func
self.period = period
def call(self) -> None:
self._coro()
after_ms = ticks_add(self._task.ph_key, self.period)
_task_queue.push_sorted(self._task, after_ms)
def create_task(
func: Callable[[None], None],
*,
after_ms: int = 0,
period_ms: int = 0,
) -> [Task, PeriodicTaskMeta]:
if period_ms:
r = PeriodicTaskMeta(func, period_ms)
t = r._task
else:
t = r = Task(func)
if after_ms:
after_ms = ticks_add(ticks_ms(), after_ms)
_task_queue.push_sorted(t, after_ms)
else:
_task_queue.push_head(t)
return r
def get_due_task() -> [Callable, None]:
while True:
t = _task_queue.peek()
if not t or ticks_diff(t.ph_key, ticks_ms()) > 0:
break
_task_queue.pop_head()
yield t.coro
def cancel_task(t: [Task, PeriodicTaskMeta]) -> None:
if isinstance(t, PeriodicTaskMeta):
t = t._task
_task_queue.remove(t)

View File

@ -1,8 +1,3 @@
try:
from typing import Optional
except ImportError:
pass
from supervisor import ticks_ms from supervisor import ticks_ms
@ -21,12 +16,8 @@ class Debug:
def __init__(self, name: str = __name__): def __init__(self, name: str = __name__):
self.name = name self.name = name
def __call__(self, *message: str, name: Optional[str] = None) -> None: def __call__(self, message: str) -> None:
if not name: print(f'{ticks_ms()} {self.name}: {message}')
name = self.name
print(ticks_ms(), end=' ')
print(name, end=': ')
print(*message, sep='')
@property @property
def enabled(self) -> bool: def enabled(self) -> bool:

View File

@ -6,7 +6,6 @@ from kmk.keys import KC, ModifierKey
from kmk.kmk_keyboard import KMKKeyboard from kmk.kmk_keyboard import KMKKeyboard
from kmk.scanners import DiodeOrientation from kmk.scanners import DiodeOrientation
from kmk.scanners.digitalio import MatrixScanner from kmk.scanners.digitalio import MatrixScanner
from kmk.scheduler import _task_queue
class DigitalInOut(Mock): class DigitalInOut(Mock):
@ -24,8 +23,6 @@ def code2name(code):
class KeyboardTest: class KeyboardTest:
loop_delay_ms = 2
def __init__( def __init__(
self, self,
modules, modules,
@ -82,7 +79,7 @@ class KeyboardTest:
timeout = time.time_ns() + 10 * 1_000_000_000 timeout = time.time_ns() + 10 * 1_000_000_000
while timeout > time.time_ns(): while timeout > time.time_ns():
self.do_main_loop() self.do_main_loop()
if not _task_queue.peek() and not self.keyboard._resume_buffer: if not self.keyboard._timeouts and not self.keyboard._resume_buffer:
break break
assert timeout > time.time_ns(), 'infinite loop detected' assert timeout > time.time_ns(), 'infinite loop detected'
@ -131,4 +128,4 @@ class KeyboardTest:
def do_main_loop(self): def do_main_loop(self):
self.keyboard._main_loop() self.keyboard._main_loop()
time.sleep(self.loop_delay_ms / 1000) time.sleep(0.002)

View File

@ -9,10 +9,6 @@ class KeyEvent:
self.pressed = pressed self.pressed = pressed
def ticks_ms():
return (time.time_ns() // 1_000_000) % (1 << 29)
def init_circuit_python_modules_mocks(): def init_circuit_python_modules_mocks():
sys.modules['usb_hid'] = Mock() sys.modules['usb_hid'] = Mock()
sys.modules['digitalio'] = Mock() sys.modules['digitalio'] = Mock()
@ -30,8 +26,4 @@ def init_circuit_python_modules_mocks():
sys.modules['micropython'].const = lambda x: x sys.modules['micropython'].const = lambda x: x
sys.modules['supervisor'] = Mock() sys.modules['supervisor'] = Mock()
sys.modules['supervisor'].ticks_ms = ticks_ms sys.modules['supervisor'].ticks_ms = lambda: time.time_ns() // 1_000_000
from . import task
sys.modules['_asyncio'] = task

View File

@ -1,196 +0,0 @@
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
# This file contains the core TaskQueue based on a pairing heap, and the core Task class.
# They can optionally be replaced by C implementations.
# This file is a modified version, based on the extmod in Circuitpython, for
# unit testing in KMK only.
from supervisor import ticks_ms
from kmk.kmktime import ticks_diff
cur_task = None
__task_queue = None
class CancelledError(BaseException):
pass
# pairing-heap meld of 2 heaps; O(1)
def ph_meld(h1, h2):
if h1 is None:
return h2
if h2 is None:
return h1
lt = ticks_diff(h1.ph_key, h2.ph_key) < 0
if lt:
if h1.ph_child is None:
h1.ph_child = h2
else:
h1.ph_child_last.ph_next = h2
h1.ph_child_last = h2
h2.ph_next = None
h2.ph_rightmost_parent = h1
return h1
else:
h1.ph_next = h2.ph_child
h2.ph_child = h1
if h1.ph_next is None:
h2.ph_child_last = h1
h1.ph_rightmost_parent = h2
return h2
# pairing-heap pairing operation; amortised O(log N)
def ph_pairing(child):
heap = None
while child is not None:
n1 = child
child = child.ph_next
n1.ph_next = None
if child is not None:
n2 = child
child = child.ph_next
n2.ph_next = None
n1 = ph_meld(n1, n2)
heap = ph_meld(heap, n1)
return heap
# pairing-heap delete of a node; stable, amortised O(log N)
def ph_delete(heap, node):
if node is heap:
child = heap.ph_child
node.ph_child = None
return ph_pairing(child)
# Find parent of node
parent = node
while parent.ph_next is not None:
parent = parent.ph_next
parent = parent.ph_rightmost_parent
if parent is None or parent.ph_child is None:
return heap
# Replace node with pairing of its children
if node is parent.ph_child and node.ph_child is None:
parent.ph_child = node.ph_next
node.ph_next = None
return heap
elif node is parent.ph_child:
child = node.ph_child
next = node.ph_next
node.ph_child = None
node.ph_next = None
node = ph_pairing(child)
parent.ph_child = node
else:
n = parent.ph_child
while node is not n.ph_next:
n = n.ph_next
if not n:
return heap
child = node.ph_child
next = node.ph_next
node.ph_child = None
node.ph_next = None
node = ph_pairing(child)
if node is None:
node = n
else:
n.ph_next = node
node.ph_next = next
if next is None:
node.ph_rightmost_parent = parent
parent.ph_child_last = node
return heap
# TaskQueue class based on the above pairing-heap functions.
class TaskQueue:
def __init__(self):
self.heap = None
def peek(self):
return self.heap
def push_sorted(self, v, key):
v.data = None
v.ph_key = key
v.ph_child = None
v.ph_next = None
self.heap = ph_meld(v, self.heap)
def push_head(self, v):
self.push_sorted(v, ticks_ms())
def pop_head(self):
v = self.heap
self.heap = ph_pairing(v.ph_child)
# v.ph_child = None
return v
def remove(self, v):
self.heap = ph_delete(self.heap, v)
# Task class representing a coroutine, can be waited on and cancelled.
class Task:
def __init__(self, coro, globals=None):
self.coro = coro # Coroutine of this Task
self.data = None # General data for queue it is waiting on
self.state = True # None, False, True or a TaskQueue instance
self.ph_key = 0 # Pairing heap
self.ph_child = None # Paring heap
self.ph_child_last = None # Paring heap
self.ph_next = None # Paring heap
self.ph_rightmost_parent = None # Paring heap
def __await__(self):
if not self.state:
# Task finished, signal that is has been await'ed on.
self.state = False
elif self.state is True:
# Allocated head of linked list of Tasks waiting on completion of this task.
self.state = TaskQueue()
return self
def __next__(self):
if not self.state:
if self.data is None:
# Task finished but has already been sent to the loop's exception handler.
raise StopIteration
else:
# Task finished, raise return value to caller so it can continue.
raise self.data
else:
# Put calling task on waiting queue.
self.state.push_head(cur_task)
# Set calling task's data to this task that it waits on, to double-link it.
cur_task.data = self
def done(self):
return not self.state
def cancel(self):
# Check if task is already finished.
if not self.state:
return False
# Can't cancel self (not supported yet).
if self is cur_task:
raise RuntimeError("can't cancel self")
# If Task waits on another task then forward the cancel to the one it's waiting on.
while isinstance(self.data, Task):
self = self.data
# Reschedule Task as a cancelled task.
if hasattr(self.data, 'remove'):
# Not on the main running queue, remove the task from the queue it's on.
self.data.remove(self)
__task_queue.push_head(self)
elif ticks_diff(self.ph_key, ticks_ms()) > 0:
# On the main running queue but scheduled in the future, so bring it forward to now.
__task_queue.remove(self)
__task_queue.push_head(self)
self.data = CancelledError
return True

View File

@ -10,7 +10,7 @@ from tests.keyboard_test import KeyboardTest
class TestCapsWord(unittest.TestCase): class TestCapsWord(unittest.TestCase):
def setUp(self): def setUp(self):
self.kb = KeyboardTest( self.kb = KeyboardTest(
[CapsWord(timeout=2 * KeyboardTest.loop_delay_ms)], [CapsWord()],
[ [
[KC.CW, KC.A, KC.Z, KC.N1, KC.N0, KC.SPC], [KC.CW, KC.A, KC.Z, KC.N1, KC.N0, KC.SPC],
], ],

View File

@ -1,36 +1,28 @@
import unittest import unittest
from kmk.keys import KC from kmk.keys import KC
from kmk.modules.combos import Chord, Combo, Combos, Sequence from kmk.modules.combos import Chord, Combos, Sequence
from kmk.modules.layers import Layers from kmk.modules.layers import Layers
from tests.keyboard_test import KeyboardTest from tests.keyboard_test import KeyboardTest
class TestCombo(unittest.TestCase): class TestCombo(unittest.TestCase):
def setUp(self): def setUp(self):
self.t_within = 2 * KeyboardTest.loop_delay_ms
self.t_after = 7 * KeyboardTest.loop_delay_ms
timeout = (self.t_after + self.t_within) // 2
# overide default timeouts
Combo.timeout = timeout
Sequence.timeout = timeout
combos = Combos() combos = Combos()
layers = Layers() layers = Layers()
KCMO = KC.MO(1) KCMO = KC.MO(1)
combos.combos = [ combos.combos = [
Chord((KC.A, KC.B, KC.C), KC.Y), Chord((KC.A, KC.B, KC.C), KC.Y),
Chord((KC.A, KC.B), KC.X), Chord((KC.A, KC.B), KC.X),
Chord((KC.C, KC.D), KC.Z, timeout=2 * timeout), Chord((KC.C, KC.D), KC.Z, timeout=80),
Chord((KC.C, KCMO), KC.Z), Chord((KC.C, KCMO), KC.Z),
Chord((KC.F, KC.G), KC.Z, timeout=3 * timeout), Chord((KC.F, KC.G), KC.Z, timeout=130),
Sequence((KC.N1, KC.N2, KC.N3), KC.Y), Sequence((KC.N1, KC.N2, KC.N3), KC.Y, timeout=50),
Sequence((KC.N1, KC.N2), KC.X), Sequence((KC.N1, KC.N2), KC.X, timeout=50),
Sequence((KC.N3, KC.N4), KC.Z, timeout=2 * timeout), Sequence((KC.N3, KC.N4), KC.Z, timeout=100),
Sequence((KC.N1, KC.N1, KC.N1), KC.W), Sequence((KC.N1, KC.N1, KC.N1), KC.W, timeout=50),
Sequence((KC.N3, KC.N2, KC.N1), KC.Y, fast_reset=False), Sequence((KC.N3, KC.N2, KC.N1), KC.Y, timeout=50, fast_reset=False),
Sequence((KC.LEADER, KC.N1), KC.V), Sequence((KC.LEADER, KC.N1), KC.V, timeout=50),
] ]
self.keyboard = KeyboardTest( self.keyboard = KeyboardTest(
[combos, layers], [combos, layers],
@ -41,6 +33,9 @@ class TestCombo(unittest.TestCase):
debug_enabled=False, debug_enabled=False,
) )
self.t_within = 40
self.t_after = 60
def test_chord(self): def test_chord(self):
keyboard = self.keyboard keyboard = self.keyboard
t_within = self.t_within t_within = self.t_within

View File

@ -10,37 +10,20 @@ class TestHoldTap(unittest.TestCase):
def setUp(self): def setUp(self):
KC.clear() KC.clear()
self.t_within = 2 * KeyboardTest.loop_delay_ms
self.t_after = 6 * KeyboardTest.loop_delay_ms
tap_time = 5 * KeyboardTest.loop_delay_ms
# overide default timeouts
HoldTap.tap_time = tap_time
def test_holdtap(self): def test_holdtap(self):
t_within = self.t_within
t_after = self.t_after
keyboard = KeyboardTest( keyboard = KeyboardTest(
[Layers(), HoldTap()], [Layers(), HoldTap()],
[ [
[ [KC.HT(KC.A, KC.LCTL), KC.LT(1, KC.B), KC.C, KC.D],
KC.HT(KC.A, KC.LCTL),
KC.LT(1, KC.B),
KC.C,
KC.D,
],
[KC.N1, KC.N2, KC.N3, KC.N4], [KC.N1, KC.N2, KC.N3, KC.N4],
], ],
debug_enabled=False, debug_enabled=False,
) )
keyboard.test( keyboard.test('HT tap behaviour', [(0, True), 100, (0, False)], [{KC.A}, {}])
'HT tap behaviour', [(0, True), t_within, (0, False)], [{KC.A}, {}]
)
keyboard.test( keyboard.test(
'HT hold behaviour', [(0, True), t_after, (0, False)], [{KC.LCTL}, {}] 'HT hold behaviour', [(0, True), 350, (0, False)], [{KC.LCTL}, {}]
) )
# TODO test multiple mods being held # TODO test multiple mods being held
@ -48,74 +31,74 @@ class TestHoldTap(unittest.TestCase):
# HT # HT
keyboard.test( keyboard.test(
'HT within tap time sequential -> tap behavior', 'HT within tap time sequential -> tap behavior',
[(0, True), t_within, (0, False), (3, True), (3, False)], [(0, True), 100, (0, False), (3, True), (3, False)],
[{KC.A}, {}, {KC.D}, {}], [{KC.A}, {}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'HT within tap time rolling -> hold behavior', 'HT within tap time rolling -> hold behavior',
[(0, True), t_within, (3, True), t_after, (0, False), (3, False)], [(0, True), 100, (3, True), 250, (0, False), (3, False)],
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}], [{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'HT within tap time nested -> hold behavior', 'HT within tap time nested -> hold behavior',
[(0, True), t_within, (3, True), (3, False), t_after, (0, False)], [(0, True), 100, (3, True), (3, False), 250, (0, False)],
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}], [{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}],
) )
keyboard.test( keyboard.test(
'HT after tap time sequential -> hold behavior', 'HT after tap time sequential -> hold behavior',
[(0, True), t_after, (0, False), (3, True), (3, False)], [(0, True), 350, (0, False), (3, True), (3, False)],
[{KC.LCTL}, {}, {KC.D}, {}], [{KC.LCTL}, {}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'HT after tap time rolling -> hold behavior', 'HT after tap time rolling -> hold behavior',
[(0, True), t_after, (3, True), (0, False), (3, False)], [(0, True), 350, (3, True), (0, False), (3, False)],
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}], [{KC.LCTL}, {KC.LCTL, KC.D}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'HT after tap time nested -> hold behavior', 'HT after tap time nested -> hold behavior',
[(0, True), t_after, (3, True), (3, False), (0, False)], [(0, True), 350, (3, True), (3, False), (0, False)],
[{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}], [{KC.LCTL}, {KC.LCTL, KC.D}, {KC.LCTL}, {}],
) )
# LT # LT
keyboard.test( keyboard.test(
'LT within tap time sequential -> tap behavior', 'LT within tap time sequential -> tap behavior',
[(1, True), t_within, (1, False), (3, True), (3, False)], [(1, True), 100, (1, False), (3, True), (3, False)],
[{KC.B}, {}, {KC.D}, {}], [{KC.B}, {}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'LT within tap time rolling -> tap behavior', 'LT within tap time rolling -> tap behavior',
[(1, True), t_within, (3, True), (1, False), (3, False)], [(1, True), 100, (3, True), 250, (1, False), (3, False)],
[{KC.B}, {KC.B, KC.D}, {KC.D}, {}], [{KC.B}, {KC.B, KC.D}, {KC.D}, {}],
) )
keyboard.test( keyboard.test(
'LT within tap time nested -> tap behavior', 'LT within tap time nested -> tap behavior',
[(1, True), t_within, (3, True), (3, False), (1, False)], [(1, True), 100, (3, True), (3, False), 250, (1, False)],
[{KC.B}, {KC.B, KC.D}, {KC.B}, {}], [{KC.B}, {KC.B, KC.D}, {KC.B}, {}],
) )
keyboard.test( keyboard.test(
'LT after tap time sequential -> hold behavior', 'LT after tap time sequential -> hold behavior',
[(1, True), t_after, (1, False), (3, True), (3, False)], [(1, True), 350, (1, False), (3, True), (3, False)],
[{KC.D}, {}], [{KC.D}, {}],
) )
keyboard.test( keyboard.test(
'LT after tap time rolling -> hold behavior', 'LT after tap time rolling -> hold behavior',
[(1, True), t_after, (3, True), (1, False), (3, False)], [(1, True), 350, (3, True), (1, False), (3, False)],
[{KC.N4}, {}], [{KC.N4}, {}],
) )
keyboard.test( keyboard.test(
'LT after tap time nested -> hold behavior', 'LT after tap time nested -> hold behavior',
[(1, True), t_after, (3, True), (3, False), (1, False)], [(1, True), 350, (3, True), (3, False), (1, False)],
[{KC.N4}, {}], [{KC.N4}, {}],
) )
@ -123,9 +106,9 @@ class TestHoldTap(unittest.TestCase):
'LT after tap time nested -> hold behavior', 'LT after tap time nested -> hold behavior',
[ [
(0, True), (0, True),
t_after, 350,
(1, True), (1, True),
t_after, 350,
(3, True), (3, True),
(3, False), (3, False),
(1, False), (1, False),
@ -135,25 +118,26 @@ class TestHoldTap(unittest.TestCase):
) )
def test_holdtap_chain(self): def test_holdtap_chain(self):
t_after = self.t_after
keyboard = KeyboardTest( keyboard = KeyboardTest(
[HoldTap()], [HoldTap()],
[ [
[ [
KC.N0, KC.N0,
KC.HT(KC.N1, KC.LCTL), KC.HT(KC.N1, KC.LCTL, tap_time=50),
KC.HT(KC.N2, KC.LSFT, tap_interrupted=True), KC.HT(KC.N2, KC.LSFT, tap_interrupted=True, tap_time=50),
KC.HT( KC.HT(
KC.N3, KC.N3,
KC.LALT, KC.LALT,
prefer_hold=False, prefer_hold=False,
tap_interrupted=True, tap_interrupted=True,
tap_time=50,
), ),
], ],
], ],
debug_enabled=False, debug_enabled=False,
) )
# t_within = 40
t_after = 60
keyboard.test( keyboard.test(
'chained 0', 'chained 0',
@ -172,7 +156,7 @@ class TestHoldTap(unittest.TestCase):
'chained 1', 'chained 1',
[(2, True), (1, True), (0, True), (0, False), (1, False), (2, False)], [(2, True), (1, True), (0, True), (0, False), (1, False), (2, False)],
[ [
{KC.LSFT}, {KC.LCTL},
{KC.LCTL, KC.LSFT}, {KC.LCTL, KC.LSFT},
{KC.LCTL, KC.LSFT, KC.N0}, {KC.LCTL, KC.LSFT, KC.N0},
{KC.LCTL, KC.LSFT}, {KC.LCTL, KC.LSFT},
@ -224,7 +208,7 @@ class TestHoldTap(unittest.TestCase):
'chained 5', 'chained 5',
[(3, True), (1, True), (0, True), (0, False), (1, False), (3, False)], [(3, True), (1, True), (0, True), (0, False), (1, False), (3, False)],
[ [
{KC.N3}, {KC.LCTL},
{KC.LCTL, KC.N3}, {KC.LCTL, KC.N3},
{KC.LCTL, KC.N3, KC.N0}, {KC.LCTL, KC.N3, KC.N0},
{KC.LCTL, KC.N3}, {KC.LCTL, KC.N3},
@ -291,21 +275,21 @@ class TestHoldTap(unittest.TestCase):
# TODO test TT # TODO test TT
def test_holdtap_repeat(self): def test_holdtap_repeat(self):
t_within = self.t_within
t_after = self.t_after
keyboard = KeyboardTest( keyboard = KeyboardTest(
[HoldTap()], [HoldTap()],
[ [
[ [
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.ALL), KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.ALL, tap_time=50),
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.TAP), KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.TAP, tap_time=50),
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.HOLD), KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.HOLD, tap_time=50),
] ]
], ],
debug_enabled=False, debug_enabled=False,
) )
t_within = 40
t_after = 60
keyboard.test( keyboard.test(
'repeat tap', 'repeat tap',
[ [
@ -317,6 +301,7 @@ class TestHoldTap(unittest.TestCase):
(0, False), (0, False),
(0, True), (0, True),
(0, False), (0, False),
t_after,
], ],
[{KC.A}, {}, {KC.A}, {}, {KC.A}, {}], [{KC.A}, {}, {KC.A}, {}, {KC.A}, {}],
) )
@ -332,6 +317,7 @@ class TestHoldTap(unittest.TestCase):
(0, False), (0, False),
(0, True), (0, True),
(0, False), (0, False),
t_after,
], ],
[{KC.B}, {}, {KC.B}, {}, {KC.B}, {}], [{KC.B}, {}, {KC.B}, {}, {KC.B}, {}],
) )
@ -348,6 +334,7 @@ class TestHoldTap(unittest.TestCase):
t_after, t_after,
(0, True), (0, True),
(0, False), (0, False),
t_after,
], ],
[{KC.A}, {}, {KC.B}, {}, {KC.A}, {}], [{KC.A}, {}, {KC.B}, {}, {KC.A}, {}],
) )

View File

@ -123,10 +123,12 @@ class TestKeys_dot(unittest.TestCase):
assert primary_key is secondary_key assert primary_key is secondary_key
def test_invalid_key_upper(self): def test_invalid_key_upper(self):
assert KC.INVALID_KEY == KC.NO with self.assertRaises(ValueError):
KC.INVALID_KEY
def test_invalid_key_lower(self): def test_invalid_key_lower(self):
assert KC.invalid_key == KC.NO with self.assertRaises(ValueError):
KC.invalid_key
def test_custom_key(self): def test_custom_key(self):
created = make_key( created = make_key(
@ -166,10 +168,12 @@ class TestKeys_index(unittest.TestCase):
assert upper_key is lower_key assert upper_key is lower_key
def test_invalid_key_upper(self): def test_invalid_key_upper(self):
assert KC.INVALID_KEY == KC.NO with self.assertRaises(ValueError):
KC['NOT_A_VALID_KEY']
def test_invalid_key_lower(self): def test_invalid_key_lower(self):
assert KC.invalid_key == KC.NO with self.assertRaises(ValueError):
KC['not_a_valid_key']
def test_custom_key(self): def test_custom_key(self):
created = make_key( created = make_key(
@ -214,10 +218,10 @@ class TestKeys_get(unittest.TestCase):
assert primary_key is secondary_key assert primary_key is secondary_key
def test_invalid_key_upper(self): def test_invalid_key_upper(self):
assert KC.get('INVALID_KEY') is KC.NO assert KC.get('INVALID_KEY') is None
def test_invalid_key_lower(self): def test_invalid_key_lower(self):
assert KC.get('not_a_valid_key') is KC.NO assert KC.get('not_a_valid_key') is None
def test_custom_key(self): def test_custom_key(self):
created = make_key( created = make_key(

View File

@ -10,13 +10,8 @@ class TestLayers(unittest.TestCase):
self.kb = KeyboardTest( self.kb = KeyboardTest(
[Layers()], [Layers()],
[ [
[ [KC.N0, KC.LM(1, KC.LCTL)],
KC.N0, [KC.A, KC.B],
KC.LM(1, KC.LCTL),
KC.LT(1, KC.N2, tap_interrupted=True, prefer_hold=True),
KC.LT(1, KC.N3, tap_interrupted=False, prefer_hold=True),
],
[KC.A, KC.B, KC.C, KC.D],
], ],
debug_enabled=False, debug_enabled=False,
) )
@ -28,25 +23,6 @@ class TestLayers(unittest.TestCase):
[{KC.LCTL}, {KC.LCTL, KC.A}, {KC.A}, {}], [{KC.LCTL}, {KC.LCTL, KC.A}, {KC.A}, {}],
) )
def test_layertap(self):
self.kb.test(
'Layertap roll',
[(2, True), (0, True), (2, False), (0, False)],
[{KC.N2}, {KC.N0, KC.N2}, {KC.N0}, {}],
)
self.kb.test(
'Layertap tap interrupted',
[(2, True), (0, True), 200, (0, False), (2, False)],
[{KC.A}, {}],
)
self.kb.test(
'Layertap tap interrupted by holdtap',
[(3, True), (2, True), (2, False), (3, False)],
[{KC.C}, {}],
)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -6,30 +6,25 @@ from kmk.modules.oneshot import OneShot
from tests.keyboard_test import KeyboardTest from tests.keyboard_test import KeyboardTest
class TestOneshot(unittest.TestCase): class TestHoldTap(unittest.TestCase):
def test_oneshot(self): def test_oneshot(self):
t_within = 2 * KeyboardTest.loop_delay_ms
t_after = 7 * KeyboardTest.loop_delay_ms
timeout = (t_after + t_within) // 2
# overide default timeouts
OneShot.tap_time = timeout
keyboard = KeyboardTest( keyboard = KeyboardTest(
[Layers(), OneShot()], [Layers(), OneShot()],
[ [
[ [
KC.OS(KC.MO(1)), KC.OS(KC.MO(1), tap_time=50),
KC.MO(1), KC.MO(1),
KC.C, KC.C,
KC.D, KC.D,
KC.OS(KC.E), KC.OS(KC.E, tap_time=50),
KC.OS(KC.F), KC.OS(KC.F, tap_time=50),
], ],
[KC.N0, KC.N1, KC.N2, KC.N3, KC.OS(KC.LSFT), KC.TRNS], [KC.N0, KC.N1, KC.N2, KC.N3, KC.OS(KC.LSFT, tap_time=50), KC.TRNS],
], ],
debug_enabled=False, debug_enabled=False,
) )
t_within = 40
t_after = 60
keyboard.test( keyboard.test(
'OS timed out', 'OS timed out',

View File

@ -37,8 +37,11 @@ class TestStickyMod(unittest.TestCase):
[ [
(4, True), (4, True),
(4, False), (4, False),
100,
(4, True), (4, True),
200,
(4, False), (4, False),
100,
(1, True), (1, True),
(1, False), (1, False),
], ],
@ -58,19 +61,26 @@ class TestStickyMod(unittest.TestCase):
(1, True), (1, True),
(1, False), (1, False),
(2, True), (2, True),
200,
(0, True), (0, True),
50,
(0, False), (0, False),
50,
(0, True), (0, True),
50,
(0, False), (0, False),
(1, True), (1, True),
(1, False), (1, False),
50,
(1, True), (1, True),
(1, False), (1, False),
(0, True), (0, True),
50,
(0, False), (0, False),
(3, True), (3, True),
(3, False), (3, False),
(2, False), (2, False),
100,
(4, True), (4, True),
(4, False), (4, False),
(1, True), (1, True),

View File

@ -7,7 +7,6 @@ from tests.keyboard_test import KeyboardTest
class TestStringSubstitution(unittest.TestCase): class TestStringSubstitution(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.delay = KeyboardTest.loop_delay_ms
self.symbols = '`-=[]\\;\',./~!@#$%^&*()_+{}|:\"<>?' self.symbols = '`-=[]\\;\',./~!@#$%^&*()_+{}|:\"<>?'
self.everything = ALL_NUMBERS + ALL_ALPHAS + ALL_ALPHAS.lower() + self.symbols self.everything = ALL_NUMBERS + ALL_ALPHAS + ALL_ALPHAS.lower() + self.symbols
self.test_dictionary = { self.test_dictionary = {
@ -47,21 +46,21 @@ class TestStringSubstitution(unittest.TestCase):
# that results in a corresponding match, as that key is never sent # that results in a corresponding match, as that key is never sent
self.keyboard.test( self.keyboard.test(
'multi-character key, single-character value', 'multi-character key, single-character value',
[(0, True), (0, False), (0, True), (0, False), self.delay], [(0, True), (0, False), (0, True), (0, False), 50],
[{KC.A}, {}, {KC.BACKSPACE}, {}, {KC.B}, {}], [{KC.A}, {}, {KC.BACKSPACE}, {}, {KC.B}, {}],
) )
# note: the pressed key is never sent here, as the event is # note: the pressed key is never sent here, as the event is
# intercepted and the replacement is sent instead # intercepted and the replacement is sent instead
self.keyboard.test( self.keyboard.test(
'multi-character value, single-character key', 'multi-character value, single-character key',
[(1, True), (1, False), self.delay], [(1, True), (1, False), 50],
[{KC.A}, {}, {KC.A}, {}], [{KC.A}, {}, {KC.A}, {}],
) )
# modifiers are force-released if there's a match, # modifiers are force-released if there's a match,
# so the keyup event for them isn't sent # so the keyup event for them isn't sent
self.keyboard.test( self.keyboard.test(
'shifted alphanumeric or symbol in key and/or value', 'shifted alphanumeric or symbol in key and/or value',
[(3, True), (2, True), (2, False), (3, False), self.delay], [(3, True), (2, True), (2, False), (3, False), 50],
[{KC.LSHIFT}, {KC.LSHIFT, KC.N2}, {}], [{KC.LSHIFT}, {KC.LSHIFT, KC.N2}, {}],
) )
self.keyboard.test( self.keyboard.test(
@ -75,7 +74,7 @@ class TestStringSubstitution(unittest.TestCase):
(5, False), (5, False),
(5, True), (5, True),
(5, False), (5, False),
self.delay, 10,
], ],
[ [
{KC.D}, {KC.D},
@ -94,7 +93,7 @@ class TestStringSubstitution(unittest.TestCase):
) )
self.keyboard.test( self.keyboard.test(
'the presence of non-shift modifiers prevents a multi-character match', 'the presence of non-shift modifiers prevents a multi-character match',
[(4, True), (0, True), (0, False), (0, True), (0, False), (4, False)], [(4, True), (0, True), (0, False), (0, True), (0, False), (4, False), 50],
[ [
{KC.LCTRL}, {KC.LCTRL},
{KC.LCTRL, KC.A}, {KC.LCTRL, KC.A},
@ -106,7 +105,7 @@ class TestStringSubstitution(unittest.TestCase):
) )
self.keyboard.test( self.keyboard.test(
'the presence of non-shift modifiers prevents a single-character match', 'the presence of non-shift modifiers prevents a single-character match',
[(4, True), (1, True), (1, False), (4, False)], [(4, True), (1, True), (1, False), (4, False), 50],
[ [
{KC.LCTRL}, {KC.LCTRL},
{KC.LCTRL, KC.B}, {KC.LCTRL, KC.B},
@ -116,7 +115,7 @@ class TestStringSubstitution(unittest.TestCase):
) )
self.keyboard.test( self.keyboard.test(
'the presence of non-shift modifiers resets current potential matches', 'the presence of non-shift modifiers resets current potential matches',
[(0, True), (0, False), (4, True), (0, True), (0, False), (4, False)], [(0, True), (0, False), (4, True), (0, True), (0, False), (4, False), 50],
[ [
{KC.A}, {KC.A},
{}, {},
@ -129,15 +128,7 @@ class TestStringSubstitution(unittest.TestCase):
self.keyboard.test( self.keyboard.test(
'match found and replaced when there are preceding characters', 'match found and replaced when there are preceding characters',
[ [(5, True), (5, False), (0, True), (0, False), (0, True), (0, False), 50],
(5, True),
(5, False),
(0, True),
(0, False),
(0, True),
(0, False),
self.delay,
],
[ [
{KC.C}, {KC.C},
{}, {},
@ -151,15 +142,7 @@ class TestStringSubstitution(unittest.TestCase):
) )
self.keyboard.test( self.keyboard.test(
'match found and replaced when there are trailing characters, and the trailing characters are sent', 'match found and replaced when there are trailing characters, and the trailing characters are sent',
[ [(0, True), (0, False), (0, True), (0, False), (5, True), (5, False), 50],
(0, True),
(0, False),
(0, True),
(0, False),
(5, True),
(5, False),
self.delay,
],
[ [
{KC.A}, {KC.A},
{}, {},
@ -173,7 +156,7 @@ class TestStringSubstitution(unittest.TestCase):
) )
self.keyboard.test( self.keyboard.test(
'no match', 'no match',
[(0, True), (0, False), (2, True), (2, False)], [(0, True), (0, False), (2, True), (2, False), 50],
[ [
{KC.A}, {KC.A},
{}, {},
@ -200,7 +183,7 @@ class TestStringSubstitution(unittest.TestCase):
(6, False), (6, False),
(0, True), (0, True),
(0, False), (0, False),
10 * self.delay, 50,
], ],
[ [
{KC.D}, {KC.D},
@ -258,7 +241,7 @@ class TestStringSubstitution(unittest.TestCase):
# send the unreachable match "cccc" after matching "ccc" # send the unreachable match "cccc" after matching "ccc"
(5, True), (5, True),
(5, False), (5, False),
self.delay, 10,
], ],
[ [
{KC.C}, {KC.C},
@ -289,7 +272,7 @@ class TestStringSubstitution(unittest.TestCase):
(0, True), (0, True),
(0, False), (0, False),
(7, False), (7, False),
self.delay, 10,
], ],
[ [
{KC.RSHIFT}, {KC.RSHIFT},
@ -320,6 +303,7 @@ class TestStringSubstitution(unittest.TestCase):
(0, False), (0, False),
(4, False), (4, False),
(8, False), (8, False),
10,
], ],
[ [
{KC.RALT}, {KC.RALT},
@ -341,6 +325,7 @@ class TestStringSubstitution(unittest.TestCase):
(1, False), (1, False),
(3, False), (3, False),
(8, False), (8, False),
10,
], ],
[ [
{KC.RALT}, {KC.RALT},

View File

@ -9,39 +9,36 @@ from tests.keyboard_test import KeyboardTest
class TestTapDance(unittest.TestCase): class TestTapDance(unittest.TestCase):
def setUp(self): def setUp(self):
self.t_within = 2 * KeyboardTest.loop_delay_ms
self.t_after = 10 * KeyboardTest.loop_delay_ms
tap_time = (self.t_after + self.t_within) // 4 * 3
TapDance.tap_time = tap_time
self.keyboard = KeyboardTest( self.keyboard = KeyboardTest(
[Layers(), HoldTap(), TapDance()], [Layers(), HoldTap(), TapDance()],
[ [
[ [
KC.TD(KC.N0, KC.N1), KC.TD(KC.N0, KC.N1, tap_time=50),
KC.TD( KC.TD(
KC.HT(KC.N1, KC.A), KC.HT(KC.N1, KC.A, tap_time=50),
KC.HT(KC.N2, KC.B, tap_time=2 * tap_time), KC.HT(KC.N2, KC.B, tap_time=100),
), ),
KC.TD(KC.HT(KC.X, KC.Y), KC.X, tap_time=0), KC.TD(KC.HT(KC.X, KC.Y, tap_time=50), KC.X, tap_time=0),
KC.TD(KC.LT(1, KC.N3), KC.X, tap_time=0), KC.TD(KC.LT(1, KC.N3, tap_time=50), KC.X, tap_time=0),
KC.N4, KC.N4,
], ],
[KC.N9, KC.N8, KC.N7, KC.N6, KC.N5], [KC.N9, KC.N8, KC.N7, KC.N6, KC.N5],
], ],
debug_enabled=False, debug_enabled=False,
) )
self.t_within = 40
self.t_after = 60
def test_normal_key(self): def test_normal_key(self):
keyboard = self.keyboard keyboard = self.keyboard
t_within = self.t_within t_within = self.t_within
t_after = self.t_after
keyboard.test('Tap x1', [(0, True), (0, False)], [{KC.N0}, {}]) keyboard.test('Tap x1', [(0, True), (0, False), t_after], [{KC.N0}, {}])
keyboard.test( keyboard.test(
'Tap x2', 'Tap x2',
[(0, True), (0, False), t_within, (0, True), (0, False)], [(0, True), (0, False), t_within, (0, True), (0, False), t_after],
[{KC.N1}, {}], [{KC.N1}, {}],
) )
@ -54,6 +51,7 @@ class TestTapDance(unittest.TestCase):
(0, False), (0, False),
(0, True), (0, True),
(0, False), (0, False),
t_after,
], ],
[{KC.N1}, {}, {KC.N0}, {}], [{KC.N1}, {}, {KC.N0}, {}],
) )
@ -95,11 +93,11 @@ class TestTapDance(unittest.TestCase):
t_within = self.t_within t_within = self.t_within
t_after = self.t_after t_after = self.t_after
keyboard.test('Tap x1', [(1, True), (1, False)], [{KC.N1}, {}]) keyboard.test('Tap x1', [(1, True), (1, False), t_after], [{KC.N1}, {}])
keyboard.test( keyboard.test(
'Tap x2', 'Tap x2',
[(1, True), (1, False), t_within, (1, True), (1, False)], [(1, True), (1, False), t_within, (1, True), (1, False), 2 * t_after],
[{KC.N2}, {}], [{KC.N2}, {}],
) )
@ -133,7 +131,7 @@ class TestTapDance(unittest.TestCase):
keyboard.test( keyboard.test(
'', '',
[(0, True), (0, False), t_within, (1, True), (1, False)], [(0, True), (0, False), t_within, (1, True), (1, False), t_after],
[{KC.N0}, {}, {KC.N1}, {}], [{KC.N0}, {}, {KC.N1}, {}],
) )
@ -147,6 +145,7 @@ class TestTapDance(unittest.TestCase):
(2, False), (2, False),
t_after, t_after,
(0, False), (0, False),
t_after,
], ],
[{KC.N1}, {KC.N1, KC.X}, {KC.N1}, {}], [{KC.N1}, {KC.N1, KC.X}, {KC.N1}, {}],
) )
@ -161,6 +160,7 @@ class TestTapDance(unittest.TestCase):
(0, False), (0, False),
t_after, t_after,
(2, False), (2, False),
t_after,
], ],
[{KC.X}, {KC.X, KC.N0}, {KC.X}, {}], [{KC.X}, {KC.X, KC.N0}, {KC.X}, {}],
) )
@ -172,7 +172,7 @@ class TestTapDance(unittest.TestCase):
keyboard.test( keyboard.test(
'', '',
[(3, True), (3, False), t_within, (1, True), (1, False)], [(3, True), (3, False), t_within, (1, True), (1, False), t_after],
[{KC.N3}, {}, {KC.N1}, {}], [{KC.N3}, {}, {KC.N1}, {}],
) )