feat(extensions): most of the extensions implementation, by kdb424

This commit is contained in:
Kyle Brown
2020-10-21 12:19:42 -07:00
committed by Josh Klar
parent 9821f7bcc3
commit e72d2b8c34
140 changed files with 3860 additions and 2312 deletions

View File

@@ -1,102 +0,0 @@
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.hid import HIDService
from kmk.hid import AbstractHID
BLE_APPEARANCE_HID_KEYBOARD = 961
# Hardcoded in CPy
MAX_CONNECTIONS = 2
class BLEHID(AbstractHID):
def post_init(self, ble_name='KMK Keyboard', **kwargs):
self.conn_id = -1
self.ble = BLERadio()
self.ble.name = 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()
self.conn_id = 0
@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 = []
# Security issue:
# This introduces a race condition. Let's say you have 2 active
# connections: Alice and Bob - Alice is connection 1 and Bob 2.
# Now Chuck who has already paired with the device in the past
# (this assumption is needed only in the case of LESC)
# wants to gather the keystrokes you send to Alice. You have
# selected right now to talk to Alice (1) and you're typing a secret.
# If Chuck kicks Alice off and is quick enough to connect to you,
# which means quicker than the running interval of this function,
# he'll be earlier in the `self.hid.devices` so will take over the
# selected 1 position in the resulted array.
# If no LESC is in place, Chuck can sniff the keystrokes anyway
for device in self.hid.devices:
if hasattr(device, 'send_report'):
result.append(device)
return result
def _check_connection(self):
devices = self.devices
if not devices:
return False
if self.conn_id >= len(devices):
self.conn_id = len(devices) - 1
if self.conn_id < 0:
return False
if not devices[self.conn_id]:
return False
return True
def hid_send(self, evt):
if not self._check_connection():
return
device = self.devices[self.conn_id]
while len(evt) < len(device._characteristic.value) + 1:
evt.append(0)
return device.send_report(evt[1:])
def clear_bonds(self):
import _bleio
_bleio.adapter.erase_bonding()
def next_connection(self):
self.conn_id = (self.conn_id + 1) % len(self.devices)
def previous_connection(self):
self.conn_id = (self.conn_id - 1) % len(self.devices)
def start_advertising(self):
advertisement = ProvideServicesAdvertisement(self.hid)
advertisement.appearance = BLE_APPEARANCE_HID_KEYBOARD
self.ble.start_advertising(advertisement)
def stop_advertising(self):
self.ble.stop_advertising()

View File

View File

@@ -1,10 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.D9, board.D10, board.D11, board.D12, board.D13, board.SCL)
row_pins = (board.A3, board.A4, board.A5, board.SCK, board.MOSI)
diode_orientation = DiodeOrientation.COLUMNS

View File

@@ -1,30 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.A0,
board.A1,
board.A2,
board.A3,
board.A4,
board.A5,
board.SCK,
board.MOSI,
)
row_pins = (
board.TX,
board.RX,
board.SDA,
board.SCL,
board.D13,
board.D12,
board.D11,
board.D10,
)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.D9
rgb_num_pixels = 12

View File

@@ -1,20 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.RX,
board.D13,
board.A0,
board.D11,
board.A4,
board.A5,
board.D10,
board.D9,
board.SCK,
)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A1, board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.A0, board.D11, board.D10, board.D9)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [7, 7, 7, 7]

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.D11, board.D10, board.D9, board.D7, board.D13)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6, 6]

View File

@@ -1,43 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
from kmk.matrix import intify_coordinate as ic
class KMKKeyboard(_KMKKeyboard):
# Pin mappings for converter board found at hardware/README.md
# QMK: MATRIX_COL_PINS { F6, F7, B1, B3, B2, B6 }
# QMK: MATRIX_ROW_PINS { D7, E6, B4, D2, D4 }
col_pins = (board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.D11, board.D10, board.D9, board.RX, board.D13)
diode_orientation = DiodeOrientation.COLUMNS
split_flip = True
split_offsets = (6, 6, 6, 6, 6)
split_type = 'UART'
uart_pin = board.SCL
extra_data_pin = board.SDA
rgb_pixel_pin = board.TX
led_pin = board.D7
coord_mapping = []
coord_mapping.extend(ic(0, x) for x in range(12))
coord_mapping.extend(ic(1, x) for x in range(12))
coord_mapping.extend(ic(2, x) for x in range(12))
# Buckle up friends, the bottom row of this keyboard is wild, and making
# our layouts match, visually, what the keyboard looks like, requires some
# surgery on the bottom two rows of coords
# Row index 3 is actually perfectly sane and we _could_ expose it
# just like the above three rows, however, visually speaking, the
# top-right thumb cluster button (when looking at the left-half PCB)
# is more inline with R3, so we'll jam that key (and its mirror) in here
coord_mapping.extend(ic(3, x) for x in range(6))
coord_mapping.append(ic(4, 2))
coord_mapping.append(ic(4, 9))
coord_mapping.extend(ic(3, x) for x in range(6, 12)) # Now, the rest of R3
# And now, to handle R4, which at this point is down to just six keys
coord_mapping.extend(ic(4, x) for x in range(3, 9))

View File

@@ -1,28 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.SDA,
board.A2,
board.A3,
board.A4,
board.A5,
board.SCK,
board.MOSI,
)
row_pins = (
board.TX,
board.A0,
board.RX,
board.A1,
board.D11,
board.D9,
board.D12,
board.D10,
)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.D13

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A2, board.A3, board.A4, board.A5, board.SCK, board.A0)
row_pins = (board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6]

View File

@@ -1,18 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.D13, board.D11, board.D10, board.D9)
diode_orientation = DiodeOrientation.COLUMNS
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6, 6]
uart_pin = board.SCL
extra_data_pin = board.SDA
rgb_pixel_pin = board.TX
# led_pin = board.D7

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.D13, board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6, 6]

View File

@@ -1,17 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.RX, board.A1, board.A2, board.A3, board.A4, board.A5)
row_pins = (board.D13, board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6, 6]
uart_pin = board.SCL
rgb_pixel_pin = board.TX
extra_data_pin = board.SDA

View File

@@ -1,25 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
# Will need additional work and testing
col_pins = (
board.A1,
board.A2,
board.A3,
board.A4,
board.A5,
board.SCK,
board.MOSI,
board.D12,
)
row_pins = (board.A0, board.D13, board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = False
split_offsets = [8, 8, 8, 8, 8, 8]

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.D11, board.D10, board.D9, board.RX, board.D13)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6, 6]

View File

@@ -1,29 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.A0,
board.A1,
board.A2,
board.A3,
board.A4,
board.A5,
board.SCK,
board.MOSI,
)
row_pins = (
board.TX,
board.RX,
board.SDA,
board.SCL,
board.D9,
board.D10,
board.D12,
board.D11,
board.D13,
)
diode_orientation = DiodeOrientation.COLUMNS

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A1, board.A2, board.A3, board.A4, board.A5, board.SCK, board.MOSI)
row_pins = (board.D13, board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [7, 7, 7, 7, 7]

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A0, board.A1, board.A2, board.A3, board.A4, board.A5, board.SCK)
row_pins = (board.D13, board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [7, 7, 7, 7, 7]

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.A5, board.A4, board.A3, board.A2, board.A1, board.A0)
row_pins = (board.D7, board.D9, board.D10, board.D11)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6]

View File

@@ -1,15 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
class KMKKeyboard(_KMKKeyboard):
col_pins = (board.MOSI, board.SCK, board.A5, board.A4, board.A3, board.A2)
row_pins = (board.D11, board.D10, board.D9, board.D7)
diode_orientation = DiodeOrientation.COLUMNS
rgb_pixel_pin = board.TX
uart_pin = board.SCL
split_type = 'UART'
split_flip = True
split_offsets = [6, 6, 6, 6]

View File

@@ -1,42 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
from kmk.matrix import intify_coordinate as ic
# Implements what used to be handled by KMKKeyboard.swap_indicies for this
# board, by flipping various row3 (bottom physical row) keys so their
# coord_mapping matches what the user pressed (even if the wiring
# underneath is sending different coordinates)
_r3_swap_conversions = {3: 9, 4: 10, 5: 11, 9: 3, 10: 4, 11: 5}
def r3_swap(col):
try:
return _r3_swap_conversions[col]
except KeyError:
return col
class KMKKeyboard(_KMKKeyboard):
# physical, visible cols (SCK, MO, MI, RX, TX, D4)
# physical, visible rows (10, 11, 12, 13) (9, 6, 5, SCL)
col_pins = (board.SCK, board.MOSI, board.MISO, board.RX, board.TX, board.D4)
row_pins = (
board.D10,
board.D11,
board.D12,
board.D13,
board.D9,
board.D6,
board.D5,
board.SCL,
)
rollover_cols_every_rows = 4
diode_orientation = DiodeOrientation.COLUMNS
coord_mapping = []
coord_mapping.extend(ic(0, x) for x in range(12))
coord_mapping.extend(ic(1, x) for x in range(12))
coord_mapping.extend(ic(2, x) for x in range(12))
coord_mapping.extend(ic(3, r3_swap(x)) for x in range(12))

View File

