Initial attempt to merge internal_state with kmk_keyboard. Seems to work on Plank so far

This commit is contained in:
Josh Klar 2019-07-28 17:09:58 -07:00
parent ea327f8f76
commit 9821f7bcc3
No known key found for this signature in database
GPG Key ID: A4A0C7B4E8EEE222
15 changed files with 879 additions and 812 deletions

View File

@ -9,10 +9,3 @@ class UnicodeMode:
LINUX = IBUS = 1
MACOS = OSX = RALT = 2
WINC = 3
class LeaderMode:
TIMEOUT = 0
TIMEOUT_ACTIVE = 1
ENTER = 2
ENTER_ACTIVE = 3

View File

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

111
kmk/extensions/leader.py Normal file
View File

@ -0,0 +1,111 @@
import gc
from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import KC, make_key
class LeaderMode:
TIMEOUT = 0
TIMEOUT_ACTIVE = 1
ENTER = 2
ENTER_ACTIVE = 3
class Leader(Extension):
def __init__(self, mode=LeaderMode.TIMEOUT, timeout=1000, sequences=None):
if sequences is None:
raise InvalidExtensionEnvironment(
'sequences must be a dictionary, not None'
)
self._mode = mode
self._timeout = timeout
self._sequences = self._compile_sequences(sequences)
self._leader_pending = None
self._assembly_last_len = 0
self._sequence_assembly = []
make_key(
names=('LEADER', 'LEAD'),
on_press=self._key_leader_pressed,
on_release=handler_passthrough,
)
gc.collect()
def after_matrix_scan(self, keyboard_state, *args):
if self._mode % 2 == 1:
keys_pressed = keyboard_state._keys_pressed
if self._assembly_last_len and self._sequence_assembly:
history_set = set(self._sequence_assembly)
keys_pressed = keys_pressed - history_set
self._assembly_last_len = len(keyboard_state._keys_pressed)
for key in keys_pressed:
if self._mode == LeaderMode.ENTER_ACTIVE and key == KC.ENT:
self._handle_leader_sequence(keyboard_state)
break
elif key == KC.ESC or key == KC.GESC:
# Clean self and turn leader mode off.
self._exit_leader_mode(keyboard_state)
break
elif key == KC.LEAD:
break
else:
# Add key if not needing to escape
# This needs replaced later with a proper debounce
self._sequence_assembly.append(key)
keyboard_state._hid_pending = False
def _compile_sequences(self, sequences):
gc.collect()
for k, v in sequences.items():
if not isinstance(k, tuple):
new_key = tuple(KC[c] for c in k)
sequences[new_key] = v
for k, v in sequences.items():
if not isinstance(k, tuple):
del sequences[k]
gc.collect()
return sequences
def _handle_leader_sequence(self, keyboard_state):
lmh = tuple(self._sequence_assembly)
# Will get caught in infinite processing loops if we don't
# exit leader mode before processing the target key
self._exit_leader_mode(keyboard_state)
if lmh in self._sequences:
# Stack depth exceeded if try to use add_key here with a unicode sequence
keyboard_state._process_key(self._sequences[lmh], True)
keyboard_state._set_timeout(
False, lambda: keyboard_state._remove_key(self._sequences[lmh])
)
def _exit_leader_mode(self, keyboard_state):
self._sequence_assembly.clear()
self._mode -= 1
self._assembly_last_len = 0
keyboard_state._keys_pressed.clear()
def _key_leader_pressed(self, key, keyboard_state, *args, **kwargs):
if self._mode % 2 == 0:
keyboard_state._keys_pressed.discard(key)
# All leader modes are one number higher when activating
self._mode += 1
if self._mode == LeaderMode.TIMEOUT_ACTIVE:
keyboard_state._set_timeout(
self._timeout, lambda: self._handle_leader_sequence(keyboard_state)
)

103
kmk/extensions/split.py Normal file
View File

@ -0,0 +1,103 @@
import busio
import gc
from kmk.extensions import Extension
from kmk.kmktime import sleep_ms
from kmk.matrix import intify_coordinate
class SplitType:
UART = 1
I2C = 2 # unused
ONEWIRE = 3 # unused
BLE = 4 # unused
class Split(Extension):
def __init__(
self,
extra_data_pin=None,
offsets=(),
flip=False,
side=None,
stype=None,
master_left=True,
uart_flip=True,
uart_pin=None,
uart_timeout=20,
):
self.extra_data_pin = extra_data_pin
self.split_offsets = offsets
self.split_flip = flip
self.split_side = side
self.split_type = stype
self.split_master_left = master_left
self._uart = None
self.uart_flip = uart_flip
self.uart_pin = uart_pin
self.uart_timeout = uart_timeout
def during_bootup(self, keyboard):
if self.split_type is not None:
try:
# Working around https://github.com/adafruit/circuitpython/issues/1769
keyboard._hid_helper_inst.create_report([]).send()
self._is_master = True
# Sleep 2s so master portion doesn't "appear" to boot quicker than
# dependent portions (which will take ~2s to time out on the HID send)
sleep_ms(2000)
except OSError:
self._is_master = False
if self.split_flip and not self._is_master:
keyboard.col_pins = list(reversed(self.col_pins))
if self.split_side == 'Left':
self.split_master_left = self._is_master
elif self.split_side == 'Right':
self.split_master_left = not self._is_master
else:
self._is_master = True
if self.uart_pin is not None:
if self._is_master:
self._uart = busio.UART(
tx=None, rx=self.uart_pin, timeout=self.uart_timeout
)
else:
self._uart = busio.UART(
tx=self.uart_pin, rx=None, timeout=self.uart_timeout
)
# Attempt to sanely guess a coord_mapping if one is not provided.
if not keyboard.coord_mapping:
keyboard.coord_mapping = []
rows_to_calc = len(keyboard.row_pins)
cols_to_calc = len(keyboard.col_pins)
if self.split_offsets:
rows_to_calc *= 2
cols_to_calc *= 2
for ridx in range(rows_to_calc):
for cidx in range(cols_to_calc):
keyboard.coord_mapping.append(intify_coordinate(ridx, cidx))
gc.collect()
def before_matrix_scan(self, keyboard_state):
if self.split_type is not None and self._is_master:
return self._receive_from_slave()
def after_matrix_scan(self, keyboard_state, matrix_update):
if matrix_update is not None and not self._is_master:
self._send_to_master(matrix_update)
def _send_to_master(self, update):
if self.split_master_left:
update[1] += self.split_offsets[update[0]]
else:
update[1] -= self.split_offsets[update[0]]
if self._uart is not None:
self._uart.write(update)

View File

