initial repo setup which includes current release of kmk
This commit is contained in:
commit
69f2363278
0
kmk/__init__.py
Normal file
0
kmk/__init__.py
Normal file
8
kmk/consts.py
Normal file
8
kmk/consts.py
Normal 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)
|
54
kmk/extensions/__init__.py
Normal file
54
kmk/extensions/__init__.py
Normal 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
|
59
kmk/extensions/international.py
Normal file
59
kmk/extensions/international.py
Normal 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
|
34
kmk/extensions/keymap_extras/keymap_jp.py
Normal file
34
kmk/extensions/keymap_extras/keymap_jp.py
Normal 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
259
kmk/extensions/led.py
Normal 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
|
85
kmk/extensions/lock_status.py
Normal file
85
kmk/extensions/lock_status.py
Normal 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)
|
57
kmk/extensions/media_keys.py
Normal file
57
kmk/extensions/media_keys.py
Normal 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
272
kmk/extensions/oled.py
Normal 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()
|
161
kmk/extensions/peg_oled_display.py
Normal file
161
kmk/extensions/peg_oled_display.py
Normal 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
|
201
kmk/extensions/peg_rgb_matrix.py
Normal file
201
kmk/extensions/peg_rgb_matrix.py
Normal 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
599
kmk/extensions/rgb.py
Normal 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
145
kmk/extensions/statusled.py
Normal 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()
|
45
kmk/extensions/stringy_keymaps.py
Normal file
45
kmk/extensions/stringy_keymaps.py
Normal 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
0
kmk/handlers/__init__.py
Normal file
155
kmk/handlers/sequences.py
Normal file
155
kmk/handlers/sequences.py
Normal 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
146
kmk/handlers/stock.py
Normal 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
355
kmk/hid.py
Normal 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
9
kmk/key_validators.py
Normal 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
825
kmk/keys.py
Normal 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
547
kmk/kmk_keyboard.py
Normal 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
34
kmk/kmktime.py
Normal 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
46
kmk/modules/__init__.py
Normal 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
227
kmk/modules/adns9800.py
Normal 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
99
kmk/modules/capsword.py
Normal 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
70
kmk/modules/cg_swap.py
Normal 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
335
kmk/modules/combos.py
Normal 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
|
259
kmk/modules/dynamic_sequences.py
Normal file
259
kmk/modules/dynamic_sequences.py
Normal 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
130
kmk/modules/easypoint.py
Normal 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
312
kmk/modules/encoder.py
Normal 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
266
kmk/modules/holdtap.py
Normal 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
184
kmk/modules/layers.py
Normal 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
103
kmk/modules/midi.py
Normal 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
14
kmk/modules/modtap.py
Normal 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
180
kmk/modules/mouse_keys.py
Normal 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
87
kmk/modules/oneshot.py
Normal 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
|
314
kmk/modules/pimoroni_trackball.py
Normal file
314
kmk/modules/pimoroni_trackball.py
Normal 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
|
94
kmk/modules/potentiometer.py
Normal file
94
kmk/modules/potentiometer.py
Normal 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
149
kmk/modules/power.py
Normal 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
103
kmk/modules/rapidfire.py
Normal 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
63
kmk/modules/serialace.py
Normal 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
384
kmk/modules/split.py
Normal 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
63
kmk/modules/sticky_mod.py
Normal 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
|
222
kmk/modules/string_substitution.py
Normal file
222
kmk/modules/string_substitution.py
Normal 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
122
kmk/modules/tapdance.py
Normal 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)
|
20
kmk/quickpin/pro_micro/avr_promicro.py
Normal file
20
kmk/quickpin/pro_micro/avr_promicro.py
Normal 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,
|
||||||
|
}
|
28
kmk/quickpin/pro_micro/boardsource_blok.py
Normal file
28
kmk/quickpin/pro_micro/boardsource_blok.py
Normal 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
|
||||||
|
]
|
28
kmk/quickpin/pro_micro/helios.py
Normal file
28
kmk/quickpin/pro_micro/helios.py
Normal 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
|
||||||
|
]
|
28
kmk/quickpin/pro_micro/kb2040.py
Normal file
28
kmk/quickpin/pro_micro/kb2040.py
Normal 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
|
||||||
|
]
|
28
kmk/quickpin/pro_micro/nice_nano.py
Normal file
28
kmk/quickpin/pro_micro/nice_nano.py
Normal 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+
|
||||||
|
]
|
28
kmk/quickpin/pro_micro/sparkfun_promicro_rp2040.py
Normal file
28
kmk/quickpin/pro_micro/sparkfun_promicro_rp2040.py
Normal 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
42
kmk/scanners/__init__.py
Normal 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
142
kmk/scanners/digitalio.py
Normal 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
43
kmk/scanners/encoder.py
Normal 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
107
kmk/scanners/keypad.py
Normal 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
67
kmk/scheduler.py
Normal 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)
|
0
kmk/transports/__init__.py
Normal file
0
kmk/transports/__init__.py
Normal file
92
kmk/transports/pio_uart.py
Normal file
92
kmk/transports/pio_uart.py
Normal 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
26
kmk/types.py
Normal 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
39
kmk/utils.py
Normal 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
|
Loading…
Reference in a new issue