@@ -1,33 +0,0 @@
import board
from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.matrix import DiodeOrientation
from kmk.matrix import intify_coordinate as ic
class KMKKeyboard(_KMKKeyboard):
col_pins = (
board.P0_31,
board.P0_29,
board.P0_02,
board.P1_15,
board.P1_13,
board.P1_11,
)
row_pins = (board.P0_22, board.P0_24, board.P1_00, board.P0_11)
diode_orientation = DiodeOrientation.COLUMNS
split_type = 'UART' # TODO add bluetooth support as well
split_flip = True
split_offsets = [6, 6, 6, 6, 6]
uart_pin = board.P0_08
rgb_pixel_pin = board.P0_06
extra_data_pin = board.SDA # TODO This is incorrect. Find better solution
coord_mapping = []
coord_mapping.extend(ic(0, x) for x in range(12))
coord_mapping.extend(ic(1, x) for x in range(12))
coord_mapping.extend(ic(2, x) for x in range(12))
# And now, to handle R3, which at this point is down to just six keys
coord_mapping.extend(ic(3, x) for x in range(3, 9))

View File

@@ -1,3 +1,5 @@
from micropython import const
try:
from kmk.release_info import KMK_RELEASE
except Exception:
@@ -5,7 +7,7 @@ except Exception:
class UnicodeMode:
NOOP = 0
LINUX = IBUS = 1
MACOS = OSX = RALT = 2
WINC = 3
NOOP = const(0)
LINUX = IBUS = const(1)
MACOS = OSX = RALT = const(2)
WINC = const(3)

View File

@@ -8,35 +8,44 @@ class Extension:
def enable(self, keyboard):
self._enabled = True
self.on_runtime_enable(self, keyboard)
self.on_runtime_enable(keyboard)
def disable(self, keyboard):
self._enabled = False
self.on_runtime_disable(self, keyboard)
self.on_runtime_disable(keyboard)
# The below methods should be implemented by subclasses
def on_runtime_enable(self, keyboard):
pass
raise NotImplementedError
def on_runtime_disable(self, keyboard):
pass
raise NotImplementedError
def during_bootup(self, keyboard):
pass
raise NotImplementedError
def before_matrix_scan(self, keyboard):
'''
Return value will be injected as an extra matrix update
'''
pass
raise NotImplementedError
def after_matrix_scan(self, keyboard, matrix_update):
pass
def after_matrix_scan(self, keyboard):
'''
Return value will be replace matrix update if supplied
'''
raise NotImplementedError
def before_hid_send(self, keyboard):
pass
raise NotImplementedError
def after_hid_send(self, keyboard):
pass
raise NotImplementedError
def on_powersave_enable(self, keyboard):
raise NotImplementedError
def on_powersave_disable(self, keyboard):
raise NotImplementedError

View File

@@ -0,0 +1,59 @@
'''Adds international keys'''
from kmk.extensions import Extension
from kmk.keys import make_key
class International(Extension):
'''Adds international keys'''
def __init__(self):
# International
make_key(code=50, names=('NONUS_HASH', 'NUHS'))
make_key(code=100, names=('NONUS_BSLASH', 'NUBS'))
make_key(code=101, names=('APP', 'APPLICATION', 'SEL', 'WINMENU'))
make_key(code=135, names=('INT1', 'RO'))
make_key(code=136, names=('INT2', 'KANA'))
make_key(code=137, names=('INT3', 'JYEN'))
make_key(code=138, names=('INT4', 'HENK'))
make_key(code=139, names=('INT5', 'MHEN'))
make_key(code=140, names=('INT6',))
make_key(code=141, names=('INT7',))
make_key(code=142, names=('INT8',))
make_key(code=143, names=('INT9',))
make_key(code=144, names=('LANG1', 'HAEN'))
make_key(code=145, names=('LANG2', 'HAEJ'))
make_key(code=146, names=('LANG3',))
make_key(code=147, names=('LANG4',))
make_key(code=148, names=('LANG5',))
make_key(code=149, names=('LANG6',))
make_key(code=150, names=('LANG7',))
make_key(code=151, names=('LANG8',))
make_key(code=152, names=('LANG9',))
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return

View File

@@ -1,111 +0,0 @@
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)
)

View File