@ -5,7 +5,7 @@ def df_pressed(key, state, *args, **kwargs):
'''
Switches the default layer
'''
state.active_layers[-1] = key.meta.layer
state._active_layers[-1] = key.meta.layer
return state
@ -13,7 +13,7 @@ def mo_pressed(key, state, *args, **kwargs):
'''
Momentarily activates layer, switches off when you let go
'''
state.active_layers.insert(0, key.meta.layer)
state._active_layers.insert(0, key.meta.layer)
return state
@ -27,8 +27,8 @@ def mo_released(key, state, KC, *args, **kwargs):
# triggered by MO() and then defaulting to the MO()'s layer
# would result in no layers active
try:
del_idx = state.active_layers.index(key.meta.layer)
del state.active_layers[del_idx]
del_idx = state._active_layers.index(key.meta.layer)
del state._active_layers[del_idx]
except ValueError:
pass
@ -39,10 +39,10 @@ def lm_pressed(key, state, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
state.hid_pending = True
state._hid_pending = True
# Sets the timer start and acts like MO otherwise
state.start_time['lm'] = ticks_ms()
state.keys_pressed.add(key.meta.kc)
state._start_time['lm'] = ticks_ms()
state._keys_pressed.add(key.meta.kc)
return mo_pressed(key, state, *args, **kwargs)
@ -50,28 +50,28 @@ def lm_released(key, state, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
state.hid_pending = True
state.keys_pressed.discard(key.meta.kc)
state.start_time['lm'] = None
state._hid_pending = True
state._keys_pressed.discard(key.meta.kc)
state._start_time['lm'] = None
return mo_released(key, state, *args, **kwargs)
def lt_pressed(key, state, *args, **kwargs):
# Sets the timer start and acts like MO otherwise
state.start_time['lt'] = ticks_ms()
state._start_time['lt'] = ticks_ms()
return mo_pressed(key, state, *args, **kwargs)
def lt_released(key, state, *args, **kwargs):
# On keyup, check timer, and press key if needed.
if state.start_time['lt'] and (
ticks_diff(ticks_ms(), state.start_time['lt']) < state.config.tap_time
if state._start_time['lt'] and (
ticks_diff(ticks_ms(), state._start_time['lt']) < state.tap_time
):
state.hid_pending = True
state.tap_key(key.meta.kc)
state._hid_pending = True
state._tap_key(key.meta.kc)
mo_released(key, state, *args, **kwargs)
state.start_time['lt'] = None
state._start_time['lt'] = None
return state
@ -81,10 +81,10 @@ def tg_pressed(key, state, *args, **kwargs):
'''
# See mo_released for implementation details around this
try:
del_idx = state.active_layers.index(key.meta.layer)
del state.active_layers[del_idx]
del_idx = state._active_layers.index(key.meta.layer)
del state._active_layers[del_idx]
except ValueError:
state.active_layers.insert(0, key.meta.layer)
state._active_layers.insert(0, key.meta.layer)
return state
@ -93,8 +93,8 @@ def to_pressed(key, state, *args, **kwargs):
'''
Activates layer and deactivates all other layers
'''
state.active_layers.clear()
state.active_layers.insert(0, key.meta.layer)
state._active_layers.clear()
state._active_layers.insert(0, key.meta.layer)
return state
@ -104,23 +104,21 @@ def tt_pressed(key, state, *args, **kwargs):
Momentarily activates layer if held, toggles it if tapped repeatedly
'''
# TODO Make this work with tap dance to function more correctly, but technically works.
if state.start_time['tt'] is None:
if state._start_time['tt'] is None:
# Sets the timer start and acts like MO otherwise
state.start_time['tt'] = ticks_ms()
state._start_time['tt'] = ticks_ms()
return mo_pressed(key, state, *args, **kwargs)
elif ticks_diff(ticks_ms(), state.start_time['tt']) < state.config.tap_time:
state.start_time['tt'] = None
elif ticks_diff(ticks_ms(), state._start_time['tt']) < state.tap_time:
state._start_time['tt'] = None
return tg_pressed(key, state, *args, **kwargs)
def tt_released(key, state, *args, **kwargs):
tap_timed_out = (
ticks_diff(ticks_ms(), state.start_time['tt']) >= state.config.tap_time
)
if state.start_time['tt'] is None or tap_timed_out:
tap_timed_out = ticks_diff(ticks_ms(), state._start_time['tt']) >= state.tap_time
if state._start_time['tt'] is None or tap_timed_out:
# On first press, works like MO. On second press, does nothing unless let up within
# time window, then acts like TG.
state.start_time['tt'] = None
state._start_time['tt'] = None
return mo_released(key, state, *args, **kwargs)
return state

View File

@ -3,21 +3,21 @@ from kmk.kmktime import ticks_diff, ticks_ms
def mt_pressed(key, state, *args, **kwargs):
# Sets the timer start and acts like a modifier otherwise
state.keys_pressed.add(key.meta.mods)
state._keys_pressed.add(key.meta.mods)
state.start_time['mod_tap'] = ticks_ms()
state._start_time['mod_tap'] = ticks_ms()
return state
def mt_released(key, state, *args, **kwargs):
# On keyup, check timer, and press key if needed.
state.keys_pressed.discard(key.meta.mods)
state._keys_pressed.discard(key.meta.mods)
timer_name = 'mod_tap'
if state.start_time[timer_name] and (
ticks_diff(ticks_ms(), state.start_time[timer_name]) < state.config.tap_time
if state._start_time[timer_name] and (
ticks_diff(ticks_ms(), state._start_time[timer_name]) < state.tap_time
):
state.hid_pending = True
state.tap_key(key.meta.kc)
state._hid_pending = True
state._tap_key(key.meta.kc)
state.start_time[timer_name] = None
state._start_time[timer_name] = None
return state

View File

@ -12,18 +12,18 @@ def get_wide_ordinal(char):
def sequence_press_handler(key, state, KC, *args, **kwargs):
old_keys_pressed = state.keys_pressed
state.keys_pressed = set()
old_keys_pressed = state._keys_pressed
state._keys_pressed = set()
for ikey in key.meta.seq:
if not getattr(ikey, 'no_press', None):
state.process_key(ikey, True)
state.config._send_hid()
state._process_key(ikey, True)
state._send_hid()
if not getattr(ikey, 'no_release', None):
state.process_key(ikey, False)
state.config._send_hid()
state._process_key(ikey, False)
state._send_hid()
state.keys_pressed = old_keys_pressed
state._keys_pressed = old_keys_pressed
return state
@ -103,16 +103,16 @@ def unicode_codepoint_sequence(codepoints):
kc_macros = [simple_key_sequence(kc_seq) for kc_seq in kc_seqs]
def _unicode_sequence(key, state, *args, **kwargs):
if state.config.unicode_mode == UnicodeMode.IBUS:
state.process_key(
if state.unicode_mode == UnicodeMode.IBUS:
state._process_key(
simple_key_sequence(_ibus_unicode_sequence(kc_macros, state)), True
)
elif state.config.unicode_mode == UnicodeMode.RALT:
state.process_key(
elif state.unicode_mode == UnicodeMode.RALT:
state._process_key(
simple_key_sequence(_ralt_unicode_sequence(kc_macros, state)), True
)
elif state.config.unicode_mode == UnicodeMode.WINC:
state.process_key(
elif state.unicode_mode == UnicodeMode.WINC:
state._process_key(
simple_key_sequence(_winc_unicode_sequence(kc_macros, state)), True
)

View File

@ -6,23 +6,23 @@ def passthrough(key, state, *args, **kwargs):
def default_pressed(key, state, KC, coord_int=None, coord_raw=None):
state.hid_pending = True
state._hid_pending = True
if coord_int is not None:
state.coord_keys_pressed[coord_int] = key
state._coord_keys_pressed[coord_int] = key
state.keys_pressed.add(key)
state._keys_pressed.add(key)
return state
def default_released(key, state, KC, coord_int=None, coord_raw=None):
state.hid_pending = True
state.keys_pressed.discard(key)
state._hid_pending = True
state._keys_pressed.discard(key)
if coord_int is not None:
state.keys_pressed.discard(state.coord_keys_pressed.get(coord_int, None))
state.coord_keys_pressed[coord_int] = None
state._keys_pressed.discard(state._coord_keys_pressed.get(coord_int, None))
state._coord_keys_pressed[coord_int] = None
return state
@ -53,12 +53,12 @@ def bootloader(*args, **kwargs):
def debug_pressed(key, state, KC, *args, **kwargs):
if state.config.debug_enabled:
if state.debug_enabled:
print('DebugDisable()')
else:
print('DebugEnable()')
state.config.debug_enabled = not state.config.debug_enabled
state.debug_enabled = not state.debug_enabled
return state
@ -66,48 +66,48 @@ def debug_pressed(key, state, KC, *args, **kwargs):
def gesc_pressed(key, state, KC, *args, **kwargs):
GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI}
if GESC_TRIGGERS.intersection(state.keys_pressed):
if GESC_TRIGGERS.intersection(state._keys_pressed):
# First, release GUI if already pressed
state.config._send_hid()
state._send_hid()
# if Shift is held, KC_GRAVE will become KC_TILDE on OS level
state.keys_pressed.add(KC.GRAVE)
state.hid_pending = True
state._keys_pressed.add(KC.GRAVE)
state._hid_pending = True
return state
# else return KC_ESC
state.keys_pressed.add(KC.ESCAPE)
state.hid_pending = True
state._keys_pressed.add(KC.ESCAPE)
state._hid_pending = True
return state
def gesc_released(key, state, KC, *args, **kwargs):
state.keys_pressed.discard(KC.ESCAPE)
state.keys_pressed.discard(KC.GRAVE)
state.hid_pending = True
state._keys_pressed.discard(KC.ESCAPE)
state._keys_pressed.discard(KC.GRAVE)
state._hid_pending = True
return state
def bkdl_pressed(key, state, KC, *args, **kwargs):
BKDL_TRIGGERS = {KC.LGUI, KC.RGUI}
if BKDL_TRIGGERS.intersection(state.keys_pressed):
state.config._send_hid()
state.keys_pressed.add(KC.DEL)
state.hid_pending = True
if BKDL_TRIGGERS.intersection(state._keys_pressed):
state._send_hid()
state._keys_pressed.add(KC.DEL)
state._hid_pending = True
return state
# else return KC_ESC
state.keys_pressed.add(KC.BKSP)
state.hid_pending = True
state._keys_pressed.add(KC.BKSP)
state._hid_pending = True
return state
def bkdl_released(key, state, KC, *args, **kwargs):
state.keys_pressed.discard(KC.BKSP)
state.keys_pressed.discard(KC.DEL)
state.hid_pending = True
state._keys_pressed.discard(KC.BKSP)
state._keys_pressed.discard(KC.DEL)
state._hid_pending = True
return state
@ -117,15 +117,11 @@ def sleep_pressed(key, state, KC, *args, **kwargs):
def uc_mode_pressed(key, state, *args, **kwargs):
state.config.unicode_mode = key.meta.mode
state.unicode_mode = key.meta.mode
return state
def leader_pressed(key, state, *args, **kwargs):
return state._begin_leader_mode()
def td_pressed(key, state, *args, **kwargs):
return state._process_tap_dance(key, True)
@ -135,137 +131,83 @@ def td_released(key, state, *args, **kwargs):
def rgb_tog(key, state, *args, **kwargs):
if state.config.pixels.animation_mode == 'static_standby':
state.config.pixels.animation_mode = 'static'
state.config.pixels.enabled = not state.config.pixels.enabled
if state.pixels.animation_mode == 'static_standby':
state.pixels.animation_mode = 'static'
state.pixels.enabled = not state.pixels.enabled
return state
def rgb_hui(key, state, *args, **kwargs):
state.config.pixels.increase_hue()
state.pixels.increase_hue()
return state
def rgb_hud(key, state, *args, **kwargs):
state.config.pixels.decrease_hue()
state.pixels.decrease_hue()
return state
def rgb_sai(key, state, *args, **kwargs):
state.config.pixels.increase_sat()
state.pixels.increase_sat()
return state
def rgb_sad(key, state, *args, **kwargs):
state.config.pixels.decrease_sat()
state.pixels.decrease_sat()
return state
def rgb_vai(key, state, *args, **kwargs):
state.config.pixels.increase_val()
state.pixels.increase_val()
return state
def rgb_vad(key, state, *args, **kwargs):
state.config.pixels.decrease_val()
state.pixels.decrease_val()
return state
def rgb_ani(key, state, *args, **kwargs):
state.config.pixels.increase_ani()
state.pixels.increase_ani()
return state
def rgb_and(key, state, *args, **kwargs):
state.config.pixels.decrease_ani()
state.pixels.decrease_ani()
return state
def rgb_mode_static(key, state, *args, **kwargs):
state.config.pixels.effect_init = True
state.config.pixels.animation_mode = 'static'
state.pixels.effect_init = True
state.pixels.animation_mode = 'static'
return state
def rgb_mode_breathe(key, state, *args, **kwargs):
state.config.pixels.effect_init = True
state.config.pixels.animation_mode = 'breathing'
state.pixels.effect_init = True
state.pixels.animation_mode = 'breathing'
return state
def rgb_mode_breathe_rainbow(key, state, *args, **kwargs):
state.config.pixels.effect_init = True
state.config.pixels.animation_mode = 'breathing_rainbow'
state.pixels.effect_init = True
state.pixels.animation_mode = 'breathing_rainbow'
return state
def rgb_mode_rainbow(key, state, *args, **kwargs):
state.config.pixels.effect_init = True
state.config.pixels.animation_mode = 'rainbow'
state.pixels.effect_init = True
state.pixels.animation_mode = 'rainbow'
return state
def rgb_mode_swirl(key, state, *args, **kwargs):
state.config.pixels.effect_init = True
state.config.pixels.animation_mode = 'swirl'
state.pixels.effect_init = True
state.pixels.animation_mode = 'swirl'
return state
def rgb_mode_knight(key, state, *args, **kwargs):
state.config.pixels.effect_init = True
state.config.pixels.animation_mode = 'knight'
return state
def led_tog(key, state, *args, **kwargs):
if state.config.led.animation_mode == 'static_standby':
state.config.led.animation_mode = 'static'
state.config.led.enabled = not state.config.led.enabled
return state
def led_inc(key, state, *args, **kwargs):
state.config.led.increase_brightness()
return state
def led_dec(key, state, *args, **kwargs):
state.config.led.decrease_brightness()
return state
def led_ani(key, state, *args, **kwargs):
state.config.led.increase_ani()
return state
def led_and(key, state, *args, **kwargs):
state.config.led.decrease_ani()
return state
def led_mode_static(key, state, *args, **kwargs):
state.config.led.effect_init = True
state.config.led.animation_mode = 'static'
return state
def led_mode_breathe(key, state, *args, **kwargs):
state.config.led.effect_init = True
state.config.led.animation_mode = 'breathing'
return state
def bt_clear_bonds(key, state, *args, **kwargs):
state.config._hid_helper_inst.clear_bonds()
return state
def bt_next_conn(key, state, *args, **kwargs):
state.config._hid_helper_inst.next_connection()
return state
def bt_prev_conn(key, state, *args, **kwargs):
state.config._hid_helper_inst.previous_connection()
state.pixels.effect_init = True
state.pixels.animation_mode = 'knight'
return state

View File

@ -1,290 +0,0 @@
from kmk.consts import LeaderMode
from kmk.keys import KC
from kmk.kmktime import ticks_ms
from kmk.matrix import intify_coordinate
from kmk.types import TapDanceKeyMeta
class InternalState:
keys_pressed = set()
coord_keys_pressed = {}
leader_pending = None
leader_last_len = 0
hid_pending = False
leader_mode_history = []
# 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]
start_time = {'lt': None, 'tg': None, 'tt': None, 'lm': None, 'leader': None}
timeouts = {}
tapping = False
tap_dance_counts = {}
tap_side_effects = {}
def __init__(self, config):
self.config = config
def __repr__(self):
return (
'InternalState('
'keys_pressed={} '
'coord_keys_pressed={} '
'leader_pending={} '
'leader_last_len={} '
'hid_pending={} '
'leader_mode_history={} '
'active_layers={} '
'start_time={} '
'timeouts={} '
'tapping={} '
'tap_dance_counts={} '
'tap_side_effects={}'
')'
).format(
self.keys_pressed,
self.coord_keys_pressed,
self.leader_pending,
self.leader_last_len,
self.hid_pending,
self.leader_mode_history,
self.active_layers,
self.start_time,
self.timeouts,
self.tapping,
self.tap_dance_counts,
self.tap_side_effects,
)
def _find_key_in_map(self, row, col):
ic = intify_coordinate(row, col)
try:
idx = self.config.coord_mapping.index(ic)
except ValueError:
if self.config.debug_enabled:
print(
'CoordMappingNotFound(ic={}, row={}, col={})'.format(ic, row, col)
)
return None
for layer in self.active_layers:
layer_key = self.config.keymap[layer][idx]
if not layer_key or layer_key == KC.TRNS:
continue
if self.config.debug_enabled:
print('KeyResolution(key={})'.format(layer_key))
return layer_key
def set_timeout(self, after_ticks, callback):
if after_ticks is False:
# We allow passing False as an implicit "run this on the next process timeouts cycle"
timeout_key = ticks_ms()
else:
timeout_key = ticks_ms() + after_ticks
while timeout_key in self.timeouts:
timeout_key += 1
self.timeouts[timeout_key] = callback
return timeout_key
def cancel_timeout(self, timeout_key):
if timeout_key in self.timeouts:
del self.timeouts[timeout_key]
def process_timeouts(self):
if not self.timeouts:
return self
current_time = ticks_ms()
# cast this to a tuple to ensure that if a callback itself sets
# timeouts, we do not handle them on the current cycle
timeouts = tuple(self.timeouts.items())
for k, v in timeouts:
if k <= current_time:
v()
del self.timeouts[k]
return self
def matrix_changed(self, row, col, is_pressed):
if self.config.debug_enabled:
print('MatrixChange(col={} row={} pressed={})'.format(col, row, is_pressed))
int_coord = intify_coordinate(row, col)
kc_changed = self._find_key_in_map(row, col)
if kc_changed is None:
print('MatrixUndefinedCoordinate(col={} row={})'.format(col, row))
return self
return self.process_key(kc_changed, is_pressed, int_coord, (row, col))
def process_key(self, key, is_pressed, coord_int=None, coord_raw=None):
if self.tapping and not isinstance(key.meta, TapDanceKeyMeta):
self._process_tap_dance(key, is_pressed)
else:
if is_pressed:
key._on_press(self, coord_int, coord_raw)
else:
key._on_release(self, coord_int, coord_raw)
if self.config.leader_mode % 2 == 1:
self._process_leader_mode()
return self
def remove_key(self, keycode):
self.keys_pressed.discard(keycode)
return self.process_key(keycode, False)
def add_key(self, keycode):
self.keys_pressed.add(keycode)
return self.process_key(keycode, True)
def tap_key(self, keycode):
self.add_key(keycode)
# On the next cycle, we'll remove the key.
self.set_timeout(False, lambda: self.remove_key(keycode))
return self
def resolve_hid(self):
self.hid_pending = False
return self
def _process_tap_dance(self, changed_key, is_pressed):
if is_pressed:
if not isinstance(changed_key.meta, TapDanceKeyMeta):
# If we get here, changed_key is not a TapDanceKey and thus
# the user kept typing elsewhere (presumably). End ALL of the
# currently outstanding tap dance runs.
for k, v in self.tap_dance_counts.items():
if v:
self._end_tap_dance(k)
return self
if (
changed_key not in self.tap_dance_counts
or not self.tap_dance_counts[changed_key]
):
self.tap_dance_counts[changed_key] = 1
self.set_timeout(
self.config.tap_time, lambda: self._end_tap_dance(changed_key)
)
self.tapping = True
else:
self.tap_dance_counts[changed_key] += 1
if changed_key not in self.tap_side_effects:
self.tap_side_effects[changed_key] = None
else:
has_side_effects = self.tap_side_effects[changed_key] is not None
hit_max_defined_taps = self.tap_dance_counts[changed_key] == len(
changed_key.codes
)
if has_side_effects or hit_max_defined_taps:
self._end_tap_dance(changed_key)
return self
def _end_tap_dance(self, td_key):
v = self.tap_dance_counts[td_key] - 1
if v >= 0:
if td_key in self.keys_pressed:
key_to_press = td_key.codes[v]
self.add_key(key_to_press)
self.tap_side_effects[td_key] = key_to_press
self.hid_pending = True
else:
if self.tap_side_effects[td_key]:
self.remove_key(self.tap_side_effects[td_key])
self.tap_side_effects[td_key] = None
self.hid_pending = True
self._cleanup_tap_dance(td_key)
else:
self.tap_key(td_key.codes[v])
self._cleanup_tap_dance(td_key)
return self
def _cleanup_tap_dance(self, td_key):
self.tap_dance_counts[td_key] = 0
self.tapping = any(count > 0 for count in self.tap_dance_counts.values())
return self
def _begin_leader_mode(self):
if self.config.leader_mode % 2 == 0:
self.keys_pressed.discard(KC.LEAD)
# All leader modes are one number higher when activating
self.config.leader_mode += 1
if self.config.leader_mode == LeaderMode.TIMEOUT_ACTIVE:
self.set_timeout(
self.config.leader_timeout, self._handle_leader_sequence
)
return self
def _handle_leader_sequence(self):
lmh = tuple(self.leader_mode_history)
# Will get caught in infinite processing loops if we don't
# exit leader mode before processing the target key
self._exit_leader_mode()
if lmh in self.config.leader_dictionary:
# Stack depth exceeded if try to use add_key here with a unicode sequence
self.process_key(self.config.leader_dictionary[lmh], True)
self.set_timeout(
False, lambda: self.remove_key(self.config.leader_dictionary[lmh])
)
return self
def _process_leader_mode(self):
keys_pressed = self.keys_pressed
if self.leader_last_len and self.leader_mode_history:
history_set = set(self.leader_mode_history)
keys_pressed = keys_pressed - history_set
self.leader_last_len = len(self.keys_pressed)
for key in keys_pressed:
if self.config.leader_mode == LeaderMode.ENTER_ACTIVE and key == KC.ENT:
self._handle_leader_sequence()
break
elif key == KC.ESC or key == KC.GESC:
# Clean self and turn leader mode off.
self._exit_leader_mode()
break
elif key == KC.LEAD:
break
else:
# Add key if not needing to escape
# This needs replaced later with a proper debounce
self.leader_mode_history.append(key)
self.hid_pending = False
return self
def _exit_leader_mode(self):
self.leader_mode_history.clear()
self.config.leader_mode -= 1
self.leader_last_len = 0
self.keys_pressed.clear()
return self

View File

@ -629,25 +629,6 @@ make_key(
make_key(names=('RGB_MODE_SWIRL', 'RGB_M_S'), on_press=handlers.rgb_mode_swirl)
make_key(names=('RGB_MODE_KNIGHT', 'RGB_M_K'), on_press=handlers.rgb_mode_knight)
make_key(names=('LED_TOG',), on_press=handlers.led_tog)
make_key(names=('LED_INC',), on_press=handlers.led_inc)
make_key(names=('LED_DEC',), on_press=handlers.led_dec)
make_key(names=('LED_ANI',), on_press=handlers.led_ani)
make_key(names=('LED_AND',), on_press=handlers.led_and)
make_key(names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=handlers.led_mode_static)
make_key(names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=handlers.led_mode_breathe)
make_key(names=('BT_CLEAR_BONDS', 'BT_CLR'), on_press=handlers.bt_clear_bonds)
make_key(names=('BT_NEXT_CONN', 'BT_NXT'), on_press=handlers.bt_next_conn)
make_key(names=('BT_PREV_CONN', 'BT_PRV'), on_press=handlers.bt_prev_conn)
make_key(
names=('LEADER', 'LEAD'),
on_press=handlers.leader_pressed,
on_release=handlers.passthrough,
)
# Layers
make_argumented_key(
validator=layer_key_validator,

View File

@ -3,19 +3,20 @@
# a line into their keymaps.
import kmk.preload_imports # isort:skip # NOQA
import busio
import gc
from kmk import led, rgb
from kmk.consts import LeaderMode, UnicodeMode
from kmk.hid import AbstractHID, HIDModes
from kmk.internal_state import InternalState
from kmk import rgb
from kmk.consts import KMK_RELEASE, UnicodeMode
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
from kmk.keys import KC
from kmk.kmktime import sleep_ms
from kmk.matrix import MatrixScanner
from kmk.matrix import intify_coordinate as ic
from kmk.kmktime import ticks_ms
from kmk.matrix import MatrixScanner, intify_coordinate
from kmk.types import TapDanceKeyMeta
class KMKKeyboard:
#####
# User-configurable
debug_enabled = False
keymap = None
@ -29,29 +30,27 @@ class KMKKeyboard:
unicode_mode = UnicodeMode.NOOP
tap_time = 300
leader_mode = LeaderMode.TIMEOUT
leader_dictionary = {}
leader_timeout = 1000
# Split config
extra_data_pin = None
split_offsets = ()
split_flip = False
target_side = None
split_type = None
split_target_left = True
is_target = None
uart = None
uart_flip = True
uart_pin = None
# RGB config
rgb_pixel_pin = None
rgb_config = rgb.rgb_config
# led config (mono color)
led_pin = None
led_config = led.led_config
#####
# Internal State
_keys_pressed = set()
_coord_keys_pressed = {}
_hid_pending = 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]
_start_time = {'lt': None, 'tg': None, 'tt': None, 'lm': None}
_timeouts = {}
_tapping = False
_tap_dance_counts = {}
_tap_side_effects = {}
def __repr__(self):
return (
@ -65,20 +64,16 @@ class KMKKeyboard:
'matrix_scanner={} '
'unicode_mode={} '
'tap_time={} '
'leader_mode={} '
'leader_dictionary=truncated '
'leader_timeout={} '
'hid_helper={} '
'extra_data_pin={} '
'split_offsets={} '
'split_flip={} '
'target_side={} '
'split_type={} '
'split_target_left={} '
'is_target={} '
'uart={} '
'uart_flip={} '
'uart_pin={}'
'keys_pressed={} '
'coord_keys_pressed={} '
'hid_pending={} '
'active_layers={} '
'start_time={} '
'timeouts={} '
'tapping={} '
'tap_dance_counts={} '
'tap_side_effects={}'
')'
).format(
self.debug_enabled,
@ -90,51 +85,43 @@ class KMKKeyboard:
self.matrix_scanner,
self.unicode_mode,
self.tap_time,
self.leader_mode,
# self.leader_dictionary,
self.leader_timeout,
self.hid_helper.__name__,
self.extra_data_pin,
self.split_offsets,
self.split_flip,
self.target_side,
self.split_type,
self.split_target_left,
self.is_target,
self.uart,
self.uart_flip,
self.uart_pin,
# internal state
self._keys_pressed,
self._coord_keys_pressed,
self._hid_pending,
self._active_layers,
self._start_time,
self._timeouts,
self._tapping,
self._tap_dance_counts,
self._tap_side_effects,
)
def _print_debug_cycle(self, init=False):
pre_alloc = gc.mem_alloc()
pre_free = gc.mem_free()
if self.debug_enabled:
if init:
print('KMKInit(release={})'.format(KMK_RELEASE))
print(self)
print(self)
print(
'GCStats(pre_alloc={} pre_free={} alloc={} free={})'.format(
pre_alloc, pre_free, gc.mem_alloc(), gc.mem_free()
)
)
def _send_hid(self):
self._hid_helper_inst.create_report(self._state.keys_pressed).send()
self._state.resolve_hid()
def _send_key(self, key):
if not getattr(key, 'no_press', None):
self._state.add_key(key)
self._send_hid()
if not getattr(key, 'no_release', None):
self._state.remove_key(key)
self._send_hid()
self._hid_helper_inst.create_report(self._keys_pressed).send()
self._hid_pending = False
def _handle_matrix_report(self, update=None):
'''
Bulk processing of update code for each cycle
:param update:
'''
if update is not None:
self._state.matrix_changed(update[0], update[1], update[2])
def _send_to_target(self, update):
if self.split_target_left:
update[1] += self.split_offsets[update[0]]
else:
update[1] -= self.split_offsets[update[0]]
if self.uart is not None:
self.uart.write(update)
self._on_matrix_changed(update[0], update[1], update[2])
self.state_changed = True
def _receive_from_initiator(self):
if self.uart is not None and self.uart.in_waiting > 0 or self.uart_buffer:
@ -144,14 +131,14 @@ class KMKKeyboard:
microcontroller.reset()
while self.uart.in_waiting >= 3:
self.uart_buffer.append(self.uart.read(3))
while self._uart.in_waiting >= 3:
self.uart_buffer.append(self._uart.read(3))
if self.uart_buffer:
update = bytearray(self.uart_buffer.pop(0))
# Built in debug mode switch
if update == b'DEB':
print(self.uart.readline())
print(self._uart.readline())
return None
return update
@ -163,26 +150,207 @@ class KMKKeyboard:
be detected and handled differently than typical keypresses.
:param message: Debug message
'''
if self.uart is not None:
self.uart.write('DEB')
self.uart.write(message, '\n')
if self._uart is not None:
self._uart.write('DEB')
self._uart.write(message, '\n')
def init_uart(self, pin, timeout=20):
if self.is_target:
return busio.UART(tx=None, rx=pin, timeout=timeout)
#####
# SPLICE: INTERNAL STATE
# FIXME CLEAN THIS
#####
def _find_key_in_map(self, row, col):
ic = intify_coordinate(row, col)
try:
idx = self.coord_mapping.index(ic)
except ValueError:
if self.debug_enabled:
print(
'CoordMappingNotFound(ic={}, row={}, col={})'.format(ic, row, col)
)
return None
for layer in self._active_layers:
layer_key = self.keymap[layer][idx]
if not layer_key or layer_key == KC.TRNS:
continue
if self.debug_enabled:
print('KeyResolution(key={})'.format(layer_key))
return layer_key
def _on_matrix_changed(self, row, col, is_pressed):
if self.debug_enabled:
print('MatrixChange(col={} row={} pressed={})'.format(col, row, is_pressed))
int_coord = intify_coordinate(row, col)
kc_changed = self._find_key_in_map(row, col)
if kc_changed is None:
print('MatrixUndefinedCoordinate(col={} row={})'.format(col, row))
return self
return self._process_key(kc_changed, is_pressed, int_coord, (row, col))
def _process_key(self, key, is_pressed, coord_int=None, coord_raw=None):
if self._tapping and not isinstance(key.meta, TapDanceKeyMeta):
self._process_tap_dance(key, is_pressed)
else:
return busio.UART(tx=pin, rx=None, timeout=timeout)
if is_pressed:
key._on_press(self, coord_int, coord_raw)
else:
key._on_release(self, coord_int, coord_raw)
def go(self, hid_type=HIDModes.USB, **kwargs):
return self
def _remove_key(self, keycode):
self._keys_pressed.discard(keycode)
return self._process_key(keycode, False)
def _add_key(self, keycode):
self._keys_pressed.add(keycode)
return self._process_key(keycode, True)
def _tap_key(self, keycode):
self._add_key(keycode)
# On the next cycle, we'll remove the key.
self._set_timeout(False, lambda: self._remove_key(keycode))
return self
def _process_tap_dance(self, changed_key, is_pressed):
if is_pressed:
if not isinstance(changed_key.meta, TapDanceKeyMeta):
# If we get here, changed_key is not a TapDanceKey and thus
# the user kept typing elsewhere (presumably). End ALL of the
# currently outstanding tap dance runs.
for k, v in self._tap_dance_counts.items():
if v:
self._end_tap_dance(k)
return self
if (
changed_key not in self._tap_dance_counts
or not self._tap_dance_counts[changed_key]
):
self._tap_dance_counts[changed_key] = 1
self._set_timeout(
self.tap_time, lambda: self._end_tap_dance(changed_key)
)
self._tapping = True
else:
self._tap_dance_counts[changed_key] += 1
if changed_key not in self._tap_side_effects:
self._tap_side_effects[changed_key] = None
else:
has_side_effects = self._tap_side_effects[changed_key] is not None
hit_max_defined_taps = self._tap_dance_counts[changed_key] == len(
changed_key.codes
)
if has_side_effects or hit_max_defined_taps:
self._end_tap_dance(changed_key)
return self
def _end_tap_dance(self, td_key):
v = self._tap_dance_counts[td_key] - 1
if v >= 0:
if td_key in self._keys_pressed:
key_to_press = td_key.codes[v]
self._add_key(key_to_press)
self._tap_side_effects[td_key] = key_to_press
self._hid_pending = True
else:
if self._tap_side_effects[td_key]:
self._remove_key(self._tap_side_effects[td_key])
self._tap_side_effects[td_key] = None
self._hid_pending = True
self._cleanup_tap_dance(td_key)
else:
self._tap_key(td_key.codes[v])
self._cleanup_tap_dance(td_key)
return self
def _cleanup_tap_dance(self, td_key):
self._tap_dance_counts[td_key] = 0
self._tapping = any(count > 0 for count in self._tap_dance_counts.values())
return self
def _set_timeout(self, after_ticks, callback):
if after_ticks is False:
# We allow passing False as an implicit "run this on the next process timeouts cycle"
timeout_key = ticks_ms()
else:
timeout_key = ticks_ms() + after_ticks
while timeout_key in self._timeouts:
timeout_key += 1
self._timeouts[timeout_key] = callback
return timeout_key
def _cancel_timeout(self, timeout_key):
if timeout_key in self._timeouts:
del self._timeouts[timeout_key]
def _process_timeouts(self):
if not self._timeouts:
return self
current_time = ticks_ms()
# cast this to a tuple to ensure that if a callback itself sets
# timeouts, we do not handle them on the current cycle
timeouts = tuple(self._timeouts.items())
for k, v in timeouts:
if k <= current_time:
v()
del self._timeouts[k]
return self
#####
# SPLICE END: INTERNAL STATE
# TODO FIXME REMOVE THIS
#####
def _init_sanity_check(self):
'''
Ensure the provided configuration is *probably* bootable
'''
assert self.keymap, 'must define a keymap with at least one row'
assert self.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'
assert (
hid_type in HIDModes.ALL_MODES
self.hid_type in HIDModes.ALL_MODES
), 'hid_type must be a value from kmk.consts.HIDModes'
# Attempt to sanely guess a coord_mapping if one is not provided
return self
def _init_coord_mapping(self):
'''
Attempt to sanely guess a coord_mapping if one is not provided. No-op
if `kmk.extensions.split.Split` is used, it provides equivalent
functionality in `on_bootup`
To save RAM on boards that don't use Split, we don't import Split
and do an isinstance check, but instead do string detection
'''
if any(
x.__class__.__module__ == 'kmk.extensions.split' for x in self._extensions
):
return
if not self.coord_mapping:
self.coord_mapping = []
@ -190,19 +358,14 @@ class KMKKeyboard:
rows_to_calc = len(self.row_pins)
cols_to_calc = len(self.col_pins)
if self.split_offsets:
rows_to_calc *= 2
cols_to_calc *= 2
for ridx in range(rows_to_calc):
for cidx in range(cols_to_calc):
self.coord_mapping.append(ic(ridx, cidx))
self.coord_mapping.append(intify_coordinate(ridx, cidx))
self._state = InternalState(self)
if hid_type == HIDModes.NOOP:
def _init_hid(self):
if self.hid_type == HIDModes.NOOP:
self.hid_helper = AbstractHID
elif hid_type == HIDModes.USB:
elif self.hid_type == HIDModes.USB:
try:
from kmk.hid import USBHID
@ -210,7 +373,7 @@ class KMKKeyboard:
except ImportError:
self.hid_helper = AbstractHID
print('USB HID is unsupported ')
elif hid_type == HIDModes.BLE:
elif self.hid_type == HIDModes.BLE:
try:
from kmk.ble import BLEHID
@ -221,92 +384,87 @@ class KMKKeyboard:
self._hid_helper_inst = self.hid_helper(**kwargs)
# Split keyboard Init
if self.split_type is not None:
try:
# Working around https://github.com/adafruit/circuitpython/issues/1769
self._hid_helper_inst.create_report([]).send()
self.is_target = True
# Sleep 2s so target portion doesn't "appear" to boot quicker than
# dependent portions (which will take ~2s to time out on the HID send)
sleep_ms(2000)
except OSError:
self.is_target = False
if self.split_flip and not self.is_target:
self.col_pins = list(reversed(self.col_pins))
if self.target_side == 'Left':
self.split_target_left = self.is_target
elif self.target_side == 'Right':
self.split_target_left = not self.is_target
else:
self.is_target = True
if self.uart_pin is not None:
self.uart = self.init_uart(self.uart_pin)
if self.rgb_pixel_pin:
self.pixels = rgb.RGB(self.rgb_config, self.rgb_pixel_pin)
self.rgb_config = None # No longer needed
self.pixels.loopcounter = 0
else:
self.pixels = None
if self.led_pin:
self.led = led.led(self.led_pin, self.led_config)
self.led_config = None # No longer needed
else:
self.led = None
self.matrix = self.matrix_scanner(
def _init_matrix(self):
self.matrix = MatrixScanner(
cols=self.col_pins,
rows=self.row_pins,
diode_orientation=self.diode_orientation,
rollover_cols_every_rows=getattr(self, 'rollover_cols_every_rows', None),
)
# Compile string leader sequences
for k, v in self.leader_dictionary.items():
if not isinstance(k, tuple):
new_key = tuple(KC[c] for c in k)
self.leader_dictionary[new_key] = v
return self
for k, v in self.leader_dictionary.items():
if not isinstance(k, tuple):
del self.leader_dictionary[k]
def go(self, hid_type=HIDModes.USB, **kwargs):
self._extensions = [] + getattr(self, 'extensions', [])
try:
del self.extensions
except Exception:
pass
finally:
gc.collect()
self.hid_type = hid_type
self._init_sanity_check()
self._init_coord_mapping()
self._init_hid()
for ext in self._extensions:
try:
ext.during_bootup(self)
except Exception:
# TODO FIXME log the exceptions or something
pass
self._init_matrix()
self._print_debug_cycle(init=True)
while True:
if self.split_type is not None and self.is_target:
update = self._receive_from_initiator()
if update is not None:
self._handle_matrix_report(update)
self.state_changed = False
update = self.matrix.scan_for_changes()
for ext in self._extensions:
try:
self._handle_matrix_report(ext.before_matrix_scan(self))
except Exception as e:
print(e)
if update is not None:
if self.is_target:
self._handle_matrix_report(update)
else:
# This keyboard is a initiator, and needs to send data to target
self._send_to_target(update)
matrix_update = self.matrix.scan_for_changes()
self._handle_matrix_report(matrix_update)
if self._state.hid_pending:
for ext in self._extensions:
try:
ext.after_matrix_scan(self, matrix_update)
except Exception as e:
print(e)
for ext in self._extensions:
try:
ext.before_hid_send(self)
except Exception:
# TODO FIXME log the exceptions or something
pass
if self._hid_pending:
self._send_hid()
old_timeouts_len = len(self._state.timeouts)
self._state.process_timeouts()
new_timeouts_len = len(self._state.timeouts)
old_timeouts_len = len(self._timeouts)
self._process_timeouts()
new_timeouts_len = len(self._timeouts)
if old_timeouts_len != new_timeouts_len:
if self._state.hid_pending:
self.state_changed = True
if self._hid_pending:
self._send_hid()
if self.pixels and self.pixels.animation_mode:
self.pixels.loopcounter += 1
if self.pixels.loopcounter >= 30:
self.pixels = self.pixels.animate()
self.pixels.loopcounter = 0
for ext in self._extensions:
try:
ext.after_hid_send(self)
except Exception:
# TODO FIXME log the exceptions or something
pass
if self.led and self.led.enabled and self.led.animation_mode:
self.led = self.led.animate()
if self.state_changed:
self._print_debug_cycle()

View File

@ -1,88 +1,105 @@
import pulseio
from micropython import const
import time
from math import e, exp, pi, sin
led_config = {
'brightness_step': 5,
'brightness_limit': 100,
'breathe_center': 1.5,
'animation_mode': 'static',
'animation_speed': 1,
}
from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.keys import make_key
class led:
brightness = 0
time = int(time.monotonic() * 1000)
pos = 0
effect_init = False
class AnimationModes:
OFF = 0
STATIC = 1
STATIC_STANDBY = 2
BREATHING = 3
USER = 4
led = None
brightness_step = 5
brightness_limit = 100
breathe_center = 1.5
animation_mode = 'static'
animation_speed = 1
enabled = True
user_animation = None
def __init__(self, led_pin, config):
self.led = pulseio.PWMOut(led_pin)
self.brightness_step = const(config['brightness_step'])
self.brightness_limit = const(config['brightness_limit'])
self.animation_mode = const(config['animation_mode'])
self.animation_speed = const(config['animation_speed'])
self.breathe_center = const(config['breathe_center'])
if config.get('user_animation'):
self.user_animation = config['user_animation']
class LED(Extension):
def __init__(
self,
led_pin,
brightness_step=5,
brightness_limit=100,
breathe_center=1.5,
animation_mode=AnimationModes.STATIC,
animation_speed=1,
user_animation=None,
):
try:
self._led = pulseio.PWMOut(led_pin)
except Exception as e:
print(e)
raise InvalidExtensionEnvironment(
'Unable to create pulseio.PWMOut() instance with provided led_pin'
)
self._brightness = 0
self._pos = 0
self._effect_init = False
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
if user_animation is not None:
self.user_animation = user_animation
make_key(names=('LED_TOG',), on_press=self._key_led_tog)
make_key(names=('LED_INC',), on_press=self._key_led_inc)
make_key(names=('LED_DEC',), on_press=self._key_led_dec)
make_key(names=('LED_ANI',), on_press=self._key_led_ani)
make_key(names=('LED_AND',), on_press=self._key_led_and)
make_key(
names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static
)
make_key(
names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe
)
def __repr__(self):
return 'LED({})'.format(self._to_dict())
def _to_dict(self):
return {
'led': self.led,
'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,
}
# TODO FIXME remove
pass
def _init_effect(self):
self.pos = 0
self.effect_init = False
self._pos = 0
self._effect_init = False
return self
def time_ms(self):
return int(time.monotonic() * 1000)
def after_hid_send(self, keyboard):
if self._enabled and self.animation_mode:
self.animate()
return keyboard
def set_brightness(self, percent):
self.led.duty_cycle = int(percent / 100 * 65535)
self._led.duty_cycle = int(percent / 100 * 65535)
def increase_brightness(self, step=None):
if not step:
self.brightness += self.brightness_step
self._brightness += self.brightness_step
else:
self.brightness += step
self._brightness += step
if self.brightness > 100:
self.brightness = 100
if self._brightness > 100:
self._brightness = 100
self.set_brightness(self.brightness)
self.set_brightness(self._brightness)
def decrease_brightness(self, step=None):
if not step:
self.brightness -= self.brightness_step
self._brightness -= self.brightness_step
else:
self.brightness -= step
self._brightness -= step
if self.brightness < 0:
self.brightness = 0
if self._brightness < 0:
self._brightness = 0
self.set_brightness(self.brightness)
self.set_brightness(self._brightness)
def off(self):
self.set_brightness(0)
@ -110,18 +127,18 @@ class led:
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)
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)
self._brightness = int(multip_1 * multip_2)
self._pos = (self._pos + self.animation_speed) % 256
self.set_brightness(self._brightness)
return self
def effect_static(self):
self.set_brightness(self.brightness)
self.set_brightness(self._brightness)
# Set animation mode to none to prevent cycles from being wasted
self.animation_mode = None
return self
@ -131,16 +148,49 @@ class led:
Activates a "step" in the animation based on the active mode
:return: Returns the new state in animation
'''
if self.effect_init:
if self._effect_init:
self._init_effect()
if self.enabled:
if self.animation_mode == 'breathing':
if self._enabled:
if self.animation_mode == AnimationModes.BREATHING:
return self.effect_breathing()
elif self.animation_mode == 'static':
elif self.animation_mode == AnimationModes.STATIC:
return self.effect_static()
elif self.animation_mode == 'user':
elif self.animation_mode == AnimationModes.USER:
return self.user_animation(self)
else:
self.off()
return self
def _key_led_tog(self, key, state, *args, **kwargs):
if self.animation_mode == AnimationModes.STATIC_STANDBY:
self.animation_mode = AnimationModes.STATIC
self._enabled = not self._enabled
return state
def _key_led_inc(self, key, state, *args, **kwargs):
self.increase_brightness()
return state
def _key_led_dec(self, key, state, *args, **kwargs):
self.decrease_brightness()
return state
def _key_led_ani(self, key, state, *args, **kwargs):
self.increase_ani()
return state
def _key_led_and(self, key, state, *args, **kwargs):
self.decrease_ani()
return state
def _key_led_mode_static(self, key, state, *args, **kwargs):
self._effect_init = True
self.animation_mode = AnimationModes.STATIC
return state
def _key_led_mode_breathe(self, key, state, *args, **kwargs):
self._effect_init = True
self.animation_mode = AnimationModes.BREATHING
return state

View File

@ -27,19 +27,11 @@ import kmk.consts # isort:skip
import kmk.kmktime # isort:skip
import kmk.types # isort:skip
from kmk.consts import LeaderMode, UnicodeMode, KMK_RELEASE # isort:skip
from kmk.hid import USBHID # isort:skip
from kmk.internal_state import InternalState # isort:skip
from kmk.keys import KC # isort:skip
from kmk.matrix import MatrixScanner # isort:skip
# Now handlers that will be used in keys later
import kmk.handlers.layers # isort:skip
import kmk.handlers.stock # isort:skip
# Now stuff that depends on the above (and so on)
import kmk.hid # isort:skip
import kmk.keys # isort:skip
import kmk.matrix # isort:skip
import kmk.hid # isort:skip
import kmk.internal_state # isort:skip

View File

@ -1,99 +1,84 @@
from micropython import const
import neopixel
import time
from math import e, exp, pi, sin
rgb_config = {
'pixels': None,
'num_pixels': 0,
'pixel_pin': None,
'val_limit': 255,
'hue_default': 0,
'sat_default': 100,
'rgb_order': (1, 0, 2), # GRB WS2812
'val_default': 100,
'hue_step': 1,
'sat_step': 1,
'val_step': 1,
'animation_speed': 1,
'breathe_center': 1.5, # 1.0-2.7
'knight_effect_length': 3,
'animation_mode': 'static',
}
from kmk.extensions import Extension
rgb_config = {}
class RGB:
hue = 0
sat = 100
val = 80
class AnimationModes:
OFF = 0
STATIC = 1
STATIC_STANDBY = 2
BREATHING = 3
USER = 4
class RGB(Extension):
pos = 0
time = int(time.monotonic() * 10)
intervals = (30, 20, 10, 5)
animation_speed = 1
enabled = True
neopixel = None
rgbw = False
reverse_animation = False
disable_auto_write = False
animation_mode = 'static'
# Set by config
num_pixels = None
hue_step = None
sat_step = None
val_step = None
breathe_center = None # 1.0-2.7
knight_effect_length = None
val_limit = None
effect_init = False
user_animation = None
def __init__(
self,
pixel_pin,
num_pixels=0,
val_limit=255,
hue_default=0,
sat_default=100,
rgb_order=(1, 0, 2), # GRB WS2812
val_default=100,
hue_step=1,
sat_step=1,
val_step=1,
animation_speed=1,
breathe_center=1.5, # 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,
):
self.neopixel = neopixel.NeoPixel(
pixel_pin,
num_pixels,
pixel_order=rgb_order,
auto_write=not disable_auto_write,
)
def __init__(self, config, pixel_pin):
try:
import neopixel
if len(rgb_order) == 4:
self.rgbw = True
self.neopixel = neopixel.NeoPixel(
pixel_pin,
config['num_pixels'],
pixel_order=config['rgb_order'],
auto_write=False,
)
if len(config['rgb_order']) == 4:
self.rgbw = True
self.num_pixels = const(config['num_pixels'])
self.hue_step = const(config['hue_step'])
self.sat_step = const(config['sat_step'])
self.val_step = const(config['val_step'])
self.hue = const(config['hue_default'])
self.sat = const(config['sat_default'])
self.val = const(config['val_default'])
self.breathe_center = const(config['breathe_center'])
self.knight_effect_length = const(config['knight_effect_length'])
self.val_limit = const(config['val_limit'])
self.animation_mode = config['animation_mode']
self.animation_speed = const(config['animation_speed'])
if 'user_animation' in config:
self.user_animation = config['user_animation']
self.num_pixels = num_pixels
self.hue_step = hue_step
self.sat_step = sat_step
self.val_step = val_step
self.hue = hue_default
self.sat = sat_default
self.val = 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.reverse_animation = reverse_animation
self.user_animation = user_animation
self.disable_auto_write = disable_auto_write
except ImportError as e:
print(e)
def during_bootup(self, keyboard):
pass
def __repr__(self):
return 'RGB({})'.format(self._to_dict())
def after_hid_send(self, keyboard):
if self.animation_mode:
self.loopcounter += 1
if self.loopcounter >= 7:
self.animate()
self.loopcounter = 0
def _to_dict(self):
return {
'hue': self.hue,
'sat': self.sat,
'val': self.val,
'time': self.time,
'intervals': self.intervals,
'animation_mode': self.animation_mode,
'animation_speed': self.animation_speed,
'enabled': self.enabled,
'neopixel': self.neopixel,
'disable_auto_write': self.disable_auto_write,
}
return keyboard
def time_ms(self):
return int(time.monotonic() * 1000)

View File

@ -1,5 +1,6 @@
from kmk.boards.klarank import KMKKeyboard
from kmk.consts import LeaderMode, UnicodeMode
from kmk.consts import UnicodeMode
from kmk.extensions.leader import Leader, LeaderMode
from kmk.handlers.sequences import compile_unicode_string_sequences as cuss
from kmk.handlers.sequences import send_string
from kmk.keys import KC, make_key
@ -42,8 +43,16 @@ emoticons = cuss({
WPM = send_string('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum arcu vitae elementum curabitur vitae nunc sed. Facilisis sed odio morbi quis.')
keyboard.leader_mode = LeaderMode.TIMEOUT
keyboard.leader_dictionary = {
_______ = KC.TRNS
xxxxxxx = KC.NO
HELLA_TD = KC.TD(
KC.A,
KC.B,
send_string('macros in a tap dance? I think yes'),
KC.TG(1),
)
leader_ext = Leader(mode=LeaderMode.ENTER, sequences={
'hello': send_string('hello world from kmk macros'),
'wpm': WPM,
'atf': emoticons.ANGRY_TABLE_FLIP,
@ -55,16 +64,9 @@ keyboard.leader_dictionary = {
'poop': emoticons.POOP,
'ls': KC.LGUI(KC.HOME),
'dbg': KC.DBG,
}
})
_______ = KC.TRNS
xxxxxxx = KC.NO
HELLA_TD = KC.TD(
KC.A,
KC.B,
send_string('macros in a tap dance? I think yes'),
KC.TG(1),
)
keyboard.extensions = [leader_ext]
def shrek_is_life(*args, **kwargs):