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
31 changed files with 365 additions and 790 deletions

View File

@@ -1,27 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
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]"
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
[CircuitPython](https://github.com/adafruit/circuitpython).
## Support
For asynchronous support and chatter about KMK, [join our Zulip
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.
community](https://kmkfw.zulipchat.com)! 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,6 +20,8 @@ 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,

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

@@ -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
- 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
@@ -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
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,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.
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!

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):
raise NotImplementedError
def deinit(self, keyboard):
pass

View File

@@ -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,9 +109,32 @@ class RGB(Extension):
pixels=None,
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.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.sat_step = sat_step
self.val_step = val_step
@@ -130,11 +153,8 @@ 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(
@@ -207,28 +227,6 @@ class RGB(Extension):
return
def during_bootup(self, sandbox):
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._timer = PeriodicTimer(1000 // self.refresh_rate)
def before_matrix_scan(self, sandbox):
@@ -249,10 +247,6 @@ 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

View File

@@ -475,9 +475,7 @@ class KeyAttrDict:
break
if not maybe_key:
if debug.enabled:
debug(f'Invalid key: {name}')
return KC.NO
raise ValueError(f'Invalid key: {name}')
if debug.enabled:
debug(f'{name}: {maybe_key}')

View File

@@ -16,20 +16,13 @@ from kmk.modules import Module
from kmk.scanners.keypad import MatrixScanner
from kmk.utils import Debug
debug = Debug('kmk.keyboard')
debug = Debug(__name__)
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
@@ -66,6 +59,7 @@ 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
@@ -81,24 +75,47 @@ 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 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:
if not self._hid_send_enabled:
return
if debug.enabled:
if self.keys_pressed:
debug('keys_pressed=', self.keys_pressed)
if self.axes:
debug('axes=', self.axes)
if self.axes and debug.enabled:
debug(f'axes={self.axes}')
self._hid_helper.create_report(self.keys_pressed, self.axes)
try:
self._hid_helper.send()
except Exception as err:
debug_error(self._hid_helper, 'send', err)
except KeyError as e:
if debug.enabled:
debug(f'HidNotFound(HIDReportType={e})')
self.hid_pending = False
@@ -108,32 +125,35 @@ 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('no such int_coord: ', int_coord)
debug(f'CoordMappingNotFound(ic={int_coord})')
return None
for layer in self.active_layers:
try:
key = self.keymap[layer][idx]
layer_key = self.keymap[layer][idx]
except IndexError:
key = None
layer_key = None
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
return key
return layer_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:
@@ -141,16 +161,18 @@ class KMKKeyboard:
key = self._coordkeys_pressed[int_coord]
except KeyError:
if debug.enabled:
debug('release w/o press: ', int_coord)
debug(f'KeyNotPressed(ic={int_coord})')
if key is None:
key = self._find_key_in_map(int_coord)
if key is None:
return
if key is None:
if debug.enabled:
debug(f'MatrixUndefinedCoordinate(ic={int_coord})')
return self
if debug.enabled:
debug(kevent, ': ', key)
debug(f'KeyResolution(key={key})')
self.pre_process_key(key, is_pressed, int_coord)
@@ -175,7 +197,7 @@ class KMKKeyboard:
key = ksf.key
# 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)
# Resume the processing of the key event and update the HID report
@@ -216,7 +238,8 @@ class KMKKeyboard:
if key is None:
break
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 is_pressed:
@@ -226,20 +249,18 @@ class KMKKeyboard:
del self._coordkeys_pressed[int_coord]
except KeyError:
if debug.enabled:
debug('release w/o press:', int_coord)
if debug.enabled:
debug('coordkeys_pressed=', self._coordkeys_pressed)
debug(f'ReleaseKeyError(ic={int_coord})')
if key:
self.process_key(key, is_pressed, int_coord)
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:
if is_pressed:
key.on_press(self, int_coord)
key.on_press(self, coord_int)
else:
key.on_release(self, int_coord)
key.on_release(self, coord_int)
def resume_process_key(
self,
@@ -247,9 +268,8 @@ class KMKKeyboard:
key: Key,
is_pressed: bool,
int_coord: Optional[int] = None,
reprocess: Optional[bool] = False,
) -> None:
index = self.modules.index(module) + (0 if reprocess else 1)
index = self.modules.index(module) + 1
ksf = KeyBufferFrame(
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
)
@@ -366,15 +386,14 @@ 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,
@@ -390,124 +409,96 @@ 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:
debug_error(module, 'before_matrix_scan', err)
if debug.enabled:
debug(f'Error in {module}.before_matrix_scan: {err}')
for ext in self.extensions:
try:
ext.before_matrix_scan(self.sandbox)
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:
for module in self.modules:
try:
module.after_matrix_scan(self)
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:
try:
ext.after_matrix_scan(self.sandbox)
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:
for module in self.modules:
try:
module.before_hid_send(self)
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:
try:
ext.before_hid_send(self.sandbox)
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:
for module in self.modules:
try:
module.after_hid_send(self)
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:
try:
ext.after_hid_send(self.sandbox)
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:
for module in self.modules:
try:
module.on_powersave_enable(self)
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:
try:
ext.on_powersave_enable(self.sandbox)
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:
for module in self.modules:
try:
module.on_powersave_disable(self)
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:
try:
ext.on_powersave_disable(self.sandbox)
except Exception as 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)
if debug.enabled:
debug(f'Error in {ext}.powersave_disable: {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)
@@ -517,7 +508,6 @@ class KMKKeyboard:
finally:
debug('Unexpected error: cleaning up')
self._deinit_hid()
self.deinit()
def _init(
self,
@@ -529,22 +519,29 @@ class KMKKeyboard:
self.hid_type = hid_type
self.secondary_hid_type = secondary_hid_type
if debug.enabled:
debug('Initialising ', self)
debug('unicode_mode=', self.unicode_mode)
self._init_sanity_check()
self._init_hid()
self._init_matrix()
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:
import gc
gc.collect()
debug('mem_info used:', gc.mem_alloc(), ' free:', gc.mem_free())
debug(f'init: {self}')
def _main_loop(self) -> None:
self.state_changed = False
self.sandbox.active_layers = self.active_layers.copy()
self.before_matrix_scan()
@@ -582,6 +579,7 @@ class KMKKeyboard:
if self.hid_pending:
self._send_hid()
self.state_changed = True
self.after_hid_send()
@@ -590,3 +588,6 @@ class KMKKeyboard:
if self._trigger_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):
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.kmk_keyboard import KMKKeyboard
from kmk.modules import Module
from kmk.utils import Debug
debug = Debug(__name__)
class _ComboState:
@@ -217,7 +214,7 @@ class Combos(Module):
combo.insert(key, int_coord)
combo._state = _ComboState.MATCHING
key = None
key = combo.result
break
else:
@@ -304,14 +301,10 @@ 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

@@ -1,212 +0,0 @@
try:
from typing import Callable
except ImportError:
pass
from micropython import const
from kmk.keys import AX
from kmk.modules import Module
from kmk.utils import Debug
debug = Debug(__name__)
_ADDRESS = const(0x2A)
_MASK_READ = const(0xA0)
_MASK_WRITE = const(0x80)
_REG_FW_ID = const(0x00)
_REG_FW_VER = const(0x01)
_REG_STATUS = const(0x02)
_REG_SYS_CFG = const(0x03)
_REG_FEED_CFG1 = const(0x04)
_REG_FEED_CFG2 = const(0x05)
_REG_CAL_CFG = const(0x07)
_REG_AUX_CTL = const(0x08)
_REG_SAMPLE_RATE = const(0x09)
_REG_ZIDLE = const(0x0A)
_REG_Z_SCALER = const(0x0B)
_REG_SLEEP_INTERVAL = const(0x0C)
_REG_SLEEP_TIMER = const(0x0D)
_REG_DATA = const(0x12)
_FEED1_ENABLE = const(0x01)
_FEED1_ABSOLUTE = const(0x02)
_FEED1_NO_FILTER = const(0x04)
_FEED1_NO_X_DATA = const(0x08)
_FEED1_NO_Y_DATA = const(0x10)
_FEED1_INVERT_X = const(0x40)
_FEED1_INVERT_Y = const(0x80)
_FEED2_INTELLIMOUSE = const(0x01)
_FEED2_NO_TAPS = const(0x02)
_FEED2_NO_SEC_TAPS = const(0x04)
_FEED2_NO_SCROLL = const(0x08)
_FEED2_NO_GLIDEEXTEND = const(0x10)
_FEED2_SWAP_XY = const(0x80)
def with_i2c_lock(rw: Callable[[object, int], None]) -> Callable[[object, int], None]:
def _(self, n: int) -> None:
if not self._i2c.try_lock():
if debug.enabled:
debug("can't acquire lock")
return
try:
rw(self, n)
finally:
self._i2c.unlock()
return _
class AbsoluteHandler:
cfg = (
_REG_FEED_CFG2,
0x00,
_REG_FEED_CFG1,
_FEED1_ENABLE | _FEED1_NO_FILTER | _FEED1_ABSOLUTE | _FEED1_INVERT_X,
)
def handle(buffer: bytearray, keyboard) -> None:
button = buffer[0]
x_low = buffer[2]
y_low = buffer[3]
high = buffer[4]
z_lvl = buffer[5]
x_pos = ((high & 0x0F) << 8) | x_low
y_pos = ((high & 0xF0) << 4) | y_low
if debug.enabled:
debug(
'buttons:',
bin(button),
' x_pos:',
x_pos,
' y_pos:',
y_pos,
'z_lvl:',
z_lvl,
)
class RelativeHandler:
cfg = (
_REG_FEED_CFG2,
0x00,
_REG_FEED_CFG1,
_FEED1_ENABLE | _FEED1_NO_FILTER | _FEED1_INVERT_X,
)
def handle(buffer: bytearray, keyboard) -> None:
button = buffer[0] & 0b00000111
x_sign = buffer[0] & 0b00010000
y_sign = buffer[0] & 0b00100000
x_delta = buffer[1]
y_delta = buffer[2]
w_delta = buffer[3]
if x_sign:
x_delta -= 0xFF
if y_sign:
y_delta -= 0xFF
if x_delta != 0:
AX.X.move(keyboard, x_delta)
if y_delta != 0:
AX.Y.move(keyboard, y_delta)
if debug.enabled:
debug(
'buttons:',
bin(button),
' x_delta:',
x_delta,
' y_delta:',
y_delta,
' w_delta',
w_delta,
)
class GlidePoint(Module):
def __init__(self, i2c):
self._i2c = i2c
self._buffer = bytearray(8)
# self.handler = RelativeHandler
self.handler = AbsoluteHandler
@with_i2c_lock
def _read(self, n: int) -> None:
self._buffer[0] |= _MASK_READ
self._i2c.writeto_then_readfrom(
_ADDRESS, self._buffer, self._buffer, out_end=1, in_end=n
)
@with_i2c_lock
def _write(self, n: int) -> None:
for i in range(0, n, 2):
self._buffer[i] |= _MASK_WRITE
self._i2c.writeto(_ADDRESS, self._buffer, end=n)
def _check_firmware(self) -> bool:
self._buffer[0] = _REG_FW_ID
self._read(2)
return self._buffer[:2] == b'\x07:'
def _clear_status_flags(self) -> None:
self._buffer[0] = _REG_STATUS
self._buffer[1] = 0x00
self._write(2)
def _configure(self) -> None:
self._buffer[0] = _REG_SYS_CFG
self._buffer[1] = 0x00
for idx, val in enumerate(self.handler.cfg):
self._buffer[idx + 2] = val
self._write(2 + len(self.handler.cfg))
def _data_ready(self) -> bool:
self._buffer[0] = _REG_STATUS
self._read(1)
return self._buffer[0] != 0x00
def during_bootup(self, keyboard):
if not self._check_firmware:
raise OSError('Firmware ID mismatch')
self._clear_status_flags()
self._configure()
def before_matrix_scan(self, keyboard):
pass
def after_matrix_scan(self, keyboard):
if not self._data_ready():
return
self._buffer[0] = _REG_DATA
self._read(6)
self.handler.handle(self._buffer, keyboard)
self._clear_status_flags()
def before_hid_send(self, keyboard):
pass
def after_hid_send(self, keyboard):
pass
def on_powersave_enable(self, keyboard):
self._buffer[0] = _REG_SYS_CFG
self._buffer[1] = 0x04
self._write(2)
def on_powersave_disable(self, keyboard):
self._buffer[0] = _REG_SYS_CFG
self._buffer[1] = 0x00
self._write(2)

View File

@@ -54,7 +54,7 @@ class HoldTap(Module):
def __init__(self):
self.key_buffer = []
self.key_states = {}
if KC.get('HT') == KC.NO:
if not KC.get('HT'):
make_argumented_key(
validator=HoldTapKeyMeta,
names=('HT',),
@@ -83,11 +83,6 @@ 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
@@ -98,12 +93,15 @@ 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:
if (
key.meta.tap_interrupted
and is_pressed
and not isinstance(current_key.meta, HoldTapKeyMeta)
):
append_buffer = True
# 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))
current_key = None
if send_buffer:
elif 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,11 +221,8 @@ 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, reprocess)
if isinstance(key.meta, HoldTapKeyMeta):
reprocess = True
keyboard.resume_process_key(self, key, is_pressed, int_coord)
self.key_buffer.clear()

View File

@@ -36,15 +36,9 @@ class LayerKeyMeta:
class Layers(HoldTap):
'''Gives access to the keys used to enable the layer system'''
_active_combo = None
def __init__(
self,
combo_layers=None,
):
def __init__(self):
# Layers
super().__init__()
self.combo_layers = combo_layers
make_argumented_key(
validator=layer_key_validator,
names=('MO',),
@@ -52,7 +46,9 @@ 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,
@@ -61,10 +57,14 @@ 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,102 +83,67 @@ class Layers(HoldTap):
'''
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):
'''
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):
self.deactivate_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 _lm_pressed(self, key, keyboard, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
keyboard.hid_pending = True
keyboard.keys_pressed.add(key.meta.kc)
self.activate_layer(keyboard, key.meta.layer)
# Sets the timer start and acts like MO otherwise
keyboard.add_key(key.meta.kc)
self._mo_pressed(key, keyboard, *args, **kwargs)
def _lm_released(self, key, keyboard, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
keyboard.hid_pending = True
keyboard.keys_pressed.discard(key.meta.kc)
self.deactivate_layer(keyboard, key.meta.layer)
keyboard.remove_key(key.meta.kc)
self._mo_released(key, keyboard, *args, **kwargs)
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
if key.meta.layer in keyboard.active_layers:
self.deactivate_layer(keyboard, key.meta.layer)
else:
self.activate_layer(keyboard, key.meta.layer)
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)
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, (None, key, False))
self.key_buffer.insert(0, (0, key, False))
if send_buffer:
self.key_buffer.append((int_coord, current_key, is_pressed))

View File

@@ -42,11 +42,12 @@ class Phrase:
self._characters: list[Character] = []
self._index: int = 0
for char in string:
key_code = KC[char]
if key_code == KC.NO:
try:
key_code = KC[char]
shifted = char.isupper() or key_code.has_modifiers == {2}
self._characters.append(Character(key_code, shifted))
except ValueError:
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=True,
prefer_hold=False,
tap_interrupted=False,
tap_time=self.tap_time,
)

View File

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

View File

@@ -23,8 +23,6 @@ def code2name(code):
class KeyboardTest:
loop_delay_ms = 2
def __init__(
self,
modules,
@@ -130,4 +128,4 @@ class KeyboardTest:
def do_main_loop(self):
self.keyboard._main_loop()
time.sleep(self.loop_delay_ms / 1000)
time.sleep(0.002)

View File

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

View File

@@ -1,36 +1,28 @@
import unittest
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 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=2 * timeout),
Chord((KC.C, KC.D), KC.Z, timeout=80),
Chord((KC.C, KCMO), KC.Z),
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),
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),
]
self.keyboard = KeyboardTest(
[combos, layers],
@@ -41,6 +33,9 @@ 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,37 +10,20 @@ 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), t_within, (0, False)], [{KC.A}, {}]
)
keyboard.test('HT tap behaviour', [(0, True), 100, (0, False)], [{KC.A}, {}])
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
@@ -48,74 +31,74 @@ class TestHoldTap(unittest.TestCase):
# HT
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
# LT
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
keyboard.test(
'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}, {}],
)
@@ -123,9 +106,9 @@ class TestHoldTap(unittest.TestCase):
'LT after tap time nested -> hold behavior',
[
(0, True),
t_after,
350,
(1, True),
t_after,
350,
(3, True),
(3, False),
(1, False),
@@ -135,25 +118,26 @@ 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),
KC.HT(KC.N2, KC.LSFT, tap_interrupted=True),
KC.HT(KC.N1, KC.LCTL, tap_time=50),
KC.HT(KC.N2, KC.LSFT, tap_interrupted=True, tap_time=50),
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',
@@ -172,7 +156,7 @@ class TestHoldTap(unittest.TestCase):
'chained 1',
[(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.N0},
{KC.LCTL, KC.LSFT},
@@ -224,7 +208,7 @@ class TestHoldTap(unittest.TestCase):
'chained 5',
[(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.N0},
{KC.LCTL, KC.N3},
@@ -291,21 +275,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),
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.TAP),
KC.HT(KC.A, KC.B, repeat=HoldTapRepeat.HOLD),
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),
]
],
debug_enabled=False,
)
t_within = 40
t_after = 60
keyboard.test(
'repeat tap',
[
@@ -317,6 +301,7 @@ class TestHoldTap(unittest.TestCase):
(0, False),
(0, True),
(0, False),
t_after,
],
[{KC.A}, {}, {KC.A}, {}, {KC.A}, {}],
)
@@ -332,6 +317,7 @@ class TestHoldTap(unittest.TestCase):
(0, False),
(0, True),
(0, False),
t_after,
],
[{KC.B}, {}, {KC.B}, {}, {KC.B}, {}],
)
@@ -348,6 +334,7 @@ class TestHoldTap(unittest.TestCase):
t_after,
(0, True),
(0, False),
t_after,
],
[{KC.A}, {}, {KC.B}, {}, {KC.A}, {}],
)