@@ -24,6 +24,7 @@ class LED(Extension):
animation_mode=AnimationModes.STATIC,
animation_speed=1,
user_animation=None,
val=100,
):
try:
self._led = pulseio.PWMOut(led_pin)
@@ -36,12 +37,14 @@ class LED(Extension):
self._brightness = 0
self._pos = 0
self._effect_init = False
self._enabled = True
self.brightness_step = brightness_step
self.brightness_limit = brightness_limit
self.animation_mode = animation_mode
self.animation_speed = animation_speed
self.breathe_center = breathe_center
self.val = val
if user_animation is not None:
self.user_animation = user_animation
@@ -62,20 +65,51 @@ class LED(Extension):
return 'LED({})'.format(self._to_dict())
def _to_dict(self):
# TODO FIXME remove
pass
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 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)
@@ -135,13 +169,10 @@ class LED(Extension):
self._pos = (self._pos + self.animation_speed) % 256
self.set_brightness(self._brightness)
return self
def effect_static(self):
self.set_brightness(self._brightness)
# Set animation mode to none to prevent cycles from being wasted
self.animation_mode = None
return self
def animate(self):
'''
@@ -160,37 +191,28 @@ class LED(Extension):
else:
self.off()
return self
def _key_led_tog(self, key, state, *args, **kwargs):
def _key_led_tog(self, *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):
def _key_led_inc(self, *args, **kwargs):
self.increase_brightness()
return state
def _key_led_dec(self, key, state, *args, **kwargs):
def _key_led_dec(self, *args, **kwargs):
self.decrease_brightness()
return state
def _key_led_ani(self, key, state, *args, **kwargs):
def _key_led_ani(self, *args, **kwargs):
self.increase_ani()
return state
def _key_led_and(self, key, state, *args, **kwargs):
def _key_led_and(self, *args, **kwargs):
self.decrease_ani()
return state
def _key_led_mode_static(self, key, state, *args, **kwargs):
def _key_led_mode_static(self, *args, **kwargs):
self._effect_init = True
self.animation_mode = AnimationModes.STATIC
return state
def _key_led_mode_breathe(self, key, state, *args, **kwargs):
def _key_led_mode_breathe(self, *args, **kwargs):
self._effect_init = True
self.animation_mode = AnimationModes.BREATHING
return state

View File

@@ -0,0 +1,55 @@
from kmk.extensions import Extension
from kmk.keys import make_consumer_key
class MediaKeys(Extension):
def __init__(self):
# Consumer ("media") keys. Most known keys aren't supported here. A much
# longer list used to exist in this file, but the codes were almost certainly
# incorrect, conflicting with each other, or otherwise 'weird'. We'll add them
# back in piecemeal as needed. PRs welcome.
#
# A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php
# Note that currently we only have the PC codes. Recent MacOS versions seem to
# support PC media keys, so I don't know how much value we would get out of
# adding the old Apple-specific consumer codes, but again, PRs welcome if the
# lack of them impacts you.
make_consumer_key(code=226, names=('AUDIO_MUTE', 'MUTE')) # 0xE2
make_consumer_key(code=233, names=('AUDIO_VOL_UP', 'VOLU')) # 0xE9
make_consumer_key(code=234, names=('AUDIO_VOL_DOWN', 'VOLD')) # 0xEA
make_consumer_key(code=181, names=('MEDIA_NEXT_TRACK', 'MNXT')) # 0xB5
make_consumer_key(code=182, names=('MEDIA_PREV_TRACK', 'MPRV')) # 0xB6
make_consumer_key(code=183, names=('MEDIA_STOP', 'MSTP')) # 0xB7
make_consumer_key(
code=205, names=('MEDIA_PLAY_PAUSE', 'MPLY')
) # 0xCD (this may not be right)
make_consumer_key(code=184, names=('MEDIA_EJECT', 'EJCT')) # 0xB8
make_consumer_key(code=179, names=('MEDIA_FAST_FORWARD', 'MFFD')) # 0xB3
make_consumer_key(code=180, names=('MEDIA_REWIND', 'MRWD')) # 0xB4
def on_runtime_enable(self, sandbox):
return
def on_runtime_disable(self, sandbox):
return
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
return
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
return

View File

@@ -4,6 +4,8 @@ import time
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
rgb_config = {}
@@ -13,7 +15,11 @@ class AnimationModes:
STATIC = 1
STATIC_STANDBY = 2
BREATHING = 3
USER = 4
RAINBOW = 4
BREATHING_RAINBOW = 5
KNIGHT = 6
SWIRL = 7
USER = 8
class RGB(Extension):
@@ -25,22 +31,23 @@ class RGB(Extension):
self,
pixel_pin,
num_pixels=0,
val_limit=255,
val_limit=100,
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,
hue_step=5,
sat_step=5,
val_step=5,
animation_speed=1,
breathe_center=1.5, # 1.0-2.7
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,
loopcounter=0,
):
self.neopixel = neopixel.NeoPixel(
pixel_pin,
@@ -49,38 +56,121 @@ class RGB(Extension):
auto_write=not disable_auto_write,
)
if len(rgb_order) == 4:
self.rgbw = True
self.rgbw = bool(len(rgb_order) == 4)
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.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.loopcounter = loopcounter
def during_bootup(self, keyboard):
pass
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 after_hid_send(self, keyboard):
if self.animation_mode:
self.loopcounter += 1
if self.loopcounter >= 7:
self.animate()
self.loopcounter = 0
def on_runtime_enable(self, sandbox):
return
return keyboard
def on_runtime_disable(self, sandbox):
return
def time_ms(self):
def during_bootup(self, sandbox):
return
def before_matrix_scan(self, sandbox):
return
def after_matrix_scan(self, sandbox):
return
def before_hid_send(self, sandbox):
return
def after_hid_send(self, sandbox):
self.animate()
def on_powersave_enable(self, sandbox):
return
def on_powersave_disable(self, sandbox):
self._do_update()
@staticmethod
def time_ms():
return int(time.monotonic() * 1000)
def hsv_to_rgb(self, hue, sat, val):
@@ -105,7 +195,7 @@ class RGB(Extension):
else:
base = ((100 - sat) * val) / 100
color = int((val - base) * ((hue % 60) / 60))
color = (val - base) * ((hue % 60) / 60)
x = int(hue / 60)
if x == 0:
@@ -160,8 +250,6 @@ class RGB(Extension):
else:
self.set_rgb(self.hsv_to_rgb(hue, sat, val), index)
return self
def set_hsv_fill(self, hue, sat, val):
'''
Takes HSV values and displays it on all LEDs/Neopixels
@@ -174,7 +262,6 @@ class RGB(Extension):
self.set_rgb_fill(self.hsv_to_rgbw(hue, sat, val))
else:
self.set_rgb_fill(self.hsv_to_rgb(hue, sat, val))
return self
def set_rgb(self, rgb, index):
'''
@@ -187,8 +274,6 @@ class RGB(Extension):
if not self.disable_auto_write:
self.neopixel.show()
return self
def set_rgb_fill(self, rgb):
'''
Takes an RGB or RGBW and displays it on all LEDs/Neopixels
@@ -199,8 +284,6 @@ class RGB(Extension):
if not self.disable_auto_write:
self.neopixel.show()
return self
def increase_hue(self, step=None):
'''
Increases hue by step amount rolling at 360 and returning to 0
@@ -214,8 +297,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def decrease_hue(self, step=None):
'''
Decreases hue by step amount rolling at 0 and returning to 360
@@ -232,8 +313,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def increase_sat(self, step=None):
'''
Increases saturation by step amount stopping at 100
@@ -250,8 +329,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def decrease_sat(self, step=None):
'''
Decreases saturation by step amount stopping at 0
@@ -268,8 +345,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def increase_val(self, step=None):
'''
Increases value by step amount stopping at 100
@@ -285,8 +360,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def decrease_val(self, step=None):
'''
Decreases value by step amount stopping at 0
@@ -302,8 +375,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def increase_ani(self):
'''
Increases animation speed by 1 amount stopping at 10
@@ -316,8 +387,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def decrease_ani(self):
'''
Decreases animation speed by 1 amount stopping at 0
@@ -330,8 +399,6 @@ class RGB(Extension):
if self._check_update():
self._do_update()
return self
def off(self):
'''
Turns off all LEDs/Neopixels without changing stored values
@@ -339,8 +406,6 @@ class RGB(Extension):
if self.neopixel:
self.set_hsv_fill(0, 0, 0)
return self
def show(self):
'''
Turns on all LEDs/Neopixels without changing stored values
@@ -348,8 +413,6 @@ class RGB(Extension):
if self.neopixel:
self.neopixel.show()
return self
def animate(self):
'''
Activates a "step" in the animation based on the active mode
@@ -358,27 +421,30 @@ class RGB(Extension):
if self.effect_init:
self._init_effect()
if self.enabled:
if self.animation_mode == 'breathing':
return self.effect_breathing()
elif self.animation_mode == 'rainbow':
return self.effect_rainbow()
elif self.animation_mode == 'breathing_rainbow':
return self.effect_breathing_rainbow()
elif self.animation_mode == 'static':
return self.effect_static()
elif self.animation_mode == 'knight':
return self.effect_knight()
elif self.animation_mode == 'swirl':
return self.effect_swirl()
elif self.animation_mode == 'user':
return self.user_animation(self)
elif self.animation_mode == 'static_standby':
pass
else:
self.off()
return self
if self.animation_mode is not AnimationModes.STATIC_STANDBY:
self.loopcounter += 1
if self.loopcounter >= 7 and self.enable:
self.loopcounter = 0
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()
if self.loopcounter >= 7:
self.loopcounter = 0
def _animation_step(self):
interval = self.time_ms() - self.time
@@ -387,35 +453,31 @@ class RGB(Extension):
return max(self.intervals)
if interval in self.intervals:
return interval
else:
return False
return None
def _init_effect(self):
if (
self.animation_mode == 'breathing'
or self.animation_mode == 'breathing_rainbow'
self.animation_mode == AnimationModes.BREATHING
or self.animation_mode == AnimationModes.BREATHING_RAINBOW
):
self.intervals = (30, 20, 10, 5)
elif self.animation_mode == 'swirl':
elif self.animation_mode == AnimationModes.SWIRL:
self.intervals = (50, 50)
self.pos = 0
self.reverse_animation = False
self.effect_init = False
return self
def _check_update(self):
if self.animation_mode == 'static_standby':
return True
return bool(self.animation_mode == AnimationModes.STATIC_STANDBY)
def _do_update(self):
if self.animation_mode == 'static_standby':
self.animation_mode = 'static'
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 = 'static_standby'
return self
self.animation_mode = AnimationModes.STATIC_STANDBY
def effect_breathing(self):
# http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
@@ -428,20 +490,14 @@ class RGB(Extension):
self.pos = (self.pos + self.animation_speed) % 256
self.set_hsv_fill(self.hue, self.sat, self.val)
return self
def effect_breathing_rainbow(self):
self.increase_hue(self.animation_speed)
self.effect_breathing()
return self
def effect_rainbow(self):
self.increase_hue(self.animation_speed)
self.set_hsv_fill(self.hue, self.sat, self.val)
return self
def effect_swirl(self):
self.increase_hue(self.animation_speed)
self.disable_auto_write = True # Turn off instantly showing
@@ -453,7 +509,6 @@ class RGB(Extension):
# Show final results
self.disable_auto_write = False # Resume showing changes
self.show()
return self
def effect_knight(self):
# Determine which LEDs should be lit up
@@ -478,4 +533,69 @@ class RGB(Extension):
self.disable_auto_write = False # Resume showing changes
self.show()
return self
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()

View File

@@ -1,103 +0,0 @@
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

@@ -1,124 +0,0 @@
from kmk.kmktime import ticks_diff, ticks_ms
def df_pressed(key, state, *args, **kwargs):
'''
Switches the default layer
'''
state._active_layers[-1] = key.meta.layer
return state
def mo_pressed(key, state, *args, **kwargs):
'''
Momentarily activates layer, switches off when you let go
'''
state._active_layers.insert(0, key.meta.layer)
return state
def mo_released(key, state, KC, *args, **kwargs):
# remove the first instance of the target layer
# from the active list
# under almost all normal use cases, this will
# disable the layer (but preserve it if it was triggered
# as a default layer, etc.)
# this also resolves an issue where using DF() on a layer
# triggered by MO() and then defaulting to the MO()'s layer
# would result in no layers active
try:
del_idx = state._active_layers.index(key.meta.layer)
del state._active_layers[del_idx]
except ValueError:
pass
return state
def lm_pressed(key, state, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
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)
return mo_pressed(key, state, *args, **kwargs)
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
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()
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.tap_time
):
state._hid_pending = True
state._tap_key(key.meta.kc)
mo_released(key, state, *args, **kwargs)
state._start_time['lt'] = None
return state
def tg_pressed(key, state, *args, **kwargs):
'''
Toggles the layer (enables it if not active, and vise versa)
'''
# See mo_released for implementation details around this
try:
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)
return state
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)
return state
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:
# Sets the timer start and acts like MO otherwise
state._start_time['tt'] = ticks_ms()
return mo_pressed(key, state, *args, **kwargs)
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.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
return mo_released(key, state, *args, **kwargs)
return state

View File

@@ -1,23 +0,0 @@
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._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)
timer_name = 'mod_tap'
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._start_time[timer_name] = None
return state

View File

@@ -1,3 +1,5 @@
import gc
from kmk.consts import UnicodeMode
from kmk.handlers.stock import passthrough
from kmk.keys import KC, make_key
@@ -11,21 +13,21 @@ def get_wide_ordinal(char):
return 0x10000 + (ord(char[0]) - 0xD800) * 0x400 + (ord(char[1]) - 0xDC00)
def sequence_press_handler(key, state, KC, *args, **kwargs):
old_keys_pressed = state._keys_pressed
state._keys_pressed = set()
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):
state._process_key(ikey, True)
state._send_hid()
keyboard.process_key(ikey, True)
keyboard._send_hid()
if not getattr(ikey, 'no_release', None):
state._process_key(ikey, False)
state._send_hid()
keyboard.process_key(ikey, False)
keyboard._send_hid()
state._keys_pressed = old_keys_pressed
keyboard.keys_pressed = oldkeys_pressed
return state
return keyboard
def simple_key_sequence(seq):
@@ -59,10 +61,23 @@ RALT_UP_NO_PRESS = simple_key_sequence((KC.RALT(no_press=True),))
def compile_unicode_string_sequences(string_table):
for k, v in string_table.items():
string_table[k] = unicode_string_sequence(v)
'''
Destructively convert ("compile") unicode strings into key sequences. This
will, for RAM saving reasons, empty the input dictionary and trigger
garbage collection.
'''
target = AttrDict()
return AttrDict(string_table)
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):
@@ -82,9 +97,6 @@ def generate_codepoint_keysym_seq(codepoint, expected_length=4):
# 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
#
# As a bonus, this function can be pretty useful for
# leader dictionary keys as strings.
seq = [KC.N0 for _ in range(max(len(codepoint), expected_length))]
for idx, codepoint_fragment in enumerate(reversed(codepoint)):
@@ -93,47 +105,43 @@ def generate_codepoint_keysym_seq(codepoint, expected_length=4):
return seq
def generate_leader_dictionary_seq(string):
return tuple(generate_codepoint_keysym_seq(string, 1))
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, state, *args, **kwargs):
if state.unicode_mode == UnicodeMode.IBUS:
state._process_key(
simple_key_sequence(_ibus_unicode_sequence(kc_macros, state)), True
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 state.unicode_mode == UnicodeMode.RALT:
state._process_key(
simple_key_sequence(_ralt_unicode_sequence(kc_macros, state)), True
elif keyboard.unicode_mode == UnicodeMode.RALT:
keyboard.process_key(
simple_key_sequence(_ralt_unicode_sequence(kc_macros, keyboard)), True
)
elif state.unicode_mode == UnicodeMode.WINC:
state._process_key(
simple_key_sequence(_winc_unicode_sequence(kc_macros, state)), 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, state):
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, state):
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, state):
def _winc_unicode_sequence(kc_macros, keyboard):
'''
Send unicode sequence using WinCompose:

