initial repo setup which includes current release of kmk

This commit is contained in:
KemoNine 2023-04-19 08:30:30 -04:00
commit 69f2363278
60 changed files with 8594 additions and 0 deletions

0
kmk/__init__.py Normal file
View file

8
kmk/consts.py Normal file
View file

@ -0,0 +1,8 @@
from micropython import const
class UnicodeMode:
NOOP = const(0)
LINUX = IBUS = const(1)
MACOS = OSX = RALT = const(2)
WINC = const(3)

View file

@ -0,0 +1,54 @@
class InvalidExtensionEnvironment(Exception):
pass
class Extension:
_enabled = True
def enable(self, keyboard):
self._enabled = True
self.on_runtime_enable(keyboard)
def disable(self, keyboard):
self._enabled = False
self.on_runtime_disable(keyboard)
# The below methods should be implemented by subclasses
def on_runtime_enable(self, keyboard):
raise NotImplementedError
def on_runtime_disable(self, keyboard):
raise NotImplementedError
def during_bootup(self, keyboard):
raise NotImplementedError
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
raise NotImplementedError
def after_matrix_scan(self, keyboard):
'''
Return value will be replace matrix update if supplied
'''
raise NotImplementedError
def before_hid_send(self, keyboard):
raise NotImplementedError
def after_hid_send(self, keyboard):
raise NotImplementedError
def on_powersave_enable(self, keyboard):
raise NotImplementedError
def on_powersave_disable(self, keyboard):
raise NotImplementedError
def deinit(self, keyboard):
pass

View file

@ -0,0 +1,59 @@
'''Adds international keys'''
from kmk.extensions import Extension
from kmk.keys import make_key
class International(Extension):
'''Adds international keys'''
def __init__(self):
# International
make_key(code=50, names=('NONUS_HASH', 'NUHS'))
make_key(code=100, names=('NONUS_BSLASH', 'NUBS'))
make_key(code=101, names=('APP', 'APPLICATION', 'SEL', 'WINMENU'))
make_key(code=135, names=('INT1', 'RO'))
make_key(code=136, names=('INT2', 'KANA'))
make_key(code=137, names=('INT3', 'JYEN'))
make_key(code=138, names=('INT4', 'HENK'))
make_key(code=139, names=('INT5', 'MHEN'))
make_key(code=140, names=('INT6',))
make_key(code=141, names=('INT7',))
make_key(code=142, names=('INT8',))
make_key(code=143, names=('INT9',))
make_key(code=144, names=('LANG1', 'HAEN'))
make_key(code=145, names=('LANG2', 'HAEJ'))
make_key(code=146, names=('LANG3',))
make_key(code=147, names=('LANG4',))
make_key(code=148, names=('LANG5',))
make_key(code=149, names=('LANG6',))
make_key(code=150, names=('LANG7',))
make_key(code=151, names=('LANG8',))
make_key(code=152, names=('LANG9',))
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return

View file

@ -0,0 +1,34 @@
# What's this?
# This is a keycode conversion script. With this, KMK will work as a JIS keyboard.
# Usage
# ```python
# import kmk.extensions.keymap_extras.keymap_jp
# ```
from kmk.keys import KC
KC.CIRC = KC.EQL # ^
KC.AT = KC.LBRC # @
KC.LBRC = KC.RBRC # [
KC.EISU = KC.CAPS # Eisū (英数)
KC.COLN = KC.QUOT # :
KC.LCBR = KC.LSFT(KC.RBRC) # {
KC.RBRC = KC.NUHS # ]
KC.BSLS = KC.INT1 # (backslash)
KC.PLUS = KC.LSFT(KC.SCLN)
KC.TILD = KC.LSFT(KC.EQL) # ~
KC.GRV = KC.LSFT(KC.AT) # `
KC.DQUO = KC.LSFT(KC.N2) # "
KC.AMPR = KC.LSFT(KC.N6) # &
KC.ASTR = KC.LSFT(KC.QUOT) # *
KC.QUOT = KC.LSFT(KC.N7) # '
KC.LPRN = KC.LSFT(KC.N8) # (
KC.RPRN = KC.LSFT(KC.N9) # )
KC.EQL = KC.LSFT(KC.MINS) # =
KC.PIPE = KC.LSFT(KC.INT3) # |
KC.RCBR = KC.LSFT(KC.NUHS) # }
KC.LABK = KC.LSFT(KC.COMM) # <
KC.RABK = KC.LSFT(KC.DOT) # >
KC.QUES = KC.LSFT(KC.SLSH) # ?
KC.UNDS = KC.LSFT(KC.INT1) # _

259
kmk/extensions/led.py Normal file
View file

@ -0,0 +1,259 @@
import pwmio
from math import e, exp, pi, sin
from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.keys import make_argumented_key, make_key
from kmk.utils import clamp
class LEDKeyMeta:
def __init__(self, *leds):
self.leds = leds
self.brightness = None
class AnimationModes:
OFF = 0
STATIC = 1
STATIC_STANDBY = 2
BREATHING = 3
USER = 4
class LED(Extension):
def __init__(
self,
led_pin,
brightness=50,
brightness_step=5,
brightness_limit=100,
breathe_center=1.5,
animation_mode=AnimationModes.STATIC,
animation_speed=1,
user_animation=None,
val=100,
):
try:
pins_iter = iter(led_pin)
except TypeError:
pins_iter = [led_pin]
try:
self._leds = [pwmio.PWMOut(pin) for pin in pins_iter]
except Exception as e:
print(e)
raise InvalidExtensionEnvironment(
'Unable to create pwmio.PWMOut() instance with provided led_pin'
)
self._brightness = brightness
self._pos = 0
self._effect_init = False
self._enabled = True
self.brightness_step = brightness_step
self.brightness_limit = brightness_limit
self.animation_mode = animation_mode
self.animation_speed = animation_speed
self.breathe_center = breathe_center
self.val = val
if user_animation is not None:
self.user_animation = user_animation
make_argumented_key(
names=('LED_TOG',),
validator=self._led_key_validator,
on_press=self._key_led_tog,
)
make_argumented_key(
names=('LED_INC',),
validator=self._led_key_validator,
on_press=self._key_led_inc,
)
make_argumented_key(
names=('LED_DEC',),
validator=self._led_key_validator,
on_press=self._key_led_dec,
)
make_argumented_key(
names=('LED_SET',),
validator=self._led_set_key_validator,
on_press=self._key_led_set,
)
make_key(names=('LED_ANI',), on_press=self._key_led_ani)
make_key(names=('LED_AND',), on_press=self._key_led_and)
make_key(
names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static
)
make_key(
names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe
)
def __repr__(self):
return f'LED({self._to_dict()})'
def _to_dict(self):
return {
'_brightness': self._brightness,
'_pos': self._pos,
'brightness_step': self.brightness_step,
'brightness_limit': self.brightness_limit,
'animation_mode': self.animation_mode,
'animation_speed': self.animation_speed,
'breathe_center': self.breathe_center,
'val': self.val,
}
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
self.animate()
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return
def _init_effect(self):
self._pos = 0
self._effect_init = False
return self
def set_brightness(self, percent, leds=None):
leds = leds or range(0, len(self._leds))
for i in leds:
self._leds[i].duty_cycle = int(percent / 100 * 65535)
def step_brightness(self, step, leds=None):
leds = leds or range(0, len(self._leds))
for i in leds:
brightness = int(self._leds[i].duty_cycle / 65535 * 100) + step
self.set_brightness(clamp(brightness), [i])
def increase_brightness(self, step=None, leds=None):
if step is None:
step = self.brightness_step
self.step_brightness(step, leds)
def decrease_brightness(self, step=None, leds=None):
if step is None:
step = self.brightness_step
self.step_brightness(-step, leds)
def off(self):
self.set_brightness(0)
def increase_ani(self):
'''
Increases animation speed by 1 amount stopping at 10
:param step:
'''
if (self.animation_speed + 1) >= 10:
self.animation_speed = 10
else:
self.val += 1
def decrease_ani(self):
'''
Decreases animation speed by 1 amount stopping at 0
:param step:
'''
if (self.val - 1) <= 0:
self.val = 0
else:
self.val -= 1
def effect_breathing(self):
# http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
# https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
sined = sin((self._pos / 255.0) * pi)
multip_1 = exp(sined) - self.breathe_center / e
multip_2 = self.brightness_limit / (e - 1 / e)
self._brightness = int(multip_1 * multip_2)
self._pos = (self._pos + self.animation_speed) % 256
self.set_brightness(self._brightness)
def effect_static(self):
self.set_brightness(self._brightness)
# Set animation mode to standby to prevent cycles from being wasted
self.animation_mode = AnimationModes.STATIC_STANDBY
def animate(self):
'''
Activates a "step" in the animation based on the active mode
:return: Returns the new state in animation
'''
if self._effect_init:
self._init_effect()
if self._enabled:
if self.animation_mode == AnimationModes.BREATHING:
return self.effect_breathing()
elif self.animation_mode == AnimationModes.STATIC:
return self.effect_static()
elif self.animation_mode == AnimationModes.STATIC_STANDBY:
pass
elif self.animation_mode == AnimationModes.USER:
return self.user_animation(self)
else:
self.off()
def _led_key_validator(self, *leds):
if leds:
for led in leds:
assert self._leds[led]
return LEDKeyMeta(*leds)
def _led_set_key_validator(self, brightness, *leds):
meta = self._led_key_validator(*leds)
meta.brightness = brightness
return meta
def _key_led_tog(self, *args, **kwargs):
if self.animation_mode == AnimationModes.STATIC_STANDBY:
self.animation_mode = AnimationModes.STATIC
if self._enabled:
self.off()
self._enabled = not self._enabled
def _key_led_inc(self, key, *args, **kwargs):
self.increase_brightness(leds=key.meta.leds)
def _key_led_dec(self, key, *args, **kwargs):
self.decrease_brightness(leds=key.meta.leds)
def _key_led_set(self, key, *args, **kwargs):
self.set_brightness(percent=key.meta.brightness, leds=key.meta.leds)
def _key_led_ani(self, *args, **kwargs):
self.increase_ani()
def _key_led_and(self, *args, **kwargs):
self.decrease_ani()
def _key_led_mode_static(self, *args, **kwargs):
self._effect_init = True
self.animation_mode = AnimationModes.STATIC
def _key_led_mode_breathe(self, *args, **kwargs):
self._effect_init = True
self.animation_mode = AnimationModes.BREATHING

View file

@ -0,0 +1,85 @@
import usb_hid
from kmk.extensions import Extension
from kmk.hid import HIDUsage
class LockCode:
NUMLOCK = 0x01
CAPSLOCK = 0x02
SCROLLLOCK = 0x04
COMPOSE = 0x08
KANA = 0x10
RESERVED = 0x20
class LockStatus(Extension):
def __init__(self):
self.report = None
self.hid = None
self._report_updated = False
for device in usb_hid.devices:
if device.usage == HIDUsage.KEYBOARD:
self.hid = device
def __repr__(self):
return f'LockStatus(report={self.report})'
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
if self.hid:
report = self.hid.get_last_received_report()
if report and report[0] != self.report:
self.report = report[0]
self._report_updated = True
else:
self._report_updated = False
else:
# _report_updated shouldn't ever be True if hid is
# falsy, but I would rather be safe than sorry.
self._report_updated = False
return
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return
@property
def report_updated(self):
return self._report_updated
def check_state(self, lock_code):
# This is false if there's no valid report, or all report bits are zero
if self.report:
return bool(self.report & lock_code)
else:
# Just in case, default to False if we don't know anything
return False
def get_num_lock(self):
return self.check_state(LockCode.NUMLOCK)
def get_caps_lock(self):
return self.check_state(LockCode.CAPSLOCK)
def get_scroll_lock(self):
return self.check_state(LockCode.SCROLLLOCK)
def get_compose(self):
return self.check_state(LockCode.COMPOSE)
def get_kana(self):
return self.check_state(LockCode.KANA)

View file

@ -0,0 +1,57 @@
from kmk.extensions import Extension
from kmk.keys import make_consumer_key
class MediaKeys(Extension):
def __init__(self):
# Consumer ("media") keys. Most known keys aren't supported here. A much
# longer list used to exist in this file, but the codes were almost certainly
# incorrect, conflicting with each other, or otherwise 'weird'. We'll add them
# back in piecemeal as needed. PRs welcome.
#
# A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php
# Note that currently we only have the PC codes. Recent MacOS versions seem to
# support PC media keys, so I don't know how much value we would get out of
# adding the old Apple-specific consumer codes, but again, PRs welcome if the
# lack of them impacts you.
make_consumer_key(code=226, names=('AUDIO_MUTE', 'MUTE')) # 0xE2
make_consumer_key(code=233, names=('AUDIO_VOL_UP', 'VOLU')) # 0xE9
make_consumer_key(code=234, names=('AUDIO_VOL_DOWN', 'VOLD')) # 0xEA
make_consumer_key(code=111, names=('BRIGHTNESS_UP', 'BRIU')) # 0x6F
make_consumer_key(code=112, names=('BRIGHTNESS_DOWN', 'BRID')) # 0x70
make_consumer_key(code=181, names=('MEDIA_NEXT_TRACK', 'MNXT')) # 0xB5
make_consumer_key(code=182, names=('MEDIA_PREV_TRACK', 'MPRV')) # 0xB6
make_consumer_key(code=183, names=('MEDIA_STOP', 'MSTP')) # 0xB7
make_consumer_key(
code=205, names=('MEDIA_PLAY_PAUSE', 'MPLY')
) # 0xCD (this may not be right)
make_consumer_key(code=184, names=('MEDIA_EJECT', 'EJCT')) # 0xB8
make_consumer_key(code=179, names=('MEDIA_FAST_FORWARD', 'MFFD')) # 0xB3
make_consumer_key(code=180, names=('MEDIA_REWIND', 'MRWD')) # 0xB4
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return

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

@ -0,0 +1,161 @@
import busio
import gc
import adafruit_displayio_ssd1306
import displayio
import terminalio
from adafruit_display_text import label
from kmk.extensions import Extension
class OledDisplayMode:
TXT = 0
IMG = 1
class OledReactionType:
STATIC = 0
LAYER = 1
class OledData:
def __init__(
self,
image=None,
corner_one=None,
corner_two=None,
corner_three=None,
corner_four=None,
):
if image:
self.data = [image]
elif corner_one and corner_two and corner_three and corner_four:
self.data = [corner_one, corner_two, corner_three, corner_four]
class Oled(Extension):
def __init__(
self,
views,
toDisplay=OledDisplayMode.TXT,
oWidth=128,
oHeight=32,
flip: bool = False,
):
displayio.release_displays()
self.rotation = 180 if flip else 0
self._views = views.data
self._toDisplay = toDisplay
self._width = oWidth
self._height = oHeight
self._prevLayers = 0
gc.collect()
def returnCurrectRenderText(self, layer, singleView):
# for now we only have static things and react to layers. But when we react to battery % and wpm we can handle the logic here
if singleView[0] == OledReactionType.STATIC:
return singleView[1][0]
if singleView[0] == OledReactionType.LAYER:
return singleView[1][layer]
def renderOledTextLayer(self, layer):
splash = displayio.Group()
splash.append(
label.Label(
terminalio.FONT,
text=self.returnCurrectRenderText(layer, self._views[0]),
color=0xFFFFFF,
x=0,
y=10,
)
)
splash.append(
label.Label(
terminalio.FONT,
text=self.returnCurrectRenderText(layer, self._views[1]),
color=0xFFFFFF,
x=64,
y=10,
)
)
splash.append(
label.Label(
terminalio.FONT,
text=self.returnCurrectRenderText(layer, self._views[2]),
color=0xFFFFFF,
x=0,
y=25,
)
)
splash.append(
label.Label(
terminalio.FONT,
text=self.returnCurrectRenderText(layer, self._views[3]),
color=0xFFFFFF,
x=64,
y=25,
)
)
self._display.show(splash)
gc.collect()
def renderOledImgLayer(self, layer):
splash = displayio.Group()
odb = displayio.OnDiskBitmap(
'/' + self.returnCurrectRenderText(layer, self._views[0])
)
image = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
splash.append(image)
self._display.show(splash)
gc.collect()
def updateOLED(self, sandbox):
if self._toDisplay == OledDisplayMode.TXT:
self.renderOledTextLayer(sandbox.active_layers[0])
if self._toDisplay == OledDisplayMode.IMG:
self.renderOledImgLayer(sandbox.active_layers[0])
gc.collect()
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, keyboard):
displayio.release_displays()
i2c = busio.I2C(keyboard.SCL, keyboard.SDA)
self._display = adafruit_displayio_ssd1306.SSD1306(
displayio.I2CDisplay(i2c, device_address=0x3C),
width=self._width,
height=self._height,
rotation=self.rotation,
)
if self._toDisplay == OledDisplayMode.TXT:
self.renderOledTextLayer(0)
if self._toDisplay == OledDisplayMode.IMG:
self.renderOledImgLayer(0)
return
def before_matrix_scan(self, sandbox):
if sandbox.active_layers[0] != self._prevLayers:
self._prevLayers = sandbox.active_layers[0]
self.updateOLED(sandbox)
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return

View file

@ -0,0 +1,201 @@
import neopixel
from storage import getmount
from kmk.extensions import Extension
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key
class Color:
OFF = [0, 0, 0]
BLACK = OFF
WHITE = [249, 249, 249]
RED = [255, 0, 0]
AZURE = [153, 245, 255]
BLUE = [0, 0, 255]
CYAN = [0, 255, 255]
GREEN = [0, 255, 0]
YELLOW = [255, 247, 0]
MAGENTA = [255, 0, 255]
ORANGE = [255, 77, 0]
PURPLE = [255, 0, 242]
TEAL = [0, 128, 128]
PINK = [255, 0, 255]
class Rgb_matrix_data:
def __init__(self, keys=[], underglow=[]):
if len(keys) == 0:
print('No colors passed for your keys')
return
if len(underglow) == 0:
print('No colors passed for your underglow')
return
self.data = keys + underglow
@staticmethod
def generate_led_map(
number_of_keys, number_of_underglow, key_color, underglow_color
):
keys = [key_color] * number_of_keys
underglow = [underglow_color] * number_of_underglow
print(f'Rgb_matrix_data(keys={keys},\nunderglow={underglow})')
class Rgb_matrix(Extension):
def __init__(
self,
rgb_order=(1, 0, 2), # GRB WS2812
disable_auto_write=False,
ledDisplay=[],
split=False,
rightSide=False,
):
name = str(getmount('/').label)
self.rgb_order = rgb_order
self.disable_auto_write = disable_auto_write
self.split = split
self.rightSide = rightSide
self.brightness_step = 0.1
self.brightness = 0
if name.endswith('L'):
self.rightSide = False
elif name.endswith('R'):
self.rightSide = True
if type(ledDisplay) is Rgb_matrix_data:
self.ledDisplay = ledDisplay.data
else:
self.ledDisplay = ledDisplay
make_key(
names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
)
make_key(
names=('RGB_BRI',), on_press=self._rgb_bri, on_release=handler_passthrough
)
make_key(
names=('RGB_BRD',), on_press=self._rgb_brd, on_release=handler_passthrough
)
def _rgb_tog(self, *args, **kwargs):
if self.enable:
self.off()
else:
self.on()
self.enable = not self.enable
def _rgb_bri(self, *args, **kwargs):
self.increase_brightness()
def _rgb_brd(self, *args, **kwargs):
self.decrease_brightness()
def on(self):
if self.neopixel:
self.setBasedOffDisplay()
self.neopixel.show()
def off(self):
if self.neopixel:
self.set_rgb_fill((0, 0, 0))
def set_rgb_fill(self, rgb):
if self.neopixel:
self.neopixel.fill(rgb)
if self.disable_auto_write:
self.neopixel.show()
def set_brightness(self, brightness=None):
if brightness is None:
brightness = self.brightness
if self.neopixel:
self.neopixel.brightness = brightness
if self.disable_auto_write:
self.neopixel.show()
def increase_brightness(self, step=None):
if step is None:
step = self.brightness_step
self.brightness = (
self.brightness + step if self.brightness + step <= 1.0 else 1.0
)
self.set_brightness(self.brightness)
def decrease_brightness(self, step=None):
if step is None:
step = self.brightness_step
self.brightness = (
self.brightness - step if self.brightness - step >= 0.0 else 0.0
)
self.set_brightness(self.brightness)
def setBasedOffDisplay(self):
if self.split:
for i, val in enumerate(self.ledDisplay):
if self.rightSide:
if self.keyPos[i] >= (self.num_pixels / 2):
self.neopixel[int(self.keyPos[i] - (self.num_pixels / 2))] = (
val[0],
val[1],
val[2],
)
else:
if self.keyPos[i] <= (self.num_pixels / 2):
self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
else:
for i, val in enumerate(self.ledDisplay):
self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, board):
self.neopixel = neopixel.NeoPixel(
board.rgb_pixel_pin,
board.num_pixels,
brightness=board.brightness_limit,
pixel_order=self.rgb_order,
auto_write=not self.disable_auto_write,
)
self.num_pixels = board.num_pixels
self.keyPos = board.led_key_pos
self.brightness = board.brightness_limit
self.on()
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
if self.neopixel:
self.neopixel.brightness = (
self.neopixel.brightness / 2
if self.neopixel.brightness / 2 > 0
else 0.1
)
if self.disable_auto_write:
self.neopixel.show()
def on_powersave_disable(self, sandbox):
if self.neopixel:
self.neopixel.brightness = self.brightness
if self.disable_auto_write:
self.neopixel.show()

599
kmk/extensions/rgb.py Normal file
View file

