Compare commits

...

21 Commits

Author SHA1 Message Date
xs5871
b84cd0bdab Add missing keycode 2023-04-01 21:55:45 +00:00
xs5871
d15569611e Port RGB extension from timer to periodic task 2023-04-01 00:12:49 +00:00
xs5871
3c4e064201 Implement a heap based task scheduler 2023-04-01 00:12:49 +00:00
regicidal.plutophage
bc5fb9dc9e Fix OLED ImageEntry
Fixed a silly oversight that was preventing image entries from working
2023-03-26 22:28:35 +00:00
xs5871
0992bfb962 Clarfify title format of issue template 2023-03-25 00:51:15 +00:00
regicidal.plutophage
f532a57e9a
New OLED extension (#742)
* Adding a new OLED extension and updated kyria code to use it.

* Fixed some basic errors and improved performance of extension.

* Add support for changing LED brightness.

* Add support for changing OLED brightness.

* Update oled.py

* Small bugfix

* Take busio.I2C object as Oled parameter

* Add a missing statement into deinit

* Optionally initialize I2C inside the extension

* Implement suggested changes

---------

Co-authored-by: Jan Lindblom <janlindblom@fastmail.fm>
2023-03-24 22:22:41 +00:00
xs5871
5448cb4479 Denoise core debugging and improve improve performance.
* Declutter and denoise the debug output in general.
* Avoid f-strings. They're nice to look at, but use a lot of ROM, RAM,
  and time during formatting.
* Remove all "debug" information from `KMKKeyboard.__repr__`. It's
  printed out once at init and the info it gave was useless at that
  point. Even more free memory.
* Add a memory footprint debug info after initialization.
2023-03-24 21:48:44 +00:00
Leon Anavi
23d7c2d670 boards/anavi/knob1/code.py: Cleanup
Remove print to cleanup the code.

Signed-off-by: Leon Anavi <leon@anavi.org>
2023-03-24 21:48:28 +00:00
xs5871
20ba48b623 Fix oneshot as combo result getting stuck 2023-03-16 21:52:26 +00:00
xs5871
76e6feda6f Speed up and reduce memory footprint of debug template 2023-03-16 21:52:13 +00:00
xs5871
3e13c8c321 Speed up unit tests 2023-03-16 21:51:16 +00:00
xs5871
ba06d3c8a5 Fix some old holdtap inconsistencies 2023-03-16 21:50:58 +00:00
xs5871
26bf630608 Fix layer tap interrupted 2023-03-16 21:50:42 +00:00
Alex Miller
878fe0deca
Adding ComboLayers to Layers Module (See #658) (#666)
---------

Co-authored-by: xs5871 <60395129+xs5871@users.noreply.github.com>
2023-03-10 20:40:57 +00:00
xs5871
adff02e88a Add deinit method to modules and extensions 2023-03-10 19:17:09 +00:00
xs5871
55b3a3a9b1 Move neopixel initialization into during_bootup 2023-03-10 19:16:44 +00:00
xs5871
3c796c16f8 Highlight support section of README 2023-03-09 21:19:28 +00:00
xs5871
b9c85c02e2 Remove modules and extension that fail during_bootup 2023-03-09 21:18:54 +00:00
xs5871
bff7584fe0 Create issue templates 2023-03-09 21:18:34 +00:00
xs5871
47fe859e11 Speed up unit tests 2023-03-09 21:18:18 +00:00
xs5871
fd700cff44 Resolve invalid keys to KC.NO instead of ValueError 2023-03-09 21:18:02 +00:00
35 changed files with 1145 additions and 364 deletions

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
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

@ -0,0 +1,20 @@
---
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,10 +9,15 @@ KMK is a feature-rich and beginner-friendly firmware for computer keyboards
written and configured in
[CircuitPython](https://github.com/adafruit/circuitpython).
## Support
For asynchronous support and chatter about KMK, [join our Zulip
community](https://kmkfw.zulipchat.com)! In particular, swing by the Zulip chat
*before* opening a GitHub Issue about configuration, documentation, etc.
concerns.
community](https://kmkfw.zulipchat.com)!
If you ask for help in chat or open a bug report, if possible
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
> longer officially supported, please do not use them!

View File

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

98
docs/en/combo_layers.md Normal file
View File

@ -0,0 +1,98 @@
## 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,6 +188,7 @@
| `KC.RESET` | Restarts the keyboard |
| `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.ANY` | Any key between `A and `/` |
| `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.UC_MODE_NOOP` | Sets UnicodeMode to NOOP |

View File

@ -33,6 +33,11 @@ Some helpful guidelines to keep in mind as you design your layers:
- 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
## 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
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
@ -40,6 +45,7 @@ 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
layers with `KC.DF()` the same way that you would treat using `KC.TO()`
## Example Code
For our example, let's take a simple 3x3 macropad with two layers as follows:

View File

@ -7,3 +7,6 @@ If you ask for help in chat or open a bug report, if possible
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
> longer officially supported, please do not use them!

View File

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

272
kmk/extensions/oled.py Normal file
View File

@ -0,0 +1,272 @@
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.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key
from kmk.kmktime import PeriodicTimer
from kmk.scheduler import create_task
from kmk.utils import Debug, clamp
debug = Debug(__name__)
@ -90,10 +90,10 @@ class RGB(Extension):
self,
pixel_pin,
num_pixels=0,
rgb_order=(1, 0, 2), # GRB WS2812
val_limit=255,
hue_default=0,
sat_default=255,
rgb_order=(1, 0, 2), # GRB WS2812
val_default=255,
hue_step=4,
sat_step=13,
@ -109,32 +109,9 @@ class RGB(Extension):
pixels=None,
refresh_rate=60,
):
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.pixel_pin = pixel_pin
self.num_pixels = num_pixels
# 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.rgb_order = rgb_order
self.hue_step = hue_step
self.sat_step = sat_step
self.val_step = val_step
@ -153,8 +130,11 @@ class RGB(Extension):
self.reverse_animation = reverse_animation
self.user_animation = user_animation
self.disable_auto_write = disable_auto_write
self.pixels = pixels
self.refresh_rate = refresh_rate
self.rgbw = bool(len(rgb_order) == 4)
self._substep = 0
make_key(
@ -227,7 +207,29 @@ class RGB(Extension):
return
def during_bootup(self, sandbox):
self._timer = PeriodicTimer(1000 // self.refresh_rate)
if self.pixels is None:
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):
return
@ -239,7 +241,7 @@ class RGB(Extension):
return
def after_hid_send(self, sandbox):
self.animate()
pass
def on_powersave_enable(self, sandbox):
return
@ -247,6 +249,10 @@ class RGB(Extension):
def on_powersave_disable(self, sandbox):
self._do_update()
def deinit(self, sandbox):
for pixel in self.pixels:
pixel.deinit()
def set_hsv(self, hue, sat, val, index):
'''
Takes HSV values and displays it on a single LED/Neopixel
@ -429,7 +435,7 @@ class RGB(Extension):
if self.animation_mode is AnimationModes.STATIC_STANDBY:
return
if self.enable and self._timer.tick():
if self.enable:
self._animation_step()
if self.animation_mode == AnimationModes.BREATHING:
self.effect_breathing()

View File

@ -137,3 +137,10 @@ def ble_disconnect(key, keyboard, *args, **kwargs):
keyboard._hid_helper.clear_bonds()
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,6 +370,7 @@ def maybe_make_firmware_key(candidate: str) -> Optional[Key]:
((('HID_SWITCH', 'HID'), handlers.hid_switch)),
((('RELOAD', 'RLD'), handlers.reload)),
((('RESET',), handlers.reset)),
((('ANY',), handlers.any_pressed)),
)
for names, handler in keys:
@ -475,7 +476,9 @@ class KeyAttrDict:
break
if not maybe_key:
raise ValueError(f'Invalid key: {name}')
if debug.enabled:
debug(f'Invalid key: {name}')
return KC.NO
if debug.enabled:
debug(f'{name}: {maybe_key}')

View File

@ -1,28 +1,33 @@
try:
from typing import Callable, Optional, Tuple
from typing import Callable, Optional
except ImportError:
pass
from supervisor import ticks_ms
from collections import namedtuple
from keypad import Event as KeyEvent
from kmk.consts import UnicodeMode
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
from kmk.keys import KC, Key
from kmk.kmktime import ticks_add, ticks_diff
from kmk.modules import Module
from kmk.scanners.keypad import MatrixScanner
from kmk.scheduler import Task, cancel_task, create_task, get_due_task
from kmk.utils import Debug
debug = Debug(__name__)
debug = Debug('kmk.keyboard')
KeyBufferFrame = namedtuple(
'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:
matrix_update = None
secondary_matrix_update = None
@ -59,7 +64,6 @@ class KMKKeyboard:
matrix_update = None
secondary_matrix_update = None
matrix_update_queue = []
state_changed = False
_trigger_powersave_enable = False
_trigger_powersave_disable = False
i2c_deinit_count = 0
@ -75,47 +79,24 @@ class KMKKeyboard:
_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:
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}')
return self.__class__.__name__
def _send_hid(self) -> None:
if not self._hid_send_enabled:
return
if self.axes and debug.enabled:
debug(f'axes={self.axes}')
if debug.enabled:
if self.keys_pressed:
debug('keys_pressed=', self.keys_pressed)
if self.axes:
debug('axes=', self.axes)
self._hid_helper.create_report(self.keys_pressed, self.axes)
try:
self._hid_helper.send()
except KeyError as e:
if debug.enabled:
debug(f'HidNotFound(HIDReportType={e})')
except Exception as err:
debug_error(self._hid_helper, 'send', err)
self.hid_pending = False
@ -125,35 +106,32 @@ class KMKKeyboard:
def _handle_matrix_report(self, kevent: KeyEvent) -> None:
if kevent is not None:
self._on_matrix_changed(kevent)
self.state_changed = True
def _find_key_in_map(self, int_coord: int) -> Key:
try:
idx = self.coord_mapping.index(int_coord)
except ValueError:
if debug.enabled:
debug(f'CoordMappingNotFound(ic={int_coord})')
debug('no such int_coord: ', int_coord)
return None
for layer in self.active_layers:
try:
layer_key = self.keymap[layer][idx]
key = self.keymap[layer][idx]
except IndexError:
layer_key = None
key = None
if debug.enabled:
debug(f'KeymapIndexError(idx={idx}, layer={layer})')
debug('keymap IndexError: idx=', idx, ' layer=', layer)
if not layer_key or layer_key == KC.TRNS:
if not key or key == KC.TRNS:
continue
return layer_key
return key
def _on_matrix_changed(self, kevent: KeyEvent) -> None:
int_coord = kevent.key_number
is_pressed = kevent.pressed
if debug.enabled:
debug(f'MatrixChange(ic={int_coord}, pressed={is_pressed})')
key = None
if not is_pressed:
@ -161,18 +139,16 @@ class KMKKeyboard:
key = self._coordkeys_pressed[int_coord]
except KeyError:
if debug.enabled:
debug(f'KeyNotPressed(ic={int_coord})')
debug('release w/o press: ', int_coord)
if key is None:
key = self._find_key_in_map(int_coord)
if key is None:
if debug.enabled:
debug(f'MatrixUndefinedCoordinate(ic={int_coord})')
return self
if key is None:
return
if debug.enabled:
debug(f'KeyResolution(key={key})')
debug(kevent, ': ', key)
self.pre_process_key(key, is_pressed, int_coord)
@ -197,7 +173,7 @@ class KMKKeyboard:
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():
if ksf.int_coord is not None:
key = self._find_key_in_map(ksf.int_coord)
# Resume the processing of the key event and update the HID report
@ -238,8 +214,7 @@ class KMKKeyboard:
if key is None:
break
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.process_key: {err}')
debug_error(module, 'process_key', err)
if int_coord is not None:
if is_pressed:
@ -249,18 +224,20 @@ class KMKKeyboard:
del self._coordkeys_pressed[int_coord]
except KeyError:
if debug.enabled:
debug(f'ReleaseKeyError(ic={int_coord})')
debug('release w/o press:', int_coord)
if debug.enabled:
debug('coordkeys_pressed=', self._coordkeys_pressed)
if key:
self.process_key(key, is_pressed, int_coord)
def process_key(
self, key: Key, is_pressed: bool, coord_int: Optional[int] = None
self, key: Key, is_pressed: bool, int_coord: Optional[int] = None
) -> None:
if is_pressed:
key.on_press(self, coord_int)
key.on_press(self, int_coord)
else:
key.on_release(self, coord_int)
key.on_release(self, int_coord)
def resume_process_key(
self,
@ -268,8 +245,9 @@ class KMKKeyboard:
key: Key,
is_pressed: bool,
int_coord: Optional[int] = None,
reprocess: Optional[bool] = False,
) -> None:
index = self.modules.index(module) + 1
index = self.modules.index(module) + (0 if reprocess else 1)
ksf = KeyBufferFrame(
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
)
@ -286,60 +264,17 @@ class KMKKeyboard:
def tap_key(self, keycode: Key) -> None:
self.add_key(keycode)
# On the next cycle, we'll remove the key.
self.set_timeout(False, lambda: self.remove_key(keycode))
self.set_timeout(0, lambda: self.remove_key(keycode))
def set_timeout(
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 set_timeout(self, after_ticks: int, callback: Callable[[None], None]) -> [Task]:
return create_task(callback, after_ms=after_ticks)
def cancel_timeout(self, timeout_key: int) -> None:
try:
self._timeouts[timeout_key[0]][timeout_key[1]] = None
except (KeyError, IndexError):
if debug.enabled:
debug(f'no such timeout: {timeout_key}')
cancel_task(timeout_key)
def _process_timeouts(self) -> None:
if not self._timeouts:
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
for task in get_due_task():
task()
def _init_sanity_check(self) -> None:
'''
@ -386,14 +321,15 @@ class KMKKeyboard:
self._hid_helper = self._hid_helper(**self._go_args)
self._hid_send_enabled = True
if debug.enabled:
debug('hid=', self._hid_helper)
def _deinit_hid(self) -> None:
self._hid_helper.clear_all()
self._hid_helper.send()
def _init_matrix(self) -> None:
if self.matrix is None:
if debug.enabled:
debug('Initialising default matrix scanner.')
self.matrix = MatrixScanner(
column_pins=self.col_pins,
row_pins=self.row_pins,
@ -409,96 +345,124 @@ class KMKKeyboard:
except TypeError:
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:
for module in self.modules:
try:
module.before_matrix_scan(self)
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.before_matrix_scan: {err}')
debug_error(module, 'before_matrix_scan', err)
for ext in self.extensions:
try:
ext.before_matrix_scan(self.sandbox)
except Exception as err:
if debug.enabled:
debug(f'Error in {ext}.before_matrix_scan: {err}')
debug_error(ext, 'before_matrix_scan', err)
def after_matrix_scan(self) -> None:
for module in self.modules:
try:
module.after_matrix_scan(self)
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.after_matrix_scan: {err}')
debug_error(module, 'after_matrix_scan', err)
for ext in self.extensions:
try:
ext.after_matrix_scan(self.sandbox)
except Exception as err:
if debug.enabled:
debug(f'Error in {ext}.after_matrix_scan: {err}')
debug_error(ext, 'after_matrix_scan', err)
def before_hid_send(self) -> None:
for module in self.modules:
try:
module.before_hid_send(self)
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.before_hid_send: {err}')
debug_error(module, 'before_hid_send', err)
for ext in self.extensions:
try:
ext.before_hid_send(self.sandbox)
except Exception as err:
if debug.enabled:
debug(
f'Error in {ext}.before_hid_send: {err}',
)
debug_error(ext, 'before_hid_send', err)
def after_hid_send(self) -> None:
for module in self.modules:
try:
module.after_hid_send(self)
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.after_hid_send: {err}')
debug_error(module, 'after_hid_send', err)
for ext in self.extensions:
try:
ext.after_hid_send(self.sandbox)
except Exception as err:
if debug.enabled:
debug(f'Error in {ext}.after_hid_send: {err}')
debug_error(ext, 'after_hid_send', err)
def powersave_enable(self) -> None:
for module in self.modules:
try:
module.on_powersave_enable(self)
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.on_powersave: {err}')
debug_error(module, 'powersave_enable', err)
for ext in self.extensions:
try:
ext.on_powersave_enable(self.sandbox)
except Exception as err:
if debug.enabled:
debug(f'Error in {ext}.powersave_enable: {err}')
debug_error(ext, 'powersave_enable', err)
def powersave_disable(self) -> None:
for module in self.modules:
try:
module.on_powersave_disable(self)
except Exception as err:
if debug.enabled:
debug(f'Error in {module}.powersave_disable: {err}')
debug_error(module, 'powersave_disable', err)
for ext in self.extensions:
try:
ext.on_powersave_disable(self.sandbox)
except Exception as err:
if debug.enabled:
debug(f'Error in {ext}.powersave_disable: {err}')
debug_error(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:
self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
@ -508,6 +472,7 @@ class KMKKeyboard:
finally:
debug('Unexpected error: cleaning up')
self._deinit_hid()
self.deinit()
def _init(
self,
@ -519,29 +484,22 @@ class KMKKeyboard:
self.hid_type = hid_type
self.secondary_hid_type = secondary_hid_type
self._init_sanity_check()
if debug.enabled:
debug('Initialising ', self)
debug('unicode_mode=', self.unicode_mode)
self._init_hid()
self._init_matrix()
self._init_coord_mapping()
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}')
self.during_bootup()
if debug.enabled:
debug(f'init: {self}')
import gc
gc.collect()
debug('mem_info used:', gc.mem_alloc(), ' free:', gc.mem_free())
def _main_loop(self) -> None:
self.state_changed = False
self.sandbox.active_layers = self.active_layers.copy()
self.before_matrix_scan()
@ -579,7 +537,6 @@ class KMKKeyboard:
if self.hid_pending:
self._send_hid()
self.state_changed = True
self.after_hid_send()
@ -588,6 +545,3 @@ class KMKKeyboard:
if self._trigger_powersave_disable:
self.powersave_disable()
if self.state_changed:
self._print_debug_cycle()

View File

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

View File

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

View File

@ -54,7 +54,7 @@ class HoldTap(Module):
def __init__(self):
self.key_buffer = []
self.key_states = {}
if not KC.get('HT'):
if KC.get('HT') == KC.NO:
make_argumented_key(
validator=HoldTapKeyMeta,
names=('HT',),
@ -83,6 +83,11 @@ class HoldTap(Module):
if state.activated != ActivationType.PRESSED:
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.
if (is_pressed and not key.meta.tap_interrupted) or (
not is_pressed and key.meta.tap_interrupted and self.key_buffer
@ -93,15 +98,12 @@ class HoldTap(Module):
self.ht_activate_on_interrupt(
key, keyboard, *state.args, **state.kwargs
)
append_buffer = True
send_buffer = True
# if interrupt on release: store interrupting keys until one of them
# is released.
if (
key.meta.tap_interrupted
and is_pressed
and not isinstance(current_key.meta, HoldTapKeyMeta)
):
if key.meta.tap_interrupted and is_pressed:
append_buffer = True
# apply changes with 'side-effects' on key_states or the loop behaviour
@ -110,10 +112,8 @@ class HoldTap(Module):
self.key_buffer.append((int_coord, current_key, is_pressed))
current_key = None
elif send_buffer:
if send_buffer:
self.send_key_buffer(keyboard)
keyboard.resume_process_key(self, current_key, is_pressed, int_coord)
current_key = None
return current_key
@ -221,8 +221,11 @@ class HoldTap(Module):
if not self.key_buffer:
return
reprocess = False
for (int_coord, key, is_pressed) in self.key_buffer:
keyboard.resume_process_key(self, key, is_pressed, int_coord)
keyboard.resume_process_key(self, key, is_pressed, int_coord, reprocess)
if isinstance(key.meta, HoldTapKeyMeta):
reprocess = True
self.key_buffer.clear()

View File

@ -36,9 +36,15 @@ class LayerKeyMeta:
class Layers(HoldTap):
'''Gives access to the keys used to enable the layer system'''
def __init__(self):
_active_combo = None
def __init__(
self,
combo_layers=None,
):
# Layers
super().__init__()
self.combo_layers = combo_layers
make_argumented_key(
validator=layer_key_validator,
names=('MO',),
@ -46,9 +52,7 @@ class Layers(HoldTap):
on_release=self._mo_released,
)
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(
validator=layer_key_validator,
@ -57,14 +61,10 @@ class Layers(HoldTap):
on_release=self._lm_released,
)
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(
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(
validator=layer_key_validator_lt,
@ -83,67 +83,102 @@ class Layers(HoldTap):
'''
Switches the default layer
'''
keyboard.active_layers[-1] = key.meta.layer
self._print_debug(keyboard)
self.activate_layer(keyboard, key.meta.layer, as_default=True)
def _mo_pressed(self, key, keyboard, *args, **kwargs):
'''
Momentarily activates layer, switches off when you let go
'''
keyboard.active_layers.insert(0, key.meta.layer)
self._print_debug(keyboard)
self.activate_layer(keyboard, key.meta.layer)
@staticmethod
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 _mo_released(self, key, keyboard, *args, **kwargs):
self.deactivate_layer(keyboard, key.meta.layer)
def _lm_pressed(self, key, keyboard, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
# Sets the timer start and acts like MO otherwise
keyboard.add_key(key.meta.kc)
self._mo_pressed(key, keyboard, *args, **kwargs)
keyboard.hid_pending = True
keyboard.keys_pressed.add(key.meta.kc)
self.activate_layer(keyboard, key.meta.layer)
def _lm_released(self, key, keyboard, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
keyboard.remove_key(key.meta.kc)
self._mo_released(key, keyboard, *args, **kwargs)
keyboard.hid_pending = True
keyboard.keys_pressed.discard(key.meta.kc)
self.deactivate_layer(keyboard, key.meta.layer)
def _tg_pressed(self, key, keyboard, *args, **kwargs):
'''
Toggles the layer (enables it if not active, and vise versa)
'''
# See mo_released for implementation details around this
try:
del_idx = keyboard.active_layers.index(key.meta.layer)
del keyboard.active_layers[del_idx]
except ValueError:
keyboard.active_layers.insert(0, key.meta.layer)
if key.meta.layer in keyboard.active_layers:
self.deactivate_layer(keyboard, key.meta.layer)
else:
self.activate_layer(keyboard, key.meta.layer)
def _to_pressed(self, key, keyboard, *args, **kwargs):
'''
Activates layer and deactivates all other layers
'''
self._active_combo = None
keyboard.active_layers.clear()
keyboard.active_layers.insert(0, key.meta.layer)
def _print_debug(self, keyboard):
# debug(f'__getitem__ {key}')
if debug.enabled:
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:
if is_pressed:
send_buffer = True
self.key_buffer.insert(0, (0, key, False))
self.key_buffer.insert(0, (None, key, False))
if send_buffer:
self.key_buffer.append((int_coord, current_key, is_pressed))

View File

@ -42,12 +42,11 @@ class Phrase:
self._characters: list[Character] = []
self._index: int = 0
for char in string:
try:
key_code = KC[char]
shifted = char.isupper() or key_code.has_modifiers == {2}
self._characters.append(Character(key_code, shifted))
except ValueError:
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}
self._characters.append(Character(key_code, shifted))
def next_character(self) -> None:
'''Increment the current index for this phrase'''

View File

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

67
kmk/scheduler.py Normal file
View File

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

View File

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

View File

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

196
tests/task.py Normal file
View File

@ -0,0 +1,196 @@
# 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):
def setUp(self):
self.kb = KeyboardTest(
[CapsWord()],
[CapsWord(timeout=2 * KeyboardTest.loop_delay_ms)],
[
[KC.CW, KC.A, KC.Z, KC.N1, KC.N0, KC.SPC],
],

View File

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

View File

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

View File

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

View File

@ -10,8 +10,13 @@ class TestLayers(unittest.TestCase):
self.kb = KeyboardTest(
[Layers()],
[
[KC.N0, KC.LM(1, KC.LCTL)],
[KC.A, KC.B],
[
KC.N0,
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,
)
@ -23,6 +28,25 @@ class TestLayers(unittest.TestCase):
[{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__':
unittest.main()

View File

@ -6,25 +6,30 @@ from kmk.modules.oneshot import OneShot
from tests.keyboard_test import KeyboardTest
class TestHoldTap(unittest.TestCase):
class TestOneshot(unittest.TestCase):
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(
[Layers(), OneShot()],
[
[
KC.OS(KC.MO(1), tap_time=50),
KC.OS(KC.MO(1)),
KC.MO(1),
KC.C,
KC.D,
KC.OS(KC.E, tap_time=50),
KC.OS(KC.F, tap_time=50),
KC.OS(KC.E),
KC.OS(KC.F),
],
[KC.N0, KC.N1, KC.N2, KC.N3, KC.OS(KC.LSFT, tap_time=50), KC.TRNS],
[KC.N0, KC.N1, KC.N2, KC.N3, KC.OS(KC.LSFT), KC.TRNS],
],
debug_enabled=False,
)
t_within = 40
t_after = 60
keyboard.test(
'OS timed out',

View File

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

View File

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

View File

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