View File

@@ -1,213 +1,129 @@
from kmk.kmktime import sleep_ms
def passthrough(key, state, *args, **kwargs):
return state
def passthrough(key, keyboard, *args, **kwargs):
return keyboard
def default_pressed(key, state, KC, coord_int=None, coord_raw=None):
state._hid_pending = True
def default_pressed(key, keyboard, KC, coord_int=None, coord_raw=None, *args, **kwargs):
keyboard.hid_pending = True
if coord_int is not None:
state._coord_keys_pressed[coord_int] = key
keyboard._coordkeys_pressed[coord_int] = key
state._keys_pressed.add(key)
keyboard.keys_pressed.add(key)
return state
return keyboard
def default_released(key, state, KC, coord_int=None, coord_raw=None):
state._hid_pending = True
state._keys_pressed.discard(key)
def default_released(
key, keyboard, KC, coord_int=None, coord_raw=None, *args, **kwargs # NOQA
):
keyboard.hid_pending = True
keyboard.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
keyboard.keys_pressed.discard(keyboard._coordkeys_pressed.get(coord_int, None))
keyboard._coordkeys_pressed[coord_int] = None
return state
return keyboard
def reset(*args, **kwargs):
try:
import machine
import microcontroller
machine.reset()
except ImportError:
import microcontroller
microcontroller.reset()
microcontroller.reset()
def bootloader(*args, **kwargs):
try:
import machine
import microcontroller
machine.bootloader()
except ImportError:
import microcontroller
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset()
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset()
def debug_pressed(key, state, KC, *args, **kwargs):
if state.debug_enabled:
def debug_pressed(key, keyboard, KC, *args, **kwargs):
if keyboard.debug_enabled:
print('DebugDisable()')
else:
print('DebugEnable()')
state.debug_enabled = not state.debug_enabled
keyboard.debug_enabled = not keyboard.debug_enabled
return state
return keyboard
def gesc_pressed(key, state, KC, *args, **kwargs):
def gesc_pressed(key, keyboard, KC, *args, **kwargs):
GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI}
if GESC_TRIGGERS.intersection(state._keys_pressed):
if GESC_TRIGGERS.intersection(keyboard.keys_pressed):
# First, release GUI if already pressed
state._send_hid()
keyboard._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
return state
keyboard.keys_pressed.add(KC.GRAVE)
keyboard.hid_pending = True
return keyboard
# else return KC_ESC
state._keys_pressed.add(KC.ESCAPE)
state._hid_pending = True
keyboard.keys_pressed.add(KC.ESCAPE)
keyboard.hid_pending = True
return state
return keyboard
def gesc_released(key, state, KC, *args, **kwargs):
state._keys_pressed.discard(KC.ESCAPE)
state._keys_pressed.discard(KC.GRAVE)
state._hid_pending = True
return state
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, state, KC, *args, **kwargs):
def bkdl_pressed(key, keyboard, KC, *args, **kwargs):
BKDL_TRIGGERS = {KC.LGUI, KC.RGUI}
if BKDL_TRIGGERS.intersection(state._keys_pressed):
state._send_hid()
state._keys_pressed.add(KC.DEL)
state._hid_pending = True
return state
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
state._keys_pressed.add(KC.BKSP)
state._hid_pending = True
keyboard.keys_pressed.add(KC.BKSP)
keyboard.hid_pending = True
return state
return keyboard
def bkdl_released(key, state, KC, *args, **kwargs):
state._keys_pressed.discard(KC.BKSP)
state._keys_pressed.discard(KC.DEL)
state._hid_pending = True
return state
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, state, KC, *args, **kwargs):
def sleep_pressed(key, keyboard, KC, *args, **kwargs):
sleep_ms(key.meta.ms)
return state
return keyboard
def uc_mode_pressed(key, state, *args, **kwargs):
state.unicode_mode = key.meta.mode
def uc_mode_pressed(key, keyboard, *args, **kwargs):
keyboard.unicode_mode = key.meta.mode
return state
return keyboard
def td_pressed(key, state, *args, **kwargs):
return state._process_tap_dance(key, True)
def td_pressed(key, keyboard, *args, **kwargs):
return keyboard._process_tap_dance(key, True)
def td_released(key, state, *args, **kwargs):
return state._process_tap_dance(key, False)
def td_released(key, keyboard, *args, **kwargs):
return keyboard._process_tap_dance(key, False)
def rgb_tog(key, state, *args, **kwargs):
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.pixels.increase_hue()
return state
def rgb_hud(key, state, *args, **kwargs):
state.pixels.decrease_hue()
return state
def rgb_sai(key, state, *args, **kwargs):
state.pixels.increase_sat()
return state
def rgb_sad(key, state, *args, **kwargs):
state.pixels.decrease_sat()
return state
def rgb_vai(key, state, *args, **kwargs):
state.pixels.increase_val()
return state
def rgb_vad(key, state, *args, **kwargs):
state.pixels.decrease_val()
return state
def rgb_ani(key, state, *args, **kwargs):
state.pixels.increase_ani()
return state
def rgb_and(key, state, *args, **kwargs):
state.pixels.decrease_ani()
return state
def rgb_mode_static(key, state, *args, **kwargs):
state.pixels.effect_init = True
state.pixels.animation_mode = 'static'
return state
def rgb_mode_breathe(key, state, *args, **kwargs):
state.pixels.effect_init = True
state.pixels.animation_mode = 'breathing'
return state
def rgb_mode_breathe_rainbow(key, state, *args, **kwargs):
state.pixels.effect_init = True
state.pixels.animation_mode = 'breathing_rainbow'
return state
def rgb_mode_rainbow(key, state, *args, **kwargs):
state.pixels.effect_init = True
state.pixels.animation_mode = 'rainbow'
return state
def rgb_mode_swirl(key, state, *args, **kwargs):
state.pixels.effect_init = True
state.pixels.animation_mode = 'swirl'
return state
def rgb_mode_knight(key, state, *args, **kwargs):
state.pixels.effect_init = True
state.pixels.animation_mode = 'knight'
return state
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

View File