View File

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

View File

@@ -10,13 +10,8 @@ class TestLayers(unittest.TestCase):
self.kb = KeyboardTest(
[Layers()],
[
[
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],
[KC.N0, KC.LM(1, KC.LCTL)],
[KC.A, KC.B],
],
debug_enabled=False,
)
@@ -28,25 +23,6 @@ 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,30 +6,25 @@ from kmk.modules.oneshot import OneShot
from tests.keyboard_test import KeyboardTest
class TestOneshot(unittest.TestCase):
class TestHoldTap(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)),
KC.OS(KC.MO(1), tap_time=50),
KC.MO(1),
KC.C,
KC.D,
KC.OS(KC.E),
KC.OS(KC.F),
KC.OS(KC.E, tap_time=50),
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,
)
t_within = 40
t_after = 60
keyboard.test(
'OS timed out',

View File

@@ -37,8 +37,11 @@ class TestStickyMod(unittest.TestCase):
[
(4, True),
(4, False),
100,
(4, True),
200,
(4, False),
100,
(1, True),
(1, False),
],
@@ -58,19 +61,26 @@ 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,7 +7,6 @@ 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 = {
@@ -47,21 +46,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), self.delay],
[(0, True), (0, False), (0, True), (0, False), 50],
[{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), self.delay],
[(1, True), (1, False), 50],
[{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), self.delay],
[(3, True), (2, True), (2, False), (3, False), 50],
[{KC.LSHIFT}, {KC.LSHIFT, KC.N2}, {}],
)
self.keyboard.test(
@@ -75,7 +74,7 @@ class TestStringSubstitution(unittest.TestCase):
(5, False),
(5, True),
(5, False),
self.delay,
10,
],
[
{KC.D},
@@ -94,7 +93,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)],
[(4, True), (0, True), (0, False), (0, True), (0, False), (4, False), 50],
[
{KC.LCTRL},
{KC.LCTRL, KC.A},
@@ -106,7 +105,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)],
[(4, True), (1, True), (1, False), (4, False), 50],
[
{KC.LCTRL},
{KC.LCTRL, KC.B},
@@ -116,7 +115,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)],
[(0, True), (0, False), (4, True), (0, True), (0, False), (4, False), 50],
[
{KC.A},
{},
@@ -129,15 +128,7 @@ 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),
self.delay,
],
[(5, True), (5, False), (0, True), (0, False), (0, True), (0, False), 50],
[
{KC.C},
{},
@@ -151,15 +142,7 @@ 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),
self.delay,
],
[(0, True), (0, False), (0, True), (0, False), (5, True), (5, False), 50],
[
{KC.A},
{},
@@ -173,7 +156,7 @@ class TestStringSubstitution(unittest.TestCase):
)
self.keyboard.test(
'no match',
[(0, True), (0, False), (2, True), (2, False)],
[(0, True), (0, False), (2, True), (2, False), 50],
[
{KC.A},
{},
@@ -200,7 +183,7 @@ class TestStringSubstitution(unittest.TestCase):
(6, False),
(0, True),
(0, False),
10 * self.delay,
50,
],
[
{KC.D},
@@ -258,7 +241,7 @@ class TestStringSubstitution(unittest.TestCase):
# send the unreachable match "cccc" after matching "ccc"
(5, True),
(5, False),
self.delay,
10,
],
[
{KC.C},
@@ -289,7 +272,7 @@ class TestStringSubstitution(unittest.TestCase):
(0, True),
(0, False),
(7, False),
self.delay,
10,
],
[
{KC.RSHIFT},
@@ -320,6 +303,7 @@ class TestStringSubstitution(unittest.TestCase):
(0, False),
(4, False),
(8, False),
10,
],
[
{KC.RALT},
@@ -341,6 +325,7 @@ class TestStringSubstitution(unittest.TestCase):
(1, False),
(3, False),
(8, False),
10,
],
[
{KC.RALT},

View File

@@ -9,39 +9,36 @@ 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),
KC.TD(KC.N0, KC.N1, tap_time=50),
KC.TD(
KC.HT(KC.N1, KC.A),
KC.HT(KC.N2, KC.B, tap_time=2 * tap_time),
KC.HT(KC.N1, KC.A, tap_time=50),
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.LT(1, KC.N3), 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, tap_time=50), 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)], [{KC.N0}, {}])
keyboard.test('Tap x1', [(0, True), (0, False), t_after], [{KC.N0}, {}])
keyboard.test(
'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}, {}],
)
@@ -54,6 +51,7 @@ class TestTapDance(unittest.TestCase):
(0, False),
(0, True),
(0, False),
t_after,
],
[{KC.N1}, {}, {KC.N0}, {}],
)
@@ -95,11 +93,11 @@ class TestTapDance(unittest.TestCase):
t_within = self.t_within
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(
'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}, {}],
)
@@ -133,7 +131,7 @@ class TestTapDance(unittest.TestCase):
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}, {}],
)
@@ -147,6 +145,7 @@ class TestTapDance(unittest.TestCase):
(2, False),
t_after,
(0, False),
t_after,
],
[{KC.N1}, {KC.N1, KC.X}, {KC.N1}, {}],
)
@@ -161,6 +160,7 @@ 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)],
[(3, True), (3, False), t_within, (1, True), (1, False), t_after],
[{KC.N3}, {}, {KC.N1}, {}],
)