@ -0,0 +1,599 @@
from adafruit_pixelbuf import PixelBuf
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.scheduler import create_task
from kmk.utils import Debug, clamp
debug = Debug(__name__)
rgb_config = {}
def hsv_to_rgb(hue, sat, val):
'''
Converts HSV values, and returns a tuple of RGB values
:param hue:
:param sat:
:param val:
:return: (r, g, b)
'''
if sat == 0:
return (val, val, val)
hue = 6 * (hue & 0xFF)
frac = hue & 0xFF
sxt = hue >> 8
base = (0xFF - sat) * val
color = (val * sat * frac) >> 8
val <<= 8
if sxt == 0:
r = val
g = base + color
b = base
elif sxt == 1:
r = val - color
g = val
b = base
elif sxt == 2:
r = base
g = val
b = base + color
elif sxt == 3:
r = base
g = val - color
b = val
elif sxt == 4:
r = base + color
g = base
b = val
elif sxt == 5:
r = val
g = base
b = val - color
return (r >> 8), (g >> 8), (b >> 8)
def hsv_to_rgbw(hue, sat, val):
'''
Converts HSV values, and returns a tuple of RGBW values
:param hue:
:param sat:
:param val:
:return: (r, g, b, w)
'''
rgb = hsv_to_rgb(hue, sat, val)
return rgb[0], rgb[1], rgb[2], min(rgb)
class AnimationModes:
OFF = 0
STATIC = 1
STATIC_STANDBY = 2
BREATHING = 3
RAINBOW = 4
BREATHING_RAINBOW = 5
KNIGHT = 6
SWIRL = 7
USER = 8
class RGB(Extension):
pos = 0
def __init__(
self,
pixel_pin,
num_pixels=0,
rgb_order=(1, 0, 2), # GRB WS2812
val_limit=255,
hue_default=0,
sat_default=255,
val_default=255,
hue_step=4,
sat_step=13,
val_step=13,
animation_speed=1,
breathe_center=1, # 1.0-2.7
knight_effect_length=3,
animation_mode=AnimationModes.STATIC,
effect_init=False,
reverse_animation=False,
user_animation=None,
disable_auto_write=False,
pixels=None,
refresh_rate=60,
):
self.pixel_pin = pixel_pin
self.num_pixels = num_pixels
self.rgb_order = rgb_order
self.hue_step = hue_step
self.sat_step = sat_step
self.val_step = val_step
self.hue = hue_default
self.hue_default = hue_default
self.sat = sat_default
self.sat_default = sat_default
self.val = val_default
self.val_default = val_default
self.breathe_center = breathe_center
self.knight_effect_length = knight_effect_length
self.val_limit = val_limit
self.animation_mode = animation_mode
self.animation_speed = animation_speed
self.effect_init = effect_init
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(
names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
)
make_key(
names=('RGB_HUI',), on_press=self._rgb_hui, on_release=handler_passthrough
)
make_key(
names=('RGB_HUD',), on_press=self._rgb_hud, on_release=handler_passthrough
)
make_key(
names=('RGB_SAI',), on_press=self._rgb_sai, on_release=handler_passthrough
)
make_key(
names=('RGB_SAD',), on_press=self._rgb_sad, on_release=handler_passthrough
)
make_key(
names=('RGB_VAI',), on_press=self._rgb_vai, on_release=handler_passthrough
)
make_key(
names=('RGB_VAD',), on_press=self._rgb_vad, on_release=handler_passthrough
)
make_key(
names=('RGB_ANI',), on_press=self._rgb_ani, on_release=handler_passthrough
)
make_key(
names=('RGB_AND',), on_press=self._rgb_and, on_release=handler_passthrough
)
make_key(
names=('RGB_MODE_PLAIN', 'RGB_M_P'),
on_press=self._rgb_mode_static,
on_release=handler_passthrough,
)
make_key(
names=('RGB_MODE_BREATHE', 'RGB_M_B'),
on_press=self._rgb_mode_breathe,
on_release=handler_passthrough,
)
make_key(
names=('RGB_MODE_RAINBOW', 'RGB_M_R'),
on_press=self._rgb_mode_rainbow,
on_release=handler_passthrough,
)
make_key(
names=('RGB_MODE_BREATHE_RAINBOW', 'RGB_M_BR'),
on_press=self._rgb_mode_breathe_rainbow,
on_release=handler_passthrough,
)
make_key(
names=('RGB_MODE_SWIRL', 'RGB_M_S'),
on_press=self._rgb_mode_swirl,
on_release=handler_passthrough,
)
make_key(
names=('RGB_MODE_KNIGHT', 'RGB_M_K'),
on_press=self._rgb_mode_knight,
on_release=handler_passthrough,
)
make_key(
names=('RGB_RESET', 'RGB_RST'),
on_press=self._rgb_reset,
on_release=handler_passthrough,
)
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
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._task = create_task(self.animate, period_ms=(1000 // self.refresh_rate))
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
pass
def on_powersave_enable(self, sandbox):
return
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
:param hue:
:param sat:
:param val:
:param index: Index of LED/Pixel
'''
val = clamp(val, 0, self.val_limit)
if self.rgbw:
self.set_rgb(hsv_to_rgbw(hue, sat, val), index)
else:
self.set_rgb(hsv_to_rgb(hue, sat, val), index)
def set_hsv_fill(self, hue, sat, val):
'''
Takes HSV values and displays it on all LEDs/Neopixels
:param hue:
:param sat:
:param val:
'''
val = clamp(val, 0, self.val_limit)
if self.rgbw:
self.set_rgb_fill(hsv_to_rgbw(hue, sat, val))
else:
self.set_rgb_fill(hsv_to_rgb(hue, sat, val))
def set_rgb(self, rgb, index):
'''
Takes an RGB or RGBW and displays it on a single LED/Neopixel
:param rgb: RGB or RGBW
:param index: Index of LED/Pixel
'''
if 0 <= index <= self.num_pixels - 1:
for pixels in self.pixels:
if index <= (len(pixels) - 1):
pixels[index] = rgb
break
index -= len(pixels)
if not self.disable_auto_write:
pixels.show()
def set_rgb_fill(self, rgb):
'''
Takes an RGB or RGBW and displays it on all LEDs/Neopixels
:param rgb: RGB or RGBW
'''
for pixels in self.pixels:
pixels.fill(rgb)
if not self.disable_auto_write:
pixels.show()
def increase_hue(self, step=None):
'''
Increases hue by step amount rolling at 256 and returning to 0
:param step:
'''
if step is None:
step = self.hue_step
self.hue = (self.hue + step) % 256
if self._check_update():
self._do_update()
def decrease_hue(self, step=None):
'''
Decreases hue by step amount rolling at 0 and returning to 256
:param step:
'''
if step is None:
step = self.hue_step
if (self.hue - step) <= 0:
self.hue = (self.hue + 256 - step) % 256
else:
self.hue = (self.hue - step) % 256
if self._check_update():
self._do_update()
def increase_sat(self, step=None):
'''
Increases saturation by step amount stopping at 255
:param step:
'''
if step is None:
step = self.sat_step
self.sat = clamp(self.sat + step, 0, 255)
if self._check_update():
self._do_update()
def decrease_sat(self, step=None):
'''
Decreases saturation by step amount stopping at 0
:param step:
'''
if step is None:
step = self.sat_step
self.sat = clamp(self.sat - step, 0, 255)
if self._check_update():
self._do_update()
def increase_val(self, step=None):
'''
Increases value by step amount stopping at 100
:param step:
'''
if step is None:
step = self.val_step
self.val = clamp(self.val + step, 0, 255)
if self._check_update():
self._do_update()
def decrease_val(self, step=None):
'''
Decreases value by step amount stopping at 0
:param step:
'''
if step is None:
step = self.val_step
self.val = clamp(self.val - step, 0, 255)
if self._check_update():
self._do_update()
def increase_ani(self):
'''
Increases animation speed by 1 amount stopping at 10
:param step:
'''
self.animation_speed = clamp(self.animation_speed + 1, 0, 10)
if self._check_update():
self._do_update()
def decrease_ani(self):
'''
Decreases animation speed by 1 amount stopping at 0
:param step:
'''
self.animation_speed = clamp(self.animation_speed - 1, 0, 10)
if self._check_update():
self._do_update()
def off(self):
'''
Turns off all LEDs/Neopixels without changing stored values
'''
self.set_hsv_fill(0, 0, 0)
def show(self):
'''
Turns on all LEDs/Neopixels without changing stored values
'''
for pixels in self.pixels:
pixels.show()
def animate(self):
'''
Activates a "step" in the animation based on the active mode
:return: Returns the new state in animation
'''
if self.effect_init:
self._init_effect()
if self.animation_mode is AnimationModes.STATIC_STANDBY:
return
if self.enable:
self._animation_step()
if self.animation_mode == AnimationModes.BREATHING:
self.effect_breathing()
elif self.animation_mode == AnimationModes.RAINBOW:
self.effect_rainbow()
elif self.animation_mode == AnimationModes.BREATHING_RAINBOW:
self.effect_breathing_rainbow()
elif self.animation_mode == AnimationModes.STATIC:
self.effect_static()
elif self.animation_mode == AnimationModes.KNIGHT:
self.effect_knight()
elif self.animation_mode == AnimationModes.SWIRL:
self.effect_swirl()
elif self.animation_mode == AnimationModes.USER:
self.user_animation(self)
elif self.animation_mode == AnimationModes.STATIC_STANDBY:
pass
else:
self.off()
def _animation_step(self):
self._substep += self.animation_speed / 4
self._step = int(self._substep)
self._substep -= self._step
def _init_effect(self):
self.pos = 0
self.reverse_animation = False
self.effect_init = False
def _check_update(self):
return bool(self.animation_mode == AnimationModes.STATIC_STANDBY)
def _do_update(self):
if self.animation_mode == AnimationModes.STATIC_STANDBY:
self.animation_mode = AnimationModes.STATIC
def effect_static(self):
self.set_hsv_fill(self.hue, self.sat, self.val)
self.animation_mode = AnimationModes.STATIC_STANDBY
def effect_breathing(self):
# http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
# https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
sined = sin((self.pos / 255.0) * pi)
multip_1 = exp(sined) - self.breathe_center / e
multip_2 = self.val_limit / (e - 1 / e)
self.val = int(multip_1 * multip_2)
self.pos = (self.pos + self._step) % 256
self.set_hsv_fill(self.hue, self.sat, self.val)
def effect_breathing_rainbow(self):
self.increase_hue(self._step)
self.effect_breathing()
def effect_rainbow(self):
self.increase_hue(self._step)
self.set_hsv_fill(self.hue, self.sat, self.val)
def effect_swirl(self):
self.increase_hue(self._step)
self.disable_auto_write = True # Turn off instantly showing
for i in range(0, self.num_pixels):
self.set_hsv(
(self.hue - (i * self.num_pixels)) % 256, self.sat, self.val, i
)
# Show final results
self.disable_auto_write = False # Resume showing changes
self.show()
def effect_knight(self):
# Determine which LEDs should be lit up
self.disable_auto_write = True # Turn off instantly showing
self.off() # Fill all off
pos = int(self.pos)
# Set all pixels on in range of animation length offset by position
for i in range(pos, (pos + self.knight_effect_length)):
self.set_hsv(self.hue, self.sat, self.val, i)
# Reverse animation when a boundary is hit
if pos >= self.num_pixels or pos - 1 < (self.knight_effect_length * -1):
self.reverse_animation = not self.reverse_animation
if self.reverse_animation:
self.pos -= self._step / 2
else:
self.pos += self._step / 2
# Show final results
self.disable_auto_write = False # Resume showing changes
self.show()
def _rgb_tog(self, *args, **kwargs):
if self.animation_mode == AnimationModes.STATIC:
self.animation_mode = AnimationModes.STATIC_STANDBY
self._do_update()
if self.animation_mode == AnimationModes.STATIC_STANDBY:
self.animation_mode = AnimationModes.STATIC
self._do_update()
if self.enable:
self.off()
self.enable = not self.enable
def _rgb_hui(self, *args, **kwargs):
self.increase_hue()
def _rgb_hud(self, *args, **kwargs):
self.decrease_hue()
def _rgb_sai(self, *args, **kwargs):
self.increase_sat()
def _rgb_sad(self, *args, **kwargs):
self.decrease_sat()
def _rgb_vai(self, *args, **kwargs):
self.increase_val()
def _rgb_vad(self, *args, **kwargs):
self.decrease_val()
def _rgb_ani(self, *args, **kwargs):
self.increase_ani()
def _rgb_and(self, *args, **kwargs):
self.decrease_ani()
def _rgb_mode_static(self, *args, **kwargs):
self.effect_init = True
self.animation_mode = AnimationModes.STATIC
def _rgb_mode_breathe(self, *args, **kwargs):
self.effect_init = True
self.animation_mode = AnimationModes.BREATHING
def _rgb_mode_breathe_rainbow(self, *args, **kwargs):
self.effect_init = True
self.animation_mode = AnimationModes.BREATHING_RAINBOW
def _rgb_mode_rainbow(self, *args, **kwargs):
self.effect_init = True
self.animation_mode = AnimationModes.RAINBOW
def _rgb_mode_swirl(self, *args, **kwargs):
self.effect_init = True
self.animation_mode = AnimationModes.SWIRL
def _rgb_mode_knight(self, *args, **kwargs):
self.effect_init = True
self.animation_mode = AnimationModes.KNIGHT
def _rgb_reset(self, *args, **kwargs):
self.hue = self.hue_default
self.sat = self.sat_default
self.val = self.val_default
if self.animation_mode == AnimationModes.STATIC_STANDBY:
self.animation_mode = AnimationModes.STATIC
self._do_update()

145
kmk/extensions/statusled.py Normal file
View file

@ -0,0 +1,145 @@
# Use this extension for showing layer status with three leds
import pwmio
import time
from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.keys import make_key
class statusLED(Extension):
def __init__(
self,
led_pins,
brightness=30,
brightness_step=5,
brightness_limit=100,
):
self._leds = []
for led in led_pins:
try:
self._leds.append(pwmio.PWMOut(led))
except Exception as e:
print(e)
raise InvalidExtensionEnvironment(
'Unable to create pulseio.PWMOut() instance with provided led_pin'
)
self._led_count = len(self._leds)
self.brightness = brightness
self._layer_last = -1
self.brightness_step = brightness_step
self.brightness_limit = brightness_limit
make_key(names=('SLED_INC',), on_press=self._key_led_inc)
make_key(names=('SLED_DEC',), on_press=self._key_led_dec)
def _layer_indicator(self, layer_active, *args, **kwargs):
'''
Indicates layer with leds
For the time being just a simple consecutive single led
indicator. And when there are more layers than leds it
wraps around to the first led again.
(Also works for a single led, which just lights when any
layer is active)
'''
if self._layer_last != layer_active:
led_last = 0 if self._layer_last == 0 else 1 + (self._layer_last - 1) % 3
if layer_active > 0:
led_active = 0 if layer_active == 0 else 1 + (layer_active - 1) % 3
self.set_brightness(self.brightness, led_active)
self.set_brightness(0, led_last)
else:
self.set_brightness(0, led_last)
self._layer_last = layer_active
def __repr__(self):
return f'SLED({self._to_dict()})'
def _to_dict(self):
return {
'_brightness': self.brightness,
'brightness_step': self.brightness_step,
'brightness_limit': self.brightness_limit,
}
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, sandbox):
'''Light up every single led once for 200 ms'''
for i in range(self._led_count + 2):
if i < self._led_count:
self._leds[i].duty_cycle = int(self.brightness / 100 * 65535)
i_off = i - 2
if i_off >= 0 and i_off < self._led_count:
self._leds[i_off].duty_cycle = int(0)
time.sleep(0.1)
for led in self._leds:
led.duty_cycle = int(0)
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
self._layer_indicator(sandbox.active_layers[0])
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
self.set_brightness(0)
return
def on_powersave_disable(self, sandbox):
self.set_brightness(self._brightness)
self._leds[2].duty_cycle = int(50 / 100 * 65535)
time.sleep(0.2)
self._leds[2].duty_cycle = int(0)
return
def set_brightness(self, percent, layer_id=-1):
if layer_id < 0:
for led in self._leds:
led.duty_cycle = int(percent / 100 * 65535)
else:
self._leds[layer_id - 1].duty_cycle = int(percent / 100 * 65535)
def increase_brightness(self, step=None):
if not step:
self._brightness += self.brightness_step
else:
self._brightness += step
if self._brightness > 100:
self._brightness = 100
self.set_brightness(self._brightness, self._layer_last)
def decrease_brightness(self, step=None):
if not step:
self._brightness -= self.brightness_step
else:
self._brightness -= step
if self._brightness < 0:
self._brightness = 0
self.set_brightness(self._brightness, self._layer_last)
def _key_led_inc(self, *args, **kwargs):
self.increase_brightness()
def _key_led_dec(self, *args, **kwargs):
self.decrease_brightness()

View file

@ -0,0 +1,45 @@
from kmk.extensions import Extension
from kmk.keys import KC
class StringyKeymaps(Extension):
#####
# User-configurable
debug_enabled = False
def on_runtime_enable(self, keyboard):
return
def on_runtime_disable(self, keyboard):
return
def during_bootup(self, keyboard):
for _, layer in enumerate(keyboard.keymap):
for key_idx, key in enumerate(layer):
if isinstance(key, str):
replacement = KC.get(key)
if replacement is None:
replacement = KC.NO
if self.debug_enabled:
print(f"Failed replacing '{key}'. Using KC.NO")
elif self.debug_enabled:
print(f"Replacing '{key}' with {replacement}")
layer[key_idx] = replacement
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return

0
kmk/handlers/__init__.py Normal file
View file

155
kmk/handlers/sequences.py Normal file
View file

@ -0,0 +1,155 @@
import gc
from kmk.consts import UnicodeMode
from kmk.handlers.stock import passthrough
from kmk.keys import KC, make_key
from kmk.types import AttrDict, KeySequenceMeta
def get_wide_ordinal(char):
if len(char) != 2:
return ord(char)
return 0x10000 + (ord(char[0]) - 0xD800) * 0x400 + (ord(char[1]) - 0xDC00)
def sequence_press_handler(key, keyboard, KC, *args, **kwargs):
oldkeys_pressed = keyboard.keys_pressed
keyboard.keys_pressed = set()
for ikey in key.meta.seq:
if not getattr(ikey, 'no_press', None):
keyboard.process_key(ikey, True)
keyboard._send_hid()
if not getattr(ikey, 'no_release', None):
keyboard.process_key(ikey, False)
keyboard._send_hid()
keyboard.keys_pressed = oldkeys_pressed
return keyboard
def simple_key_sequence(seq):
return make_key(
meta=KeySequenceMeta(seq),
on_press=sequence_press_handler,
on_release=passthrough,
)
def send_string(message):
seq = []
for char in message:
kc = getattr(KC, char.upper())
if char.isupper():
kc = KC.LSHIFT(kc)
seq.append(kc)
return simple_key_sequence(seq)
IBUS_KEY_COMBO = simple_key_sequence((KC.LCTRL(KC.LSHIFT(KC.U)),))
RALT_KEY = simple_key_sequence((KC.RALT,))
U_KEY = simple_key_sequence((KC.U,))
ENTER_KEY = simple_key_sequence((KC.ENTER,))
RALT_DOWN_NO_RELEASE = simple_key_sequence((KC.RALT(no_release=True),))
RALT_UP_NO_PRESS = simple_key_sequence((KC.RALT(no_press=True),))
def compile_unicode_string_sequences(string_table):
'''
Destructively convert ("compile") unicode strings into key sequences. This
will, for RAM saving reasons, empty the input dictionary and trigger
garbage collection.
'''
target = AttrDict()
for k, v in string_table.items():
target[k] = unicode_string_sequence(v)
# now loop through and kill the input dictionary to save RAM
for k in target.keys():
del string_table[k]
gc.collect()
return target
def unicode_string_sequence(unistring):
'''
Allows sending things like (°° directly, without
manual conversion to Unicode codepoints.
'''
return unicode_codepoint_sequence([hex(get_wide_ordinal(s))[2:] for s in unistring])
def generate_codepoint_keysym_seq(codepoint, expected_length=4):
# To make MacOS and Windows happy, always try to send
# sequences that are of length 4 at a minimum
# On Linux systems, we can happily send longer strings.
# They will almost certainly break on MacOS and Windows,
# but this is a documentation problem more than anything.
# Not sure how to send emojis on Mac/Windows like that,
# though, since (for example) the Canadian flag is assembled
# from two five-character codepoints, 1f1e8 and 1f1e6
seq = [KC.N0 for _ in range(max(len(codepoint), expected_length))]
for idx, codepoint_fragment in enumerate(reversed(codepoint)):
seq[-(idx + 1)] = KC.__getattr__(codepoint_fragment.upper())
return seq
def unicode_codepoint_sequence(codepoints):
kc_seqs = (generate_codepoint_keysym_seq(codepoint) for codepoint in codepoints)
kc_macros = [simple_key_sequence(kc_seq) for kc_seq in kc_seqs]
def _unicode_sequence(key, keyboard, *args, **kwargs):
if keyboard.unicode_mode == UnicodeMode.IBUS:
keyboard.process_key(
simple_key_sequence(_ibus_unicode_sequence(kc_macros, keyboard)), True
)
elif keyboard.unicode_mode == UnicodeMode.RALT:
keyboard.process_key(
simple_key_sequence(_ralt_unicode_sequence(kc_macros, keyboard)), True
)
elif keyboard.unicode_mode == UnicodeMode.WINC:
keyboard.process_key(
simple_key_sequence(_winc_unicode_sequence(kc_macros, keyboard)), True
)
return make_key(on_press=_unicode_sequence)
def _ralt_unicode_sequence(kc_macros, keyboard):
for kc_macro in kc_macros:
yield RALT_DOWN_NO_RELEASE
yield kc_macro
yield RALT_UP_NO_PRESS
def _ibus_unicode_sequence(kc_macros, keyboard):
for kc_macro in kc_macros:
yield IBUS_KEY_COMBO
yield kc_macro
yield ENTER_KEY
def _winc_unicode_sequence(kc_macros, keyboard):
'''
Send unicode sequence using WinCompose:
http://wincompose.info/
https://github.com/SamHocevar/wincompose
'''
for kc_macro in kc_macros:
yield RALT_KEY
yield U_KEY
yield kc_macro
yield ENTER_KEY

146
kmk/handlers/stock.py Normal file
View file

@ -0,0 +1,146 @@
from time import sleep
def passthrough(key, keyboard, *args, **kwargs):
return keyboard
def default_pressed(key, keyboard, KC, coord_int=None, *args, **kwargs):
keyboard.hid_pending = True
keyboard.keys_pressed.add(key)
return keyboard
def default_released(key, keyboard, KC, coord_int=None, *args, **kwargs): # NOQA
keyboard.hid_pending = True
keyboard.keys_pressed.discard(key)
return keyboard
def reset(*args, **kwargs):
import microcontroller
microcontroller.reset()
def reload(*args, **kwargs):
import supervisor
supervisor.reload()
def bootloader(*args, **kwargs):
import microcontroller
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset()
def debug_pressed(key, keyboard, KC, *args, **kwargs):
if keyboard.debug_enabled:
print('DebugDisable()')
else:
print('DebugEnable()')
keyboard.debug_enabled = not keyboard.debug_enabled
return keyboard
def gesc_pressed(key, keyboard, KC, *args, **kwargs):
GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI}
if GESC_TRIGGERS.intersection(keyboard.keys_pressed):
# First, release GUI if already pressed
keyboard._send_hid()
# if Shift is held, KC_GRAVE will become KC_TILDE on OS level
keyboard.keys_pressed.add(KC.GRAVE)
keyboard.hid_pending = True
return keyboard
# else return KC_ESC
keyboard.keys_pressed.add(KC.ESCAPE)
keyboard.hid_pending = True
return keyboard
def gesc_released(key, keyboard, KC, *args, **kwargs):
keyboard.keys_pressed.discard(KC.ESCAPE)
keyboard.keys_pressed.discard(KC.GRAVE)
keyboard.hid_pending = True
return keyboard
def bkdl_pressed(key, keyboard, KC, *args, **kwargs):
BKDL_TRIGGERS = {KC.LGUI, KC.RGUI}
if BKDL_TRIGGERS.intersection(keyboard.keys_pressed):
keyboard._send_hid()
keyboard.keys_pressed.add(KC.DEL)
keyboard.hid_pending = True
return keyboard
# else return KC_ESC
keyboard.keys_pressed.add(KC.BKSP)
keyboard.hid_pending = True
return keyboard
def bkdl_released(key, keyboard, KC, *args, **kwargs):
keyboard.keys_pressed.discard(KC.BKSP)
keyboard.keys_pressed.discard(KC.DEL)
keyboard.hid_pending = True
return keyboard
def sleep_pressed(key, keyboard, KC, *args, **kwargs):
sleep(key.meta.ms / 1000)
return keyboard
def uc_mode_pressed(key, keyboard, *args, **kwargs):
keyboard.unicode_mode = key.meta.mode
return keyboard
def hid_switch(key, keyboard, *args, **kwargs):
keyboard.hid_type, keyboard.secondary_hid_type = (
keyboard.secondary_hid_type,
keyboard.hid_type,
)
keyboard._init_hid()
return keyboard
def ble_refresh(key, keyboard, *args, **kwargs):
from kmk.hid import HIDModes
if keyboard.hid_type != HIDModes.BLE:
return keyboard
keyboard._hid_helper.stop_advertising()
keyboard._hid_helper.start_advertising()
return keyboard
def ble_disconnect(key, keyboard, *args, **kwargs):
from kmk.hid import HIDModes
if keyboard.hid_type != HIDModes.BLE:
return keyboard
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)

355
kmk/hid.py Normal file
View file

@ -0,0 +1,355 @@
import supervisor
import usb_hid
from micropython import const
from storage import getmount
from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey, MouseKey
from kmk.utils import clamp
try:
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.hid import HIDService
except ImportError:
# BLE not supported on this platform
pass
class HIDModes:
NOOP = 0 # currently unused; for testing?
USB = 1
BLE = 2
ALL_MODES = (NOOP, USB, BLE)
class HIDReportTypes:
KEYBOARD = 1
MOUSE = 2
CONSUMER = 3
SYSCONTROL = 4
class HIDUsage:
KEYBOARD = 0x06
MOUSE = 0x02
CONSUMER = 0x01
SYSCONTROL = 0x80
class HIDUsagePage:
CONSUMER = 0x0C
KEYBOARD = MOUSE = SYSCONTROL = 0x01
HID_REPORT_SIZES = {
HIDReportTypes.KEYBOARD: 8,
HIDReportTypes.MOUSE: 4,
HIDReportTypes.CONSUMER: 2,
HIDReportTypes.SYSCONTROL: 8, # TODO find the correct value for this
}
class AbstractHID:
REPORT_BYTES = 8
def __init__(self, **kwargs):
self._prev_evt = bytearray(self.REPORT_BYTES)
self._evt = bytearray(self.REPORT_BYTES)
self.report_device = memoryview(self._evt)[0:1]
self.report_device[0] = HIDReportTypes.KEYBOARD
# Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view
# is "reserved" and evidently (mostly?) unused. However, other modes (or
# at least consumer, so far) will use this byte, which is the main reason
# this view exists. For KEYBOARD, use report_mods and report_non_mods
self.report_keys = memoryview(self._evt)[1:]
self.report_mods = memoryview(self._evt)[1:2]
self.report_non_mods = memoryview(self._evt)[3:]
self._cc_report = bytearray(HID_REPORT_SIZES[HIDReportTypes.CONSUMER] + 1)
self._cc_report[0] = HIDReportTypes.CONSUMER
self._cc_pending = False
self._pd_report = bytearray(HID_REPORT_SIZES[HIDReportTypes.MOUSE] + 1)
self._pd_report[0] = HIDReportTypes.MOUSE
self._pd_pending = False
self.post_init()
def __repr__(self):
return f'{self.__class__.__name__}(REPORT_BYTES={self.REPORT_BYTES})'
def post_init(self):
pass
def create_report(self, keys_pressed, axes):
self.clear_all()
for key in keys_pressed:
if key.code >= FIRST_KMK_INTERNAL_KEY:
continue
if isinstance(key, ModifierKey):
self.add_modifier(key)
elif isinstance(key, ConsumerKey):
self.add_cc(key)
elif isinstance(key, MouseKey):
self.add_pd(key)
else:
self.add_key(key)
if key.has_modifiers:
for mod in key.has_modifiers:
self.add_modifier(mod)
for axis in axes:
self.move_axis(axis)
def hid_send(self, evt):
# Don't raise a NotImplementedError so this can serve as our "dummy" HID
# when MCU/board doesn't define one to use (which should almost always be
# the CircuitPython-targeting one, except when unit testing or doing
# something truly bizarre. This will likely change eventually when Bluetooth
# is added)
pass
def send(self):
if self._evt != self._prev_evt:
self._prev_evt[:] = self._evt
self.hid_send(self._evt)
if self._cc_pending:
self.hid_send(self._cc_report)
self._cc_pending = False
if self._pd_pending:
self.hid_send(self._pd_report)
self._pd_pending = False
return self
def clear_all(self):
for idx, _ in enumerate(self.report_keys):
self.report_keys[idx] = 0x00
self.remove_cc()
self.remove_pd()
self.clear_axis()
return self
def clear_non_modifiers(self):
for idx, _ in enumerate(self.report_non_mods):
self.report_non_mods[idx] = 0x00
return self
def add_modifier(self, modifier):
if isinstance(modifier, ModifierKey):
if modifier.code == ModifierKey.FAKE_CODE:
for mod in modifier.has_modifiers:
self.report_mods[0] |= mod
else:
self.report_mods[0] |= modifier.code
else:
self.report_mods[0] |= modifier
return self
def remove_modifier(self, modifier):
if isinstance(modifier, ModifierKey):
if modifier.code == ModifierKey.FAKE_CODE:
for mod in modifier.has_modifiers:
self.report_mods[0] ^= mod
else:
self.report_mods[0] ^= modifier.code
else:
self.report_mods[0] ^= modifier
return self
def add_key(self, key):
# Try to find the first empty slot in the key report, and fill it
placed = False
where_to_place = self.report_non_mods
for idx, _ in enumerate(where_to_place):
if where_to_place[idx] == 0x00:
where_to_place[idx] = key.code
placed = True
break
if not placed:
# TODO what do we do here?......
pass
return self
def remove_key(self, key):
where_to_place = self.report_non_mods
for idx, _ in enumerate(where_to_place):
if where_to_place[idx] == key.code:
where_to_place[idx] = 0x00
return self
def add_cc(self, cc):
# Add (or write over) consumer control report. There can only be one CC
# active at any time.
memoryview(self._cc_report)[1:3] = cc.code.to_bytes(2, 'little')
self._cc_pending = True
def remove_cc(self):
# Remove consumer control report.
report = memoryview(self._cc_report)[1:3]
if report != b'\x00\x00':
report[:] = b'\x00\x00'
self._cc_pending = True
def add_pd(self, key):
self._pd_report[1] |= key.code
self._pd_pending = True
def remove_pd(self):
if self._pd_report[1]:
self._pd_pending = True
self._pd_report[1] = 0x00
def move_axis(self, axis):
delta = clamp(axis.delta, -127, 127)
axis.delta -= delta
self._pd_report[axis.code + 2] = 0xFF & delta
self._pd_pending = True
def clear_axis(self):
for idx in range(2, len(self._pd_report)):
self._pd_report[idx] = 0x00
class USBHID(AbstractHID):
REPORT_BYTES = 9
def post_init(self):
self.devices = {}
for device in usb_hid.devices:
us = device.usage
up = device.usage_page
if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
self.devices[HIDReportTypes.CONSUMER] = device
continue
if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
self.devices[HIDReportTypes.KEYBOARD] = device
continue
if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
self.devices[HIDReportTypes.MOUSE] = device
continue
if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
self.devices[HIDReportTypes.SYSCONTROL] = device
continue
def hid_send(self, evt):
if not supervisor.runtime.usb_connected:
return
# int, can be looked up in HIDReportTypes
reporting_device_const = evt[0]
return self.devices[reporting_device_const].send_report(
evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1]
)
class BLEHID(AbstractHID):
BLE_APPEARANCE_HID_KEYBOARD = const(961)
# Hardcoded in CPy
MAX_CONNECTIONS = const(2)
def __init__(self, ble_name=str(getmount('/').label), **kwargs):
self.ble_name = ble_name
super().__init__()
def post_init(self):
self.ble = BLERadio()
self.ble.name = self.ble_name
self.hid = HIDService()
self.hid.protocol_mode = 0 # Boot protocol
# Security-wise this is not right. While you're away someone turns
# on your keyboard and they can pair with it nice and clean and then
# listen to keystrokes.
# On the other hand we don't have LESC so it's like shouting your
# keystrokes in the air
if not self.ble.connected or not self.hid.devices:
self.start_advertising()
@property
def devices(self):
'''Search through the provided list of devices to find the ones with the
send_report attribute.'''
if not self.ble.connected:
return {}
result = {}
for device in self.hid.devices:
if not hasattr(device, 'send_report'):
continue
us = device.usage
up = device.usage_page
if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
result[HIDReportTypes.CONSUMER] = device
continue
if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
result[HIDReportTypes.KEYBOARD] = device
continue
if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
result[HIDReportTypes.MOUSE] = device
continue
if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
result[HIDReportTypes.SYSCONTROL] = device
continue
return result
def hid_send(self, evt):
if not self.ble.connected:
return
# int, can be looked up in HIDReportTypes
reporting_device_const = evt[0]
device = self.devices[reporting_device_const]
report_size = len(device._characteristic.value)
while len(evt) < report_size + 1:
evt.append(0)
return device.send_report(evt[1 : report_size + 1])
def clear_bonds(self):
import _bleio
_bleio.adapter.erase_bonding()
def start_advertising(self):
if not self.ble.advertising:
advertisement = ProvideServicesAdvertisement(self.hid)
advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD
self.ble.start_advertising(advertisement)
def stop_advertising(self):
self.ble.stop_advertising()

9
kmk/key_validators.py Normal file
View file

@ -0,0 +1,9 @@
from kmk.types import KeySeqSleepMeta, UnicodeModeKeyMeta
def key_seq_sleep_validator(ms):
return KeySeqSleepMeta(ms)
def unicode_mode_key_validator(mode):
return UnicodeModeKeyMeta(mode)

825
kmk/keys.py Normal file
View file

@ -0,0 +1,825 @@
try:
from typing import Callable, Optional, Tuple
except ImportError:
pass
from micropython import const
import kmk.handlers.stock as handlers
from kmk.consts import UnicodeMode
from kmk.key_validators import key_seq_sleep_validator, unicode_mode_key_validator
from kmk.types import UnicodeModeKeyMeta
from kmk.utils import Debug
# Type aliases / forward declaration; can't use the proper types because of circular imports.
Keyboard = object
Key = object
class KeyType:
SIMPLE = const(0)
MODIFIER = const(1)
CONSUMER = const(2)
MOUSE = const(3)
FIRST_KMK_INTERNAL_KEY = const(1000)
NEXT_AVAILABLE_KEY = 1000
ALL_ALPHAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALL_NUMBERS = '1234567890'
# since KC.1 isn't valid Python, alias to KC.N1
ALL_NUMBER_ALIASES = tuple(f'N{x}' for x in ALL_NUMBERS)
debug = Debug(__name__)
class Axis:
def __init__(self, code: int) -> None:
self.code = code
self.delta = 0
def __repr__(self) -> str:
return f'Axis(code={self.code}, delta={self.delta})'
def move(self, keyboard: Keyboard, delta: int):
self.delta += delta
if self.delta:
keyboard.axes.add(self)
keyboard.hid_pending = True
else:
keyboard.axes.discard(self)
class AX:
W = Axis(2)
X = Axis(0)
Y = Axis(1)
def maybe_make_key(
code: Optional[int],
names: Tuple[str, ...],
*args,
**kwargs,
) -> Callable[[str], Key]:
def closure(candidate):
if candidate in names:
return make_key(code=code, names=names, *args, **kwargs)
return closure
def maybe_make_argumented_key(
validator=lambda *validator_args, **validator_kwargs: object(),
names: Tuple[str, ...] = tuple(), # NOQA
*constructor_args,
**constructor_kwargs,
) -> Callable[[str], Key]:
def closure(candidate):
if candidate in names:
return make_argumented_key(
validator, names, *constructor_args, **constructor_kwargs
)
return closure
def maybe_make_no_key(candidate: str) -> Optional[Key]:
# NO and TRNS are functionally identical in how they (don't) mutate
# the state, but are tracked semantically separately, so create
# two keys with the exact same functionality
keys = (
('NO', 'XXXXXXX'),
('TRANSPARENT', 'TRNS'),
)
for names in keys:
if candidate in names:
return make_key(
names=names,
on_press=handlers.passthrough,
on_release=handlers.passthrough,
)
def maybe_make_alpha_key(candidate: str) -> Optional[Key]:
if len(candidate) != 1:
return
candidate_upper = candidate.upper()
if candidate_upper in ALL_ALPHAS:
return make_key(
code=4 + ALL_ALPHAS.index(candidate_upper),
names=(candidate_upper, candidate.lower()),
)
def maybe_make_numeric_key(candidate: str) -> Optional[Key]:
if candidate in ALL_NUMBERS or candidate in ALL_NUMBER_ALIASES:
try:
offset = ALL_NUMBERS.index(candidate)
except ValueError:
offset = ALL_NUMBER_ALIASES.index(candidate)
return make_key(
code=30 + offset,
names=(ALL_NUMBERS[offset], ALL_NUMBER_ALIASES[offset]),
)
def maybe_make_mod_key(candidate: str) -> Optional[Key]:
# MEH = LCTL | LALT | LSFT
# HYPR = LCTL | LALT | LSFT | LGUI
mods = (
(0x01, ('LEFT_CONTROL', 'LCTRL', 'LCTL')),
(0x02, ('LEFT_SHIFT', 'LSHIFT', 'LSFT')),
(0x04, ('LEFT_ALT', 'LALT', 'LOPT')),
(0x08, ('LEFT_SUPER', 'LGUI', 'LCMD', 'LWIN')),
(0x10, ('RIGHT_CONTROL', 'RCTRL', 'RCTL')),
(0x20, ('RIGHT_SHIFT', 'RSHIFT', 'RSFT')),
(0x40, ('RIGHT_ALT', 'RALT', 'ROPT')),
(0x80, ('RIGHT_SUPER', 'RGUI', 'RCMD', 'RWIN')),
(0x07, ('MEH',)),
(0x0F, ('HYPER', 'HYPR')),
)
for code, names in mods:
if candidate in names:
return make_key(code=code, names=names, type=KeyType.MODIFIER)
def maybe_make_more_ascii(candidate: str) -> Optional[Key]:
codes = (
(40, ('ENTER', 'ENT', '\n')),
(41, ('ESCAPE', 'ESC')),
(42, ('BACKSPACE', 'BSPACE', 'BSPC', 'BKSP')),
(43, ('TAB', '\t')),
(44, ('SPACE', 'SPC', ' ')),
(45, ('MINUS', 'MINS', '-')),
(46, ('EQUAL', 'EQL', '=')),
(47, ('LBRACKET', 'LBRC', '[')),
(48, ('RBRACKET', 'RBRC', ']')),
(49, ('BACKSLASH', 'BSLASH', 'BSLS', '\\')),
(51, ('SEMICOLON', 'SCOLON', 'SCLN', ';')),
(52, ('QUOTE', 'QUOT', "'")),
(53, ('GRAVE', 'GRV', 'ZKHK', '`')),
(54, ('COMMA', 'COMM', ',')),
(55, ('DOT', '.')),
(56, ('SLASH', 'SLSH', '/')),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_fn_key(candidate: str) -> Optional[Key]:
codes = (
(58, ('F1',)),
(59, ('F2',)),
(60, ('F3',)),
(61, ('F4',)),
(62, ('F5',)),
(63, ('F6',)),
(64, ('F7',)),
(65, ('F8',)),
(66, ('F9',)),
(67, ('F10',)),
(68, ('F11',)),
(69, ('F12',)),
(104, ('F13',)),
(105, ('F14',)),
(106, ('F15',)),
(107, ('F16',)),
(108, ('F17',)),
(109, ('F18',)),
(110, ('F19',)),
(111, ('F20',)),
(112, ('F21',)),
(113, ('F22',)),
(114, ('F23',)),
(115, ('F24',)),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_navlock_key(candidate: str) -> Optional[Key]:
codes = (
(57, ('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')),
# FIXME: Investigate whether this key actually works, and
# uncomment when/if it does.
# (130, ('LOCKING_CAPS', 'LCAP')),
(70, ('PRINT_SCREEN', 'PSCREEN', 'PSCR')),
(71, ('SCROLL_LOCK', 'SCROLLLOCK', 'SLCK')),
# FIXME: Investigate whether this key actually works, and
# uncomment when/if it does.
# (132, ('LOCKING_SCROLL', 'LSCRL')),
(72, ('PAUSE', 'PAUS', 'BRK')),
(73, ('INSERT', 'INS')),
(74, ('HOME',)),
(75, ('PGUP',)),
(76, ('DELETE', 'DEL')),
(77, ('END',)),
(78, ('PGDOWN', 'PGDN')),
(79, ('RIGHT', 'RGHT')),
(80, ('LEFT',)),
(81, ('DOWN',)),
(82, ('UP',)),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_numpad_key(candidate: str) -> Optional[Key]:
codes = (
(83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')),
(84, ('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')),
(85, ('KP_ASTERISK', 'NUMPAD_ASTERISK', 'PAST')),
(86, ('KP_MINUS', 'NUMPAD_MINUS', 'PMNS')),
(87, ('KP_PLUS', 'NUMPAD_PLUS', 'PPLS')),
(88, ('KP_ENTER', 'NUMPAD_ENTER', 'PENT')),
(89, ('KP_1', 'P1', 'NUMPAD_1')),
(90, ('KP_2', 'P2', 'NUMPAD_2')),
(91, ('KP_3', 'P3', 'NUMPAD_3')),
(92, ('KP_4', 'P4', 'NUMPAD_4')),
(93, ('KP_5', 'P5', 'NUMPAD_5')),
(94, ('KP_6', 'P6', 'NUMPAD_6')),
(95, ('KP_7', 'P7', 'NUMPAD_7')),
(96, ('KP_8', 'P8', 'NUMPAD_8')),
(97, ('KP_9', 'P9', 'NUMPAD_9')),
(98, ('KP_0', 'P0', 'NUMPAD_0')),
(99, ('KP_DOT', 'PDOT', 'NUMPAD_DOT')),
(103, ('KP_EQUAL', 'PEQL', 'NUMPAD_EQUAL')),
(133, ('KP_COMMA', 'PCMM', 'NUMPAD_COMMA')),
(134, ('KP_EQUAL_AS400', 'NUMPAD_EQUAL_AS400')),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_shifted_key(candidate: str) -> Optional[Key]:
codes = (
(30, ('EXCLAIM', 'EXLM', '!')),
(31, ('AT', '@')),
(32, ('HASH', 'POUND', '#')),
(33, ('DOLLAR', 'DLR', '$')),
(34, ('PERCENT', 'PERC', '%')),
(35, ('CIRCUMFLEX', 'CIRC', '^')),
(36, ('AMPERSAND', 'AMPR', '&')),
(37, ('ASTERISK', 'ASTR', '*')),
(38, ('LEFT_PAREN', 'LPRN', '(')),
(39, ('RIGHT_PAREN', 'RPRN', ')')),
(45, ('UNDERSCORE', 'UNDS', '_')),
(46, ('PLUS', '+')),
(47, ('LEFT_CURLY_BRACE', 'LCBR', '{')),
(48, ('RIGHT_CURLY_BRACE', 'RCBR', '}')),
(49, ('PIPE', '|')),
(51, ('COLON', 'COLN', ':')),
(52, ('DOUBLE_QUOTE', 'DQUO', 'DQT', '"')),
(53, ('TILDE', 'TILD', '~')),
(54, ('LEFT_ANGLE_BRACKET', 'LABK', '<')),
(55, ('RIGHT_ANGLE_BRACKET', 'RABK', '>')),
(56, ('QUESTION', 'QUES', '?')),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names, has_modifiers={KC.LSFT.code})
def maybe_make_international_key(candidate: str) -> Optional[Key]:
codes = (
(50, ('NONUS_HASH', 'NUHS')),
(100, ('NONUS_BSLASH', 'NUBS')),
(101, ('APP', 'APPLICATION', 'SEL', 'WINMENU')),
(135, ('INT1', 'RO')),
(136, ('INT2', 'KANA')),
(137, ('INT3', 'JYEN')),
(138, ('INT4', 'HENK')),
(139, ('INT5', 'MHEN')),
(140, ('INT6',)),
(141, ('INT7',)),
(142, ('INT8',)),
(143, ('INT9',)),
(144, ('LANG1', 'HAEN')),
(145, ('LANG2', 'HAEJ')),
(146, ('LANG3',)),
(147, ('LANG4',)),
(148, ('LANG5',)),
(149, ('LANG6',)),
(150, ('LANG7',)),
(151, ('LANG8',)),
(152, ('LANG9',)),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_unicode_key(candidate: str) -> Optional[Key]:
keys = (
(
('UC_MODE_NOOP', 'UC_DISABLE'),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.NOOP),
),
(
('UC_MODE_LINUX', 'UC_MODE_IBUS'),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.IBUS),
),
(
('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.RALT),
),
(
('UC_MODE_WINC',),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.WINC),
),
)
for names, handler, meta in keys:
if candidate in names:
return make_key(names=names, on_press=handler, meta=meta)
if candidate in ('UC_MODE',):
return make_argumented_key(
names=('UC_MODE',),
validator=unicode_mode_key_validator,
on_press=handlers.uc_mode_pressed,
)
def maybe_make_firmware_key(candidate: str) -> Optional[Key]:
keys = (
((('BLE_REFRESH',), handlers.ble_refresh)),
((('BLE_DISCONNECT',), handlers.ble_disconnect)),
((('BOOTLOADER',), handlers.bootloader)),
((('DEBUG', 'DBG'), handlers.debug_pressed)),
((('HID_SWITCH', 'HID'), handlers.hid_switch)),
((('RELOAD', 'RLD'), handlers.reload)),
((('RESET',), handlers.reset)),
((('ANY',), handlers.any_pressed)),
)
for names, handler in keys:
if candidate in names:
return make_key(names=names, on_press=handler)
KEY_GENERATORS = (
maybe_make_no_key,
maybe_make_alpha_key,
maybe_make_numeric_key,
maybe_make_firmware_key,
maybe_make_key(
None,
('BKDL',),
on_press=handlers.bkdl_pressed,
on_release=handlers.bkdl_released,
),
maybe_make_key(
None,
('GESC', 'GRAVE_ESC'),
on_press=handlers.gesc_pressed,
on_release=handlers.gesc_released,
),
# A dummy key to trigger a sleep_ms call in a sequence of other keys in a
# simple sequence macro.
maybe_make_argumented_key(
key_seq_sleep_validator,
('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'),
on_press=handlers.sleep_pressed,
),
maybe_make_mod_key,
# More ASCII standard keys
maybe_make_more_ascii,
# Function Keys
maybe_make_fn_key,
# Lock Keys, Navigation, etc.
maybe_make_navlock_key,
# Numpad
# FIXME: Investigate whether this key actually works, and
# uncomment when/if it does.
# maybe_make_key(131, ('LOCKING_NUM', 'LNUM')),
maybe_make_numpad_key,
# Making life better for folks on tiny keyboards especially: exposes
# the 'shifted' keys as raw keys. Under the hood we're still
# sending Shift+(whatever key is normally pressed) to get these, so
# for example `KC_AT` will hold shift and press 2.
maybe_make_shifted_key,
# International
maybe_make_international_key,
maybe_make_unicode_key,
)
class KeyAttrDict:
# Instead of relying on the uncontrollable availability of a big chunk of
# contiguous memory for key caching, we can manually fragment the cache into
# reasonably small partitions. The partition size is chosen from the magic
# values of CPs hash allocation sizes.
# (https://github.com/adafruit/circuitpython/blob/main/py/map.c, 2023-02)
__partition_size = 37
__cache = [{}]
def __iter__(self):
for partition in self.__cache:
for name in partition:
yield name
def __setitem__(self, name: str, key: Key):
# Overwrite existing reference.
for partition in self.__cache:
if name in partition:
partition[name] = key
return key
# Insert new reference.
if len(self.__cache[-1]) >= self.__partition_size:
self.__cache.append({})
self.__cache[-1][name] = key
return key
def __getattr__(self, name: str):
return self.__getitem__(name)
def get(self, name: str, default: Optional[Key] = None):
try:
return self.__getitem__(name)
except Exception:
return default
def clear(self):
self.__cache.clear()
self.__cache.append({})
def __getitem__(self, name: str):
for partition in self.__cache:
if name in partition:
return partition[name]
for func in KEY_GENERATORS:
maybe_key = func(name)
if maybe_key:
break
if not maybe_key:
if debug.enabled:
debug(f'Invalid key: {name}')
return KC.NO
if debug.enabled:
debug(f'{name}: {maybe_key}')
return maybe_key
# Global state, will be filled in throughout this file, and
# anywhere the user creates custom keys
KC = KeyAttrDict()
class Key:
def __init__(
self,
code: int,
has_modifiers: Optional[list[Key, ...]] = None,
no_press: bool = False,
no_release: bool = False,
on_press: Callable[
[object, Key, Keyboard, ...], None
] = handlers.default_pressed,
on_release: Callable[
[object, Key, Keyboard, ...], None
] = handlers.default_released,
meta: object = object(),
):
self.code = code
self.has_modifiers = has_modifiers
# cast to bool() in case we get a None value
self.no_press = bool(no_press)
self.no_release = bool(no_release)
self._handle_press = on_press
self._handle_release = on_release
self.meta = meta
def __call__(
self, no_press: Optional[bool] = None, no_release: Optional[bool] = None
) -> Key:
if no_press is None and no_release is None:
return self
return type(self)(
code=self.code,
has_modifiers=self.has_modifiers,
no_press=no_press,
no_release=no_release,
on_press=self._handle_press,
on_release=self._handle_release,
meta=self.meta,
)
def __repr__(self):
return f'Key(code={self.code}, has_modifiers={self.has_modifiers})'
def on_press(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
if hasattr(self, '_pre_press_handlers'):
for fn in self._pre_press_handlers:
if not fn(self, keyboard, KC, coord_int):
return
self._handle_press(self, keyboard, KC, coord_int)
if hasattr(self, '_post_press_handlers'):
for fn in self._post_press_handlers:
fn(self, keyboard, KC, coord_int)
def on_release(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
if hasattr(self, '_pre_release_handlers'):
for fn in self._pre_release_handlers:
if not fn(self, keyboard, KC, coord_int):
return
self._handle_release(self, keyboard, KC, coord_int)
if hasattr(self, '_post_release_handlers'):
for fn in self._post_release_handlers:
fn(self, keyboard, KC, coord_int)
def clone(self) -> Key:
'''
Return a shallow clone of the current key without any pre/post press/release
handlers attached. Almost exclusively useful for creating non-colliding keys
to use such handlers.
'''
return type(self)(
code=self.code,
has_modifiers=self.has_modifiers,
no_press=self.no_press,
no_release=self.no_release,
on_press=self._handle_press,
on_release=self._handle_release,
meta=self.meta,
)
def before_press_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
Attach a callback to be run prior to the on_press handler for this key.
Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
If return value of the provided callback is evaluated to False, press
processing is cancelled. Exceptions are _not_ caught, and will likely
crash KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_pre_press_handlers'):
self._pre_press_handlers = []
self._pre_press_handlers.append(fn)
def after_press_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
Attach a callback to be run after the on_release handler for this key.
Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
The return value of the provided callback is discarded. Exceptions are _not_
caught, and will likely crash KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_post_press_handlers'):
self._post_press_handlers = []
self._post_press_handlers.append(fn)
def before_release_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
Attach a callback to be run prior to the on_release handler for this
key. Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
If return value of the provided callback evaluates to False, the release
processing is cancelled. Exceptions are _not_ caught, and will likely crash
KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_pre_release_handlers'):
self._pre_release_handlers = []
self._pre_release_handlers.append(fn)
def after_release_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
Attach a callback to be run after the on_release handler for this key.
Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
The return value of the provided callback is discarded. Exceptions are _not_
caught, and will likely crash KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_post_release_handlers'):
self._post_release_handlers = []
self._post_release_handlers.append(fn)
class ModifierKey(Key):
FAKE_CODE = const(-1)
def __call__(
self,
modified_key: Optional[Key] = None,
no_press: Optional[bool] = None,
no_release: Optional[bool] = None,
) -> Key:
if modified_key is None:
return super().__call__(no_press=no_press, no_release=no_release)
modifiers = set()
code = modified_key.code
if self.code != ModifierKey.FAKE_CODE:
modifiers.add(self.code)
if self.has_modifiers:
modifiers |= self.has_modifiers
if modified_key.has_modifiers:
modifiers |= modified_key.has_modifiers
if isinstance(modified_key, ModifierKey):
if modified_key.code != ModifierKey.FAKE_CODE:
modifiers.add(modified_key.code)
code = ModifierKey.FAKE_CODE
return type(modified_key)(
code=code,
has_modifiers=modifiers,
no_press=no_press,
no_release=no_release,
on_press=modified_key._handle_press,
on_release=modified_key._handle_release,
meta=modified_key.meta,
)
def __repr__(self):
return f'ModifierKey(code={self.code}, has_modifiers={self.has_modifiers})'
class ConsumerKey(Key):
pass
class MouseKey(Key):
pass
def make_key(
code: Optional[int] = None,
names: Tuple[str, ...] = tuple(), # NOQA
type: KeyType = KeyType.SIMPLE,
**kwargs,
) -> Key:
'''
Create a new key, aliased by `names` in the KC lookup table.
If a code is not specified, the key is assumed to be a custom
internal key to be handled in a state callback rather than
sent directly to the OS. These codes will autoincrement.
Names are globally unique. If a later key is created with
the same name as an existing entry in `KC`, it will overwrite
the existing entry.
Names are case sensitive.
All **kwargs are passed to the Key constructor
'''
global NEXT_AVAILABLE_KEY
if type == KeyType.SIMPLE:
constructor = Key
elif type == KeyType.MODIFIER:
constructor = ModifierKey
elif type == KeyType.CONSUMER:
constructor = ConsumerKey
elif type == KeyType.MOUSE:
constructor = MouseKey
else:
raise ValueError('Unrecognized key type')
if code is None:
code = NEXT_AVAILABLE_KEY
NEXT_AVAILABLE_KEY += 1
elif code >= FIRST_KMK_INTERNAL_KEY:
# Try to ensure future auto-generated internal keycodes won't
# be overridden by continuing to +1 the sequence from the provided
# code
NEXT_AVAILABLE_KEY = max(NEXT_AVAILABLE_KEY, code + 1)
key = constructor(code=code, **kwargs)
for name in names:
KC[name] = key
return key
def make_mod_key(code: int, names: Tuple[str, ...], *args, **kwargs) -> Key:
return make_key(code, names, *args, **kwargs, type=KeyType.MODIFIER)
def make_shifted_key(code: int, names: Tuple[str, ...]) -> Key:
return make_key(code, names, has_modifiers={KC.LSFT.code})
def make_consumer_key(*args, **kwargs) -> Key:
return make_key(*args, **kwargs, type=KeyType.CONSUMER)
def make_mouse_key(*args, **kwargs) -> Key:
return make_key(*args, **kwargs, type=KeyType.MOUSE)
# Argumented keys are implicitly internal, so auto-gen of code
# is almost certainly the best plan here
def make_argumented_key(
validator: object = lambda *validator_args, **validator_kwargs: object(),
names: Tuple[str, ...] = tuple(), # NOQA
*constructor_args,
**constructor_kwargs,
) -> Key:
global NEXT_AVAILABLE_KEY
def _argumented_key(*user_args, **user_kwargs) -> Key:
global NEXT_AVAILABLE_KEY
meta = validator(*user_args, **user_kwargs)
if meta:
key = Key(
NEXT_AVAILABLE_KEY, meta=meta, *constructor_args, **constructor_kwargs
)
NEXT_AVAILABLE_KEY += 1
return key
else:
raise ValueError(
'Argumented key validator failed for unknown reasons. '
"This may not be the keymap's fault, as a more specific error "
'should have been raised.'
)
for name in names:
KC[name] = _argumented_key
return _argumented_key

547
kmk/kmk_keyboard.py Normal file
View file

@ -0,0 +1,547 @@
try:
from typing import Callable, Optional
except ImportError:
pass
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.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('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
active_layers = None
class KMKKeyboard:
#####
# User-configurable
keymap = []
coord_mapping = None
row_pins = None
col_pins = None
diode_orientation = None
matrix = None
unicode_mode = UnicodeMode.NOOP
modules = []
extensions = []
sandbox = Sandbox()
#####
# Internal State
keys_pressed = set()
axes = set()
_coordkeys_pressed = {}
hid_type = HIDModes.USB
secondary_hid_type = None
_hid_helper = None
_hid_send_enabled = False
hid_pending = False
matrix_update = None
secondary_matrix_update = None
matrix_update_queue = []
_trigger_powersave_enable = False
_trigger_powersave_disable = False
i2c_deinit_count = 0
_go_args = None
_processing_timeouts = False
_resume_buffer = []
_resume_buffer_x = []
# this should almost always be PREpended to, replaces
# former use of reversed_active_layers which had pointless
# overhead (the underlying list was never used anyway)
active_layers = [0]
_timeouts = {}
def __repr__(self) -> str:
return self.__class__.__name__
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)
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)
self.hid_pending = False
for axis in self.axes:
axis.move(self, 0)
def _handle_matrix_report(self, kevent: KeyEvent) -> None:
if kevent is not None:
self._on_matrix_changed(kevent)
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)
return None
for layer in self.active_layers:
try:
key = self.keymap[layer][idx]
except IndexError:
key = None
if debug.enabled:
debug('keymap IndexError: idx=', idx, ' layer=', layer)
if not key or key == KC.TRNS:
continue
return key
def _on_matrix_changed(self, kevent: KeyEvent) -> None:
int_coord = kevent.key_number
is_pressed = kevent.pressed
key = None
if not is_pressed:
try:
key = self._coordkeys_pressed[int_coord]
except KeyError:
if debug.enabled:
debug('release w/o press: ', int_coord)
if key is None:
key = self._find_key_in_map(int_coord)
if key is None:
return
if debug.enabled:
debug(kevent, ': ', key)
self.pre_process_key(key, is_pressed, int_coord)
def _process_resume_buffer(self):
'''
Resume the processing of buffered, delayed, deferred, etc. key events
emitted by modules.
We use a copy of the `_resume_buffer` as a working buffer. The working
buffer holds all key events in the correct order for processing. If
during processing new events are pushed to the `_resume_buffer`, they
are prepended to the working buffer (which may not be emptied), in
order to preserve key event order.
We also double-buffer `_resume_buffer` with `_resume_buffer_x`, only
copying the reference to hopefully safe some time on allocations.
'''
buffer, self._resume_buffer = self._resume_buffer, self._resume_buffer_x
while buffer:
ksf = buffer.pop(0)
key = ksf.key
# Handle any unaccounted-for layer shifts by looking up the key resolution again.
if ksf.int_coord is not None:
key = self._find_key_in_map(ksf.int_coord)
# Resume the processing of the key event and update the HID report
# when applicable.
self.pre_process_key(key, ksf.is_pressed, ksf.int_coord, ksf.index)
if self.hid_pending:
self._send_hid()
self.hid_pending = False
# Any newly buffered key events must be prepended to the working
# buffer.
if self._resume_buffer:
self._resume_buffer.extend(buffer)
buffer.clear()
buffer, self._resume_buffer = self._resume_buffer, buffer
self._resume_buffer_x = buffer
@property
def debug_enabled(self) -> bool:
return debug.enabled
@debug_enabled.setter
def debug_enabled(self, enabled: bool):
debug.enabled = enabled
def pre_process_key(
self,
key: Key,
is_pressed: bool,
int_coord: Optional[int] = None,
index: int = 0,
) -> None:
for module in self.modules[index:]:
try:
key = module.process_key(self, key, is_pressed, int_coord)
if key is None:
break
except Exception as err:
debug_error(module, 'process_key', err)
if int_coord is not None:
if is_pressed:
self._coordkeys_pressed[int_coord] = key
else:
try:
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)
if key:
self.process_key(key, is_pressed, int_coord)
def process_key(
self, key: Key, is_pressed: bool, int_coord: Optional[int] = None
) -> None:
if is_pressed:
key.on_press(self, int_coord)
else:
key.on_release(self, int_coord)
def resume_process_key(
self,
module: Module,
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)
ksf = KeyBufferFrame(
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
)
self._resume_buffer.append(ksf)
def remove_key(self, keycode: Key) -> None:
self.keys_pressed.discard(keycode)
self.process_key(keycode, False)
def add_key(self, keycode: Key) -> None:
self.keys_pressed.add(keycode)
self.process_key(keycode, True)
def tap_key(self, keycode: Key) -> None:
self.add_key(keycode)
# On the next cycle, we'll remove the key.
self.set_timeout(0, lambda: self.remove_key(keycode))
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:
cancel_task(timeout_key)
def _process_timeouts(self) -> None:
for task in get_due_task():
task()
def _init_sanity_check(self) -> None:
'''
Ensure the provided configuration is *probably* bootable
'''
assert self.keymap, 'must define a keymap with at least one row'
assert (
self.hid_type in HIDModes.ALL_MODES
), 'hid_type must be a value from kmk.consts.HIDModes'
if not self.matrix:
assert self.row_pins, 'no GPIO pins defined for matrix rows'
assert self.col_pins, 'no GPIO pins defined for matrix columns'
assert (
self.diode_orientation is not None
), 'diode orientation must be defined'
def _init_coord_mapping(self) -> None:
'''
Attempt to sanely guess a coord_mapping if one is not provided. No-op
if `kmk.extensions.split.Split` is used, it provides equivalent
functionality in `on_bootup`
To save RAM on boards that don't use Split, we don't import Split
and do an isinstance check, but instead do string detection
'''
if any(x.__class__.__module__ == 'kmk.modules.split' for x in self.modules):
return
if not self.coord_mapping:
cm = []
for m in self.matrix:
cm.extend(m.coord_mapping)
self.coord_mapping = tuple(cm)
def _init_hid(self) -> None:
if self.hid_type == HIDModes.NOOP:
self._hid_helper = AbstractHID
elif self.hid_type == HIDModes.USB:
self._hid_helper = USBHID
elif self.hid_type == HIDModes.BLE:
self._hid_helper = BLEHID
else:
self._hid_helper = AbstractHID
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:
self.matrix = MatrixScanner(
column_pins=self.col_pins,
row_pins=self.row_pins,
columns_to_anodes=self.diode_orientation,
)
try:
self.matrix = tuple(iter(self.matrix))
offset = 0
for matrix in self.matrix:
matrix.offset = offset
offset += matrix.key_count
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)
for ext in self.extensions:
try:
ext.before_matrix_scan(self.sandbox)
except Exception as 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:
debug_error(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)
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)
for ext in self.extensions:
try:
ext.before_hid_send(self.sandbox)
except Exception as 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:
debug_error(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)
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)
for ext in self.extensions:
try:
ext.on_powersave_enable(self.sandbox)
except Exception as 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:
debug_error(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)
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)
try:
while True:
self._main_loop()
finally:
debug('Unexpected error: cleaning up')
self._deinit_hid()
self.deinit()
def _init(
self,
hid_type: HIDModes = HIDModes.USB,
secondary_hid_type: Optional[HIDModes] = None,
**kwargs,
) -> None:
self._go_args = kwargs
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_hid()
self._init_matrix()
self._init_coord_mapping()
self.during_bootup()
if debug.enabled:
import gc
gc.collect()
debug('mem_info used:', gc.mem_alloc(), ' free:', gc.mem_free())
def _main_loop(self) -> None:
self.sandbox.active_layers = self.active_layers.copy()
self.before_matrix_scan()
self._process_resume_buffer()
for matrix in self.matrix:
update = matrix.scan_for_changes()
if update:
self.matrix_update = update
break
self.sandbox.matrix_update = self.matrix_update
self.sandbox.secondary_matrix_update = self.secondary_matrix_update
self.after_matrix_scan()
if self.secondary_matrix_update:
self.matrix_update_queue.append(self.secondary_matrix_update)
self.secondary_matrix_update = None
if self.matrix_update:
self.matrix_update_queue.append(self.matrix_update)
self.matrix_update = None
# only handle one key per cycle.
if self.matrix_update_queue:
self._handle_matrix_report(self.matrix_update_queue.pop(0))
self.before_hid_send()
if self.hid_pending:
self._send_hid()
self._process_timeouts()
if self.hid_pending:
self._send_hid()
self.after_hid_send()
if self._trigger_powersave_enable:
self.powersave_enable()
if self._trigger_powersave_disable:
self.powersave_disable()

34
kmk/kmktime.py Normal file
View file

@ -0,0 +1,34 @@
from micropython import const
from supervisor import ticks_ms
_TICKS_PERIOD = const(1 << 29)
_TICKS_MAX = const(_TICKS_PERIOD - 1)
_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
def ticks_diff(new: int, start: int) -> int:
diff = (new - start) & _TICKS_MAX
diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
return diff
def ticks_add(ticks: int, delta: int) -> int:
return (ticks + delta) % _TICKS_PERIOD
def check_deadline(new: int, start: int, ms: int) -> int:
return ticks_diff(new, start) < ms
class PeriodicTimer:
def __init__(self, period: int):
self.period = period
self.last_tick = ticks_ms()
def tick(self) -> bool:
now = ticks_ms()
if ticks_diff(now, self.last_tick) >= self.period:
self.last_tick = now
return True
else:
return False

46
kmk/modules/__init__.py Normal file
View file

@ -0,0 +1,46 @@
class InvalidExtensionEnvironment(Exception):
pass
class Module:
'''
Modules differ from extensions in that they not only can read the state, but
are allowed to modify the state. The will be loaded on boot, and are not
allowed to be unloaded as they are required to continue functioning in a
consistant manner.
'''
# The below methods should be implemented by subclasses
def during_bootup(self, keyboard):
raise NotImplementedError
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
raise NotImplementedError
def after_matrix_scan(self, keyboard):
'''
Return value will be replace matrix update if supplied
'''
raise NotImplementedError
def process_key(self, keyboard, key, is_pressed, int_coord):
return key
def before_hid_send(self, keyboard):
raise NotImplementedError
def after_hid_send(self, keyboard):
raise NotImplementedError
def on_powersave_enable(self, keyboard):
raise NotImplementedError
def on_powersave_disable(self, keyboard):
raise NotImplementedError
def deinit(self, keyboard):
pass

227
kmk/modules/adns9800.py Normal file
View file

@ -0,0 +1,227 @@
import busio
import digitalio
import microcontroller
import time
from kmk.keys import AX
from kmk.modules import Module
from kmk.modules.adns9800_firmware import firmware
class REG:
Product_ID = 0x0
Revision_ID = 0x1
MOTION = 0x2
DELTA_X_L = 0x3
DELTA_X_H = 0x4
DELTA_Y_L = 0x5
DELTA_Y_H = 0x6
SQUAL = 0x7
PIXEL_SUM = 0x8
Maximum_Pixel = 0x9
Minimum_Pixel = 0xA
Shutter_Lower = 0xB
Shutter_Upper = 0xC
Frame_Period_Lower = 0xD
Frame_Period_Upper = 0xE
Configuration_I = 0xF
Configuration_II = 0x10
Frame_Capture = 0x12
SROM_Enable = 0x13
Run_Downshift = 0x14
Rest1_Rate = 0x15
Rest1_Downshift = 0x16
Rest2_Rate = 0x17
Rest2_Downshift = 0x18
Rest3_Rate = 0x19
Frame_Period_Max_Bound_Lower = 0x1A
Frame_Period_Max_Bound_Upper = 0x1B
Frame_Period_Min_Bound_Lower = 0x1C
Frame_Period_Min_Bound_Upper = 0x1D
Shutter_Max_Bound_Lower = 0x1E
Shutter_Max_Bound_Upper = 0x1F
LASER_CTRL0 = 0x20
Observation = 0x24
Data_Out_Lower = 0x25
Data_Out_Upper = 0x26
SROM_ID = 0x2A
Lift_Detection_Thr = 0x2E
Configuration_V = 0x2F
Configuration_IV = 0x39
Power_Up_Reset = 0x3A
Shutdown = 0x3B
Inverse_Product_ID = 0x3F
Snap_Angle = 0x42
Motion_Burst = 0x50
SROM_Load_Burst = 0x62
Pixel_Burst = 0x64
class ADNS9800(Module):
tswr = tsww = 120
tsrw = tsrr = 20
tsrad = 100
tbexit = 1
baud = 2000000
cpol = 1
cpha = 1
DIR_WRITE = 0x80
DIR_READ = 0x7F
def __init__(self, cs, sclk, miso, mosi, invert_x=False, invert_y=False):
self.cs = digitalio.DigitalInOut(cs)
self.cs.direction = digitalio.Direction.OUTPUT
self.spi = busio.SPI(clock=sclk, MOSI=mosi, MISO=miso)
self.invert_x = invert_x
self.invert_y = invert_y
def adns_start(self):
self.cs.value = False
def adns_stop(self):
self.cs.value = True
def adns_write(self, reg, data):
while not self.spi.try_lock():
pass
try:
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
self.adns_start()
self.spi.write(bytes([reg | self.DIR_WRITE, data]))
finally:
self.spi.unlock()
self.adns_stop()
def adns_read(self, reg):
result = bytearray(1)
while not self.spi.try_lock():
pass
try:
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
self.adns_start()
self.spi.write(bytes([reg & self.DIR_READ]))
microcontroller.delay_us(self.tsrad)
self.spi.readinto(result)
finally:
self.spi.unlock()
self.adns_stop()
return result[0]
def adns_upload_srom(self):
while not self.spi.try_lock():
pass
try:
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
self.adns_start()
self.spi.write(bytes([REG.SROM_Load_Burst | self.DIR_WRITE]))
for b in firmware:
self.spi.write(bytes([b]))
finally:
self.spi.unlock()
self.adns_stop()
def delta_to_int(self, high, low):
comp = (high << 8) | low
if comp & 0x8000:
return (-1) * (0xFFFF + 1 - comp)
return comp
def adns_read_motion(self):
result = bytearray(14)
while not self.spi.try_lock():
pass
try:
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
self.adns_start()
self.spi.write(bytes([REG.Motion_Burst & self.DIR_READ]))
microcontroller.delay_us(self.tsrad)
self.spi.readinto(result)
finally:
self.spi.unlock()
self.adns_stop()
microcontroller.delay_us(self.tbexit)
self.adns_write(REG.MOTION, 0x0)
return result
def during_bootup(self, keyboard):
self.adns_write(REG.Power_Up_Reset, 0x5A)
time.sleep(0.1)
self.adns_read(REG.MOTION)
microcontroller.delay_us(self.tsrr)
self.adns_read(REG.DELTA_X_L)
microcontroller.delay_us(self.tsrr)
self.adns_read(REG.DELTA_X_H)
microcontroller.delay_us(self.tsrr)
self.adns_read(REG.DELTA_Y_L)
microcontroller.delay_us(self.tsrr)
self.adns_read(REG.DELTA_Y_H)
microcontroller.delay_us(self.tsrw)
self.adns_write(REG.Configuration_IV, 0x2)
microcontroller.delay_us(self.tsww)
self.adns_write(REG.SROM_Enable, 0x1D)
microcontroller.delay_us(1000)
self.adns_write(REG.SROM_Enable, 0x18)
microcontroller.delay_us(self.tsww)
self.adns_upload_srom()
microcontroller.delay_us(2000)
laser_ctrl0 = self.adns_read(REG.LASER_CTRL0)
microcontroller.delay_us(self.tsrw)
self.adns_write(REG.LASER_CTRL0, laser_ctrl0 & 0xF0)
microcontroller.delay_us(self.tsww)
self.adns_write(REG.Configuration_I, 0x10)
microcontroller.delay_us(self.tsww)
if keyboard.debug_enabled:
print('ADNS: Product ID ', hex(self.adns_read(REG.Product_ID)))
microcontroller.delay_us(self.tsrr)
print('ADNS: Revision ID ', hex(self.adns_read(REG.Revision_ID)))
microcontroller.delay_us(self.tsrr)
print('ADNS: SROM ID ', hex(self.adns_read(REG.SROM_ID)))
microcontroller.delay_us(self.tsrr)
if self.adns_read(REG.Observation) & 0x20:
print('ADNS: Sensor is running SROM')
else:
print('ADNS: Error! Sensor is not runnin SROM!')
return
def before_matrix_scan(self, keyboard):
motion = self.adns_read_motion()
if motion[0] & 0x80:
delta_x = self.delta_to_int(motion[3], motion[2])
delta_y = self.delta_to_int(motion[5], motion[4])
if self.invert_x:
delta_x *= -1
if self.invert_y:
delta_y *= -1
if delta_x:
AX.X.move(delta_x)
if delta_y:
AX.Y.move(delta_y)
if keyboard.debug_enabled:
print('Delta: ', delta_x, ' ', delta_y)
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return

99
kmk/modules/capsword.py Normal file
View file

@ -0,0 +1,99 @@
from kmk.keys import FIRST_KMK_INTERNAL_KEY, KC, ModifierKey, make_key
from kmk.modules import Module
class CapsWord(Module):
# default timeout is 8000
# alphabets, numbers and few more keys will not disable capsword
def __init__(self, timeout=8000):
self._alphabets = range(KC.A.code, KC.Z.code + 1)
self._numbers = range(KC.N1.code, KC.N0.code + 1)
self.keys_ignored = [
KC.MINS,
KC.BSPC,
KC.UNDS,
]
self._timeout_key = False
self._cw_active = False
self.timeout = timeout
make_key(
names=(
'CAPSWORD',
'CW',
),
on_press=self.cw_pressed,
)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def process_key(self, keyboard, key, is_pressed, int_coord):
if self._cw_active and key != KC.CW:
continue_cw = False
# capitalize alphabets
if key.code in self._alphabets:
continue_cw = True
keyboard.process_key(KC.LSFT, is_pressed)
elif (
key.code in self._numbers
or isinstance(key, ModifierKey)
or key in self.keys_ignored
or key.code
>= FIRST_KMK_INTERNAL_KEY # user defined keys are also ignored
):
continue_cw = True
# requests and cancels existing timeouts
if is_pressed:
if continue_cw:
self.discard_timeout(keyboard)
self.request_timeout(keyboard)
else:
self.process_timeout()
return key
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def process_timeout(self):
self._cw_active = False
self._timeout_key = False
def request_timeout(self, keyboard):
if self._cw_active:
if self.timeout:
self._timeout_key = keyboard.set_timeout(
self.timeout, lambda: self.process_timeout()
)
def discard_timeout(self, keyboard):
if self._timeout_key:
if self.timeout:
keyboard.cancel_timeout(self._timeout_key)
self._timeout_key = False
def cw_pressed(self, key, keyboard, *args, **kwargs):
# enables/disables capsword
if key == KC.CW:
if not self._cw_active:
self._cw_active = True
self.discard_timeout(keyboard)
self.request_timeout(keyboard)
else:
self.discard_timeout(keyboard)
self.process_timeout()

70
kmk/modules/cg_swap.py Normal file
View file

@ -0,0 +1,70 @@
from kmk.keys import KC, ModifierKey, make_key
from kmk.modules import Module
class CgSwap(Module):
# default cg swap is disabled, can be eanbled too if needed
def __init__(self, cg_swap_enable=False):
self.cg_swap_enable = cg_swap_enable
self._cg_mapping = {
KC.LCTL: KC.LGUI,
KC.RCTL: KC.RGUI,
KC.LGUI: KC.LCTL,
KC.RGUI: KC.RCTL,
}
make_key(
names=('CG_SWAP',),
)
make_key(
names=('CG_NORM',),
)
make_key(
names=('CG_TOGG',),
)
def during_bootup(self, keyboard):
return
def matrix_detected_press(self, keyboard):
return keyboard.matrix_update is None
def before_matrix_scan(self, keyboard):
return
def process_key(self, keyboard, key, is_pressed, int_coord):
if is_pressed:
# enables or disables or toggles cg swap
if key == KC.CG_SWAP:
self.cg_swap_enable = True
elif key == KC.CG_NORM:
self.cg_swap_enable = False
elif key == KC.CG_TOGG:
if not self.cg_swap_enable:
self.cg_swap_enable = True
else:
self.cg_swap_enable = False
# performs cg swap
if (
self.cg_swap_enable
and key not in (KC.CG_SWAP, KC.CG_NORM, KC.CG_TOGG)
and isinstance(key, ModifierKey)
and key in self._cg_mapping
):
key = self._cg_mapping.get(key)
return key
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return

335
kmk/modules/combos.py Normal file
View file

@ -0,0 +1,335 @@
try:
from typing import Optional, Tuple, Union
except ImportError:
pass
from micropython import const
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:
RESET = const(0)
MATCHING = const(1)
ACTIVE = const(2)
IDLE = const(3)
class Combo:
fast_reset = False
per_key_timeout = False
timeout = 50
_remaining = []
_timeout = None
_state = _ComboState.IDLE
_match_coord = False
def __init__(
self,
match: Tuple[Union[Key, int], ...],
result: Key,
fast_reset=None,
per_key_timeout=None,
timeout=None,
match_coord=None,
):
'''
match: tuple of keys (KC.A, KC.B)
result: key KC.C
'''
self.match = match
self.result = result
if fast_reset is not None:
self.fast_reset = fast_reset
if per_key_timeout is not None:
self.per_key_timeout = per_key_timeout
if timeout is not None:
self.timeout = timeout
if match_coord is not None:
self._match_coord = match_coord
def __repr__(self):
return f'{self.__class__.__name__}({[k.code for k in self.match]})'
def matches(self, key: Key, int_coord: int):
raise NotImplementedError
def has_match(self, key: Key, int_coord: int):
return self._match_coord and int_coord in self.match or key in self.match
def insert(self, key: Key, int_coord: int):
if self._match_coord:
self._remaining.insert(0, int_coord)
else:
self._remaining.insert(0, key)
def reset(self):
self._remaining = list(self.match)
class Chord(Combo):
def matches(self, key: Key, int_coord: int):
if not self._match_coord and key in self._remaining:
self._remaining.remove(key)
return True
elif self._match_coord and int_coord in self._remaining:
self._remaining.remove(int_coord)
return True
else:
return False
class Sequence(Combo):
fast_reset = True
per_key_timeout = True
timeout = 1000
def matches(self, key: Key, int_coord: int):
if (
not self._match_coord and self._remaining and self._remaining[0] == key
) or (
self._match_coord and self._remaining and self._remaining[0] == int_coord
):
self._remaining.pop(0)
return True
else:
return False
class Combos(Module):
def __init__(self, combos=[]):
self.combos = combos
self._key_buffer = []
make_key(
names=('LEADER', 'LDR'),
on_press=handlers.passthrough,
on_release=handlers.passthrough,
)
def during_bootup(self, keyboard):
self.reset(keyboard)
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def process_key(self, keyboard, key: Key, is_pressed, int_coord):
if is_pressed:
return self.on_press(keyboard, key, int_coord)
else:
return self.on_release(keyboard, key, int_coord)
def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
# refill potential matches from timed-out matches
if self.count_matching() == 0:
for combo in self.combos:
if combo._state == _ComboState.RESET:
combo._state = _ComboState.MATCHING
# filter potential matches
for combo in self.combos:
if combo._state != _ComboState.MATCHING:
continue
if combo.matches(key, int_coord):
continue
combo._state = _ComboState.IDLE
if combo._timeout:
keyboard.cancel_timeout(combo._timeout)
combo._timeout = keyboard.set_timeout(
combo.timeout, lambda c=combo: self.reset_combo(keyboard, c)
)
match_count = self.count_matching()
if match_count:
# At least one combo matches current key: append key to buffer.
self._key_buffer.append((int_coord, key, True))
key = None
for first_match in self.combos:
if first_match._state == _ComboState.MATCHING:
break
# Single match left: don't wait on timeout to activate
if match_count == 1 and not any(first_match._remaining):
combo = first_match
self.activate(keyboard, combo)
if combo._timeout:
keyboard.cancel_timeout(combo._timeout)
combo._timeout = None
self._key_buffer = []
self.reset(keyboard)
# Start or reset individual combo timeouts.
for combo in self.combos:
if combo._state != _ComboState.MATCHING:
continue
if combo._timeout:
if combo.per_key_timeout:
keyboard.cancel_timeout(combo._timeout)
else:
continue
combo._timeout = keyboard.set_timeout(
combo.timeout, lambda c=combo: self.on_timeout(keyboard, c)
)
else:
# There's no matching combo: send and reset key buffer
if self._key_buffer:
self._key_buffer.append((int_coord, key, True))
self.send_key_buffer(keyboard)
self._key_buffer = []
key = None
return key
def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
for combo in self.combos:
if combo._state != _ComboState.ACTIVE:
continue
if combo.has_match(key, int_coord):
# Deactivate combo if it matches current key.
self.deactivate(keyboard, combo)
if combo.fast_reset:
self.reset_combo(keyboard, combo)
self._key_buffer = []
else:
combo.insert(key, int_coord)
combo._state = _ComboState.MATCHING
key = None
break
else:
# Non-active but matching combos can either activate on key release
# if they're the only match, or "un-match" the released key but stay
# matching if they're a repeatable combo.
for combo in self.combos:
if combo._state != _ComboState.MATCHING:
continue
if not combo.has_match(key, int_coord):
continue
# Combo matches, but first key released before timeout.
elif not any(combo._remaining) and self.count_matching() == 1:
keyboard.cancel_timeout(combo._timeout)
self.activate(keyboard, combo)
self._key_buffer = []
keyboard._send_hid()
self.deactivate(keyboard, combo)
if combo.fast_reset:
self.reset_combo(keyboard, combo)
else:
combo.insert(key, int_coord)
combo._state = _ComboState.MATCHING
self.reset(keyboard)
elif not any(combo._remaining):
continue
# Skip combos that allow tapping.
elif combo.fast_reset:
continue
# This was the last key released of a repeatable combo.
elif len(combo._remaining) == len(combo.match) - 1:
self.reset_combo(keyboard, combo)
if not self.count_matching():
self._key_buffer.append((int_coord, key, False))
self.send_key_buffer(keyboard)
self._key_buffer = []
key = None
# Anything between first and last key released.
else:
combo.insert(key, int_coord)
# Don't propagate key-release events for keys that have been
# buffered. Append release events only if corresponding press is in
# buffer.
pressed = self._key_buffer.count((int_coord, key, True))
released = self._key_buffer.count((int_coord, key, False))
if (pressed - released) > 0:
self._key_buffer.append((int_coord, key, False))
key = None
# Reset on non-combo key up
if not self.count_matching():
self.reset(keyboard)
return key
def on_timeout(self, keyboard, combo):
# If combo reaches timeout and has no remaining keys, activate it;
# else, drop it from the match list.
combo._timeout = None
if not any(combo._remaining):
self.activate(keyboard, combo)
# check if the last buffered key event was a 'release'
if not self._key_buffer[-1][2]:
keyboard._send_hid()
self.deactivate(keyboard, combo)
self._key_buffer = []
self.reset(keyboard)
else:
if self.count_matching() == 1:
# This was the last pending combo: flush key buffer.
self.send_key_buffer(keyboard)
self._key_buffer = []
self.reset_combo(keyboard, combo)
def send_key_buffer(self, keyboard):
for (int_coord, key, is_pressed) in self._key_buffer:
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
def reset_combo(self, keyboard, combo):
combo.reset()
if combo._timeout is not None:
keyboard.cancel_timeout(combo._timeout)
combo._timeout = None
combo._state = _ComboState.RESET
def reset(self, keyboard):
for combo in self.combos:
if combo._state != _ComboState.ACTIVE:
self.reset_combo(keyboard, combo)
def count_matching(self):
match_count = 0
for combo in self.combos:
if combo._state == _ComboState.MATCHING:
match_count += 1
return match_count

View file

@ -0,0 +1,259 @@
from micropython import const
from supervisor import ticks_ms
from collections import namedtuple
from kmk.keys import KC, make_argumented_key
from kmk.kmktime import check_deadline, ticks_diff
from kmk.modules import Module
class DSMeta:
def __init__(self, sequence_select=None):
self.sequence_select = sequence_select
class SequenceStatus:
STOPPED = const(0)
RECORDING = const(1)
PLAYING = const(2)
SET_REPEPITIONS = const(3)
SET_INTERVAL = const(4)
# Keycodes for number keys
_numbers = range(KC.N1.code, KC.N0.code + 1)
SequenceFrame = namedtuple('SequenceFrame', ['keys_pressed', 'timestamp'])
class Sequence:
def __init__(self):
self.repetitions = 1
self.interval = 0
self.sequence_data = [SequenceFrame(set(), 0) for i in range(3)]
class DynamicSequences(Module):
def __init__(
self, slots=1, timeout=60000, key_interval=0, use_recorded_speed=False
):
self.sequences = [Sequence() for i in range(slots)]
self.current_slot = self.sequences[0]
self.status = SequenceStatus.STOPPED
self.index = 0
self.start_time = 0
self.current_repetition = 0
self.last_config_frame = set()
self.timeout = timeout
self.key_interval = key_interval
self.use_recorded_speed = use_recorded_speed
# Create keycodes
make_argumented_key(
validator=DSMeta, names=('RECORD_SEQUENCE',), on_press=self._record_sequence
)
make_argumented_key(
validator=DSMeta, names=('PLAY_SEQUENCE',), on_press=self._play_sequence
)
make_argumented_key(
validator=DSMeta,
names=(
'SET_SEQUENCE',
'STOP_SEQUENCE',
),
on_press=self._stop_sequence,
)
make_argumented_key(
validator=DSMeta,
names=('SET_SEQUENCE_REPETITIONS',),
on_press=self._set_sequence_repetitions,
)
make_argumented_key(
validator=DSMeta,
names=('SET_SEQUENCE_INTERVAL',),
on_press=self._set_sequence_interval,
)
def _record_sequence(self, key, keyboard, *args, **kwargs):
self._stop_sequence(key, keyboard)
self.status = SequenceStatus.RECORDING
self.start_time = ticks_ms()
self.current_slot.sequence_data = [SequenceFrame(set(), 0)]
self.index = 0
def _play_sequence(self, key, keyboard, *args, **kwargs):
self._stop_sequence(key, keyboard)
self.status = SequenceStatus.PLAYING
self.start_time = ticks_ms()
self.index = 0
self.current_repetition = 0
def _stop_sequence(self, key, keyboard, *args, **kwargs):
if self.status == SequenceStatus.RECORDING:
self.stop_recording()
elif self.status == SequenceStatus.SET_INTERVAL:
self.stop_config()
self.status = SequenceStatus.STOPPED
# Change sequences here because stop is always called
if key.meta.sequence_select is not None:
self.current_slot = self.sequences[key.meta.sequence_select]
# Configure repeat settings
def _set_sequence_repetitions(self, key, keyboard, *args, **kwargs):
self._stop_sequence(key, keyboard)
self.status = SequenceStatus.SET_REPEPITIONS
self.last_config_frame = set()
self.current_slot.repetitions = 0
self.start_time = ticks_ms()
def _set_sequence_interval(self, key, keyboard, *args, **kwargs):
self._stop_sequence(key, keyboard)
self.status = SequenceStatus.SET_INTERVAL
self.last_config_frame = set()
self.current_slot.interval = 0
self.start_time = ticks_ms()
# Add the current keypress state to the sequence
def record_frame(self, keys_pressed):
if self.current_slot.sequence_data[self.index].keys_pressed != keys_pressed:
self.index += 1
# Recorded speed
if self.use_recorded_speed:
self.current_slot.sequence_data.append(
SequenceFrame(
keys_pressed.copy(), ticks_diff(ticks_ms(), self.start_time)
)
)
# Constant speed
else:
self.current_slot.sequence_data.append(
SequenceFrame(keys_pressed.copy(), self.index * self.key_interval)
)
if not check_deadline(ticks_ms(), self.start_time, self.timeout):
self.stop_recording()
# Add the ending frames to the sequence
def stop_recording(self):
# Clear the remaining keys
self.current_slot.sequence_data.append(
SequenceFrame(set(), self.current_slot.sequence_data[-1].timestamp + 20)
)
# Wait for the specified interval
prev_timestamp = self.current_slot.sequence_data[-1].timestamp
self.current_slot.sequence_data.append(
SequenceFrame(
set(),
prev_timestamp + self.current_slot.interval * 1000,
)
)
self.status = SequenceStatus.STOPPED
def play_frame(self, keyboard):
# Send the keypresses at this point in the sequence
if not check_deadline(
ticks_ms(),
self.start_time,
self.current_slot.sequence_data[self.index].timestamp,
):
if self.index:
prev = self.current_slot.sequence_data[self.index - 1].keys_pressed
cur = self.current_slot.sequence_data[self.index].keys_pressed
for key in prev.difference(cur):
keyboard.remove_key(key)
for key in cur.difference(prev):
keyboard.add_key(key)
self.index += 1
if self.index >= len(self.current_slot.sequence_data): # Reached the end
self.current_repetition += 1
if self.current_repetition == self.current_slot.repetitions:
self.status = SequenceStatus.STOPPED
else:
self.index = 0
self.start_time = ticks_ms()
# Configuration for repeating sequences
def config_mode(self, keyboard):
for key in keyboard.keys_pressed.difference(self.last_config_frame):
if key.code in _numbers:
digit = (key.code - KC.N1.code + 1) % 10
if self.status == SequenceStatus.SET_REPEPITIONS:
self.current_slot.repetitions = (
self.current_slot.repetitions * 10 + digit
)
elif self.status == SequenceStatus.SET_INTERVAL:
self.current_slot.interval = self.current_slot.interval * 10 + digit
elif key.code == KC.ENTER.code:
self.stop_config()
self.last_config_frame = keyboard.keys_pressed.copy()
keyboard.hid_pending = False # Disable typing
if not check_deadline(ticks_ms(), self.start_time, self.timeout):
self.stop_config()
# Finish configuring repetitions
def stop_config(self):
self.current_slot.sequence_data[-1] = SequenceFrame(
self.current_slot.sequence_data[-1].keys_pressed,
self.current_slot.sequence_data[-2].timestamp
+ self.current_slot.interval * 1000,
)
self.current_slot.repetitions = max(self.current_slot.repetitions, 1)
self.status = SequenceStatus.STOPPED
def on_runtime_enable(self, keyboard):
return
def on_runtime_disable(self, keyboard):
return
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
if not self.status:
return
elif self.status == SequenceStatus.RECORDING:
self.record_frame(keyboard.keys_pressed)
elif self.status == SequenceStatus.PLAYING:
self.play_frame(keyboard)
elif (
self.status == SequenceStatus.SET_REPEPITIONS
or self.status == SequenceStatus.SET_INTERVAL
):
self.config_mode(keyboard)
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return

130
kmk/modules/easypoint.py Normal file
View file

@ -0,0 +1,130 @@
'''
Extension handles usage of AS5013 by AMS
'''
from supervisor import ticks_ms
from kmk.keys import AX
from kmk.modules import Module
I2C_ADDRESS = 0x40
I2X_ALT_ADDRESS = 0x41
X = 0x10
Y_RES_INT = 0x11
XP = 0x12
XN = 0x13
YP = 0x14
YN = 0x15
M_CTRL = 0x2B
T_CTRL = 0x2D
Y_OFFSET = 17
X_OFFSET = 7
DEAD_X = 5
DEAD_Y = 5
class Easypoint(Module):
'''Module handles usage of AS5013 by AMS'''
def __init__(
self,
i2c,
address=I2C_ADDRESS,
y_offset=Y_OFFSET,
x_offset=X_OFFSET,
dead_x=DEAD_X,
dead_y=DEAD_Y,
):
self._i2c_address = address
self._i2c_bus = i2c
# HID parameters
self.polling_interval = 20
self.last_tick = ticks_ms()
# Offsets for poor soldering
self.y_offset = y_offset
self.x_offset = x_offset
# Deadzone
self.dead_x = DEAD_X
self.dead_y = DEAD_Y
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
now = ticks_ms()
if now - self.last_tick < self.polling_interval:
return
self.last_tick = now
x, y = self._read_raw_state()
# I'm a shit coder, so offset is handled in software side
s_x = self.getSignedNumber(x, 8) - self.x_offset
s_y = self.getSignedNumber(y, 8) - self.y_offset
# Evaluate Deadzone
if s_x in range(-self.dead_x, self.dead_x) and s_y in range(
-self.dead_y, self.dead_y
):
# Within bounds, just die
return
else:
# Set the X/Y from easypoint
AX.X.move(keyboard, x)
AX.Y.move(keyboard, y)
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def _read_raw_state(self):
'''Read data from AS5013'''
x, y = self._i2c_rdwr([X], length=2)
return x, y
def getSignedNumber(self, number, bitLength=8):
mask = (2**bitLength) - 1
if number & (1 << (bitLength - 1)):
return number | ~mask
else:
return number & mask
def _i2c_rdwr(self, data, length=1):
'''Write and optionally read I2C data.'''
while not self._i2c_bus.try_lock():
pass
try:
if length > 0:
result = bytearray(length)
self._i2c_bus.writeto_then_readfrom(
self._i2c_address, bytes(data), result
)
return result
else:
self._i2c_bus.writeto(self._i2c_address, bytes(data))
return []
finally:
self._i2c_bus.unlock()

312
kmk/modules/encoder.py Normal file
View file

@ -0,0 +1,312 @@
# See docs/encoder.md for how to use
import busio
import digitalio
from supervisor import ticks_ms
from kmk.modules import Module
# NB : not using rotaryio as it requires the pins to be consecutive
class BaseEncoder:
VELOCITY_MODE = True
def __init__(self, is_inverted=False, divisor=4):
self.is_inverted = is_inverted
self.divisor = divisor
self._state = None
self._start_state = None
self._direction = None
self._pos = 0
self._button_state = True
self._button_held = None
self._velocity = 0
self._movement = 0
self._timestamp = ticks_ms()
# callback functions on events. Need to be defined externally
self.on_move_do = None
self.on_button_do = None
def get_state(self):
return {
'direction': self.is_inverted and -self._direction or self._direction,
'position': self.is_inverted and -self._pos or self._pos,
'is_pressed': not self._button_state,
'velocity': self._velocity,
}
# Called in a loop to refresh encoder state
def update_state(self):
# Rotation events
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
if new_state != self._state:
# encoder moved
self._movement += 1
# false / false and true / true are common half steps
# looking on the step just before helps determining
# the direction
if new_state[0] == new_state[1] and self._state[0] != self._state[1]:
if new_state[1] == self._state[0]:
self._direction = 1
else:
self._direction = -1
# when the encoder settles on a position (every 2 steps)
if new_state[0] == new_state[1]:
# an encoder returned to the previous
# position halfway, cancel rotation
if (
self._start_state[0] == new_state[0]
and self._start_state[1] == new_state[1]
and self._movement <= 2
):
self._movement = 0
self._direction = 0
# when the encoder made a full loop according to its divisor
elif self._movement >= self.divisor - 1:
# 1 full step is 4 movements (2 for high-resolution encoder),
# however, when rotated quickly, some steps may be missed.
# This makes it behave more naturally
real_movement = self._movement // self.divisor
self._pos += self._direction * real_movement
if self.on_move_do is not None:
for i in range(real_movement):
self.on_move_do(self.get_state())
# Rotation finished, reset to identify new movement
self._movement = 0
self._direction = 0
self._start_state = new_state
self._state = new_state
# Velocity
self.velocity_event()
# Button event
self.button_event()
def velocity_event(self):
if self.VELOCITY_MODE:
new_timestamp = ticks_ms()
self._velocity = new_timestamp - self._timestamp
self._timestamp = new_timestamp
def button_event(self):
raise NotImplementedError('subclasses must override button_event()!')
# return knob velocity as milliseconds between position changes (detents)
# for backwards compatibility
def vel_report(self):
# print(self._velocity)
return self._velocity
class GPIOEncoder(BaseEncoder):
def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False, divisor=None):
super().__init__(is_inverted)
# Divisor can be 4 or 2 depending on whether the detent
# on the encoder is defined by 2 or 4 pulses
self.divisor = divisor
self.pin_a = EncoderPin(pin_a)
self.pin_b = EncoderPin(pin_b)
self.pin_button = (
EncoderPin(pin_button, button_type=True) if pin_button is not None else None
)
self._state = (self.pin_a.get_value(), self.pin_b.get_value())
self._start_state = self._state
def button_event(self):
if self.pin_button:
new_button_state = self.pin_button.get_value()
if new_button_state != self._button_state:
self._button_state = new_button_state
if self.on_button_do is not None:
self.on_button_do(self.get_state())
class EncoderPin:
def __init__(self, pin, button_type=False):
self.pin = pin
self.button_type = button_type
self.prepare_pin()
def prepare_pin(self):
if self.pin is not None:
self.io = digitalio.DigitalInOut(self.pin)
self.io.direction = digitalio.Direction.INPUT
self.io.pull = digitalio.Pull.UP
else:
self.io = None
def get_value(self):
return self.io.value
class I2CEncoder(BaseEncoder):
def __init__(self, i2c, address, is_inverted=False):
try:
from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
except ImportError:
print('seesaw missing')
return
super().__init__(is_inverted)
self.seesaw = seesaw.Seesaw(i2c, address)
# Check for correct product
seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF
if seesaw_product != 4991:
print('Wrong firmware loaded? Expected 4991')
self.encoder = rotaryio.IncrementalEncoder(self.seesaw)
self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP)
self.switch = digitalio.DigitalIO(self.seesaw, 24)
self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1)
self._state = self.encoder.position
def update_state(self):
# Rotation events
new_state = self.encoder.position
if new_state != self._state:
# it moves !
self._movement += 1
# false / false and true / true are common half steps
# looking on the step just before helps determining
# the direction
if self.encoder.position > self._state:
self._direction = 1
else:
self._direction = -1
self._state = new_state
self.on_move_do(self.get_state())
# Velocity
self.velocity_event()
# Button events
self.button_event()
def button_event(self):
if not self.switch.value and not self._button_held:
# Pressed
self._button_held = True
if self.on_button_do is not None:
self.on_button_do(self.get_state())
if self.switch.value and self._button_held:
# Released
self._button_held = False
def get_state(self):
return {
'direction': self.is_inverted and -self._direction or self._direction,
'position': self._state,
'is_pressed': not self.switch.value,
'is_held': self._button_held,
'velocity': self._velocity,
}
class EncoderHandler(Module):
def __init__(self):
self.encoders = []
self.pins = None
self.map = None
self.divisor = 4
def on_runtime_enable(self, keyboard):
return
def on_runtime_disable(self, keyboard):
return
def during_bootup(self, keyboard):
if self.pins and self.map:
for idx, pins in enumerate(self.pins):
try:
# Check for busio.I2C
if isinstance(pins[0], busio.I2C):
new_encoder = I2CEncoder(*pins)
# Else fall back to GPIO
else:
new_encoder = GPIOEncoder(*pins)
# Set default divisor if unset
if new_encoder.divisor is None:
new_encoder.divisor = self.divisor
# In our case, we need to define keybord and encoder_id for callbacks
new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do(
keyboard, bound_idx, x
)
new_encoder.on_button_do = (
lambda x, bound_idx=idx: self.on_button_do(
keyboard, bound_idx, x
)
)
self.encoders.append(new_encoder)
except Exception as e:
print(e)
return
def on_move_do(self, keyboard, encoder_id, state):
if self.map:
layer_id = keyboard.active_layers[0]
# if Left, key index 0 else key index 1
if state['direction'] == -1:
key_index = 0
else:
key_index = 1
key = self.map[layer_id][encoder_id][key_index]
keyboard.tap_key(key)
def on_button_do(self, keyboard, encoder_id, state):
if state['is_pressed'] is True:
layer_id = keyboard.active_layers[0]
key = self.map[layer_id][encoder_id][2]
keyboard.tap_key(key)
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
for encoder in self.encoders:
encoder.update_state()
return keyboard
def after_matrix_scan(self, keyboard):
'''
Return value will be replace matrix update if supplied
'''
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return

266
kmk/modules/holdtap.py Normal file
View file

@ -0,0 +1,266 @@
from micropython import const
from kmk.keys import KC, make_argumented_key
from kmk.modules import Module
from kmk.utils import Debug
debug = Debug(__name__)
class ActivationType:
PRESSED = const(0)
RELEASED = const(1)
HOLD_TIMEOUT = const(2)
INTERRUPTED = const(3)
REPEAT = const(4)
class HoldTapRepeat:
NONE = const(0)
TAP = const(1)
HOLD = const(2)
ALL = const(3)
class HoldTapKeyState:
def __init__(self, timeout_key, *args, **kwargs):
self.timeout_key = timeout_key
self.args = args
self.kwargs = kwargs
self.activated = ActivationType.PRESSED
class HoldTapKeyMeta:
def __init__(
self,
tap,
hold,
prefer_hold=True,
tap_interrupted=False,
tap_time=None,
repeat=HoldTapRepeat.NONE,
):
self.tap = tap
self.hold = hold
self.prefer_hold = prefer_hold
self.tap_interrupted = tap_interrupted
self.tap_time = tap_time
self.repeat = repeat
class HoldTap(Module):
tap_time = 300
def __init__(self):
self.key_buffer = []
self.key_states = {}
if KC.get('HT') == KC.NO:
make_argumented_key(
validator=HoldTapKeyMeta,
names=('HT',),
on_press=self.ht_pressed,
on_release=self.ht_released,
)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def process_key(self, keyboard, key, is_pressed, int_coord):
'''Handle holdtap being interrupted by another key press/release.'''
current_key = key
send_buffer = False
append_buffer = False
for key, state in self.key_states.items():
if key == current_key:
continue
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
):
keyboard.cancel_timeout(state.timeout_key)
self.key_states[key].activated = ActivationType.INTERRUPTED
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:
append_buffer = True
# apply changes with 'side-effects' on key_states or the loop behaviour
# outside the loop.
if append_buffer:
self.key_buffer.append((int_coord, current_key, is_pressed))
current_key = None
if send_buffer:
self.send_key_buffer(keyboard)
return current_key
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def ht_pressed(self, key, keyboard, *args, **kwargs):
'''Unless in repeat mode, do nothing yet, action resolves when key is released, timer expires or other key is pressed.'''
if key in self.key_states:
state = self.key_states[key]
keyboard.cancel_timeout(self.key_states[key].timeout_key)
if state.activated == ActivationType.RELEASED:
state.activated = ActivationType.REPEAT
self.ht_activate_tap(key, keyboard, *args, **kwargs)
elif state.activated == ActivationType.HOLD_TIMEOUT:
self.ht_activate_hold(key, keyboard, *args, **kwargs)
elif state.activated == ActivationType.INTERRUPTED:
self.ht_activate_on_interrupt(key, keyboard, *args, **kwargs)
return
if key.meta.tap_time is None:
tap_time = self.tap_time
else:
tap_time = key.meta.tap_time
timeout_key = keyboard.set_timeout(
tap_time,
lambda: self.on_tap_time_expired(key, keyboard, *args, **kwargs),
)
self.key_states[key] = HoldTapKeyState(timeout_key, *args, **kwargs)
return keyboard
def ht_released(self, key, keyboard, *args, **kwargs):
'''On keyup, release mod or tap key.'''
if key not in self.key_states:
return keyboard
state = self.key_states[key]
keyboard.cancel_timeout(state.timeout_key)
repeat = key.meta.repeat & HoldTapRepeat.TAP
if state.activated == ActivationType.HOLD_TIMEOUT:
# release hold
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
repeat = key.meta.repeat & HoldTapRepeat.HOLD
elif state.activated == ActivationType.INTERRUPTED:
# release tap
self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs)
if key.meta.prefer_hold:
repeat = key.meta.repeat & HoldTapRepeat.HOLD
elif state.activated == ActivationType.PRESSED:
# press and release tap because key released within tap time
self.ht_activate_tap(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard)
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
state.activated = ActivationType.RELEASED
self.send_key_buffer(keyboard)
elif state.activated == ActivationType.REPEAT:
state.activated = ActivationType.RELEASED
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
# don't delete the key state right now in this case
if repeat:
if key.meta.tap_time is None:
tap_time = self.tap_time
else:
tap_time = key.meta.tap_time
state.timeout_key = keyboard.set_timeout(
tap_time, lambda: self.key_states.pop(key)
)
else:
del self.key_states[key]
return keyboard
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
'''When tap time expires activate hold if key is still being pressed.
Remove key if ActivationType is RELEASED.'''
try:
state = self.key_states[key]
except KeyError:
if debug.enabled:
debug(f'on_tap_time_expired: no such key {key}')
return
if self.key_states[key].activated == ActivationType.PRESSED:
# press hold because timer expired after tap time
self.key_states[key].activated = ActivationType.HOLD_TIMEOUT
self.ht_activate_hold(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard)
elif state.activated == ActivationType.RELEASED:
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
del self.key_states[key]
def send_key_buffer(self, keyboard):
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
self.key_buffer.clear()
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
if debug.enabled:
debug('ht_activate_hold')
keyboard.resume_process_key(self, key.meta.hold, True)
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
if debug.enabled:
debug('ht_deactivate_hold')
keyboard.resume_process_key(self, key.meta.hold, False)
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
if debug.enabled:
debug('ht_activate_tap')
keyboard.resume_process_key(self, key.meta.tap, True)
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
if debug.enabled:
debug('ht_deactivate_tap')
keyboard.resume_process_key(self, key.meta.tap, False)
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
if debug.enabled:
debug('ht_activate_on_interrupt')
if key.meta.prefer_hold:
self.ht_activate_hold(key, keyboard, *args, **kwargs)
else:
self.ht_activate_tap(key, keyboard, *args, **kwargs)
def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs):
if debug.enabled:
debug('ht_deactivate_on_interrupt')
if key.meta.prefer_hold:
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
else:
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)

184
kmk/modules/layers.py Normal file
View file

@ -0,0 +1,184 @@
'''One layer isn't enough. Adds keys to get to more of them'''
from kmk.keys import KC, make_argumented_key
from kmk.modules.holdtap import HoldTap, HoldTapKeyMeta
from kmk.utils import Debug
debug = Debug(__name__)
def layer_key_validator(layer, kc=None):
'''
Validates the syntax (but not semantics) of a layer key call. We won't
have access to the keymap here, so we can't verify much of anything useful
here (like whether the target layer actually exists). The spirit of this
existing is mostly that Python will catch extraneous args/kwargs and error
out.
'''
return LayerKeyMeta(layer, kc)
def layer_key_validator_lt(layer, kc, prefer_hold=False, **kwargs):
return HoldTapKeyMeta(tap=kc, hold=KC.MO(layer), prefer_hold=prefer_hold, **kwargs)
def layer_key_validator_tt(layer, prefer_hold=True, **kwargs):
return HoldTapKeyMeta(
tap=KC.TG(layer), hold=KC.MO(layer), prefer_hold=prefer_hold, **kwargs
)
class LayerKeyMeta:
def __init__(self, layer, kc=None):
self.layer = layer
self.kc = kc
class Layers(HoldTap):
'''Gives access to the keys used to enable the layer system'''
_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',),
on_press=self._mo_pressed,
on_release=self._mo_released,
)
make_argumented_key(
validator=layer_key_validator, names=('DF',), on_press=self._df_pressed
)
make_argumented_key(
validator=layer_key_validator,
names=('LM',),
on_press=self._lm_pressed,
on_release=self._lm_released,
)
make_argumented_key(
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
)
make_argumented_key(
validator=layer_key_validator_lt,
names=('LT',),
on_press=self.ht_pressed,
on_release=self.ht_released,
)
make_argumented_key(
validator=layer_key_validator_tt,
names=('TT',),
on_press=self.ht_pressed,
on_release=self.ht_released,
)
def _df_pressed(self, key, keyboard, *args, **kwargs):
'''
Switches the default layer
'''
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
'''
self.activate_layer(keyboard, key.meta.layer)
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
'''
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.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
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):
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

103
kmk/modules/midi.py Normal file
View file

@ -0,0 +1,103 @@
import adafruit_midi
import usb_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
from adafruit_midi.pitch_bend import PitchBend
from adafruit_midi.program_change import ProgramChange
from adafruit_midi.start import Start
from adafruit_midi.stop import Stop
from kmk.keys import make_argumented_key
from kmk.modules import Module
class midiNoteValidator:
def __init__(self, note=69, velocity=64, channel=None):
self.note = note
self.velocity = velocity
self.channel = channel
class MidiKeys(Module):
def __init__(self):
make_argumented_key(
names=('MIDI_CC',),
validator=ControlChange,
on_press=self.on_press,
)
make_argumented_key(
names=('MIDI_NOTE',),
validator=midiNoteValidator,
on_press=self.note_on,
on_release=self.note_off,
)
make_argumented_key(
names=('MIDI_PB',),
validator=PitchBend,
on_press=self.on_press,
)
make_argumented_key(
names=('MIDI_PC',),
validator=ProgramChange,
on_press=self.on_press,
)
make_argumented_key(
names=('MIDI_START',),
validator=Start,
on_press=self.on_press,
)
make_argumented_key(
names=('MIDI_STOP',),
validator=Stop,
on_press=self.on_press,
)
try:
self.midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
except IndexError:
self.midi = None
# if debug_enabled:
print('No midi device found.')
def during_bootup(self, keyboard):
return None
def before_matrix_scan(self, keyboard):
return None
def after_matrix_scan(self, keyboard):
return None
def process_key(self, keyboard, key, is_pressed, int_coord):
return key
def before_hid_send(self, keyboard):
return None
def after_hid_send(self, keyboard):
return None
def on_powersave_enable(self, keyboard):
return None
def on_powersave_disable(self, keyboard):
return None
def send(self, message):
if self.midi:
self.midi.send(message)
def on_press(self, key, keyboard, *args, **kwargs):
self.send(key.meta)
def note_on(self, key, keyboard, *args, **kwargs):
self.send(NoteOn(key.meta.note, key.meta.velocity, channel=key.meta.channel))
def note_off(self, key, keyboard, *args, **kwargs):
self.send(NoteOff(key.meta.note, key.meta.velocity, channel=key.meta.channel))

14
kmk/modules/modtap.py Normal file
View file

@ -0,0 +1,14 @@
from kmk.keys import make_argumented_key
from kmk.modules.holdtap import HoldTap, HoldTapKeyMeta
# Deprecation Notice: The `ModTap` class serves as an alias for `HoldTap` and will be removed in a future update. Please use `HoldTap` instead.
class ModTap(HoldTap):
def __init__(self):
super().__init__()
make_argumented_key(
validator=HoldTapKeyMeta,
names=('MT',),
on_press=self.ht_pressed,
on_release=self.ht_released,
)

180
kmk/modules/mouse_keys.py Normal file
View file

@ -0,0 +1,180 @@
from kmk.keys import AX, make_key, make_mouse_key
from kmk.kmktime import PeriodicTimer
from kmk.modules import Module
class MouseKeys(Module):
def __init__(self):
self._nav_key_activated = 0
self._up_activated = False
self._down_activated = False
self._left_activated = False
self._right_activated = False
self._mw_up_activated = False
self._mw_down_activated = False
self.max_speed = 10
self.acc_interval = 10 # Delta ms to apply acceleration
self.move_step = 1
make_mouse_key(
names=('MB_LMB',),
code=1,
)
make_mouse_key(
names=('MB_MMB',),
code=4,
)
make_mouse_key(
names=('MB_RMB',),
code=2,
)
make_mouse_key(
names=('MB_BTN4',),
code=8,
)
make_mouse_key(
names=('MB_BTN5',),
code=16,
)
make_key(
names=('MW_UP',),
on_press=self._mw_up_press,
on_release=self._mw_up_release,
)
make_key(
names=(
'MW_DOWN',
'MW_DN',
),
on_press=self._mw_down_press,
on_release=self._mw_down_release,
)
make_key(
names=('MS_UP',),
on_press=self._ms_up_press,
on_release=self._ms_up_release,
)
make_key(
names=(
'MS_DOWN',
'MS_DN',
),
on_press=self._ms_down_press,
on_release=self._ms_down_release,
)
make_key(
names=(
'MS_LEFT',
'MS_LT',
),
on_press=self._ms_left_press,
on_release=self._ms_left_release,
)
make_key(
names=(
'MS_RIGHT',
'MS_RT',
),
on_press=self._ms_right_press,
on_release=self._ms_right_release,
)
def during_bootup(self, keyboard):
self._timer = PeriodicTimer(self.acc_interval)
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
if not self._timer.tick():
return
if self._nav_key_activated:
if self.move_step < self.max_speed:
self.move_step = self.move_step + 1
if self._right_activated:
AX.X.move(keyboard, self.move_step)
if self._left_activated:
AX.X.move(keyboard, -self.move_step)
if self._up_activated:
AX.Y.move(keyboard, -self.move_step)
if self._down_activated:
AX.Y.move(keyboard, self.move_step)
if self._mw_up_activated:
AX.W.move(keyboard, 1)
if self._mw_down_activated:
AX.W.move(keyboard, -1)
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def _mw_up_press(self, key, keyboard, *args, **kwargs):
self._mw_up_activated = True
def _mw_up_release(self, key, keyboard, *args, **kwargs):
self._mw_up_activated = False
def _mw_down_press(self, key, keyboard, *args, **kwargs):
self._mw_down_activated = True
def _mw_down_release(self, key, keyboard, *args, **kwargs):
self._mw_down_activated = False
# Mouse movement
def _reset_next_interval(self):
if self._nav_key_activated == 1:
self.move_step = 1
def _check_last(self):
if self._nav_key_activated == 0:
self.move_step = 1
def _ms_up_press(self, key, keyboard, *args, **kwargs):
self._nav_key_activated += 1
self._reset_next_interval()
self._up_activated = True
def _ms_up_release(self, key, keyboard, *args, **kwargs):
self._up_activated = False
self._nav_key_activated -= 1
self._check_last()
def _ms_down_press(self, key, keyboard, *args, **kwargs):
self._nav_key_activated += 1
self._reset_next_interval()
self._down_activated = True
def _ms_down_release(self, key, keyboard, *args, **kwargs):
self._down_activated = False
self._nav_key_activated -= 1
self._check_last()
def _ms_left_press(self, key, keyboard, *args, **kwargs):
self._nav_key_activated += 1
self._reset_next_interval()
self._left_activated = True
def _ms_left_release(self, key, keyboard, *args, **kwargs):
self._nav_key_activated -= 1
self._left_activated = False
self._check_last()
def _ms_right_press(self, key, keyboard, *args, **kwargs):
self._nav_key_activated += 1
self._reset_next_interval()
self._right_activated = True
def _ms_right_release(self, key, keyboard, *args, **kwargs):
self._nav_key_activated -= 1
self._right_activated = False
self._check_last()

87
kmk/modules/oneshot.py Normal file
View file

@ -0,0 +1,87 @@
from kmk.keys import make_argumented_key
from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKeyMeta
from kmk.modules.layers import LayerKeyMeta
from kmk.utils import Debug
debug = Debug(__name__)
class OneShotKeyMeta(HoldTapKeyMeta):
def __init__(self, kc, tap_time=None):
super().__init__(tap=kc, hold=kc, prefer_hold=False, tap_time=tap_time)
class OneShot(HoldTap):
tap_time = 1000
def __init__(self):
super().__init__()
make_argumented_key(
validator=OneShotKeyMeta,
names=('OS', 'ONESHOT'),
on_press=self.osk_pressed,
on_release=self.osk_released,
)
def process_key(self, keyboard, current_key, is_pressed, int_coord):
'''Release os key after interrupting non-os keyup, or reset timeout and
stack multiple os keys.'''
send_buffer = False
for key, state in self.key_states.items():
if key == current_key:
continue
if (isinstance(current_key.meta, OneShotKeyMeta)) or (
isinstance(current_key.meta, LayerKeyMeta)
):
keyboard.cancel_timeout(state.timeout_key)
if key.meta.tap_time is None:
tap_time = self.tap_time
else:
tap_time = key.meta.tap_time
state.timeout_key = keyboard.set_timeout(
tap_time,
lambda k=key: self.on_tap_time_expired(k, keyboard),
)
continue
if state.activated == ActivationType.PRESSED and is_pressed:
state.activated = ActivationType.HOLD_TIMEOUT
elif state.activated == ActivationType.RELEASED and is_pressed:
state.activated = ActivationType.INTERRUPTED
elif state.activated == ActivationType.INTERRUPTED:
if is_pressed:
send_buffer = True
self.key_buffer.insert(0, (None, key, False))
if send_buffer:
self.key_buffer.append((int_coord, current_key, is_pressed))
current_key = None
self.send_key_buffer(keyboard)
return current_key
def osk_pressed(self, key, keyboard, *args, **kwargs):
'''Register HoldTap mechanism and activate os key.'''
self.ht_pressed(key, keyboard, *args, **kwargs)
self.ht_activate_tap(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard)
return keyboard
def osk_released(self, key, keyboard, *args, **kwargs):
'''On keyup, mark os key as released or handle HoldTap.'''
try:
state = self.key_states[key]
except KeyError:
if debug.enabled:
debug(f'OneShot.osk_released: no such key {key}')
return keyboard
if state.activated == ActivationType.PRESSED:
state.activated = ActivationType.RELEASED
else:
self.ht_released(key, keyboard, *args, **kwargs)
return keyboard

View file

@ -0,0 +1,314 @@
'''
Extension handles usage of Trackball Breakout by Pimoroni
Product page: https://shop.pimoroni.com/products/trackball-breakout
'''
from micropython import const
import math
import struct
from kmk.keys import AX, KC, make_argumented_key, make_key
from kmk.kmktime import PeriodicTimer
from kmk.modules import Module
I2C_ADDRESS = 0x0A
I2C_ADDRESS_ALTERNATIVE = 0x0B
CHIP_ID = 0xBA11
VERSION = 1
REG_LED_RED = 0x00
REG_LED_GRN = 0x01
REG_LED_BLU = 0x02
REG_LED_WHT = 0x03
REG_LEFT = 0x04
REG_RIGHT = 0x05
REG_UP = 0x06
REG_DOWN = 0x07
REG_SWITCH = 0x08
MSK_SWITCH_STATE = 0b10000000
REG_USER_FLASH = 0xD0
REG_FLASH_PAGE = 0xF0
REG_INT = 0xF9
MSK_INT_TRIGGERED = 0b00000001
MSK_INT_OUT_EN = 0b00000010
REG_CHIP_ID_L = 0xFA
RED_CHIP_ID_H = 0xFB
REG_VERSION = 0xFC
REG_I2C_ADDR = 0xFD
REG_CTRL = 0xFE
MSK_CTRL_SLEEP = 0b00000001
MSK_CTRL_RESET = 0b00000010
MSK_CTRL_FREAD = 0b00000100
MSK_CTRL_FWRITE = 0b00001000
ANGLE_OFFSET = 0
class TrackballHandlerKeyMeta:
def __init__(self, handler=0):
self.handler = handler
def layer_key_validator(handler):
return TrackballHandlerKeyMeta(handler=handler)
class TrackballMode:
'''Behaviour mode of trackball: mouse movement or vertical scroll'''
MOUSE_MODE = const(0)
SCROLL_MODE = const(1)
class ScrollDirection:
'''Behaviour mode of scrolling: natural or reverse scrolling'''
NATURAL = const(0)
REVERSE = const(1)
class TrackballHandler:
def handle(self, keyboard, trackball, x, y, switch, state):
raise NotImplementedError
class PointingHandler(TrackballHandler):
def handle(self, keyboard, trackball, x, y, switch, state):
if x:
AX.X.move(keyboard, x)
if y:
AX.Y.move(keyboard, y)
if switch == 1: # Button pressed
keyboard.pre_process_key(KC.MB_LMB, is_pressed=True)
if not state and trackball.previous_state is True: # Button released
keyboard.pre_process_key(KC.MB_LMB, is_pressed=False)
trackball.previous_state = state
class ScrollHandler(TrackballHandler):
def __init__(self, scroll_direction=ScrollDirection.NATURAL):
self.scroll_direction = scroll_direction
def handle(self, keyboard, trackball, x, y, switch, state):
if self.scroll_direction == ScrollDirection.REVERSE:
y = -y
if y != 0:
AX.W.move(keyboard, y)
if switch == 1: # Button pressed
keyboard.pre_process_key(KC.MB_LMB, is_pressed=True)
if not state and trackball.previous_state is True: # Button released
keyboard.pre_process_key(KC.MB_LMB, is_pressed=False)
trackball.previous_state = state
class KeyHandler(TrackballHandler):
x = 0
y = 0
def __init__(self, up, right, down, left, press, axis_snap=0.25, steps=8):
self.up = up
self.right = right
self.down = down
self.left = left
self.press = press
self.axis_snap = axis_snap
self.steps = steps
def handle(self, keyboard, trackball, x, y, switch, state):
if y and abs(x / y) < self.axis_snap:
x = 0
if x and abs(y / x) < self.axis_snap:
y = 0
self.x += x
self.y += y
x_taps = self.x // self.steps
y_taps = self.y // self.steps
self.x %= self.steps
self.y %= self.steps
for i in range(x_taps, 0, 1):
keyboard.tap_key(self.left)
for i in range(x_taps, 0, -1):
keyboard.tap_key(self.right)
for i in range(y_taps, 0, 1):
keyboard.tap_key(self.up)
for i in range(y_taps, 0, -1):
keyboard.tap_key(self.down)
if switch and state:
keyboard.tap_key(self.press)
class Trackball(Module):
'''Module handles usage of Trackball Breakout by Pimoroni'''
def __init__(
self,
i2c,
mode=TrackballMode.MOUSE_MODE,
address=I2C_ADDRESS,
angle_offset=ANGLE_OFFSET,
handlers=None,
):
self.angle_offset = angle_offset
if not handlers:
handlers = [PointingHandler(), ScrollHandler()]
if mode == TrackballMode.SCROLL_MODE:
handlers.reverse()
self._i2c_address = address
self._i2c_bus = i2c
self.mode = mode
self.previous_state = False # click state
self.handlers = handlers
self.current_handler = self.handlers[0]
self.polling_interval = 20
chip_id = struct.unpack('<H', bytearray(self._i2c_rdwr([REG_CHIP_ID_L], 2)))[0]
if chip_id != CHIP_ID:
raise RuntimeError(
f'Invalid chip ID: 0x{chip_id:04X}, expected 0x{CHIP_ID:04X}'
)
make_key(
names=('TB_MODE', 'TB_NEXT_HANDLER', 'TB_N'),
on_press=self._tb_handler_next_press,
)
make_argumented_key(
validator=layer_key_validator,
names=('TB_HANDLER', 'TB_H'),
on_press=self._tb_handler_press,
)
def during_bootup(self, keyboard):
self._timer = PeriodicTimer(self.polling_interval)
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
if not self._timer.tick():
return
up, down, left, right, switch, state = self._read_raw_state()
x, y = self._calculate_movement(right - left, down - up)
self.current_handler.handle(keyboard, self, x, y, switch, state)
return
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def set_rgbw(self, r, g, b, w):
'''Set all LED brightness as RGBW.'''
self._i2c_rdwr([REG_LED_RED, r, g, b, w])
def set_red(self, value):
'''Set brightness of trackball red LED.'''
self._i2c_rdwr([REG_LED_RED, value & 0xFF])
def set_green(self, value):
'''Set brightness of trackball green LED.'''
self._i2c_rdwr([REG_LED_GRN, value & 0xFF])
def set_blue(self, value):
'''Set brightness of trackball blue LED.'''
self._i2c_rdwr([REG_LED_BLU, value & 0xFF])
def set_white(self, value):
'''Set brightness of trackball white LED.'''
self._i2c_rdwr([REG_LED_WHT, value & 0xFF])
def activate_handler(self, handler):
if isinstance(handler, TrackballHandler):
self.current_handler = handler
else:
try:
self.current_handler = self.handlers[handler]
except KeyError:
print(f'no handler found with id {handler}')
def next_handler(self):
next_index = self.handlers.index(self.current_handler) + 1
if next_index >= len(self.handlers):
next_index = 0
self.activate_handler(next_index)
def _read_raw_state(self):
'''Read up, down, left, right and switch data from trackball.'''
left, right, up, down, switch = self._i2c_rdwr([REG_LEFT], 5)
switch, switch_state = (
switch & ~MSK_SWITCH_STATE,
(switch & MSK_SWITCH_STATE) > 0,
)
return up, down, left, right, switch, switch_state
def _i2c_rdwr(self, data, length=0):
'''Write and optionally read I2C data.'''
while not self._i2c_bus.try_lock():
pass
try:
if length > 0:
result = bytearray(length)
self._i2c_bus.writeto_then_readfrom(
self._i2c_address, bytes(data), result
)
return list(result)
else:
self._i2c_bus.writeto(self._i2c_address, bytes(data))
return []
finally:
self._i2c_bus.unlock()
def _tb_handler_press(self, key, keyboard, *args, **kwargs):
self.activate_handler(key.meta.handler)
def _tb_handler_next_press(self, key, keyboard, *args, **kwargs):
self.next_handler()
def _calculate_movement(self, raw_x, raw_y):
'''Calculate accelerated movement vector from raw data'''
if raw_x == 0 and raw_y == 0:
return 0, 0
var_accel = 1
power = 2.5
angle_rad = math.atan2(raw_y, raw_x) + self.angle_offset
vector_length = math.sqrt(pow(raw_x, 2) + pow(raw_y, 2))
vector_length = pow(vector_length * var_accel, power)
x = math.floor(vector_length * math.cos(angle_rad))
y = math.floor(vector_length * math.sin(angle_rad))
limit = 127 # hid size limit
x_clamped = max(min(limit, x), -limit)
y_clamped = max(min(limit, y), -limit)
return x_clamped, y_clamped

View file

@ -0,0 +1,94 @@
from analogio import AnalogIn
from supervisor import ticks_ms
from kmk.modules import Module
class PotentiometerState:
def __init__(self, direction: int, position: int):
self.direction = direction
self.position = position
class Potentiometer:
def __init__(self, pin, move_callback, is_inverted=False):
self.is_inverted = is_inverted
self.read_pin = AnalogIn(pin)
self._direction = None
self._pos = self.get_pos()
self._timestamp = ticks_ms()
self.cb = move_callback
# callback function on events.
self.on_move_do = lambda state: self.cb(state)
def get_state(self) -> PotentiometerState:
return PotentiometerState(
direction=(self.is_inverted and -self._direction or self._direction),
position=(self.is_inverted and -self._pos or self._pos),
)
def get_pos(self):
'''
Read from the analog pin assingned, truncate to 7 bits,
average over 10 readings, and return a value 0-127
'''
return int(sum([(self.read_pin.value >> 9) for i in range(10)]) / 10)
def update_state(self):
self._direction = 0
new_pos = self.get_pos()
if abs(new_pos - self._pos) > 2:
# movement detected!
if new_pos > self._pos:
self._direction = 1
else:
self._direction = -1
self._pos = new_pos
if self.on_move_do is not None:
self.on_move_do(self.get_state())
class PotentiometerHandler(Module):
def __init__(self):
self.potentiometers = []
self.pins = None
def on_runtime_enable(self, keyboard):
return
def on_runtime_disable(self, keyboard):
return
def during_bootup(self, keyboard):
if self.pins:
for args in self.pins:
self.potentiometers.append(Potentiometer(*args))
return
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
for potentiometer in self.potentiometers:
potentiometer.update_state()
return keyboard
def after_matrix_scan(self, keyboard):
'''
Return value will be replace matrix update if supplied
'''
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return

149
kmk/modules/power.py Normal file
View file

@ -0,0 +1,149 @@
import board
import digitalio
from supervisor import ticks_ms
from time import sleep
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key
from kmk.kmktime import check_deadline
from kmk.modules import Module
class Power(Module):
def __init__(self, powersave_pin=None):
self.enable = False
self.powersave_pin = powersave_pin # Powersave pin board object
self._powersave_start = ticks_ms()
self._usb_last_scan = ticks_ms() - 5000
self._psp = None # Powersave pin object
self._i2c = 0
self._loopcounter = 0
make_key(
names=('PS_TOG',), on_press=self._ps_tog, on_release=handler_passthrough
)
make_key(
names=('PS_ON',), on_press=self._ps_enable, on_release=handler_passthrough
)
make_key(
names=('PS_OFF',), on_press=self._ps_disable, on_release=handler_passthrough
)
def __repr__(self):
return f'Power({self._to_dict()})'
def _to_dict(self):
return {
'enable': self.enable,
'powersave_pin': self.powersave_pin,
'_powersave_start': self._powersave_start,
'_usb_last_scan': self._usb_last_scan,
'_psp': self._psp,
}
def during_bootup(self, keyboard):
self._i2c_scan()
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
if keyboard.matrix_update or keyboard.secondary_matrix_update:
self.psave_time_reset()
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
if self.enable:
self.psleep()
def on_powersave_enable(self, keyboard):
'''Gives 10 cycles to allow other extensions to clean up before powersave'''
if self._loopcounter > 10:
self.enable_powersave(keyboard)
self._loopcounter = 0
else:
self._loopcounter += 1
return
def on_powersave_disable(self, keyboard):
self.disable_powersave(keyboard)
return
def enable_powersave(self, keyboard):
'''Enables power saving features'''
if keyboard.i2c_deinit_count >= self._i2c and self.powersave_pin:
# Allows power save to prevent RGB drain.
# Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic
if not self._psp:
self._psp = digitalio.DigitalInOut(self.powersave_pin)
self._psp.direction = digitalio.Direction.OUTPUT
if self._psp:
self._psp.value = True
self.enable = True
keyboard._trigger_powersave_enable = False
return
def disable_powersave(self, keyboard):
'''Disables power saving features'''
if self._psp:
self._psp.value = False
# Allows power save to prevent RGB drain.
# Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic
keyboard._trigger_powersave_disable = False
self.enable = False
return
def psleep(self):
'''
Sleeps longer and longer to save power the more time in between updates.
'''
if check_deadline(ticks_ms(), self._powersave_start, 60000):
sleep(8 / 1000)
elif check_deadline(ticks_ms(), self._powersave_start, 240000) is False:
sleep(180 / 1000)
return
def psave_time_reset(self):
self._powersave_start = ticks_ms()
def _i2c_scan(self):
i2c = board.I2C()
while not i2c.try_lock():
pass
try:
self._i2c = len(i2c.scan())
finally:
i2c.unlock()
return
def usb_rescan_timer(self):
return bool(check_deadline(ticks_ms(), self._usb_last_scan, 5000) is False)
def usb_time_reset(self):
self._usb_last_scan = ticks_ms()
return
def usb_scan(self):
# TODO Add USB detection here. Currently lies that it's connected
# https://github.com/adafruit/circuitpython/pull/3513
return True
def _ps_tog(self, key, keyboard, *args, **kwargs):
if self.enable:
keyboard._trigger_powersave_disable = True
else:
keyboard._trigger_powersave_enable = True
def _ps_enable(self, key, keyboard, *args, **kwargs):
if not self.enable:
keyboard._trigger_powersave_enable = True
def _ps_disable(self, key, keyboard, *args, **kwargs):
if self.enable:
keyboard._trigger_powersave_disable = True

103
kmk/modules/rapidfire.py Normal file
View file

@ -0,0 +1,103 @@
from random import randint
from kmk.keys import make_argumented_key
from kmk.modules import Module
class RapidFireMeta:
def __init__(
self,
kc,
interval=100,
timeout=200,
enable_interval_randomization=False,
randomization_magnitude=15,
toggle=False,
):
self.kc = kc
self.interval = interval
self.timeout = timeout
self.enable_interval_randomization = enable_interval_randomization
self.randomization_magnitude = randomization_magnitude
self.toggle = toggle
class RapidFire(Module):
_active_keys = {}
_toggled_keys = []
_waiting_keys = []
def __init__(self):
make_argumented_key(
validator=RapidFireMeta,
names=('RF',),
on_press=self._rf_pressed,
on_release=self._rf_released,
)
def _get_repeat(self, key):
if key.meta.enable_interval_randomization:
return key.meta.interval + randint(
-key.meta.randomization_magnitude, key.meta.randomization_magnitude
)
return key.meta.interval
def _on_timer_timeout(self, key, keyboard):
keyboard.tap_key(key.meta.kc)
if key in self._waiting_keys:
self._waiting_keys.remove(key)
if key.meta.toggle and key not in self._toggled_keys:
self._toggled_keys.append(key)
self._active_keys[key] = keyboard.set_timeout(
self._get_repeat(key), lambda: self._on_timer_timeout(key, keyboard)
)
def _rf_pressed(self, key, keyboard, *args, **kwargs):
if key in self._toggled_keys:
self._toggled_keys.remove(key)
self._deactivate_key(key, keyboard)
return
if key.meta.timeout > 0:
keyboard.tap_key(key.meta.kc)
self._waiting_keys.append(key)
self._active_keys[key] = keyboard.set_timeout(
key.meta.timeout, lambda: self._on_timer_timeout(key, keyboard)
)
else:
self._on_timer_timeout(key, keyboard)
def _rf_released(self, key, keyboard, *args, **kwargs):
if key not in self._active_keys:
return
if key in self._toggled_keys:
if key not in self._waiting_keys:
return
self._toggled_keys.remove(key)
if key in self._waiting_keys:
self._waiting_keys.remove(key)
self._deactivate_key(key, keyboard)
def _deactivate_key(self, key, keyboard):
keyboard.cancel_timeout(self._active_keys[key])
self._active_keys.pop(key)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return

63
kmk/modules/serialace.py Normal file
View file

@ -0,0 +1,63 @@
from usb_cdc import data
from kmk.modules import Module
from kmk.utils import Debug
debug = Debug(__name__)
class SerialACE(Module):
buffer = bytearray()
def during_bootup(self, keyboard):
try:
data.timeout = 0
except AttributeError:
pass
def before_matrix_scan(self, keyboard):
pass
def after_matrix_scan(self, keyboard):
pass
def process_key(self, keyboard, key, is_pressed, int_coord):
return key
def before_hid_send(self, keyboard):
# Serial.data isn't initialized.
if not data:
return
# Nothing to parse.
if data.in_waiting == 0:
return
self.buffer.extend(data.read())
idx = self.buffer.find(b'\n')
# No full command yet.
if idx == -1:
return
# Split off command and evaluate.
line = self.buffer[:idx]
self.buffer = self.buffer[idx + 1 :]
try:
if debug.enabled:
debug(f'eval({line})')
ret = eval(line, {'keyboard': keyboard})
data.write(bytearray(str(ret) + '\n'))
except Exception as err:
if debug.enabled:
debug(f'error: {err}')
def after_hid_send(self, keyboard):
pass
def on_powersave_enable(self, keyboard):
pass
def on_powersave_disable(self, keyboard):
pass

384
kmk/modules/split.py Normal file
View file

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

63
kmk/modules/sticky_mod.py Normal file
View file

@ -0,0 +1,63 @@
from kmk.keys import make_argumented_key
from kmk.modules import Module
class StickyModMeta:
def __init__(self, kc, mod):
self.kc = kc
self.mod = mod
class StickyMod(Module):
def __init__(self):
self._active = False
self._active_key = None
make_argumented_key(
names=('SM',),
validator=StickyModMeta,
on_press=self.sm_pressed,
on_release=self.sm_released,
)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def process_key(self, keyboard, key, is_pressed, int_coord):
# release previous key if any other key is pressed
if self._active and self._active_key is not None:
self.release_key(keyboard, self._active_key)
return key
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def release_key(self, keyboard, key):
keyboard.process_key(key.meta.mod, False)
self._active = False
self._active_key = None
def sm_pressed(self, key, keyboard, *args, **kwargs):
keyboard.process_key(key.meta.mod, True)
keyboard.process_key(key.meta.kc, True)
self._active_key = key
def sm_released(self, key, keyboard, *args, **kwargs):
keyboard.process_key(key.meta.kc, False)
self._active_key = key
self._active = True

View file

@ -0,0 +1,222 @@
try:
from typing import Optional
except ImportError:
# we're not in a dev environment, so we don't need to worry about typing
pass
from micropython import const
from kmk.keys import KC, Key, ModifierKey
from kmk.modules import Module
class State:
LISTENING = const(0)
DELETING = const(1)
SENDING = const(2)
IGNORING = const(3)
class Character:
'''Helper class for making a left-shifted key identical to a right-shifted key'''
is_shifted: bool = False
def __init__(self, key_code: Key, is_shifted: bool) -> None:
self.is_shifted = is_shifted
self.key_code = KC.LSHIFT(key_code) if is_shifted else key_code
def __eq__(self, other: any) -> bool: # type: ignore
try:
return (
self.key_code.code == other.key_code.code
and self.is_shifted == other.is_shifted
)
except AttributeError:
return False
class Phrase:
'''Manages a collection of characters and keeps an index of them so that potential matches can be tracked'''
def __init__(self, string: str) -> None:
self._characters: list[Character] = []
self._index: int = 0
for char in string:
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'''
if not self.index_at_end():
self._index += 1
def get_character_at_index(self, index: int) -> Character:
'''Returns the character at the given index'''
return self._characters[index]
def get_character_at_current_index(self) -> Character:
'''Returns the character at the current index for this phrase'''
return self._characters[self._index]
def reset_index(self) -> None:
'''Reset the index to the start of the phrase'''
self._index = 0
def index_at_end(self) -> bool:
'''Returns True if the index is at the end of the phrase'''
return self._index == len(self._characters)
def character_is_at_current_index(self, character) -> bool:
'''Returns True if the given character is the next character in the phrase'''
return self.get_character_at_current_index() == character
class Rule:
'''Represents the relationship between a phrase to be substituted and its substitution'''
def __init__(self, to_substitute: Phrase, substitution: Phrase) -> None:
self.to_substitute: Phrase = to_substitute
self.substitution: Phrase = substitution
def restart(self) -> None:
'''Resets this rule's to_substitute and substitution phrases'''
self.to_substitute.reset_index()
self.substitution.reset_index()
class StringSubstitution(Module):
_shifted: bool = False
_rules: list = []
_state: State = State.LISTENING
_matched_rule: Optional[Phrase] = None
_active_modifiers: list[ModifierKey] = []
def __init__(
self,
dictionary: dict,
):
for key, value in dictionary.items():
self._rules.append(Rule(Phrase(key), Phrase(value)))
def process_key(self, keyboard, key, is_pressed, int_coord):
if key is KC.LSFT or key is KC.RSFT:
if is_pressed:
self._shifted = True
else:
self._shifted = False
# control ignoring state if the key is a non-shift modifier
elif type(key) is ModifierKey:
if is_pressed and key not in self._active_modifiers:
self._active_modifiers.append(key)
self._state = State.IGNORING
elif key in self._active_modifiers:
self._active_modifiers.remove(key)
if not self._active_modifiers:
self._state = State.LISTENING
# reset rules because pressing a modifier combination
# should interrupt any current matches
for rule in self._rules:
rule.restart()
if not self._state == State.LISTENING:
return key
if is_pressed:
character = Character(key, self._shifted)
# run through the dictionary to check for a possible match on each new keypress
for rule in self._rules:
if rule.to_substitute.character_is_at_current_index(character):
rule.to_substitute.next_character()
else:
rule.restart()
# if character is not a match at the current index,
# it could still be a match at the start of the sequence
# so redo the check after resetting the sequence
if rule.to_substitute.character_is_at_current_index(character):
rule.to_substitute.next_character()
# we've matched all of the characters in a phrase to be substituted
if rule.to_substitute.index_at_end():
rule.restart()
# set the phrase indexes to where they differ
# so that only the characters that differ are replaced
for character in rule.to_substitute._characters:
if (
character
== rule.substitution.get_character_at_current_index()
):
rule.to_substitute.next_character()
rule.substitution.next_character()
else:
break
if rule.to_substitute.index_at_end():
break
self._matched_rule = rule
self._state = State.DELETING
# if we have a match there's no reason to continue the full key processing, so return out
return
return key
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
if self._state == State.LISTENING:
return
if self._state == State.DELETING:
# force-release modifiers so sending the replacement text doesn't interact with them
# it should not be possible for any modifiers other than shift to be held upon rule activation
# as a modified key won't send a keycode that is matched against the user's dictionary,
# but, just in case, we'll release those too
modifiers_to_release = [
KC.LSFT,
KC.RSFT,
KC.LCTL,
KC.LGUI,
KC.LALT,
KC.RCTL,
KC.RGUI,
KC.RALT,
]
for modifier in modifiers_to_release:
keyboard.remove_key(modifier)
# send backspace taps equivalent to the length of the phrase to be substituted
to_substitute: Phrase = self._matched_rule.to_substitute # type: ignore
to_substitute.next_character()
if not to_substitute.index_at_end():
keyboard.tap_key(KC.BSPC)
else:
self._state = State.SENDING
if self._state == State.SENDING:
substitution = self._matched_rule.substitution # type: ignore
if not substitution.index_at_end():
keyboard.tap_key(substitution.get_character_at_current_index().key_code)
substitution.next_character()
else:
self._state = State.LISTENING
self._matched_rule = None
for rule in self._rules:
rule.restart()
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return

122
kmk/modules/tapdance.py Normal file
View file

@ -0,0 +1,122 @@
from kmk.keys import KC, make_argumented_key
from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKeyMeta
class TapDanceKeyMeta:
def __init__(self, *keys, tap_time=None):
'''
Any key in the tapdance sequence that is not already a holdtap
key gets converted to a holdtap key with identical tap and hold
meta attributes.
'''
self.tap_time = tap_time
self.keys = []
for key in keys:
if not isinstance(key.meta, HoldTapKeyMeta):
ht_key = KC.HT(
tap=key,
hold=key,
prefer_hold=True,
tap_interrupted=False,
tap_time=self.tap_time,
)
self.keys.append(ht_key)
else:
self.keys.append(key)
self.keys = tuple(self.keys)
class TapDance(HoldTap):
def __init__(self):
super().__init__()
make_argumented_key(
validator=TapDanceKeyMeta,
names=('TD',),
on_press=self.td_pressed,
on_release=self.td_released,
)
self.td_counts = {}
def process_key(self, keyboard, key, is_pressed, int_coord):
if isinstance(key.meta, TapDanceKeyMeta):
if key in self.td_counts:
return key
for _key, state in self.key_states.copy().items():
if state.activated == ActivationType.RELEASED:
keyboard.cancel_timeout(state.timeout_key)
self.ht_activate_tap(_key, keyboard)
self.send_key_buffer(keyboard)
self.ht_deactivate_tap(_key, keyboard)
keyboard.resume_process_key(self, key, is_pressed, int_coord)
key = None
del self.key_states[_key]
del self.td_counts[state.tap_dance]
key = super().process_key(keyboard, key, is_pressed, int_coord)
return key
def td_pressed(self, key, keyboard, *args, **kwargs):
# active tap dance
if key in self.td_counts:
count = self.td_counts[key]
kc = key.meta.keys[count]
keyboard.cancel_timeout(self.key_states[kc].timeout_key)
count += 1
# Tap dance reached the end of the list: send last tap in sequence
# and start from the beginning.
if count >= len(key.meta.keys):
self.key_states[kc].activated = ActivationType.RELEASED
self.on_tap_time_expired(kc, keyboard)
count = 0
else:
del self.key_states[kc]
# new tap dance
else:
count = 0
current_key = key.meta.keys[count]
self.ht_pressed(current_key, keyboard, *args, **kwargs)
self.td_counts[key] = count
# Add the active tap dance to key_states; `on_tap_time_expired` needs
# the back-reference.
self.key_states[current_key].tap_dance = key
def td_released(self, key, keyboard, *args, **kwargs):
try:
kc = key.meta.keys[self.td_counts[key]]
except KeyError:
return
state = self.key_states[kc]
if state.activated == ActivationType.HOLD_TIMEOUT:
# release hold
self.ht_deactivate_hold(kc, keyboard, *args, **kwargs)
del self.key_states[kc]
del self.td_counts[key]
elif state.activated == ActivationType.INTERRUPTED:
# release tap
self.ht_deactivate_on_interrupt(kc, keyboard, *args, **kwargs)
del self.key_states[kc]
del self.td_counts[key]
else:
# keep counting
state.activated = ActivationType.RELEASED
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
# Note: the `key` argument is the current holdtap key in the sequence,
# not the tapdance key.
state = self.key_states[key]
if state.activated == ActivationType.RELEASED:
self.ht_activate_tap(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard)
del self.td_counts[state.tap_dance]
super().on_tap_time_expired(key, keyboard, *args, **kwargs)

View file

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

View file

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

View file

@ -0,0 +1,28 @@
import board
pinout = [
board.TX,
board.RX,
None, # GND
None, # GND
board.GP2,
board.GP3,
board.GP4,
board.GP5,
board.GP6,
board.GP7,
board.GP8,
board.GP9,
board.CS,
board.SDO,
board.SDI,
board.SCK,
board.GP26,
board.GP27,
board.GP28,
board.GP29,
None, # 3.3v
None, # RST
None, # GND
None, # RAW
]

View file

@ -0,0 +1,28 @@
import board
pinout = [
board.D0,
board.D1,
None, # GND
None, # GND
board.D2,
board.D3,
board.D4,
board.D5,
board.D6,
board.D7,
board.D8,
board.D9,
board.D10,
board.MOSI,
board.MISO,
board.SCK,
board.A0,
board.A1,
board.A2,
board.A3,
None, # 3.3v
None, # RST
None, # GND
None, # RAW
]

View file

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

View file

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

42
kmk/scanners/__init__.py Normal file
View file

@ -0,0 +1,42 @@
def intify_coordinate(row, col, len_cols):
return len_cols * row + col
class DiodeOrientation:
'''
Orientation of diodes on handwired boards. You can think of:
COLUMNS = vertical
ROWS = horizontal
COL2ROW and ROW2COL are equivalent to their meanings in QMK.
'''
COLUMNS = 0
ROWS = 1
COL2ROW = COLUMNS
ROW2COL = ROWS
class Scanner:
'''
Base class for scanners.
'''
# for split keyboards, the offset value will be assigned in Split module
offset = 0
@property
def coord_mapping(self):
return tuple(range(self.offset, self.offset + self.key_count))
@property
def key_count(self):
raise NotImplementedError
def scan_for_changes(self):
'''
Scan for key events and return a key report if an event exists.
The key report is a byte array with contents [row, col, True if pressed else False]
'''
raise NotImplementedError

142
kmk/scanners/digitalio.py Normal file
View file

@ -0,0 +1,142 @@
import digitalio
from keypad import Event as KeyEvent
from kmk.scanners import DiodeOrientation, Scanner
class MatrixScanner(Scanner):
def __init__(
self,
cols,
rows,
diode_orientation=DiodeOrientation.COLUMNS,
rollover_cols_every_rows=None,
offset=0,
):
self.len_cols = len(cols)
self.len_rows = len(rows)
self.offset = offset
# A pin cannot be both a row and column, detect this by combining the
# two tuples into a set and validating that the length did not drop
#
# repr() hackery is because CircuitPython Pin objects are not hashable
unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows}
assert (
len(unique_pins) == self.len_cols + self.len_rows
), 'Cannot use a pin as both a column and row'
del unique_pins
self.diode_orientation = diode_orientation
# __class__.__name__ is used instead of isinstance as the MCP230xx lib
# does not use the digitalio.DigitalInOut, but rather a self defined one:
# https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33
if self.diode_orientation == DiodeOrientation.COLUMNS:
self.outputs = [
x
if x.__class__.__name__ == 'DigitalInOut'
else digitalio.DigitalInOut(x)
for x in cols
]
self.inputs = [
x
if x.__class__.__name__ == 'DigitalInOut'
else digitalio.DigitalInOut(x)
for x in rows
]
self.translate_coords = True
elif self.diode_orientation == DiodeOrientation.ROWS:
self.outputs = [
x
if x.__class__.__name__ == 'DigitalInOut'
else digitalio.DigitalInOut(x)
for x in rows
]
self.inputs = [
x
if x.__class__.__name__ == 'DigitalInOut'
else digitalio.DigitalInOut(x)
for x in cols
]
self.translate_coords = False
else:
raise ValueError(f'Invalid DiodeOrientation: {self.diode_orienttaion}')
for pin in self.outputs:
pin.switch_to_output()
for pin in self.inputs:
pin.switch_to_input(pull=digitalio.Pull.DOWN)
self.rollover_cols_every_rows = rollover_cols_every_rows
if self.rollover_cols_every_rows is None:
self.rollover_cols_every_rows = self.len_rows
self._key_count = self.len_cols * self.len_rows
self.state = bytearray(self.key_count)
@property
def key_count(self):
return self._key_count
def scan_for_changes(self):
'''
Poll the matrix for changes and return either None (if nothing updated)
or a bytearray (reused in later runs so copy this if you need the raw
array itself for some crazy reason) consisting of (row, col, pressed)
which are (int, int, bool)
'''
ba_idx = 0
any_changed = False
for oidx, opin in enumerate(self.outputs):
opin.value = True
for iidx, ipin in enumerate(self.inputs):
# cast to int to avoid
#
# >>> xyz = bytearray(3)
# >>> xyz[2] = True
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# OverflowError: value would overflow a 1 byte buffer
#
# I haven't dived too far into what causes this, but it's
# almost certainly because bool types in Python aren't just
# aliases to int values, but are proper pseudo-types
new_val = int(ipin.value)
old_val = self.state[ba_idx]
if old_val != new_val:
if self.translate_coords:
new_oidx = oidx + self.len_cols * (
iidx // self.rollover_cols_every_rows
)
new_iidx = iidx - self.rollover_cols_every_rows * (
iidx // self.rollover_cols_every_rows
)
row = new_iidx
col = new_oidx
else:
row = oidx
col = iidx
pressed = new_val
self.state[ba_idx] = new_val
any_changed = True
break
ba_idx += 1
opin.value = False
if any_changed:
break
if any_changed:
key_number = self.len_cols * row + col + self.offset
return KeyEvent(key_number, pressed)