@@ -1,12 +1,22 @@
import usb_hid
from micropython import const
from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey
from storage import getmount
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 # currently unused; for bluetooth
BLE = 2
ALL_MODES = (NOOP, USB, BLE)
@@ -41,7 +51,7 @@ HID_REPORT_SIZES = {
class AbstractHID:
REPORT_BYTES = 8
def __init__(self, **kwargs):
def __init__(self):
self._evt = bytearray(self.REPORT_BYTES)
self.report_device = memoryview(self._evt)[0:1]
self.report_device[0] = HIDReportTypes.KEYBOARD
@@ -55,7 +65,7 @@ class AbstractHID:
self.report_mods = memoryview(self._evt)[1:2]
self.report_non_mods = memoryview(self._evt)[3:]
self.post_init(**kwargs)
self.post_init()
def __repr__(self):
return '{}(REPORT_BYTES={})'.format(self.__class__.__name__, self.REPORT_BYTES)
@@ -191,7 +201,7 @@ class AbstractHID:
class USBHID(AbstractHID):
REPORT_BYTES = 9
def post_init(self, **kwargs):
def post_init(self):
self.devices = {}
for device in usb_hid.devices:
@@ -221,3 +231,100 @@ class USBHID(AbstractHID):
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 post_init(self, ble_name=str(getmount('/').label), **kwargs):
self.conn_id = -1
self.ble = BLERadio()
self.ble.name = 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()
self.conn_id = 0
@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 = []
# Security issue:
# This introduces a race condition. Let's say you have 2 active
# connections: Alice and Bob - Alice is connection 1 and Bob 2.
# Now Chuck who has already paired with the device in the past
# (this assumption is needed only in the case of LESC)
# wants to gather the keystrokes you send to Alice. You have
# selected right now to talk to Alice (1) and you're typing a secret.
# If Chuck kicks Alice off and is quick enough to connect to you,
# which means quicker than the running interval of this function,
# he'll be earlier in the `self.hid.devices` so will take over the
# selected 1 position in the resulted array.
# If no LESC is in place, Chuck can sniff the keystrokes anyway
for device in self.hid.devices:
if hasattr(device, 'send_report'):
result.append(device)
return result
def _check_connection(self):
devices = self.devices
if not devices:
return False
if self.conn_id >= len(devices):
self.conn_id = len(devices) - 1
if self.conn_id < 0:
return False
if not devices[self.conn_id]:
return False
return True
def hid_send(self, evt):
if not self._check_connection():
return
device = self.devices[self.conn_id]
while len(evt) < len(device._characteristic.value) + 1:
evt.append(0)
return device.send_report(evt[1:])
def clear_bonds(self):
import _bleio
_bleio.adapter.erase_bonding()
def next_connection(self):
self.conn_id = (self.conn_id + 1) % len(self.devices)
def previous_connection(self):
self.conn_id = (self.conn_id - 1) % len(self.devices)
def start_advertising(self):
advertisement = ProvideServicesAdvertisement(self.hid)
advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD
self.ble.start_advertising(advertisement)
def stop_advertising(self):
self.ble.stop_advertising()

View File

@@ -1,22 +1,20 @@
import kmk.handlers.layers as layers
import kmk.handlers.modtap as modtap
from micropython import const
import kmk.handlers.stock as handlers
from kmk.consts import UnicodeMode
from kmk.key_validators import (
key_seq_sleep_validator,
layer_key_validator,
mod_tap_validator,
tap_dance_key_validator,
unicode_mode_key_validator,
)
from kmk.types import AttrDict, UnicodeModeKeyMeta
FIRST_KMK_INTERNAL_KEY = 1000
FIRST_KMK_INTERNAL_KEY = const(1000)
NEXT_AVAILABLE_KEY = 1000
KEY_SIMPLE = 0
KEY_MODIFIER = 1
KEY_CONSUMER = 2
KEY_SIMPLE = const(0)
KEY_MODIFIER = const(1)
KEY_CONSUMER = const(2)
# Global state, will be filled in througout this file, and
# anywhere the user creates custom keys
@@ -62,7 +60,7 @@ class Key:
def __repr__(self):
return 'Key(code={}, has_modifiers={})'.format(self.code, self.has_modifiers)
def _on_press(self, state, coord_int, coord_raw):
def on_press(self, state, coord_int, coord_raw):
for fn in self._pre_press_handlers:
if not fn(self, state, KC, coord_int, coord_raw):
return None
@@ -74,7 +72,7 @@ class Key:
return ret
def _on_release(self, state, coord_int, coord_raw):
def on_release(self, state, coord_int, coord_raw):
for fn in self._pre_release_handlers:
if not fn(self, state, KC, coord_int, coord_raw):
return None
@@ -202,7 +200,7 @@ class ModifierKey(Key):
# FIXME this is atrocious to read. Please, please, please, strike down upon
# this with great vengeance and furious anger.
FAKE_CODE = -1
FAKE_CODE = const(-1)
def __call__(self, modified_code=None, no_press=None, no_release=None):
if modified_code is None and no_press is None and no_release is None:
@@ -610,66 +608,6 @@ make_key(
on_press=handlers.gesc_pressed,
on_release=handlers.gesc_released,
)
make_key(names=('RGB_TOG',), on_press=handlers.rgb_tog)
make_key(names=('RGB_HUI',), on_press=handlers.rgb_hui)
make_key(names=('RGB_HUD',), on_press=handlers.rgb_hud)
make_key(names=('RGB_SAI',), on_press=handlers.rgb_sai)
make_key(names=('RGB_SAD',), on_press=handlers.rgb_sad)
make_key(names=('RGB_VAI',), on_press=handlers.rgb_vai)
make_key(names=('RGB_VAD',), on_press=handlers.rgb_vad)
make_key(names=('RGB_ANI',), on_press=handlers.rgb_ani)
make_key(names=('RGB_AND',), on_press=handlers.rgb_and)
make_key(names=('RGB_MODE_PLAIN', 'RGB_M_P'), on_press=handlers.rgb_mode_static)
make_key(names=('RGB_MODE_BREATHE', 'RGB_M_B'), on_press=handlers.rgb_mode_breathe)
make_key(names=('RGB_MODE_RAINBOW', 'RGB_M_R'), on_press=handlers.rgb_mode_rainbow)
make_key(
names=('RGB_MODE_BREATHE_RAINBOW', 'RGB_M_BR'),
on_press=handlers.rgb_mode_breathe_rainbow,
)
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)
# Layers
make_argumented_key(
validator=layer_key_validator,
names=('MO',),
on_press=layers.mo_pressed,
on_release=layers.mo_released,
)
make_argumented_key(
validator=layer_key_validator, names=('DF',), on_press=layers.df_pressed
)
make_argumented_key(
validator=layer_key_validator,
names=('LM',),
on_press=layers.lm_pressed,
on_release=layers.lm_released,
)
make_argumented_key(
validator=layer_key_validator,
names=('LT',),
on_press=layers.lt_pressed,
on_release=layers.lt_released,
)
make_argumented_key(
validator=layer_key_validator, names=('TG',), on_press=layers.tg_pressed
)
make_argumented_key(
validator=layer_key_validator, names=('TO',), on_press=layers.to_pressed
)
make_argumented_key(
validator=layer_key_validator,
names=('TT',),
on_press=layers.tt_pressed,
on_release=layers.tt_released,
)
make_argumented_key(
validator=mod_tap_validator,
names=('MT',),
on_press=modtap.mt_pressed,
on_release=modtap.mt_released,
)
# A dummy key to trigger a sleep_ms call in a sequence of other keys in a
# simple sequence macro.
@@ -711,3 +649,4 @@ make_argumented_key(
on_press=handlers.td_pressed,
on_release=handlers.td_released,
)
make_key(names=('HID_SWITCH', 'HID'), on_press=handlers.hid_switch)

View File

