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