43
kmk/scanners/encoder.py Normal file
View file

@ -0,0 +1,43 @@
import keypad
import rotaryio
from kmk.scanners import Scanner
class RotaryioEncoder(Scanner):
def __init__(self, pin_a, pin_b, divisor=4):
self.encoder = rotaryio.IncrementalEncoder(pin_a, pin_b, divisor)
self.position = 0
self._pressed = False
self._queue = []
@property
def key_count(self):
return 2
def scan_for_changes(self):
position = self.encoder.position
if position != self.position:
self._queue.append(position - self.position)
self.position = position
if not self._queue:
return
key_number = self.offset
if self._queue[0] > 0:
key_number += 1
if self._pressed:
self._queue[0] -= 1 if self._queue[0] > 0 else -1
if self._queue[0] == 0:
self._queue.pop(0)
self._pressed = False
else:
self._pressed = True
return keypad.Event(key_number, self._pressed)

107
kmk/scanners/keypad.py Normal file
View file

@ -0,0 +1,107 @@
import keypad
from kmk.scanners import DiodeOrientation, Scanner
class KeypadScanner(Scanner):
'''
Translation layer around a CircuitPython 7 keypad scanner.
:param pin_map: A sequence of (row, column) tuples for each key.
:param kp: An instance of the keypad class.
'''
@property
def key_count(self):
return self.keypad.key_count
def scan_for_changes(self):
'''
Scan for key events and return a key report if an event exists.
The key report is a byte array with contents [row, col, True if pressed else False]
'''
ev = self.keypad.events.get()
if ev and self.offset:
return keypad.Event(ev.key_number + self.offset, ev.pressed)
return ev
class MatrixScanner(KeypadScanner):
'''
Row/Column matrix using the CircuitPython 7 keypad scanner.
:param row_pins: A sequence of pins used for rows.
:param col_pins: A sequence of pins used for columns.
:param direction: The diode orientation of the matrix.
'''
def __init__(
self,
row_pins,
column_pins,
*,
columns_to_anodes=DiodeOrientation.COL2ROW,
interval=0.02,
max_events=64,
):
self.keypad = keypad.KeyMatrix(
row_pins,
column_pins,
columns_to_anodes=(columns_to_anodes == DiodeOrientation.COL2ROW),
interval=interval,
max_events=max_events,
)
super().__init__()
class KeysScanner(KeypadScanner):
'''
GPIO-per-key 'matrix' using the native CircuitPython 7 keypad scanner.
:param pins: An array of arrays of CircuitPython Pin objects, such that pins[r][c] is the pin for row r, column c.
'''
def __init__(
self,
pins,
*,
value_when_pressed=False,
pull=True,
interval=0.02,
max_events=64,
):
self.keypad = keypad.Keys(
pins,
value_when_pressed=value_when_pressed,
pull=pull,
interval=interval,
max_events=max_events,
)
super().__init__()
class ShiftRegisterKeys(KeypadScanner):
def __init__(
self,
*,
clock,
data,
latch,
value_to_latch=True,
key_count,
value_when_pressed=False,
interval=0.02,
max_events=64,
):
self.keypad = keypad.ShiftRegisterKeys(
clock=clock,
data=data,
latch=latch,
value_to_latch=value_to_latch,
key_count=key_count,
value_when_pressed=value_when_pressed,
interval=interval,
max_events=max_events,
)
super().__init__()

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