@@ -1,11 +1,3 @@
# There's a chance doing preload RAM hacks this late will cause recursion
# errors, but we'll see. I'd rather do it here than require everyone copy-paste
# a line into their keymaps.
import kmk.preload_imports # isort:skip # NOQA
import gc
from kmk import rgb
from kmk.consts import KMK_RELEASE, UnicodeMode
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
from kmk.keys import KC
@@ -14,62 +6,80 @@ from kmk.matrix import MatrixScanner, intify_coordinate
from kmk.types import TapDanceKeyMeta
class Sandbox:
matrix_update = None
secondary_matrix_update = None
active_layers = None
class KMKKeyboard:
#####
# User-configurable
debug_enabled = False
keymap = None
keymap = []
coord_mapping = None
row_pins = None
col_pins = None
diode_orientation = None
matrix = None
matrix_scanner = MatrixScanner
uart_buffer = []
unicode_mode = UnicodeMode.NOOP
tap_time = 300
# RGB config
rgb_pixel_pin = None
rgb_config = rgb.rgb_config
modules = []
extensions = []
sandbox = Sandbox()
#####
# Internal State
_keys_pressed = set()
_coord_keys_pressed = {}
_hid_pending = False
keys_pressed = set()
_coordkeys_pressed = {}
hid_type = HIDModes.USB
secondary_hid_type = None
_hid_helper = None
hid_pending = False
state_layer_key = None
matrix_update = None
secondary_matrix_update = None
_matrix_modify = None
state_changed = False
_old_timeouts_len = None
_new_timeouts_len = None
_trigger_powersave_enable = False
_trigger_powersave_disable = False
i2c_deinit_count = 0
# 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]
active_layers = [0]
_start_time = {'lt': None, 'tg': None, 'tt': None, 'lm': None}
_timeouts = {}
_tapping = False
_tap_dance_counts = {}
_tap_side_effects = {}
# on some M4 setups (such as klardotsh/klarank_feather_m4, CircuitPython
# 6.0rc1) this runs out of RAM every cycle and takes down the board. no
# real known fix yet other than turning off debug, but M4s have always been
# tight on RAM so....
def __repr__(self):
return (
'KMKKeyboard('
'debug_enabled={} '
'keymap=truncated '
'coord_mapping=truncated '
'row_pins=truncated '
'col_pins=truncated '
'diode_orientation={} '
'matrix_scanner={} '
'unicode_mode={} '
'tap_time={} '
'hid_helper={} '
'_hid_helper={} '
'keys_pressed={} '
'coord_keys_pressed={} '
'coordkeys_pressed={} '
'hid_pending={} '
'active_layers={} '
'start_time={} '
'timeouts={} '
'tapping={} '
'tap_dance_counts={} '
@@ -77,21 +87,16 @@ class KMKKeyboard:
')'
).format(
self.debug_enabled,
# self.keymap,
# self.coord_mapping,
# self.row_pins,
# self.col_pins,
self.diode_orientation,
self.matrix_scanner,
self.unicode_mode,
self.tap_time,
self.hid_helper.__name__,
self._hid_helper,
# internal state
self._keys_pressed,
self._coord_keys_pressed,
self._hid_pending,
self._active_layers,
self._start_time,
self.keys_pressed,
self._coordkeys_pressed,
self.hid_pending,
self.active_layers,
self._timeouts,
self._tapping,
self._tap_dance_counts,
@@ -99,126 +104,81 @@ class KMKKeyboard:
)
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._keys_pressed).send()
self._hid_pending = False
self._hid_helper.create_report(self.keys_pressed).send()
self.hid_pending = False
def _handle_matrix_report(self, update=None):
if update is not None:
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:
if self.uart.in_waiting >= 60:
# This is a dirty hack to prevent crashes in unrealistic cases
import microcontroller
microcontroller.reset()
while self._uart.in_waiting >= 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())
return None
return update
return None
def _send_debug(self, message):
'''
Prepends DEB and appends a newline to allow debug messages to
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')
#####
# SPLICE: INTERNAL STATE
# FIXME CLEAN THIS
#####
def _find_key_in_map(self, row, col):
ic = intify_coordinate(row, col)
def _find_key_in_map(self, int_coord, row, col):
self.state_layer_key = None
try:
idx = self.coord_mapping.index(ic)
idx = self.coord_mapping.index(int_coord)
except ValueError:
if self.debug_enabled:
print(
'CoordMappingNotFound(ic={}, row={}, col={})'.format(ic, row, col)
'CoordMappingNotFound(ic={}, row={}, col={})'.format(
int_coord, row, col
)
)
return None
for layer in self._active_layers:
layer_key = self.keymap[layer][idx]
for layer in self.active_layers:
self.state_layer_key = self.keymap[layer][idx]
if not layer_key or layer_key == KC.TRNS:
if not self.state_layer_key or self.state_layer_key == KC.TRNS:
continue
if self.debug_enabled:
print('KeyResolution(key={})'.format(layer_key))
print('KeyResolution(key={})'.format(self.state_layer_key))
return layer_key
return self.state_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)
kc_changed = self._find_key_in_map(int_coord, 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))
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):
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)
key.on_press(self, coord_int, coord_raw)
else:
key._on_release(self, coord_int, coord_raw)
key.on_release(self, coord_int, coord_raw)
return self
def _remove_key(self, keycode):
self._keys_pressed.discard(keycode)
return self._process_key(keycode, False)
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 add_key(self, keycode):
self.keys_pressed.add(keycode)
return self.process_key(keycode, True)
def _tap_key(self, keycode):
self._add_key(keycode)
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))
self.set_timeout(False, lambda: self.remove_key(keycode))
return self
@@ -239,7 +199,7 @@ class KMKKeyboard:
or not self._tap_dance_counts[changed_key]
):
self._tap_dance_counts[changed_key] = 1
self._set_timeout(
self.set_timeout(
self.tap_time, lambda: self._end_tap_dance(changed_key)
)
self._tapping = True
@@ -263,19 +223,19 @@ class KMKKeyboard:
v = self._tap_dance_counts[td_key] - 1
if v >= 0:
if td_key in self._keys_pressed:
if td_key in self.keys_pressed:
key_to_press = td_key.codes[v]
self._add_key(key_to_press)
self.add_key(key_to_press)
self._tap_side_effects[td_key] = key_to_press
self._hid_pending = True
self.hid_pending = True
else:
if self._tap_side_effects[td_key]:
self._remove_key(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.hid_pending = True
self._cleanup_tap_dance(td_key)
else:
self._tap_key(td_key.codes[v])
self.tap_key(td_key.codes[v])
self._cleanup_tap_dance(td_key)
return self
@@ -285,7 +245,7 @@ class KMKKeyboard:
self._tapping = any(count > 0 for count in self._tap_dance_counts.values())
return self
def _set_timeout(self, after_ticks, callback):
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()
@@ -319,11 +279,6 @@ class KMKKeyboard:
return self
#####
# SPLICE END: INTERNAL STATE
# TODO FIXME REMOVE THIS
#####
def _init_sanity_check(self):
'''
Ensure the provided configuration is *probably* bootable
@@ -347,9 +302,7 @@ class KMKKeyboard:
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
):
if any(x.__class__.__module__ == 'kmk.modules.split' for x in self.modules):
return
if not self.coord_mapping:
@@ -364,25 +317,14 @@ class KMKKeyboard:
def _init_hid(self):
if self.hid_type == HIDModes.NOOP:
self.hid_helper = AbstractHID
self._hid_helper = AbstractHID
elif self.hid_type == HIDModes.USB:
try:
from kmk.hid import USBHID
self.hid_helper = USBHID
except ImportError:
self.hid_helper = AbstractHID
print('USB HID is unsupported ')
self._hid_helper = USBHID
elif self.hid_type == HIDModes.BLE:
try:
from kmk.ble import BLEHID
self.hid_helper = BLEHID
except ImportError:
self.hid_helper = AbstractHID
print('Bluetooth is unsupported ')
self._hid_helper_inst = self.hid_helper(**kwargs)
self._hid_helper = BLEHID
else:
self._hid_helper = AbstractHID
self._hid_helper = self._hid_helper()
def _init_matrix(self):
self.matrix = MatrixScanner(
@@ -394,28 +336,115 @@ class KMKKeyboard:
return self
def go(self, hid_type=HIDModes.USB, **kwargs):
self._extensions = [] + getattr(self, 'extensions', [])
def before_matrix_scan(self):
for module in self.modules:
try:
module.before_matrix_scan(self)
except Exception as err:
if self.debug_enabled:
print('Failed to run pre matrix function in module: ', err, module)
try:
del self.extensions
except Exception:
pass
finally:
gc.collect()
for ext in self.extensions:
try:
ext.before_matrix_scan(self.sandbox)
except Exception as err:
if self.debug_enabled:
print('Failed to run pre matrix function in extension: ', err, ext)
def after_matrix_scan(self):
for module in self.modules:
try:
module.after_matrix_scan(self)
except Exception as err:
if self.debug_enabled:
print('Failed to run post matrix function in module: ', err, module)
for ext in self.extensions:
try:
ext.after_matrix_scan(self.sandbox)
except Exception as err:
if self.debug_enabled:
print('Failed to run post matrix function in extension: ', err, ext)
def before_hid_send(self):
for module in self.modules:
try:
module.before_hid_send(self)
except Exception as err:
if self.debug_enabled:
print('Failed to run pre hid function in module: ', err, module)
for ext in self.extensions:
try:
ext.before_hid_send(self.sandbox)
except Exception as err:
if self.debug_enabled:
print('Failed to run pre hid function in extension: ', err, ext)
def after_hid_send(self):
for module in self.modules:
try:
module.after_hid_send(self)
except Exception as err:
if self.debug_enabled:
print('Failed to run post hid function in module: ', err, module)
for ext in self.extensions:
try:
ext.after_hid_send(self.sandbox)
except Exception as err:
if self.debug_enabled:
print('Failed to run post hid function in extension: ', err, ext)
def powersave_enable(self):
for module in self.modules:
try:
module.on_powersave_enable(self)
except Exception as err:
if self.debug_enabled:
print('Failed to run post hid function in module: ', err, module)
for ext in self.extensions:
try:
ext.on_powersave_enable(self.sandbox)
except Exception as err:
if self.debug_enabled:
print('Failed to run post hid function in extension: ', err, ext)
def powersave_disable(self):
for module in self.modules:
try:
module.on_powersave_disable(self)
except Exception as err:
if self.debug_enabled:
print('Failed to run post hid function in module: ', err, module)
for ext in self.extensions:
try:
ext.on_powersave_disable(self.sandbox)
except Exception as err:
if self.debug_enabled:
print('Failed to run post hid function in extension: ', err, ext)
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs):
self.hid_type = hid_type
self.secondary_hid_type = secondary_hid_type
self._init_sanity_check()
self._init_coord_mapping()
self._init_hid()
for ext in self._extensions:
for module in self.modules:
try:
module.during_bootup(self)
except Exception:
if self.debug_enabled:
print('Failed to load module', module)
for ext in self.extensions:
try:
ext.during_bootup(self)
except Exception:
# TODO FIXME log the exceptions or something
pass
if self.debug_enabled:
print('Failed to load extention', ext)
self._init_matrix()
@@ -423,48 +452,43 @@ class KMKKeyboard:
while True:
self.state_changed = False
self.sandbox.active_layers = self.active_layers.copy()
for ext in self._extensions:
try:
self._handle_matrix_report(ext.before_matrix_scan(self))
except Exception as e:
print(e)
self.before_matrix_scan()
matrix_update = self.matrix.scan_for_changes()
self._handle_matrix_report(matrix_update)
self.matrix_update = (
self.sandbox.matrix_update
) = self.matrix.scan_for_changes()
self.sandbox.secondary_matrix_update = self.secondary_matrix_update
for ext in self._extensions:
try:
ext.after_matrix_scan(self, matrix_update)
except Exception as e:
print(e)
self.after_matrix_scan()
for ext in self._extensions:
try:
ext.before_hid_send(self)
except Exception:
# TODO FIXME log the exceptions or something
pass
self._handle_matrix_report(self.secondary_matrix_update)
self.secondary_matrix_update = None
self._handle_matrix_report(self.matrix_update)
self.matrix_update = None
if self._hid_pending:
self.before_hid_send()
if self.hid_pending:
self._send_hid()
old_timeouts_len = len(self._timeouts)
self._old_timeouts_len = len(self._timeouts)
self._process_timeouts()
new_timeouts_len = len(self._timeouts)
self._new_timeouts_len = len(self._timeouts)
if old_timeouts_len != new_timeouts_len:
if self._old_timeouts_len != self._new_timeouts_len:
self.state_changed = True
if self._hid_pending:
if self.hid_pending:
self._send_hid()
for ext in self._extensions:
try:
ext.after_hid_send(self)
except Exception:
# TODO FIXME log the exceptions or something
pass
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()

View File

@@ -1,29 +1,23 @@
import math
import time
USE_UTIME = False
def sleep_ms(ms):
'''
Tries to sleep for a number of milliseconds in a cross-implementation
way. Will raise an ImportError if time is not available on the platform.
'''
if USE_UTIME:
return time.sleep_ms(ms)
else:
return time.sleep(ms / 1000)
return time.sleep(ms / 1000)
def ticks_ms():
if USE_UTIME:
return time.ticks_ms()
else:
return math.floor(time.monotonic() * 1000)
'''Has .25s granularity, but is cheap'''
return time.monotonic() * 1000
def ticks_diff(new, old):
if USE_UTIME:
return time.ticks_diff(new, old)
else:
return new - old
return new - old
def accurate_ticks():
'''Is more expensive, but good for time critical things'''
return time.monotonic_ns()
def accurate_ticks_diff(new, old, ms):
return bool(new - old < ms * 1000000)

40
kmk/modules/__init__.py Normal file
View File

@@ -0,0 +1,40 @@
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 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

191
kmk/modules/layers.py Normal file
View File

