Initial
This commit is contained in:
commit
4c75ff8edd
2
boot_out.txt
Normal file
2
boot_out.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Adafruit CircuitPython 7.1.1 on 2022-01-14; Adafruit KB2040 with rp2040
|
||||||
|
Board ID:adafruit_kb2040
|
||||||
BIN
font5x8.bin
Normal file
BIN
font5x8.bin
Normal file
Binary file not shown.
0
kmk/__init__.py
Normal file
0
kmk/__init__.py
Normal file
13
kmk/consts.py
Normal file
13
kmk/consts.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from micropython import const
|
||||||
|
|
||||||
|
try:
|
||||||
|
from kmk.release_info import KMK_RELEASE
|
||||||
|
except Exception:
|
||||||
|
KMK_RELEASE = 'copied-from-git'
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeMode:
|
||||||
|
NOOP = const(0)
|
||||||
|
LINUX = IBUS = const(1)
|
||||||
|
MACOS = OSX = RALT = const(2)
|
||||||
|
WINC = const(3)
|
||||||
51
kmk/extensions/__init__.py
Normal file
51
kmk/extensions/__init__.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
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) # _
|
||||||
256
kmk/extensions/led.py
Normal file
256
kmk/extensions/led.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
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_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 = 0
|
||||||
|
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 'LED({})'.format(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):
|
||||||
|
if self._enabled and self.animation_mode:
|
||||||
|
self.animate()
|
||||||
|
return
|
||||||
|
|
||||||
|
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 none to prevent cycles from being wasted
|
||||||
|
self.animation_mode = None
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
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
|
||||||
65
kmk/extensions/lock_status.py
Normal file
65
kmk/extensions/lock_status.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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 = 0x00
|
||||||
|
self.hid = None
|
||||||
|
for device in usb_hid.devices:
|
||||||
|
if device.usage == HIDUsage.KEYBOARD:
|
||||||
|
self.hid = device
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('LockStatus(report={})').format(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[0] != self.report:
|
||||||
|
self.report = report[0]
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_enable(self, sandbox):
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_disable(self, sandbox):
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_num_lock(self):
|
||||||
|
return bool(self.report & LockCode.NUMLOCK)
|
||||||
|
|
||||||
|
def get_caps_lock(self):
|
||||||
|
return bool(self.report & LockCode.CAPSLOCK)
|
||||||
|
|
||||||
|
def get_scroll_lock(self):
|
||||||
|
return bool(self.report & LockCode.SCROLLLOCK)
|
||||||
|
|
||||||
|
def get_compose(self):
|
||||||
|
return bool(self.report & LockCode.COMPOSE)
|
||||||
|
|
||||||
|
def get_kana(self):
|
||||||
|
return bool(self.report & LockCode.KANA)
|
||||||
55
kmk/extensions/media_keys.py
Normal file
55
kmk/extensions/media_keys.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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=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
|
||||||
582
kmk/extensions/rgb.py
Normal file
582
kmk/extensions/rgb.py
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
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.kmktime import PeriodicTimer
|
||||||
|
from kmk.utils import clamp
|
||||||
|
|
||||||
|
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(self, 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,
|
||||||
|
val_limit=255,
|
||||||
|
hue_default=0,
|
||||||
|
sat_default=255,
|
||||||
|
rgb_order=(1, 0, 2), # GRB WS2812
|
||||||
|
val_default=255,
|
||||||
|
hue_step=4,
|
||||||
|
sat_step=13,
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
if pixels is None:
|
||||||
|
import neopixel
|
||||||
|
|
||||||
|
pixels = neopixel.NeoPixel(
|
||||||
|
pixel_pin,
|
||||||
|
num_pixels,
|
||||||
|
pixel_order=rgb_order,
|
||||||
|
auto_write=not disable_auto_write,
|
||||||
|
)
|
||||||
|
self.pixels = pixels
|
||||||
|
self.num_pixels = num_pixels
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
self.rgbw = bool(len(rgb_order) == 4)
|
||||||
|
|
||||||
|
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.refresh_rate = refresh_rate
|
||||||
|
|
||||||
|
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):
|
||||||
|
self._timer = PeriodicTimer(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):
|
||||||
|
self.animate()
|
||||||
|
|
||||||
|
def on_powersave_enable(self, sandbox):
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_disable(self, sandbox):
|
||||||
|
self._do_update()
|
||||||
|
|
||||||
|
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
|
||||||
|
'''
|
||||||
|
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:
|
||||||
|
'''
|
||||||
|
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):
|
||||||
|
break
|
||||||
|
index -= len(pixels)
|
||||||
|
|
||||||
|
pixels[index] = rgb
|
||||||
|
|
||||||
|
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 and self._timer.tick():
|
||||||
|
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 'SLED({})'.format(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()
|
||||||
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
|
||||||
125
kmk/handlers/stock.py
Normal file
125
kmk/handlers/stock.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
def passthrough(key, keyboard, *args, **kwargs):
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def default_pressed(key, keyboard, KC, coord_int=None, coord_raw=None, *args, **kwargs):
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
|
||||||
|
keyboard.keys_pressed.add(key)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def default_released(
|
||||||
|
key, keyboard, KC, coord_int=None, coord_raw=None, *args, **kwargs # NOQA
|
||||||
|
):
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
keyboard.keys_pressed.discard(key)
|
||||||
|
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def reset(*args, **kwargs):
|
||||||
|
import microcontroller
|
||||||
|
|
||||||
|
microcontroller.reset()
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
324
kmk/hid.py
Normal file
324
kmk/hid.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import usb_hid
|
||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from storage import getmount
|
||||||
|
|
||||||
|
from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey
|
||||||
|
|
||||||
|
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.post_init()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '{}(REPORT_BYTES={})'.format(self.__class__.__name__, self.REPORT_BYTES)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_report(self, keys_pressed):
|
||||||
|
self.clear_all()
|
||||||
|
|
||||||
|
consumer_key = None
|
||||||
|
for key in keys_pressed:
|
||||||
|
if isinstance(key, ConsumerKey):
|
||||||
|
consumer_key = key
|
||||||
|
break
|
||||||
|
|
||||||
|
reporting_device = self.report_device[0]
|
||||||
|
needed_reporting_device = HIDReportTypes.KEYBOARD
|
||||||
|
|
||||||
|
if consumer_key:
|
||||||
|
needed_reporting_device = HIDReportTypes.CONSUMER
|
||||||
|
|
||||||
|
if reporting_device != needed_reporting_device:
|
||||||
|
# If we are about to change reporting devices, release
|
||||||
|
# all keys and close our proverbial tab on the existing
|
||||||
|
# device, or keys will get stuck (mostly when releasing
|
||||||
|
# media/consumer keys)
|
||||||
|
self.send()
|
||||||
|
|
||||||
|
self.report_device[0] = needed_reporting_device
|
||||||
|
|
||||||
|
if consumer_key:
|
||||||
|
self.add_key(consumer_key)
|
||||||
|
else:
|
||||||
|
for key in keys_pressed:
|
||||||
|
if key.code >= FIRST_KMK_INTERNAL_KEY:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(key, ModifierKey):
|
||||||
|
self.add_modifier(key)
|
||||||
|
else:
|
||||||
|
self.add_key(key)
|
||||||
|
|
||||||
|
if key.has_modifiers:
|
||||||
|
for mod in key.has_modifiers:
|
||||||
|
self.add_modifier(mod)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def clear_all(self):
|
||||||
|
for idx, _ in enumerate(self.report_keys):
|
||||||
|
self.report_keys[idx] = 0x00
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if self.report_device[0] == HIDReportTypes.CONSUMER:
|
||||||
|
where_to_place = self.report_keys
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if self.report_device[0] == HIDReportTypes.CONSUMER:
|
||||||
|
where_to_place = self.report_keys
|
||||||
|
|
||||||
|
for idx, _ in enumerate(where_to_place):
|
||||||
|
if where_to_place[idx] == key.code:
|
||||||
|
where_to_place[idx] = 0x00
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
# 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()
|
||||||
53
kmk/key_validators.py
Normal file
53
kmk/key_validators.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from kmk.types import (
|
||||||
|
KeySeqSleepMeta,
|
||||||
|
LayerKeyMeta,
|
||||||
|
ModTapKeyMeta,
|
||||||
|
TapDanceKeyMeta,
|
||||||
|
UnicodeModeKeyMeta,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def key_seq_sleep_validator(ms):
|
||||||
|
return KeySeqSleepMeta(ms)
|
||||||
|
|
||||||
|
|
||||||
|
def layer_key_validator(
|
||||||
|
layer, kc=None, prefer_hold=False, tap_interrupted=False, tap_time=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=layer,
|
||||||
|
kc=kc,
|
||||||
|
prefer_hold=prefer_hold,
|
||||||
|
tap_interrupted=tap_interrupted,
|
||||||
|
tap_time=tap_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mod_tap_validator(
|
||||||
|
kc, mods=None, prefer_hold=True, tap_interrupted=False, tap_time=None
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Validates that mod tap keys are correctly used
|
||||||
|
'''
|
||||||
|
return ModTapKeyMeta(
|
||||||
|
kc=kc,
|
||||||
|
mods=mods,
|
||||||
|
prefer_hold=prefer_hold,
|
||||||
|
tap_interrupted=tap_interrupted,
|
||||||
|
tap_time=tap_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tap_dance_key_validator(*codes):
|
||||||
|
return TapDanceKeyMeta(codes)
|
||||||
|
|
||||||
|
|
||||||
|
def unicode_mode_key_validator(mode):
|
||||||
|
return UnicodeModeKeyMeta(mode)
|
||||||
721
kmk/keys.py
Normal file
721
kmk/keys.py
Normal file
@ -0,0 +1,721 @@
|
|||||||
|
import gc
|
||||||
|
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
|
||||||
|
|
||||||
|
DEBUG_OUTPUT = False
|
||||||
|
|
||||||
|
FIRST_KMK_INTERNAL_KEY = const(1000)
|
||||||
|
NEXT_AVAILABLE_KEY = 1000
|
||||||
|
|
||||||
|
KEY_SIMPLE = const(0)
|
||||||
|
KEY_MODIFIER = const(1)
|
||||||
|
KEY_CONSUMER = const(2)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
# this is a bit of an FP style thing - combining a pipe operator a-la F# with
|
||||||
|
# a bootleg Maybe monad to clean up these make_key sequences
|
||||||
|
def left_pipe_until_some(candidate, functor, *args_iter):
|
||||||
|
for args in args_iter:
|
||||||
|
result = functor(candidate, *args)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def first_truthy(candidate, *funcs):
|
||||||
|
for func in funcs:
|
||||||
|
result = func(candidate)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_make_mod_key(candidate, code, names):
|
||||||
|
if candidate in names:
|
||||||
|
return make_mod_key(code=code, names=names)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_make_key(candidate, code, names):
|
||||||
|
if candidate in names:
|
||||||
|
return make_key(code=code, names=names)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_make_shifted_key(candidate, code, names):
|
||||||
|
if candidate in names:
|
||||||
|
return make_shifted_key(code=code, names=names)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_make_consumer_key(candidate, code, names):
|
||||||
|
if candidate in names:
|
||||||
|
return make_consumer_key(code=code, names=names)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyAttrDict:
|
||||||
|
__cache = {}
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if DEBUG_OUTPUT:
|
||||||
|
print(f'__setitem__ {key}, {value}')
|
||||||
|
self.__cache.__setitem__(key, value)
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
if DEBUG_OUTPUT:
|
||||||
|
print(f'__getattr__ {key}')
|
||||||
|
return self.__getitem__(key)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
try:
|
||||||
|
return self.__getitem__(key)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.__cache.clear()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if DEBUG_OUTPUT:
|
||||||
|
print(f'__getitem__ {key}')
|
||||||
|
try:
|
||||||
|
return self.__cache[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
key_upper = key.upper()
|
||||||
|
|
||||||
|
# Try all the other weird special cases to get them out of our way:
|
||||||
|
# This need to be done before or ALPHAS because NO will be parsed as alpha
|
||||||
|
# Internal, diagnostic, or auxiliary/enhanced keys
|
||||||
|
|
||||||
|
# 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
|
||||||
|
if key in ('NO', 'XXXXXXX'):
|
||||||
|
make_key(
|
||||||
|
names=('NO', 'XXXXXXX'),
|
||||||
|
on_press=handlers.passthrough,
|
||||||
|
on_release=handlers.passthrough,
|
||||||
|
)
|
||||||
|
elif key in ('TRANSPARENT', 'TRNS'):
|
||||||
|
make_key(
|
||||||
|
names=('TRANSPARENT', 'TRNS'),
|
||||||
|
on_press=handlers.passthrough,
|
||||||
|
on_release=handlers.passthrough,
|
||||||
|
)
|
||||||
|
# Basic ASCII letters/numbers don't need anything fancy, so check those
|
||||||
|
# in the laziest way
|
||||||
|
elif key_upper in ALL_ALPHAS:
|
||||||
|
make_key(
|
||||||
|
code=4 + ALL_ALPHAS.index(key_upper),
|
||||||
|
names=(
|
||||||
|
key_upper,
|
||||||
|
key.lower(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif key in ALL_NUMBERS or key in ALL_NUMBER_ALIASES:
|
||||||
|
try:
|
||||||
|
offset = ALL_NUMBERS.index(key)
|
||||||
|
except ValueError:
|
||||||
|
offset = ALL_NUMBER_ALIASES.index(key)
|
||||||
|
|
||||||
|
names = (ALL_NUMBERS[offset], ALL_NUMBER_ALIASES[offset])
|
||||||
|
make_key(code=30 + offset, names=names)
|
||||||
|
elif key in ('RESET',):
|
||||||
|
make_key(names=('RESET',), on_press=handlers.reset)
|
||||||
|
elif key in ('BOOTLOADER',):
|
||||||
|
make_key(names=('BOOTLOADER',), on_press=handlers.bootloader)
|
||||||
|
elif key in ('DEBUG', 'DBG'):
|
||||||
|
make_key(
|
||||||
|
names=('DEBUG', 'DBG'),
|
||||||
|
on_press=handlers.debug_pressed,
|
||||||
|
on_release=handlers.passthrough,
|
||||||
|
)
|
||||||
|
elif key in ('BKDL',):
|
||||||
|
make_key(
|
||||||
|
names=('BKDL',),
|
||||||
|
on_press=handlers.bkdl_pressed,
|
||||||
|
on_release=handlers.bkdl_released,
|
||||||
|
)
|
||||||
|
elif key in ('GESC', 'GRAVE_ESC'):
|
||||||
|
make_key(
|
||||||
|
names=('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.
|
||||||
|
elif key in ('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'):
|
||||||
|
make_argumented_key(
|
||||||
|
validator=key_seq_sleep_validator,
|
||||||
|
names=('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'),
|
||||||
|
on_press=handlers.sleep_pressed,
|
||||||
|
)
|
||||||
|
elif key in ('UC_MODE_NOOP', 'UC_DISABLE'):
|
||||||
|
make_key(
|
||||||
|
names=('UC_MODE_NOOP', 'UC_DISABLE'),
|
||||||
|
meta=UnicodeModeKeyMeta(UnicodeMode.NOOP),
|
||||||
|
on_press=handlers.uc_mode_pressed,
|
||||||
|
)
|
||||||
|
elif key in ('UC_MODE_LINUX', 'UC_MODE_IBUS'):
|
||||||
|
make_key(
|
||||||
|
names=('UC_MODE_LINUX', 'UC_MODE_IBUS'),
|
||||||
|
meta=UnicodeModeKeyMeta(UnicodeMode.IBUS),
|
||||||
|
on_press=handlers.uc_mode_pressed,
|
||||||
|
)
|
||||||
|
elif key in ('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'):
|
||||||
|
make_key(
|
||||||
|
names=('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'),
|
||||||
|
meta=UnicodeModeKeyMeta(UnicodeMode.RALT),
|
||||||
|
on_press=handlers.uc_mode_pressed,
|
||||||
|
)
|
||||||
|
elif key in ('UC_MODE_WINC',):
|
||||||
|
make_key(
|
||||||
|
names=('UC_MODE_WINC',),
|
||||||
|
meta=UnicodeModeKeyMeta(UnicodeMode.WINC),
|
||||||
|
on_press=handlers.uc_mode_pressed,
|
||||||
|
)
|
||||||
|
elif key in ('UC_MODE',):
|
||||||
|
make_argumented_key(
|
||||||
|
validator=unicode_mode_key_validator,
|
||||||
|
names=('UC_MODE',),
|
||||||
|
on_press=handlers.uc_mode_pressed,
|
||||||
|
)
|
||||||
|
elif key in ('HID_SWITCH', 'HID'):
|
||||||
|
make_key(names=('HID_SWITCH', 'HID'), on_press=handlers.hid_switch)
|
||||||
|
elif key in ('BLE_REFRESH'):
|
||||||
|
make_key(names=('BLE_REFRESH',), on_press=handlers.ble_refresh)
|
||||||
|
else:
|
||||||
|
maybe_key = first_truthy(
|
||||||
|
key,
|
||||||
|
# Modifiers
|
||||||
|
lambda key: left_pipe_until_some(
|
||||||
|
key,
|
||||||
|
maybe_make_mod_key,
|
||||||
|
(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')),
|
||||||
|
# MEH = LCTL | LALT | LSFT# MEH = LCTL |
|
||||||
|
(0x07, ('MEH',)),
|
||||||
|
# HYPR = LCTL | LALT | LSFT | LGUI
|
||||||
|
(0x0F, ('HYPER', 'HYPR')),
|
||||||
|
),
|
||||||
|
lambda key: left_pipe_until_some(
|
||||||
|
key,
|
||||||
|
maybe_make_key,
|
||||||
|
# More ASCII standard keys
|
||||||
|
(40, ('ENTER', 'ENT', '\n')),
|
||||||
|
(41, ('ESCAPE', 'ESC')),
|
||||||
|
(42, ('BACKSPACE', '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', '/')),
|
||||||
|
# Function Keys
|
||||||
|
(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',)),
|
||||||
|
# Lock Keys, Navigation, etc.
|
||||||
|
(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',)),
|
||||||
|
# Numpad
|
||||||
|
(83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')),
|
||||||
|
# FIXME: Investigate whether this key actually works, and
|
||||||
|
# uncomment when/if it does.
|
||||||
|
# (131, names=('LOCKING_NUM', 'LNUM')),
|
||||||
|
(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')),
|
||||||
|
),
|
||||||
|
# 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.
|
||||||
|
lambda key: left_pipe_until_some(
|
||||||
|
key,
|
||||||
|
maybe_make_shifted_key,
|
||||||
|
(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', '?')),
|
||||||
|
),
|
||||||
|
# International
|
||||||
|
lambda key: left_pipe_until_some(
|
||||||
|
key,
|
||||||
|
maybe_make_key,
|
||||||
|
(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',)),
|
||||||
|
),
|
||||||
|
# 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.
|
||||||
|
lambda key: left_pipe_until_some(
|
||||||
|
key,
|
||||||
|
maybe_make_consumer_key,
|
||||||
|
(226, ('AUDIO_MUTE', 'MUTE')), # 0xE2
|
||||||
|
(233, ('AUDIO_VOL_UP', 'VOLU')), # 0xE9
|
||||||
|
(234, ('AUDIO_VOL_DOWN', 'VOLD')), # 0xEA
|
||||||
|
(181, ('MEDIA_NEXT_TRACK', 'MNXT')), # 0xB5
|
||||||
|
(182, ('MEDIA_PREV_TRACK', 'MPRV')), # 0xB6
|
||||||
|
(183, ('MEDIA_STOP', 'MSTP')), # 0xB7
|
||||||
|
(205, ('MEDIA_PLAY_PAUSE', 'MPLY')), # 0xCD (this may not be right)
|
||||||
|
(184, ('MEDIA_EJECT', 'EJCT')), # 0xB8
|
||||||
|
(179, ('MEDIA_FAST_FORWARD', 'MFFD')), # 0xB3
|
||||||
|
(180, ('MEDIA_REWIND', 'MRWD')), # 0xB4
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if DEBUG_OUTPUT:
|
||||||
|
print(f'{key}: {maybe_key}')
|
||||||
|
|
||||||
|
if not maybe_key:
|
||||||
|
raise ValueError('Invalid key')
|
||||||
|
|
||||||
|
return self.__cache[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,
|
||||||
|
has_modifiers=None,
|
||||||
|
no_press=False,
|
||||||
|
no_release=False,
|
||||||
|
on_press=handlers.default_pressed,
|
||||||
|
on_release=handlers.default_released,
|
||||||
|
meta=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=None, no_release=None):
|
||||||
|
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 'Key(code={}, has_modifiers={})'.format(self.code, self.has_modifiers)
|
||||||
|
|
||||||
|
def on_press(self, state, coord_int=None, coord_raw=None):
|
||||||
|
if hasattr(self, '_pre_press_handlers'):
|
||||||
|
for fn in self._pre_press_handlers:
|
||||||
|
if not fn(self, state, KC, coord_int, coord_raw):
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret = self._handle_press(self, state, KC, coord_int, coord_raw)
|
||||||
|
|
||||||
|
if hasattr(self, '_post_press_handlers'):
|
||||||
|
for fn in self._post_press_handlers:
|
||||||
|
fn(self, state, KC, coord_int, coord_raw)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def on_release(self, state, coord_int=None, coord_raw=None):
|
||||||
|
if hasattr(self, '_pre_release_handlers'):
|
||||||
|
for fn in self._pre_release_handlers:
|
||||||
|
if not fn(self, state, KC, coord_int, coord_raw):
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret = self._handle_release(self, state, KC, coord_int, coord_raw)
|
||||||
|
|
||||||
|
if hasattr(self, '_post_release_handlers'):
|
||||||
|
for fn in self._post_release_handlers:
|
||||||
|
fn(self, state, KC, coord_int, coord_raw)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
'''
|
||||||
|
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):
|
||||||
|
'''
|
||||||
|
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)
|
||||||
|
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def after_press_handler(self, fn):
|
||||||
|
'''
|
||||||
|
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)
|
||||||
|
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def before_release_handler(self, fn):
|
||||||
|
'''
|
||||||
|
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)
|
||||||
|
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def after_release_handler(self, fn):
|
||||||
|
'''
|
||||||
|
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)
|
||||||
|
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ModifierKey(Key):
|
||||||
|
FAKE_CODE = const(-1)
|
||||||
|
|
||||||
|
def __call__(self, modified_key=None, no_press=None, no_release=None):
|
||||||
|
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 'ModifierKey(code={}, has_modifiers={})'.format(
|
||||||
|
self.code, self.has_modifiers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsumerKey(Key):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def make_key(code=None, names=tuple(), type=KEY_SIMPLE, **kwargs): # NOQA
|
||||||
|
'''
|
||||||
|
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 == KEY_SIMPLE:
|
||||||
|
constructor = Key
|
||||||
|
elif type == KEY_MODIFIER:
|
||||||
|
constructor = ModifierKey
|
||||||
|
elif type == KEY_CONSUMER:
|
||||||
|
constructor = ConsumerKey
|
||||||
|
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
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def make_mod_key(code, names, *args, **kwargs):
|
||||||
|
return make_key(code, names, *args, **kwargs, type=KEY_MODIFIER)
|
||||||
|
|
||||||
|
|
||||||
|
def make_shifted_key(code, names):
|
||||||
|
return make_key(code, names, has_modifiers={KC.LSFT.code})
|
||||||
|
|
||||||
|
|
||||||
|
def make_consumer_key(*args, **kwargs):
|
||||||
|
return make_key(*args, **kwargs, type=KEY_CONSUMER)
|
||||||
|
|
||||||
|
|
||||||
|
# Argumented keys are implicitly internal, so auto-gen of code
|
||||||
|
# is almost certainly the best plan here
|
||||||
|
def make_argumented_key(
|
||||||
|
validator=lambda *validator_args, **validator_kwargs: object(),
|
||||||
|
names=tuple(), # NOQA
|
||||||
|
*constructor_args,
|
||||||
|
**constructor_kwargs,
|
||||||
|
):
|
||||||
|
global NEXT_AVAILABLE_KEY
|
||||||
|
|
||||||
|
def _argumented_key(*user_args, **user_kwargs):
|
||||||
|
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
|
||||||
502
kmk/kmk_keyboard.py
Normal file
502
kmk/kmk_keyboard.py
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
from supervisor import ticks_ms
|
||||||
|
|
||||||
|
from kmk.consts import KMK_RELEASE, UnicodeMode
|
||||||
|
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
|
||||||
|
from kmk.keys import KC
|
||||||
|
from kmk.kmktime import ticks_add, ticks_diff
|
||||||
|
from kmk.scanners.keypad import MatrixScanner
|
||||||
|
|
||||||
|
|
||||||
|
class Sandbox:
|
||||||
|
matrix_update = None
|
||||||
|
secondary_matrix_update = None
|
||||||
|
active_layers = None
|
||||||
|
|
||||||
|
|
||||||
|
class KMKKeyboard:
|
||||||
|
#####
|
||||||
|
# User-configurable
|
||||||
|
debug_enabled = False
|
||||||
|
|
||||||
|
keymap = []
|
||||||
|
coord_mapping = None
|
||||||
|
|
||||||
|
row_pins = None
|
||||||
|
col_pins = None
|
||||||
|
diode_orientation = None
|
||||||
|
matrix = None
|
||||||
|
uart_buffer = []
|
||||||
|
|
||||||
|
unicode_mode = UnicodeMode.NOOP
|
||||||
|
|
||||||
|
modules = []
|
||||||
|
extensions = []
|
||||||
|
sandbox = Sandbox()
|
||||||
|
|
||||||
|
#####
|
||||||
|
# Internal State
|
||||||
|
keys_pressed = 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 = []
|
||||||
|
_matrix_modify = None
|
||||||
|
state_changed = False
|
||||||
|
_old_timeouts_len = None
|
||||||
|
_new_timeouts_len = None
|
||||||
|
_trigger_powersave_enable = False
|
||||||
|
_trigger_powersave_disable = False
|
||||||
|
i2c_deinit_count = 0
|
||||||
|
_go_args = None
|
||||||
|
_processing_timeouts = False
|
||||||
|
|
||||||
|
# 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 = {}
|
||||||
|
|
||||||
|
# on some M4 setups (such as klardotsh/klarank_feather_m4, CircuitPython
|
||||||
|
# 6.0rc1) this runs out of RAM every cycle and takes down the board. no
|
||||||
|
# real known fix yet other than turning off debug, but M4s have always been
|
||||||
|
# tight on RAM so....
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
'KMKKeyboard('
|
||||||
|
'debug_enabled={} '
|
||||||
|
'diode_orientation={} '
|
||||||
|
'matrix={} '
|
||||||
|
'unicode_mode={} '
|
||||||
|
'_hid_helper={} '
|
||||||
|
'keys_pressed={} '
|
||||||
|
'coordkeys_pressed={} '
|
||||||
|
'hid_pending={} '
|
||||||
|
'active_layers={} '
|
||||||
|
'timeouts={} '
|
||||||
|
')'
|
||||||
|
).format(
|
||||||
|
self.debug_enabled,
|
||||||
|
self.diode_orientation,
|
||||||
|
self.matrix,
|
||||||
|
self.unicode_mode,
|
||||||
|
self._hid_helper,
|
||||||
|
# internal state
|
||||||
|
self.keys_pressed,
|
||||||
|
self._coordkeys_pressed,
|
||||||
|
self.hid_pending,
|
||||||
|
self.active_layers,
|
||||||
|
self._timeouts,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _print_debug_cycle(self, init=False):
|
||||||
|
if self.debug_enabled:
|
||||||
|
if init:
|
||||||
|
print('KMKInit(release={})'.format(KMK_RELEASE))
|
||||||
|
print(self)
|
||||||
|
|
||||||
|
def _send_hid(self):
|
||||||
|
if self._hid_send_enabled:
|
||||||
|
hid_report = self._hid_helper.create_report(self.keys_pressed)
|
||||||
|
try:
|
||||||
|
hid_report.send()
|
||||||
|
except KeyError as e:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('HidNotFound(HIDReportType={})'.format(e))
|
||||||
|
self.hid_pending = False
|
||||||
|
|
||||||
|
def _handle_matrix_report(self, update=None):
|
||||||
|
if update is not None:
|
||||||
|
self._on_matrix_changed(update)
|
||||||
|
self.state_changed = True
|
||||||
|
|
||||||
|
def _find_key_in_map(self, int_coord):
|
||||||
|
try:
|
||||||
|
idx = self.coord_mapping.index(int_coord)
|
||||||
|
except ValueError:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('CoordMappingNotFound(ic={})'.format(int_coord))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
for layer in self.active_layers:
|
||||||
|
try:
|
||||||
|
layer_key = self.keymap[layer][idx]
|
||||||
|
except IndexError:
|
||||||
|
layer_key = None
|
||||||
|
if self.debug_enabled:
|
||||||
|
print(f'KeymapIndexError(idx={idx}, layer={layer})')
|
||||||
|
|
||||||
|
if not layer_key or layer_key == KC.TRNS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return layer_key
|
||||||
|
|
||||||
|
def _on_matrix_changed(self, kevent):
|
||||||
|
int_coord = kevent.key_number
|
||||||
|
is_pressed = kevent.pressed
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('MatrixChange(ic={} pressed={})'.format(int_coord, is_pressed))
|
||||||
|
|
||||||
|
key = None
|
||||||
|
if not is_pressed:
|
||||||
|
try:
|
||||||
|
key = self._coordkeys_pressed[int_coord]
|
||||||
|
except KeyError:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print(f'KeyNotPressed(ic={int_coord})')
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
key = self._find_key_in_map(int_coord)
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('MatrixUndefinedCoordinate(ic={})'.format(int_coord))
|
||||||
|
return self
|
||||||
|
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('KeyResolution(key={})'.format(key))
|
||||||
|
|
||||||
|
self.pre_process_key(key, is_pressed, int_coord)
|
||||||
|
|
||||||
|
def pre_process_key(self, key, is_pressed, int_coord=None):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
key = module.process_key(self, key, is_pressed, int_coord)
|
||||||
|
if key is None:
|
||||||
|
break
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run process_key function in module: ', err, module)
|
||||||
|
|
||||||
|
if int_coord is not None:
|
||||||
|
if is_pressed:
|
||||||
|
self._coordkeys_pressed[int_coord] = key
|
||||||
|
else:
|
||||||
|
del self._coordkeys_pressed[int_coord]
|
||||||
|
|
||||||
|
if key:
|
||||||
|
self.process_key(key, is_pressed, int_coord)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def process_key(self, key, is_pressed, coord_int=None):
|
||||||
|
if is_pressed:
|
||||||
|
key.on_press(self, coord_int)
|
||||||
|
else:
|
||||||
|
key.on_release(self, coord_int)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_key(self, keycode):
|
||||||
|
self.keys_pressed.discard(keycode)
|
||||||
|
return self.process_key(keycode, False)
|
||||||
|
|
||||||
|
def add_key(self, keycode):
|
||||||
|
self.keys_pressed.add(keycode)
|
||||||
|
return self.process_key(keycode, True)
|
||||||
|
|
||||||
|
def tap_key(self, keycode):
|
||||||
|
self.add_key(keycode)
|
||||||
|
# On the next cycle, we'll remove the key.
|
||||||
|
self.set_timeout(False, lambda: self.remove_key(keycode))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_timeout(self, after_ticks, callback):
|
||||||
|
# We allow passing False as an implicit "run this on the next process timeouts cycle"
|
||||||
|
if after_ticks is False:
|
||||||
|
after_ticks = 0
|
||||||
|
|
||||||
|
if after_ticks == 0 and self._processing_timeouts:
|
||||||
|
after_ticks += 1
|
||||||
|
|
||||||
|
timeout_key = ticks_add(ticks_ms(), after_ticks)
|
||||||
|
|
||||||
|
if timeout_key not in self._timeouts:
|
||||||
|
self._timeouts[timeout_key] = []
|
||||||
|
|
||||||
|
idx = len(self._timeouts[timeout_key])
|
||||||
|
self._timeouts[timeout_key].append(callback)
|
||||||
|
|
||||||
|
return (timeout_key, idx)
|
||||||
|
|
||||||
|
def cancel_timeout(self, timeout_key):
|
||||||
|
try:
|
||||||
|
self._timeouts[timeout_key[0]][timeout_key[1]] = None
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
if self.debug_enabled:
|
||||||
|
print(f'no such timeout: {timeout_key}')
|
||||||
|
|
||||||
|
def _process_timeouts(self):
|
||||||
|
if not self._timeouts:
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Copy timeout keys to a temporary list to allow sorting.
|
||||||
|
# Prevent net timeouts set during handling from running on the current
|
||||||
|
# cycle by setting a flag `_processing_timeouts`.
|
||||||
|
current_time = ticks_ms()
|
||||||
|
timeout_keys = []
|
||||||
|
self._processing_timeouts = True
|
||||||
|
|
||||||
|
for k in self._timeouts.keys():
|
||||||
|
if ticks_diff(k, current_time) <= 0:
|
||||||
|
timeout_keys.append(k)
|
||||||
|
|
||||||
|
for k in sorted(timeout_keys):
|
||||||
|
for callback in self._timeouts[k]:
|
||||||
|
if callback:
|
||||||
|
callback()
|
||||||
|
del self._timeouts[k]
|
||||||
|
|
||||||
|
self._processing_timeouts = False
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _init_sanity_check(self):
|
||||||
|
'''
|
||||||
|
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'
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _init_coord_mapping(self):
|
||||||
|
'''
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
|
||||||
|
def _init_matrix(self):
|
||||||
|
if self.matrix is None:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Initialising default matrix scanner.')
|
||||||
|
self.matrix = MatrixScanner(
|
||||||
|
column_pins=self.col_pins,
|
||||||
|
row_pins=self.row_pins,
|
||||||
|
columns_to_anodes=self.diode_orientation,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Matrix scanner already set, not overwriting.')
|
||||||
|
|
||||||
|
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,)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def before_matrix_scan(self):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.before_matrix_scan(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run pre matrix function in module: ', err, module)
|
||||||
|
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.before_matrix_scan(self.sandbox)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run pre matrix function in extension: ', err, ext)
|
||||||
|
|
||||||
|
def after_matrix_scan(self):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.after_matrix_scan(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post matrix function in module: ', err, module)
|
||||||
|
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.after_matrix_scan(self.sandbox)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post matrix function in extension: ', err, ext)
|
||||||
|
|
||||||
|
def before_hid_send(self):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.before_hid_send(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run pre hid function in module: ', err, module)
|
||||||
|
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.before_hid_send(self.sandbox)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run pre hid function in extension: ', err, ext)
|
||||||
|
|
||||||
|
def after_hid_send(self):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.after_hid_send(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post hid function in module: ', err, module)
|
||||||
|
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.after_hid_send(self.sandbox)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post hid function in extension: ', err, ext)
|
||||||
|
|
||||||
|
def powersave_enable(self):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.on_powersave_enable(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post hid function in module: ', err, module)
|
||||||
|
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.on_powersave_enable(self.sandbox)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post hid function in extension: ', err, ext)
|
||||||
|
|
||||||
|
def powersave_disable(self):
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.on_powersave_disable(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post hid function in module: ', err, module)
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.on_powersave_disable(self.sandbox)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to run post hid function in extension: ', err, ext)
|
||||||
|
|
||||||
|
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs):
|
||||||
|
self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
|
||||||
|
while True:
|
||||||
|
self._main_loop()
|
||||||
|
|
||||||
|
def _init(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs):
|
||||||
|
self._go_args = kwargs
|
||||||
|
self.hid_type = hid_type
|
||||||
|
self.secondary_hid_type = secondary_hid_type
|
||||||
|
|
||||||
|
self._init_sanity_check()
|
||||||
|
self._init_hid()
|
||||||
|
self._init_matrix()
|
||||||
|
self._init_coord_mapping()
|
||||||
|
|
||||||
|
for module in self.modules:
|
||||||
|
try:
|
||||||
|
module.during_bootup(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to load module', err, module)
|
||||||
|
print()
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
ext.during_bootup(self)
|
||||||
|
except Exception as err:
|
||||||
|
if self.debug_enabled:
|
||||||
|
print('Failed to load extension', err, ext)
|
||||||
|
|
||||||
|
self._print_debug_cycle(init=True)
|
||||||
|
|
||||||
|
def _main_loop(self):
|
||||||
|
self.state_changed = False
|
||||||
|
self.sandbox.active_layers = self.active_layers.copy()
|
||||||
|
|
||||||
|
self.before_matrix_scan()
|
||||||
|
|
||||||
|
for matrix in self.matrix:
|
||||||
|
update = matrix.scan_for_changes()
|
||||||
|
if update:
|
||||||
|
self.matrix_update = update
|
||||||
|
break
|
||||||
|
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._old_timeouts_len = len(self._timeouts)
|
||||||
|
self._process_timeouts()
|
||||||
|
self._new_timeouts_len = len(self._timeouts)
|
||||||
|
|
||||||
|
if self._old_timeouts_len != self._new_timeouts_len:
|
||||||
|
self.state_changed = True
|
||||||
|
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()
|
||||||
|
|
||||||
|
if self.state_changed:
|
||||||
|
self._print_debug_cycle()
|
||||||
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, start):
|
||||||
|
diff = (new - start) & _TICKS_MAX
|
||||||
|
diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
|
||||||
|
return diff
|
||||||
|
|
||||||
|
|
||||||
|
def ticks_add(ticks, delta):
|
||||||
|
return (ticks + delta) % _TICKS_PERIOD
|
||||||
|
|
||||||
|
|
||||||
|
def check_deadline(new, start, ms):
|
||||||
|
return ticks_diff(new, start) < ms
|
||||||
|
|
||||||
|
|
||||||
|
class PeriodicTimer:
|
||||||
|
def __init__(self, period):
|
||||||
|
self.period = period
|
||||||
|
self.last_tick = ticks_ms()
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
now = ticks_ms()
|
||||||
|
if ticks_diff(now, self.last_tick) >= self.period:
|
||||||
|
self.last_tick = now
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
43
kmk/modules/__init__.py
Normal file
43
kmk/modules/__init__.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
241
kmk/modules/adns9800.py
Normal file
241
kmk/modules/adns9800.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import busio
|
||||||
|
import digitalio
|
||||||
|
import microcontroller
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from kmk.modules import Module
|
||||||
|
from kmk.modules.adns9800_firmware import firmware
|
||||||
|
from kmk.modules.mouse_keys import PointingDevice
|
||||||
|
|
||||||
|
|
||||||
|
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.pointing_device = PointingDevice()
|
||||||
|
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 < 0:
|
||||||
|
self.pointing_device.report_x[0] = (delta_x & 0xFF) | 0x80
|
||||||
|
else:
|
||||||
|
self.pointing_device.report_x[0] = delta_x & 0xFF
|
||||||
|
|
||||||
|
if delta_y < 0:
|
||||||
|
self.pointing_device.report_y[0] = (delta_y & 0xFF) | 0x80
|
||||||
|
else:
|
||||||
|
self.pointing_device.report_y[0] = delta_y & 0xFF
|
||||||
|
|
||||||
|
if keyboard.debug_enabled:
|
||||||
|
print('Delta: ', delta_x, ' ', delta_y)
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
if self.pointing_device.hid_pending:
|
||||||
|
keyboard._hid_helper.hid_send(self.pointing_device._evt)
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
|
self.pointing_device.report_x[0] = 0
|
||||||
|
self.pointing_device.report_y[0] = 0
|
||||||
|
|
||||||
|
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
|
||||||
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)
|
||||||
|
self._numbers = range(KC.N1.code, KC.N0.code)
|
||||||
|
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
|
||||||
206
kmk/modules/combos.py
Normal file
206
kmk/modules/combos.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import kmk.handlers.stock as handlers
|
||||||
|
from kmk.keys import make_key
|
||||||
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
|
||||||
|
class Combo:
|
||||||
|
timeout = 50
|
||||||
|
per_key_timeout = False
|
||||||
|
_timeout = None
|
||||||
|
_remaining = []
|
||||||
|
|
||||||
|
def __init__(self, match, result, timeout=None, per_key_timeout=None):
|
||||||
|
'''
|
||||||
|
match: tuple of keys (KC.A, KC.B)
|
||||||
|
result: key KC.C
|
||||||
|
'''
|
||||||
|
self.match = match
|
||||||
|
self.result = result
|
||||||
|
if timeout:
|
||||||
|
self.timeout = timeout
|
||||||
|
if per_key_timeout:
|
||||||
|
self.per_key_timeout = per_key_timeout
|
||||||
|
|
||||||
|
def matches(self, key):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._remaining = list(self.match)
|
||||||
|
|
||||||
|
|
||||||
|
class Chord(Combo):
|
||||||
|
def matches(self, key):
|
||||||
|
try:
|
||||||
|
self._remaining.remove(key)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Sequence(Combo):
|
||||||
|
timeout = 1000
|
||||||
|
per_key_timeout = True
|
||||||
|
|
||||||
|
def matches(self, key):
|
||||||
|
try:
|
||||||
|
return key == self._remaining.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Combos(Module):
|
||||||
|
def __init__(self, combos=[]):
|
||||||
|
self.combos = combos
|
||||||
|
self._active = []
|
||||||
|
self._matching = []
|
||||||
|
self._reset = set()
|
||||||
|
self._key_buffer = []
|
||||||
|
|
||||||
|
make_key(
|
||||||
|
names=('LEADER',),
|
||||||
|
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, 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, key, int_coord):
|
||||||
|
# refill potential matches from timed-out matches
|
||||||
|
if not self._matching:
|
||||||
|
self._matching = list(self._reset)
|
||||||
|
self._reset = set()
|
||||||
|
|
||||||
|
# filter potential matches
|
||||||
|
for combo in self._matching.copy():
|
||||||
|
if combo.matches(key):
|
||||||
|
continue
|
||||||
|
self._matching.remove(combo)
|
||||||
|
if combo._timeout:
|
||||||
|
keyboard.cancel_timeout(combo._timeout)
|
||||||
|
combo._timeout = keyboard.set_timeout(
|
||||||
|
combo.timeout, lambda c=combo: self.reset_combo(keyboard, c)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._matching:
|
||||||
|
# At least one combo matches current key: append key to buffer.
|
||||||
|
self._key_buffer.append((int_coord, key, True))
|
||||||
|
key = None
|
||||||
|
|
||||||
|
# Start or reset individual combo timeouts.
|
||||||
|
for combo in self._matching:
|
||||||
|
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
|
||||||
|
self.send_key_buffer(keyboard)
|
||||||
|
self._key_buffer = []
|
||||||
|
key = keyboard._find_key_in_map(int_coord)
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def on_release(self, keyboard, key, int_coord):
|
||||||
|
for combo in self._active:
|
||||||
|
if key in combo.match:
|
||||||
|
# Deactivate combo if it matches current key.
|
||||||
|
self.deactivate(keyboard, combo)
|
||||||
|
self.reset_combo(keyboard, combo)
|
||||||
|
key = combo.result
|
||||||
|
break
|
||||||
|
|
||||||
|
# Don't propagate key-release events for keys that have been buffered.
|
||||||
|
# Append release events only if corresponding press is in buffer.
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
self._matching.remove(combo)
|
||||||
|
|
||||||
|
if not combo._remaining:
|
||||||
|
self.activate(keyboard, combo)
|
||||||
|
if any([not pressed for (int_coord, key, pressed) in self._key_buffer]):
|
||||||
|
# At least one of the combo keys has already been released:
|
||||||
|
# "tap" the combo result.
|
||||||
|
keyboard._send_hid()
|
||||||
|
self.deactivate(keyboard, combo)
|
||||||
|
self.reset(keyboard)
|
||||||
|
self._key_buffer = []
|
||||||
|
else:
|
||||||
|
if not self._matching:
|
||||||
|
# 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:
|
||||||
|
try:
|
||||||
|
new_key = keyboard._coordkeys_pressed[int_coord]
|
||||||
|
except KeyError:
|
||||||
|
new_key = None
|
||||||
|
if new_key is None:
|
||||||
|
new_key = keyboard._find_key_in_map(int_coord)
|
||||||
|
|
||||||
|
keyboard._coordkeys_pressed[int_coord] = new_key
|
||||||
|
|
||||||
|
keyboard.process_key(new_key, is_pressed)
|
||||||
|
keyboard._send_hid()
|
||||||
|
|
||||||
|
def activate(self, keyboard, combo):
|
||||||
|
combo.result.on_press(keyboard)
|
||||||
|
self._active.append(combo)
|
||||||
|
|
||||||
|
def deactivate(self, keyboard, combo):
|
||||||
|
combo.result.on_release(keyboard)
|
||||||
|
self._active.remove(combo)
|
||||||
|
|
||||||
|
def reset_combo(self, keyboard, combo):
|
||||||
|
combo.reset()
|
||||||
|
if combo._timeout is not None:
|
||||||
|
keyboard.cancel_timeout(combo._timeout)
|
||||||
|
combo._timeout = None
|
||||||
|
self._reset.add(combo)
|
||||||
|
|
||||||
|
def reset(self, keyboard):
|
||||||
|
self._matching = []
|
||||||
|
for combo in self.combos:
|
||||||
|
self.reset_combo(keyboard, combo)
|
||||||
145
kmk/modules/easypoint.py
Normal file
145
kmk/modules/easypoint.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
'''
|
||||||
|
Extension handles usage of AS5013 by AMS
|
||||||
|
'''
|
||||||
|
|
||||||
|
from supervisor import ticks_ms
|
||||||
|
|
||||||
|
from kmk.modules import Module
|
||||||
|
from kmk.modules.mouse_keys import PointingDevice
|
||||||
|
|
||||||
|
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.pointing_device = PointingDevice()
|
||||||
|
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
|
||||||
|
self.pointing_device.report_x[0] = x
|
||||||
|
self.pointing_device.report_y[0] = y
|
||||||
|
|
||||||
|
self.pointing_device.hid_pending = x != 0 or y != 0
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_matrix_scan(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def before_hid_send(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_hid_send(self, keyboard):
|
||||||
|
if self.pointing_device.hid_pending:
|
||||||
|
keyboard._hid_helper.hid_send(self.pointing_device._evt)
|
||||||
|
self._clear_pending_hid()
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_enable(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_disable(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _clear_pending_hid(self):
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
|
self.pointing_device.report_x[0] = 0
|
||||||
|
self.pointing_device.report_y[0] = 0
|
||||||
|
self.pointing_device.report_w[0] = 0
|
||||||
|
self.pointing_device.button_status[0] = 0
|
||||||
|
|
||||||
|
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()
|
||||||
289
kmk/modules/encoder.py
Normal file
289
kmk/modules/encoder.py
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
# 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):
|
||||||
|
|
||||||
|
self.is_inverted = is_inverted
|
||||||
|
|
||||||
|
self._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:
|
||||||
|
# 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 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 == (True, True):
|
||||||
|
if self._movement > 2:
|
||||||
|
# 1 full step is 4 movements, however, when rotated quickly,
|
||||||
|
# some steps may be missed. This makes it behaves more
|
||||||
|
# naturally
|
||||||
|
real_movement = round(self._movement / 4)
|
||||||
|
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())
|
||||||
|
# Reinit to properly identify new movement
|
||||||
|
self._movement = 0
|
||||||
|
self._direction = 0
|
||||||
|
|
||||||
|
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):
|
||||||
|
super().__init__(is_inverted)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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:
|
||||||
|
gpio_pins = pins[:3]
|
||||||
|
new_encoder = GPIOEncoder(*gpio_pins)
|
||||||
|
|
||||||
|
# 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
|
||||||
164
kmk/modules/holdtap.py
Normal file
164
kmk/modules/holdtap.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
|
||||||
|
class ActivationType:
|
||||||
|
PRESSED = const(0)
|
||||||
|
RELEASED = const(1)
|
||||||
|
HOLD_TIMEOUT = const(2)
|
||||||
|
INTERRUPTED = 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 HoldTap(Module):
|
||||||
|
tap_time = 300
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.key_buffer = []
|
||||||
|
self.key_states = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
for key, state in self.key_states.items():
|
||||||
|
if key == current_key:
|
||||||
|
continue
|
||||||
|
if state.activated != ActivationType.PRESSED:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard._send_hid()
|
||||||
|
|
||||||
|
self.send_key_buffer(keyboard)
|
||||||
|
|
||||||
|
# if interrupt on release: store interrupting keys until one of them
|
||||||
|
# is released.
|
||||||
|
if key.meta.tap_interrupted:
|
||||||
|
if is_pressed:
|
||||||
|
self.key_buffer.append((int_coord, current_key))
|
||||||
|
current_key = None
|
||||||
|
|
||||||
|
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):
|
||||||
|
'''Do nothing yet, action resolves when key is released, timer expires or other key is pressed.'''
|
||||||
|
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 in self.key_states:
|
||||||
|
state = self.key_states[key]
|
||||||
|
keyboard.cancel_timeout(state.timeout_key)
|
||||||
|
if state.activated == ActivationType.HOLD_TIMEOUT:
|
||||||
|
# release hold
|
||||||
|
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
|
||||||
|
elif state.activated == ActivationType.INTERRUPTED:
|
||||||
|
# release tap
|
||||||
|
self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs)
|
||||||
|
elif state.activated == ActivationType.PRESSED:
|
||||||
|
# press and release tap because key released within tap time
|
||||||
|
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
||||||
|
keyboard.set_timeout(
|
||||||
|
False,
|
||||||
|
lambda: self.ht_deactivate_tap(key, keyboard, *args, **kwargs),
|
||||||
|
)
|
||||||
|
self.send_key_buffer(keyboard)
|
||||||
|
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 keyboard.debug_enabled:
|
||||||
|
print(f'HoldTap.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):
|
||||||
|
for (int_coord, key) in self.key_buffer:
|
||||||
|
key.on_press(keyboard)
|
||||||
|
keyboard._send_hid()
|
||||||
|
self.key_buffer.clear()
|
||||||
|
|
||||||
|
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
|
||||||
|
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 key.meta.prefer_hold:
|
||||||
|
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
||||||
183
kmk/modules/layers.py
Normal file
183
kmk/modules/layers.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
'''One layer isn't enough. Adds keys to get to more of them'''
|
||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from kmk.key_validators import layer_key_validator
|
||||||
|
from kmk.keys import make_argumented_key
|
||||||
|
from kmk.modules.holdtap import ActivationType, HoldTap
|
||||||
|
|
||||||
|
|
||||||
|
class LayerType:
|
||||||
|
'''Defines layer types to be passed on as on_press and on_release kwargs where needed'''
|
||||||
|
|
||||||
|
LT = const(0)
|
||||||
|
TT = const(1)
|
||||||
|
|
||||||
|
|
||||||
|
def curry(fn, *args, **kwargs):
|
||||||
|
def curried(*fn_args, **fn_kwargs):
|
||||||
|
merged_args = args + fn_args
|
||||||
|
merged_kwargs = kwargs.copy()
|
||||||
|
merged_kwargs.update(fn_kwargs)
|
||||||
|
return fn(*merged_args, **merged_kwargs)
|
||||||
|
|
||||||
|
return curried
|
||||||
|
|
||||||
|
|
||||||
|
class Layers(HoldTap):
|
||||||
|
'''Gives access to the keys used to enable the layer system'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Layers
|
||||||
|
super().__init__()
|
||||||
|
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=('LT',),
|
||||||
|
on_press=curry(self.ht_pressed, key_type=LayerType.LT),
|
||||||
|
on_release=curry(self.ht_released, key_type=LayerType.LT),
|
||||||
|
)
|
||||||
|
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=curry(layer_key_validator, prefer_hold=True),
|
||||||
|
names=('TT',),
|
||||||
|
on_press=curry(self.ht_pressed, key_type=LayerType.TT),
|
||||||
|
on_release=curry(self.ht_released, key_type=LayerType.TT),
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||||
|
current_key = super().process_key(keyboard, key, is_pressed, int_coord)
|
||||||
|
|
||||||
|
for key, state in self.key_states.items():
|
||||||
|
if key == current_key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# on interrupt: key must be translated here, because it was asigned
|
||||||
|
# before the layer shift happend.
|
||||||
|
if state.activated == ActivationType.INTERRUPTED:
|
||||||
|
current_key = keyboard._find_key_in_map(int_coord)
|
||||||
|
|
||||||
|
return current_key
|
||||||
|
|
||||||
|
def send_key_buffer(self, keyboard):
|
||||||
|
for (int_coord, old_key) in self.key_buffer:
|
||||||
|
new_key = keyboard._find_key_in_map(int_coord)
|
||||||
|
|
||||||
|
# adding keys late to _coordkeys_pressed isn't pretty,
|
||||||
|
# but necessary to mitigate race conditions when multiple
|
||||||
|
# keys are pressed during a tap-interrupted hold-tap.
|
||||||
|
keyboard._coordkeys_pressed[int_coord] = new_key
|
||||||
|
new_key.on_press(keyboard)
|
||||||
|
|
||||||
|
keyboard._send_hid()
|
||||||
|
|
||||||
|
self.key_buffer.clear()
|
||||||
|
|
||||||
|
def _df_pressed(self, key, keyboard, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Switches the default layer
|
||||||
|
'''
|
||||||
|
keyboard.active_layers[-1] = key.meta.layer
|
||||||
|
|
||||||
|
def _mo_pressed(self, key, keyboard, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Momentarily activates layer, switches off when you let go
|
||||||
|
'''
|
||||||
|
keyboard.active_layers.insert(0, key.meta.layer)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _mo_released(key, keyboard, *args, **kwargs):
|
||||||
|
# remove the first instance of the target layer
|
||||||
|
# from the active list
|
||||||
|
# under almost all normal use cases, this will
|
||||||
|
# disable the layer (but preserve it if it was triggered
|
||||||
|
# as a default layer, etc.)
|
||||||
|
# this also resolves an issue where using DF() on a layer
|
||||||
|
# triggered by MO() and then defaulting to the MO()'s layer
|
||||||
|
# would result in no layers active
|
||||||
|
try:
|
||||||
|
del_idx = keyboard.active_layers.index(key.meta.layer)
|
||||||
|
del keyboard.active_layers[del_idx]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _lm_pressed(self, key, keyboard, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
As MO(layer) but with mod active
|
||||||
|
'''
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
# Sets the timer start and acts like MO otherwise
|
||||||
|
keyboard.keys_pressed.add(key.meta.kc)
|
||||||
|
self._mo_pressed(key, keyboard, *args, **kwargs)
|
||||||
|
|
||||||
|
def _lm_released(self, key, keyboard, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
As MO(layer) but with mod active
|
||||||
|
'''
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
keyboard.keys_pressed.discard(key.meta.kc)
|
||||||
|
self._mo_released(key, keyboard, *args, **kwargs)
|
||||||
|
|
||||||
|
def _tg_pressed(self, key, keyboard, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Toggles the layer (enables it if not active, and vise versa)
|
||||||
|
'''
|
||||||
|
# See mo_released for implementation details around this
|
||||||
|
try:
|
||||||
|
del_idx = keyboard.active_layers.index(key.meta.layer)
|
||||||
|
del keyboard.active_layers[del_idx]
|
||||||
|
except ValueError:
|
||||||
|
keyboard.active_layers.insert(0, key.meta.layer)
|
||||||
|
|
||||||
|
def _to_pressed(self, key, keyboard, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Activates layer and deactivates all other layers
|
||||||
|
'''
|
||||||
|
keyboard.active_layers.clear()
|
||||||
|
keyboard.active_layers.insert(0, key.meta.layer)
|
||||||
|
|
||||||
|
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
key_type = kwargs['key_type']
|
||||||
|
if key_type == LayerType.LT:
|
||||||
|
self._mo_pressed(key, keyboard, *args, **kwargs)
|
||||||
|
elif key_type == LayerType.TT:
|
||||||
|
self._tg_pressed(key, keyboard, *args, **kwargs)
|
||||||
|
|
||||||
|
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
key_type = kwargs['key_type']
|
||||||
|
if key_type == LayerType.LT:
|
||||||
|
self._mo_released(key, keyboard, *args, **kwargs)
|
||||||
|
elif key_type == LayerType.TT:
|
||||||
|
self._tg_pressed(key, keyboard, *args, **kwargs)
|
||||||
|
|
||||||
|
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
key_type = kwargs['key_type']
|
||||||
|
if key_type == LayerType.LT:
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
keyboard.keys_pressed.add(key.meta.kc)
|
||||||
|
elif key_type == LayerType.TT:
|
||||||
|
self._tg_pressed(key, keyboard, *args, **kwargs)
|
||||||
|
|
||||||
|
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
key_type = kwargs['key_type']
|
||||||
|
if key_type == LayerType.LT:
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
keyboard.keys_pressed.discard(key.meta.kc)
|
||||||
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))
|
||||||
27
kmk/modules/modtap.py
Normal file
27
kmk/modules/modtap.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import kmk.handlers.stock as handlers
|
||||||
|
from kmk.key_validators import mod_tap_validator
|
||||||
|
from kmk.keys import make_argumented_key
|
||||||
|
from kmk.modules.holdtap import HoldTap
|
||||||
|
|
||||||
|
|
||||||
|
class ModTap(HoldTap):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
make_argumented_key(
|
||||||
|
validator=mod_tap_validator,
|
||||||
|
names=('MT',),
|
||||||
|
on_press=self.ht_pressed,
|
||||||
|
on_release=self.ht_released,
|
||||||
|
)
|
||||||
|
|
||||||
|
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
handlers.default_pressed(key.meta.mods, keyboard, None)
|
||||||
|
|
||||||
|
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
handlers.default_released(key.meta.mods, keyboard, None)
|
||||||
|
|
||||||
|
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
handlers.default_pressed(key.meta.kc, keyboard, None)
|
||||||
|
|
||||||
|
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
handlers.default_released(key.meta.kc, keyboard, None)
|
||||||
245
kmk/modules/mouse_keys.py
Normal file
245
kmk/modules/mouse_keys.py
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
from supervisor import ticks_ms
|
||||||
|
|
||||||
|
from kmk.hid import HID_REPORT_SIZES, HIDReportTypes
|
||||||
|
from kmk.keys import make_key
|
||||||
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
|
||||||
|
class PointingDevice:
|
||||||
|
MB_LMB = 1
|
||||||
|
MB_RMB = 2
|
||||||
|
MB_MMB = 4
|
||||||
|
_evt = bytearray(HID_REPORT_SIZES[HIDReportTypes.MOUSE] + 1)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.key_states = {}
|
||||||
|
self.hid_pending = False
|
||||||
|
self.report_device = memoryview(self._evt)[0:1]
|
||||||
|
self.report_device[0] = HIDReportTypes.MOUSE
|
||||||
|
self.button_status = memoryview(self._evt)[1:2]
|
||||||
|
self.report_x = memoryview(self._evt)[2:3]
|
||||||
|
self.report_y = memoryview(self._evt)[3:4]
|
||||||
|
self.report_w = memoryview(self._evt)[4:]
|
||||||
|
|
||||||
|
|
||||||
|
class MouseKeys(Module):
|
||||||
|
def __init__(self):
|
||||||
|
self.pointing_device = PointingDevice()
|
||||||
|
self._nav_key_activated = 0
|
||||||
|
self._up_activated = False
|
||||||
|
self._down_activated = False
|
||||||
|
self._left_activated = False
|
||||||
|
self._right_activated = False
|
||||||
|
self.max_speed = 10
|
||||||
|
self.ac_interval = 100 # Delta ms to apply acceleration
|
||||||
|
self._next_interval = 0 # Time for next tick interval
|
||||||
|
self.move_step = 1
|
||||||
|
|
||||||
|
make_key(
|
||||||
|
names=('MB_LMB',),
|
||||||
|
on_press=self._mb_lmb_press,
|
||||||
|
on_release=self._mb_lmb_release,
|
||||||
|
)
|
||||||
|
make_key(
|
||||||
|
names=('MB_MMB',),
|
||||||
|
on_press=self._mb_mmb_press,
|
||||||
|
on_release=self._mb_mmb_release,
|
||||||
|
)
|
||||||
|
make_key(
|
||||||
|
names=('MB_RMB',),
|
||||||
|
on_press=self._mb_rmb_press,
|
||||||
|
on_release=self._mb_rmb_release,
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
return
|
||||||
|
|
||||||
|
def matrix_detected_press(self, keyboard):
|
||||||
|
return keyboard.matrix_update is None
|
||||||
|
|
||||||
|
def before_matrix_scan(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_matrix_scan(self, keyboard):
|
||||||
|
if self._nav_key_activated:
|
||||||
|
if self._next_interval <= ticks_ms():
|
||||||
|
# print("hello: ")
|
||||||
|
# print(ticks_ms())
|
||||||
|
self._next_interval = ticks_ms() + self.ac_interval
|
||||||
|
# print(self._next_interval)
|
||||||
|
if self.move_step < self.max_speed:
|
||||||
|
self.move_step = self.move_step + 1
|
||||||
|
if self._right_activated:
|
||||||
|
self.pointing_device.report_x[0] = self.move_step
|
||||||
|
if self._left_activated:
|
||||||
|
self.pointing_device.report_x[0] = 0xFF & (0 - self.move_step)
|
||||||
|
if self._up_activated:
|
||||||
|
self.pointing_device.report_y[0] = 0xFF & (0 - self.move_step)
|
||||||
|
if self._down_activated:
|
||||||
|
self.pointing_device.report_y[0] = self.move_step
|
||||||
|
# self.pointing_device.hid_pending = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def before_hid_send(self, keyboard):
|
||||||
|
if self.pointing_device.hid_pending:
|
||||||
|
keyboard._hid_helper.hid_send(self.pointing_device._evt)
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_hid_send(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_enable(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_disable(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _mb_lmb_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.button_status[0] |= self.pointing_device.MB_LMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mb_lmb_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.button_status[0] &= ~self.pointing_device.MB_LMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mb_mmb_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.button_status[0] |= self.pointing_device.MB_MMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mb_mmb_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.button_status[0] &= ~self.pointing_device.MB_MMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mb_rmb_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.button_status[0] |= self.pointing_device.MB_RMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mb_rmb_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.button_status[0] &= ~self.pointing_device.MB_RMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mw_up_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.report_w[0] = self.move_step
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mw_up_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.report_w[0] = 0
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mw_down_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.report_w[0] = 0xFF
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _mw_down_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.pointing_device.report_w[0] = 0
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
# Mouse movement
|
||||||
|
def _reset_next_interval(self):
|
||||||
|
if self._nav_key_activated == 1:
|
||||||
|
self._next_interval = ticks_ms() + self.ac_interval
|
||||||
|
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
|
||||||
|
self.pointing_device.report_y[0] = 0xFF & (0 - self.move_step)
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _ms_up_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._up_activated = False
|
||||||
|
self._nav_key_activated -= 1
|
||||||
|
self._check_last()
|
||||||
|
self.pointing_device.report_y[0] = 0
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
|
|
||||||
|
def _ms_down_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._nav_key_activated += 1
|
||||||
|
self._reset_next_interval()
|
||||||
|
self._down_activated = True
|
||||||
|
# if not self.x_activated and not self.y_activated:
|
||||||
|
# self.next_interval = ticks_ms() + self.ac_intervalle
|
||||||
|
self.pointing_device.report_y[0] = self.move_step
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _ms_down_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._down_activated = False
|
||||||
|
self._nav_key_activated -= 1
|
||||||
|
self._check_last()
|
||||||
|
self.pointing_device.report_y[0] = 0
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
|
|
||||||
|
def _ms_left_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._nav_key_activated += 1
|
||||||
|
self._reset_next_interval()
|
||||||
|
self._left_activated = True
|
||||||
|
self.pointing_device.report_x[0] = 0xFF & (0 - self.move_step)
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _ms_left_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._nav_key_activated -= 1
|
||||||
|
self._left_activated = False
|
||||||
|
self._check_last()
|
||||||
|
self.pointing_device.report_x[0] = 0
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
|
|
||||||
|
def _ms_right_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._nav_key_activated += 1
|
||||||
|
self._reset_next_interval()
|
||||||
|
self._right_activated = True
|
||||||
|
self.pointing_device.report_x[0] = self.move_step
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
def _ms_right_release(self, key, keyboard, *args, **kwargs):
|
||||||
|
self._nav_key_activated -= 1
|
||||||
|
self._right_activated = False
|
||||||
|
self._check_last()
|
||||||
|
self.pointing_device.report_x[0] = 0
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
85
kmk/modules/oled.py
Normal file
85
kmk/modules/oled.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import adafruit_ssd1306
|
||||||
|
import busio
|
||||||
|
import time
|
||||||
|
|
||||||
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
class Oled(Module):
|
||||||
|
def __init__(self, pin1=None, pin2=None, i2c=None, startup_image = None, anim1 = None, anim2 = None):
|
||||||
|
print("Init OLED")
|
||||||
|
if i2c is None:
|
||||||
|
self.i2c = busio.I2C(scl=pin1, sda=pin2)
|
||||||
|
else:
|
||||||
|
self.i2c = i2c
|
||||||
|
self.startup_image = startup_image
|
||||||
|
self.anims = [anim1, anim2]
|
||||||
|
self.anim_state = 0
|
||||||
|
self.last_state = None
|
||||||
|
self.oled = adafruit_ssd1306.SSD1306_I2C(128, 32, self.i2c)
|
||||||
|
self.oled.text("Init", 0, 0, 1)
|
||||||
|
self.oled.show()
|
||||||
|
self.inactive_timer = time.time()
|
||||||
|
self.timeout = 3
|
||||||
|
self.inactive_timeout = 30
|
||||||
|
self.last_anim = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _oled_blank(self):
|
||||||
|
self.oled.fill(0)
|
||||||
|
self.oled.show()
|
||||||
|
|
||||||
|
def _draw_image(self, image, size=(32, 128), offset=0, clear=False):
|
||||||
|
print("calling draw_image")
|
||||||
|
if clear:
|
||||||
|
self.oled.fill(0)
|
||||||
|
for i in range(size[0]):
|
||||||
|
for j in range(size[1]):
|
||||||
|
if image[i][j] is False:
|
||||||
|
continue
|
||||||
|
# self.oled.buffer[i + j] = image[i][j]
|
||||||
|
self.oled.pixel(j+offset, i, 0 if image[i][j] is False else True)
|
||||||
|
# self.oled_changed = True
|
||||||
|
self.oled.show()
|
||||||
|
|
||||||
|
def during_bootup(self, keyboard):
|
||||||
|
if self.startup_image is not None:
|
||||||
|
self._draw_image(self.startup_image, clear=True)
|
||||||
|
|
||||||
|
print("booting")
|
||||||
|
keyboard.oled = self
|
||||||
|
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):
|
||||||
|
curr_time = time.time()
|
||||||
|
# print(time.time(), self.timeout + self.last_anim )
|
||||||
|
if curr_time > self.timeout + self.last_anim and self.last_anim != 0:
|
||||||
|
self._draw_image(self.startup_image, clear = True)
|
||||||
|
self.last_anim = 0
|
||||||
|
if curr_time > self.inactive_timer + self.inactive_timeout:
|
||||||
|
self._oled_blank()
|
||||||
|
return
|
||||||
|
|
||||||
|
def process_key(self, keyboard, key, is_pressed):
|
||||||
|
curr_time = time.time()
|
||||||
|
if is_pressed == 1:
|
||||||
|
# Move the drawing out of process_key
|
||||||
|
# self._draw_image(self.anims[self.anim_state], (32,32), 64-16, True)
|
||||||
|
self.anim_state = 1 if self.anim_state == 0 else 0
|
||||||
|
self.last_anim = curr_time
|
||||||
|
self.inactive_timer = curr_time
|
||||||
|
return super().process_key(keyboard, key, is_pressed)
|
||||||
78
kmk/modules/oled2.py
Normal file
78
kmk/modules/oled2.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import busio
|
||||||
|
import terminalio
|
||||||
|
import displayio
|
||||||
|
import adafruit_displayio_ssd1306
|
||||||
|
import time
|
||||||
|
from adafruit_display_text import label
|
||||||
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Oled(Module):
|
||||||
|
def __init__(self):
|
||||||
|
self.pins = None
|
||||||
|
self.label = label.Label(font=terminalio.FONT, text="", color = 0xFFFFFF)
|
||||||
|
displayio.release_displays()
|
||||||
|
self.lock_values = ['x', 'x', 'x']
|
||||||
|
self.old_locks_values = None
|
||||||
|
self.locks = None
|
||||||
|
self.layer_labels = {
|
||||||
|
0: "Def",
|
||||||
|
1: "RGB"
|
||||||
|
}
|
||||||
|
self.refresh_rate = 0.2
|
||||||
|
self.last_update = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _init_oled(self, pins):
|
||||||
|
self.bus = None
|
||||||
|
self._I2C = busio.I2C(scl=pins[0], sda=pins[1])
|
||||||
|
self.display_bus = displayio.I2CDisplay(i2c_bus=self._I2C, device_address=60)
|
||||||
|
self.display = adafruit_displayio_ssd1306.SSD1306(self.display_bus, width=128, height=32)
|
||||||
|
self.label.text = "INIT"
|
||||||
|
self.label.x = 1
|
||||||
|
self.label.y = 5
|
||||||
|
self.display.rotation = 90
|
||||||
|
self.display.show(self.label)
|
||||||
|
|
||||||
|
def _update_locks(self):
|
||||||
|
self.lock_values[0] = 'N' if self.locks.get_num_lock() else 'n'
|
||||||
|
self.lock_values[1] = 'C' if self.locks.get_caps_lock() else 'c'
|
||||||
|
self.lock_values[2] = 'S' if self.locks.get_scroll_lock() else 's'
|
||||||
|
|
||||||
|
def write_text(self, text, label):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def during_bootup(self, keyboard):
|
||||||
|
self._init_oled(self.pins)
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_hid_send(self, keyboard):
|
||||||
|
t = time.monotonic()
|
||||||
|
if self.last_update + self.refresh_rate < t:
|
||||||
|
self._update_locks()
|
||||||
|
self.label.text = f"{self.lock_values[0]}|{self.lock_values[1]}|{self.lock_values[2]}\n{self.layer_labels[keyboard.sandbox.active_layers[0]]}"
|
||||||
|
self.last_update = t
|
||||||
|
# if self.lock_values != self.old_locks_values:
|
||||||
|
# self.label.text = f"{self.lock_values[0]} {self.lock_values[1]} {self.lock_values[2]}\n{self.layer_labels[keyboard.sandbox.active_layers[0]]}"
|
||||||
|
# self.old_locks_values = self.lock_values
|
||||||
|
return
|
||||||
|
|
||||||
|
def before_hid_send(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def before_matrix_scan(self, keyboard):
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_matrix_scan(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_powersave_enable(self, keyboard):
|
||||||
|
self.display.sleep()
|
||||||
|
return super()
|
||||||
|
|
||||||
|
def on_powersave_disable(self, keyboard):
|
||||||
|
self.display.wake()
|
||||||
|
return
|
||||||
66
kmk/modules/oneshot.py
Normal file
66
kmk/modules/oneshot.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from kmk.keys import make_argumented_key
|
||||||
|
from kmk.modules.holdtap import ActivationType, HoldTap
|
||||||
|
from kmk.types import HoldTapKeyMeta
|
||||||
|
|
||||||
|
|
||||||
|
def oneshot_validator(kc, tap_time=None):
|
||||||
|
return HoldTapKeyMeta(kc=kc, prefer_hold=False, tap_time=tap_time)
|
||||||
|
|
||||||
|
|
||||||
|
class OneShot(HoldTap):
|
||||||
|
tap_time = 1000
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
make_argumented_key(
|
||||||
|
validator=oneshot_validator,
|
||||||
|
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 keyup.'''
|
||||||
|
for key, state in self.key_states.items():
|
||||||
|
if key == current_key:
|
||||||
|
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:
|
||||||
|
self.ht_released(key, 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)
|
||||||
|
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 keyboard.debug_enabled:
|
||||||
|
print(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
|
||||||
|
|
||||||
|
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
keyboard.process_key(key.meta.kc, True)
|
||||||
|
|
||||||
|
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
|
||||||
|
keyboard.process_key(key.meta.kc, False)
|
||||||
|
|
||||||
|
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
|
||||||
|
keyboard.process_key(key.meta.kc, False)
|
||||||
218
kmk/modules/pimoroni_trackball.py
Normal file
218
kmk/modules/pimoroni_trackball.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
'''
|
||||||
|
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 make_key
|
||||||
|
from kmk.kmktime import PeriodicTimer
|
||||||
|
from kmk.modules import Module
|
||||||
|
from kmk.modules.mouse_keys import PointingDevice
|
||||||
|
|
||||||
|
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 TrackballMode:
|
||||||
|
'''Behaviour mode of trackball: mouse movement or vertical scroll'''
|
||||||
|
|
||||||
|
MOUSE_MODE = const(0)
|
||||||
|
SCROLL_MODE = const(1)
|
||||||
|
|
||||||
|
|
||||||
|
class Trackball(Module):
|
||||||
|
'''Module handles usage of Trackball Breakout by Pimoroni'''
|
||||||
|
|
||||||
|
def __init__(self, i2c, mode=TrackballMode.MOUSE_MODE, address=I2C_ADDRESS):
|
||||||
|
self._i2c_address = address
|
||||||
|
self._i2c_bus = i2c
|
||||||
|
|
||||||
|
self.pointing_device = PointingDevice()
|
||||||
|
self.mode = mode
|
||||||
|
self.previous_state = False # click state
|
||||||
|
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(
|
||||||
|
'Invalid chip ID: 0x{:04X}, expected 0x{:04X}'.format(chip_id, CHIP_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
make_key(
|
||||||
|
names=('TB_MODE',),
|
||||||
|
on_press=self._tb_mode_press,
|
||||||
|
on_release=self._tb_mode_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()
|
||||||
|
|
||||||
|
if self.mode == TrackballMode.MOUSE_MODE:
|
||||||
|
x_axis, y_axis = self._calculate_movement(right - left, down - up)
|
||||||
|
if x_axis >= 0:
|
||||||
|
self.pointing_device.report_x[0] = x_axis
|
||||||
|
else:
|
||||||
|
self.pointing_device.report_x[0] = 0xFF & x_axis
|
||||||
|
if y_axis >= 0:
|
||||||
|
self.pointing_device.report_y[0] = y_axis
|
||||||
|
else:
|
||||||
|
self.pointing_device.report_y[0] = 0xFF & y_axis
|
||||||
|
self.pointing_device.hid_pending = x_axis != 0 or y_axis != 0
|
||||||
|
else: # SCROLL_MODE
|
||||||
|
if up >= 0:
|
||||||
|
self.pointing_device.report_w[0] = up
|
||||||
|
if down > 0:
|
||||||
|
self.pointing_device.report_w[0] = 0xFF & (0 - down)
|
||||||
|
self.pointing_device.hid_pending = up != 0 or down != 0
|
||||||
|
|
||||||
|
if switch == 1: # Button pressed
|
||||||
|
self.pointing_device.button_status[0] |= self.pointing_device.MB_LMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
if not state and self.previous_state is True: # Button released
|
||||||
|
self.pointing_device.button_status[0] &= ~self.pointing_device.MB_LMB
|
||||||
|
self.pointing_device.hid_pending = True
|
||||||
|
|
||||||
|
self.previous_state = state
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_matrix_scan(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def before_hid_send(self, keyboard):
|
||||||
|
return
|
||||||
|
|
||||||
|
def after_hid_send(self, keyboard):
|
||||||
|
if self.pointing_device.hid_pending:
|
||||||
|
keyboard._hid_helper.hid_send(self.pointing_device._evt)
|
||||||
|
self._clear_pending_hid()
|
||||||
|
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 _clear_pending_hid(self):
|
||||||
|
self.pointing_device.hid_pending = False
|
||||||
|
self.pointing_device.report_x[0] = 0
|
||||||
|
self.pointing_device.report_y[0] = 0
|
||||||
|
self.pointing_device.report_w[0] = 0
|
||||||
|
self.pointing_device.button_status[0] = 0
|
||||||
|
|
||||||
|
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 _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) + 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
|
||||||
|
|
||||||
|
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_mode_press(self, key, keyboard, *args, **kwargs):
|
||||||
|
self.mode = not self.mode
|
||||||
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
386
kmk/modules/split.py
Normal file
386
kmk/modules/split.py
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
'''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,
|
||||||
|
target_left=True,
|
||||||
|
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.target_left = target_left
|
||||||
|
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)
|
||||||
124
kmk/modules/tapdance.py
Normal file
124
kmk/modules/tapdance.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
from kmk.key_validators import tap_dance_key_validator
|
||||||
|
from kmk.keys import make_argumented_key
|
||||||
|
from kmk.modules import Module
|
||||||
|
from kmk.types import TapDanceKeyMeta
|
||||||
|
|
||||||
|
|
||||||
|
class TapDance(Module):
|
||||||
|
# User-configurable
|
||||||
|
tap_time = 300
|
||||||
|
|
||||||
|
# Internal State
|
||||||
|
_tapping = False
|
||||||
|
_tap_dance_counts = {}
|
||||||
|
_tap_timeout = None
|
||||||
|
_tap_side_effects = {}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
make_argumented_key(
|
||||||
|
validator=tap_dance_key_validator,
|
||||||
|
names=('TAP_DANCE', 'TD'),
|
||||||
|
on_press=self.td_pressed,
|
||||||
|
on_release=self.td_released,
|
||||||
|
)
|
||||||
|
|
||||||
|
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):
|
||||||
|
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, is_pressed, int_coord):
|
||||||
|
if self._tapping and is_pressed and not isinstance(key.meta, TapDanceKeyMeta):
|
||||||
|
for k, v in self._tap_dance_counts.items():
|
||||||
|
if v:
|
||||||
|
self._end_tap_dance(k, keyboard, hold=True)
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
keyboard._send_hid()
|
||||||
|
keyboard.set_timeout(
|
||||||
|
False, lambda: keyboard.process_key(key, is_pressed)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def td_pressed(self, key, keyboard, *args, **kwargs):
|
||||||
|
if key not in self._tap_dance_counts or not self._tap_dance_counts[key]:
|
||||||
|
self._tap_dance_counts[key] = 1
|
||||||
|
self._tapping = True
|
||||||
|
else:
|
||||||
|
keyboard.cancel_timeout(self._tap_timeout)
|
||||||
|
self._tap_dance_counts[key] += 1
|
||||||
|
|
||||||
|
if key not in self._tap_side_effects:
|
||||||
|
self._tap_side_effects[key] = None
|
||||||
|
|
||||||
|
self._tap_timeout = keyboard.set_timeout(
|
||||||
|
self.tap_time, lambda: self._end_tap_dance(key, keyboard, hold=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def td_released(self, key, keyboard, *args, **kwargs):
|
||||||
|
has_side_effects = self._tap_side_effects[key] is not None
|
||||||
|
hit_max_defined_taps = self._tap_dance_counts[key] == len(key.meta.codes)
|
||||||
|
|
||||||
|
keyboard.cancel_timeout(self._tap_timeout)
|
||||||
|
if has_side_effects or hit_max_defined_taps:
|
||||||
|
self._end_tap_dance(key, keyboard)
|
||||||
|
else:
|
||||||
|
self._tap_timeout = keyboard.set_timeout(
|
||||||
|
self.tap_time, lambda: self._end_tap_dance(key, keyboard)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _end_tap_dance(self, key, keyboard, hold=False):
|
||||||
|
v = self._tap_dance_counts[key] - 1
|
||||||
|
|
||||||
|
if v < 0:
|
||||||
|
return self
|
||||||
|
|
||||||
|
if key in keyboard.keys_pressed:
|
||||||
|
key_to_press = key.meta.codes[v]
|
||||||
|
keyboard.add_key(key_to_press)
|
||||||
|
self._tap_side_effects[key] = key_to_press
|
||||||
|
elif self._tap_side_effects[key]:
|
||||||
|
keyboard.remove_key(self._tap_side_effects[key])
|
||||||
|
self._tap_side_effects[key] = None
|
||||||
|
self._cleanup_tap_dance(key)
|
||||||
|
elif hold is False:
|
||||||
|
if key.meta.codes[v] in keyboard.keys_pressed:
|
||||||
|
keyboard.remove_key(key.meta.codes[v])
|
||||||
|
else:
|
||||||
|
keyboard.tap_key(key.meta.codes[v])
|
||||||
|
self._cleanup_tap_dance(key)
|
||||||
|
else:
|
||||||
|
key_to_press = key.meta.codes[v]
|
||||||
|
keyboard.add_key(key_to_press)
|
||||||
|
self._tap_side_effects[key] = key_to_press
|
||||||
|
self._tapping = 0
|
||||||
|
|
||||||
|
keyboard.hid_pending = True
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _cleanup_tap_dance(self, key):
|
||||||
|
self._tap_dance_counts[key] = 0
|
||||||
|
self._tapping = any(count > 0 for count in self._tap_dance_counts.values())
|
||||||
|
return self
|
||||||
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
|
||||||
144
kmk/scanners/digitalio.py
Normal file
144
kmk/scanners/digitalio.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
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(
|
||||||
|
'Invalid DiodeOrientation: {}'.format(self.diode_orientation)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
113
kmk/scanners/keypad.py
Normal file
113
kmk/scanners/keypad.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.curr_event = keypad.Event()
|
||||||
|
|
||||||
|
@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.curr_event
|
||||||
|
has_event = self.keypad.events.get_into(ev)
|
||||||
|
if has_event:
|
||||||
|
if self.offset:
|
||||||
|
return keypad.Event(ev.key_number + self.offset, ev.pressed)
|
||||||
|
else:
|
||||||
|
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__()
|
||||||
0
kmk/transports/__init__.py
Normal file
0
kmk/transports/__init__.py
Normal file
90
kmk/transports/pio_uart.py
Normal file
90
kmk/transports/pio_uart.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
'''
|
||||||
|
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 adafruit_pioasm
|
||||||
|
import rp2pio
|
||||||
|
|
||||||
|
tx_code = adafruit_pioasm.assemble(
|
||||||
|
'''
|
||||||
|
.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.
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
rx_code = adafruit_pioasm.assemble(
|
||||||
|
'''
|
||||||
|
.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
|
||||||
|
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
51
kmk/types.py
Normal file
51
kmk/types.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 HoldTapKeyMeta:
|
||||||
|
def __init__(self, kc=None, prefer_hold=True, tap_interrupted=False, tap_time=None):
|
||||||
|
self.kc = kc
|
||||||
|
self.prefer_hold = prefer_hold
|
||||||
|
self.tap_interrupted = tap_interrupted
|
||||||
|
self.tap_time = tap_time
|
||||||
|
|
||||||
|
|
||||||
|
class LayerKeyMeta(HoldTapKeyMeta):
|
||||||
|
def __init__(self, layer, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.layer = layer
|
||||||
|
|
||||||
|
|
||||||
|
class ModTapKeyMeta(HoldTapKeyMeta):
|
||||||
|
def __init__(self, kc=None, mods=None, **kwargs):
|
||||||
|
super().__init__(kc=kc, **kwargs)
|
||||||
|
self.mods = mods
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class TapDanceKeyMeta:
|
||||||
|
def __init__(self, codes):
|
||||||
|
self.codes = codes
|
||||||
2
kmk/utils.py
Normal file
2
kmk/utils.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
def clamp(x, bottom=0, top=100):
|
||||||
|
return min(max(bottom, x), top)
|
||||||
0
lib/adafruit_bitmap_font/__init__.py
Normal file
0
lib/adafruit_bitmap_font/__init__.py
Normal file
BIN
lib/adafruit_bitmap_font/bdf.mpy
Normal file
BIN
lib/adafruit_bitmap_font/bdf.mpy
Normal file
Binary file not shown.
BIN
lib/adafruit_bitmap_font/bitmap_font.mpy
Normal file
BIN
lib/adafruit_bitmap_font/bitmap_font.mpy
Normal file
Binary file not shown.
BIN
lib/adafruit_bitmap_font/glyph_cache.mpy
Normal file
BIN
lib/adafruit_bitmap_font/glyph_cache.mpy
Normal file
Binary file not shown.
BIN
lib/adafruit_bitmap_font/pcf.mpy
Normal file
BIN
lib/adafruit_bitmap_font/pcf.mpy
Normal file
Binary file not shown.
BIN
lib/adafruit_bitmap_font/ttf.mpy
Normal file
BIN
lib/adafruit_bitmap_font/ttf.mpy
Normal file
Binary file not shown.
465
lib/adafruit_display_text/__init__.py
Normal file
465
lib/adafruit_display_text/__init__.py
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text`
|
||||||
|
=======================
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Optional, Union, List, Tuple
|
||||||
|
from fontio import BuiltinFont
|
||||||
|
from adafruit_bitmap_font.bdf import BDF
|
||||||
|
from adafruit_bitmap_font.pcf import PCF
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
from displayio import Group, Palette
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text_to_pixels(
|
||||||
|
string: str,
|
||||||
|
max_width: int,
|
||||||
|
font: Optional[Union[BuiltinFont, BDF, PCF]] = None,
|
||||||
|
indent0: str = "",
|
||||||
|
indent1: str = "",
|
||||||
|
) -> List[str]:
|
||||||
|
# pylint: disable=too-many-branches, too-many-locals, too-many-nested-blocks, too-many-statements
|
||||||
|
|
||||||
|
"""wrap_text_to_pixels function
|
||||||
|
A helper that will return a list of lines with word-break wrapping.
|
||||||
|
Leading and trailing whitespace in your string will be removed. If
|
||||||
|
you wish to use leading whitespace see ``indent0`` and ``indent1``
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
:param str string: The text to be wrapped.
|
||||||
|
:param int max_width: The maximum number of pixels on a line before wrapping.
|
||||||
|
:param font: The font to use for measuring the text.
|
||||||
|
:type font: ~BuiltinFont, ~BDF, or ~PCF
|
||||||
|
:param str indent0: Additional character(s) to add to the first line.
|
||||||
|
:param str indent1: Additional character(s) to add to all other lines.
|
||||||
|
|
||||||
|
:return: A list of the lines resulting from wrapping the
|
||||||
|
input text at ``max_width`` pixels size
|
||||||
|
:rtype: List[str]
|
||||||
|
|
||||||
|
"""
|
||||||
|
if font is None:
|
||||||
|
|
||||||
|
def measure(text):
|
||||||
|
return len(text)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if hasattr(font, "load_glyphs"):
|
||||||
|
font.load_glyphs(string)
|
||||||
|
|
||||||
|
def measure(text):
|
||||||
|
return sum(font.get_glyph(ord(c)).shift_x for c in text)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
partial = [indent0]
|
||||||
|
width = measure(indent0)
|
||||||
|
swidth = measure(" ")
|
||||||
|
firstword = True
|
||||||
|
for line_in_input in string.split("\n"):
|
||||||
|
newline = True
|
||||||
|
for index, word in enumerate(line_in_input.split(" ")):
|
||||||
|
wwidth = measure(word)
|
||||||
|
word_parts = []
|
||||||
|
cur_part = ""
|
||||||
|
|
||||||
|
if wwidth > max_width:
|
||||||
|
for char in word:
|
||||||
|
if newline:
|
||||||
|
extraspace = 0
|
||||||
|
leadchar = ""
|
||||||
|
else:
|
||||||
|
extraspace = swidth
|
||||||
|
leadchar = " "
|
||||||
|
if (
|
||||||
|
measure("".join(partial))
|
||||||
|
+ measure(cur_part)
|
||||||
|
+ measure(char)
|
||||||
|
+ measure("-")
|
||||||
|
+ extraspace
|
||||||
|
> max_width
|
||||||
|
):
|
||||||
|
if cur_part:
|
||||||
|
word_parts.append(
|
||||||
|
"".join(partial) + leadchar + cur_part + "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
word_parts.append("".join(partial))
|
||||||
|
cur_part = char
|
||||||
|
partial = [indent1]
|
||||||
|
newline = True
|
||||||
|
else:
|
||||||
|
cur_part += char
|
||||||
|
if cur_part:
|
||||||
|
word_parts.append(cur_part)
|
||||||
|
for line in word_parts[:-1]:
|
||||||
|
lines.append(line)
|
||||||
|
partial.append(word_parts[-1])
|
||||||
|
width = measure(word_parts[-1])
|
||||||
|
if firstword:
|
||||||
|
firstword = False
|
||||||
|
else:
|
||||||
|
if firstword:
|
||||||
|
partial.append(word)
|
||||||
|
firstword = False
|
||||||
|
width += wwidth
|
||||||
|
elif width + swidth + wwidth < max_width:
|
||||||
|
if index > 0:
|
||||||
|
partial.append(" ")
|
||||||
|
partial.append(word)
|
||||||
|
width += wwidth + swidth
|
||||||
|
else:
|
||||||
|
lines.append("".join(partial))
|
||||||
|
partial = [indent1, word]
|
||||||
|
width = measure(indent1) + wwidth
|
||||||
|
if newline:
|
||||||
|
newline = False
|
||||||
|
|
||||||
|
lines.append("".join(partial))
|
||||||
|
partial = [indent1]
|
||||||
|
width = measure(indent1)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text_to_lines(string: str, max_chars: int) -> List[str]:
|
||||||
|
"""wrap_text_to_lines function
|
||||||
|
A helper that will return a list of lines with word-break wrapping
|
||||||
|
|
||||||
|
:param str string: The text to be wrapped
|
||||||
|
:param int max_chars: The maximum number of characters on a line before wrapping
|
||||||
|
|
||||||
|
:return: A list of lines where each line is separated based on the amount
|
||||||
|
of ``max_chars`` provided
|
||||||
|
:rtype: List[str]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def chunks(lst, n):
|
||||||
|
"""Yield successive n-sized chunks from lst."""
|
||||||
|
for i in range(0, len(lst), n):
|
||||||
|
yield lst[i : i + n]
|
||||||
|
|
||||||
|
string = string.replace("\n", "").replace("\r", "") # Strip confusing newlines
|
||||||
|
words = string.split(" ")
|
||||||
|
the_lines = []
|
||||||
|
the_line = ""
|
||||||
|
for w in words:
|
||||||
|
if len(w) > max_chars:
|
||||||
|
if the_line: # add what we had stored
|
||||||
|
the_lines.append(the_line)
|
||||||
|
parts = []
|
||||||
|
for part in chunks(w, max_chars - 1):
|
||||||
|
parts.append("{}-".format(part))
|
||||||
|
the_lines.extend(parts[:-1])
|
||||||
|
the_line = parts[-1][:-1]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(the_line + " " + w) <= max_chars:
|
||||||
|
the_line += " " + w
|
||||||
|
elif not the_line and len(w) == max_chars:
|
||||||
|
the_lines.append(w)
|
||||||
|
else:
|
||||||
|
the_lines.append(the_line)
|
||||||
|
the_line = "" + w
|
||||||
|
if the_line: # Last line remaining
|
||||||
|
the_lines.append(the_line)
|
||||||
|
# Remove any blank lines
|
||||||
|
while not the_lines[0]:
|
||||||
|
del the_lines[0]
|
||||||
|
# Remove first space from first line:
|
||||||
|
if the_lines[0][0] == " ":
|
||||||
|
the_lines[0] = the_lines[0][1:]
|
||||||
|
return the_lines
|
||||||
|
|
||||||
|
|
||||||
|
class LabelBase(Group):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
|
"""Superclass that all other types of labels will extend. This contains
|
||||||
|
all of the properties and functions that work the same way in all labels.
|
||||||
|
|
||||||
|
**Note:** This should be treated as an abstract base class.
|
||||||
|
|
||||||
|
Subclasses should implement ``_set_text``, ``_set_font``, and ``_set_line_spacing`` to
|
||||||
|
have the correct behavior for that type of label.
|
||||||
|
|
||||||
|
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||||
|
Must include a capital M for measuring character size.
|
||||||
|
:type font: ~BuiltinFont, ~BDF, or ~PCF
|
||||||
|
:param str text: Text to display
|
||||||
|
:param int color: Color of all text in RGB hex
|
||||||
|
:param int background_color: Color of the background, use `None` for transparent
|
||||||
|
:param float line_spacing: Line spacing of text to display
|
||||||
|
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||||
|
surround text. When set to 'True' Padding parameters will be ignored.
|
||||||
|
:param int padding_top: Additional pixels added to background bounding box at top
|
||||||
|
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
||||||
|
:param int padding_left: Additional pixels added to background bounding box at left
|
||||||
|
:param int padding_right: Additional pixels added to background bounding box at right
|
||||||
|
:param (float,float) anchor_point: Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||||
|
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple
|
||||||
|
containing x,y pixel coordinates.
|
||||||
|
:param int scale: Integer value of the pixel scaling
|
||||||
|
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||||
|
This is helpful when two or more labels need to be aligned to the same baseline
|
||||||
|
:param (int,str) tab_replacement: tuple with tab character replace information. When
|
||||||
|
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||||
|
tab character
|
||||||
|
:param str label_direction: string defining the label text orientation. See the
|
||||||
|
subclass documentation for the possible values."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
font: Union[BuiltinFont, BDF, PCF],
|
||||||
|
x: int = 0,
|
||||||
|
y: int = 0,
|
||||||
|
text: str = "",
|
||||||
|
color: int = 0xFFFFFF,
|
||||||
|
background_color: int = None,
|
||||||
|
line_spacing: float = 1.25,
|
||||||
|
background_tight: bool = False,
|
||||||
|
padding_top: int = 0,
|
||||||
|
padding_bottom: int = 0,
|
||||||
|
padding_left: int = 0,
|
||||||
|
padding_right: int = 0,
|
||||||
|
anchor_point: Tuple[float, float] = None,
|
||||||
|
anchored_position: Tuple[int, int] = None,
|
||||||
|
scale: int = 1,
|
||||||
|
base_alignment: bool = False,
|
||||||
|
tab_replacement: Tuple[int, str] = (4, " "),
|
||||||
|
label_direction: str = "LTR",
|
||||||
|
**kwargs, # pylint: disable=unused-argument
|
||||||
|
) -> None:
|
||||||
|
# pylint: disable=too-many-arguments, too-many-locals
|
||||||
|
|
||||||
|
super().__init__(x=x, y=y, scale=1)
|
||||||
|
|
||||||
|
self._font = font
|
||||||
|
self._text = text
|
||||||
|
self._palette = Palette(2)
|
||||||
|
self._color = 0xFFFFFF
|
||||||
|
self._background_color = None
|
||||||
|
self._line_spacing = line_spacing
|
||||||
|
self._background_tight = background_tight
|
||||||
|
self._padding_top = padding_top
|
||||||
|
self._padding_bottom = padding_bottom
|
||||||
|
self._padding_left = padding_left
|
||||||
|
self._padding_right = padding_right
|
||||||
|
self._anchor_point = anchor_point
|
||||||
|
self._anchored_position = anchored_position
|
||||||
|
self._base_alignment = base_alignment
|
||||||
|
self._label_direction = label_direction
|
||||||
|
self._tab_replacement = tab_replacement
|
||||||
|
self._tab_text = self._tab_replacement[1] * self._tab_replacement[0]
|
||||||
|
|
||||||
|
if "max_glyphs" in kwargs:
|
||||||
|
print("Please update your code: 'max_glyphs' is not needed anymore.")
|
||||||
|
|
||||||
|
self._ascent, self._descent = self._get_ascent_descent()
|
||||||
|
self._bounding_box = None
|
||||||
|
|
||||||
|
self.color = color
|
||||||
|
self.background_color = background_color
|
||||||
|
|
||||||
|
# local group will hold background and text
|
||||||
|
# the self group scale should always remain at 1, the self._local_group will
|
||||||
|
# be used to set the scale of the label
|
||||||
|
self._local_group = Group(scale=scale)
|
||||||
|
self.append(self._local_group)
|
||||||
|
|
||||||
|
self._baseline = -1.0
|
||||||
|
|
||||||
|
if self._base_alignment:
|
||||||
|
self._y_offset = 0
|
||||||
|
else:
|
||||||
|
self._y_offset = self._ascent // 2
|
||||||
|
|
||||||
|
def _get_ascent_descent(self) -> Tuple[int, int]:
|
||||||
|
"""Private function to calculate ascent and descent font values"""
|
||||||
|
if hasattr(self.font, "ascent") and hasattr(self.font, "descent"):
|
||||||
|
return self.font.ascent, self.font.descent
|
||||||
|
|
||||||
|
# check a few glyphs for maximum ascender and descender height
|
||||||
|
glyphs = "M j'" # choose glyphs with highest ascender and lowest
|
||||||
|
try:
|
||||||
|
self._font.load_glyphs(glyphs)
|
||||||
|
except AttributeError:
|
||||||
|
# Builtin font doesn't have or need load_glyphs
|
||||||
|
pass
|
||||||
|
# descender, will depend upon font used
|
||||||
|
ascender_max = descender_max = 0
|
||||||
|
for char in glyphs:
|
||||||
|
this_glyph = self._font.get_glyph(ord(char))
|
||||||
|
if this_glyph:
|
||||||
|
ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
|
||||||
|
descender_max = max(descender_max, -this_glyph.dy)
|
||||||
|
return ascender_max, descender_max
|
||||||
|
|
||||||
|
@property
|
||||||
|
def font(self) -> Union[BuiltinFont, BDF, PCF]:
|
||||||
|
"""Font to use for text display."""
|
||||||
|
return self._font
|
||||||
|
|
||||||
|
def _set_font(self, new_font: Union[BuiltinFont, BDF, PCF]) -> None:
|
||||||
|
raise NotImplementedError("{} MUST override '_set_font'".format(type(self)))
|
||||||
|
|
||||||
|
@font.setter
|
||||||
|
def font(self, new_font: Union[BuiltinFont, BDF, PCF]) -> None:
|
||||||
|
self._set_font(new_font)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self) -> int:
|
||||||
|
"""Color of the text as an RGB hex number."""
|
||||||
|
return self._color
|
||||||
|
|
||||||
|
@color.setter
|
||||||
|
def color(self, new_color: int):
|
||||||
|
self._color = new_color
|
||||||
|
if new_color is not None:
|
||||||
|
self._palette[1] = new_color
|
||||||
|
self._palette.make_opaque(1)
|
||||||
|
else:
|
||||||
|
self._palette[1] = 0
|
||||||
|
self._palette.make_transparent(1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def background_color(self) -> int:
|
||||||
|
"""Color of the background as an RGB hex number."""
|
||||||
|
return self._background_color
|
||||||
|
|
||||||
|
def _set_background_color(self, new_color):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_set_background_color'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
@background_color.setter
|
||||||
|
def background_color(self, new_color: int) -> None:
|
||||||
|
self._set_background_color(new_color)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anchor_point(self) -> Tuple[float, float]:
|
||||||
|
"""Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)"""
|
||||||
|
return self._anchor_point
|
||||||
|
|
||||||
|
@anchor_point.setter
|
||||||
|
def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None:
|
||||||
|
if new_anchor_point[1] == self._baseline:
|
||||||
|
self._anchor_point = (new_anchor_point[0], -1.0)
|
||||||
|
else:
|
||||||
|
self._anchor_point = new_anchor_point
|
||||||
|
|
||||||
|
# update the anchored_position using setter
|
||||||
|
self.anchored_position = self._anchored_position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anchored_position(self) -> Tuple[int, int]:
|
||||||
|
"""Position relative to the anchor_point. Tuple containing x,y
|
||||||
|
pixel coordinates."""
|
||||||
|
return self._anchored_position
|
||||||
|
|
||||||
|
@anchored_position.setter
|
||||||
|
def anchored_position(self, new_position: Tuple[int, int]) -> None:
|
||||||
|
self._anchored_position = new_position
|
||||||
|
# Calculate (x,y) position
|
||||||
|
if (self._anchor_point is not None) and (self._anchored_position is not None):
|
||||||
|
self.x = int(
|
||||||
|
new_position[0]
|
||||||
|
- (self._bounding_box[0] * self.scale)
|
||||||
|
- round(self._anchor_point[0] * (self._bounding_box[2] * self.scale))
|
||||||
|
)
|
||||||
|
if self._anchor_point[1] == self._baseline:
|
||||||
|
self.y = int(new_position[1] - (self._y_offset * self.scale))
|
||||||
|
else:
|
||||||
|
self.y = int(
|
||||||
|
new_position[1]
|
||||||
|
- (self._bounding_box[1] * self.scale)
|
||||||
|
- round(self._anchor_point[1] * self._bounding_box[3] * self.scale)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale(self) -> int:
|
||||||
|
"""Set the scaling of the label, in integer values"""
|
||||||
|
return self._local_group.scale
|
||||||
|
|
||||||
|
@scale.setter
|
||||||
|
def scale(self, new_scale: int) -> None:
|
||||||
|
self._local_group.scale = new_scale
|
||||||
|
self.anchored_position = self._anchored_position # update the anchored_position
|
||||||
|
|
||||||
|
def _set_text(self, new_text: str, scale: int) -> None:
|
||||||
|
raise NotImplementedError("{} MUST override '_set_text'".format(type(self)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self) -> str:
|
||||||
|
"""Text to be displayed."""
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
@text.setter # Cannot set color or background color with text setter, use separate setter
|
||||||
|
def text(self, new_text: str) -> None:
|
||||||
|
self._set_text(new_text, self.scale)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounding_box(self) -> Tuple[int, int]:
|
||||||
|
"""An (x, y, w, h) tuple that completely covers all glyphs. The
|
||||||
|
first two numbers are offset from the x, y origin of this group"""
|
||||||
|
return tuple(self._bounding_box)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> int:
|
||||||
|
"""The height of the label determined from the bounding box."""
|
||||||
|
return self._bounding_box[3] - self._bounding_box[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
"""The width of the label determined from the bounding box."""
|
||||||
|
return self._bounding_box[2] - self._bounding_box[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def line_spacing(self) -> float:
|
||||||
|
"""The amount of space between lines of text, in multiples of the font's
|
||||||
|
bounding-box height. (E.g. 1.0 is the bounding-box height)"""
|
||||||
|
return self._line_spacing
|
||||||
|
|
||||||
|
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_set_line_spacing'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
@line_spacing.setter
|
||||||
|
def line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
self._set_line_spacing(new_line_spacing)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_direction(self) -> str:
|
||||||
|
"""Set the text direction of the label"""
|
||||||
|
return self._label_direction
|
||||||
|
|
||||||
|
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_set_label_direction'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"{} MUST override '_get_valid_label_direction'".format(type(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
@label_direction.setter
|
||||||
|
def label_direction(self, new_label_direction: str) -> None:
|
||||||
|
"""Set the text direction of the label"""
|
||||||
|
if new_label_direction not in self._get_valid_label_directions():
|
||||||
|
raise RuntimeError("Please provide a valid text direction")
|
||||||
|
self._set_label_direction(new_label_direction)
|
||||||
|
|
||||||
|
def _replace_tabs(self, text: str) -> str:
|
||||||
|
return text if text.find("\t") < 0 else self._tab_text.join(text.split("\t"))
|
||||||
571
lib/adafruit_display_text/bitmap_label.py
Normal file
571
lib/adafruit_display_text/bitmap_label.py
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2020 Kevin Matocha
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.bitmap_label`
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Text graphics handling for CircuitPython, including text boxes
|
||||||
|
|
||||||
|
|
||||||
|
* Author(s): Kevin Matocha
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "2.22.3"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Union, Optional, Tuple
|
||||||
|
from fontio import BuiltinFont
|
||||||
|
from adafruit_bitmap_font.bdf import BDF
|
||||||
|
from adafruit_bitmap_font.pcf import PCF
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import displayio
|
||||||
|
|
||||||
|
from adafruit_display_text import LabelBase
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class Label(LabelBase):
|
||||||
|
"""A label displaying a string of text that is stored in a bitmap.
|
||||||
|
Note: This ``bitmap_label.py`` library utilizes a :py:class:`~displayio.Bitmap`
|
||||||
|
to display the text. This method is memory-conserving relative to ``label.py``.
|
||||||
|
|
||||||
|
For further reduction in memory usage, set ``save_text=False`` (text string will not
|
||||||
|
be stored and ``line_spacing`` and ``font`` are immutable with ``save_text``
|
||||||
|
set to ``False``).
|
||||||
|
|
||||||
|
The origin point set by ``x`` and ``y``
|
||||||
|
properties will be the left edge of the bounding box, and in the center of a M
|
||||||
|
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
||||||
|
it will try to have it be center-left as close as possible.
|
||||||
|
|
||||||
|
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||||
|
Must include a capital M for measuring character size.
|
||||||
|
:type font: ~BuiltinFont, ~BDF, or ~PCF
|
||||||
|
:param str text: Text to display
|
||||||
|
:param int color: Color of all text in RGB hex
|
||||||
|
:param int background_color: Color of the background, use `None` for transparent
|
||||||
|
:param float line_spacing: Line spacing of text to display
|
||||||
|
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||||
|
surround text. When set to 'True' Padding parameters will be ignored.
|
||||||
|
:param int padding_top: Additional pixels added to background bounding box at top
|
||||||
|
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
||||||
|
:param int padding_left: Additional pixels added to background bounding box at left
|
||||||
|
:param int padding_right: Additional pixels added to background bounding box at right
|
||||||
|
:param (float,float) anchor_point: Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||||
|
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple
|
||||||
|
containing x,y pixel coordinates.
|
||||||
|
:param int scale: Integer value of the pixel scaling
|
||||||
|
:param bool save_text: Set True to save the text string as a constant in the
|
||||||
|
label structure. Set False to reduce memory use.
|
||||||
|
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||||
|
This is helpful when two or more labels need to be aligned to the same baseline
|
||||||
|
:param (int,str) tab_replacement: tuple with tab character replace information. When
|
||||||
|
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||||
|
tab character
|
||||||
|
:param str label_direction: string defining the label text orientation. There are 5
|
||||||
|
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
||||||
|
``UPD``-Upside Down ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``"""
|
||||||
|
|
||||||
|
# This maps label_direction to TileGrid's transpose_xy, flip_x, flip_y
|
||||||
|
_DIR_MAP = {
|
||||||
|
"UPR": (True, True, False),
|
||||||
|
"DWR": (True, False, True),
|
||||||
|
"UPD": (False, True, True),
|
||||||
|
"LTR": (False, False, False),
|
||||||
|
"RTL": (False, False, False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, font: Union[BuiltinFont, BDF, PCF], save_text: bool = True, **kwargs
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self._bitmap = None
|
||||||
|
self._tilegrid = None
|
||||||
|
self._prev_label_direction = None
|
||||||
|
|
||||||
|
super().__init__(font, **kwargs)
|
||||||
|
|
||||||
|
self._save_text = save_text
|
||||||
|
self._text = self._replace_tabs(self._text)
|
||||||
|
|
||||||
|
# call the text updater with all the arguments.
|
||||||
|
self._reset_text(
|
||||||
|
font=font,
|
||||||
|
text=self._text,
|
||||||
|
line_spacing=self._line_spacing,
|
||||||
|
scale=self.scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _reset_text(
|
||||||
|
self,
|
||||||
|
font: Optional[Union[BuiltinFont, BDF, PCF]] = None,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
line_spacing: Optional[float] = None,
|
||||||
|
scale: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
|
||||||
|
|
||||||
|
# Store all the instance variables
|
||||||
|
if font is not None:
|
||||||
|
self._font = font
|
||||||
|
if line_spacing is not None:
|
||||||
|
self._line_spacing = line_spacing
|
||||||
|
|
||||||
|
# if text is not provided as a parameter (text is None), use the previous value.
|
||||||
|
if (text is None) and self._save_text:
|
||||||
|
text = self._text
|
||||||
|
|
||||||
|
if self._save_text: # text string will be saved
|
||||||
|
self._text = self._replace_tabs(text)
|
||||||
|
else:
|
||||||
|
self._text = None # save a None value since text string is not saved
|
||||||
|
|
||||||
|
# Check for empty string
|
||||||
|
if (text == "") or (
|
||||||
|
text is None
|
||||||
|
): # If empty string, just create a zero-sized bounding box and that's it.
|
||||||
|
|
||||||
|
self._bounding_box = (
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0, # zero width with text == ""
|
||||||
|
0, # zero height with text == ""
|
||||||
|
)
|
||||||
|
# Clear out any items in the self._local_group Group, in case this is an
|
||||||
|
# update to the bitmap_label
|
||||||
|
for _ in self._local_group:
|
||||||
|
self._local_group.pop(0)
|
||||||
|
|
||||||
|
# Free the bitmap and tilegrid since they are removed
|
||||||
|
self._bitmap = None
|
||||||
|
self._tilegrid = None
|
||||||
|
|
||||||
|
else: # The text string is not empty, so create the Bitmap and TileGrid and
|
||||||
|
# append to the self Group
|
||||||
|
|
||||||
|
# Calculate the text bounding box
|
||||||
|
|
||||||
|
# Calculate both "tight" and "loose" bounding box dimensions to match label for
|
||||||
|
# anchor_position calculations
|
||||||
|
(
|
||||||
|
box_x,
|
||||||
|
tight_box_y,
|
||||||
|
x_offset,
|
||||||
|
tight_y_offset,
|
||||||
|
loose_box_y,
|
||||||
|
loose_y_offset,
|
||||||
|
) = self._text_bounding_box(
|
||||||
|
text,
|
||||||
|
self._font,
|
||||||
|
) # calculate the box size for a tight and loose backgrounds
|
||||||
|
|
||||||
|
if self._background_tight:
|
||||||
|
box_y = tight_box_y
|
||||||
|
y_offset = tight_y_offset
|
||||||
|
|
||||||
|
else: # calculate the box size for a loose background
|
||||||
|
box_y = loose_box_y
|
||||||
|
y_offset = loose_y_offset
|
||||||
|
|
||||||
|
# Calculate the background size including padding
|
||||||
|
box_x = box_x + self._padding_left + self._padding_right
|
||||||
|
box_y = box_y + self._padding_top + self._padding_bottom
|
||||||
|
|
||||||
|
# Create the Bitmap unless it can be reused
|
||||||
|
new_bitmap = None
|
||||||
|
if (
|
||||||
|
self._bitmap is None
|
||||||
|
or self._bitmap.width != box_x
|
||||||
|
or self._bitmap.height != box_y
|
||||||
|
):
|
||||||
|
new_bitmap = displayio.Bitmap(box_x, box_y, len(self._palette))
|
||||||
|
self._bitmap = new_bitmap
|
||||||
|
else:
|
||||||
|
self._bitmap.fill(0)
|
||||||
|
|
||||||
|
# Place the text into the Bitmap
|
||||||
|
self._place_text(
|
||||||
|
self._bitmap,
|
||||||
|
text if self._label_direction != "RTL" else "".join(reversed(text)),
|
||||||
|
self._font,
|
||||||
|
self._padding_left - x_offset,
|
||||||
|
self._padding_top + y_offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._base_alignment:
|
||||||
|
label_position_yoffset = 0
|
||||||
|
else:
|
||||||
|
label_position_yoffset = self._ascent // 2
|
||||||
|
|
||||||
|
# Create the TileGrid if not created bitmap unchanged
|
||||||
|
if self._tilegrid is None or new_bitmap:
|
||||||
|
self._tilegrid = displayio.TileGrid(
|
||||||
|
self._bitmap,
|
||||||
|
pixel_shader=self._palette,
|
||||||
|
width=1,
|
||||||
|
height=1,
|
||||||
|
tile_width=box_x,
|
||||||
|
tile_height=box_y,
|
||||||
|
default_tile=0,
|
||||||
|
x=-self._padding_left + x_offset,
|
||||||
|
y=label_position_yoffset - y_offset - self._padding_top,
|
||||||
|
)
|
||||||
|
# Clear out any items in the local_group Group, in case this is an update to
|
||||||
|
# the bitmap_label
|
||||||
|
for _ in self._local_group:
|
||||||
|
self._local_group.pop(0)
|
||||||
|
self._local_group.append(
|
||||||
|
self._tilegrid
|
||||||
|
) # add the bitmap's tilegrid to the group
|
||||||
|
|
||||||
|
# Set TileGrid properties based on label_direction
|
||||||
|
if self._label_direction != self._prev_label_direction:
|
||||||
|
tg1 = self._tilegrid
|
||||||
|
tg1.transpose_xy, tg1.flip_x, tg1.flip_y = self._DIR_MAP[
|
||||||
|
self._label_direction
|
||||||
|
]
|
||||||
|
|
||||||
|
# Update bounding_box values. Note: To be consistent with label.py,
|
||||||
|
# this is the bounding box for the text only, not including the background.
|
||||||
|
if self._label_direction in ("UPR", "DWR"):
|
||||||
|
self._bounding_box = (
|
||||||
|
self._tilegrid.x,
|
||||||
|
self._tilegrid.y,
|
||||||
|
tight_box_y,
|
||||||
|
box_x,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._bounding_box = (
|
||||||
|
self._tilegrid.x,
|
||||||
|
self._tilegrid.y,
|
||||||
|
box_x,
|
||||||
|
tight_box_y,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
scale is not None
|
||||||
|
): # Scale will be defined in local_group (Note: self should have scale=1)
|
||||||
|
self.scale = scale # call the setter
|
||||||
|
|
||||||
|
# set the anchored_position with setter after bitmap is created, sets the
|
||||||
|
# x,y positions of the label
|
||||||
|
self.anchored_position = self._anchored_position
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _line_spacing_ypixels(
|
||||||
|
font: Union[BuiltinFont, BDF, PCF], line_spacing: float
|
||||||
|
) -> int:
|
||||||
|
# Note: Scaling is provided at the Group level
|
||||||
|
return_value = int(line_spacing * font.get_bounding_box()[1])
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
def _text_bounding_box(
|
||||||
|
self, text: str, font: Union[BuiltinFont, BDF, PCF]
|
||||||
|
) -> Tuple[int, int, int, int, int, int]:
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
ascender_max, descender_max = self._ascent, self._descent
|
||||||
|
|
||||||
|
lines = 1
|
||||||
|
|
||||||
|
xposition = (
|
||||||
|
x_start
|
||||||
|
) = yposition = y_start = 0 # starting x and y position (left margin)
|
||||||
|
|
||||||
|
left = None
|
||||||
|
right = x_start
|
||||||
|
top = bottom = y_start
|
||||||
|
|
||||||
|
y_offset_tight = self._ascent // 2
|
||||||
|
|
||||||
|
newline = False
|
||||||
|
line_spacing = self._line_spacing
|
||||||
|
|
||||||
|
for char in text:
|
||||||
|
|
||||||
|
if char == "\n": # newline
|
||||||
|
newline = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
my_glyph = font.get_glyph(ord(char))
|
||||||
|
|
||||||
|
if my_glyph is None: # Error checking: no glyph found
|
||||||
|
print("Glyph not found: {}".format(repr(char)))
|
||||||
|
else:
|
||||||
|
if newline:
|
||||||
|
newline = False
|
||||||
|
xposition = x_start # reset to left column
|
||||||
|
yposition = yposition + self._line_spacing_ypixels(
|
||||||
|
font, line_spacing
|
||||||
|
) # Add a newline
|
||||||
|
lines += 1
|
||||||
|
if xposition == x_start:
|
||||||
|
if left is None:
|
||||||
|
left = my_glyph.dx
|
||||||
|
else:
|
||||||
|
left = min(left, my_glyph.dx)
|
||||||
|
xright = xposition + my_glyph.width + my_glyph.dx
|
||||||
|
xposition += my_glyph.shift_x
|
||||||
|
|
||||||
|
right = max(right, xposition, xright)
|
||||||
|
|
||||||
|
if yposition == y_start: # first line, find the Ascender height
|
||||||
|
top = min(top, -my_glyph.height - my_glyph.dy + y_offset_tight)
|
||||||
|
bottom = max(bottom, yposition - my_glyph.dy + y_offset_tight)
|
||||||
|
|
||||||
|
if left is None:
|
||||||
|
left = 0
|
||||||
|
|
||||||
|
final_box_width = right - left
|
||||||
|
|
||||||
|
final_box_height_tight = bottom - top
|
||||||
|
final_y_offset_tight = -top + y_offset_tight
|
||||||
|
|
||||||
|
final_box_height_loose = (lines - 1) * self._line_spacing_ypixels(
|
||||||
|
font, line_spacing
|
||||||
|
) + (ascender_max + descender_max)
|
||||||
|
final_y_offset_loose = ascender_max
|
||||||
|
|
||||||
|
# return (final_box_width, final_box_height, left, final_y_offset)
|
||||||
|
|
||||||
|
return (
|
||||||
|
final_box_width,
|
||||||
|
final_box_height_tight,
|
||||||
|
left,
|
||||||
|
final_y_offset_tight,
|
||||||
|
final_box_height_loose,
|
||||||
|
final_y_offset_loose,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _place_text(
|
||||||
|
self,
|
||||||
|
bitmap: displayio.Bitmap,
|
||||||
|
text: str,
|
||||||
|
font: Union[BuiltinFont, BDF, PCF],
|
||||||
|
xposition: int,
|
||||||
|
yposition: int,
|
||||||
|
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
||||||
|
# when copying glyph bitmaps (this is important for slanted text
|
||||||
|
# where rectangular glyph boxes overlap)
|
||||||
|
) -> Tuple[int, int, int, int]:
|
||||||
|
# pylint: disable=too-many-arguments, too-many-locals
|
||||||
|
|
||||||
|
# placeText - Writes text into a bitmap at the specified location.
|
||||||
|
#
|
||||||
|
# Note: scale is pushed up to Group level
|
||||||
|
|
||||||
|
x_start = xposition # starting x position (left margin)
|
||||||
|
y_start = yposition
|
||||||
|
|
||||||
|
left = None
|
||||||
|
right = x_start
|
||||||
|
top = bottom = y_start
|
||||||
|
line_spacing = self._line_spacing
|
||||||
|
|
||||||
|
for char in text:
|
||||||
|
|
||||||
|
if char == "\n": # newline
|
||||||
|
xposition = x_start # reset to left column
|
||||||
|
yposition = yposition + self._line_spacing_ypixels(
|
||||||
|
font, line_spacing
|
||||||
|
) # Add a newline
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
my_glyph = font.get_glyph(ord(char))
|
||||||
|
|
||||||
|
if my_glyph is None: # Error checking: no glyph found
|
||||||
|
print("Glyph not found: {}".format(repr(char)))
|
||||||
|
else:
|
||||||
|
if xposition == x_start:
|
||||||
|
if left is None:
|
||||||
|
left = my_glyph.dx
|
||||||
|
else:
|
||||||
|
left = min(left, my_glyph.dx)
|
||||||
|
|
||||||
|
right = max(
|
||||||
|
right,
|
||||||
|
xposition + my_glyph.shift_x,
|
||||||
|
xposition + my_glyph.width + my_glyph.dx,
|
||||||
|
)
|
||||||
|
if yposition == y_start: # first line, find the Ascender height
|
||||||
|
top = min(top, -my_glyph.height - my_glyph.dy)
|
||||||
|
bottom = max(bottom, yposition - my_glyph.dy)
|
||||||
|
|
||||||
|
glyph_offset_x = (
|
||||||
|
my_glyph.tile_index * my_glyph.width
|
||||||
|
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
|
||||||
|
# for BDF loaded fonts, this should equal 0
|
||||||
|
|
||||||
|
y_blit_target = yposition - my_glyph.height - my_glyph.dy
|
||||||
|
|
||||||
|
# Clip glyph y-direction if outside the font ascent/descent metrics.
|
||||||
|
# Note: bitmap.blit will automatically clip the bottom of the glyph.
|
||||||
|
y_clip = 0
|
||||||
|
if y_blit_target < 0:
|
||||||
|
y_clip = -y_blit_target # clip this amount from top of bitmap
|
||||||
|
y_blit_target = 0 # draw the clipped bitmap at y=0
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format(
|
||||||
|
char
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (y_blit_target + my_glyph.height) > bitmap.height:
|
||||||
|
print(
|
||||||
|
'Warning: Glyph clipped, exceeds descent property: "{}"'.format(
|
||||||
|
char
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._blit(
|
||||||
|
bitmap,
|
||||||
|
xposition + my_glyph.dx,
|
||||||
|
y_blit_target,
|
||||||
|
my_glyph.bitmap,
|
||||||
|
x_1=glyph_offset_x,
|
||||||
|
y_1=y_clip,
|
||||||
|
x_2=glyph_offset_x + my_glyph.width,
|
||||||
|
y_2=my_glyph.height,
|
||||||
|
skip_index=skip_index, # do not copy over any 0 background pixels
|
||||||
|
)
|
||||||
|
|
||||||
|
xposition = xposition + my_glyph.shift_x
|
||||||
|
|
||||||
|
# bounding_box
|
||||||
|
return left, top, right - left, bottom - top
|
||||||
|
|
||||||
|
def _blit(
|
||||||
|
self,
|
||||||
|
bitmap: displayio.Bitmap, # target bitmap
|
||||||
|
x: int, # target x upper left corner
|
||||||
|
y: int, # target y upper left corner
|
||||||
|
source_bitmap: displayio.Bitmap, # source bitmap
|
||||||
|
x_1: int = 0, # source x start
|
||||||
|
y_1: int = 0, # source y start
|
||||||
|
x_2: int = None, # source x end
|
||||||
|
y_2: int = None, # source y end
|
||||||
|
skip_index: int = None, # palette index that will not be copied
|
||||||
|
# (for example: the background color of a glyph)
|
||||||
|
) -> None:
|
||||||
|
# pylint: disable=no-self-use, too-many-arguments
|
||||||
|
|
||||||
|
if hasattr(bitmap, "blit"): # if bitmap has a built-in blit function, call it
|
||||||
|
# this function should perform its own input checks
|
||||||
|
bitmap.blit(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
source_bitmap,
|
||||||
|
x1=x_1,
|
||||||
|
y1=y_1,
|
||||||
|
x2=x_2,
|
||||||
|
y2=y_2,
|
||||||
|
skip_index=skip_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # perform pixel by pixel copy of the bitmap
|
||||||
|
|
||||||
|
# Perform input checks
|
||||||
|
|
||||||
|
if x_2 is None:
|
||||||
|
x_2 = source_bitmap.width
|
||||||
|
if y_2 is None:
|
||||||
|
y_2 = source_bitmap.height
|
||||||
|
|
||||||
|
# Rearrange so that x_1 < x_2 and y1 < y2
|
||||||
|
if x_1 > x_2:
|
||||||
|
x_1, x_2 = x_2, x_1
|
||||||
|
if y_1 > y_2:
|
||||||
|
y_1, y_2 = y_2, y_1
|
||||||
|
|
||||||
|
# Ensure that x2 and y2 are within source bitmap size
|
||||||
|
x_2 = min(x_2, source_bitmap.width)
|
||||||
|
y_2 = min(y_2, source_bitmap.height)
|
||||||
|
|
||||||
|
for y_count in range(y_2 - y_1):
|
||||||
|
for x_count in range(x_2 - x_1):
|
||||||
|
x_placement = x + x_count
|
||||||
|
y_placement = y + y_count
|
||||||
|
|
||||||
|
if (bitmap.width > x_placement >= 0) and (
|
||||||
|
bitmap.height > y_placement >= 0
|
||||||
|
): # ensure placement is within target bitmap
|
||||||
|
|
||||||
|
# get the palette index from the source bitmap
|
||||||
|
this_pixel_color = source_bitmap[
|
||||||
|
y_1
|
||||||
|
+ (
|
||||||
|
y_count * source_bitmap.width
|
||||||
|
) # Direct index into a bitmap array is speedier than [x,y] tuple
|
||||||
|
+ x_1
|
||||||
|
+ x_count
|
||||||
|
]
|
||||||
|
|
||||||
|
if (skip_index is None) or (this_pixel_color != skip_index):
|
||||||
|
bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
|
||||||
|
y_placement * bitmap.width + x_placement
|
||||||
|
] = this_pixel_color
|
||||||
|
elif y_placement > bitmap.height:
|
||||||
|
break
|
||||||
|
|
||||||
|
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
if self._save_text:
|
||||||
|
self._reset_text(line_spacing=new_line_spacing, scale=self.scale)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("line_spacing is immutable when save_text is False")
|
||||||
|
|
||||||
|
def _set_font(self, new_font: Union[BuiltinFont, BDF, PCF]) -> None:
|
||||||
|
self._font = new_font
|
||||||
|
if self._save_text:
|
||||||
|
self._reset_text(font=new_font, scale=self.scale)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("font is immutable when save_text is False")
|
||||||
|
|
||||||
|
def _set_text(self, new_text: str, scale: int) -> None:
|
||||||
|
self._reset_text(text=self._replace_tabs(new_text), scale=self.scale)
|
||||||
|
|
||||||
|
def _set_background_color(self, new_color: Optional[int]):
|
||||||
|
self._background_color = new_color
|
||||||
|
if new_color is not None:
|
||||||
|
self._palette[0] = new_color
|
||||||
|
self._palette.make_opaque(0)
|
||||||
|
else:
|
||||||
|
self._palette[0] = 0
|
||||||
|
self._palette.make_transparent(0)
|
||||||
|
|
||||||
|
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||||
|
self._prev_label_direction = self._label_direction
|
||||||
|
self._label_direction = new_label_direction
|
||||||
|
self._reset_text(text=str(self._text)) # Force a recalculation
|
||||||
|
|
||||||
|
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||||
|
return "LTR", "RTL", "UPD", "UPR", "DWR"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bitmap(self) -> displayio.Bitmap:
|
||||||
|
"""
|
||||||
|
The Bitmap object that the text and background are drawn into.
|
||||||
|
|
||||||
|
:rtype: displayio.Bitmap
|
||||||
|
"""
|
||||||
|
return self._bitmap
|
||||||
427
lib/adafruit_display_text/label.py
Normal file
427
lib/adafruit_display_text/label.py
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.label`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Displays text labels using CircuitPython's displayio.
|
||||||
|
|
||||||
|
* Author(s): Scott Shawcroft
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "2.22.3"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Union, Optional, Tuple
|
||||||
|
from fontio import BuiltinFont
|
||||||
|
from adafruit_bitmap_font.bdf import BDF
|
||||||
|
from adafruit_bitmap_font.pcf import PCF
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from displayio import Bitmap, Palette, TileGrid
|
||||||
|
|
||||||
|
from adafruit_display_text import LabelBase
|
||||||
|
|
||||||
|
|
||||||
|
class Label(LabelBase):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
|
"""A label displaying a string of text. The origin point set by ``x`` and ``y``
|
||||||
|
properties will be the left edge of the bounding box, and in the center of a M
|
||||||
|
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
||||||
|
it will try to have it be center-left as close as possible.
|
||||||
|
|
||||||
|
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||||
|
Must include a capital M for measuring character size.
|
||||||
|
:type font: ~BuiltinFont, ~BDF, or ~PCF
|
||||||
|
:param str text: Text to display
|
||||||
|
:param int color: Color of all text in RGB hex
|
||||||
|
:param int background_color: Color of the background, use `None` for transparent
|
||||||
|
:param float line_spacing: Line spacing of text to display
|
||||||
|
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||||
|
surround text. When set to 'True' Padding parameters will be ignored.
|
||||||
|
:param int padding_top: Additional pixels added to background bounding box at top.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param int padding_bottom: Additional pixels added to background bounding box at bottom.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param int padding_left: Additional pixels added to background bounding box at left.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param int padding_right: Additional pixels added to background bounding box at right.
|
||||||
|
This parameter could be negative indicating additional pixels subtracted from the
|
||||||
|
background bounding box.
|
||||||
|
:param (float,float) anchor_point: Point that anchored_position moves relative to.
|
||||||
|
Tuple with decimal percentage of width and height.
|
||||||
|
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||||
|
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple
|
||||||
|
containing x,y pixel coordinates.
|
||||||
|
:param int scale: Integer value of the pixel scaling
|
||||||
|
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||||
|
This is helpful when two or more labels need to be aligned to the same baseline
|
||||||
|
:param (int,str) tab_replacement: tuple with tab character replace information. When
|
||||||
|
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||||
|
tab character
|
||||||
|
:param str label_direction: string defining the label text orientation. There are 5
|
||||||
|
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
||||||
|
``TTB``-Top-To-Bottom ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``"""
|
||||||
|
|
||||||
|
def __init__(self, font: Union[BuiltinFont, BDF, PCF], **kwargs) -> None:
|
||||||
|
self._background_palette = Palette(1)
|
||||||
|
self._added_background_tilegrid = False
|
||||||
|
|
||||||
|
super().__init__(font, **kwargs)
|
||||||
|
|
||||||
|
text = self._replace_tabs(self._text)
|
||||||
|
|
||||||
|
self._width = len(text)
|
||||||
|
self._height = self._font.get_bounding_box()[1]
|
||||||
|
|
||||||
|
# Create the two-color text palette
|
||||||
|
self._palette[0] = 0
|
||||||
|
self._palette.make_transparent(0)
|
||||||
|
|
||||||
|
if text is not None:
|
||||||
|
self._reset_text(str(text))
|
||||||
|
|
||||||
|
def _create_background_box(self, lines: int, y_offset: int) -> TileGrid:
|
||||||
|
"""Private Class function to create a background_box
|
||||||
|
:param lines: int number of lines
|
||||||
|
:param y_offset: int y pixel bottom coordinate for the background_box"""
|
||||||
|
|
||||||
|
left = self._bounding_box[0]
|
||||||
|
if self._background_tight: # draw a tight bounding box
|
||||||
|
box_width = self._bounding_box[2]
|
||||||
|
box_height = self._bounding_box[3]
|
||||||
|
x_box_offset = 0
|
||||||
|
y_box_offset = self._bounding_box[1]
|
||||||
|
|
||||||
|
else: # draw a "loose" bounding box to include any ascenders/descenders.
|
||||||
|
ascent, descent = self._ascent, self._descent
|
||||||
|
|
||||||
|
if self._label_direction in ("UPR", "DWR", "TTB"):
|
||||||
|
box_height = (
|
||||||
|
self._bounding_box[3] + self._padding_top + self._padding_bottom
|
||||||
|
)
|
||||||
|
x_box_offset = -self._padding_bottom
|
||||||
|
box_width = (
|
||||||
|
(ascent + descent)
|
||||||
|
+ int((lines - 1) * self._width * self._line_spacing)
|
||||||
|
+ self._padding_left
|
||||||
|
+ self._padding_right
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
box_width = (
|
||||||
|
self._bounding_box[2] + self._padding_left + self._padding_right
|
||||||
|
)
|
||||||
|
x_box_offset = -self._padding_left
|
||||||
|
box_height = (
|
||||||
|
(ascent + descent)
|
||||||
|
+ int((lines - 1) * self._height * self._line_spacing)
|
||||||
|
+ self._padding_top
|
||||||
|
+ self._padding_bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._base_alignment:
|
||||||
|
y_box_offset = -ascent - self._padding_top
|
||||||
|
else:
|
||||||
|
y_box_offset = -ascent + y_offset - self._padding_top
|
||||||
|
|
||||||
|
box_width = max(0, box_width) # remove any negative values
|
||||||
|
box_height = max(0, box_height) # remove any negative values
|
||||||
|
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
movx = left + x_box_offset
|
||||||
|
movy = -box_height - x_box_offset
|
||||||
|
elif self._label_direction == "DWR":
|
||||||
|
movx = left + x_box_offset
|
||||||
|
movy = x_box_offset
|
||||||
|
elif self._label_direction == "TTB":
|
||||||
|
movx = left + x_box_offset
|
||||||
|
movy = x_box_offset
|
||||||
|
else:
|
||||||
|
movx = left + x_box_offset
|
||||||
|
movy = y_box_offset
|
||||||
|
|
||||||
|
background_bitmap = Bitmap(box_width, box_height, 1)
|
||||||
|
tile_grid = TileGrid(
|
||||||
|
background_bitmap,
|
||||||
|
pixel_shader=self._background_palette,
|
||||||
|
x=movx,
|
||||||
|
y=movy,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tile_grid
|
||||||
|
|
||||||
|
def _set_background_color(self, new_color: Optional[int]) -> None:
|
||||||
|
"""Private class function that allows updating the font box background color
|
||||||
|
|
||||||
|
:param int new_color: Color as an RGB hex number, setting to None makes it transparent
|
||||||
|
"""
|
||||||
|
|
||||||
|
if new_color is None:
|
||||||
|
self._background_palette.make_transparent(0)
|
||||||
|
if self._added_background_tilegrid:
|
||||||
|
self._local_group.pop(0)
|
||||||
|
self._added_background_tilegrid = False
|
||||||
|
else:
|
||||||
|
self._background_palette.make_opaque(0)
|
||||||
|
self._background_palette[0] = new_color
|
||||||
|
self._background_color = new_color
|
||||||
|
|
||||||
|
lines = self._text.rstrip("\n").count("\n") + 1
|
||||||
|
y_offset = self._ascent // 2
|
||||||
|
|
||||||
|
if self._bounding_box is None:
|
||||||
|
# Still in initialization
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._added_background_tilegrid: # no bitmap is in the self Group
|
||||||
|
# add bitmap if text is present and bitmap sizes > 0 pixels
|
||||||
|
if (
|
||||||
|
(len(self._text) > 0)
|
||||||
|
and (
|
||||||
|
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self._local_group.insert(
|
||||||
|
0, self._create_background_box(lines, y_offset)
|
||||||
|
)
|
||||||
|
self._added_background_tilegrid = True
|
||||||
|
|
||||||
|
else: # a bitmap is present in the self Group
|
||||||
|
# update bitmap if text is present and bitmap sizes > 0 pixels
|
||||||
|
if (
|
||||||
|
(len(self._text) > 0)
|
||||||
|
and (
|
||||||
|
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self._local_group[0] = self._create_background_box(
|
||||||
|
lines, self._y_offset
|
||||||
|
)
|
||||||
|
else: # delete the existing bitmap
|
||||||
|
self._local_group.pop(0)
|
||||||
|
self._added_background_tilegrid = False
|
||||||
|
|
||||||
|
def _update_text(self, new_text: str) -> None:
|
||||||
|
# pylint: disable=too-many-branches,too-many-statements
|
||||||
|
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
if self._added_background_tilegrid:
|
||||||
|
i = 1
|
||||||
|
else:
|
||||||
|
i = 0
|
||||||
|
tilegrid_count = i
|
||||||
|
if self._base_alignment:
|
||||||
|
self._y_offset = 0
|
||||||
|
else:
|
||||||
|
self._y_offset = self._ascent // 2
|
||||||
|
|
||||||
|
if self._label_direction == "RTL":
|
||||||
|
left = top = bottom = 0
|
||||||
|
right = None
|
||||||
|
elif self._label_direction == "LTR":
|
||||||
|
right = top = bottom = 0
|
||||||
|
left = None
|
||||||
|
else:
|
||||||
|
top = right = left = 0
|
||||||
|
bottom = 0
|
||||||
|
|
||||||
|
for character in new_text:
|
||||||
|
if character == "\n":
|
||||||
|
y += int(self._height * self._line_spacing)
|
||||||
|
x = 0
|
||||||
|
continue
|
||||||
|
glyph = self._font.get_glyph(ord(character))
|
||||||
|
if not glyph:
|
||||||
|
continue
|
||||||
|
|
||||||
|
position_x, position_y = 0, 0
|
||||||
|
|
||||||
|
if self._label_direction in ("LTR", "RTL"):
|
||||||
|
bottom = max(bottom, y - glyph.dy + self._y_offset)
|
||||||
|
if y == 0: # first line, find the Ascender height
|
||||||
|
top = min(top, -glyph.height - glyph.dy + self._y_offset)
|
||||||
|
position_y = y - glyph.height - glyph.dy + self._y_offset
|
||||||
|
|
||||||
|
if self._label_direction == "LTR":
|
||||||
|
right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx)
|
||||||
|
if x == 0:
|
||||||
|
if left is None:
|
||||||
|
left = glyph.dx
|
||||||
|
else:
|
||||||
|
left = min(left, glyph.dx)
|
||||||
|
position_x = x + glyph.dx
|
||||||
|
else:
|
||||||
|
left = max(
|
||||||
|
left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx
|
||||||
|
)
|
||||||
|
if x == 0:
|
||||||
|
if right is None:
|
||||||
|
right = glyph.dx
|
||||||
|
else:
|
||||||
|
right = max(right, glyph.dx)
|
||||||
|
position_x = x - glyph.width
|
||||||
|
|
||||||
|
elif self._label_direction == "TTB":
|
||||||
|
if x == 0:
|
||||||
|
if left is None:
|
||||||
|
left = glyph.dx
|
||||||
|
else:
|
||||||
|
left = min(left, glyph.dx)
|
||||||
|
if y == 0:
|
||||||
|
top = min(top, -glyph.dy)
|
||||||
|
|
||||||
|
bottom = max(bottom, y + glyph.height, y + glyph.height + glyph.dy)
|
||||||
|
right = max(
|
||||||
|
right, x + glyph.width + glyph.dx, x + glyph.shift_x + glyph.dx
|
||||||
|
)
|
||||||
|
position_y = y + glyph.dy
|
||||||
|
position_x = x - glyph.width // 2 + self._y_offset
|
||||||
|
|
||||||
|
elif self._label_direction == "UPR":
|
||||||
|
if x == 0:
|
||||||
|
if bottom is None:
|
||||||
|
bottom = -glyph.dx
|
||||||
|
|
||||||
|
if y == 0: # first line, find the Ascender height
|
||||||
|
bottom = min(bottom, -glyph.dy)
|
||||||
|
left = min(left, x - glyph.height + self._y_offset)
|
||||||
|
top = min(top, y - glyph.width - glyph.dx, y - glyph.shift_x)
|
||||||
|
right = max(right, x + glyph.height, x + glyph.height - glyph.dy)
|
||||||
|
position_y = y - glyph.width - glyph.dx
|
||||||
|
position_x = x - glyph.height - glyph.dy + self._y_offset
|
||||||
|
|
||||||
|
elif self._label_direction == "DWR":
|
||||||
|
if y == 0:
|
||||||
|
if top is None:
|
||||||
|
top = -glyph.dx
|
||||||
|
top = min(top, -glyph.dx)
|
||||||
|
if x == 0:
|
||||||
|
left = min(left, -glyph.dy)
|
||||||
|
left = min(left, x, x - glyph.dy - self._y_offset)
|
||||||
|
bottom = max(bottom, y + glyph.width + glyph.dx, y + glyph.shift_x)
|
||||||
|
right = max(right, x + glyph.height)
|
||||||
|
position_y = y + glyph.dx
|
||||||
|
position_x = x + glyph.dy - self._y_offset
|
||||||
|
|
||||||
|
if glyph.width > 0 and glyph.height > 0:
|
||||||
|
face = TileGrid(
|
||||||
|
glyph.bitmap,
|
||||||
|
pixel_shader=self._palette,
|
||||||
|
default_tile=glyph.tile_index,
|
||||||
|
tile_width=glyph.width,
|
||||||
|
tile_height=glyph.height,
|
||||||
|
x=position_x,
|
||||||
|
y=position_y,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
face.transpose_xy = True
|
||||||
|
face.flip_x = True
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
face.transpose_xy = True
|
||||||
|
face.flip_y = True
|
||||||
|
|
||||||
|
if tilegrid_count < len(self._local_group):
|
||||||
|
self._local_group[tilegrid_count] = face
|
||||||
|
else:
|
||||||
|
self._local_group.append(face)
|
||||||
|
tilegrid_count += 1
|
||||||
|
|
||||||
|
if self._label_direction == "RTL":
|
||||||
|
x = x - glyph.shift_x
|
||||||
|
if self._label_direction == "TTB":
|
||||||
|
if glyph.height < 2:
|
||||||
|
y = y + glyph.shift_x
|
||||||
|
else:
|
||||||
|
y = y + glyph.height + 1
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
y = y - glyph.shift_x
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
y = y + glyph.shift_x
|
||||||
|
if self._label_direction == "LTR":
|
||||||
|
x = x + glyph.shift_x
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if self._label_direction == "LTR" and left is None:
|
||||||
|
left = 0
|
||||||
|
if self._label_direction == "RTL" and right is None:
|
||||||
|
right = 0
|
||||||
|
if self._label_direction == "TTB" and top is None:
|
||||||
|
top = 0
|
||||||
|
|
||||||
|
while len(self._local_group) > tilegrid_count: # i:
|
||||||
|
self._local_group.pop()
|
||||||
|
|
||||||
|
if self._label_direction == "RTL":
|
||||||
|
# pylint: disable=invalid-unary-operand-type
|
||||||
|
# type-checkers think left can be None
|
||||||
|
self._bounding_box = (-left, top, left - right, bottom - top)
|
||||||
|
if self._label_direction == "TTB":
|
||||||
|
self._bounding_box = (left, top, right - left, bottom - top)
|
||||||
|
if self._label_direction == "UPR":
|
||||||
|
self._bounding_box = (left, top, right, bottom - top)
|
||||||
|
if self._label_direction == "DWR":
|
||||||
|
self._bounding_box = (left, top, right, bottom - top)
|
||||||
|
if self._label_direction == "LTR":
|
||||||
|
self._bounding_box = (left, top, right - left, bottom - top)
|
||||||
|
|
||||||
|
self._text = new_text
|
||||||
|
|
||||||
|
if self._background_color is not None:
|
||||||
|
self._set_background_color(self._background_color)
|
||||||
|
|
||||||
|
def _reset_text(self, new_text: str) -> None:
|
||||||
|
current_anchored_position = self.anchored_position
|
||||||
|
self._update_text(str(self._replace_tabs(new_text)))
|
||||||
|
self.anchored_position = current_anchored_position
|
||||||
|
|
||||||
|
def _set_font(self, new_font: Union[BuiltinFont, BDF, PCF]) -> None:
|
||||||
|
old_text = self._text
|
||||||
|
current_anchored_position = self.anchored_position
|
||||||
|
self._text = ""
|
||||||
|
self._font = new_font
|
||||||
|
self._height = self._font.get_bounding_box()[1]
|
||||||
|
self._update_text(str(old_text))
|
||||||
|
self.anchored_position = current_anchored_position
|
||||||
|
|
||||||
|
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||||
|
self._line_spacing = new_line_spacing
|
||||||
|
self.text = self._text # redraw the box
|
||||||
|
|
||||||
|
def _set_text(self, new_text: str, scale: int) -> None:
|
||||||
|
self._reset_text(new_text)
|
||||||
|
|
||||||
|
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||||
|
self._label_direction = new_label_direction
|
||||||
|
self._update_text(str(self._text))
|
||||||
|
|
||||||
|
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||||
|
return "LTR", "RTL", "UPR", "DWR", "TTB"
|
||||||
144
lib/adafruit_display_text/scrolling_label.py
Normal file
144
lib/adafruit_display_text/scrolling_label.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_display_text.scrolling_label`
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Displays text into a fixed-width label that scrolls leftward
|
||||||
|
if the full_text is large enough to need it.
|
||||||
|
|
||||||
|
* Author(s): Tim Cocks
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython firmware for the supported boards:
|
||||||
|
https://circuitpython.org/downloads
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "2.22.3"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||||
|
|
||||||
|
import time
|
||||||
|
from adafruit_display_text import bitmap_label
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollingLabel(bitmap_label.Label):
|
||||||
|
|
||||||
|
"""
|
||||||
|
ScrollingLabel - A fixed-width label that will scroll to the left
|
||||||
|
in order to show the full text if it's larger than the fixed-width.
|
||||||
|
|
||||||
|
:param font: The font to use for the label.
|
||||||
|
:param max_characters: The number of characters that sets the fixed-width. Default is 10.
|
||||||
|
:param text: The full text to show in the label. If this is longer than
|
||||||
|
`max_characters` then the label will scroll to show everything.
|
||||||
|
:param animate_time: The number of seconds in between scrolling animation
|
||||||
|
frames. Default is 0.3 seconds.
|
||||||
|
:param current_index: The index of the first visible character in the label.
|
||||||
|
Default is 0, the first character. Will increase while scrolling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
font,
|
||||||
|
max_characters=10,
|
||||||
|
text="",
|
||||||
|
animate_time=0.3,
|
||||||
|
current_index=0,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__(font, **kwargs)
|
||||||
|
self.animate_time = animate_time
|
||||||
|
self._current_index = current_index
|
||||||
|
self._last_animate_time = -1
|
||||||
|
self.max_characters = max_characters
|
||||||
|
|
||||||
|
if text[-1] != " ":
|
||||||
|
text = "{} ".format(text)
|
||||||
|
self._full_text = text
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self, force=False):
|
||||||
|
"""
|
||||||
|
Attempt to update the display. If `animate_time` has elapsed since
|
||||||
|
previews animation frame then move the characters over by 1 index.
|
||||||
|
Must be called in the main loop of user code.
|
||||||
|
|
||||||
|
:param force: whether to ignore `animation_time` and force the update. Default is False.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
_now = time.monotonic()
|
||||||
|
if force or self._last_animate_time + self.animate_time <= _now:
|
||||||
|
|
||||||
|
if len(self.full_text) <= self.max_characters:
|
||||||
|
self.text = self.full_text
|
||||||
|
self._last_animate_time = _now
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.current_index + self.max_characters <= len(self.full_text):
|
||||||
|
_showing_string = self.full_text[
|
||||||
|
self.current_index : self.current_index + self.max_characters
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
_showing_string_start = self.full_text[self.current_index :]
|
||||||
|
_showing_string_end = "{}".format(
|
||||||
|
self.full_text[
|
||||||
|
: (self.current_index + self.max_characters)
|
||||||
|
% len(self.full_text)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
_showing_string = "{}{}".format(
|
||||||
|
_showing_string_start, _showing_string_end
|
||||||
|
)
|
||||||
|
self.text = _showing_string
|
||||||
|
|
||||||
|
self.current_index += 1
|
||||||
|
self._last_animate_time = _now
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_index(self):
|
||||||
|
"""
|
||||||
|
Index of the first visible character.
|
||||||
|
|
||||||
|
:return int: the current index
|
||||||
|
"""
|
||||||
|
return self._current_index
|
||||||
|
|
||||||
|
@current_index.setter
|
||||||
|
def current_index(self, new_index):
|
||||||
|
if new_index < len(self.full_text):
|
||||||
|
self._current_index = new_index
|
||||||
|
else:
|
||||||
|
self._current_index = new_index % len(self.full_text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_text(self):
|
||||||
|
"""
|
||||||
|
The full text to be shown. If it's longer than `max_characters` then
|
||||||
|
scrolling will occur as needed.
|
||||||
|
|
||||||
|
:return string: The full text of this label.
|
||||||
|
"""
|
||||||
|
return self._full_text
|
||||||
|
|
||||||
|
@full_text.setter
|
||||||
|
def full_text(self, new_text):
|
||||||
|
if new_text[-1] != " ":
|
||||||
|
new_text = "{} ".format(new_text)
|
||||||
|
self._full_text = new_text
|
||||||
|
self.current_index = 0
|
||||||
|
self.update()
|
||||||
BIN
lib/adafruit_displayio_ssd1306.mpy
Normal file
BIN
lib/adafruit_displayio_ssd1306.mpy
Normal file
Binary file not shown.
117
lib/adafruit_displayio_ssd1306.py
Normal file
117
lib/adafruit_displayio_ssd1306.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
`adafruit_displayio_ssd1306`
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
DisplayIO driver for SSD1306 monochrome displays
|
||||||
|
|
||||||
|
|
||||||
|
* Author(s): Scott Shawcroft
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
**Hardware:**
|
||||||
|
|
||||||
|
* `Monochrome 1.3" 128x64 OLED graphic display <https://www.adafruit.com/product/938>`_
|
||||||
|
* `Monochrome 128x32 I2C OLED graphic display <https://www.adafruit.com/product/931>`_
|
||||||
|
* `Monochrome 0.96" 128x64 OLED graphic display <https://www.adafruit.com/product/326>`_
|
||||||
|
* `Monochrome 128x32 SPI OLED graphic display <https://www.adafruit.com/product/661>`_
|
||||||
|
* `Adafruit FeatherWing OLED - 128x32 OLED <https://www.adafruit.com/product/2900>`_
|
||||||
|
|
||||||
|
**Software and Dependencies:**
|
||||||
|
|
||||||
|
* Adafruit CircuitPython (version 5+) firmware for the supported boards:
|
||||||
|
https://github.com/adafruit/circuitpython/releases
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import displayio
|
||||||
|
|
||||||
|
__version__ = "0.0.0-auto.0"
|
||||||
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SSD1306.git"
|
||||||
|
|
||||||
|
# Sequence from page 19 here: https://cdn-shop.adafruit.com/datasheets/UG-2864HSWEG01+user+guide.pdf
|
||||||
|
_INIT_SEQUENCE = (
|
||||||
|
b"\xAE\x00" # DISPLAY_OFF
|
||||||
|
b"\x20\x01\x00" # Set memory addressing to horizontal mode.
|
||||||
|
b"\x81\x01\xcf" # set contrast control
|
||||||
|
b"\xA1\x00" # Column 127 is segment 0
|
||||||
|
b"\xA6\x00" # Normal display
|
||||||
|
b"\xc8\x00" # Normal display
|
||||||
|
b"\xA8\x01\x3f" # Mux ratio is 1/64
|
||||||
|
b"\xd5\x01\x80" # Set divide ratio
|
||||||
|
b"\xd9\x01\xf1" # Set pre-charge period
|
||||||
|
b"\xda\x01\x12" # Set com configuration
|
||||||
|
b"\xdb\x01\x40" # Set vcom configuration
|
||||||
|
b"\x8d\x01\x14" # Enable charge pump
|
||||||
|
b"\xAF\x00" # DISPLAY_ON
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SSD1306(displayio.Display):
|
||||||
|
"""
|
||||||
|
SSD1306 driver
|
||||||
|
|
||||||
|
:param int width: The width of the display
|
||||||
|
:param int height: The height of the display
|
||||||
|
:param int rotation: The rotation of the display in degrees. Default is 0. Must be one of
|
||||||
|
(0, 90, 180, 270)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bus, **kwargs):
|
||||||
|
# Patch the init sequence for 32 pixel high displays.
|
||||||
|
init_sequence = bytearray(_INIT_SEQUENCE)
|
||||||
|
height = kwargs["height"]
|
||||||
|
if "rotation" in kwargs and kwargs["rotation"] % 180 != 0:
|
||||||
|
height = kwargs["width"]
|
||||||
|
init_sequence[16] = height - 1 # patch mux ratio
|
||||||
|
if kwargs["height"] == 32:
|
||||||
|
init_sequence[25] = 0x02 # patch com configuration
|
||||||
|
super().__init__(
|
||||||
|
bus,
|
||||||
|
init_sequence,
|
||||||
|
**kwargs,
|
||||||
|
color_depth=1,
|
||||||
|
grayscale=True,
|
||||||
|
pixels_in_byte_share_row=False,
|
||||||
|
set_column_command=0x21,
|
||||||
|
set_row_command=0x22,
|
||||||
|
data_as_commands=True,
|
||||||
|
brightness_command=0x81,
|
||||||
|
single_byte_bounds=True,
|
||||||
|
)
|
||||||
|
self._is_awake = True # Display starts in active state (_INIT_SEQUENCE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_awake(self):
|
||||||
|
"""
|
||||||
|
The power state of the display. (read-only)
|
||||||
|
|
||||||
|
`True` if the display is active, `False` if in sleep mode.
|
||||||
|
|
||||||
|
:type: bool
|
||||||
|
"""
|
||||||
|
return self._is_awake
|
||||||
|
|
||||||
|
def sleep(self):
|
||||||
|
"""
|
||||||
|
Put display into sleep mode.
|
||||||
|
|
||||||
|
Display uses < 10uA in sleep mode. Display remembers display data and operation mode
|
||||||
|
active prior to sleeping. MP can access (update) the built-in display RAM.
|
||||||
|
"""
|
||||||
|
if self._is_awake:
|
||||||
|
self.bus.send(0xAE, b"") # 0xAE = display off, sleep mode
|
||||||
|
self._is_awake = False
|
||||||
|
|
||||||
|
def wake(self):
|
||||||
|
"""
|
||||||
|
Wake display from sleep mode
|
||||||
|
"""
|
||||||
|
if not self._is_awake:
|
||||||
|
self.bus.send(0xAF, b"") # 0xAF = display on
|
||||||
|
self._is_awake = True
|
||||||
BIN
lib/adafruit_framebuf.mpy
Normal file
BIN
lib/adafruit_framebuf.mpy
Normal file
Binary file not shown.
BIN
lib/adafruit_pixelbuf.mpy
Normal file
BIN
lib/adafruit_pixelbuf.mpy
Normal file
Binary file not shown.
BIN
lib/adafruit_ssd1306.mpy
Normal file
BIN
lib/adafruit_ssd1306.mpy
Normal file
Binary file not shown.
BIN
lib/neopixel.mpy
Normal file
BIN
lib/neopixel.mpy
Normal file
Binary file not shown.
86
main.py
Normal file
86
main.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
print("Starting")
|
||||||
|
|
||||||
|
import board
|
||||||
|
|
||||||
|
from kmk.kmk_keyboard import KMKKeyboard
|
||||||
|
from kmk.keys import KC
|
||||||
|
from kmk.scanners import DiodeOrientation
|
||||||
|
from kmk.modules.encoder import EncoderHandler
|
||||||
|
from kmk.modules.oled2 import Oled
|
||||||
|
from kmk.extensions.lock_status import LockStatus
|
||||||
|
from kmk.extensions.RGB import RGB
|
||||||
|
from kmk.extensions.rgb import AnimationModes
|
||||||
|
from kmk.modules.power import Power
|
||||||
|
from kmk.modules.layers import Layers
|
||||||
|
|
||||||
|
## Create KMK Keyboard object
|
||||||
|
keyboard = KMKKeyboard()
|
||||||
|
##
|
||||||
|
|
||||||
|
## Add layer support
|
||||||
|
keyboard.modules.append(Layers())
|
||||||
|
##
|
||||||
|
|
||||||
|
## Add status indicator support
|
||||||
|
locks = LockStatus()
|
||||||
|
keyboard.extensions.append(locks)
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
## RGB extension config
|
||||||
|
rgb_ext = RGB(pixel_pin=board.A3, num_pixels=9,
|
||||||
|
hue_default=249, refresh_rate=60,
|
||||||
|
animation_speed=1, breathe_center=1,
|
||||||
|
knight_effect_length=3,
|
||||||
|
animation_mode=AnimationModes.STATIC)
|
||||||
|
|
||||||
|
keyboard.extensions.append(rgb_ext)
|
||||||
|
##
|
||||||
|
|
||||||
|
## Oled module configuration
|
||||||
|
oled_handler = Oled()
|
||||||
|
oled_handler.locks = locks
|
||||||
|
oled_handler.refresh_rate = 0.2
|
||||||
|
oled_handler.pins = (board.D3, board.D2)
|
||||||
|
|
||||||
|
keyboard.modules.append(oled_handler)
|
||||||
|
##
|
||||||
|
|
||||||
|
## Encoder configuration
|
||||||
|
encoder_handler = EncoderHandler()
|
||||||
|
encoder_handler.pins = ((board.A2, board.A1, None, False),)
|
||||||
|
encoder_handler.map = [(( KC.VOLU, KC.VOLD, KC.C),), (( KC.RGB_HUI, KC.RGB_HUD, KC.C), )]
|
||||||
|
|
||||||
|
keyboard.modules.append(encoder_handler)
|
||||||
|
##
|
||||||
|
|
||||||
|
## Keyboard debug flag
|
||||||
|
keyboard.debug_enabled = True
|
||||||
|
##
|
||||||
|
|
||||||
|
## Keyboard Matrix definition
|
||||||
|
keyboard.row_pins = (board.D5, board.D6, board.D7, board.D8, board.D9)
|
||||||
|
keyboard.col_pins = (board.SCK, board.MISO, board.MOSI, board.D10)
|
||||||
|
keyboard.diode_orientation = DiodeOrientation.COL2ROW
|
||||||
|
##
|
||||||
|
|
||||||
|
## Define keymap for the board
|
||||||
|
#TODO: Move keymap to its own module
|
||||||
|
keyboard.keymap = [
|
||||||
|
[KC.MO(1), KC.NUMPAD_SLASH, KC.NUMPAD_ASTERISK, KC.NUMPAD_MINUS,
|
||||||
|
KC.NUMPAD_7, KC.NUMPAD_8, KC.NUMPAD_9, KC.NUMPAD_PLUS,
|
||||||
|
KC.NUMPAD_4, KC.NUMPAD_5, KC.NUMPAD_6, KC.NO,
|
||||||
|
KC.NUMPAD_1, KC.NUMPAD_2, KC.NUMPAD_3, KC.NO, KC.NO,
|
||||||
|
KC.NUMPAD_0, KC.NUMPAD_DOT, KC.NUMPAD_ENTER],
|
||||||
|
[KC.NO, KC.NUM_LOCK, KC.NUMPAD_ASTERISK, KC.RGB_AND,
|
||||||
|
KC.RGB_MODE_PLAIN, KC.RGB_MODE_BREATHE, KC.RGB_MODE_RAINBOW, KC.RGB_ANI,
|
||||||
|
KC.NUMPAD_4, KC.NUMPAD_5, KC.NUMPAD_6, KC.NO,
|
||||||
|
KC.NUMPAD_1, KC.NUMPAD_2, KC.NUMPAD_3, KC.NO, KC.NO,
|
||||||
|
KC.NUMPAD_0, KC.NUMPAD_DOT, KC.RGB_MODE_RAINBOW]
|
||||||
|
]
|
||||||
|
##
|
||||||
|
|
||||||
|
## Start the board
|
||||||
|
if __name__ == '__main__':
|
||||||
|
keyboard.go()
|
||||||
|
##
|
||||||
Loading…
x
Reference in New Issue
Block a user