View file

@ -0,0 +1,92 @@
'''
Circuit Python wrapper around PIO implementation of UART
Original source of these examples: https://github.com/adafruit/Adafruit_CircuitPython_PIOASM/tree/main/examples (MIT)
'''
import rp2pio
from array import array
'''
.program uart_tx
.side_set 1 opt
; An 8n1 UART transmit program.
; OUT pin 0 and side-set pin 0 are both mapped to UART TX pin.
pull side 1 [7] ; Assert stop bit, or stall with line in idle state
set x, 7 side 0 [7] ; Preload bit counter, assert start bit for 8 clocks
bitloop: ; This loop will run 8 times (8n1 UART)
out pins, 1 ; Shift 1 bit from OSR to the first OUT pin
jmp x-- bitloop [6] ; Each loop iteration is 8 cycles.
; compiles to:
'''
tx_code = array('H', [40864, 63271, 24577, 1602])
'''
.program uart_rx_mini
; Minimum viable 8n1 UART receiver. Wait for the start bit, then sample 8 bits
; with the correct timing.
; IN pin 0 is mapped to the GPIO used as UART RX.
; Autopush must be enabled, with a threshold of 8.
wait 0 pin 0 ; Wait for start bit
set x, 7 [10] ; Preload bit counter, delay until eye of first data bit
bitloop: ; Loop 8 times
in pins, 1 ; Sample data
jmp x-- bitloop [6] ; Each iteration is 8 cycles
; compiles to:
'''
rx_code = array('H', [8224, 59943, 16385, 1602])
class PIO_UART:
def __init__(self, *, tx, rx, baudrate=9600):
if tx:
self.tx_pio = rp2pio.StateMachine(
tx_code,
first_out_pin=tx,
first_sideset_pin=tx,
frequency=8 * baudrate,
initial_sideset_pin_state=1,
initial_sideset_pin_direction=1,
initial_out_pin_state=1,
initial_out_pin_direction=1,
sideset_enable=True,
)
if rx:
self.rx_pio = rp2pio.StateMachine(
rx_code,
first_in_pin=rx,
frequency=8 * baudrate,
auto_push=True,
push_threshold=8,
)
@property
def timeout(self):
return 0
@property
def baudrate(self):
return self.tx_pio.frequency // 8
@baudrate.setter
def baudrate(self, frequency):
self.tx_pio.frequency = frequency * 8
self.rx_pio.frequency = frequency * 8
def write(self, buf):
return self.tx_pio.write(buf)
@property
def in_waiting(self):
return self.rx_pio.in_waiting
def read(self, n):
b = bytearray(n)
n = self.rx_pio.readinto(b)
return b[:n]
def readinto(self, buf):
return self.rx_pio.readinto(buf)

26
kmk/types.py Normal file
View file

@ -0,0 +1,26 @@
class AttrDict(dict):
'''
Primitive support for accessing dictionary entries in dot notation.
Mostly for user-facing stuff (allows for `k.KC_ESC` rather than
`k['KC_ESC']`, which gets a bit obnoxious).
This is read-only on purpose.
'''
def __getattr__(self, key):
return self[key]
class KeySequenceMeta:
def __init__(self, seq):
self.seq = seq
class KeySeqSleepMeta:
def __init__(self, ms):
self.ms = ms
class UnicodeModeKeyMeta:
def __init__(self, mode):
self.mode = mode

39
kmk/utils.py Normal file
View file

@ -0,0 +1,39 @@
try:
from typing import Optional
except ImportError:
pass
from supervisor import ticks_ms
def clamp(x: int, bottom: int = 0, top: int = 100) -> int:
return min(max(bottom, x), top)
_debug_enabled = False
class Debug:
'''default usage:
debug = Debug(__name__)
'''
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='')
@property
def enabled(self) -> bool:
global _debug_enabled
return _debug_enabled
@enabled.setter
def enabled(self, enabled: bool):
global _debug_enabled
_debug_enabled = enabled