@@ -0,0 +1,191 @@
'''One layer isn't enough. Adds keys to get to more of them'''
from micropython import const
from kmk.key_validators import layer_key_validator
from kmk.keys import make_argumented_key
from kmk.kmktime import accurate_ticks, accurate_ticks_diff
from kmk.modules import Module
class LayerType:
'''Defines layer type values for readability'''
MO = const(0)
DF = const(1)
LM = const(2)
LT = const(3)
TG = const(4)
TT = const(5)
class Layers(Module):
'''Gives access to the keys used to enable the layer system'''
def __init__(self):
# Layers
self.start_time = {
LayerType.LT: None,
LayerType.TG: None,
LayerType.TT: None,
LayerType.LM: None,
}
make_argumented_key(
validator=layer_key_validator,
names=('MO',),
on_press=self._mo_pressed,
on_release=self._mo_released,
)
make_argumented_key(
validator=layer_key_validator, names=('DF',), on_press=self._df_pressed
)
make_argumented_key(
validator=layer_key_validator,
names=('LM',),
on_press=self._lm_pressed,
on_release=self._lm_released,
)
make_argumented_key(
validator=layer_key_validator,
names=('LT',),
on_press=self._lt_pressed,
on_release=self._lt_released,
)
make_argumented_key(
validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed
)
make_argumented_key(
validator=layer_key_validator, names=('TO',), on_press=self._to_pressed
)
make_argumented_key(
validator=layer_key_validator,
names=('TT',),
on_press=self._tt_pressed,
on_release=self._tt_released,
)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def _df_pressed(self, key, keyboard, *args, **kwargs):
'''
Switches the default layer
'''
keyboard.active_layers[-1] = key.meta.layer
def _mo_pressed(self, key, keyboard, *args, **kwargs):
'''
Momentarily activates layer, switches off when you let go
'''
keyboard.active_layers.insert(0, key.meta.layer)
@staticmethod
def _mo_released(key, keyboard, *args, **kwargs):
# remove the first instance of the target layer
# from the active list
# under almost all normal use cases, this will
# disable the layer (but preserve it if it was triggered
# as a default layer, etc.)
# this also resolves an issue where using DF() on a layer
# triggered by MO() and then defaulting to the MO()'s layer
# would result in no layers active
try:
del_idx = keyboard.active_layers.index(key.meta.layer)
del keyboard.active_layers[del_idx]
except ValueError:
pass
def _lm_pressed(self, key, keyboard, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
keyboard.hid_pending = True
# Sets the timer start and acts like MO otherwise
keyboard.keys_pressed.add(key.meta.kc)
self._mo_pressed(key, keyboard, *args, **kwargs)
def _lm_released(self, key, keyboard, *args, **kwargs):
'''
As MO(layer) but with mod active
'''
keyboard.hid_pending = True
keyboard.keys_pressed.discard(key.meta.kc)
self._mo_released(key, keyboard, *args, **kwargs)
def _lt_pressed(self, key, keyboard, *args, **kwargs):
# Sets the timer start and acts like MO otherwise
self.start_time[LayerType.LT] = accurate_ticks()
self._mo_pressed(key, keyboard, *args, **kwargs)
def _lt_released(self, key, keyboard, *args, **kwargs):
# On keyup, check timer, and press key if needed.
if self.start_time[LayerType.LT] and (
accurate_ticks_diff(
accurate_ticks(), self.start_time[LayerType.LT], keyboard.tap_time
)
):
keyboard.hid_pending = True
keyboard.tap_key(key.meta.kc)
self._mo_released(key, keyboard, *args, **kwargs)
self.start_time[LayerType.LT] = None
def _tg_pressed(self, key, keyboard, *args, **kwargs):
'''
Toggles the layer (enables it if not active, and vise versa)
'''
# See mo_released for implementation details around this
try:
del_idx = keyboard.active_layers.index(key.meta.layer)
del keyboard.active_layers[del_idx]
except ValueError:
keyboard.active_layers.insert(0, key.meta.layer)
def _to_pressed(self, key, keyboard, *args, **kwargs):
'''
Activates layer and deactivates all other layers
'''
keyboard.active_layers.clear()
keyboard.active_layers.insert(0, key.meta.layer)
def _tt_pressed(self, key, keyboard, *args, **kwargs):
'''
Momentarily activates layer if held, toggles it if tapped repeatedly
'''
if self.start_time[LayerType.TT] is None:
# Sets the timer start and acts like MO otherwise
self.start_time[LayerType.TT] = accurate_ticks()
self._mo_pressed(key, keyboard, *args, **kwargs)
elif accurate_ticks_diff(
accurate_ticks(), self.start_time[LayerType.TT], keyboard.tap_time
):
self.start_time[LayerType.TT] = None
self._tg_pressed(key, keyboard, *args, **kwargs)
return
return
def _tt_released(self, key, keyboard, *args, **kwargs):
if self.start_time[LayerType.TT] is None or not accurate_ticks_diff(
accurate_ticks(), self.start_time[LayerType.TT], keyboard.tap_time
):
# On first press, works like MO. On second press, does nothing unless let up within
# time window, then acts like TG.
self.start_time[LayerType.TT] = None
self._mo_released(key, keyboard, *args, **kwargs)

57
kmk/modules/modtap.py Normal file
View File

@@ -0,0 +1,57 @@
from kmk.key_validators import mod_tap_validator
from kmk.keys import make_argumented_key
from kmk.kmktime import accurate_ticks, accurate_ticks_diff
from kmk.modules import Module
class ModTap(Module):
def __init__(self):
self._mod_tap_timer = None
make_argumented_key(
validator=mod_tap_validator,
names=('MT',),
on_press=self.mt_pressed,
on_release=self.mt_released,
)
def during_bootup(self, keyboard):
return
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
return
def on_powersave_disable(self, keyboard):
return
def mt_pressed(self, key, keyboard, *args, **kwargs):
'''Sets the timer start and acts like a modifier otherwise'''
keyboard.keys_pressed.add(key.meta.mods)
self._mod_tap_timer = accurate_ticks()
return keyboard
def mt_released(self, key, keyboard, *args, **kwargs):
''' On keyup, check timer, and press key if needed.'''
keyboard.keys_pressed.discard(key.meta.mods)
if self._mod_tap_timer and (
accurate_ticks_diff(
accurate_ticks(), self._mod_tap_timer, keyboard.tap_time
)
):
keyboard.hid_pending = True
keyboard.tap_key(key.meta.kc)
self._mod_tap_timer = None
return keyboard

146
kmk/modules/power.py Normal file
View File

@@ -0,0 +1,146 @@
import board
import digitalio
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key
from kmk.kmktime import sleep_ms, ticks_diff, ticks_ms
from kmk.modules import Module
class Power(Module):
def __init__(self, powersave_pin=None):
self.enable = False
self.powersave_pin = powersave_pin # Powersave pin board object
self._powersave_start = ticks_ms()
self._usb_last_scan = ticks_ms() - 5000
self._psp = None # Powersave pin object
self._i2c = 0
self._loopcounter = 0
make_key(
names=('PS_TOG',), on_press=self._ps_tog, on_release=handler_passthrough
)
make_key(
names=('PS_ON',), on_press=self._ps_enable, on_release=handler_passthrough
)
make_key(
names=('PS_OFF',), on_press=self._ps_disable, on_release=handler_passthrough
)
def __repr__(self):
return f'Power({self._to_dict()})'
def _to_dict(self):
return {
'enable': self.enable,
'powersave_pin': self.powersave_pin,
'_powersave_start': self._powersave_start,
'_usb_last_scan': self._usb_last_scan,
'_psp': self._psp,
}
def during_bootup(self, keyboard):
self._i2c_scan()
def before_matrix_scan(self, keyboard):
return
def after_matrix_scan(self, keyboard):
if keyboard.matrix_update or keyboard.secondary_matrix_update:
self.psave_time_reset()
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
if self.enable:
self.psleep()
def on_powersave_enable(self, keyboard):
'''Gives 10 cycles to allow other extentions to clean up before powersave'''
if self._loopcounter > 10:
self.enable_powersave(keyboard)
self._loopcounter = 0
else:
self._loopcounter += 1
return
def on_powersave_disable(self, keyboard):
self.disable_powersave(keyboard)
return
def enable_powersave(self, keyboard):
'''Enables power saving features'''
if keyboard.i2c_deinit_count >= self._i2c and self.powersave_pin:
# Allows power save to prevent RGB drain.
# Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic
if not self._psp:
self._psp = digitalio.DigitalInOut(self.powersave_pin)
self._psp.direction = digitalio.Direction.OUTPUT
if self._psp:
self._psp.value = True
self.enable = True
keyboard._trigger_powersave_enable = False
return
def disable_powersave(self, keyboard):
'''Disables power saving features'''
if self._psp:
self._psp.value = False
# Allows power save to prevent RGB drain.
# Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic
keyboard._trigger_powersave_disable = False
self.enable = False
return
def psleep(self):
'''
Sleeps longer and longer to save power the more time in between updates.
'''
if ticks_diff(ticks_ms(), self._powersave_start) <= 60000:
sleep_ms(8)
elif ticks_diff(ticks_ms(), self._powersave_start) >= 240000:
sleep_ms(180)
return
def psave_time_reset(self):
self._powersave_start = ticks_ms()
def _i2c_scan(self):
i2c = board.I2C()
while not i2c.try_lock():
pass
try:
self._i2c = len(i2c.scan())
finally:
i2c.unlock()
return
def usb_rescan_timer(self):
return bool(ticks_diff(ticks_ms(), self._usb_last_scan) > 5000)
def usb_time_reset(self):
self._usb_last_scan = ticks_ms()
return
def usb_scan(self):
# TODO Add USB detection here. Currently lies that it's connected
# https://github.com/adafruit/circuitpython/pull/3513
return True
def _ps_tog(self, key, keyboard, *args, **kwargs):
if self.enable:
keyboard._trigger_powersave_disable = True
else:
keyboard._trigger_powersave_enable = True
def _ps_enable(self, key, keyboard, *args, **kwargs):
if not self.enable:
keyboard._trigger_powersave_enable = True
def _ps_disable(self, key, keyboard, *args, **kwargs):
if self.enable:
keyboard._trigger_powersave_disable = True

308
kmk/modules/split.py Normal file
View File

@@ -0,0 +1,308 @@
'''Enables splitting keyboards wirelessly or wired'''
import busio
from micropython import const
from kmk.hid import HIDModes
from kmk.kmktime import ticks_diff, ticks_ms
from kmk.matrix import intify_coordinate
from kmk.modules import Module
from storage import getmount
class SplitSide:
LEFT = const(1)
RIGHT = const(2)
class SplitType:
UART = const(1)
I2C = const(2) # unused
ONEWIRE = const(3) # unused
BLE = const(4)
class Split(Module):
'''Enables splitting keyboards wirelessly, or wired'''
def __init__(
self,
split_flip=True,
split_side=None,
split_type=SplitType.UART,
split_target_left=True,
uart_interval=20,
data_pin=None,
data_pin2=None,
target_left=True,
uart_flip=True,
):
self._is_target = True
self._uart_buffer = []
self.split_flip = split_flip
self.split_side = split_side
self.split_type = split_type
self.split_target_left = split_target_left
self.split_offset = None
self.data_pin = data_pin
self.data_pin2 = data_pin2
self.target_left = target_left
self.uart_flip = uart_flip
self._is_target = True
self._uart = None
self._uart_interval = uart_interval
self._debug_enabled = False
if self.split_type == SplitType.BLE:
try:
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import (
ProvideServicesAdvertisement,
)
from adafruit_ble.services.nordic import UARTService
self.ProvideServicesAdvertisement = ProvideServicesAdvertisement
self.UARTService = UARTService
except ImportError:
pass # BLE isn't supported on this platform
self._ble = BLERadio()
self._ble_last_scan = ticks_ms() - 5000
self._connection_count = 0
self._uart_connection = None
self._advertisment = None
self._advertising = False
self._psave_enable = False
def __repr__(self):
return f'BLE_SPLIT({self._to_dict()})'
def _to_dict(self):
return {
'_ble': self._ble,
'_ble_last_scan': self._ble_last_scan,
'_is_target': self._is_target,
'uart_buffer': self._uart_buffer,
'_split_flip': self.split_flip,
'_split_side': self.split_side,
}
def during_bootup(self, keyboard):
# Set up name for target side detection and BLE advertisment
name = str(getmount('/').label)
if self.split_type == SplitType.BLE:
self._ble.name = name
else:
# Try to guess data pins if not supplied
if not self.data_pin:
self.data_pin = keyboard.data_pin
# Detect split side from name
if self.split_side is None:
if name.endswith('L'):
# If name ends in 'L' assume left and strip from name
self._is_target = bool(self.split_target_left)
self.split_side = SplitSide.LEFT
elif name.endswith('R'):
# If name ends in 'R' assume right and strip from name
self._is_target = not bool(self.split_target_left)
self.split_side = SplitSide.RIGHT
# if split side was given, find master from split_side.
elif self.split_side == SplitSide.LEFT:
self._is_target = bool(self.split_target_left)
elif self.split_side == SplitSide.RIGHT:
self._is_target = not bool(self.split_target_left)
# Flips the col pins if PCB is the same but flipped on right
if self.split_flip and self.split_side == SplitSide.RIGHT:
keyboard.col_pins = list(reversed(keyboard.col_pins))
self.split_offset = len(keyboard.col_pins)
if self.split_type == SplitType.UART and self.data_pin is not None:
if self._is_target:
self._uart = busio.UART(
tx=self.data_pin2, rx=self.data_pin, timeout=self._uart_interval
)
else:
self._uart = busio.UART(
tx=self.data_pin, rx=self.data_pin2, timeout=self._uart_interval
)
# Attempt to sanely guess a coord_mapping if one is not provided.
if not keyboard.coord_mapping:
keyboard.coord_mapping = []
rows_to_calc = len(keyboard.row_pins) * 2
cols_to_calc = len(keyboard.col_pins) * 2
for ridx in range(rows_to_calc):
for cidx in range(cols_to_calc):
keyboard.coord_mapping.append(intify_coordinate(ridx, cidx))
def before_matrix_scan(self, keyboard):
if self.split_type == SplitType.BLE:
self._check_all_connections(keyboard._hid_helper)
self._receive_ble(keyboard)
elif self.split_type == SplitType.UART:
if self._is_target or self.data_pin2:
self._receive_uart(keyboard)
elif self.split_type == SplitType.ONEWIRE:
pass # Protocol needs written
return
def after_matrix_scan(self, keyboard):
if keyboard.matrix_update:
if self.split_type == SplitType.BLE:
self._send_ble(keyboard.matrix_update)
elif self.split_type == SplitType.UART and self.data_pin2:
self._send_uart(keyboard.matrix_update)
elif self.split_type == SplitType.ONEWIRE:
pass # Protocol needs written
return
def before_hid_send(self, keyboard):
return
def after_hid_send(self, keyboard):
return
def on_powersave_enable(self, keyboard):
if self.split_type == SplitType.BLE:
if self._uart_connection and not self._psave_enable:
self._uart_connection.connection_interval = self._uart_interval
self._psave_enable = True
def on_powersave_disable(self, keyboard):
if self.split_type == SplitType.BLE:
if self._uart_connection and self._psave_enable:
self._uart_connection.connection_interval = 11.25
self._psave_enable = False
def _check_all_connections(self, hid_type):
'''Validates the correct number of BLE connections'''
self._connection_count = len(self._ble.connections)
if self._is_target and hid_type == HIDModes.BLE and self._connection_count < 2:
self._target_advertise()
elif not self._is_target and self._connection_count < 1:
self._initiator_scan()
def _initiator_scan(self):
'''Scans for target device'''
self._uart = None
self._uart_connection = None
# See if any existing connections are providing UARTService.
self._connection_count = len(self._ble.connections)
if self._connection_count > 0 and not self._uart:
for connection in self._ble.connections:
if self.UARTService in connection:
self._uart_connection = connection
self._uart_connection.connection_interval = 11.25
self._uart = self._uart_connection[self.UARTService]
break
if not self._uart:
if self._debug_enabled:
print('Scanning')
self._ble.stop_scan()
for adv in self._ble.start_scan(
self.ProvideServicesAdvertisement, timeout=20
):
if self._debug_enabled:
print('Scanning')
if self.UARTService in adv.services and adv.rssi > -70:
self._uart_connection = self._ble.connect(adv)
self._uart_connection.connection_interval = 11.25
self._uart = self._uart_connection[self.UARTService]
self._ble.stop_scan()
if self._debug_enabled:
print('Scan complete')
break
self._ble.stop_scan()
def _target_advertise(self):
'''Advertises the target for the initiator to find'''
self._ble.stop_advertising()
if self._debug_enabled:
print('Advertising')
# Uart must not change on this connection if reconnecting
if not self._uart:
self._uart = self.UARTService()
advertisement = self.ProvideServicesAdvertisement(self._uart)
self._ble.start_advertising(advertisement)
self.ble_time_reset()
while not self.ble_rescan_timer():
self._connection_count = len(self._ble.connections)
if self._connection_count > 1:
self.ble_time_reset()
if self._debug_enabled:
print('Advertising complete')
break
self._ble.stop_advertising()
def ble_rescan_timer(self):
'''If true, the rescan timer is up'''
return bool(ticks_diff(ticks_ms(), self._ble_last_scan) > 5000)
def ble_time_reset(self):
'''Resets the rescan timer'''
self._ble_last_scan = ticks_ms()
def _send_ble(self, update):
if self._uart:
try:
if not self._is_target:
update[1] += self.split_offset
self._uart.write(update)
except OSError:
try:
self._uart.disconnect()
except: # noqa: E722
if self._debug_enabled:
print('UART disconnect failed')
if self._debug_enabled:
print('Connection error')
self._uart_connection = None
self._uart = None
def _receive_ble(self, keyboard):
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
while self._uart.in_waiting >= 3:
self._uart_buffer.append(self._uart.read(3))
if self._uart_buffer:
keyboard.secondary_matrix_update = bytearray(self._uart_buffer.pop(0))
return
def _send_uart(self, update):
# Change offsets depending on where the data is going to match the correct
# matrix location of the receiever
if self._is_target:
if self.split_target_left:
update[1] += self.split_offset
else:
update[1] -= self.split_offset
else:
if self.split_target_left:
update[1] -= self.split_offset
else:
update[1] += self.split_offset
if self._uart is not None:
self._uart.write(update)
def _receive_uart(self, keyboard):
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
if self._uart.in_waiting >= 60:
# This is a dirty hack to prevent crashes in unrealistic cases
import microcontroller
microcontroller.reset()
while self._uart.in_waiting >= 3:
self._uart_buffer.append(self._uart.read(3))
if self._uart_buffer:
keyboard.secondary_matrix_update = bytearray(self._uart_buffer.pop(0))
return

View File

@@ -1,37 +0,0 @@
# Welcome to RAM and stack size hacks central, I'm your host, klardotsh!
# Our import structure is deeply nested enough that stuff
# breaks in some truly bizarre ways, including:
# - explicit RuntimeError exceptions, complaining that our
# stack depth is too deep
#
# - silent hard locks of the device (basically unrecoverable without
# UF2 flash if done in main.py, fixable with a reboot if done
# in REPL)
#
# However, there's a hackaround that works for us! Because sys.modules
# caches everything it sees (and future imports will use that cached
# copy of the module), let's take this opportunity _way_ up the import
# chain to import _every single thing_ KMK eventually uses in a normal
# workflow, in nesting order
#
# GC runs automatically after CircuitPython imports.
# First, system-provided deps
import busio
import collections
import gc
import supervisor
# Now "light" KMK stuff with few/no external deps
import kmk.consts # isort:skip
import kmk.kmktime # isort:skip
import kmk.types # 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