A => boot.py +3 -0
@@ 1,3 @@
+import supervisor
+
+supervisor.set_next_stack_limit(4096 + 4096)
A => kb.py +44 -0
@@ 1,44 @@
+import board
+
+from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
+from kmk.scanners import DiodeOrientation
+
+
+class KMKKeyboard(_KMKKeyboard):
+ col_pins = (
+ board.A3,
+ board.A2,
+ board.A1,
+ board.A0,
+ board.SCK,
+ board.MISO,
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ row_pins = (board.D4, board.D5, board.D6, board.D7)
+ diode_orientation = DiodeOrientation.COLUMNS
+ data_pin = board.D2
+ data_pin2 = board.D3
+ rgb_pixel_pin = board.D0
+ #i2c = board.I2C
+
+ # flake8: noqa
+ coord_mapping = [
+ 0, 1, 2, 3, 4, 5, 29, 28, 27, 26, 25, 24,
+ 6, 7, 8, 9, 10, 11, 35, 34, 33, 32, 31, 30,
+ 12, 13, 14, 15, 16, 17, 41, 40, 39, 38, 37, 36,
+ 21, 22, 23, 47, 46, 45,
+ ]
A => kmk/__init__.py +0 -0
A => kmk/consts.py +8 -0
@@ 1,8 @@
+from micropython import const
+
+
+class UnicodeMode:
+ NOOP = const(0)
+ LINUX = IBUS = const(1)
+ MACOS = OSX = RALT = const(2)
+ WINC = const(3)
A => kmk/extensions/__init__.py +51 -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
A => kmk/extensions/international.py +59 -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
A => +34 -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) # _
A => kmk/extensions/led.py +257 -0
@@ 1,257 @@
+import pwmio
+from math import e, exp, pi, sin
+
+from kmk.extensions import Extension, InvalidExtensionEnvironment
+from kmk.keys import make_argumented_key, make_key
+from kmk.utils import clamp
+
+
+class LEDKeyMeta:
+ def __init__(self, *leds):
+ self.leds = leds
+ self.brightness = None
+
+
+class AnimationModes:
+ OFF = 0
+ STATIC = 1
+ STATIC_STANDBY = 2
+ BREATHING = 3
+ USER = 4
+
+
+class LED(Extension):
+ def __init__(
+ self,
+ led_pin,
+ brightness=50,
+ brightness_step=5,
+ brightness_limit=100,
+ breathe_center=1.5,
+ animation_mode=AnimationModes.STATIC,
+ animation_speed=1,
+ user_animation=None,
+ val=100,
+ ):
+ try:
+ pins_iter = iter(led_pin)
+ except TypeError:
+ pins_iter = [led_pin]
+
+ try:
+ self._leds = [pwmio.PWMOut(pin) for pin in pins_iter]
+ except Exception as e:
+ print(e)
+ raise InvalidExtensionEnvironment(
+ 'Unable to create pwmio.PWMOut() instance with provided led_pin'
+ )
+
+ self._brightness = brightness
+ self._pos = 0
+ self._effect_init = False
+ self._enabled = True
+
+ self.brightness_step = brightness_step
+ self.brightness_limit = brightness_limit
+ self.animation_mode = animation_mode
+ self.animation_speed = animation_speed
+ self.breathe_center = breathe_center
+ self.val = val
+
+ if user_animation is not None:
+ self.user_animation = user_animation
+
+ make_argumented_key(
+ names=('LED_TOG',),
+ validator=self._led_key_validator,
+ on_press=self._key_led_tog,
+ )
+ make_argumented_key(
+ names=('LED_INC',),
+ validator=self._led_key_validator,
+ on_press=self._key_led_inc,
+ )
+ make_argumented_key(
+ names=('LED_DEC',),
+ validator=self._led_key_validator,
+ on_press=self._key_led_dec,
+ )
+ make_argumented_key(
+ names=('LED_SET',),
+ validator=self._led_set_key_validator,
+ on_press=self._key_led_set,
+ )
+ make_key(names=('LED_ANI',), on_press=self._key_led_ani)
+ make_key(names=('LED_AND',), on_press=self._key_led_and)
+ make_key(
+ names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static
+ )
+ make_key(
+ names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe
+ )
+
+ def __repr__(self):
+ return f'LED({self._to_dict()})'
+
+ def _to_dict(self):
+ return {
+ '_brightness': self._brightness,
+ '_pos': self._pos,
+ 'brightness_step': self.brightness_step,
+ 'brightness_limit': self.brightness_limit,
+ 'animation_mode': self.animation_mode,
+ 'animation_speed': self.animation_speed,
+ 'breathe_center': self.breathe_center,
+ 'val': self.val,
+ }
+
+ def on_runtime_enable(self, sandbox):
+ return
+
+ def on_runtime_disable(self, sandbox):
+ return
+
+ def during_bootup(self, sandbox):
+ return
+
+ def before_matrix_scan(self, sandbox):
+ return
+
+ def after_matrix_scan(self, sandbox):
+ return
+
+ def before_hid_send(self, sandbox):
+ return
+
+ def after_hid_send(self, sandbox):
+ 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
A => kmk/extensions/lock_status.py +85 -0
@@ 1,85 @@
+import usb_hid
+
+from kmk.extensions import Extension
+from kmk.hid import HIDUsage
+
+
+class LockCode:
+ NUMLOCK = 0x01
+ CAPSLOCK = 0x02
+ SCROLLLOCK = 0x04
+ COMPOSE = 0x08
+ KANA = 0x10
+ RESERVED = 0x20
+
+
+class LockStatus(Extension):
+ def __init__(self):
+ self.report = None
+ self.hid = None
+ self._report_updated = False
+ for device in usb_hid.devices:
+ if device.usage == HIDUsage.KEYBOARD:
+ self.hid = device
+
+ def __repr__(self):
+ return f'LockStatus(report={self.report})'
+
+ def during_bootup(self, sandbox):
+ return
+
+ def before_matrix_scan(self, sandbox):
+ return
+
+ def after_matrix_scan(self, sandbox):
+ return
+
+ def before_hid_send(self, sandbox):
+ return
+
+ def after_hid_send(self, sandbox):
+ if self.hid:
+ report = self.hid.get_last_received_report()
+ if report and report[0] != self.report:
+ self.report = report[0]
+ self._report_updated = True
+ else:
+ self._report_updated = False
+ else:
+ # _report_updated shouldn't ever be True if hid is
+ # falsy, but I would rather be safe than sorry.
+ self._report_updated = False
+ return
+
+ def on_powersave_enable(self, sandbox):
+ return
+
+ def on_powersave_disable(self, sandbox):
+ return
+
+ @property
+ def report_updated(self):
+ return self._report_updated
+
+ def check_state(self, lock_code):
+ # This is false if there's no valid report, or all report bits are zero
+ if self.report:
+ return bool(self.report & lock_code)
+ else:
+ # Just in case, default to False if we don't know anything
+ return False
+
+ def get_num_lock(self):
+ return self.check_state(LockCode.NUMLOCK)
+
+ def get_caps_lock(self):
+ return self.check_state(LockCode.CAPSLOCK)
+
+ def get_scroll_lock(self):
+ return self.check_state(LockCode.SCROLLLOCK)
+
+ def get_compose(self):
+ return self.check_state(LockCode.COMPOSE)
+
+ def get_kana(self):
+ return self.check_state(LockCode.KANA)
A => kmk/extensions/media_keys.py +57 -0
@@ 1,57 @@
+from kmk.extensions import Extension
+from kmk.keys import make_consumer_key
+
+
+class MediaKeys(Extension):
+ def __init__(self):
+ # Consumer ("media") keys. Most known keys aren't supported here. A much
+ # longer list used to exist in this file, but the codes were almost certainly
+ # incorrect, conflicting with each other, or otherwise 'weird'. We'll add them
+ # back in piecemeal as needed. PRs welcome.
+ #
+ # A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php
+ # Note that currently we only have the PC codes. Recent MacOS versions seem to
+ # support PC media keys, so I don't know how much value we would get out of
+ # adding the old Apple-specific consumer codes, but again, PRs welcome if the
+ # lack of them impacts you.
+ make_consumer_key(code=226, names=('AUDIO_MUTE', 'MUTE')) # 0xE2
+ make_consumer_key(code=233, names=('AUDIO_VOL_UP', 'VOLU')) # 0xE9
+ make_consumer_key(code=234, names=('AUDIO_VOL_DOWN', 'VOLD')) # 0xEA
+ make_consumer_key(code=111, names=('BRIGHTNESS_UP', 'BRIU')) # 0x6F
+ make_consumer_key(code=112, names=('BRIGHTNESS_DOWN', 'BRID')) # 0x70
+ make_consumer_key(code=181, names=('MEDIA_NEXT_TRACK', 'MNXT')) # 0xB5
+ make_consumer_key(code=182, names=('MEDIA_PREV_TRACK', 'MPRV')) # 0xB6
+ make_consumer_key(code=183, names=('MEDIA_STOP', 'MSTP')) # 0xB7
+ make_consumer_key(
+ code=205, names=('MEDIA_PLAY_PAUSE', 'MPLY')
+ ) # 0xCD (this may not be right)
+ make_consumer_key(code=184, names=('MEDIA_EJECT', 'EJCT')) # 0xB8
+ make_consumer_key(code=179, names=('MEDIA_FAST_FORWARD', 'MFFD')) # 0xB3
+ make_consumer_key(code=180, names=('MEDIA_REWIND', 'MRWD')) # 0xB4
+
+ def on_runtime_enable(self, sandbox):
+ return
+
+ def on_runtime_disable(self, sandbox):
+ return
+
+ def during_bootup(self, sandbox):
+ return
+
+ def before_matrix_scan(self, sandbox):
+ return
+
+ def after_matrix_scan(self, sandbox):
+ return
+
+ def before_hid_send(self, sandbox):
+ return
+
+ def after_hid_send(self, sandbox):
+ return
+
+ def on_powersave_enable(self, sandbox):
+ return
+
+ def on_powersave_disable(self, sandbox):
+ return
A => kmk/extensions/peg_oled_display.py +161 -0
@@ 1,161 @@
+import busio
+import gc
+
+import adafruit_displayio_ssd1306
+import displayio
+import terminalio
+from adafruit_display_text import label
+
+from kmk.extensions import Extension
+
+
+class OledDisplayMode:
+ TXT = 0
+ IMG = 1
+
+
+class OledReactionType:
+ STATIC = 0
+ LAYER = 1
+
+
+class OledData:
+ def __init__(
+ self,
+ image=None,
+ corner_one=None,
+ corner_two=None,
+ corner_three=None,
+ corner_four=None,
+ ):
+ if image:
+ self.data = [image]
+ elif corner_one and corner_two and corner_three and corner_four:
+ self.data = [corner_one, corner_two, corner_three, corner_four]
+
+
+class Oled(Extension):
+ def __init__(
+ self,
+ views,
+ toDisplay=OledDisplayMode.TXT,
+ oWidth=128,
+ oHeight=32,
+ flip: bool = False,
+ ):
+ displayio.release_displays()
+ self.rotation = 180 if flip else 0
+ self._views = views.data
+ self._toDisplay = toDisplay
+ self._width = oWidth
+ self._height = oHeight
+ self._prevLayers = 0
+ gc.collect()
+
+ def returnCurrectRenderText(self, layer, singleView):
+ # for now we only have static things and react to layers. But when we react to battery % and wpm we can handle the logic here
+ if singleView[0] == OledReactionType.STATIC:
+ return singleView[1][0]
+ if singleView[0] == OledReactionType.LAYER:
+ return singleView[1][layer]
+
+ def renderOledTextLayer(self, layer):
+ splash = displayio.Group()
+ splash.append(
+ label.Label(
+ terminalio.FONT,
+ text=self.returnCurrectRenderText(layer, self._views[0]),
+ color=0xFFFFFF,
+ x=0,
+ y=10,
+ )
+ )
+ splash.append(
+ label.Label(
+ terminalio.FONT,
+ text=self.returnCurrectRenderText(layer, self._views[1]),
+ color=0xFFFFFF,
+ x=64,
+ y=10,
+ )
+ )
+ splash.append(
+ label.Label(
+ terminalio.FONT,
+ text=self.returnCurrectRenderText(layer, self._views[2]),
+ color=0xFFFFFF,
+ x=0,
+ y=25,
+ )
+ )
+ splash.append(
+ label.Label(
+ terminalio.FONT,
+ text=self.returnCurrectRenderText(layer, self._views[3]),
+ color=0xFFFFFF,
+ x=64,
+ y=25,
+ )
+ )
+ self._display.show(splash)
+ gc.collect()
+
+ def renderOledImgLayer(self, layer):
+ splash = displayio.Group()
+ odb = displayio.OnDiskBitmap(
+ '/' + self.returnCurrectRenderText(layer, self._views[0])
+ )
+ image = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
+ splash.append(image)
+ self._display.show(splash)
+ gc.collect()
+
+ def updateOLED(self, sandbox):
+ if self._toDisplay == OledDisplayMode.TXT:
+ self.renderOledTextLayer(sandbox.active_layers[0])
+ if self._toDisplay == OledDisplayMode.IMG:
+ self.renderOledImgLayer(sandbox.active_layers[0])
+ gc.collect()
+
+ def on_runtime_enable(self, sandbox):
+ return
+
+ def on_runtime_disable(self, sandbox):
+ return
+
+ def during_bootup(self, board):
+ displayio.release_displays()
+ i2c = busio.I2C(board.SCL, board.SDA)
+ self._display = adafruit_displayio_ssd1306.SSD1306(
+ displayio.I2CDisplay(i2c, device_address=0x3C),
+ width=self._width,
+ height=self._height,
+ rotation=self.rotation,
+ )
+ if self._toDisplay == OledDisplayMode.TXT:
+ self.renderOledTextLayer(0)
+ if self._toDisplay == OledDisplayMode.IMG:
+ self.renderOledImgLayer(0)
+ return
+
+ def before_matrix_scan(self, sandbox):
+ if sandbox.active_layers[0] != self._prevLayers:
+ self._prevLayers = sandbox.active_layers[0]
+ self.updateOLED(sandbox)
+ return
+
+ def after_matrix_scan(self, sandbox):
+
+ return
+
+ def before_hid_send(self, sandbox):
+ return
+
+ def after_hid_send(self, sandbox):
+ return
+
+ def on_powersave_enable(self, sandbox):
+ return
+
+ def on_powersave_disable(self, sandbox):
+ return
A => kmk/extensions/peg_rgb_matrix.py +201 -0
@@ 1,201 @@
+import neopixel
+
+from storage import getmount
+
+from kmk.extensions import Extension
+from kmk.handlers.stock import passthrough as handler_passthrough
+from kmk.keys import make_key
+
+
+class Color:
+ OFF = [0, 0, 0]
+ BLACK = OFF
+ WHITE = [249, 249, 249]
+ RED = [255, 0, 0]
+ AZURE = [153, 245, 255]
+ BLUE = [0, 0, 255]
+ CYAN = [0, 255, 255]
+ GREEN = [0, 255, 0]
+ YELLOW = [255, 247, 0]
+ MAGENTA = [255, 0, 255]
+ ORANGE = [255, 77, 0]
+ PURPLE = [255, 0, 242]
+ TEAL = [0, 128, 128]
+ PINK = [255, 0, 255]
+
+
+class Rgb_matrix_data:
+ def __init__(self, keys=[], underglow=[]):
+ if len(keys) == 0:
+ print('No colors passed for your keys')
+ return
+ if len(underglow) == 0:
+ print('No colors passed for your underglow')
+ return
+ self.data = keys + underglow
+
+ @staticmethod
+ def generate_led_map(
+ number_of_keys, number_of_underglow, key_color, underglow_color
+ ):
+ keys = [key_color] * number_of_keys
+ underglow = [underglow_color] * number_of_underglow
+ print(f'Rgb_matrix_data(keys={keys},\nunderglow={underglow})')
+
+
+class Rgb_matrix(Extension):
+ def __init__(
+ self,
+ rgb_order=(1, 0, 2), # GRB WS2812
+ disable_auto_write=False,
+ ledDisplay=[],
+ split=False,
+ rightSide=False,
+ ):
+ name = str(getmount('/').label)
+ self.rgb_order = rgb_order
+ self.disable_auto_write = disable_auto_write
+ self.split = split
+ self.rightSide = rightSide
+ self.brightness_step = 0.1
+ self.brightness = 0
+
+ if name.endswith('L'):
+ self.rightSide = False
+ elif name.endswith('R'):
+ self.rightSide = True
+ if type(ledDisplay) is Rgb_matrix_data:
+ self.ledDisplay = ledDisplay.data
+ else:
+ self.ledDisplay = ledDisplay
+
+ make_key(
+ names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
+ )
+ make_key(
+ names=('RGB_BRI',), on_press=self._rgb_bri, on_release=handler_passthrough
+ )
+ make_key(
+ names=('RGB_BRD',), on_press=self._rgb_brd, on_release=handler_passthrough
+ )
+
+ def _rgb_tog(self, *args, **kwargs):
+ if self.enable:
+ self.off()
+ else:
+ self.on()
+ self.enable = not self.enable
+
+ def _rgb_bri(self, *args, **kwargs):
+ self.increase_brightness()
+
+ def _rgb_brd(self, *args, **kwargs):
+ self.decrease_brightness()
+
+ def on(self):
+ if self.neopixel:
+ self.setBasedOffDisplay()
+ self.neopixel.show()
+
+ def off(self):
+ if self.neopixel:
+ self.set_rgb_fill((0, 0, 0))
+
+ def set_rgb_fill(self, rgb):
+ if self.neopixel:
+ self.neopixel.fill(rgb)
+ if self.disable_auto_write:
+ self.neopixel.show()
+
+ def set_brightness(self, brightness=None):
+ if brightness is None:
+ brightness = self.brightness
+
+ if self.neopixel:
+ self.neopixel.brightness = brightness
+ if self.disable_auto_write:
+ self.neopixel.show()
+
+ def increase_brightness(self, step=None):
+ if step is None:
+ step = self.brightness_step
+
+ self.brightness = (
+ self.brightness + step if self.brightness + step <= 1.0 else 1.0
+ )
+
+ self.set_brightness(self.brightness)
+
+ def decrease_brightness(self, step=None):
+ if step is None:
+ step = self.brightness_step
+
+ self.brightness = (
+ self.brightness - step if self.brightness - step >= 0.0 else 0.0
+ )
+ self.set_brightness(self.brightness)
+
+ def setBasedOffDisplay(self):
+ if self.split:
+ for i, val in enumerate(self.ledDisplay):
+ if self.rightSide:
+ if self.keyPos[i] >= (self.num_pixels / 2):
+ self.neopixel[int(self.keyPos[i] - (self.num_pixels / 2))] = (
+ val[0],
+ val[1],
+ val[2],
+ )
+ else:
+ if self.keyPos[i] <= (self.num_pixels / 2):
+ self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
+ else:
+ for i, val in enumerate(self.ledDisplay):
+ self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
+
+ def on_runtime_enable(self, sandbox):
+ return
+
+ def on_runtime_disable(self, sandbox):
+ return
+
+ def during_bootup(self, board):
+ self.neopixel = neopixel.NeoPixel(
+ board.rgb_pixel_pin,
+ board.num_pixels,
+ brightness=board.brightness_limit,
+ pixel_order=self.rgb_order,
+ auto_write=not self.disable_auto_write,
+ )
+ self.num_pixels = board.num_pixels
+ self.keyPos = board.led_key_pos
+ self.brightness = board.brightness_limit
+ self.on()
+ return
+
+ def before_matrix_scan(self, sandbox):
+ return
+
+ def after_matrix_scan(self, sandbox):
+ return
+
+ def before_hid_send(self, sandbox):
+ return
+
+ def after_hid_send(self, sandbox):
+ return
+
+ def on_powersave_enable(self, sandbox):
+ if self.neopixel:
+ self.neopixel.brightness = (
+ self.neopixel.brightness / 2
+ if self.neopixel.brightness / 2 > 0
+ else 0.1
+ )
+ if self.disable_auto_write:
+ self.neopixel.show()
+
+ def on_powersave_disable(self, sandbox):
+ if self.neopixel:
+ self.neopixel.brightness = self.brightness
+ if self.disable_auto_write:
+ self.neopixel.show()
A => kmk/extensions/rgb.py +593 -0
@@ 1,593 @@
+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 Debug, clamp
+
+debug = Debug(__name__)
+
+rgb_config = {}
+
+
+def hsv_to_rgb(hue, sat, val):
+ '''
+ Converts HSV values, and returns a tuple of RGB values
+ :param hue:
+ :param sat:
+ :param val:
+ :return: (r, g, b)
+ '''
+ if sat == 0:
+ return (val, val, val)
+
+ hue = 6 * (hue & 0xFF)
+ frac = hue & 0xFF
+ sxt = hue >> 8
+
+ base = (0xFF - sat) * val
+ color = (val * sat * frac) >> 8
+ val <<= 8
+
+ if sxt == 0:
+ r = val
+ g = base + color
+ b = base
+ elif sxt == 1:
+ r = val - color
+ g = val
+ b = base
+ elif sxt == 2:
+ r = base
+ g = val
+ b = base + color
+ elif sxt == 3:
+ r = base
+ g = val - color
+ b = val
+ elif sxt == 4:
+ r = base + color
+ g = base
+ b = val
+ elif sxt == 5:
+ r = val
+ g = base
+ b = val - color
+
+ return (r >> 8), (g >> 8), (b >> 8)
+
+
+def hsv_to_rgbw(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)
+
+ if debug.enabled:
+ for n, pixels in enumerate(self.pixels):
+ debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')
+
+ self.rgbw = bool(len(rgb_order) == 4)
+
+ self.hue_step = hue_step
+ self.sat_step = sat_step
+ self.val_step = val_step
+ 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
+ '''
+
+ val = clamp(val, 0, self.val_limit)
+
+ if self.rgbw:
+ self.set_rgb(hsv_to_rgbw(hue, sat, val), index)
+ else:
+ self.set_rgb(hsv_to_rgb(hue, sat, val), index)
+
+ def set_hsv_fill(self, hue, sat, val):
+ '''
+ Takes HSV values and displays it on all LEDs/Neopixels
+ :param hue:
+ :param sat:
+ :param val:
+ '''
+
+ val = clamp(val, 0, self.val_limit)
+
+ if self.rgbw:
+ self.set_rgb_fill(hsv_to_rgbw(hue, sat, val))
+ else:
+ self.set_rgb_fill(hsv_to_rgb(hue, sat, val))
+
+ def set_rgb(self, rgb, index):
+ '''
+ Takes an RGB or RGBW and displays it on a single LED/Neopixel
+ :param rgb: RGB or RGBW
+ :param index: Index of LED/Pixel
+ '''
+ if 0 <= index <= self.num_pixels - 1:
+ for pixels in self.pixels:
+ if index <= (len(pixels) - 1):
+ pixels[index] = rgb
+ break
+ index -= len(pixels)
+
+ if not self.disable_auto_write:
+ pixels.show()
+
+ def set_rgb_fill(self, rgb):
+ '''
+ Takes an RGB or RGBW and displays it on all LEDs/Neopixels
+ :param rgb: RGB or RGBW
+ '''
+ for pixels in self.pixels:
+ pixels.fill(rgb)
+ if not self.disable_auto_write:
+ pixels.show()
+
+ def increase_hue(self, step=None):
+ '''
+ Increases hue by step amount rolling at 256 and returning to 0
+ :param step:
+ '''
+ if step is None:
+ step = self.hue_step
+
+ self.hue = (self.hue + step) % 256
+
+ if self._check_update():
+ self._do_update()
+
+ def decrease_hue(self, step=None):
+ '''
+ Decreases hue by step amount rolling at 0 and returning to 256
+ :param step:
+ '''
+ if step is None:
+ step = self.hue_step
+
+ if (self.hue - step) <= 0:
+ self.hue = (self.hue + 256 - step) % 256
+ else:
+ self.hue = (self.hue - step) % 256
+
+ if self._check_update():
+ self._do_update()
+
+ def increase_sat(self, step=None):
+ '''
+ Increases saturation by step amount stopping at 255
+ :param step:
+ '''
+ if step is None:
+ step = self.sat_step
+
+ self.sat = clamp(self.sat + step, 0, 255)
+
+ if self._check_update():
+ self._do_update()
+
+ def decrease_sat(self, step=None):
+ '''
+ Decreases saturation by step amount stopping at 0
+ :param step:
+ '''
+ if step is None:
+ step = self.sat_step
+
+ self.sat = clamp(self.sat - step, 0, 255)
+
+ if self._check_update():
+ self._do_update()
+
+ def increase_val(self, step=None):
+ '''
+ Increases value by step amount stopping at 100
+ :param step:
+ '''
+ if step is None:
+ step = self.val_step
+
+ self.val = clamp(self.val + step, 0, 255)
+
+ if self._check_update():
+ self._do_update()
+
+ def decrease_val(self, step=None):
+ '''
+ Decreases value by step amount stopping at 0
+ :param step:
+ '''
+ if step is None:
+ step = self.val_step
+
+ self.val = clamp(self.val - step, 0, 255)
+
+ if self._check_update():
+ self._do_update()
+
+ def increase_ani(self):
+ '''
+ Increases animation speed by 1 amount stopping at 10
+ :param step:
+ '''
+ self.animation_speed = clamp(self.animation_speed + 1, 0, 10)
+
+ if self._check_update():
+ self._do_update()
+
+ def decrease_ani(self):
+ '''
+ Decreases animation speed by 1 amount stopping at 0
+ :param step:
+ '''
+ self.animation_speed = clamp(self.animation_speed - 1, 0, 10)
+
+ if self._check_update():
+ self._do_update()
+
+ def off(self):
+ '''
+ Turns off all LEDs/Neopixels without changing stored values
+ '''
+ self.set_hsv_fill(0, 0, 0)
+
+ def show(self):
+ '''
+ Turns on all LEDs/Neopixels without changing stored values
+ '''
+ for pixels in self.pixels:
+ pixels.show()
+
+ def animate(self):
+ '''
+ Activates a "step" in the animation based on the active mode
+ :return: Returns the new state in animation
+ '''
+ if self.effect_init:
+ self._init_effect()
+
+ if self.animation_mode is AnimationModes.STATIC_STANDBY:
+ return
+
+ if self.enable 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()
A => kmk/extensions/statusled.py +145 -0
@@ 1,145 @@
+# Use this extension for showing layer status with three leds
+
+import pwmio
+import time
+
+from kmk.extensions import Extension, InvalidExtensionEnvironment
+from kmk.keys import make_key
+
+
+class statusLED(Extension):
+ def __init__(
+ self,
+ led_pins,
+ brightness=30,
+ brightness_step=5,
+ brightness_limit=100,
+ ):
+ self._leds = []
+ for led in led_pins:
+ try:
+ self._leds.append(pwmio.PWMOut(led))
+ except Exception as e:
+ print(e)
+ raise InvalidExtensionEnvironment(
+ 'Unable to create pulseio.PWMOut() instance with provided led_pin'
+ )
+ self._led_count = len(self._leds)
+
+ self.brightness = brightness
+ self._layer_last = -1
+
+ self.brightness_step = brightness_step
+ self.brightness_limit = brightness_limit
+
+ make_key(names=('SLED_INC',), on_press=self._key_led_inc)
+ make_key(names=('SLED_DEC',), on_press=self._key_led_dec)
+
+ def _layer_indicator(self, layer_active, *args, **kwargs):
+ '''
+ Indicates layer with leds
+
+ For the time being just a simple consecutive single led
+ indicator. And when there are more layers than leds it
+ wraps around to the first led again.
+ (Also works for a single led, which just lights when any
+ layer is active)
+ '''
+
+ if self._layer_last != layer_active:
+ led_last = 0 if self._layer_last == 0 else 1 + (self._layer_last - 1) % 3
+ if layer_active > 0:
+ led_active = 0 if layer_active == 0 else 1 + (layer_active - 1) % 3
+ self.set_brightness(self.brightness, led_active)
+ self.set_brightness(0, led_last)
+ else:
+ self.set_brightness(0, led_last)
+ self._layer_last = layer_active
+
+ def __repr__(self):
+ return f'SLED({self._to_dict()})'
+
+ def _to_dict(self):
+ return {
+ '_brightness': self.brightness,
+ 'brightness_step': self.brightness_step,
+ 'brightness_limit': self.brightness_limit,
+ }
+
+ def on_runtime_enable(self, sandbox):
+ return
+
+ def on_runtime_disable(self, sandbox):
+ return
+
+ def during_bootup(self, sandbox):
+ '''Light up every single led once for 200 ms'''
+ for i in range(self._led_count + 2):
+ if i < self._led_count:
+ self._leds[i].duty_cycle = int(self.brightness / 100 * 65535)
+ i_off = i - 2
+ if i_off >= 0 and i_off < self._led_count:
+ self._leds[i_off].duty_cycle = int(0)
+ time.sleep(0.1)
+ for led in self._leds:
+ led.duty_cycle = int(0)
+ return
+
+ def before_matrix_scan(self, sandbox):
+ return
+
+ def after_matrix_scan(self, sandbox):
+ self._layer_indicator(sandbox.active_layers[0])
+ return
+
+ def before_hid_send(self, sandbox):
+ return
+
+ def after_hid_send(self, sandbox):
+ return
+
+ def on_powersave_enable(self, sandbox):
+ self.set_brightness(0)
+ return
+
+ def on_powersave_disable(self, sandbox):
+ self.set_brightness(self._brightness)
+ self._leds[2].duty_cycle = int(50 / 100 * 65535)
+ time.sleep(0.2)
+ self._leds[2].duty_cycle = int(0)
+ return
+
+ def set_brightness(self, percent, layer_id=-1):
+ if layer_id < 0:
+ for led in self._leds:
+ led.duty_cycle = int(percent / 100 * 65535)
+ else:
+ self._leds[layer_id - 1].duty_cycle = int(percent / 100 * 65535)
+
+ def increase_brightness(self, step=None):
+ if not step:
+ self._brightness += self.brightness_step
+ else:
+ self._brightness += step
+
+ if self._brightness > 100:
+ self._brightness = 100
+
+ self.set_brightness(self._brightness, self._layer_last)
+
+ def decrease_brightness(self, step=None):
+ if not step:
+ self._brightness -= self.brightness_step
+ else:
+ self._brightness -= step
+
+ if self._brightness < 0:
+ self._brightness = 0
+
+ self.set_brightness(self._brightness, self._layer_last)
+
+ def _key_led_inc(self, *args, **kwargs):
+ self.increase_brightness()
+
+ def _key_led_dec(self, *args, **kwargs):
+ self.decrease_brightness()
A => kmk/extensions/stringy_keymaps.py +45 -0
@@ 1,45 @@
+from kmk.extensions import Extension
+from kmk.keys import KC
+
+
+class StringyKeymaps(Extension):
+ #####
+ # User-configurable
+ debug_enabled = False
+
+ def on_runtime_enable(self, keyboard):
+ return
+
+ def on_runtime_disable(self, keyboard):
+ return
+
+ def during_bootup(self, keyboard):
+ for _, layer in enumerate(keyboard.keymap):
+ for key_idx, key in enumerate(layer):
+ if isinstance(key, str):
+ replacement = KC.get(key)
+ if replacement is None:
+ replacement = KC.NO
+ if self.debug_enabled:
+ print(f"Failed replacing '{key}'. Using KC.NO")
+ elif self.debug_enabled:
+ print(f"Replacing '{key}' with {replacement}")
+ layer[key_idx] = replacement
+
+ def before_matrix_scan(self, keyboard):
+ return
+
+ def after_matrix_scan(self, keyboard):
+ return
+
+ def before_hid_send(self, keyboard):
+ return
+
+ def after_hid_send(self, keyboard):
+ return
+
+ def on_powersave_enable(self, keyboard):
+ return
+
+ def on_powersave_disable(self, keyboard):
+ return
A => kmk/handlers/__init__.py +0 -0
A => kmk/handlers/sequences.py +155 -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
A => kmk/handlers/stock.py +129 -0
@@ 1,129 @@
+from time import sleep
+
+
+def passthrough(key, keyboard, *args, **kwargs):
+ return keyboard
+
+
+def default_pressed(key, keyboard, KC, coord_int=None, *args, **kwargs):
+ keyboard.hid_pending = True
+
+ keyboard.keys_pressed.add(key)
+
+ return keyboard
+
+
+def default_released(key, keyboard, KC, coord_int=None, *args, **kwargs): # NOQA
+ keyboard.hid_pending = True
+ keyboard.keys_pressed.discard(key)
+
+ return keyboard
+
+
+def reset(*args, **kwargs):
+ import microcontroller
+
+ microcontroller.reset()
+
+
+def reload(*args, **kwargs):
+ import supervisor
+
+ supervisor.reload()
+
+
+def bootloader(*args, **kwargs):
+ import microcontroller
+
+ microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
+ microcontroller.reset()
+
+
+def debug_pressed(key, keyboard, KC, *args, **kwargs):
+ if keyboard.debug_enabled:
+ print('DebugDisable()')
+ else:
+ print('DebugEnable()')
+
+ keyboard.debug_enabled = not keyboard.debug_enabled
+
+ return keyboard
+
+
+def gesc_pressed(key, keyboard, KC, *args, **kwargs):
+ GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI}
+
+ if GESC_TRIGGERS.intersection(keyboard.keys_pressed):
+ # First, release GUI if already pressed
+ keyboard._send_hid()
+ # if Shift is held, KC_GRAVE will become KC_TILDE on OS level
+ keyboard.keys_pressed.add(KC.GRAVE)
+ keyboard.hid_pending = True
+ return keyboard
+
+ # else return KC_ESC
+ keyboard.keys_pressed.add(KC.ESCAPE)
+ keyboard.hid_pending = True
+
+ return keyboard
+
+
+def gesc_released(key, keyboard, KC, *args, **kwargs):
+ keyboard.keys_pressed.discard(KC.ESCAPE)
+ keyboard.keys_pressed.discard(KC.GRAVE)
+ keyboard.hid_pending = True
+ return keyboard
+
+
+def bkdl_pressed(key, keyboard, KC, *args, **kwargs):
+ BKDL_TRIGGERS = {KC.LGUI, KC.RGUI}
+
+ if BKDL_TRIGGERS.intersection(keyboard.keys_pressed):
+ keyboard._send_hid()
+ keyboard.keys_pressed.add(KC.DEL)
+ keyboard.hid_pending = True
+ return keyboard
+
+ # else return KC_ESC
+ keyboard.keys_pressed.add(KC.BKSP)
+ keyboard.hid_pending = True
+
+ return keyboard
+
+
+def bkdl_released(key, keyboard, KC, *args, **kwargs):
+ keyboard.keys_pressed.discard(KC.BKSP)
+ keyboard.keys_pressed.discard(KC.DEL)
+ keyboard.hid_pending = True
+ return keyboard
+
+
+def sleep_pressed(key, keyboard, KC, *args, **kwargs):
+ sleep(key.meta.ms / 1000)
+ return keyboard
+
+
+def uc_mode_pressed(key, keyboard, *args, **kwargs):
+ keyboard.unicode_mode = key.meta.mode
+
+ return keyboard
+
+
+def hid_switch(key, keyboard, *args, **kwargs):
+ keyboard.hid_type, keyboard.secondary_hid_type = (
+ keyboard.secondary_hid_type,
+ keyboard.hid_type,
+ )
+ keyboard._init_hid()
+ return keyboard
+
+
+def ble_refresh(key, keyboard, *args, **kwargs):
+ from kmk.hid import HIDModes
+
+ if keyboard.hid_type != HIDModes.BLE:
+ return keyboard
+
+ keyboard._hid_helper.stop_advertising()
+ keyboard._hid_helper.start_advertising()
+ return keyboard
A => kmk/hid.py +328 -0
@@ 1,328 @@
+import supervisor
+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 f'{self.__class__.__name__}(REPORT_BYTES={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):
+ if not supervisor.runtime.usb_connected:
+ return
+
+ # int, can be looked up in HIDReportTypes
+ reporting_device_const = evt[0]
+
+ return self.devices[reporting_device_const].send_report(
+ evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1]
+ )
+
+
+class BLEHID(AbstractHID):
+ BLE_APPEARANCE_HID_KEYBOARD = const(961)
+ # Hardcoded in CPy
+ MAX_CONNECTIONS = const(2)
+
+ def __init__(self, ble_name=str(getmount('/').label), **kwargs):
+ self.ble_name = ble_name
+ super().__init__()
+
+ def post_init(self):
+ self.ble = BLERadio()
+ self.ble.name = self.ble_name
+ self.hid = HIDService()
+ self.hid.protocol_mode = 0 # Boot protocol
+
+ # Security-wise this is not right. While you're away someone turns
+ # on your keyboard and they can pair with it nice and clean and then
+ # listen to keystrokes.
+ # On the other hand we don't have LESC so it's like shouting your
+ # keystrokes in the air
+ if not self.ble.connected or not self.hid.devices:
+ self.start_advertising()
+
+ @property
+ def devices(self):
+ '''Search through the provided list of devices to find the ones with the
+ send_report attribute.'''
+ if not self.ble.connected:
+ return {}
+
+ result = {}
+
+ for device in self.hid.devices:
+ if not hasattr(device, 'send_report'):
+ continue
+ us = device.usage
+ up = device.usage_page
+
+ if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
+ result[HIDReportTypes.CONSUMER] = device
+ continue
+
+ if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
+ result[HIDReportTypes.KEYBOARD] = device
+ continue
+
+ if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
+ result[HIDReportTypes.MOUSE] = device
+ continue
+
+ if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
+ result[HIDReportTypes.SYSCONTROL] = device
+ continue
+
+ return result
+
+ def hid_send(self, evt):
+ if not self.ble.connected:
+ return
+
+ # int, can be looked up in HIDReportTypes
+ reporting_device_const = evt[0]
+
+ device = self.devices[reporting_device_const]
+
+ report_size = len(device._characteristic.value)
+ while len(evt) < report_size + 1:
+ evt.append(0)
+
+ return device.send_report(evt[1 : report_size + 1])
+
+ def clear_bonds(self):
+ import _bleio
+
+ _bleio.adapter.erase_bonding()
+
+ def start_advertising(self):
+ if not self.ble.advertising:
+ advertisement = ProvideServicesAdvertisement(self.hid)
+ advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD
+
+ self.ble.start_advertising(advertisement)
+
+ def stop_advertising(self):
+ self.ble.stop_advertising()
A => kmk/key_validators.py +9 -0
@@ 1,9 @@
+from kmk.types import KeySeqSleepMeta, UnicodeModeKeyMeta
+
+
+def key_seq_sleep_validator(ms):
+ return KeySeqSleepMeta(ms)
+
+
+def unicode_mode_key_validator(mode):
+ return UnicodeModeKeyMeta(mode)
A => kmk/keys.py +743 -0
@@ 1,743 @@
+from micropython import const
+
+import kmk.handlers.stock as handlers
+from kmk.consts import UnicodeMode
+from kmk.key_validators import key_seq_sleep_validator, unicode_mode_key_validator
+from kmk.types import UnicodeModeKeyMeta
+from kmk.utils import Debug
+
+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)
+
+debug = Debug(__name__)
+
+
+def maybe_make_key(code, names, *args, **kwargs):
+ def closure(candidate):
+ if candidate in names:
+ return make_key(code=code, names=names, *args, **kwargs)
+
+ return closure
+
+
+def maybe_make_argumented_key(
+ validator=lambda *validator_args, **validator_kwargs: object(),
+ names=tuple(), # NOQA
+ *constructor_args,
+ **constructor_kwargs,
+):
+ def closure(candidate):
+ if candidate in names:
+ return make_argumented_key(
+ validator, names, *constructor_args, **constructor_kwargs
+ )
+
+ return closure
+
+
+def maybe_make_no_key(candidate):
+ # NO and TRNS are functionally identical in how they (don't) mutate
+ # the state, but are tracked semantically separately, so create
+ # two keys with the exact same functionality
+ keys = (
+ ('NO', 'XXXXXXX'),
+ ('TRANSPARENT', 'TRNS'),
+ )
+
+ for names in keys:
+ if candidate in names:
+ return make_key(
+ names=names,
+ on_press=handlers.passthrough,
+ on_release=handlers.passthrough,
+ )
+
+
+def maybe_make_alpha_key(candidate):
+ if len(candidate) != 1:
+ return
+
+ candidate_upper = candidate.upper()
+ if candidate_upper in ALL_ALPHAS:
+ return make_key(
+ code=4 + ALL_ALPHAS.index(candidate_upper),
+ names=(candidate_upper, candidate.lower()),
+ )
+
+
+def maybe_make_numeric_key(candidate):
+ if candidate in ALL_NUMBERS or candidate in ALL_NUMBER_ALIASES:
+ try:
+ offset = ALL_NUMBERS.index(candidate)
+ except ValueError:
+ offset = ALL_NUMBER_ALIASES.index(candidate)
+
+ return make_key(
+ code=30 + offset,
+ names=(ALL_NUMBERS[offset], ALL_NUMBER_ALIASES[offset]),
+ )
+
+
+def maybe_make_mod_key(candidate):
+ # MEH = LCTL | LALT | LSFT
+ # HYPR = LCTL | LALT | LSFT | LGUI
+ mods = (
+ (0x01, ('LEFT_CONTROL', 'LCTRL', 'LCTL')),
+ (0x02, ('LEFT_SHIFT', 'LSHIFT', 'LSFT')),
+ (0x04, ('LEFT_ALT', 'LALT', 'LOPT')),
+ (0x08, ('LEFT_SUPER', 'LGUI', 'LCMD', 'LWIN')),
+ (0x10, ('RIGHT_CONTROL', 'RCTRL', 'RCTL')),
+ (0x20, ('RIGHT_SHIFT', 'RSHIFT', 'RSFT')),
+ (0x40, ('RIGHT_ALT', 'RALT', 'ROPT')),
+ (0x80, ('RIGHT_SUPER', 'RGUI', 'RCMD', 'RWIN')),
+ (0x07, ('MEH',)),
+ (0x0F, ('HYPER', 'HYPR')),
+ )
+
+ for code, names in mods:
+ if candidate in names:
+ return make_key(code=code, names=names, type=KEY_MODIFIER)
+
+
+def maybe_make_more_ascii(candidate):
+ codes = (
+ (40, ('ENTER', 'ENT', '\n')),
+ (41, ('ESCAPE', 'ESC')),
+ (42, ('BACKSPACE', 'BSPACE', 'BSPC', 'BKSP')),
+ (43, ('TAB', '\t')),
+ (44, ('SPACE', 'SPC', ' ')),
+ (45, ('MINUS', 'MINS', '-')),
+ (46, ('EQUAL', 'EQL', '=')),
+ (47, ('LBRACKET', 'LBRC', '[')),
+ (48, ('RBRACKET', 'RBRC', ']')),
+ (49, ('BACKSLASH', 'BSLASH', 'BSLS', '\\')),
+ (51, ('SEMICOLON', 'SCOLON', 'SCLN', ';')),
+ (52, ('QUOTE', 'QUOT', "'")),
+ (53, ('GRAVE', 'GRV', 'ZKHK', '`')),
+ (54, ('COMMA', 'COMM', ',')),
+ (55, ('DOT', '.')),
+ (56, ('SLASH', 'SLSH', '/')),
+ )
+
+ for code, names in codes:
+ if candidate in names:
+ return make_key(code=code, names=names)
+
+
+def maybe_make_fn_key(candidate):
+ codes = (
+ (58, ('F1',)),
+ (59, ('F2',)),
+ (60, ('F3',)),
+ (61, ('F4',)),
+ (62, ('F5',)),
+ (63, ('F6',)),
+ (64, ('F7',)),
+ (65, ('F8',)),
+ (66, ('F9',)),
+ (67, ('F10',)),
+ (68, ('F11',)),
+ (69, ('F12',)),
+ (104, ('F13',)),
+ (105, ('F14',)),
+ (106, ('F15',)),
+ (107, ('F16',)),
+ (108, ('F17',)),
+ (109, ('F18',)),
+ (110, ('F19',)),
+ (111, ('F20',)),
+ (112, ('F21',)),
+ (113, ('F22',)),
+ (114, ('F23',)),
+ (115, ('F24',)),
+ )
+
+ for code, names in codes:
+ if candidate in names:
+ return make_key(code=code, names=names)
+
+
+def maybe_make_navlock_key(candidate):
+ codes = (
+ (57, ('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')),
+ # FIXME: Investigate whether this key actually works, and
+ # uncomment when/if it does.
+ # (130, ('LOCKING_CAPS', 'LCAP')),
+ (70, ('PRINT_SCREEN', 'PSCREEN', 'PSCR')),
+ (71, ('SCROLL_LOCK', 'SCROLLLOCK', 'SLCK')),
+ # FIXME: Investigate whether this key actually works, and
+ # uncomment when/if it does.
+ # (132, ('LOCKING_SCROLL', 'LSCRL')),
+ (72, ('PAUSE', 'PAUS', 'BRK')),
+ (73, ('INSERT', 'INS')),
+ (74, ('HOME',)),
+ (75, ('PGUP',)),
+ (76, ('DELETE', 'DEL')),
+ (77, ('END',)),
+ (78, ('PGDOWN', 'PGDN')),
+ (79, ('RIGHT', 'RGHT')),
+ (80, ('LEFT',)),
+ (81, ('DOWN',)),
+ (82, ('UP',)),
+ )
+
+ for code, names in codes:
+ if candidate in names:
+ return make_key(code=code, names=names)
+
+
+def maybe_make_numpad_key(candidate):
+ codes = (
+ (83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')),
+ (84, ('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')),
+ (85, ('KP_ASTERISK', 'NUMPAD_ASTERISK', 'PAST')),
+ (86, ('KP_MINUS', 'NUMPAD_MINUS', 'PMNS')),
+ (87, ('KP_PLUS', 'NUMPAD_PLUS', 'PPLS')),
+ (88, ('KP_ENTER', 'NUMPAD_ENTER', 'PENT')),
+ (89, ('KP_1', 'P1', 'NUMPAD_1')),
+ (90, ('KP_2', 'P2', 'NUMPAD_2')),
+ (91, ('KP_3', 'P3', 'NUMPAD_3')),
+ (92, ('KP_4', 'P4', 'NUMPAD_4')),
+ (93, ('KP_5', 'P5', 'NUMPAD_5')),
+ (94, ('KP_6', 'P6', 'NUMPAD_6')),
+ (95, ('KP_7', 'P7', 'NUMPAD_7')),
+ (96, ('KP_8', 'P8', 'NUMPAD_8')),
+ (97, ('KP_9', 'P9', 'NUMPAD_9')),
+ (98, ('KP_0', 'P0', 'NUMPAD_0')),
+ (99, ('KP_DOT', 'PDOT', 'NUMPAD_DOT')),
+ (103, ('KP_EQUAL', 'PEQL', 'NUMPAD_EQUAL')),
+ (133, ('KP_COMMA', 'PCMM', 'NUMPAD_COMMA')),
+ (134, ('KP_EQUAL_AS400', 'NUMPAD_EQUAL_AS400')),
+ )
+
+ for code, names in codes:
+ if candidate in names:
+ return make_key(code=code, names=names)
+
+
+def maybe_make_shifted_key(candidate):
+ codes = (
+ (30, ('EXCLAIM', 'EXLM', '!')),
+ (31, ('AT', '@')),
+ (32, ('HASH', 'POUND', '#')),
+ (33, ('DOLLAR', 'DLR', '$')),
+ (34, ('PERCENT', 'PERC', '%')),
+ (35, ('CIRCUMFLEX', 'CIRC', '^')),
+ (36, ('AMPERSAND', 'AMPR', '&')),
+ (37, ('ASTERISK', 'ASTR', '*')),
+ (38, ('LEFT_PAREN', 'LPRN', '(')),
+ (39, ('RIGHT_PAREN', 'RPRN', ')')),
+ (45, ('UNDERSCORE', 'UNDS', '_')),
+ (46, ('PLUS', '+')),
+ (47, ('LEFT_CURLY_BRACE', 'LCBR', '{')),
+ (48, ('RIGHT_CURLY_BRACE', 'RCBR', '}')),
+ (49, ('PIPE', '|')),
+ (51, ('COLON', 'COLN', ':')),
+ (52, ('DOUBLE_QUOTE', 'DQUO', 'DQT', '"')),
+ (53, ('TILDE', 'TILD', '~')),
+ (54, ('LEFT_ANGLE_BRACKET', 'LABK', '<')),
+ (55, ('RIGHT_ANGLE_BRACKET', 'RABK', '>')),
+ (56, ('QUESTION', 'QUES', '?')),
+ )
+
+ for code, names in codes:
+ if candidate in names:
+ return make_key(code=code, names=names, has_modifiers={KC.LSFT.code})
+
+
+def maybe_make_international_key(candidate):
+ codes = (
+ (50, ('NONUS_HASH', 'NUHS')),
+ (100, ('NONUS_BSLASH', 'NUBS')),
+ (101, ('APP', 'APPLICATION', 'SEL', 'WINMENU')),
+ (135, ('INT1', 'RO')),
+ (136, ('INT2', 'KANA')),
+ (137, ('INT3', 'JYEN')),
+ (138, ('INT4', 'HENK')),
+ (139, ('INT5', 'MHEN')),
+ (140, ('INT6',)),
+ (141, ('INT7',)),
+ (142, ('INT8',)),
+ (143, ('INT9',)),
+ (144, ('LANG1', 'HAEN')),
+ (145, ('LANG2', 'HAEJ')),
+ (146, ('LANG3',)),
+ (147, ('LANG4',)),
+ (148, ('LANG5',)),
+ (149, ('LANG6',)),
+ (150, ('LANG7',)),
+ (151, ('LANG8',)),
+ (152, ('LANG9',)),
+ )
+
+ for code, names in codes:
+ if candidate in names:
+ return make_key(code=code, names=names)
+
+
+def maybe_make_unicode_key(candidate):
+ keys = (
+ (
+ ('UC_MODE_NOOP', 'UC_DISABLE'),
+ handlers.uc_mode_pressed,
+ UnicodeModeKeyMeta(UnicodeMode.NOOP),
+ ),
+ (
+ ('UC_MODE_LINUX', 'UC_MODE_IBUS'),
+ handlers.uc_mode_pressed,
+ UnicodeModeKeyMeta(UnicodeMode.IBUS),
+ ),
+ (
+ ('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'),
+ handlers.uc_mode_pressed,
+ UnicodeModeKeyMeta(UnicodeMode.RALT),
+ ),
+ (
+ ('UC_MODE_WINC',),
+ handlers.uc_mode_pressed,
+ UnicodeModeKeyMeta(UnicodeMode.WINC),
+ ),
+ )
+
+ for names, handler, meta in keys:
+ if candidate in names:
+ return make_key(names=names, on_press=handler, meta=meta)
+
+ if candidate in ('UC_MODE',):
+ return make_argumented_key(
+ names=('UC_MODE',),
+ validator=unicode_mode_key_validator,
+ on_press=handlers.uc_mode_pressed,
+ )
+
+
+def maybe_make_firmware_key(candidate):
+ keys = (
+ ((('BLE_REFRESH',), handlers.ble_refresh)),
+ ((('BOOTLOADER',), handlers.bootloader)),
+ ((('DEBUG', 'DBG'), handlers.debug_pressed)),
+ ((('HID_SWITCH', 'HID'), handlers.hid_switch)),
+ ((('RELOAD', 'RLD'), handlers.reload)),
+ ((('RESET',), handlers.reset)),
+ )
+
+ for names, handler in keys:
+ if candidate in names:
+ return make_key(names=names, on_press=handler)
+
+
+KEY_GENERATORS = (
+ maybe_make_no_key,
+ maybe_make_alpha_key,
+ maybe_make_numeric_key,
+ maybe_make_firmware_key,
+ maybe_make_key(
+ None,
+ ('BKDL',),
+ on_press=handlers.bkdl_pressed,
+ on_release=handlers.bkdl_released,
+ ),
+ maybe_make_key(
+ None,
+ ('GESC', 'GRAVE_ESC'),
+ on_press=handlers.gesc_pressed,
+ on_release=handlers.gesc_released,
+ ),
+ # A dummy key to trigger a sleep_ms call in a sequence of other keys in a
+ # simple sequence macro.
+ maybe_make_argumented_key(
+ key_seq_sleep_validator,
+ ('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'),
+ on_press=handlers.sleep_pressed,
+ ),
+ maybe_make_mod_key,
+ # More ASCII standard keys
+ maybe_make_more_ascii,
+ # Function Keys
+ maybe_make_fn_key,
+ # Lock Keys, Navigation, etc.
+ maybe_make_navlock_key,
+ # Numpad
+ # FIXME: Investigate whether this key actually works, and
+ # uncomment when/if it does.
+ # maybe_make_key(131, ('LOCKING_NUM', 'LNUM')),
+ maybe_make_numpad_key,
+ # Making life better for folks on tiny keyboards especially: exposes
+ # the 'shifted' keys as raw keys. Under the hood we're still
+ # sending Shift+(whatever key is normally pressed) to get these, so
+ # for example `KC_AT` will hold shift and press 2.
+ maybe_make_shifted_key,
+ # International
+ maybe_make_international_key,
+ maybe_make_unicode_key,
+)
+
+
+class KeyAttrDict:
+ __cache = {}
+
+ def __iter__(self):
+ return self.__cache.__iter__()
+
+ def __setitem__(self, key, value):
+ self.__cache.__setitem__(key, value)
+
+ def __getattr__(self, 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):
+ try:
+ return self.__cache[key]
+ except KeyError:
+ pass
+
+ for func in KEY_GENERATORS:
+ maybe_key = func(key)
+ if maybe_key:
+ break
+ else:
+ raise ValueError(f'Invalid key: {key}')
+
+ if debug.enabled:
+ debug(f'{key}: {maybe_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 f'Key(code={self.code}, has_modifiers={self.has_modifiers})'
+
+ def on_press(self, state, coord_int=None):
+ if hasattr(self, '_pre_press_handlers'):
+ for fn in self._pre_press_handlers:
+ if not fn(self, state, KC, coord_int):
+ return None
+
+ ret = self._handle_press(self, state, KC, coord_int)
+
+ if hasattr(self, '_post_press_handlers'):
+ for fn in self._post_press_handlers:
+ fn(self, state, KC, coord_int)
+
+ return ret
+
+ def on_release(self, state, coord_int=None):
+ if hasattr(self, '_pre_release_handlers'):
+ for fn in self._pre_release_handlers:
+ if not fn(self, state, KC, coord_int):
+ return None
+
+ ret = self._handle_release(self, state, KC, coord_int)
+
+ if hasattr(self, '_post_release_handlers'):
+ for fn in self._post_release_handlers:
+ fn(self, state, KC, coord_int)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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 f'ModifierKey(code={self.code}, has_modifiers={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
+
+ 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
A => kmk/kmk_keyboard.py +521 -0
@@ 1,521 @@
+try:
+ from typing import Callable, Optional, Tuple
+except ImportError:
+ pass
+
+from supervisor import ticks_ms
+
+from keypad import Event as KeyEvent
+
+from kmk.consts import UnicodeMode
+from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
+from kmk.keys import KC, Key
+from kmk.kmktime import ticks_add, ticks_diff
+from kmk.modules import Module
+from kmk.scanners.keypad import MatrixScanner
+from kmk.utils import Debug
+
+debug = Debug(__name__)
+
+
+class Sandbox:
+ matrix_update = None
+ secondary_matrix_update = None
+ active_layers = None
+
+
+class KMKKeyboard:
+ #####
+ # User-configurable
+ keymap = []
+ coord_mapping = None
+
+ row_pins = None
+ col_pins = None
+ diode_orientation = None
+ matrix = None
+
+ unicode_mode = UnicodeMode.NOOP
+
+ modules = []
+ extensions = []
+ sandbox = Sandbox()
+
+ #####
+ # Internal State
+ keys_pressed = set()
+ _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 = []
+ state_changed = False
+ _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) -> None:
+ return ''.join(
+ [
+ 'KMKKeyboard(\n',
+ f' debug_enabled={self.debug_enabled}, ',
+ f'diode_orientation={self.diode_orientation}, ',
+ f'matrix={self.matrix},\n',
+ f' unicode_mode={self.unicode_mode}, ',
+ f'_hid_helper={self._hid_helper},\n',
+ f' keys_pressed={self.keys_pressed},\n',
+ f' _coordkeys_pressed={self._coordkeys_pressed},\n',
+ f' hid_pending={self.hid_pending}, ',
+ f'active_layers={self.active_layers}, ',
+ f'_timeouts={self._timeouts}\n',
+ ')',
+ ]
+ )
+
+ def _print_debug_cycle(self, init: bool = False) -> None:
+ if debug.enabled:
+ debug(f'coordkeys_pressed={self._coordkeys_pressed}')
+ debug(f'keys_pressed={self.keys_pressed}')
+
+ def _send_hid(self) -> None:
+ if self._hid_send_enabled:
+ hid_report = self._hid_helper.create_report(self.keys_pressed)
+ try:
+ hid_report.send()
+ except KeyError as e:
+ if debug.enabled:
+ debug(f'HidNotFound(HIDReportType={e})')
+ self.hid_pending = False
+
+ def _handle_matrix_report(self, kevent: KeyEvent) -> None:
+ if kevent is not None:
+ self._on_matrix_changed(kevent)
+ self.state_changed = True
+
+ def _find_key_in_map(self, int_coord: int) -> Key:
+ try:
+ idx = self.coord_mapping.index(int_coord)
+ except ValueError:
+ if debug.enabled:
+ debug(f'CoordMappingNotFound(ic={int_coord})')
+
+ return None
+
+ for layer in self.active_layers:
+ try:
+ layer_key = self.keymap[layer][idx]
+ except IndexError:
+ layer_key = None
+ if debug.enabled:
+ debug(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: KeyEvent) -> None:
+ int_coord = kevent.key_number
+ is_pressed = kevent.pressed
+ if debug.enabled:
+ debug(f'MatrixChange(ic={int_coord}, pressed={is_pressed})')
+
+ key = None
+ if not is_pressed:
+ try:
+ key = self._coordkeys_pressed[int_coord]
+ except KeyError:
+ if debug.enabled:
+ debug(f'KeyNotPressed(ic={int_coord})')
+
+ if key is None:
+ key = self._find_key_in_map(int_coord)
+
+ if key is None:
+ if debug.enabled:
+ debug(f'MatrixUndefinedCoordinate(ic={int_coord})')
+ return self
+
+ if debug.enabled:
+ debug(f'KeyResolution(key={key})')
+
+ self.pre_process_key(key, is_pressed, int_coord)
+
+ @property
+ def debug_enabled(self) -> bool:
+ return debug.enabled
+
+ @debug_enabled.setter
+ def debug_enabled(self, enabled: bool):
+ debug.enabled = enabled
+
+ def pre_process_key(
+ self,
+ key: Key,
+ is_pressed: bool,
+ int_coord: Optional[int] = None,
+ index: int = 0,
+ ) -> None:
+ for module in self.modules[index:]:
+ try:
+ key = module.process_key(self, key, is_pressed, int_coord)
+ if key is None:
+ break
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.process_key: {err}')
+
+ if int_coord is not None:
+ if is_pressed:
+ self._coordkeys_pressed[int_coord] = key
+ else:
+ try:
+ del self._coordkeys_pressed[int_coord]
+ except KeyError:
+ if debug.enabled:
+ debug(f'ReleaseKeyError(ic={int_coord})')
+
+ if key:
+ self.process_key(key, is_pressed, int_coord)
+
+ def process_key(
+ self, key: Key, is_pressed: bool, coord_int: Optional[int] = None
+ ) -> None:
+ if is_pressed:
+ key.on_press(self, coord_int)
+ else:
+ key.on_release(self, coord_int)
+
+ def resume_process_key(
+ self,
+ module: Module,
+ key: Key,
+ is_pressed: bool,
+ int_coord: Optional[int] = None,
+ ) -> None:
+ index = self.modules.index(module) + 1
+ self.pre_process_key(key, is_pressed, int_coord, index)
+
+ def remove_key(self, keycode: Key) -> None:
+ self.keys_pressed.discard(keycode)
+ self.process_key(keycode, False)
+
+ def add_key(self, keycode: Key) -> None:
+ self.keys_pressed.add(keycode)
+ self.process_key(keycode, True)
+
+ def tap_key(self, keycode: Key) -> None:
+ self.add_key(keycode)
+ # On the next cycle, we'll remove the key.
+ self.set_timeout(False, lambda: self.remove_key(keycode))
+
+ def set_timeout(
+ self, after_ticks: int, callback: Callable[[None], None]
+ ) -> Tuple[int, int]:
+ # 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: int) -> None:
+ try:
+ self._timeouts[timeout_key[0]][timeout_key[1]] = None
+ except (KeyError, IndexError):
+ if debug.enabled:
+ debug(f'no such timeout: {timeout_key}')
+
+ def _process_timeouts(self) -> None:
+ if not self._timeouts:
+ return
+
+ # 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)
+
+ if timeout_keys and debug.enabled:
+ debug('processing timeouts')
+
+ for k in sorted(timeout_keys):
+ for callback in self._timeouts[k]:
+ if callback:
+ callback()
+ del self._timeouts[k]
+
+ self._processing_timeouts = False
+
+ def _init_sanity_check(self) -> None:
+ '''
+ Ensure the provided configuration is *probably* bootable
+ '''
+ assert self.keymap, 'must define a keymap with at least one row'
+ assert (
+ self.hid_type in HIDModes.ALL_MODES
+ ), 'hid_type must be a value from kmk.consts.HIDModes'
+ if not self.matrix:
+ assert self.row_pins, 'no GPIO pins defined for matrix rows'
+ assert self.col_pins, 'no GPIO pins defined for matrix columns'
+ assert (
+ self.diode_orientation is not None
+ ), 'diode orientation must be defined'
+
+ def _init_coord_mapping(self) -> None:
+ '''
+ Attempt to sanely guess a coord_mapping if one is not provided. No-op
+ if `kmk.extensions.split.Split` is used, it provides equivalent
+ functionality in `on_bootup`
+
+ To save RAM on boards that don't use Split, we don't import Split
+ and do an isinstance check, but instead do string detection
+ '''
+ if any(x.__class__.__module__ == 'kmk.modules.split' for x in self.modules):
+ return
+
+ if not self.coord_mapping:
+ cm = []
+ for m in self.matrix:
+ cm.extend(m.coord_mapping)
+ self.coord_mapping = tuple(cm)
+
+ def _init_hid(self) -> None:
+ if self.hid_type == HIDModes.NOOP:
+ self._hid_helper = AbstractHID
+ elif self.hid_type == HIDModes.USB:
+ self._hid_helper = USBHID
+ elif self.hid_type == HIDModes.BLE:
+ self._hid_helper = BLEHID
+ else:
+ self._hid_helper = AbstractHID
+ self._hid_helper = self._hid_helper(**self._go_args)
+ self._hid_send_enabled = True
+
+ def _init_matrix(self) -> None:
+ if self.matrix is None:
+ if debug.enabled:
+ debug('Initialising default matrix scanner.')
+ self.matrix = MatrixScanner(
+ column_pins=self.col_pins,
+ row_pins=self.row_pins,
+ columns_to_anodes=self.diode_orientation,
+ )
+
+ try:
+ self.matrix = tuple(iter(self.matrix))
+ offset = 0
+ for matrix in self.matrix:
+ matrix.offset = offset
+ offset += matrix.key_count
+ except TypeError:
+ self.matrix = (self.matrix,)
+
+ def before_matrix_scan(self) -> None:
+ for module in self.modules:
+ try:
+ module.before_matrix_scan(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.before_matrix_scan: {err}')
+
+ for ext in self.extensions:
+ try:
+ ext.before_matrix_scan(self.sandbox)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {ext}.before_matrix_scan: {err}')
+
+ def after_matrix_scan(self) -> None:
+ for module in self.modules:
+ try:
+ module.after_matrix_scan(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.after_matrix_scan: {err}')
+
+ for ext in self.extensions:
+ try:
+ ext.after_matrix_scan(self.sandbox)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {ext}.after_matrix_scan: {err}')
+
+ def before_hid_send(self) -> None:
+ for module in self.modules:
+ try:
+ module.before_hid_send(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.before_hid_send: {err}')
+
+ for ext in self.extensions:
+ try:
+ ext.before_hid_send(self.sandbox)
+ except Exception as err:
+ if debug.enabled:
+ debug(
+ f'Error in {ext}.before_hid_send: {err}',
+ )
+
+ def after_hid_send(self) -> None:
+ for module in self.modules:
+ try:
+ module.after_hid_send(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.after_hid_send: {err}')
+
+ for ext in self.extensions:
+ try:
+ ext.after_hid_send(self.sandbox)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {ext}.after_hid_send: {err}')
+
+ def powersave_enable(self) -> None:
+ for module in self.modules:
+ try:
+ module.on_powersave_enable(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.on_powersave: {err}')
+
+ for ext in self.extensions:
+ try:
+ ext.on_powersave_enable(self.sandbox)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {ext}.powersave_enable: {err}')
+
+ def powersave_disable(self) -> None:
+ for module in self.modules:
+ try:
+ module.on_powersave_disable(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {module}.powersave_disable: {err}')
+ for ext in self.extensions:
+ try:
+ ext.on_powersave_disable(self.sandbox)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Error in {ext}.powersave_disable: {err}')
+
+ def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs) -> None:
+ self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
+ while True:
+ self._main_loop()
+
+ def _init(
+ self,
+ hid_type: HIDModes = HIDModes.USB,
+ secondary_hid_type: Optional[HIDModes] = None,
+ **kwargs,
+ ) -> None:
+ self._go_args = kwargs
+ self.hid_type = hid_type
+ self.secondary_hid_type = secondary_hid_type
+
+ 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 debug.enabled:
+ debug(f'Failed to load module {module}: {err}')
+ for ext in self.extensions:
+ try:
+ ext.during_bootup(self)
+ except Exception as err:
+ if debug.enabled:
+ debug(f'Failed to load extensions {module}: {err}')
+
+ if debug.enabled:
+ debug(f'init: {self}')
+
+ def _main_loop(self) -> None:
+ self.state_changed = False
+ self.sandbox.active_layers = self.active_layers.copy()
+
+ self.before_matrix_scan()
+
+ for matrix in self.matrix:
+ update = matrix.scan_for_changes()
+ if update:
+ self.matrix_update = update
+ break
+ self.sandbox.matrix_update = self.matrix_update
+ self.sandbox.secondary_matrix_update = self.secondary_matrix_update
+
+ self.after_matrix_scan()
+
+ if self.secondary_matrix_update:
+ self.matrix_update_queue.append(self.secondary_matrix_update)
+ self.secondary_matrix_update = None
+
+ if self.matrix_update:
+ self.matrix_update_queue.append(self.matrix_update)
+ self.matrix_update = None
+
+ # only handle one key per cycle.
+ if self.matrix_update_queue:
+ self._handle_matrix_report(self.matrix_update_queue.pop(0))
+
+ self.before_hid_send()
+
+ if self.hid_pending:
+ self._send_hid()
+
+ self._process_timeouts()
+
+ if self.hid_pending:
+ self._send_hid()
+ self.state_changed = True
+
+ 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()
A => kmk/kmktime.py +34 -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
A => kmk/modules/__init__.py +43 -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
A => kmk/modules/adns9800.py +241 -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
A => kmk/modules/capsword.py +99 -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()
A => kmk/modules/cg_swap.py +70 -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
A => kmk/modules/combos.py +335 -0
@@ 1,335 @@
+try:
+ from typing import Optional, Tuple, Union
+except ImportError:
+ pass
+from micropython import const
+
+import kmk.handlers.stock as handlers
+from kmk.keys import Key, make_key
+from kmk.kmk_keyboard import KMKKeyboard
+from kmk.modules import Module
+
+
+class _ComboState:
+ RESET = const(0)
+ MATCHING = const(1)
+ ACTIVE = const(2)
+ IDLE = const(3)
+
+
+class Combo:
+ fast_reset = False
+ per_key_timeout = False
+ timeout = 50
+ _remaining = []
+ _timeout = None
+ _state = _ComboState.IDLE
+ _match_coord = False
+
+ def __init__(
+ self,
+ match: Tuple[Union[Key, int], ...],
+ result: Key,
+ fast_reset=None,
+ per_key_timeout=None,
+ timeout=None,
+ match_coord=None,
+ ):
+ '''
+ match: tuple of keys (KC.A, KC.B)
+ result: key KC.C
+ '''
+ self.match = match
+ self.result = result
+ if fast_reset is not None:
+ self.fast_reset = fast_reset
+ if per_key_timeout is not None:
+ self.per_key_timeout = per_key_timeout
+ if timeout is not None:
+ self.timeout = timeout
+ if match_coord is not None:
+ self._match_coord = match_coord
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}({[k.code for k in self.match]})'
+
+ def matches(self, key: Key, int_coord: int):
+ raise NotImplementedError
+
+ def has_match(self, key: Key, int_coord: int):
+ return self._match_coord and int_coord in self.match or key in self.match
+
+ def insert(self, key: Key, int_coord: int):
+ if self._match_coord:
+ self._remaining.insert(0, int_coord)
+ else:
+ self._remaining.insert(0, key)
+
+ def reset(self):
+ self._remaining = list(self.match)
+
+
+class Chord(Combo):
+ def matches(self, key: Key, int_coord: int):
+ if not self._match_coord and key in self._remaining:
+ self._remaining.remove(key)
+ return True
+ elif self._match_coord and int_coord in self._remaining:
+ self._remaining.remove(int_coord)
+ return True
+ else:
+ return False
+
+
+class Sequence(Combo):
+ fast_reset = True
+ per_key_timeout = True
+ timeout = 1000
+
+ def matches(self, key: Key, int_coord: int):
+ if (
+ not self._match_coord and self._remaining and self._remaining[0] == key
+ ) or (
+ self._match_coord and self._remaining and self._remaining[0] == int_coord
+ ):
+ self._remaining.pop(0)
+ return True
+ else:
+ return False
+
+
+class Combos(Module):
+ def __init__(self, combos=[]):
+ self.combos = combos
+ self._key_buffer = []
+
+ make_key(
+ names=('LEADER', 'LDR'),
+ on_press=handlers.passthrough,
+ on_release=handlers.passthrough,
+ )
+
+ def during_bootup(self, keyboard):
+ self.reset(keyboard)
+
+ def before_matrix_scan(self, keyboard):
+ return
+
+ def after_matrix_scan(self, keyboard):
+ return
+
+ def before_hid_send(self, keyboard):
+ return
+
+ def after_hid_send(self, keyboard):
+ return
+
+ def on_powersave_enable(self, keyboard):
+ return
+
+ def on_powersave_disable(self, keyboard):
+ return
+
+ def process_key(self, keyboard, key: Key, is_pressed, int_coord):
+ if is_pressed:
+ return self.on_press(keyboard, key, int_coord)
+ else:
+ return self.on_release(keyboard, key, int_coord)
+
+ def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
+ # refill potential matches from timed-out matches
+ if self.count_matching() == 0:
+ for combo in self.combos:
+ if combo._state == _ComboState.RESET:
+ combo._state = _ComboState.MATCHING
+
+ # filter potential matches
+ for combo in self.combos:
+ if combo._state != _ComboState.MATCHING:
+ continue
+ if combo.matches(key, int_coord):
+ continue
+ combo._state = _ComboState.IDLE
+ if combo._timeout:
+ keyboard.cancel_timeout(combo._timeout)
+ combo._timeout = keyboard.set_timeout(
+ combo.timeout, lambda c=combo: self.reset_combo(keyboard, c)
+ )
+
+ match_count = self.count_matching()
+
+ if match_count:
+ # At least one combo matches current key: append key to buffer.
+ self._key_buffer.append((int_coord, key, True))
+ key = None
+
+ for first_match in self.combos:
+ if first_match._state == _ComboState.MATCHING:
+ break
+
+ # Single match left: don't wait on timeout to activate
+ if match_count == 1 and not any(first_match._remaining):
+ combo = first_match
+ self.activate(keyboard, combo)
+ if combo._timeout:
+ keyboard.cancel_timeout(combo._timeout)
+ combo._timeout = None
+ self._key_buffer = []
+ self.reset(keyboard)
+
+ # Start or reset individual combo timeouts.
+ for combo in self.combos:
+ if combo._state != _ComboState.MATCHING:
+ continue
+ if combo._timeout:
+ if combo.per_key_timeout:
+ keyboard.cancel_timeout(combo._timeout)
+ else:
+ continue
+ combo._timeout = keyboard.set_timeout(
+ combo.timeout, lambda c=combo: self.on_timeout(keyboard, c)
+ )
+ else:
+ # There's no matching combo: send and reset key buffer
+ self.send_key_buffer(keyboard)
+ self._key_buffer = []
+ if int_coord is not None:
+ key = keyboard._find_key_in_map(int_coord)
+
+ return key
+
+ def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
+ for combo in self.combos:
+ if combo._state != _ComboState.ACTIVE:
+ continue
+ if combo.has_match(key, int_coord):
+ # Deactivate combo if it matches current key.
+ self.deactivate(keyboard, combo)
+
+ if combo.fast_reset:
+ self.reset_combo(keyboard, combo)
+ self._key_buffer = []
+ else:
+ combo.insert(key, int_coord)
+ combo._state = _ComboState.MATCHING
+
+ key = combo.result
+ break
+
+ else:
+ # Non-active but matching combos can either activate on key release
+ # if they're the only match, or "un-match" the released key but stay
+ # matching if they're a repeatable combo.
+ for combo in self.combos:
+ if combo._state != _ComboState.MATCHING:
+ continue
+ if not combo.has_match(key, int_coord):
+ continue
+
+ # Combo matches, but first key released before timeout.
+ elif not any(combo._remaining) and self.count_matching() == 1:
+ keyboard.cancel_timeout(combo._timeout)
+ self.activate(keyboard, combo)
+ self._key_buffer = []
+ keyboard._send_hid()
+ self.deactivate(keyboard, combo)
+ if combo.fast_reset:
+ self.reset_combo(keyboard, combo)
+ else:
+ combo.insert(key, int_coord)
+ combo._state = _ComboState.MATCHING
+ self.reset(keyboard)
+
+ elif not any(combo._remaining):
+ continue
+
+ # Skip combos that allow tapping.
+ elif combo.fast_reset:
+ continue
+
+ # This was the last key released of a repeatable combo.
+ elif len(combo._remaining) == len(combo.match) - 1:
+ self.reset_combo(keyboard, combo)
+ if not self.count_matching():
+ self.send_key_buffer(keyboard)
+ self._key_buffer = []
+
+ # Anything between first and last key released.
+ else:
+ combo.insert(key, int_coord)
+
+ # Don't propagate key-release events for keys that have been
+ # buffered. Append release events only if corresponding press is in
+ # buffer.
+ pressed = self._key_buffer.count((int_coord, key, True))
+ released = self._key_buffer.count((int_coord, key, False))
+ if (pressed - released) > 0:
+ self._key_buffer.append((int_coord, key, False))
+ key = None
+
+ # Reset on non-combo key up
+ if not self.count_matching():
+ self.reset(keyboard)
+
+ return key
+
+ def on_timeout(self, keyboard, combo):
+ # If combo reaches timeout and has no remaining keys, activate it;
+ # else, drop it from the match list.
+ combo._timeout = None
+
+ if not any(combo._remaining):
+ self.activate(keyboard, combo)
+ # check if the last buffered key event was a 'release'
+ if not self._key_buffer[-1][2]:
+ keyboard._send_hid()
+ self.deactivate(keyboard, combo)
+ self._key_buffer = []
+ self.reset(keyboard)
+ else:
+ if self.count_matching() == 1:
+ # This was the last pending combo: flush key buffer.
+ self.send_key_buffer(keyboard)
+ self._key_buffer = []
+ self.reset_combo(keyboard, combo)
+
+ def send_key_buffer(self, keyboard):
+ for (int_coord, key, is_pressed) in self._key_buffer:
+ new_key = None
+ if not is_pressed:
+ 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.resume_process_key(self, new_key, is_pressed, int_coord)
+ keyboard._send_hid()
+
+ def activate(self, keyboard, combo):
+ combo.result.on_press(keyboard)
+ combo._state = _ComboState.ACTIVE
+
+ def deactivate(self, keyboard, combo):
+ combo.result.on_release(keyboard)
+ combo._state = _ComboState.IDLE
+
+ def reset_combo(self, keyboard, combo):
+ combo.reset()
+ if combo._timeout is not None:
+ keyboard.cancel_timeout(combo._timeout)
+ combo._timeout = None
+ combo._state = _ComboState.RESET
+
+ def reset(self, keyboard):
+ for combo in self.combos:
+ if combo._state != _ComboState.ACTIVE:
+ self.reset_combo(keyboard, combo)
+
+ def count_matching(self):
+ match_count = 0
+ for combo in self.combos:
+ if combo._state == _ComboState.MATCHING:
+ match_count += 1
+ return match_count
A => kmk/modules/dynamic_sequences.py +259 -0
@@ 1,259 @@
+from micropython import const
+from supervisor import ticks_ms
+
+from collections import namedtuple
+
+from kmk.keys import KC, make_argumented_key
+from kmk.kmktime import check_deadline, ticks_diff
+from kmk.modules import Module
+
+
+class DSMeta:
+ def __init__(self, sequence_select=None):
+ self.sequence_select = sequence_select
+
+
+class SequenceStatus:
+ STOPPED = const(0)
+ RECORDING = const(1)
+ PLAYING = const(2)
+ SET_REPEPITIONS = const(3)
+ SET_INTERVAL = const(4)
+
+
+# Keycodes for number keys
+_numbers = range(KC.N1.code, KC.N0.code + 1)
+
+SequenceFrame = namedtuple('SequenceFrame', ['keys_pressed', 'timestamp'])
+
+
+class Sequence:
+ def __init__(self):
+ self.repetitions = 1
+ self.interval = 0
+ self.sequence_data = [SequenceFrame(set(), 0) for i in range(3)]
+
+
+class DynamicSequences(Module):
+ def __init__(
+ self, slots=1, timeout=60000, key_interval=0, use_recorded_speed=False
+ ):
+ self.sequences = [Sequence() for i in range(slots)]
+ self.current_slot = self.sequences[0]
+ self.status = SequenceStatus.STOPPED
+
+ self.index = 0
+ self.start_time = 0
+ self.current_repetition = 0
+ self.last_config_frame = set()
+
+ self.timeout = timeout
+ self.key_interval = key_interval
+ self.use_recorded_speed = use_recorded_speed
+
+ # Create keycodes
+ make_argumented_key(
+ validator=DSMeta, names=('RECORD_SEQUENCE',), on_press=self._record_sequence
+ )
+
+ make_argumented_key(
+ validator=DSMeta, names=('PLAY_SEQUENCE',), on_press=self._play_sequence
+ )
+
+ make_argumented_key(
+ validator=DSMeta,
+ names=(
+ 'SET_SEQUENCE',
+ 'STOP_SEQUENCE',
+ ),
+ on_press=self._stop_sequence,
+ )
+
+ make_argumented_key(
+ validator=DSMeta,
+ names=('SET_SEQUENCE_REPETITIONS',),
+ on_press=self._set_sequence_repetitions,
+ )
+
+ make_argumented_key(
+ validator=DSMeta,
+ names=('SET_SEQUENCE_INTERVAL',),
+ on_press=self._set_sequence_interval,
+ )
+
+ def _record_sequence(self, key, keyboard, *args, **kwargs):
+ self._stop_sequence(key, keyboard)
+ self.status = SequenceStatus.RECORDING
+ self.start_time = ticks_ms()
+ self.current_slot.sequence_data = [SequenceFrame(set(), 0)]
+ self.index = 0
+
+ def _play_sequence(self, key, keyboard, *args, **kwargs):
+ self._stop_sequence(key, keyboard)
+ self.status = SequenceStatus.PLAYING
+ self.start_time = ticks_ms()
+ self.index = 0
+ self.current_repetition = 0
+
+ def _stop_sequence(self, key, keyboard, *args, **kwargs):
+ if self.status == SequenceStatus.RECORDING:
+ self.stop_recording()
+ elif self.status == SequenceStatus.SET_INTERVAL:
+ self.stop_config()
+ self.status = SequenceStatus.STOPPED
+
+ # Change sequences here because stop is always called
+ if key.meta.sequence_select is not None:
+ self.current_slot = self.sequences[key.meta.sequence_select]
+
+ # Configure repeat settings
+ def _set_sequence_repetitions(self, key, keyboard, *args, **kwargs):
+ self._stop_sequence(key, keyboard)
+ self.status = SequenceStatus.SET_REPEPITIONS
+ self.last_config_frame = set()
+ self.current_slot.repetitions = 0
+ self.start_time = ticks_ms()
+
+ def _set_sequence_interval(self, key, keyboard, *args, **kwargs):
+ self._stop_sequence(key, keyboard)
+ self.status = SequenceStatus.SET_INTERVAL
+ self.last_config_frame = set()
+ self.current_slot.interval = 0
+ self.start_time = ticks_ms()
+
+ # Add the current keypress state to the sequence
+ def record_frame(self, keys_pressed):
+ if self.current_slot.sequence_data[self.index].keys_pressed != keys_pressed:
+ self.index += 1
+
+ # Recorded speed
+ if self.use_recorded_speed:
+ self.current_slot.sequence_data.append(
+ SequenceFrame(
+ keys_pressed.copy(), ticks_diff(ticks_ms(), self.start_time)
+ )
+ )
+
+ # Constant speed
+ else:
+ self.current_slot.sequence_data.append(
+ SequenceFrame(keys_pressed.copy(), self.index * self.key_interval)
+ )
+
+ if not check_deadline(ticks_ms(), self.start_time, self.timeout):
+ self.stop_recording()
+
+ # Add the ending frames to the sequence
+ def stop_recording(self):
+ # Clear the remaining keys
+ self.current_slot.sequence_data.append(
+ SequenceFrame(set(), self.current_slot.sequence_data[-1].timestamp + 20)
+ )
+
+ # Wait for the specified interval
+ prev_timestamp = self.current_slot.sequence_data[-1].timestamp
+ self.current_slot.sequence_data.append(
+ SequenceFrame(
+ set(),
+ prev_timestamp + self.current_slot.interval * 1000,
+ )
+ )
+
+ self.status = SequenceStatus.STOPPED
+
+ def play_frame(self, keyboard):
+ # Send the keypresses at this point in the sequence
+ if not check_deadline(
+ ticks_ms(),
+ self.start_time,
+ self.current_slot.sequence_data[self.index].timestamp,
+ ):
+ if self.index:
+ prev = self.current_slot.sequence_data[self.index - 1].keys_pressed
+ cur = self.current_slot.sequence_data[self.index].keys_pressed
+
+ for key in prev.difference(cur):
+ keyboard.remove_key(key)
+ for key in cur.difference(prev):
+ keyboard.add_key(key)
+
+ self.index += 1
+ if self.index >= len(self.current_slot.sequence_data): # Reached the end
+ self.current_repetition += 1
+ if self.current_repetition == self.current_slot.repetitions:
+ self.status = SequenceStatus.STOPPED
+ else:
+ self.index = 0
+ self.start_time = ticks_ms()
+
+ # Configuration for repeating sequences
+ def config_mode(self, keyboard):
+ for key in keyboard.keys_pressed.difference(self.last_config_frame):
+ if key.code in _numbers:
+ digit = (key.code - KC.N1.code + 1) % 10
+ if self.status == SequenceStatus.SET_REPEPITIONS:
+ self.current_slot.repetitions = (
+ self.current_slot.repetitions * 10 + digit
+ )
+ elif self.status == SequenceStatus.SET_INTERVAL:
+ self.current_slot.interval = self.current_slot.interval * 10 + digit
+
+ elif key.code == KC.ENTER.code:
+ self.stop_config()
+
+ self.last_config_frame = keyboard.keys_pressed.copy()
+ keyboard.hid_pending = False # Disable typing
+
+ if not check_deadline(ticks_ms(), self.start_time, self.timeout):
+ self.stop_config()
+
+ # Finish configuring repetitions
+ def stop_config(self):
+ self.current_slot.sequence_data[-1] = SequenceFrame(
+ self.current_slot.sequence_data[-1].keys_pressed,
+ self.current_slot.sequence_data[-2].timestamp
+ + self.current_slot.interval * 1000,
+ )
+ self.current_slot.repetitions = max(self.current_slot.repetitions, 1)
+ self.status = SequenceStatus.STOPPED
+
+ def on_runtime_enable(self, keyboard):
+ return
+
+ def on_runtime_disable(self, keyboard):
+ return
+
+ def during_bootup(self, keyboard):
+ return
+
+ def before_matrix_scan(self, keyboard):
+ return
+
+ def after_matrix_scan(self, keyboard):
+ return
+
+ def before_hid_send(self, keyboard):
+
+ if not self.status:
+ return
+
+ elif self.status == SequenceStatus.RECORDING:
+ self.record_frame(keyboard.keys_pressed)
+
+ elif self.status == SequenceStatus.PLAYING:
+ self.play_frame(keyboard)
+
+ elif (
+ self.status == SequenceStatus.SET_REPEPITIONS
+ or self.status == SequenceStatus.SET_INTERVAL
+ ):
+ self.config_mode(keyboard)
+
+ def after_hid_send(self, keyboard):
+ return
+
+ def on_powersave_enable(self, keyboard):
+ return
+
+ def on_powersave_disable(self, keyboard):
+ return
A => kmk/modules/easypoint.py +145 -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()
A => kmk/modules/encoder.py +312 -0
@@ 1,312 @@
+# See docs/encoder.md for how to use
+
+import busio
+import digitalio
+from supervisor import ticks_ms
+
+from kmk.modules import Module
+
+# NB : not using rotaryio as it requires the pins to be consecutive
+
+
+class BaseEncoder:
+
+ VELOCITY_MODE = True
+
+ def __init__(self, is_inverted=False, divisor=4):
+
+ self.is_inverted = is_inverted
+ self.divisor = divisor
+
+ self._state = None
+ self._start_state = None
+ self._direction = None
+ self._pos = 0
+ self._button_state = True
+ self._button_held = None
+ self._velocity = 0
+
+ self._movement = 0
+ self._timestamp = ticks_ms()
+
+ # callback functions on events. Need to be defined externally
+ self.on_move_do = None
+ self.on_button_do = None
+
+ def get_state(self):
+ return {
+ 'direction': self.is_inverted and -self._direction or self._direction,
+ 'position': self.is_inverted and -self._pos or self._pos,
+ 'is_pressed': not self._button_state,
+ 'velocity': self._velocity,
+ }
+
+ # Called in a loop to refresh encoder state
+
+ def update_state(self):
+ # Rotation events
+ new_state = (self.pin_a.get_value(), self.pin_b.get_value())
+
+ if new_state != self._state:
+ # encoder moved
+ self._movement += 1
+ # false / false and true / true are common half steps
+ # looking on the step just before helps determining
+ # the direction
+ if new_state[0] == new_state[1] and self._state[0] != self._state[1]:
+ if new_state[1] == self._state[0]:
+ self._direction = 1
+ else:
+ self._direction = -1
+
+ # when the encoder settles on a position (every 2 steps)
+ if new_state[0] == new_state[1]:
+ # an encoder returned to the previous
+ # position halfway, cancel rotation
+ if (
+ self._start_state[0] == new_state[0]
+ and self._start_state[1] == new_state[1]
+ and self._movement <= 2
+ ):
+ self._movement = 0
+ self._direction = 0
+
+ # when the encoder made a full loop according to its divisor
+ elif self._movement >= self.divisor - 1:
+ # 1 full step is 4 movements (2 for high-resolution encoder),
+ # however, when rotated quickly, some steps may be missed.
+ # This makes it behave more naturally
+ real_movement = self._movement // self.divisor
+ self._pos += self._direction * real_movement
+ if self.on_move_do is not None:
+ for i in range(real_movement):
+ self.on_move_do(self.get_state())
+
+ # Rotation finished, reset to identify new movement
+ self._movement = 0
+ self._direction = 0
+ self._start_state = new_state
+
+ self._state = new_state
+
+ # Velocity
+ self.velocity_event()
+
+ # Button event
+ self.button_event()
+
+ def velocity_event(self):
+ if self.VELOCITY_MODE:
+ new_timestamp = ticks_ms()
+ self._velocity = new_timestamp - self._timestamp
+ self._timestamp = new_timestamp
+
+ def button_event(self):
+ raise NotImplementedError('subclasses must override button_event()!')
+
+ # return knob velocity as milliseconds between position changes (detents)
+ # for backwards compatibility
+ def vel_report(self):
+ # print(self._velocity)
+ return self._velocity
+
+
+class GPIOEncoder(BaseEncoder):
+ def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False, divisor=None):
+ super().__init__(is_inverted)
+
+ # Divisor can be 4 or 2 depending on whether the detent
+ # on the encoder is defined by 2 or 4 pulses
+ self.divisor = divisor
+
+ self.pin_a = EncoderPin(pin_a)
+ self.pin_b = EncoderPin(pin_b)
+ self.pin_button = (
+ EncoderPin(pin_button, button_type=True) if pin_button is not None else None
+ )
+
+ self._state = (self.pin_a.get_value(), self.pin_b.get_value())
+ self._start_state = self._state
+
+ def button_event(self):
+ if self.pin_button:
+ new_button_state = self.pin_button.get_value()
+ if new_button_state != self._button_state:
+ self._button_state = new_button_state
+ if self.on_button_do is not None:
+ self.on_button_do(self.get_state())
+
+
+class EncoderPin:
+ def __init__(self, pin, button_type=False):
+ self.pin = pin
+ self.button_type = button_type
+ self.prepare_pin()
+
+ def prepare_pin(self):
+ if self.pin is not None:
+ self.io = digitalio.DigitalInOut(self.pin)
+ self.io.direction = digitalio.Direction.INPUT
+ self.io.pull = digitalio.Pull.UP
+ else:
+ self.io = None
+
+ def get_value(self):
+ return self.io.value
+
+
+class I2CEncoder(BaseEncoder):
+ def __init__(self, i2c, address, is_inverted=False):
+
+ try:
+ from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
+ except ImportError:
+ print('seesaw missing')
+ return
+
+ super().__init__(is_inverted)
+
+ self.seesaw = seesaw.Seesaw(i2c, address)
+
+ # Check for correct product
+
+ seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF
+ if seesaw_product != 4991:
+ print('Wrong firmware loaded? Expected 4991')
+
+ self.encoder = rotaryio.IncrementalEncoder(self.seesaw)
+ self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP)
+ self.switch = digitalio.DigitalIO(self.seesaw, 24)
+ self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1)
+
+ self._state = self.encoder.position
+
+ def update_state(self):
+
+ # Rotation events
+ new_state = self.encoder.position
+ if new_state != self._state:
+ # it moves !
+ self._movement += 1
+ # false / false and true / true are common half steps
+ # looking on the step just before helps determining
+ # the direction
+ if self.encoder.position > self._state:
+ self._direction = 1
+ else:
+ self._direction = -1
+ self._state = new_state
+ self.on_move_do(self.get_state())
+
+ # Velocity
+ self.velocity_event()
+
+ # Button events
+ self.button_event()
+
+ def button_event(self):
+ if not self.switch.value and not self._button_held:
+ # Pressed
+ self._button_held = True
+ if self.on_button_do is not None:
+ self.on_button_do(self.get_state())
+
+ if self.switch.value and self._button_held:
+ # Released
+ self._button_held = False
+
+ def get_state(self):
+ return {
+ 'direction': self.is_inverted and -self._direction or self._direction,
+ 'position': self._state,
+ 'is_pressed': not self.switch.value,
+ 'is_held': self._button_held,
+ 'velocity': self._velocity,
+ }
+
+
+class EncoderHandler(Module):
+ def __init__(self):
+ self.encoders = []
+ self.pins = None
+ self.map = None
+ self.divisor = 4
+
+ def on_runtime_enable(self, keyboard):
+ return
+
+ def on_runtime_disable(self, keyboard):
+ return
+
+ def during_bootup(self, keyboard):
+ if self.pins and self.map:
+ for idx, pins in enumerate(self.pins):
+ try:
+ # Check for busio.I2C
+ if isinstance(pins[0], busio.I2C):
+ new_encoder = I2CEncoder(*pins)
+
+ # Else fall back to GPIO
+ else:
+ new_encoder = GPIOEncoder(*pins)
+ # Set default divisor if unset
+ if new_encoder.divisor is None:
+ new_encoder.divisor = self.divisor
+
+ # In our case, we need to define keybord and encoder_id for callbacks
+ new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do(
+ keyboard, bound_idx, x
+ )
+ new_encoder.on_button_do = (
+ lambda x, bound_idx=idx: self.on_button_do(
+ keyboard, bound_idx, x
+ )
+ )
+ self.encoders.append(new_encoder)
+ except Exception as e:
+ print(e)
+ return
+
+ def on_move_do(self, keyboard, encoder_id, state):
+ if self.map:
+ layer_id = keyboard.active_layers[0]
+ # if Left, key index 0 else key index 1
+ if state['direction'] == -1:
+ key_index = 0
+ else:
+ key_index = 1
+ key = self.map[layer_id][encoder_id][key_index]
+ keyboard.tap_key(key)
+
+ def on_button_do(self, keyboard, encoder_id, state):
+ if state['is_pressed'] is True:
+ layer_id = keyboard.active_layers[0]
+ key = self.map[layer_id][encoder_id][2]
+ keyboard.tap_key(key)
+
+ def before_matrix_scan(self, keyboard):
+ '''
+ Return value will be injected as an extra matrix update
+ '''
+ for encoder in self.encoders:
+ encoder.update_state()
+
+ return keyboard
+
+ def after_matrix_scan(self, keyboard):
+ '''
+ Return value will be replace matrix update if supplied
+ '''
+ return
+
+ def before_hid_send(self, keyboard):
+ return
+
+ def after_hid_send(self, keyboard):
+ return
+
+ def on_powersave_enable(self, keyboard):
+ return
+
+ def on_powersave_disable(self, keyboard):