├── code ├── kmk │ ├── __init__.py │ ├── handlers │ │ ├── __init__.py │ │ ├── stock.py │ │ └── sequences.py │ ├── transports │ │ ├── __init__.py │ │ └── pio_uart.py │ ├── consts.py │ ├── key_validators.py │ ├── quickpin │ │ └── pro_micro │ │ │ ├── avr_promicro.py │ │ │ ├── kb2040.py │ │ │ ├── sparkfun_promicro_rp2040.py │ │ │ ├── boardsource_blok.py │ │ │ └── nice_nano.py │ ├── modules │ │ ├── modtap.py │ │ ├── __init__.py │ │ ├── serialace.py │ │ ├── sticky_mod.py │ │ ├── cg_swap.py │ │ ├── oneshot.py │ │ ├── potentiometer.py │ │ ├── midi.py │ │ ├── capsword.py │ │ ├── rapidfire.py │ │ ├── easypoint.py │ │ ├── tapdance.py │ │ ├── power.py │ │ ├── layers.py │ │ ├── adns9800.py │ │ ├── string_substitution.py │ │ ├── dynamic_sequences.py │ │ ├── mouse_keys.py │ │ ├── holdtap.py │ │ ├── encoder.py │ │ ├── pimoroni_trackball.py │ │ └── combos.py │ ├── types.py │ ├── utils.py │ ├── kmktime.py │ ├── extensions │ │ ├── keymap_extras │ │ │ └── keymap_jp.py │ │ ├── stringy_keymaps.py │ │ ├── __init__.py │ │ ├── international.py │ │ ├── lock_status.py │ │ ├── media_keys.py │ │ ├── statusled.py │ │ ├── peg_oled_display.py │ │ ├── peg_rgb_matrix.py │ │ └── led.py │ ├── scanners │ │ ├── __init__.py │ │ ├── encoder.py │ │ ├── keypad.py │ │ └── digitalio.py │ └── hid.py ├── boot.py ├── boot_out.txt ├── code.py └── lib │ └── neopixel.py ├── picohat.png ├── IMG_1465.jpg ├── README.md ├── LICENCE └── kicad files └── picohat.kicad_pro /code/kmk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/kmk/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/kmk/transports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /picohat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nataliethenerd/picohatpad/HEAD/picohat.png -------------------------------------------------------------------------------- /IMG_1465.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nataliethenerd/picohatpad/HEAD/IMG_1465.jpg -------------------------------------------------------------------------------- /code/boot.py: -------------------------------------------------------------------------------- 1 | import supervisor 2 | 3 | supervisor.set_next_stack_limit(4096 + 4096) 4 | -------------------------------------------------------------------------------- /code/boot_out.txt: -------------------------------------------------------------------------------- 1 | Adafruit CircuitPython 7.3.3 on 2022-08-29; Raspberry Pi Pico with rp2040 2 | Board ID:raspberry_pi_pico 3 | boot.py output: 4 | -------------------------------------------------------------------------------- /code/kmk/consts.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | 3 | 4 | class UnicodeMode: 5 | NOOP = const(0) 6 | LINUX = IBUS = const(1) 7 | MACOS = OSX = RALT = const(2) 8 | WINC = const(3) 9 | -------------------------------------------------------------------------------- /code/kmk/key_validators.py: -------------------------------------------------------------------------------- 1 | from kmk.types import KeySeqSleepMeta, UnicodeModeKeyMeta 2 | 3 | 4 | def key_seq_sleep_validator(ms): 5 | return KeySeqSleepMeta(ms) 6 | 7 | 8 | def unicode_mode_key_validator(mode): 9 | return UnicodeModeKeyMeta(mode) 10 | -------------------------------------------------------------------------------- /code/kmk/quickpin/pro_micro/avr_promicro.py: -------------------------------------------------------------------------------- 1 | translate = { 2 | 'D3': 0, 3 | 'D2': 1, 4 | 'D1': 4, 5 | 'D0': 5, 6 | 'D4': 6, 7 | 'C6': 7, 8 | 'D7': 8, 9 | 'E6': 9, 10 | 'B4': 10, 11 | 'B5': 11, 12 | 'B6': 12, 13 | 'B2': 13, 14 | 'B3': 14, 15 | 'B1': 15, 16 | 'F7': 16, 17 | 'F6': 17, 18 | 'F5': 18, 19 | 'F4': 19, 20 | } 21 | -------------------------------------------------------------------------------- /code/kmk/modules/modtap.py: -------------------------------------------------------------------------------- 1 | from kmk.keys import make_argumented_key 2 | from kmk.modules.holdtap import HoldTap, HoldTapKeyMeta 3 | 4 | 5 | class ModTap(HoldTap): 6 | def __init__(self): 7 | super().__init__() 8 | make_argumented_key( 9 | validator=HoldTapKeyMeta, 10 | names=('MT',), 11 | on_press=self.ht_pressed, 12 | on_release=self.ht_released, 13 | ) 14 | -------------------------------------------------------------------------------- /code/kmk/quickpin/pro_micro/kb2040.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | pinout = [ 4 | board.D0, 5 | board.D1, 6 | None, # GND 7 | None, # GND 8 | board.D2, 9 | board.D3, 10 | board.D4, 11 | board.D5, 12 | board.D6, 13 | board.D7, 14 | board.D8, 15 | board.D9, 16 | board.D10, 17 | board.MOSI, 18 | board.MISO, 19 | board.SCK, 20 | board.A0, 21 | board.A1, 22 | board.A2, 23 | board.A3, 24 | None, # 3.3v 25 | None, # RST 26 | None, # GND 27 | None, # RAW 28 | ] 29 | -------------------------------------------------------------------------------- /code/kmk/quickpin/pro_micro/sparkfun_promicro_rp2040.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | pinout = [ 4 | board.TX, 5 | board.RX, 6 | None, # GND 7 | None, # GND 8 | board.D2, 9 | board.D3, 10 | board.D4, 11 | board.D5, 12 | board.D6, 13 | board.D7, 14 | board.D8, 15 | board.D9, 16 | board.D21, 17 | board.MOSI, 18 | board.MISO, 19 | board.SCK, 20 | board.D26, 21 | board.D27, 22 | board.D28, 23 | board.D29, 24 | None, # 3.3v 25 | None, # RST 26 | None, # GND 27 | None, # RAW 28 | ] 29 | -------------------------------------------------------------------------------- /code/kmk/quickpin/pro_micro/boardsource_blok.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | pinout = [ 4 | board.TX, 5 | board.RX, 6 | None, # GND 7 | None, # GND 8 | board.SDA, 9 | board.SCL, 10 | board.GP04, 11 | board.GP05, 12 | board.GP06, 13 | board.GP07, 14 | board.GP08, 15 | board.GP09, 16 | board.GP21, 17 | board.GP23, 18 | board.GP20, 19 | board.GP22, 20 | board.GP26, 21 | board.GP27, 22 | board.GP28, 23 | board.GP29, 24 | None, # 3.3v 25 | None, # RST 26 | None, # GND 27 | None, # RAW 28 | ] 29 | -------------------------------------------------------------------------------- /code/kmk/quickpin/pro_micro/nice_nano.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | pinout = [ 4 | board.TX, 5 | board.RX, 6 | None, # GND 7 | None, # GND 8 | board.SDA, 9 | board.SCL, 10 | board.P0_22, 11 | board.P0_24, 12 | board.P1_00, 13 | board.P0_11, 14 | board.P1_04, 15 | board.P1_06, 16 | board.P0_09, 17 | board.P0_10, 18 | board.P1_11, 19 | board.P1_13, 20 | board.P1_15, 21 | board.P0_02, 22 | board.P0_29, 23 | board.P0_31, 24 | None, # 3.3v 25 | None, # RST 26 | None, # GND 27 | None, # Battery+ 28 | ] 29 | -------------------------------------------------------------------------------- /code/kmk/types.py: -------------------------------------------------------------------------------- 1 | class AttrDict(dict): 2 | ''' 3 | Primitive support for accessing dictionary entries in dot notation. 4 | Mostly for user-facing stuff (allows for `k.KC_ESC` rather than 5 | `k['KC_ESC']`, which gets a bit obnoxious). 6 | 7 | This is read-only on purpose. 8 | ''' 9 | 10 | def __getattr__(self, key): 11 | return self[key] 12 | 13 | 14 | class KeySequenceMeta: 15 | def __init__(self, seq): 16 | self.seq = seq 17 | 18 | 19 | class KeySeqSleepMeta: 20 | def __init__(self, ms): 21 | self.ms = ms 22 | 23 | 24 | class UnicodeModeKeyMeta: 25 | def __init__(self, mode): 26 | self.mode = mode 27 | -------------------------------------------------------------------------------- /code/kmk/utils.py: -------------------------------------------------------------------------------- 1 | from supervisor import ticks_ms 2 | 3 | 4 | def clamp(x: int, bottom: int = 0, top: int = 100) -> int: 5 | return min(max(bottom, x), top) 6 | 7 | 8 | _debug_enabled = False 9 | 10 | 11 | class Debug: 12 | '''default usage: 13 | debug = Debug(__name__) 14 | ''' 15 | 16 | def __init__(self, name: str = __name__): 17 | self.name = name 18 | 19 | def __call__(self, message: str) -> None: 20 | print(f'{ticks_ms()} {self.name}: {message}') 21 | 22 | @property 23 | def enabled(self) -> bool: 24 | global _debug_enabled 25 | return _debug_enabled 26 | 27 | @enabled.setter 28 | def enabled(self, enabled: bool): 29 | global _debug_enabled 30 | _debug_enabled = enabled 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pico Hat Pad 2 | A Raspberry Pi Pico hat with two mechanical switches and a rotary encoder. Powered by KMK 3 | 4 | 5 | This kit aims to be a cheap (under $20aud including caps, keys, pico, pcbs, head pins) macro pad and solder practice kit 6 | 7 | ![alt text](https://github.com/nataliethenerd/picohatpad/blob/0d090fafd6f77bb56b488985600c669083d9b0a3/IMG_1465.jpg) 8 | 9 | # BOM 10 | 2 X 20 stacked header pins 11 | 12 | 2 X keyboard switches 13 | 14 | 2 X WS2812B 15 | 16 | 1 X EC11 encoder 17 | 18 | 1 X Raspberry Pi Pico (I used a 1:1 clone that had USB C) 19 | 20 | # Guide/Purchase kit 21 | 22 | You can by the kit here https://www.nataliethenerd.com/product-page/kit-pico-hat-macro-pad 23 | 24 | and an in depth guide can be found here https://www.nataliethenerd.com/picohatmacropad 25 | -------------------------------------------------------------------------------- /code/kmk/kmktime.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | from supervisor import ticks_ms 3 | 4 | _TICKS_PERIOD = const(1 << 29) 5 | _TICKS_MAX = const(_TICKS_PERIOD - 1) 6 | _TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2) 7 | 8 | 9 | def ticks_diff(new: int, start: int) -> int: 10 | diff = (new - start) & _TICKS_MAX 11 | diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD 12 | return diff 13 | 14 | 15 | def ticks_add(ticks: int, delta: int) -> int: 16 | return (ticks + delta) % _TICKS_PERIOD 17 | 18 | 19 | def check_deadline(new: int, start: int, ms: int) -> int: 20 | return ticks_diff(new, start) < ms 21 | 22 | 23 | class PeriodicTimer: 24 | def __init__(self, period: int): 25 | self.period = period 26 | self.last_tick = ticks_ms() 27 | 28 | def tick(self) -> bool: 29 | now = ticks_ms() 30 | if ticks_diff(now, self.last_tick) >= self.period: 31 | self.last_tick = now 32 | return True 33 | else: 34 | return False 35 | -------------------------------------------------------------------------------- /code/code.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | from kmk.kmk_keyboard import KMKKeyboard 4 | from kmk.scanners.keypad import KeysScanner 5 | from kmk.modules.encoder import EncoderHandler 6 | from kmk.keys import KC 7 | from kmk.extensions.media_keys import MediaKeys 8 | from kmk.extensions.RGB import RGB, AnimationModes 9 | 10 | keyboard = KMKKeyboard() 11 | 12 | underglow = RGB( 13 | pixel_pin=board.GP10, 14 | num_pixels=2, 15 | val_limit=100, 16 | val_default=25, 17 | animation_mode=AnimationModes.RAINBOW, 18 | ) 19 | keyboard.extensions.append(underglow) 20 | 21 | keyboard.matrix = KeysScanner( 22 | [ 23 | board.GP0, board.GP2, 24 | ] 25 | ) 26 | encoder_handler = EncoderHandler() 27 | media_keys = MediaKeys() 28 | keyboard.modules = [encoder_handler, media_keys] 29 | 30 | encoder_handler.pins = ((board.GP3, board.GP4, board.GP5, False),) 31 | 32 | keyboard.keymap = [[KC.MNXT, KC.MPRV]] 33 | 34 | encoder_handler.map = (((KC.VOLU, KC.VOLD, KC.MPLY),),) 35 | 36 | if __name__ == "__main__": 37 | keyboard.go() 38 | -------------------------------------------------------------------------------- /code/kmk/extensions/keymap_extras/keymap_jp.py: -------------------------------------------------------------------------------- 1 | # What's this? 2 | # This is a keycode conversion script. With this, KMK will work as a JIS keyboard. 3 | 4 | # Usage 5 | # ```python 6 | # import kmk.extensions.keymap_extras.keymap_jp 7 | # ``` 8 | 9 | from kmk.keys import KC 10 | 11 | KC.CIRC = KC.EQL # ^ 12 | KC.AT = KC.LBRC # @ 13 | KC.LBRC = KC.RBRC # [ 14 | KC.EISU = KC.CAPS # Eisū (英数) 15 | KC.COLN = KC.QUOT # : 16 | KC.LCBR = KC.LSFT(KC.RBRC) # { 17 | KC.RBRC = KC.NUHS # ] 18 | KC.BSLS = KC.INT1 # (backslash) 19 | KC.PLUS = KC.LSFT(KC.SCLN) 20 | KC.TILD = KC.LSFT(KC.EQL) # ~ 21 | KC.GRV = KC.LSFT(KC.AT) # ` 22 | KC.DQUO = KC.LSFT(KC.N2) # " 23 | KC.AMPR = KC.LSFT(KC.N6) # & 24 | KC.ASTR = KC.LSFT(KC.QUOT) # * 25 | KC.QUOT = KC.LSFT(KC.N7) # ' 26 | KC.LPRN = KC.LSFT(KC.N8) # ( 27 | KC.RPRN = KC.LSFT(KC.N9) # ) 28 | KC.EQL = KC.LSFT(KC.MINS) # = 29 | KC.PIPE = KC.LSFT(KC.INT3) # | 30 | KC.RCBR = KC.LSFT(KC.NUHS) # } 31 | KC.LABK = KC.LSFT(KC.COMM) # < 32 | KC.RABK = KC.LSFT(KC.DOT) # > 33 | KC.QUES = KC.LSFT(KC.SLSH) # ? 34 | KC.UNDS = KC.LSFT(KC.INT1) # _ 35 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 nataliethenerd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /code/kmk/scanners/__init__.py: -------------------------------------------------------------------------------- 1 | def intify_coordinate(row, col, len_cols): 2 | return len_cols * row + col 3 | 4 | 5 | class DiodeOrientation: 6 | ''' 7 | Orientation of diodes on handwired boards. You can think of: 8 | COLUMNS = vertical 9 | ROWS = horizontal 10 | 11 | COL2ROW and ROW2COL are equivalent to their meanings in QMK. 12 | ''' 13 | 14 | COLUMNS = 0 15 | ROWS = 1 16 | COL2ROW = COLUMNS 17 | ROW2COL = ROWS 18 | 19 | 20 | class Scanner: 21 | ''' 22 | Base class for scanners. 23 | ''' 24 | 25 | # for split keyboards, the offset value will be assigned in Split module 26 | offset = 0 27 | 28 | @property 29 | def coord_mapping(self): 30 | return tuple(range(self.offset, self.offset + self.key_count)) 31 | 32 | @property 33 | def key_count(self): 34 | raise NotImplementedError 35 | 36 | def scan_for_changes(self): 37 | ''' 38 | Scan for key events and return a key report if an event exists. 39 | 40 | The key report is a byte array with contents [row, col, True if pressed else False] 41 | ''' 42 | raise NotImplementedError 43 | -------------------------------------------------------------------------------- /code/kmk/scanners/encoder.py: -------------------------------------------------------------------------------- 1 | import keypad 2 | import rotaryio 3 | 4 | from kmk.scanners import Scanner 5 | 6 | 7 | class RotaryioEncoder(Scanner): 8 | def __init__(self, pin_a, pin_b, divisor=4): 9 | self.encoder = rotaryio.IncrementalEncoder(pin_a, pin_b, divisor) 10 | self.position = 0 11 | self._pressed = False 12 | self._queue = [] 13 | 14 | @property 15 | def key_count(self): 16 | return 2 17 | 18 | def scan_for_changes(self): 19 | position = self.encoder.position 20 | 21 | if position != self.position: 22 | self._queue.append(position - self.position) 23 | self.position = position 24 | 25 | if not self._queue: 26 | return 27 | 28 | key_number = self.offset 29 | if self._queue[0] > 0: 30 | key_number += 1 31 | 32 | if self._pressed: 33 | self._queue[0] -= 1 if self._queue[0] > 0 else -1 34 | 35 | if self._queue[0] == 0: 36 | self._queue.pop(0) 37 | 38 | self._pressed = False 39 | 40 | else: 41 | self._pressed = True 42 | 43 | return keypad.Event(key_number, self._pressed) 44 | -------------------------------------------------------------------------------- /code/kmk/modules/__init__.py: -------------------------------------------------------------------------------- 1 | class InvalidExtensionEnvironment(Exception): 2 | pass 3 | 4 | 5 | class Module: 6 | ''' 7 | Modules differ from extensions in that they not only can read the state, but 8 | are allowed to modify the state. The will be loaded on boot, and are not 9 | allowed to be unloaded as they are required to continue functioning in a 10 | consistant manner. 11 | ''' 12 | 13 | # The below methods should be implemented by subclasses 14 | 15 | def during_bootup(self, keyboard): 16 | raise NotImplementedError 17 | 18 | def before_matrix_scan(self, keyboard): 19 | ''' 20 | Return value will be injected as an extra matrix update 21 | ''' 22 | raise NotImplementedError 23 | 24 | def after_matrix_scan(self, keyboard): 25 | ''' 26 | Return value will be replace matrix update if supplied 27 | ''' 28 | raise NotImplementedError 29 | 30 | def process_key(self, keyboard, key, is_pressed, int_coord): 31 | return key 32 | 33 | def before_hid_send(self, keyboard): 34 | raise NotImplementedError 35 | 36 | def after_hid_send(self, keyboard): 37 | raise NotImplementedError 38 | 39 | def on_powersave_enable(self, keyboard): 40 | raise NotImplementedError 41 | 42 | def on_powersave_disable(self, keyboard): 43 | raise NotImplementedError 44 | -------------------------------------------------------------------------------- /code/kmk/extensions/stringy_keymaps.py: -------------------------------------------------------------------------------- 1 | from kmk.extensions import Extension 2 | from kmk.keys import KC 3 | 4 | 5 | class StringyKeymaps(Extension): 6 | ##### 7 | # User-configurable 8 | debug_enabled = False 9 | 10 | def on_runtime_enable(self, keyboard): 11 | return 12 | 13 | def on_runtime_disable(self, keyboard): 14 | return 15 | 16 | def during_bootup(self, keyboard): 17 | for _, layer in enumerate(keyboard.keymap): 18 | for key_idx, key in enumerate(layer): 19 | if isinstance(key, str): 20 | replacement = KC.get(key) 21 | if replacement is None: 22 | replacement = KC.NO 23 | if self.debug_enabled: 24 | print(f"Failed replacing '{key}'. Using KC.NO") 25 | elif self.debug_enabled: 26 | print(f"Replacing '{key}' with {replacement}") 27 | layer[key_idx] = replacement 28 | 29 | def before_matrix_scan(self, keyboard): 30 | return 31 | 32 | def after_matrix_scan(self, keyboard): 33 | return 34 | 35 | def before_hid_send(self, keyboard): 36 | return 37 | 38 | def after_hid_send(self, keyboard): 39 | return 40 | 41 | def on_powersave_enable(self, keyboard): 42 | return 43 | 44 | def on_powersave_disable(self, keyboard): 45 | return 46 | -------------------------------------------------------------------------------- /code/kmk/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | class InvalidExtensionEnvironment(Exception): 2 | pass 3 | 4 | 5 | class Extension: 6 | _enabled = True 7 | 8 | def enable(self, keyboard): 9 | self._enabled = True 10 | 11 | self.on_runtime_enable(keyboard) 12 | 13 | def disable(self, keyboard): 14 | self._enabled = False 15 | 16 | self.on_runtime_disable(keyboard) 17 | 18 | # The below methods should be implemented by subclasses 19 | 20 | def on_runtime_enable(self, keyboard): 21 | raise NotImplementedError 22 | 23 | def on_runtime_disable(self, keyboard): 24 | raise NotImplementedError 25 | 26 | def during_bootup(self, keyboard): 27 | raise NotImplementedError 28 | 29 | def before_matrix_scan(self, keyboard): 30 | ''' 31 | Return value will be injected as an extra matrix update 32 | ''' 33 | raise NotImplementedError 34 | 35 | def after_matrix_scan(self, keyboard): 36 | ''' 37 | Return value will be replace matrix update if supplied 38 | ''' 39 | raise NotImplementedError 40 | 41 | def before_hid_send(self, keyboard): 42 | raise NotImplementedError 43 | 44 | def after_hid_send(self, keyboard): 45 | raise NotImplementedError 46 | 47 | def on_powersave_enable(self, keyboard): 48 | raise NotImplementedError 49 | 50 | def on_powersave_disable(self, keyboard): 51 | raise NotImplementedError 52 | -------------------------------------------------------------------------------- /code/kmk/modules/serialace.py: -------------------------------------------------------------------------------- 1 | from usb_cdc import data 2 | 3 | from kmk.modules import Module 4 | from kmk.utils import Debug 5 | 6 | debug = Debug(__name__) 7 | 8 | 9 | class SerialACE(Module): 10 | buffer = bytearray() 11 | 12 | def during_bootup(self, keyboard): 13 | try: 14 | data.timeout = 0 15 | except AttributeError: 16 | pass 17 | 18 | def before_matrix_scan(self, keyboard): 19 | pass 20 | 21 | def after_matrix_scan(self, keyboard): 22 | pass 23 | 24 | def process_key(self, keyboard, key, is_pressed, int_coord): 25 | return key 26 | 27 | def before_hid_send(self, keyboard): 28 | # Serial.data isn't initialized. 29 | if not data: 30 | return 31 | 32 | # Nothing to parse. 33 | if data.in_waiting == 0: 34 | return 35 | 36 | self.buffer.extend(data.read()) 37 | idx = self.buffer.find(b'\n') 38 | 39 | # No full command yet. 40 | if idx == -1: 41 | return 42 | 43 | # Split off command and evaluate. 44 | line = self.buffer[:idx] 45 | self.buffer = self.buffer[idx + 1 :] 46 | 47 | try: 48 | if debug.enabled: 49 | debug(f'eval({line})') 50 | ret = eval(line, {'keyboard': keyboard}) 51 | data.write(bytearray(str(ret) + '\n')) 52 | except Exception as err: 53 | if debug.enabled: 54 | debug(f'error: {err}') 55 | 56 | def after_hid_send(self, keyboard): 57 | pass 58 | 59 | def on_powersave_enable(self, keyboard): 60 | pass 61 | 62 | def on_powersave_disable(self, keyboard): 63 | pass 64 | -------------------------------------------------------------------------------- /code/kmk/modules/sticky_mod.py: -------------------------------------------------------------------------------- 1 | from kmk.keys import make_argumented_key 2 | from kmk.modules import Module 3 | 4 | 5 | class StickyModMeta: 6 | def __init__(self, kc, mod): 7 | self.kc = kc 8 | self.mod = mod 9 | 10 | 11 | class StickyMod(Module): 12 | def __init__(self): 13 | self._active = False 14 | self._active_key = None 15 | make_argumented_key( 16 | names=('SM',), 17 | validator=StickyModMeta, 18 | on_press=self.sm_pressed, 19 | on_release=self.sm_released, 20 | ) 21 | 22 | def during_bootup(self, keyboard): 23 | return 24 | 25 | def before_matrix_scan(self, keyboard): 26 | return 27 | 28 | def process_key(self, keyboard, key, is_pressed, int_coord): 29 | # release previous key if any other key is pressed 30 | if self._active and self._active_key is not None: 31 | self.release_key(keyboard, self._active_key) 32 | 33 | return key 34 | 35 | def before_hid_send(self, keyboard): 36 | return 37 | 38 | def after_hid_send(self, keyboard): 39 | return 40 | 41 | def on_powersave_enable(self, keyboard): 42 | return 43 | 44 | def on_powersave_disable(self, keyboard): 45 | return 46 | 47 | def after_matrix_scan(self, keyboard): 48 | return 49 | 50 | def release_key(self, keyboard, key): 51 | keyboard.process_key(key.meta.mod, False) 52 | self._active = False 53 | self._active_key = None 54 | 55 | def sm_pressed(self, key, keyboard, *args, **kwargs): 56 | keyboard.process_key(key.meta.mod, True) 57 | keyboard.process_key(key.meta.kc, True) 58 | self._active_key = key 59 | 60 | def sm_released(self, key, keyboard, *args, **kwargs): 61 | keyboard.process_key(key.meta.kc, False) 62 | self._active_key = key 63 | self._active = True 64 | -------------------------------------------------------------------------------- /code/kmk/extensions/international.py: -------------------------------------------------------------------------------- 1 | '''Adds international keys''' 2 | from kmk.extensions import Extension 3 | from kmk.keys import make_key 4 | 5 | 6 | class International(Extension): 7 | '''Adds international keys''' 8 | 9 | def __init__(self): 10 | # International 11 | make_key(code=50, names=('NONUS_HASH', 'NUHS')) 12 | make_key(code=100, names=('NONUS_BSLASH', 'NUBS')) 13 | make_key(code=101, names=('APP', 'APPLICATION', 'SEL', 'WINMENU')) 14 | 15 | make_key(code=135, names=('INT1', 'RO')) 16 | make_key(code=136, names=('INT2', 'KANA')) 17 | make_key(code=137, names=('INT3', 'JYEN')) 18 | make_key(code=138, names=('INT4', 'HENK')) 19 | make_key(code=139, names=('INT5', 'MHEN')) 20 | make_key(code=140, names=('INT6',)) 21 | make_key(code=141, names=('INT7',)) 22 | make_key(code=142, names=('INT8',)) 23 | make_key(code=143, names=('INT9',)) 24 | make_key(code=144, names=('LANG1', 'HAEN')) 25 | make_key(code=145, names=('LANG2', 'HAEJ')) 26 | make_key(code=146, names=('LANG3',)) 27 | make_key(code=147, names=('LANG4',)) 28 | make_key(code=148, names=('LANG5',)) 29 | make_key(code=149, names=('LANG6',)) 30 | make_key(code=150, names=('LANG7',)) 31 | make_key(code=151, names=('LANG8',)) 32 | make_key(code=152, names=('LANG9',)) 33 | 34 | def on_runtime_enable(self, sandbox): 35 | return 36 | 37 | def on_runtime_disable(self, sandbox): 38 | return 39 | 40 | def during_bootup(self, sandbox): 41 | return 42 | 43 | def before_matrix_scan(self, sandbox): 44 | return 45 | 46 | def after_matrix_scan(self, sandbox): 47 | return 48 | 49 | def before_hid_send(self, sandbox): 50 | return 51 | 52 | def after_hid_send(self, sandbox): 53 | return 54 | 55 | def on_powersave_enable(self, sandbox): 56 | return 57 | 58 | def on_powersave_disable(self, sandbox): 59 | return 60 | -------------------------------------------------------------------------------- /code/kmk/modules/cg_swap.py: -------------------------------------------------------------------------------- 1 | from kmk.keys import KC, ModifierKey, make_key 2 | from kmk.modules import Module 3 | 4 | 5 | class CgSwap(Module): 6 | # default cg swap is disabled, can be eanbled too if needed 7 | def __init__(self, cg_swap_enable=False): 8 | self.cg_swap_enable = cg_swap_enable 9 | self._cg_mapping = { 10 | KC.LCTL: KC.LGUI, 11 | KC.RCTL: KC.RGUI, 12 | KC.LGUI: KC.LCTL, 13 | KC.RGUI: KC.RCTL, 14 | } 15 | make_key( 16 | names=('CG_SWAP',), 17 | ) 18 | make_key( 19 | names=('CG_NORM',), 20 | ) 21 | make_key( 22 | names=('CG_TOGG',), 23 | ) 24 | 25 | def during_bootup(self, keyboard): 26 | return 27 | 28 | def matrix_detected_press(self, keyboard): 29 | return keyboard.matrix_update is None 30 | 31 | def before_matrix_scan(self, keyboard): 32 | return 33 | 34 | def process_key(self, keyboard, key, is_pressed, int_coord): 35 | if is_pressed: 36 | # enables or disables or toggles cg swap 37 | if key == KC.CG_SWAP: 38 | self.cg_swap_enable = True 39 | elif key == KC.CG_NORM: 40 | self.cg_swap_enable = False 41 | elif key == KC.CG_TOGG: 42 | if not self.cg_swap_enable: 43 | self.cg_swap_enable = True 44 | else: 45 | self.cg_swap_enable = False 46 | # performs cg swap 47 | if ( 48 | self.cg_swap_enable 49 | and key not in (KC.CG_SWAP, KC.CG_NORM, KC.CG_TOGG) 50 | and isinstance(key, ModifierKey) 51 | and key in self._cg_mapping 52 | ): 53 | key = self._cg_mapping.get(key) 54 | 55 | return key 56 | 57 | def before_hid_send(self, keyboard): 58 | return 59 | 60 | def after_hid_send(self, keyboard): 61 | return 62 | 63 | def on_powersave_enable(self, keyboard): 64 | return 65 | 66 | def on_powersave_disable(self, keyboard): 67 | return 68 | 69 | def after_matrix_scan(self, keyboard): 70 | return 71 | -------------------------------------------------------------------------------- /code/kmk/extensions/lock_status.py: -------------------------------------------------------------------------------- 1 | import usb_hid 2 | 3 | from kmk.extensions import Extension 4 | from kmk.hid import HIDUsage 5 | 6 | 7 | class LockCode: 8 | NUMLOCK = 0x01 9 | CAPSLOCK = 0x02 10 | SCROLLLOCK = 0x04 11 | COMPOSE = 0x08 12 | KANA = 0x10 13 | RESERVED = 0x20 14 | 15 | 16 | class LockStatus(Extension): 17 | def __init__(self): 18 | self.report = None 19 | self.hid = None 20 | self._report_updated = False 21 | for device in usb_hid.devices: 22 | if device.usage == HIDUsage.KEYBOARD: 23 | self.hid = device 24 | 25 | def __repr__(self): 26 | return f'LockStatus(report={self.report})' 27 | 28 | def during_bootup(self, sandbox): 29 | return 30 | 31 | def before_matrix_scan(self, sandbox): 32 | return 33 | 34 | def after_matrix_scan(self, sandbox): 35 | return 36 | 37 | def before_hid_send(self, sandbox): 38 | return 39 | 40 | def after_hid_send(self, sandbox): 41 | if self.hid: 42 | report = self.hid.get_last_received_report() 43 | if report and report[0] != self.report: 44 | self.report = report[0] 45 | self._report_updated = True 46 | else: 47 | self._report_updated = False 48 | else: 49 | # _report_updated shouldn't ever be True if hid is 50 | # falsy, but I would rather be safe than sorry. 51 | self._report_updated = False 52 | return 53 | 54 | def on_powersave_enable(self, sandbox): 55 | return 56 | 57 | def on_powersave_disable(self, sandbox): 58 | return 59 | 60 | @property 61 | def report_updated(self): 62 | return self._report_updated 63 | 64 | def check_state(self, lock_code): 65 | # This is false if there's no valid report, or all report bits are zero 66 | if self.report: 67 | return bool(self.report & lock_code) 68 | else: 69 | # Just in case, default to False if we don't know anything 70 | return False 71 | 72 | def get_num_lock(self): 73 | return self.check_state(LockCode.NUMLOCK) 74 | 75 | def get_caps_lock(self): 76 | return self.check_state(LockCode.CAPSLOCK) 77 | 78 | def get_scroll_lock(self): 79 | return self.check_state(LockCode.SCROLLLOCK) 80 | 81 | def get_compose(self): 82 | return self.check_state(LockCode.COMPOSE) 83 | 84 | def get_kana(self): 85 | return self.check_state(LockCode.KANA) 86 | -------------------------------------------------------------------------------- /code/kmk/extensions/media_keys.py: -------------------------------------------------------------------------------- 1 | from kmk.extensions import Extension 2 | from kmk.keys import make_consumer_key 3 | 4 | 5 | class MediaKeys(Extension): 6 | def __init__(self): 7 | # Consumer ("media") keys. Most known keys aren't supported here. A much 8 | # longer list used to exist in this file, but the codes were almost certainly 9 | # incorrect, conflicting with each other, or otherwise 'weird'. We'll add them 10 | # back in piecemeal as needed. PRs welcome. 11 | # 12 | # A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php 13 | # Note that currently we only have the PC codes. Recent MacOS versions seem to 14 | # support PC media keys, so I don't know how much value we would get out of 15 | # adding the old Apple-specific consumer codes, but again, PRs welcome if the 16 | # lack of them impacts you. 17 | make_consumer_key(code=226, names=('AUDIO_MUTE', 'MUTE')) # 0xE2 18 | make_consumer_key(code=233, names=('AUDIO_VOL_UP', 'VOLU')) # 0xE9 19 | make_consumer_key(code=234, names=('AUDIO_VOL_DOWN', 'VOLD')) # 0xEA 20 | make_consumer_key(code=111, names=('BRIGHTNESS_UP', 'BRIU')) # 0x6F 21 | make_consumer_key(code=112, names=('BRIGHTNESS_DOWN', 'BRID')) # 0x70 22 | make_consumer_key(code=181, names=('MEDIA_NEXT_TRACK', 'MNXT')) # 0xB5 23 | make_consumer_key(code=182, names=('MEDIA_PREV_TRACK', 'MPRV')) # 0xB6 24 | make_consumer_key(code=183, names=('MEDIA_STOP', 'MSTP')) # 0xB7 25 | make_consumer_key( 26 | code=205, names=('MEDIA_PLAY_PAUSE', 'MPLY') 27 | ) # 0xCD (this may not be right) 28 | make_consumer_key(code=184, names=('MEDIA_EJECT', 'EJCT')) # 0xB8 29 | make_consumer_key(code=179, names=('MEDIA_FAST_FORWARD', 'MFFD')) # 0xB3 30 | make_consumer_key(code=180, names=('MEDIA_REWIND', 'MRWD')) # 0xB4 31 | 32 | def on_runtime_enable(self, sandbox): 33 | return 34 | 35 | def on_runtime_disable(self, sandbox): 36 | return 37 | 38 | def during_bootup(self, sandbox): 39 | return 40 | 41 | def before_matrix_scan(self, sandbox): 42 | return 43 | 44 | def after_matrix_scan(self, sandbox): 45 | return 46 | 47 | def before_hid_send(self, sandbox): 48 | return 49 | 50 | def after_hid_send(self, sandbox): 51 | return 52 | 53 | def on_powersave_enable(self, sandbox): 54 | return 55 | 56 | def on_powersave_disable(self, sandbox): 57 | return 58 | -------------------------------------------------------------------------------- /code/kmk/modules/oneshot.py: -------------------------------------------------------------------------------- 1 | from kmk.keys import make_argumented_key 2 | from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKeyMeta 3 | 4 | 5 | def oneshot_validator(kc, tap_time=None): 6 | return HoldTapKeyMeta(tap=kc, hold=kc, prefer_hold=False, tap_time=tap_time) 7 | 8 | 9 | class OneShot(HoldTap): 10 | tap_time = 1000 11 | 12 | def __init__(self): 13 | super().__init__() 14 | make_argumented_key( 15 | validator=oneshot_validator, 16 | names=('OS', 'ONESHOT'), 17 | on_press=self.osk_pressed, 18 | on_release=self.osk_released, 19 | ) 20 | 21 | def process_key(self, keyboard, current_key, is_pressed, int_coord): 22 | '''Release os key after interrupting keyup.''' 23 | for key, state in self.key_states.items(): 24 | if key == current_key: 25 | continue 26 | 27 | if state.activated == ActivationType.PRESSED and is_pressed: 28 | state.activated = ActivationType.HOLD_TIMEOUT 29 | elif state.activated == ActivationType.RELEASED and is_pressed: 30 | state.activated = ActivationType.INTERRUPTED 31 | elif state.activated == ActivationType.INTERRUPTED: 32 | if is_pressed: 33 | keyboard.remove_key(key.meta.tap) 34 | self.key_buffer.append((int_coord, current_key, is_pressed)) 35 | keyboard.set_timeout(False, lambda: self.send_key_buffer(keyboard)) 36 | current_key = None 37 | else: 38 | self.ht_released(key, keyboard) 39 | 40 | return current_key 41 | 42 | def osk_pressed(self, key, keyboard, *args, **kwargs): 43 | '''Register HoldTap mechanism and activate os key.''' 44 | self.ht_pressed(key, keyboard, *args, **kwargs) 45 | self.ht_activate_tap(key, keyboard, *args, **kwargs) 46 | self.send_key_buffer(keyboard) 47 | return keyboard 48 | 49 | def osk_released(self, key, keyboard, *args, **kwargs): 50 | '''On keyup, mark os key as released or handle HoldTap.''' 51 | try: 52 | state = self.key_states[key] 53 | except KeyError: 54 | if keyboard.debug_enabled: 55 | print(f'OneShot.osk_released: no such key {key}') 56 | return keyboard 57 | 58 | if state.activated == ActivationType.PRESSED: 59 | state.activated = ActivationType.RELEASED 60 | else: 61 | self.ht_released(key, keyboard, *args, **kwargs) 62 | 63 | return keyboard 64 | -------------------------------------------------------------------------------- /code/kmk/modules/potentiometer.py: -------------------------------------------------------------------------------- 1 | from analogio import AnalogIn 2 | from supervisor import ticks_ms 3 | 4 | from kmk.modules import Module 5 | 6 | 7 | class PotentiometerState: 8 | def __init__(self, direction: int, position: int): 9 | self.direction = direction 10 | self.position = position 11 | 12 | 13 | class Potentiometer: 14 | def __init__(self, pin, move_callback, is_inverted=False): 15 | self.is_inverted = is_inverted 16 | self.read_pin = AnalogIn(pin) 17 | self._direction = None 18 | self._pos = self.get_pos() 19 | self._timestamp = ticks_ms() 20 | self.cb = move_callback 21 | 22 | # callback function on events. 23 | self.on_move_do = lambda state: self.cb(state) 24 | 25 | def get_state(self) -> PotentiometerState: 26 | return PotentiometerState( 27 | direction=(self.is_inverted and -self._direction or self._direction), 28 | position=(self.is_inverted and -self._pos or self._pos), 29 | ) 30 | 31 | def get_pos(self): 32 | ''' 33 | Read from the analog pin assingned, truncate to 7 bits, 34 | average over 10 readings, and return a value 0-127 35 | ''' 36 | return int(sum([(self.read_pin.value >> 9) for i in range(10)]) / 10) 37 | 38 | def update_state(self): 39 | self._direction = 0 40 | new_pos = self.get_pos() 41 | if abs(new_pos - self._pos) > 2: 42 | # movement detected! 43 | if new_pos > self._pos: 44 | self._direction = 1 45 | else: 46 | self._direction = -1 47 | self._pos = new_pos 48 | if self.on_move_do is not None: 49 | self.on_move_do(self.get_state()) 50 | 51 | 52 | class PotentiometerHandler(Module): 53 | def __init__(self): 54 | self.potentiometers = [] 55 | self.pins = None 56 | 57 | def on_runtime_enable(self, keyboard): 58 | return 59 | 60 | def on_runtime_disable(self, keyboard): 61 | return 62 | 63 | def during_bootup(self, keyboard): 64 | if self.pins: 65 | for args in self.pins: 66 | self.potentiometers.append(Potentiometer(*args)) 67 | return 68 | 69 | def before_matrix_scan(self, keyboard): 70 | ''' 71 | Return value will be injected as an extra matrix update 72 | ''' 73 | for potentiometer in self.potentiometers: 74 | potentiometer.update_state() 75 | 76 | return keyboard 77 | 78 | def after_matrix_scan(self, keyboard): 79 | ''' 80 | Return value will be replace matrix update if supplied 81 | ''' 82 | return 83 | 84 | def before_hid_send(self, keyboard): 85 | return 86 | 87 | def after_hid_send(self, keyboard): 88 | return 89 | 90 | def on_powersave_enable(self, keyboard): 91 | return 92 | 93 | def on_powersave_disable(self, keyboard): 94 | return 95 | -------------------------------------------------------------------------------- /code/kmk/transports/pio_uart.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Circuit Python wrapper around PIO implementation of UART 3 | Original source of these examples: https://github.com/adafruit/Adafruit_CircuitPython_PIOASM/tree/main/examples (MIT) 4 | ''' 5 | import rp2pio 6 | from array import array 7 | 8 | ''' 9 | .program uart_tx 10 | .side_set 1 opt 11 | ; An 8n1 UART transmit program. 12 | ; OUT pin 0 and side-set pin 0 are both mapped to UART TX pin. 13 | pull side 1 [7] ; Assert stop bit, or stall with line in idle state 14 | set x, 7 side 0 [7] ; Preload bit counter, assert start bit for 8 clocks 15 | bitloop: ; This loop will run 8 times (8n1 UART) 16 | out pins, 1 ; Shift 1 bit from OSR to the first OUT pin 17 | jmp x-- bitloop [6] ; Each loop iteration is 8 cycles. 18 | 19 | ; compiles to: 20 | ''' 21 | tx_code = array('H', [40864, 63271, 24577, 1602]) 22 | 23 | 24 | ''' 25 | .program uart_rx_mini 26 | 27 | ; Minimum viable 8n1 UART receiver. Wait for the start bit, then sample 8 bits 28 | ; with the correct timing. 29 | ; IN pin 0 is mapped to the GPIO used as UART RX. 30 | ; Autopush must be enabled, with a threshold of 8. 31 | 32 | wait 0 pin 0 ; Wait for start bit 33 | set x, 7 [10] ; Preload bit counter, delay until eye of first data bit 34 | bitloop: ; Loop 8 times 35 | in pins, 1 ; Sample data 36 | jmp x-- bitloop [6] ; Each iteration is 8 cycles 37 | 38 | ; compiles to: 39 | ''' 40 | rx_code = array('H', [8224, 59943, 16385, 1602]) 41 | 42 | 43 | class PIO_UART: 44 | def __init__(self, *, tx, rx, baudrate=9600): 45 | if tx: 46 | self.tx_pio = rp2pio.StateMachine( 47 | tx_code, 48 | first_out_pin=tx, 49 | first_sideset_pin=tx, 50 | frequency=8 * baudrate, 51 | initial_sideset_pin_state=1, 52 | initial_sideset_pin_direction=1, 53 | initial_out_pin_state=1, 54 | initial_out_pin_direction=1, 55 | sideset_enable=True, 56 | ) 57 | if rx: 58 | self.rx_pio = rp2pio.StateMachine( 59 | rx_code, 60 | first_in_pin=rx, 61 | frequency=8 * baudrate, 62 | auto_push=True, 63 | push_threshold=8, 64 | ) 65 | 66 | @property 67 | def timeout(self): 68 | return 0 69 | 70 | @property 71 | def baudrate(self): 72 | return self.tx_pio.frequency // 8 73 | 74 | @baudrate.setter 75 | def baudrate(self, frequency): 76 | self.tx_pio.frequency = frequency * 8 77 | self.rx_pio.frequency = frequency * 8 78 | 79 | def write(self, buf): 80 | return self.tx_pio.write(buf) 81 | 82 | @property 83 | def in_waiting(self): 84 | return self.rx_pio.in_waiting 85 | 86 | def read(self, n): 87 | b = bytearray(n) 88 | n = self.rx_pio.readinto(b) 89 | return b[:n] 90 | 91 | def readinto(self, buf): 92 | return self.rx_pio.readinto(buf) 93 | -------------------------------------------------------------------------------- /code/kmk/modules/midi.py: -------------------------------------------------------------------------------- 1 | import adafruit_midi 2 | import usb_midi 3 | from adafruit_midi.control_change import ControlChange 4 | from adafruit_midi.note_off import NoteOff 5 | from adafruit_midi.note_on import NoteOn 6 | from adafruit_midi.pitch_bend import PitchBend 7 | from adafruit_midi.program_change import ProgramChange 8 | from adafruit_midi.start import Start 9 | from adafruit_midi.stop import Stop 10 | 11 | from kmk.keys import make_argumented_key 12 | from kmk.modules import Module 13 | 14 | 15 | class midiNoteValidator: 16 | def __init__(self, note=69, velocity=64, channel=None): 17 | self.note = note 18 | self.velocity = velocity 19 | self.channel = channel 20 | 21 | 22 | class MidiKeys(Module): 23 | def __init__(self): 24 | make_argumented_key( 25 | names=('MIDI_CC',), 26 | validator=ControlChange, 27 | on_press=self.on_press, 28 | ) 29 | 30 | make_argumented_key( 31 | names=('MIDI_NOTE',), 32 | validator=midiNoteValidator, 33 | on_press=self.note_on, 34 | on_release=self.note_off, 35 | ) 36 | 37 | make_argumented_key( 38 | names=('MIDI_PB',), 39 | validator=PitchBend, 40 | on_press=self.on_press, 41 | ) 42 | 43 | make_argumented_key( 44 | names=('MIDI_PC',), 45 | validator=ProgramChange, 46 | on_press=self.on_press, 47 | ) 48 | 49 | make_argumented_key( 50 | names=('MIDI_START',), 51 | validator=Start, 52 | on_press=self.on_press, 53 | ) 54 | 55 | make_argumented_key( 56 | names=('MIDI_STOP',), 57 | validator=Stop, 58 | on_press=self.on_press, 59 | ) 60 | 61 | try: 62 | self.midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) 63 | except IndexError: 64 | self.midi = None 65 | # if debug_enabled: 66 | print('No midi device found.') 67 | 68 | def during_bootup(self, keyboard): 69 | return None 70 | 71 | def before_matrix_scan(self, keyboard): 72 | return None 73 | 74 | def after_matrix_scan(self, keyboard): 75 | return None 76 | 77 | def process_key(self, keyboard, key, is_pressed, int_coord): 78 | return key 79 | 80 | def before_hid_send(self, keyboard): 81 | return None 82 | 83 | def after_hid_send(self, keyboard): 84 | return None 85 | 86 | def on_powersave_enable(self, keyboard): 87 | return None 88 | 89 | def on_powersave_disable(self, keyboard): 90 | return None 91 | 92 | def send(self, message): 93 | if self.midi: 94 | self.midi.send(message) 95 | 96 | def on_press(self, key, keyboard, *args, **kwargs): 97 | self.send(key.meta) 98 | 99 | def note_on(self, key, keyboard, *args, **kwargs): 100 | self.send(NoteOn(key.meta.note, key.meta.velocity, channel=key.meta.channel)) 101 | 102 | def note_off(self, key, keyboard, *args, **kwargs): 103 | self.send(NoteOff(key.meta.note, key.meta.velocity, channel=key.meta.channel)) 104 | -------------------------------------------------------------------------------- /code/kmk/scanners/keypad.py: -------------------------------------------------------------------------------- 1 | import keypad 2 | 3 | from kmk.scanners import DiodeOrientation, Scanner 4 | 5 | 6 | class KeypadScanner(Scanner): 7 | ''' 8 | Translation layer around a CircuitPython 7 keypad scanner. 9 | 10 | :param pin_map: A sequence of (row, column) tuples for each key. 11 | :param kp: An instance of the keypad class. 12 | ''' 13 | 14 | @property 15 | def key_count(self): 16 | return self.keypad.key_count 17 | 18 | def scan_for_changes(self): 19 | ''' 20 | Scan for key events and return a key report if an event exists. 21 | 22 | The key report is a byte array with contents [row, col, True if pressed else False] 23 | ''' 24 | ev = self.keypad.events.get() 25 | if ev and self.offset: 26 | return keypad.Event(ev.key_number + self.offset, ev.pressed) 27 | return ev 28 | 29 | 30 | class MatrixScanner(KeypadScanner): 31 | ''' 32 | Row/Column matrix using the CircuitPython 7 keypad scanner. 33 | 34 | :param row_pins: A sequence of pins used for rows. 35 | :param col_pins: A sequence of pins used for columns. 36 | :param direction: The diode orientation of the matrix. 37 | ''' 38 | 39 | def __init__( 40 | self, 41 | row_pins, 42 | column_pins, 43 | *, 44 | columns_to_anodes=DiodeOrientation.COL2ROW, 45 | interval=0.02, 46 | max_events=64, 47 | ): 48 | self.keypad = keypad.KeyMatrix( 49 | row_pins, 50 | column_pins, 51 | columns_to_anodes=(columns_to_anodes == DiodeOrientation.COL2ROW), 52 | interval=interval, 53 | max_events=max_events, 54 | ) 55 | super().__init__() 56 | 57 | 58 | class KeysScanner(KeypadScanner): 59 | ''' 60 | GPIO-per-key 'matrix' using the native CircuitPython 7 keypad scanner. 61 | 62 | :param pins: An array of arrays of CircuitPython Pin objects, such that pins[r][c] is the pin for row r, column c. 63 | ''' 64 | 65 | def __init__( 66 | self, 67 | pins, 68 | *, 69 | value_when_pressed=False, 70 | pull=True, 71 | interval=0.02, 72 | max_events=64, 73 | ): 74 | self.keypad = keypad.Keys( 75 | pins, 76 | value_when_pressed=value_when_pressed, 77 | pull=pull, 78 | interval=interval, 79 | max_events=max_events, 80 | ) 81 | super().__init__() 82 | 83 | 84 | class ShiftRegisterKeys(KeypadScanner): 85 | def __init__( 86 | self, 87 | *, 88 | clock, 89 | data, 90 | latch, 91 | value_to_latch=True, 92 | key_count, 93 | value_when_pressed=False, 94 | interval=0.02, 95 | max_events=64, 96 | ): 97 | self.keypad = keypad.ShiftRegisterKeys( 98 | clock=clock, 99 | data=data, 100 | latch=latch, 101 | value_to_latch=value_to_latch, 102 | key_count=key_count, 103 | value_when_pressed=value_when_pressed, 104 | interval=interval, 105 | max_events=max_events, 106 | ) 107 | super().__init__() 108 | -------------------------------------------------------------------------------- /code/kmk/modules/capsword.py: -------------------------------------------------------------------------------- 1 | from kmk.keys import FIRST_KMK_INTERNAL_KEY, KC, ModifierKey, make_key 2 | from kmk.modules import Module 3 | 4 | 5 | class CapsWord(Module): 6 | # default timeout is 8000 7 | # alphabets, numbers and few more keys will not disable capsword 8 | def __init__(self, timeout=8000): 9 | self._alphabets = range(KC.A.code, KC.Z.code) 10 | self._numbers = range(KC.N1.code, KC.N0.code) 11 | self.keys_ignored = [ 12 | KC.MINS, 13 | KC.BSPC, 14 | KC.UNDS, 15 | ] 16 | self._timeout_key = False 17 | self._cw_active = False 18 | self.timeout = timeout 19 | make_key( 20 | names=( 21 | 'CAPSWORD', 22 | 'CW', 23 | ), 24 | on_press=self.cw_pressed, 25 | ) 26 | 27 | def during_bootup(self, keyboard): 28 | return 29 | 30 | def before_matrix_scan(self, keyboard): 31 | return 32 | 33 | def process_key(self, keyboard, key, is_pressed, int_coord): 34 | if self._cw_active and key != KC.CW: 35 | continue_cw = False 36 | # capitalize alphabets 37 | if key.code in self._alphabets: 38 | continue_cw = True 39 | keyboard.process_key(KC.LSFT, is_pressed) 40 | elif ( 41 | key.code in self._numbers 42 | or isinstance(key, ModifierKey) 43 | or key in self.keys_ignored 44 | or key.code 45 | >= FIRST_KMK_INTERNAL_KEY # user defined keys are also ignored 46 | ): 47 | continue_cw = True 48 | # requests and cancels existing timeouts 49 | if is_pressed: 50 | if continue_cw: 51 | self.discard_timeout(keyboard) 52 | self.request_timeout(keyboard) 53 | else: 54 | self.process_timeout() 55 | 56 | return key 57 | 58 | def before_hid_send(self, keyboard): 59 | return 60 | 61 | def after_hid_send(self, keyboard): 62 | return 63 | 64 | def on_powersave_enable(self, keyboard): 65 | return 66 | 67 | def on_powersave_disable(self, keyboard): 68 | return 69 | 70 | def after_matrix_scan(self, keyboard): 71 | return 72 | 73 | def process_timeout(self): 74 | self._cw_active = False 75 | self._timeout_key = False 76 | 77 | def request_timeout(self, keyboard): 78 | if self._cw_active: 79 | if self.timeout: 80 | self._timeout_key = keyboard.set_timeout( 81 | self.timeout, lambda: self.process_timeout() 82 | ) 83 | 84 | def discard_timeout(self, keyboard): 85 | if self._timeout_key: 86 | if self.timeout: 87 | keyboard.cancel_timeout(self._timeout_key) 88 | self._timeout_key = False 89 | 90 | def cw_pressed(self, key, keyboard, *args, **kwargs): 91 | # enables/disables capsword 92 | if key == KC.CW: 93 | if not self._cw_active: 94 | self._cw_active = True 95 | self.discard_timeout(keyboard) 96 | self.request_timeout(keyboard) 97 | else: 98 | self.discard_timeout(keyboard) 99 | self.process_timeout() 100 | -------------------------------------------------------------------------------- /code/kmk/modules/rapidfire.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | from kmk.keys import make_argumented_key 4 | from kmk.modules import Module 5 | 6 | 7 | class RapidFireMeta: 8 | def __init__( 9 | self, 10 | kc, 11 | interval=100, 12 | timeout=200, 13 | enable_interval_randomization=False, 14 | randomization_magnitude=15, 15 | toggle=False, 16 | ): 17 | self.kc = kc 18 | self.interval = interval 19 | self.timeout = timeout 20 | self.enable_interval_randomization = enable_interval_randomization 21 | self.randomization_magnitude = randomization_magnitude 22 | self.toggle = toggle 23 | 24 | 25 | class RapidFire(Module): 26 | _active_keys = {} 27 | _toggled_keys = [] 28 | _waiting_keys = [] 29 | 30 | def __init__(self): 31 | make_argumented_key( 32 | validator=RapidFireMeta, 33 | names=('RF',), 34 | on_press=self._rf_pressed, 35 | on_release=self._rf_released, 36 | ) 37 | 38 | def _get_repeat(self, key): 39 | if key.meta.enable_interval_randomization: 40 | return key.meta.interval + randint( 41 | -key.meta.randomization_magnitude, key.meta.randomization_magnitude 42 | ) 43 | return key.meta.interval 44 | 45 | def _on_timer_timeout(self, key, keyboard): 46 | keyboard.tap_key(key.meta.kc) 47 | if key in self._waiting_keys: 48 | self._waiting_keys.remove(key) 49 | if key.meta.toggle and key not in self._toggled_keys: 50 | self._toggled_keys.append(key) 51 | self._active_keys[key] = keyboard.set_timeout( 52 | self._get_repeat(key), lambda: self._on_timer_timeout(key, keyboard) 53 | ) 54 | 55 | def _rf_pressed(self, key, keyboard, *args, **kwargs): 56 | if key in self._toggled_keys: 57 | self._toggled_keys.remove(key) 58 | self._deactivate_key(key, keyboard) 59 | return 60 | if key.meta.timeout > 0: 61 | keyboard.tap_key(key.meta.kc) 62 | self._waiting_keys.append(key) 63 | self._active_keys[key] = keyboard.set_timeout( 64 | key.meta.timeout, lambda: self._on_timer_timeout(key, keyboard) 65 | ) 66 | else: 67 | self._on_timer_timeout(key, keyboard) 68 | 69 | def _rf_released(self, key, keyboard, *args, **kwargs): 70 | if key not in self._active_keys: 71 | return 72 | if key in self._toggled_keys: 73 | if key not in self._waiting_keys: 74 | return 75 | self._toggled_keys.remove(key) 76 | if key in self._waiting_keys: 77 | self._waiting_keys.remove(key) 78 | self._deactivate_key(key, keyboard) 79 | 80 | def _deactivate_key(self, key, keyboard): 81 | keyboard.cancel_timeout(self._active_keys[key]) 82 | self._active_keys.pop(key) 83 | 84 | def during_bootup(self, keyboard): 85 | return 86 | 87 | def before_matrix_scan(self, keyboard): 88 | return 89 | 90 | def before_hid_send(self, keyboard): 91 | return 92 | 93 | def after_hid_send(self, keyboard): 94 | return 95 | 96 | def on_powersave_enable(self, keyboard): 97 | return 98 | 99 | def on_powersave_disable(self, keyboard): 100 | return 101 | 102 | def after_matrix_scan(self, keyboard): 103 | return 104 | -------------------------------------------------------------------------------- /code/kmk/handlers/stock.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | 4 | def passthrough(key, keyboard, *args, **kwargs): 5 | return keyboard 6 | 7 | 8 | def default_pressed(key, keyboard, KC, coord_int=None, *args, **kwargs): 9 | keyboard.hid_pending = True 10 | 11 | keyboard.keys_pressed.add(key) 12 | 13 | return keyboard 14 | 15 | 16 | def default_released(key, keyboard, KC, coord_int=None, *args, **kwargs): # NOQA 17 | keyboard.hid_pending = True 18 | keyboard.keys_pressed.discard(key) 19 | 20 | return keyboard 21 | 22 | 23 | def reset(*args, **kwargs): 24 | import microcontroller 25 | 26 | microcontroller.reset() 27 | 28 | 29 | def reload(*args, **kwargs): 30 | import supervisor 31 | 32 | supervisor.reload() 33 | 34 | 35 | def bootloader(*args, **kwargs): 36 | import microcontroller 37 | 38 | microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER) 39 | microcontroller.reset() 40 | 41 | 42 | def debug_pressed(key, keyboard, KC, *args, **kwargs): 43 | if keyboard.debug_enabled: 44 | print('DebugDisable()') 45 | else: 46 | print('DebugEnable()') 47 | 48 | keyboard.debug_enabled = not keyboard.debug_enabled 49 | 50 | return keyboard 51 | 52 | 53 | def gesc_pressed(key, keyboard, KC, *args, **kwargs): 54 | GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI} 55 | 56 | if GESC_TRIGGERS.intersection(keyboard.keys_pressed): 57 | # First, release GUI if already pressed 58 | keyboard._send_hid() 59 | # if Shift is held, KC_GRAVE will become KC_TILDE on OS level 60 | keyboard.keys_pressed.add(KC.GRAVE) 61 | keyboard.hid_pending = True 62 | return keyboard 63 | 64 | # else return KC_ESC 65 | keyboard.keys_pressed.add(KC.ESCAPE) 66 | keyboard.hid_pending = True 67 | 68 | return keyboard 69 | 70 | 71 | def gesc_released(key, keyboard, KC, *args, **kwargs): 72 | keyboard.keys_pressed.discard(KC.ESCAPE) 73 | keyboard.keys_pressed.discard(KC.GRAVE) 74 | keyboard.hid_pending = True 75 | return keyboard 76 | 77 | 78 | def bkdl_pressed(key, keyboard, KC, *args, **kwargs): 79 | BKDL_TRIGGERS = {KC.LGUI, KC.RGUI} 80 | 81 | if BKDL_TRIGGERS.intersection(keyboard.keys_pressed): 82 | keyboard._send_hid() 83 | keyboard.keys_pressed.add(KC.DEL) 84 | keyboard.hid_pending = True 85 | return keyboard 86 | 87 | # else return KC_ESC 88 | keyboard.keys_pressed.add(KC.BKSP) 89 | keyboard.hid_pending = True 90 | 91 | return keyboard 92 | 93 | 94 | def bkdl_released(key, keyboard, KC, *args, **kwargs): 95 | keyboard.keys_pressed.discard(KC.BKSP) 96 | keyboard.keys_pressed.discard(KC.DEL) 97 | keyboard.hid_pending = True 98 | return keyboard 99 | 100 | 101 | def sleep_pressed(key, keyboard, KC, *args, **kwargs): 102 | sleep(key.meta.ms / 1000) 103 | return keyboard 104 | 105 | 106 | def uc_mode_pressed(key, keyboard, *args, **kwargs): 107 | keyboard.unicode_mode = key.meta.mode 108 | 109 | return keyboard 110 | 111 | 112 | def hid_switch(key, keyboard, *args, **kwargs): 113 | keyboard.hid_type, keyboard.secondary_hid_type = ( 114 | keyboard.secondary_hid_type, 115 | keyboard.hid_type, 116 | ) 117 | keyboard._init_hid() 118 | return keyboard 119 | 120 | 121 | def ble_refresh(key, keyboard, *args, **kwargs): 122 | from kmk.hid import HIDModes 123 | 124 | if keyboard.hid_type != HIDModes.BLE: 125 | return keyboard 126 | 127 | keyboard._hid_helper.stop_advertising() 128 | keyboard._hid_helper.start_advertising() 129 | return keyboard 130 | -------------------------------------------------------------------------------- /code/kmk/modules/easypoint.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Extension handles usage of AS5013 by AMS 3 | ''' 4 | 5 | from supervisor import ticks_ms 6 | 7 | from kmk.modules import Module 8 | from kmk.modules.mouse_keys import PointingDevice 9 | 10 | I2C_ADDRESS = 0x40 11 | I2X_ALT_ADDRESS = 0x41 12 | 13 | X = 0x10 14 | Y_RES_INT = 0x11 15 | 16 | XP = 0x12 17 | XN = 0x13 18 | YP = 0x14 19 | YN = 0x15 20 | 21 | M_CTRL = 0x2B 22 | T_CTRL = 0x2D 23 | 24 | Y_OFFSET = 17 25 | X_OFFSET = 7 26 | 27 | DEAD_X = 5 28 | DEAD_Y = 5 29 | 30 | 31 | class Easypoint(Module): 32 | '''Module handles usage of AS5013 by AMS''' 33 | 34 | def __init__( 35 | self, 36 | i2c, 37 | address=I2C_ADDRESS, 38 | y_offset=Y_OFFSET, 39 | x_offset=X_OFFSET, 40 | dead_x=DEAD_X, 41 | dead_y=DEAD_Y, 42 | ): 43 | self._i2c_address = address 44 | self._i2c_bus = i2c 45 | 46 | # HID parameters 47 | self.pointing_device = PointingDevice() 48 | self.polling_interval = 20 49 | self.last_tick = ticks_ms() 50 | 51 | # Offsets for poor soldering 52 | self.y_offset = y_offset 53 | self.x_offset = x_offset 54 | 55 | # Deadzone 56 | self.dead_x = DEAD_X 57 | self.dead_y = DEAD_Y 58 | 59 | def during_bootup(self, keyboard): 60 | return 61 | 62 | def before_matrix_scan(self, keyboard): 63 | ''' 64 | Return value will be injected as an extra matrix update 65 | ''' 66 | now = ticks_ms() 67 | if now - self.last_tick < self.polling_interval: 68 | return 69 | self.last_tick = now 70 | 71 | x, y = self._read_raw_state() 72 | 73 | # I'm a shit coder, so offset is handled in software side 74 | s_x = self.getSignedNumber(x, 8) - self.x_offset 75 | s_y = self.getSignedNumber(y, 8) - self.y_offset 76 | 77 | # Evaluate Deadzone 78 | if s_x in range(-self.dead_x, self.dead_x) and s_y in range( 79 | -self.dead_y, self.dead_y 80 | ): 81 | # Within bounds, just die 82 | return 83 | else: 84 | # Set the X/Y from easypoint 85 | self.pointing_device.report_x[0] = x 86 | self.pointing_device.report_y[0] = y 87 | 88 | self.pointing_device.hid_pending = x != 0 or y != 0 89 | 90 | return 91 | 92 | def after_matrix_scan(self, keyboard): 93 | return 94 | 95 | def before_hid_send(self, keyboard): 96 | return 97 | 98 | def after_hid_send(self, keyboard): 99 | if self.pointing_device.hid_pending: 100 | keyboard._hid_helper.hid_send(self.pointing_device._evt) 101 | self._clear_pending_hid() 102 | return 103 | 104 | def on_powersave_enable(self, keyboard): 105 | return 106 | 107 | def on_powersave_disable(self, keyboard): 108 | return 109 | 110 | def _clear_pending_hid(self): 111 | self.pointing_device.hid_pending = False 112 | self.pointing_device.report_x[0] = 0 113 | self.pointing_device.report_y[0] = 0 114 | self.pointing_device.report_w[0] = 0 115 | self.pointing_device.button_status[0] = 0 116 | 117 | def _read_raw_state(self): 118 | '''Read data from AS5013''' 119 | x, y = self._i2c_rdwr([X], length=2) 120 | return x, y 121 | 122 | def getSignedNumber(self, number, bitLength=8): 123 | mask = (2 ** bitLength) - 1 124 | if number & (1 << (bitLength - 1)): 125 | return number | ~mask 126 | else: 127 | return number & mask 128 | 129 | def _i2c_rdwr(self, data, length=1): 130 | '''Write and optionally read I2C data.''' 131 | while not self._i2c_bus.try_lock(): 132 | pass 133 | 134 | try: 135 | if length > 0: 136 | result = bytearray(length) 137 | self._i2c_bus.writeto_then_readfrom( 138 | self._i2c_address, bytes(data), result 139 | ) 140 | return result 141 | else: 142 | self._i2c_bus.writeto(self._i2c_address, bytes(data)) 143 | return [] 144 | finally: 145 | self._i2c_bus.unlock() 146 | -------------------------------------------------------------------------------- /code/kmk/modules/tapdance.py: -------------------------------------------------------------------------------- 1 | from kmk.keys import KC, make_argumented_key 2 | from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKeyMeta 3 | 4 | 5 | class TapDanceKeyMeta: 6 | def __init__(self, *keys, tap_time=None): 7 | ''' 8 | Any key in the tapdance sequence that is not already a holdtap 9 | key gets converted to a holdtap key with identical tap and hold 10 | meta attributes. 11 | ''' 12 | self.tap_time = tap_time 13 | self.keys = [] 14 | 15 | for key in keys: 16 | if not isinstance(key.meta, HoldTapKeyMeta): 17 | ht_key = KC.HT( 18 | tap=key, 19 | hold=key, 20 | prefer_hold=False, 21 | tap_interrupted=False, 22 | tap_time=self.tap_time, 23 | ) 24 | self.keys.append(ht_key) 25 | else: 26 | self.keys.append(key) 27 | self.keys = tuple(self.keys) 28 | 29 | 30 | class TapDance(HoldTap): 31 | def __init__(self): 32 | super().__init__() 33 | make_argumented_key( 34 | validator=TapDanceKeyMeta, 35 | names=('TD',), 36 | on_press=self.td_pressed, 37 | on_release=self.td_released, 38 | ) 39 | 40 | self.td_counts = {} 41 | 42 | def process_key(self, keyboard, key, is_pressed, int_coord): 43 | if isinstance(key.meta, TapDanceKeyMeta): 44 | if key in self.td_counts: 45 | return key 46 | 47 | for _key, state in self.key_states.copy().items(): 48 | if state.activated == ActivationType.RELEASED: 49 | keyboard.cancel_timeout(state.timeout_key) 50 | self.ht_activate_tap(_key, keyboard) 51 | self.send_key_buffer(keyboard) 52 | self.ht_deactivate_tap(_key, keyboard) 53 | keyboard.resume_process_key(self, key, is_pressed, int_coord) 54 | key = None 55 | 56 | del self.key_states[_key] 57 | del self.td_counts[state.tap_dance] 58 | 59 | key = super().process_key(keyboard, key, is_pressed, int_coord) 60 | 61 | return key 62 | 63 | def td_pressed(self, key, keyboard, *args, **kwargs): 64 | # active tap dance 65 | if key in self.td_counts: 66 | count = self.td_counts[key] 67 | kc = key.meta.keys[count] 68 | keyboard.cancel_timeout(self.key_states[kc].timeout_key) 69 | 70 | count += 1 71 | 72 | # Tap dance reached the end of the list: send last tap in sequence 73 | # and start from the beginning. 74 | if count >= len(key.meta.keys): 75 | self.key_states[kc].activated = ActivationType.RELEASED 76 | self.on_tap_time_expired(kc, keyboard) 77 | count = 0 78 | else: 79 | del self.key_states[kc] 80 | 81 | # new tap dance 82 | else: 83 | count = 0 84 | 85 | current_key = key.meta.keys[count] 86 | 87 | self.ht_pressed(current_key, keyboard, *args, **kwargs) 88 | self.td_counts[key] = count 89 | 90 | # Add the active tap dance to key_states; `on_tap_time_expired` needs 91 | # the back-reference. 92 | self.key_states[current_key].tap_dance = key 93 | 94 | def td_released(self, key, keyboard, *args, **kwargs): 95 | try: 96 | kc = key.meta.keys[self.td_counts[key]] 97 | except KeyError: 98 | return 99 | state = self.key_states[kc] 100 | if state.activated == ActivationType.HOLD_TIMEOUT: 101 | # release hold 102 | self.ht_deactivate_hold(kc, keyboard, *args, **kwargs) 103 | del self.key_states[kc] 104 | del self.td_counts[key] 105 | elif state.activated == ActivationType.INTERRUPTED: 106 | # release tap 107 | self.ht_deactivate_on_interrupt(kc, keyboard, *args, **kwargs) 108 | del self.key_states[kc] 109 | del self.td_counts[key] 110 | else: 111 | # keep counting 112 | state.activated = ActivationType.RELEASED 113 | 114 | def on_tap_time_expired(self, key, keyboard, *args, **kwargs): 115 | # Note: the `key` argument is the current holdtap key in the sequence, 116 | # not the tapdance key. 117 | state = self.key_states[key] 118 | if state.activated == ActivationType.RELEASED: 119 | self.ht_activate_tap(key, keyboard, *args, **kwargs) 120 | self.send_key_buffer(keyboard) 121 | del self.td_counts[state.tap_dance] 122 | super().on_tap_time_expired(key, keyboard, *args, **kwargs) 123 | -------------------------------------------------------------------------------- /code/kmk/extensions/statusled.py: -------------------------------------------------------------------------------- 1 | # Use this extension for showing layer status with three leds 2 | 3 | import pwmio 4 | import time 5 | 6 | from kmk.extensions import Extension, InvalidExtensionEnvironment 7 | from kmk.keys import make_key 8 | 9 | 10 | class statusLED(Extension): 11 | def __init__( 12 | self, 13 | led_pins, 14 | brightness=30, 15 | brightness_step=5, 16 | brightness_limit=100, 17 | ): 18 | self._leds = [] 19 | for led in led_pins: 20 | try: 21 | self._leds.append(pwmio.PWMOut(led)) 22 | except Exception as e: 23 | print(e) 24 | raise InvalidExtensionEnvironment( 25 | 'Unable to create pulseio.PWMOut() instance with provided led_pin' 26 | ) 27 | self._led_count = len(self._leds) 28 | 29 | self.brightness = brightness 30 | self._layer_last = -1 31 | 32 | self.brightness_step = brightness_step 33 | self.brightness_limit = brightness_limit 34 | 35 | make_key(names=('SLED_INC',), on_press=self._key_led_inc) 36 | make_key(names=('SLED_DEC',), on_press=self._key_led_dec) 37 | 38 | def _layer_indicator(self, layer_active, *args, **kwargs): 39 | ''' 40 | Indicates layer with leds 41 | 42 | For the time being just a simple consecutive single led 43 | indicator. And when there are more layers than leds it 44 | wraps around to the first led again. 45 | (Also works for a single led, which just lights when any 46 | layer is active) 47 | ''' 48 | 49 | if self._layer_last != layer_active: 50 | led_last = 0 if self._layer_last == 0 else 1 + (self._layer_last - 1) % 3 51 | if layer_active > 0: 52 | led_active = 0 if layer_active == 0 else 1 + (layer_active - 1) % 3 53 | self.set_brightness(self.brightness, led_active) 54 | self.set_brightness(0, led_last) 55 | else: 56 | self.set_brightness(0, led_last) 57 | self._layer_last = layer_active 58 | 59 | def __repr__(self): 60 | return f'SLED({self._to_dict()})' 61 | 62 | def _to_dict(self): 63 | return { 64 | '_brightness': self.brightness, 65 | 'brightness_step': self.brightness_step, 66 | 'brightness_limit': self.brightness_limit, 67 | } 68 | 69 | def on_runtime_enable(self, sandbox): 70 | return 71 | 72 | def on_runtime_disable(self, sandbox): 73 | return 74 | 75 | def during_bootup(self, sandbox): 76 | '''Light up every single led once for 200 ms''' 77 | for i in range(self._led_count + 2): 78 | if i < self._led_count: 79 | self._leds[i].duty_cycle = int(self.brightness / 100 * 65535) 80 | i_off = i - 2 81 | if i_off >= 0 and i_off < self._led_count: 82 | self._leds[i_off].duty_cycle = int(0) 83 | time.sleep(0.1) 84 | for led in self._leds: 85 | led.duty_cycle = int(0) 86 | return 87 | 88 | def before_matrix_scan(self, sandbox): 89 | return 90 | 91 | def after_matrix_scan(self, sandbox): 92 | self._layer_indicator(sandbox.active_layers[0]) 93 | return 94 | 95 | def before_hid_send(self, sandbox): 96 | return 97 | 98 | def after_hid_send(self, sandbox): 99 | return 100 | 101 | def on_powersave_enable(self, sandbox): 102 | self.set_brightness(0) 103 | return 104 | 105 | def on_powersave_disable(self, sandbox): 106 | self.set_brightness(self._brightness) 107 | self._leds[2].duty_cycle = int(50 / 100 * 65535) 108 | time.sleep(0.2) 109 | self._leds[2].duty_cycle = int(0) 110 | return 111 | 112 | def set_brightness(self, percent, layer_id=-1): 113 | if layer_id < 0: 114 | for led in self._leds: 115 | led.duty_cycle = int(percent / 100 * 65535) 116 | else: 117 | self._leds[layer_id - 1].duty_cycle = int(percent / 100 * 65535) 118 | 119 | def increase_brightness(self, step=None): 120 | if not step: 121 | self._brightness += self.brightness_step 122 | else: 123 | self._brightness += step 124 | 125 | if self._brightness > 100: 126 | self._brightness = 100 127 | 128 | self.set_brightness(self._brightness, self._layer_last) 129 | 130 | def decrease_brightness(self, step=None): 131 | if not step: 132 | self._brightness -= self.brightness_step 133 | else: 134 | self._brightness -= step 135 | 136 | if self._brightness < 0: 137 | self._brightness = 0 138 | 139 | self.set_brightness(self._brightness, self._layer_last) 140 | 141 | def _key_led_inc(self, *args, **kwargs): 142 | self.increase_brightness() 143 | 144 | def _key_led_dec(self, *args, **kwargs): 145 | self.decrease_brightness() 146 | -------------------------------------------------------------------------------- /code/kmk/extensions/peg_oled_display.py: -------------------------------------------------------------------------------- 1 | import busio 2 | import gc 3 | 4 | import adafruit_displayio_ssd1306 5 | import displayio 6 | import terminalio 7 | from adafruit_display_text import label 8 | 9 | from kmk.extensions import Extension 10 | 11 | 12 | class OledDisplayMode: 13 | TXT = 0 14 | IMG = 1 15 | 16 | 17 | class OledReactionType: 18 | STATIC = 0 19 | LAYER = 1 20 | 21 | 22 | class OledData: 23 | def __init__( 24 | self, 25 | image=None, 26 | corner_one=None, 27 | corner_two=None, 28 | corner_three=None, 29 | corner_four=None, 30 | ): 31 | if image: 32 | self.data = [image] 33 | elif corner_one and corner_two and corner_three and corner_four: 34 | self.data = [corner_one, corner_two, corner_three, corner_four] 35 | 36 | 37 | class Oled(Extension): 38 | def __init__( 39 | self, 40 | views, 41 | toDisplay=OledDisplayMode.TXT, 42 | oWidth=128, 43 | oHeight=32, 44 | flip: bool = False, 45 | ): 46 | displayio.release_displays() 47 | self.rotation = 180 if flip else 0 48 | self._views = views.data 49 | self._toDisplay = toDisplay 50 | self._width = oWidth 51 | self._height = oHeight 52 | self._prevLayers = 0 53 | gc.collect() 54 | 55 | def returnCurrectRenderText(self, layer, singleView): 56 | # for now we only have static things and react to layers. But when we react to battery % and wpm we can handle the logic here 57 | if singleView[0] == OledReactionType.STATIC: 58 | return singleView[1][0] 59 | if singleView[0] == OledReactionType.LAYER: 60 | return singleView[1][layer] 61 | 62 | def renderOledTextLayer(self, layer): 63 | splash = displayio.Group() 64 | splash.append( 65 | label.Label( 66 | terminalio.FONT, 67 | text=self.returnCurrectRenderText(layer, self._views[0]), 68 | color=0xFFFFFF, 69 | x=0, 70 | y=10, 71 | ) 72 | ) 73 | splash.append( 74 | label.Label( 75 | terminalio.FONT, 76 | text=self.returnCurrectRenderText(layer, self._views[1]), 77 | color=0xFFFFFF, 78 | x=64, 79 | y=10, 80 | ) 81 | ) 82 | splash.append( 83 | label.Label( 84 | terminalio.FONT, 85 | text=self.returnCurrectRenderText(layer, self._views[2]), 86 | color=0xFFFFFF, 87 | x=0, 88 | y=25, 89 | ) 90 | ) 91 | splash.append( 92 | label.Label( 93 | terminalio.FONT, 94 | text=self.returnCurrectRenderText(layer, self._views[3]), 95 | color=0xFFFFFF, 96 | x=64, 97 | y=25, 98 | ) 99 | ) 100 | self._display.show(splash) 101 | gc.collect() 102 | 103 | def renderOledImgLayer(self, layer): 104 | splash = displayio.Group() 105 | odb = displayio.OnDiskBitmap( 106 | '/' + self.returnCurrectRenderText(layer, self._views[0]) 107 | ) 108 | image = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader) 109 | splash.append(image) 110 | self._display.show(splash) 111 | gc.collect() 112 | 113 | def updateOLED(self, sandbox): 114 | if self._toDisplay == OledDisplayMode.TXT: 115 | self.renderOledTextLayer(sandbox.active_layers[0]) 116 | if self._toDisplay == OledDisplayMode.IMG: 117 | self.renderOledImgLayer(sandbox.active_layers[0]) 118 | gc.collect() 119 | 120 | def on_runtime_enable(self, sandbox): 121 | return 122 | 123 | def on_runtime_disable(self, sandbox): 124 | return 125 | 126 | def during_bootup(self, board): 127 | displayio.release_displays() 128 | i2c = busio.I2C(board.SCL, board.SDA) 129 | self._display = adafruit_displayio_ssd1306.SSD1306( 130 | displayio.I2CDisplay(i2c, device_address=0x3C), 131 | width=self._width, 132 | height=self._height, 133 | rotation=self.rotation, 134 | ) 135 | if self._toDisplay == OledDisplayMode.TXT: 136 | self.renderOledTextLayer(0) 137 | if self._toDisplay == OledDisplayMode.IMG: 138 | self.renderOledImgLayer(0) 139 | return 140 | 141 | def before_matrix_scan(self, sandbox): 142 | if sandbox.active_layers[0] != self._prevLayers: 143 | self._prevLayers = sandbox.active_layers[0] 144 | self.updateOLED(sandbox) 145 | return 146 | 147 | def after_matrix_scan(self, sandbox): 148 | 149 | return 150 | 151 | def before_hid_send(self, sandbox): 152 | return 153 | 154 | def after_hid_send(self, sandbox): 155 | return 156 | 157 | def on_powersave_enable(self, sandbox): 158 | return 159 | 160 | def on_powersave_disable(self, sandbox): 161 | return 162 | -------------------------------------------------------------------------------- /code/kmk/handlers/sequences.py: -------------------------------------------------------------------------------- 1 | import gc 2 | 3 | from kmk.consts import UnicodeMode 4 | from kmk.handlers.stock import passthrough 5 | from kmk.keys import KC, make_key 6 | from kmk.types import AttrDict, KeySequenceMeta 7 | 8 | 9 | def get_wide_ordinal(char): 10 | if len(char) != 2: 11 | return ord(char) 12 | 13 | return 0x10000 + (ord(char[0]) - 0xD800) * 0x400 + (ord(char[1]) - 0xDC00) 14 | 15 | 16 | def sequence_press_handler(key, keyboard, KC, *args, **kwargs): 17 | oldkeys_pressed = keyboard.keys_pressed 18 | keyboard.keys_pressed = set() 19 | 20 | for ikey in key.meta.seq: 21 | if not getattr(ikey, 'no_press', None): 22 | keyboard.process_key(ikey, True) 23 | keyboard._send_hid() 24 | if not getattr(ikey, 'no_release', None): 25 | keyboard.process_key(ikey, False) 26 | keyboard._send_hid() 27 | 28 | keyboard.keys_pressed = oldkeys_pressed 29 | 30 | return keyboard 31 | 32 | 33 | def simple_key_sequence(seq): 34 | return make_key( 35 | meta=KeySequenceMeta(seq), 36 | on_press=sequence_press_handler, 37 | on_release=passthrough, 38 | ) 39 | 40 | 41 | def send_string(message): 42 | seq = [] 43 | 44 | for char in message: 45 | kc = getattr(KC, char.upper()) 46 | 47 | if char.isupper(): 48 | kc = KC.LSHIFT(kc) 49 | 50 | seq.append(kc) 51 | 52 | return simple_key_sequence(seq) 53 | 54 | 55 | IBUS_KEY_COMBO = simple_key_sequence((KC.LCTRL(KC.LSHIFT(KC.U)),)) 56 | RALT_KEY = simple_key_sequence((KC.RALT,)) 57 | U_KEY = simple_key_sequence((KC.U,)) 58 | ENTER_KEY = simple_key_sequence((KC.ENTER,)) 59 | RALT_DOWN_NO_RELEASE = simple_key_sequence((KC.RALT(no_release=True),)) 60 | RALT_UP_NO_PRESS = simple_key_sequence((KC.RALT(no_press=True),)) 61 | 62 | 63 | def compile_unicode_string_sequences(string_table): 64 | ''' 65 | Destructively convert ("compile") unicode strings into key sequences. This 66 | will, for RAM saving reasons, empty the input dictionary and trigger 67 | garbage collection. 68 | ''' 69 | target = AttrDict() 70 | 71 | for k, v in string_table.items(): 72 | target[k] = unicode_string_sequence(v) 73 | 74 | # now loop through and kill the input dictionary to save RAM 75 | for k in target.keys(): 76 | del string_table[k] 77 | 78 | gc.collect() 79 | 80 | return target 81 | 82 | 83 | def unicode_string_sequence(unistring): 84 | ''' 85 | Allows sending things like (╯°□°)╯︵ ┻━┻ directly, without 86 | manual conversion to Unicode codepoints. 87 | ''' 88 | return unicode_codepoint_sequence([hex(get_wide_ordinal(s))[2:] for s in unistring]) 89 | 90 | 91 | def generate_codepoint_keysym_seq(codepoint, expected_length=4): 92 | # To make MacOS and Windows happy, always try to send 93 | # sequences that are of length 4 at a minimum 94 | # On Linux systems, we can happily send longer strings. 95 | # They will almost certainly break on MacOS and Windows, 96 | # but this is a documentation problem more than anything. 97 | # Not sure how to send emojis on Mac/Windows like that, 98 | # though, since (for example) the Canadian flag is assembled 99 | # from two five-character codepoints, 1f1e8 and 1f1e6 100 | seq = [KC.N0 for _ in range(max(len(codepoint), expected_length))] 101 | 102 | for idx, codepoint_fragment in enumerate(reversed(codepoint)): 103 | seq[-(idx + 1)] = KC.__getattr__(codepoint_fragment.upper()) 104 | 105 | return seq 106 | 107 | 108 | def unicode_codepoint_sequence(codepoints): 109 | kc_seqs = (generate_codepoint_keysym_seq(codepoint) for codepoint in codepoints) 110 | 111 | kc_macros = [simple_key_sequence(kc_seq) for kc_seq in kc_seqs] 112 | 113 | def _unicode_sequence(key, keyboard, *args, **kwargs): 114 | if keyboard.unicode_mode == UnicodeMode.IBUS: 115 | keyboard.process_key( 116 | simple_key_sequence(_ibus_unicode_sequence(kc_macros, keyboard)), True 117 | ) 118 | elif keyboard.unicode_mode == UnicodeMode.RALT: 119 | keyboard.process_key( 120 | simple_key_sequence(_ralt_unicode_sequence(kc_macros, keyboard)), True 121 | ) 122 | elif keyboard.unicode_mode == UnicodeMode.WINC: 123 | keyboard.process_key( 124 | simple_key_sequence(_winc_unicode_sequence(kc_macros, keyboard)), True 125 | ) 126 | 127 | return make_key(on_press=_unicode_sequence) 128 | 129 | 130 | def _ralt_unicode_sequence(kc_macros, keyboard): 131 | for kc_macro in kc_macros: 132 | yield RALT_DOWN_NO_RELEASE 133 | yield kc_macro 134 | yield RALT_UP_NO_PRESS 135 | 136 | 137 | def _ibus_unicode_sequence(kc_macros, keyboard): 138 | for kc_macro in kc_macros: 139 | yield IBUS_KEY_COMBO 140 | yield kc_macro 141 | yield ENTER_KEY 142 | 143 | 144 | def _winc_unicode_sequence(kc_macros, keyboard): 145 | ''' 146 | Send unicode sequence using WinCompose: 147 | 148 | http://wincompose.info/ 149 | https://github.com/SamHocevar/wincompose 150 | ''' 151 | for kc_macro in kc_macros: 152 | yield RALT_KEY 153 | yield U_KEY 154 | yield kc_macro 155 | yield ENTER_KEY 156 | -------------------------------------------------------------------------------- /code/kmk/modules/power.py: -------------------------------------------------------------------------------- 1 | import board 2 | import digitalio 3 | from supervisor import ticks_ms 4 | 5 | from time import sleep 6 | 7 | from kmk.handlers.stock import passthrough as handler_passthrough 8 | from kmk.keys import make_key 9 | from kmk.kmktime import check_deadline 10 | from kmk.modules import Module 11 | 12 | 13 | class Power(Module): 14 | def __init__(self, powersave_pin=None): 15 | self.enable = False 16 | self.powersave_pin = powersave_pin # Powersave pin board object 17 | self._powersave_start = ticks_ms() 18 | self._usb_last_scan = ticks_ms() - 5000 19 | self._psp = None # Powersave pin object 20 | self._i2c = 0 21 | self._loopcounter = 0 22 | 23 | make_key( 24 | names=('PS_TOG',), on_press=self._ps_tog, on_release=handler_passthrough 25 | ) 26 | make_key( 27 | names=('PS_ON',), on_press=self._ps_enable, on_release=handler_passthrough 28 | ) 29 | make_key( 30 | names=('PS_OFF',), on_press=self._ps_disable, on_release=handler_passthrough 31 | ) 32 | 33 | def __repr__(self): 34 | return f'Power({self._to_dict()})' 35 | 36 | def _to_dict(self): 37 | return { 38 | 'enable': self.enable, 39 | 'powersave_pin': self.powersave_pin, 40 | '_powersave_start': self._powersave_start, 41 | '_usb_last_scan': self._usb_last_scan, 42 | '_psp': self._psp, 43 | } 44 | 45 | def during_bootup(self, keyboard): 46 | self._i2c_scan() 47 | 48 | def before_matrix_scan(self, keyboard): 49 | return 50 | 51 | def after_matrix_scan(self, keyboard): 52 | if keyboard.matrix_update or keyboard.secondary_matrix_update: 53 | self.psave_time_reset() 54 | 55 | def before_hid_send(self, keyboard): 56 | return 57 | 58 | def after_hid_send(self, keyboard): 59 | if self.enable: 60 | self.psleep() 61 | 62 | def on_powersave_enable(self, keyboard): 63 | '''Gives 10 cycles to allow other extensions to clean up before powersave''' 64 | if self._loopcounter > 10: 65 | self.enable_powersave(keyboard) 66 | self._loopcounter = 0 67 | else: 68 | self._loopcounter += 1 69 | return 70 | 71 | def on_powersave_disable(self, keyboard): 72 | self.disable_powersave(keyboard) 73 | return 74 | 75 | def enable_powersave(self, keyboard): 76 | '''Enables power saving features''' 77 | if keyboard.i2c_deinit_count >= self._i2c and self.powersave_pin: 78 | # Allows power save to prevent RGB drain. 79 | # Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic 80 | 81 | if not self._psp: 82 | self._psp = digitalio.DigitalInOut(self.powersave_pin) 83 | self._psp.direction = digitalio.Direction.OUTPUT 84 | if self._psp: 85 | self._psp.value = True 86 | 87 | self.enable = True 88 | keyboard._trigger_powersave_enable = False 89 | return 90 | 91 | def disable_powersave(self, keyboard): 92 | '''Disables power saving features''' 93 | if self._psp: 94 | self._psp.value = False 95 | # Allows power save to prevent RGB drain. 96 | # Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic 97 | 98 | keyboard._trigger_powersave_disable = False 99 | self.enable = False 100 | return 101 | 102 | def psleep(self): 103 | ''' 104 | Sleeps longer and longer to save power the more time in between updates. 105 | ''' 106 | if check_deadline(ticks_ms(), self._powersave_start) <= 60000: 107 | sleep(8 / 1000) 108 | elif check_deadline(ticks_ms(), self._powersave_start) >= 240000: 109 | sleep(180 / 1000) 110 | return 111 | 112 | def psave_time_reset(self): 113 | self._powersave_start = ticks_ms() 114 | 115 | def _i2c_scan(self): 116 | i2c = board.I2C() 117 | while not i2c.try_lock(): 118 | pass 119 | try: 120 | self._i2c = len(i2c.scan()) 121 | finally: 122 | i2c.unlock() 123 | return 124 | 125 | def usb_rescan_timer(self): 126 | return bool(check_deadline(ticks_ms(), self._usb_last_scan) > 5000) 127 | 128 | def usb_time_reset(self): 129 | self._usb_last_scan = ticks_ms() 130 | return 131 | 132 | def usb_scan(self): 133 | # TODO Add USB detection here. Currently lies that it's connected 134 | # https://github.com/adafruit/circuitpython/pull/3513 135 | return True 136 | 137 | def _ps_tog(self, key, keyboard, *args, **kwargs): 138 | if self.enable: 139 | keyboard._trigger_powersave_disable = True 140 | else: 141 | keyboard._trigger_powersave_enable = True 142 | 143 | def _ps_enable(self, key, keyboard, *args, **kwargs): 144 | if not self.enable: 145 | keyboard._trigger_powersave_enable = True 146 | 147 | def _ps_disable(self, key, keyboard, *args, **kwargs): 148 | if self.enable: 149 | keyboard._trigger_powersave_disable = True 150 | -------------------------------------------------------------------------------- /code/kmk/modules/layers.py: -------------------------------------------------------------------------------- 1 | '''One layer isn't enough. Adds keys to get to more of them''' 2 | from kmk.keys import KC, make_argumented_key 3 | from kmk.modules.holdtap import HoldTap, HoldTapKeyMeta 4 | from kmk.utils import Debug 5 | 6 | debug = Debug(__name__) 7 | 8 | 9 | def layer_key_validator(layer, kc=None): 10 | ''' 11 | Validates the syntax (but not semantics) of a layer key call. We won't 12 | have access to the keymap here, so we can't verify much of anything useful 13 | here (like whether the target layer actually exists). The spirit of this 14 | existing is mostly that Python will catch extraneous args/kwargs and error 15 | out. 16 | ''' 17 | return LayerKeyMeta(layer, kc) 18 | 19 | 20 | def layer_key_validator_lt(layer, kc, prefer_hold=False, **kwargs): 21 | return HoldTapKeyMeta(tap=kc, hold=KC.MO(layer), prefer_hold=prefer_hold, **kwargs) 22 | 23 | 24 | def layer_key_validator_tt(layer, prefer_hold=True, **kwargs): 25 | return HoldTapKeyMeta( 26 | tap=KC.TG(layer), hold=KC.MO(layer), prefer_hold=prefer_hold, **kwargs 27 | ) 28 | 29 | 30 | class LayerKeyMeta: 31 | def __init__(self, layer, kc=None): 32 | self.layer = layer 33 | self.kc = kc 34 | 35 | 36 | class Layers(HoldTap): 37 | '''Gives access to the keys used to enable the layer system''' 38 | 39 | def __init__(self): 40 | # Layers 41 | super().__init__() 42 | make_argumented_key( 43 | validator=layer_key_validator, 44 | names=('MO',), 45 | on_press=self._mo_pressed, 46 | on_release=self._mo_released, 47 | ) 48 | make_argumented_key( 49 | validator=layer_key_validator, names=('DF',), on_press=self._df_pressed 50 | ) 51 | make_argumented_key( 52 | validator=layer_key_validator, 53 | names=('LM',), 54 | on_press=self._lm_pressed, 55 | on_release=self._lm_released, 56 | ) 57 | make_argumented_key( 58 | validator=layer_key_validator, names=('TG',), on_press=self._tg_pressed 59 | ) 60 | make_argumented_key( 61 | validator=layer_key_validator, names=('TO',), on_press=self._to_pressed 62 | ) 63 | make_argumented_key( 64 | validator=layer_key_validator_lt, 65 | names=('LT',), 66 | on_press=self.ht_pressed, 67 | on_release=self.ht_released, 68 | ) 69 | make_argumented_key( 70 | validator=layer_key_validator_tt, 71 | names=('TT',), 72 | on_press=self.ht_pressed, 73 | on_release=self.ht_released, 74 | ) 75 | 76 | def _df_pressed(self, key, keyboard, *args, **kwargs): 77 | ''' 78 | Switches the default layer 79 | ''' 80 | keyboard.active_layers[-1] = key.meta.layer 81 | self._print_debug(keyboard) 82 | 83 | def _mo_pressed(self, key, keyboard, *args, **kwargs): 84 | ''' 85 | Momentarily activates layer, switches off when you let go 86 | ''' 87 | keyboard.active_layers.insert(0, key.meta.layer) 88 | self._print_debug(keyboard) 89 | 90 | @staticmethod 91 | def _mo_released(key, keyboard, *args, **kwargs): 92 | # remove the first instance of the target layer 93 | # from the active list 94 | # under almost all normal use cases, this will 95 | # disable the layer (but preserve it if it was triggered 96 | # as a default layer, etc.) 97 | # this also resolves an issue where using DF() on a layer 98 | # triggered by MO() and then defaulting to the MO()'s layer 99 | # would result in no layers active 100 | try: 101 | del_idx = keyboard.active_layers.index(key.meta.layer) 102 | del keyboard.active_layers[del_idx] 103 | except ValueError: 104 | pass 105 | __class__._print_debug(__class__, keyboard) 106 | 107 | def _lm_pressed(self, key, keyboard, *args, **kwargs): 108 | ''' 109 | As MO(layer) but with mod active 110 | ''' 111 | keyboard.hid_pending = True 112 | # Sets the timer start and acts like MO otherwise 113 | keyboard.keys_pressed.add(key.meta.kc) 114 | self._mo_pressed(key, keyboard, *args, **kwargs) 115 | 116 | def _lm_released(self, key, keyboard, *args, **kwargs): 117 | ''' 118 | As MO(layer) but with mod active 119 | ''' 120 | keyboard.hid_pending = True 121 | keyboard.keys_pressed.discard(key.meta.kc) 122 | self._mo_released(key, keyboard, *args, **kwargs) 123 | 124 | def _tg_pressed(self, key, keyboard, *args, **kwargs): 125 | ''' 126 | Toggles the layer (enables it if not active, and vise versa) 127 | ''' 128 | # See mo_released for implementation details around this 129 | try: 130 | del_idx = keyboard.active_layers.index(key.meta.layer) 131 | del keyboard.active_layers[del_idx] 132 | except ValueError: 133 | keyboard.active_layers.insert(0, key.meta.layer) 134 | 135 | def _to_pressed(self, key, keyboard, *args, **kwargs): 136 | ''' 137 | Activates layer and deactivates all other layers 138 | ''' 139 | keyboard.active_layers.clear() 140 | keyboard.active_layers.insert(0, key.meta.layer) 141 | 142 | def _print_debug(self, keyboard): 143 | # debug(f'__getitem__ {key}') 144 | if debug.enabled: 145 | debug(f'active_layers={keyboard.active_layers}') 146 | -------------------------------------------------------------------------------- /code/kmk/scanners/digitalio.py: -------------------------------------------------------------------------------- 1 | import digitalio 2 | 3 | from keypad import Event as KeyEvent 4 | 5 | from kmk.scanners import DiodeOrientation, Scanner 6 | 7 | 8 | class MatrixScanner(Scanner): 9 | def __init__( 10 | self, 11 | cols, 12 | rows, 13 | diode_orientation=DiodeOrientation.COLUMNS, 14 | rollover_cols_every_rows=None, 15 | offset=0, 16 | ): 17 | self.len_cols = len(cols) 18 | self.len_rows = len(rows) 19 | self.offset = offset 20 | 21 | # A pin cannot be both a row and column, detect this by combining the 22 | # two tuples into a set and validating that the length did not drop 23 | # 24 | # repr() hackery is because CircuitPython Pin objects are not hashable 25 | unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows} 26 | assert ( 27 | len(unique_pins) == self.len_cols + self.len_rows 28 | ), 'Cannot use a pin as both a column and row' 29 | del unique_pins 30 | 31 | self.diode_orientation = diode_orientation 32 | 33 | # __class__.__name__ is used instead of isinstance as the MCP230xx lib 34 | # does not use the digitalio.DigitalInOut, but rather a self defined one: 35 | # https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33 36 | 37 | if self.diode_orientation == DiodeOrientation.COLUMNS: 38 | self.outputs = [ 39 | x 40 | if x.__class__.__name__ == 'DigitalInOut' 41 | else digitalio.DigitalInOut(x) 42 | for x in cols 43 | ] 44 | self.inputs = [ 45 | x 46 | if x.__class__.__name__ == 'DigitalInOut' 47 | else digitalio.DigitalInOut(x) 48 | for x in rows 49 | ] 50 | self.translate_coords = True 51 | elif self.diode_orientation == DiodeOrientation.ROWS: 52 | self.outputs = [ 53 | x 54 | if x.__class__.__name__ == 'DigitalInOut' 55 | else digitalio.DigitalInOut(x) 56 | for x in rows 57 | ] 58 | self.inputs = [ 59 | x 60 | if x.__class__.__name__ == 'DigitalInOut' 61 | else digitalio.DigitalInOut(x) 62 | for x in cols 63 | ] 64 | self.translate_coords = False 65 | else: 66 | raise ValueError(f'Invalid DiodeOrientation: {self.diode_orienttaion}') 67 | 68 | for pin in self.outputs: 69 | pin.switch_to_output() 70 | 71 | for pin in self.inputs: 72 | pin.switch_to_input(pull=digitalio.Pull.DOWN) 73 | 74 | self.rollover_cols_every_rows = rollover_cols_every_rows 75 | if self.rollover_cols_every_rows is None: 76 | self.rollover_cols_every_rows = self.len_rows 77 | 78 | self._key_count = self.len_cols * self.len_rows 79 | self.state = bytearray(self.key_count) 80 | 81 | @property 82 | def key_count(self): 83 | return self._key_count 84 | 85 | def scan_for_changes(self): 86 | ''' 87 | Poll the matrix for changes and return either None (if nothing updated) 88 | or a bytearray (reused in later runs so copy this if you need the raw 89 | array itself for some crazy reason) consisting of (row, col, pressed) 90 | which are (int, int, bool) 91 | ''' 92 | ba_idx = 0 93 | any_changed = False 94 | 95 | for oidx, opin in enumerate(self.outputs): 96 | opin.value = True 97 | 98 | for iidx, ipin in enumerate(self.inputs): 99 | # cast to int to avoid 100 | # 101 | # >>> xyz = bytearray(3) 102 | # >>> xyz[2] = True 103 | # Traceback (most recent call last): 104 | # File "", line 1, in 105 | # OverflowError: value would overflow a 1 byte buffer 106 | # 107 | # I haven't dived too far into what causes this, but it's 108 | # almost certainly because bool types in Python aren't just 109 | # aliases to int values, but are proper pseudo-types 110 | new_val = int(ipin.value) 111 | old_val = self.state[ba_idx] 112 | 113 | if old_val != new_val: 114 | if self.translate_coords: 115 | new_oidx = oidx + self.len_cols * ( 116 | iidx // self.rollover_cols_every_rows 117 | ) 118 | new_iidx = iidx - self.rollover_cols_every_rows * ( 119 | iidx // self.rollover_cols_every_rows 120 | ) 121 | 122 | row = new_iidx 123 | col = new_oidx 124 | else: 125 | row = oidx 126 | col = iidx 127 | 128 | pressed = new_val 129 | self.state[ba_idx] = new_val 130 | 131 | any_changed = True 132 | break 133 | 134 | ba_idx += 1 135 | 136 | opin.value = False 137 | if any_changed: 138 | break 139 | 140 | if any_changed: 141 | key_number = self.len_cols * row + col + self.offset 142 | return KeyEvent(key_number, pressed) 143 | -------------------------------------------------------------------------------- /code/lib/neopixel.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016 Damien P. George 2 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries 3 | # SPDX-FileCopyrightText: 2019 Carter Nelson 4 | # SPDX-FileCopyrightText: 2019 Roy Hooper 5 | # 6 | # SPDX-License-Identifier: MIT 7 | 8 | """ 9 | `neopixel` - NeoPixel strip driver 10 | ==================================================== 11 | 12 | * Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper 13 | """ 14 | 15 | # pylint: disable=ungrouped-imports 16 | import sys 17 | import board 18 | import digitalio 19 | from neopixel_write import neopixel_write 20 | 21 | try: 22 | import adafruit_pixelbuf 23 | except ImportError: 24 | try: 25 | import _pixelbuf as adafruit_pixelbuf 26 | except ImportError: 27 | import adafruit_pypixelbuf as adafruit_pixelbuf 28 | 29 | 30 | try: 31 | # Used only for typing 32 | from typing import Optional, Type 33 | from types import TracebackType 34 | import microcontroller 35 | except ImportError: 36 | pass 37 | 38 | 39 | __version__ = "0.0.0-auto.0" 40 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git" 41 | 42 | 43 | # Pixel color order constants 44 | RGB = "RGB" 45 | """Red Green Blue""" 46 | GRB = "GRB" 47 | """Green Red Blue""" 48 | RGBW = "RGBW" 49 | """Red Green Blue White""" 50 | GRBW = "GRBW" 51 | """Green Red Blue White""" 52 | 53 | 54 | class NeoPixel(adafruit_pixelbuf.PixelBuf): 55 | """ 56 | A sequence of neopixels. 57 | 58 | :param ~microcontroller.Pin pin: The pin to output neopixel data on. 59 | :param int n: The number of neopixels in the chain 60 | :param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels. 61 | :param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full 62 | brightness 63 | :param bool auto_write: True if the neopixels should immediately change when set. If False, 64 | `show` must be called explicitly. 65 | :param str pixel_order: Set the pixel color channel order. GRBW is set by default. 66 | 67 | Example for Circuit Playground Express: 68 | 69 | .. code-block:: python 70 | 71 | import neopixel 72 | from board import * 73 | 74 | RED = 0x100000 # (0x10, 0, 0) also works 75 | 76 | pixels = neopixel.NeoPixel(NEOPIXEL, 10) 77 | for i in range(len(pixels)): 78 | pixels[i] = RED 79 | 80 | Example for Circuit Playground Express setting every other pixel red using a slice: 81 | 82 | .. code-block:: python 83 | 84 | import neopixel 85 | from board import * 86 | import time 87 | 88 | RED = 0x100000 # (0x10, 0, 0) also works 89 | 90 | # Using ``with`` ensures pixels are cleared after we're done. 91 | with neopixel.NeoPixel(NEOPIXEL, 10) as pixels: 92 | pixels[::2] = [RED] * (len(pixels) // 2) 93 | time.sleep(2) 94 | 95 | .. py:method:: NeoPixel.show() 96 | 97 | Shows the new colors on the pixels themselves if they haven't already 98 | been autowritten. 99 | 100 | The colors may or may not be showing after this function returns because 101 | it may be done asynchronously. 102 | 103 | .. py:method:: NeoPixel.fill(color) 104 | 105 | Colors all pixels the given ***color***. 106 | 107 | .. py:attribute:: brightness 108 | 109 | Overall brightness of the pixel (0 to 1.0) 110 | 111 | """ 112 | 113 | def __init__( 114 | self, 115 | pin: microcontroller.Pin, 116 | n: int, 117 | *, 118 | bpp: int = 3, 119 | brightness: float = 1.0, 120 | auto_write: bool = True, 121 | pixel_order: str = None 122 | ): 123 | if not pixel_order: 124 | pixel_order = GRB if bpp == 3 else GRBW 125 | elif isinstance(pixel_order, tuple): 126 | order_list = [RGBW[order] for order in pixel_order] 127 | pixel_order = "".join(order_list) 128 | 129 | self._power = None 130 | if ( 131 | sys.implementation.version[0] >= 7 132 | and getattr(board, "NEOPIXEL", None) == pin 133 | ): 134 | power = getattr(board, "NEOPIXEL_POWER_INVERTED", None) 135 | polarity = power is None 136 | if not power: 137 | power = getattr(board, "NEOPIXEL_POWER", None) 138 | if power: 139 | try: 140 | self._power = digitalio.DigitalInOut(power) 141 | self._power.switch_to_output(value=polarity) 142 | except ValueError: 143 | pass 144 | 145 | super().__init__( 146 | n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write 147 | ) 148 | 149 | self.pin = digitalio.DigitalInOut(pin) 150 | self.pin.direction = digitalio.Direction.OUTPUT 151 | 152 | def deinit(self) -> None: 153 | """Blank out the NeoPixels and release the pin.""" 154 | self.fill(0) 155 | self.show() 156 | self.pin.deinit() 157 | if self._power: 158 | self._power.deinit() 159 | 160 | def __enter__(self): 161 | return self 162 | 163 | def __exit__( 164 | self, 165 | exception_type: Optional[Type[BaseException]], 166 | exception_value: Optional[BaseException], 167 | traceback: Optional[TracebackType], 168 | ): 169 | self.deinit() 170 | 171 | def __repr__(self): 172 | return "[" + ", ".join([str(x) for x in self]) + "]" 173 | 174 | @property 175 | def n(self) -> int: 176 | """ 177 | The number of neopixels in the chain (read-only) 178 | """ 179 | return len(self) 180 | 181 | def write(self) -> None: 182 | """.. deprecated: 1.0.0 183 | 184 | Use ``show`` instead. It matches Micro:Bit and Arduino APIs.""" 185 | self.show() 186 | 187 | def _transmit(self, buffer: bytearray) -> None: 188 | neopixel_write(self.pin, buffer) 189 | -------------------------------------------------------------------------------- /code/kmk/extensions/peg_rgb_matrix.py: -------------------------------------------------------------------------------- 1 | import neopixel 2 | 3 | from storage import getmount 4 | 5 | from kmk.extensions import Extension 6 | from kmk.handlers.stock import passthrough as handler_passthrough 7 | from kmk.keys import make_key 8 | 9 | 10 | class Color: 11 | OFF = [0, 0, 0] 12 | BLACK = OFF 13 | WHITE = [249, 249, 249] 14 | RED = [255, 0, 0] 15 | AZURE = [153, 245, 255] 16 | BLUE = [0, 0, 255] 17 | CYAN = [0, 255, 255] 18 | GREEN = [0, 255, 0] 19 | YELLOW = [255, 247, 0] 20 | MAGENTA = [255, 0, 255] 21 | ORANGE = [255, 77, 0] 22 | PURPLE = [255, 0, 242] 23 | TEAL = [0, 128, 128] 24 | PINK = [255, 0, 255] 25 | 26 | 27 | class Rgb_matrix_data: 28 | def __init__(self, keys=[], underglow=[]): 29 | if len(keys) == 0: 30 | print('No colors passed for your keys') 31 | return 32 | if len(underglow) == 0: 33 | print('No colors passed for your underglow') 34 | return 35 | self.data = keys + underglow 36 | 37 | @staticmethod 38 | def generate_led_map( 39 | number_of_keys, number_of_underglow, key_color, underglow_color 40 | ): 41 | keys = [key_color] * number_of_keys 42 | underglow = [underglow_color] * number_of_underglow 43 | print(f'Rgb_matrix_data(keys={keys},\nunderglow={underglow})') 44 | 45 | 46 | class Rgb_matrix(Extension): 47 | def __init__( 48 | self, 49 | rgb_order=(1, 0, 2), # GRB WS2812 50 | disable_auto_write=False, 51 | ledDisplay=[], 52 | split=False, 53 | rightSide=False, 54 | ): 55 | name = str(getmount('/').label) 56 | self.rgb_order = rgb_order 57 | self.disable_auto_write = disable_auto_write 58 | self.split = split 59 | self.rightSide = rightSide 60 | self.brightness_step = 0.1 61 | self.brightness = 0 62 | 63 | if name.endswith('L'): 64 | self.rightSide = False 65 | elif name.endswith('R'): 66 | self.rightSide = True 67 | if type(ledDisplay) is Rgb_matrix_data: 68 | self.ledDisplay = ledDisplay.data 69 | else: 70 | self.ledDisplay = ledDisplay 71 | 72 | make_key( 73 | names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough 74 | ) 75 | make_key( 76 | names=('RGB_BRI',), on_press=self._rgb_bri, on_release=handler_passthrough 77 | ) 78 | make_key( 79 | names=('RGB_BRD',), on_press=self._rgb_brd, on_release=handler_passthrough 80 | ) 81 | 82 | def _rgb_tog(self, *args, **kwargs): 83 | if self.enable: 84 | self.off() 85 | else: 86 | self.on() 87 | self.enable = not self.enable 88 | 89 | def _rgb_bri(self, *args, **kwargs): 90 | self.increase_brightness() 91 | 92 | def _rgb_brd(self, *args, **kwargs): 93 | self.decrease_brightness() 94 | 95 | def on(self): 96 | if self.neopixel: 97 | self.setBasedOffDisplay() 98 | self.neopixel.show() 99 | 100 | def off(self): 101 | if self.neopixel: 102 | self.set_rgb_fill((0, 0, 0)) 103 | 104 | def set_rgb_fill(self, rgb): 105 | if self.neopixel: 106 | self.neopixel.fill(rgb) 107 | if self.disable_auto_write: 108 | self.neopixel.show() 109 | 110 | def set_brightness(self, brightness=None): 111 | if brightness is None: 112 | brightness = self.brightness 113 | 114 | if self.neopixel: 115 | self.neopixel.brightness = brightness 116 | if self.disable_auto_write: 117 | self.neopixel.show() 118 | 119 | def increase_brightness(self, step=None): 120 | if step is None: 121 | step = self.brightness_step 122 | 123 | self.brightness = ( 124 | self.brightness + step if self.brightness + step <= 1.0 else 1.0 125 | ) 126 | 127 | self.set_brightness(self.brightness) 128 | 129 | def decrease_brightness(self, step=None): 130 | if step is None: 131 | step = self.brightness_step 132 | 133 | self.brightness = ( 134 | self.brightness - step if self.brightness - step >= 0.0 else 0.0 135 | ) 136 | self.set_brightness(self.brightness) 137 | 138 | def setBasedOffDisplay(self): 139 | if self.split: 140 | for i, val in enumerate(self.ledDisplay): 141 | if self.rightSide: 142 | if self.keyPos[i] >= (self.num_pixels / 2): 143 | self.neopixel[int(self.keyPos[i] - (self.num_pixels / 2))] = ( 144 | val[0], 145 | val[1], 146 | val[2], 147 | ) 148 | else: 149 | if self.keyPos[i] <= (self.num_pixels / 2): 150 | self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2]) 151 | else: 152 | for i, val in enumerate(self.ledDisplay): 153 | self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2]) 154 | 155 | def on_runtime_enable(self, sandbox): 156 | return 157 | 158 | def on_runtime_disable(self, sandbox): 159 | return 160 | 161 | def during_bootup(self, board): 162 | self.neopixel = neopixel.NeoPixel( 163 | board.rgb_pixel_pin, 164 | board.num_pixels, 165 | brightness=board.brightness_limit, 166 | pixel_order=self.rgb_order, 167 | auto_write=not self.disable_auto_write, 168 | ) 169 | self.num_pixels = board.num_pixels 170 | self.keyPos = board.led_key_pos 171 | self.brightness = board.brightness_limit 172 | self.on() 173 | return 174 | 175 | def before_matrix_scan(self, sandbox): 176 | return 177 | 178 | def after_matrix_scan(self, sandbox): 179 | return 180 | 181 | def before_hid_send(self, sandbox): 182 | return 183 | 184 | def after_hid_send(self, sandbox): 185 | return 186 | 187 | def on_powersave_enable(self, sandbox): 188 | if self.neopixel: 189 | self.neopixel.brightness = ( 190 | self.neopixel.brightness / 2 191 | if self.neopixel.brightness / 2 > 0 192 | else 0.1 193 | ) 194 | if self.disable_auto_write: 195 | self.neopixel.show() 196 | 197 | def on_powersave_disable(self, sandbox): 198 | if self.neopixel: 199 | self.neopixel.brightness = self.brightness 200 | if self.disable_auto_write: 201 | self.neopixel.show() 202 | -------------------------------------------------------------------------------- /code/kmk/modules/adns9800.py: -------------------------------------------------------------------------------- 1 | import busio 2 | import digitalio 3 | import microcontroller 4 | 5 | import time 6 | 7 | from kmk.modules import Module 8 | from kmk.modules.adns9800_firmware import firmware 9 | from kmk.modules.mouse_keys import PointingDevice 10 | 11 | 12 | class REG: 13 | Product_ID = 0x0 14 | Revision_ID = 0x1 15 | MOTION = 0x2 16 | DELTA_X_L = 0x3 17 | DELTA_X_H = 0x4 18 | DELTA_Y_L = 0x5 19 | DELTA_Y_H = 0x6 20 | SQUAL = 0x7 21 | PIXEL_SUM = 0x8 22 | Maximum_Pixel = 0x9 23 | Minimum_Pixel = 0xA 24 | Shutter_Lower = 0xB 25 | Shutter_Upper = 0xC 26 | Frame_Period_Lower = 0xD 27 | Frame_Period_Upper = 0xE 28 | Configuration_I = 0xF 29 | Configuration_II = 0x10 30 | Frame_Capture = 0x12 31 | SROM_Enable = 0x13 32 | Run_Downshift = 0x14 33 | Rest1_Rate = 0x15 34 | Rest1_Downshift = 0x16 35 | Rest2_Rate = 0x17 36 | Rest2_Downshift = 0x18 37 | Rest3_Rate = 0x19 38 | Frame_Period_Max_Bound_Lower = 0x1A 39 | Frame_Period_Max_Bound_Upper = 0x1B 40 | Frame_Period_Min_Bound_Lower = 0x1C 41 | Frame_Period_Min_Bound_Upper = 0x1D 42 | Shutter_Max_Bound_Lower = 0x1E 43 | Shutter_Max_Bound_Upper = 0x1F 44 | LASER_CTRL0 = 0x20 45 | Observation = 0x24 46 | Data_Out_Lower = 0x25 47 | Data_Out_Upper = 0x26 48 | SROM_ID = 0x2A 49 | Lift_Detection_Thr = 0x2E 50 | Configuration_V = 0x2F 51 | Configuration_IV = 0x39 52 | Power_Up_Reset = 0x3A 53 | Shutdown = 0x3B 54 | Inverse_Product_ID = 0x3F 55 | Snap_Angle = 0x42 56 | Motion_Burst = 0x50 57 | SROM_Load_Burst = 0x62 58 | Pixel_Burst = 0x64 59 | 60 | 61 | class ADNS9800(Module): 62 | tswr = tsww = 120 63 | tsrw = tsrr = 20 64 | tsrad = 100 65 | tbexit = 1 66 | baud = 2000000 67 | cpol = 1 68 | cpha = 1 69 | DIR_WRITE = 0x80 70 | DIR_READ = 0x7F 71 | 72 | def __init__(self, cs, sclk, miso, mosi, invert_x=False, invert_y=False): 73 | self.pointing_device = PointingDevice() 74 | self.cs = digitalio.DigitalInOut(cs) 75 | self.cs.direction = digitalio.Direction.OUTPUT 76 | self.spi = busio.SPI(clock=sclk, MOSI=mosi, MISO=miso) 77 | self.invert_x = invert_x 78 | self.invert_y = invert_y 79 | 80 | def adns_start(self): 81 | self.cs.value = False 82 | 83 | def adns_stop(self): 84 | self.cs.value = True 85 | 86 | def adns_write(self, reg, data): 87 | while not self.spi.try_lock(): 88 | pass 89 | try: 90 | self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) 91 | self.adns_start() 92 | self.spi.write(bytes([reg | self.DIR_WRITE, data])) 93 | finally: 94 | self.spi.unlock() 95 | self.adns_stop() 96 | 97 | def adns_read(self, reg): 98 | result = bytearray(1) 99 | while not self.spi.try_lock(): 100 | pass 101 | try: 102 | self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) 103 | self.adns_start() 104 | self.spi.write(bytes([reg & self.DIR_READ])) 105 | microcontroller.delay_us(self.tsrad) 106 | self.spi.readinto(result) 107 | finally: 108 | self.spi.unlock() 109 | self.adns_stop() 110 | 111 | return result[0] 112 | 113 | def adns_upload_srom(self): 114 | while not self.spi.try_lock(): 115 | pass 116 | try: 117 | self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) 118 | self.adns_start() 119 | self.spi.write(bytes([REG.SROM_Load_Burst | self.DIR_WRITE])) 120 | for b in firmware: 121 | self.spi.write(bytes([b])) 122 | finally: 123 | self.spi.unlock() 124 | self.adns_stop() 125 | 126 | def delta_to_int(self, high, low): 127 | comp = (high << 8) | low 128 | if comp & 0x8000: 129 | return (-1) * (0xFFFF + 1 - comp) 130 | return comp 131 | 132 | def adns_read_motion(self): 133 | result = bytearray(14) 134 | while not self.spi.try_lock(): 135 | pass 136 | try: 137 | self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha) 138 | self.adns_start() 139 | self.spi.write(bytes([REG.Motion_Burst & self.DIR_READ])) 140 | microcontroller.delay_us(self.tsrad) 141 | self.spi.readinto(result) 142 | finally: 143 | self.spi.unlock() 144 | self.adns_stop() 145 | microcontroller.delay_us(self.tbexit) 146 | self.adns_write(REG.MOTION, 0x0) 147 | return result 148 | 149 | def during_bootup(self, keyboard): 150 | 151 | self.adns_write(REG.Power_Up_Reset, 0x5A) 152 | time.sleep(0.1) 153 | self.adns_read(REG.MOTION) 154 | microcontroller.delay_us(self.tsrr) 155 | self.adns_read(REG.DELTA_X_L) 156 | microcontroller.delay_us(self.tsrr) 157 | self.adns_read(REG.DELTA_X_H) 158 | microcontroller.delay_us(self.tsrr) 159 | self.adns_read(REG.DELTA_Y_L) 160 | microcontroller.delay_us(self.tsrr) 161 | self.adns_read(REG.DELTA_Y_H) 162 | microcontroller.delay_us(self.tsrw) 163 | 164 | self.adns_write(REG.Configuration_IV, 0x2) 165 | microcontroller.delay_us(self.tsww) 166 | self.adns_write(REG.SROM_Enable, 0x1D) 167 | microcontroller.delay_us(1000) 168 | self.adns_write(REG.SROM_Enable, 0x18) 169 | microcontroller.delay_us(self.tsww) 170 | 171 | self.adns_upload_srom() 172 | microcontroller.delay_us(2000) 173 | 174 | laser_ctrl0 = self.adns_read(REG.LASER_CTRL0) 175 | microcontroller.delay_us(self.tsrw) 176 | self.adns_write(REG.LASER_CTRL0, laser_ctrl0 & 0xF0) 177 | microcontroller.delay_us(self.tsww) 178 | self.adns_write(REG.Configuration_I, 0x10) 179 | microcontroller.delay_us(self.tsww) 180 | 181 | if keyboard.debug_enabled: 182 | print('ADNS: Product ID ', hex(self.adns_read(REG.Product_ID))) 183 | microcontroller.delay_us(self.tsrr) 184 | print('ADNS: Revision ID ', hex(self.adns_read(REG.Revision_ID))) 185 | microcontroller.delay_us(self.tsrr) 186 | print('ADNS: SROM ID ', hex(self.adns_read(REG.SROM_ID))) 187 | microcontroller.delay_us(self.tsrr) 188 | if self.adns_read(REG.Observation) & 0x20: 189 | print('ADNS: Sensor is running SROM') 190 | else: 191 | print('ADNS: Error! Sensor is not runnin SROM!') 192 | 193 | return 194 | 195 | def before_matrix_scan(self, keyboard): 196 | motion = self.adns_read_motion() 197 | if motion[0] & 0x80: 198 | delta_x = self.delta_to_int(motion[3], motion[2]) 199 | delta_y = self.delta_to_int(motion[5], motion[4]) 200 | 201 | if self.invert_x: 202 | delta_x *= -1 203 | if self.invert_y: 204 | delta_y *= -1 205 | 206 | if delta_x < 0: 207 | self.pointing_device.report_x[0] = (delta_x & 0xFF) | 0x80 208 | else: 209 | self.pointing_device.report_x[0] = delta_x & 0xFF 210 | 211 | if delta_y < 0: 212 | self.pointing_device.report_y[0] = (delta_y & 0xFF) | 0x80 213 | else: 214 | self.pointing_device.report_y[0] = delta_y & 0xFF 215 | 216 | if keyboard.debug_enabled: 217 | print('Delta: ', delta_x, ' ', delta_y) 218 | self.pointing_device.hid_pending = True 219 | 220 | if self.pointing_device.hid_pending: 221 | keyboard._hid_helper.hid_send(self.pointing_device._evt) 222 | self.pointing_device.hid_pending = False 223 | self.pointing_device.report_x[0] = 0 224 | self.pointing_device.report_y[0] = 0 225 | 226 | return 227 | 228 | def after_matrix_scan(self, keyboard): 229 | return 230 | 231 | def before_hid_send(self, keyboard): 232 | return 233 | 234 | def after_hid_send(self, keyboard): 235 | return 236 | 237 | def on_powersave_enable(self, keyboard): 238 | return 239 | 240 | def on_powersave_disable(self, keyboard): 241 | return 242 | -------------------------------------------------------------------------------- /code/kmk/extensions/led.py: -------------------------------------------------------------------------------- 1 | import pwmio 2 | from math import e, exp, pi, sin 3 | 4 | from kmk.extensions import Extension, InvalidExtensionEnvironment 5 | from kmk.keys import make_argumented_key, make_key 6 | from kmk.utils import clamp 7 | 8 | 9 | class LEDKeyMeta: 10 | def __init__(self, *leds): 11 | self.leds = leds 12 | self.brightness = None 13 | 14 | 15 | class AnimationModes: 16 | OFF = 0 17 | STATIC = 1 18 | STATIC_STANDBY = 2 19 | BREATHING = 3 20 | USER = 4 21 | 22 | 23 | class LED(Extension): 24 | def __init__( 25 | self, 26 | led_pin, 27 | brightness=50, 28 | brightness_step=5, 29 | brightness_limit=100, 30 | breathe_center=1.5, 31 | animation_mode=AnimationModes.STATIC, 32 | animation_speed=1, 33 | user_animation=None, 34 | val=100, 35 | ): 36 | try: 37 | pins_iter = iter(led_pin) 38 | except TypeError: 39 | pins_iter = [led_pin] 40 | 41 | try: 42 | self._leds = [pwmio.PWMOut(pin) for pin in pins_iter] 43 | except Exception as e: 44 | print(e) 45 | raise InvalidExtensionEnvironment( 46 | 'Unable to create pwmio.PWMOut() instance with provided led_pin' 47 | ) 48 | 49 | self._brightness = brightness 50 | self._pos = 0 51 | self._effect_init = False 52 | self._enabled = True 53 | 54 | self.brightness_step = brightness_step 55 | self.brightness_limit = brightness_limit 56 | self.animation_mode = animation_mode 57 | self.animation_speed = animation_speed 58 | self.breathe_center = breathe_center 59 | self.val = val 60 | 61 | if user_animation is not None: 62 | self.user_animation = user_animation 63 | 64 | make_argumented_key( 65 | names=('LED_TOG',), 66 | validator=self._led_key_validator, 67 | on_press=self._key_led_tog, 68 | ) 69 | make_argumented_key( 70 | names=('LED_INC',), 71 | validator=self._led_key_validator, 72 | on_press=self._key_led_inc, 73 | ) 74 | make_argumented_key( 75 | names=('LED_DEC',), 76 | validator=self._led_key_validator, 77 | on_press=self._key_led_dec, 78 | ) 79 | make_argumented_key( 80 | names=('LED_SET',), 81 | validator=self._led_set_key_validator, 82 | on_press=self._key_led_set, 83 | ) 84 | make_key(names=('LED_ANI',), on_press=self._key_led_ani) 85 | make_key(names=('LED_AND',), on_press=self._key_led_and) 86 | make_key( 87 | names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static 88 | ) 89 | make_key( 90 | names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe 91 | ) 92 | 93 | def __repr__(self): 94 | return f'LED({self._to_dict()})' 95 | 96 | def _to_dict(self): 97 | return { 98 | '_brightness': self._brightness, 99 | '_pos': self._pos, 100 | 'brightness_step': self.brightness_step, 101 | 'brightness_limit': self.brightness_limit, 102 | 'animation_mode': self.animation_mode, 103 | 'animation_speed': self.animation_speed, 104 | 'breathe_center': self.breathe_center, 105 | 'val': self.val, 106 | } 107 | 108 | def on_runtime_enable(self, sandbox): 109 | return 110 | 111 | def on_runtime_disable(self, sandbox): 112 | return 113 | 114 | def during_bootup(self, sandbox): 115 | return 116 | 117 | def before_matrix_scan(self, sandbox): 118 | return 119 | 120 | def after_matrix_scan(self, sandbox): 121 | return 122 | 123 | def before_hid_send(self, sandbox): 124 | return 125 | 126 | def after_hid_send(self, sandbox): 127 | if self._enabled and self.animation_mode: 128 | self.animate() 129 | return 130 | 131 | def on_powersave_enable(self, sandbox): 132 | return 133 | 134 | def on_powersave_disable(self, sandbox): 135 | return 136 | 137 | def _init_effect(self): 138 | self._pos = 0 139 | self._effect_init = False 140 | return self 141 | 142 | def set_brightness(self, percent, leds=None): 143 | leds = leds or range(0, len(self._leds)) 144 | for i in leds: 145 | self._leds[i].duty_cycle = int(percent / 100 * 65535) 146 | 147 | def step_brightness(self, step, leds=None): 148 | leds = leds or range(0, len(self._leds)) 149 | for i in leds: 150 | brightness = int(self._leds[i].duty_cycle / 65535 * 100) + step 151 | self.set_brightness(clamp(brightness), [i]) 152 | 153 | def increase_brightness(self, step=None, leds=None): 154 | if step is None: 155 | step = self.brightness_step 156 | self.step_brightness(step, leds) 157 | 158 | def decrease_brightness(self, step=None, leds=None): 159 | if step is None: 160 | step = self.brightness_step 161 | self.step_brightness(-step, leds) 162 | 163 | def off(self): 164 | self.set_brightness(0) 165 | 166 | def increase_ani(self): 167 | ''' 168 | Increases animation speed by 1 amount stopping at 10 169 | :param step: 170 | ''' 171 | if (self.animation_speed + 1) >= 10: 172 | self.animation_speed = 10 173 | else: 174 | self.val += 1 175 | 176 | def decrease_ani(self): 177 | ''' 178 | Decreases animation speed by 1 amount stopping at 0 179 | :param step: 180 | ''' 181 | if (self.val - 1) <= 0: 182 | self.val = 0 183 | else: 184 | self.val -= 1 185 | 186 | def effect_breathing(self): 187 | # http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/ 188 | # https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806 189 | sined = sin((self._pos / 255.0) * pi) 190 | multip_1 = exp(sined) - self.breathe_center / e 191 | multip_2 = self.brightness_limit / (e - 1 / e) 192 | 193 | self._brightness = int(multip_1 * multip_2) 194 | self._pos = (self._pos + self.animation_speed) % 256 195 | self.set_brightness(self._brightness) 196 | 197 | def effect_static(self): 198 | self.set_brightness(self._brightness) 199 | # Set animation mode to none to prevent cycles from being wasted 200 | self.animation_mode = None 201 | 202 | def animate(self): 203 | ''' 204 | Activates a "step" in the animation based on the active mode 205 | :return: Returns the new state in animation 206 | ''' 207 | if self._effect_init: 208 | self._init_effect() 209 | if self._enabled: 210 | if self.animation_mode == AnimationModes.BREATHING: 211 | return self.effect_breathing() 212 | elif self.animation_mode == AnimationModes.STATIC: 213 | return self.effect_static() 214 | elif self.animation_mode == AnimationModes.USER: 215 | return self.user_animation(self) 216 | else: 217 | self.off() 218 | 219 | def _led_key_validator(self, *leds): 220 | if leds: 221 | for led in leds: 222 | assert self._leds[led] 223 | return LEDKeyMeta(*leds) 224 | 225 | def _led_set_key_validator(self, brightness, *leds): 226 | meta = self._led_key_validator(*leds) 227 | meta.brightness = brightness 228 | return meta 229 | 230 | def _key_led_tog(self, *args, **kwargs): 231 | if self.animation_mode == AnimationModes.STATIC_STANDBY: 232 | self.animation_mode = AnimationModes.STATIC 233 | 234 | self._enabled = not self._enabled 235 | 236 | def _key_led_inc(self, key, *args, **kwargs): 237 | self.increase_brightness(leds=key.meta.leds) 238 | 239 | def _key_led_dec(self, key, *args, **kwargs): 240 | self.decrease_brightness(leds=key.meta.leds) 241 | 242 | def _key_led_set(self, key, *args, **kwargs): 243 | self.set_brightness(percent=key.meta.brightness, leds=key.meta.leds) 244 | 245 | def _key_led_ani(self, *args, **kwargs): 246 | self.increase_ani() 247 | 248 | def _key_led_and(self, *args, **kwargs): 249 | self.decrease_ani() 250 | 251 | def _key_led_mode_static(self, *args, **kwargs): 252 | self._effect_init = True 253 | self.animation_mode = AnimationModes.STATIC 254 | 255 | def _key_led_mode_breathe(self, *args, **kwargs): 256 | self._effect_init = True 257 | self.animation_mode = AnimationModes.BREATHING 258 | -------------------------------------------------------------------------------- /code/kmk/modules/string_substitution.py: -------------------------------------------------------------------------------- 1 | try: 2 | from typing import Optional 3 | except ImportError: 4 | # we're not in a dev environment, so we don't need to worry about typing 5 | pass 6 | from micropython import const 7 | 8 | from kmk.keys import KC, Key, ModifierKey 9 | from kmk.modules import Module 10 | 11 | 12 | class State: 13 | LISTENING = const(0) 14 | DELETING = const(1) 15 | SENDING = const(2) 16 | IGNORING = const(3) 17 | 18 | 19 | class Character: 20 | '''Helper class for making a left-shifted key identical to a right-shifted key''' 21 | 22 | is_shifted: bool = False 23 | 24 | def __init__(self, key_code: Key, is_shifted: bool) -> None: 25 | self.is_shifted = is_shifted 26 | self.key_code = KC.LSHIFT(key_code) if is_shifted else key_code 27 | 28 | def __eq__(self, other: any) -> bool: # type: ignore 29 | try: 30 | return ( 31 | self.key_code.code == other.key_code.code 32 | and self.is_shifted == other.is_shifted 33 | ) 34 | except AttributeError: 35 | return False 36 | 37 | 38 | class Phrase: 39 | '''Manages a collection of characters and keeps an index of them so that potential matches can be tracked''' 40 | 41 | def __init__(self, string: str) -> None: 42 | self._characters: list[Character] = [] 43 | self._index: int = 0 44 | for char in string: 45 | try: 46 | key_code = KC[char] 47 | shifted = char.isupper() or key_code.has_modifiers == {2} 48 | self._characters.append(Character(key_code, shifted)) 49 | except ValueError: 50 | raise ValueError(f'Invalid character in dictionary: {char}') 51 | 52 | def next_character(self) -> None: 53 | '''Increment the current index for this phrase''' 54 | if not self.index_at_end(): 55 | self._index += 1 56 | 57 | def get_character_at_index(self, index: int) -> Character: 58 | '''Returns the character at the given index''' 59 | return self._characters[index] 60 | 61 | def get_character_at_current_index(self) -> Character: 62 | '''Returns the character at the current index for this phrase''' 63 | return self._characters[self._index] 64 | 65 | def reset_index(self) -> None: 66 | '''Reset the index to the start of the phrase''' 67 | self._index = 0 68 | 69 | def index_at_end(self) -> bool: 70 | '''Returns True if the index is at the end of the phrase''' 71 | return self._index == len(self._characters) 72 | 73 | def character_is_at_current_index(self, character) -> bool: 74 | '''Returns True if the given character is the next character in the phrase''' 75 | return self.get_character_at_current_index() == character 76 | 77 | 78 | class Rule: 79 | '''Represents the relationship between a phrase to be substituted and its substitution''' 80 | 81 | def __init__(self, to_substitute: Phrase, substitution: Phrase) -> None: 82 | self.to_substitute: Phrase = to_substitute 83 | self.substitution: Phrase = substitution 84 | 85 | def restart(self) -> None: 86 | '''Resets this rule's to_substitute and substitution phrases''' 87 | self.to_substitute.reset_index() 88 | self.substitution.reset_index() 89 | 90 | 91 | class StringSubstitution(Module): 92 | _shifted: bool = False 93 | _rules: list = [] 94 | _state: State = State.LISTENING 95 | _matched_rule: Optional[Phrase] = None 96 | _active_modifiers: list[ModifierKey] = [] 97 | 98 | def __init__( 99 | self, 100 | dictionary: dict, 101 | ): 102 | for key, value in dictionary.items(): 103 | self._rules.append(Rule(Phrase(key), Phrase(value))) 104 | 105 | def process_key(self, keyboard, key, is_pressed, int_coord): 106 | if key is KC.LSFT or key is KC.RSFT: 107 | if is_pressed: 108 | self._shifted = True 109 | else: 110 | self._shifted = False 111 | 112 | # control ignoring state if the key is a non-shift modifier 113 | elif type(key) is ModifierKey: 114 | if is_pressed and key not in self._active_modifiers: 115 | self._active_modifiers.append(key) 116 | self._state = State.IGNORING 117 | elif key in self._active_modifiers: 118 | self._active_modifiers.remove(key) 119 | if not self._active_modifiers: 120 | self._state = State.LISTENING 121 | # reset rules because pressing a modifier combination 122 | # should interrupt any current matches 123 | for rule in self._rules: 124 | rule.restart() 125 | 126 | if not self._state == State.LISTENING: 127 | return key 128 | 129 | if is_pressed: 130 | character = Character(key, self._shifted) 131 | 132 | # run through the dictionary to check for a possible match on each new keypress 133 | for rule in self._rules: 134 | if rule.to_substitute.character_is_at_current_index(character): 135 | rule.to_substitute.next_character() 136 | else: 137 | rule.restart() 138 | # if character is not a match at the current index, 139 | # it could still be a match at the start of the sequence 140 | # so redo the check after resetting the sequence 141 | if rule.to_substitute.character_is_at_current_index(character): 142 | rule.to_substitute.next_character() 143 | # we've matched all of the characters in a phrase to be substituted 144 | if rule.to_substitute.index_at_end(): 145 | rule.restart() 146 | # set the phrase indexes to where they differ 147 | # so that only the characters that differ are replaced 148 | for character in rule.to_substitute._characters: 149 | if ( 150 | character 151 | == rule.substitution.get_character_at_current_index() 152 | ): 153 | rule.to_substitute.next_character() 154 | rule.substitution.next_character() 155 | else: 156 | break 157 | if rule.to_substitute.index_at_end(): 158 | break 159 | self._matched_rule = rule 160 | self._state = State.DELETING 161 | # if we have a match there's no reason to continue the full key processing, so return out 162 | return 163 | return key 164 | 165 | def during_bootup(self, keyboard): 166 | return 167 | 168 | def before_matrix_scan(self, keyboard): 169 | return 170 | 171 | def before_hid_send(self, keyboard): 172 | 173 | if self._state == State.LISTENING: 174 | return 175 | 176 | if self._state == State.DELETING: 177 | # force-release modifiers so sending the replacement text doesn't interact with them 178 | # it should not be possible for any modifiers other than shift to be held upon rule activation 179 | # as a modified key won't send a keycode that is matched against the user's dictionary, 180 | # but, just in case, we'll release those too 181 | modifiers_to_release = [ 182 | KC.LSFT, 183 | KC.RSFT, 184 | KC.LCTL, 185 | KC.LGUI, 186 | KC.LALT, 187 | KC.RCTL, 188 | KC.RGUI, 189 | KC.RALT, 190 | ] 191 | for modifier in modifiers_to_release: 192 | keyboard.remove_key(modifier) 193 | 194 | # send backspace taps equivalent to the length of the phrase to be substituted 195 | to_substitute: Phrase = self._matched_rule.to_substitute # type: ignore 196 | to_substitute.next_character() 197 | if not to_substitute.index_at_end(): 198 | keyboard.tap_key(KC.BSPC) 199 | else: 200 | self._state = State.SENDING 201 | 202 | if self._state == State.SENDING: 203 | substitution = self._matched_rule.substitution # type: ignore 204 | if not substitution.index_at_end(): 205 | keyboard.tap_key(substitution.get_character_at_current_index().key_code) 206 | substitution.next_character() 207 | else: 208 | self._state = State.LISTENING 209 | self._matched_rule = None 210 | for rule in self._rules: 211 | rule.restart() 212 | 213 | def after_hid_send(self, keyboard): 214 | return 215 | 216 | def on_powersave_enable(self, keyboard): 217 | return 218 | 219 | def on_powersave_disable(self, keyboard): 220 | return 221 | 222 | def after_matrix_scan(self, keyboard): 223 | return 224 | -------------------------------------------------------------------------------- /code/kmk/modules/dynamic_sequences.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | from supervisor import ticks_ms 3 | 4 | from collections import namedtuple 5 | 6 | from kmk.keys import KC, make_argumented_key 7 | from kmk.kmktime import check_deadline, ticks_diff 8 | from kmk.modules import Module 9 | 10 | 11 | class DSMeta: 12 | def __init__(self, sequence_select=None): 13 | self.sequence_select = sequence_select 14 | 15 | 16 | class SequenceStatus: 17 | STOPPED = const(0) 18 | RECORDING = const(1) 19 | PLAYING = const(2) 20 | SET_REPEPITIONS = const(3) 21 | SET_INTERVAL = const(4) 22 | 23 | 24 | # Keycodes for number keys 25 | _numbers = range(KC.N1.code, KC.N0.code + 1) 26 | 27 | SequenceFrame = namedtuple('SequenceFrame', ['keys_pressed', 'timestamp']) 28 | 29 | 30 | class Sequence: 31 | def __init__(self): 32 | self.repetitions = 1 33 | self.interval = 0 34 | self.sequence_data = [SequenceFrame(set(), 0) for i in range(3)] 35 | 36 | 37 | class DynamicSequences(Module): 38 | def __init__( 39 | self, slots=1, timeout=60000, key_interval=0, use_recorded_speed=False 40 | ): 41 | self.sequences = [Sequence() for i in range(slots)] 42 | self.current_slot = self.sequences[0] 43 | self.status = SequenceStatus.STOPPED 44 | 45 | self.index = 0 46 | self.start_time = 0 47 | self.current_repetition = 0 48 | self.last_config_frame = set() 49 | 50 | self.timeout = timeout 51 | self.key_interval = key_interval 52 | self.use_recorded_speed = use_recorded_speed 53 | 54 | # Create keycodes 55 | make_argumented_key( 56 | validator=DSMeta, names=('RECORD_SEQUENCE',), on_press=self._record_sequence 57 | ) 58 | 59 | make_argumented_key( 60 | validator=DSMeta, names=('PLAY_SEQUENCE',), on_press=self._play_sequence 61 | ) 62 | 63 | make_argumented_key( 64 | validator=DSMeta, 65 | names=( 66 | 'SET_SEQUENCE', 67 | 'STOP_SEQUENCE', 68 | ), 69 | on_press=self._stop_sequence, 70 | ) 71 | 72 | make_argumented_key( 73 | validator=DSMeta, 74 | names=('SET_SEQUENCE_REPETITIONS',), 75 | on_press=self._set_sequence_repetitions, 76 | ) 77 | 78 | make_argumented_key( 79 | validator=DSMeta, 80 | names=('SET_SEQUENCE_INTERVAL',), 81 | on_press=self._set_sequence_interval, 82 | ) 83 | 84 | def _record_sequence(self, key, keyboard, *args, **kwargs): 85 | self._stop_sequence(key, keyboard) 86 | self.status = SequenceStatus.RECORDING 87 | self.start_time = ticks_ms() 88 | self.current_slot.sequence_data = [SequenceFrame(set(), 0)] 89 | self.index = 0 90 | 91 | def _play_sequence(self, key, keyboard, *args, **kwargs): 92 | self._stop_sequence(key, keyboard) 93 | self.status = SequenceStatus.PLAYING 94 | self.start_time = ticks_ms() 95 | self.index = 0 96 | self.current_repetition = 0 97 | 98 | def _stop_sequence(self, key, keyboard, *args, **kwargs): 99 | if self.status == SequenceStatus.RECORDING: 100 | self.stop_recording() 101 | elif self.status == SequenceStatus.SET_INTERVAL: 102 | self.stop_config() 103 | self.status = SequenceStatus.STOPPED 104 | 105 | # Change sequences here because stop is always called 106 | if key.meta.sequence_select is not None: 107 | self.current_slot = self.sequences[key.meta.sequence_select] 108 | 109 | # Configure repeat settings 110 | def _set_sequence_repetitions(self, key, keyboard, *args, **kwargs): 111 | self._stop_sequence(key, keyboard) 112 | self.status = SequenceStatus.SET_REPEPITIONS 113 | self.last_config_frame = set() 114 | self.current_slot.repetitions = 0 115 | self.start_time = ticks_ms() 116 | 117 | def _set_sequence_interval(self, key, keyboard, *args, **kwargs): 118 | self._stop_sequence(key, keyboard) 119 | self.status = SequenceStatus.SET_INTERVAL 120 | self.last_config_frame = set() 121 | self.current_slot.interval = 0 122 | self.start_time = ticks_ms() 123 | 124 | # Add the current keypress state to the sequence 125 | def record_frame(self, keys_pressed): 126 | if self.current_slot.sequence_data[self.index].keys_pressed != keys_pressed: 127 | self.index += 1 128 | 129 | # Recorded speed 130 | if self.use_recorded_speed: 131 | self.current_slot.sequence_data.append( 132 | SequenceFrame( 133 | keys_pressed.copy(), ticks_diff(ticks_ms(), self.start_time) 134 | ) 135 | ) 136 | 137 | # Constant speed 138 | else: 139 | self.current_slot.sequence_data.append( 140 | SequenceFrame(keys_pressed.copy(), self.index * self.key_interval) 141 | ) 142 | 143 | if not check_deadline(ticks_ms(), self.start_time, self.timeout): 144 | self.stop_recording() 145 | 146 | # Add the ending frames to the sequence 147 | def stop_recording(self): 148 | # Clear the remaining keys 149 | self.current_slot.sequence_data.append( 150 | SequenceFrame(set(), self.current_slot.sequence_data[-1].timestamp + 20) 151 | ) 152 | 153 | # Wait for the specified interval 154 | prev_timestamp = self.current_slot.sequence_data[-1].timestamp 155 | self.current_slot.sequence_data.append( 156 | SequenceFrame( 157 | set(), 158 | prev_timestamp + self.current_slot.interval * 1000, 159 | ) 160 | ) 161 | 162 | self.status = SequenceStatus.STOPPED 163 | 164 | def play_frame(self, keyboard): 165 | # Send the keypresses at this point in the sequence 166 | if not check_deadline( 167 | ticks_ms(), 168 | self.start_time, 169 | self.current_slot.sequence_data[self.index].timestamp, 170 | ): 171 | if self.index: 172 | prev = self.current_slot.sequence_data[self.index - 1].keys_pressed 173 | cur = self.current_slot.sequence_data[self.index].keys_pressed 174 | 175 | for key in prev.difference(cur): 176 | keyboard.remove_key(key) 177 | for key in cur.difference(prev): 178 | keyboard.add_key(key) 179 | 180 | self.index += 1 181 | if self.index >= len(self.current_slot.sequence_data): # Reached the end 182 | self.current_repetition += 1 183 | if self.current_repetition == self.current_slot.repetitions: 184 | self.status = SequenceStatus.STOPPED 185 | else: 186 | self.index = 0 187 | self.start_time = ticks_ms() 188 | 189 | # Configuration for repeating sequences 190 | def config_mode(self, keyboard): 191 | for key in keyboard.keys_pressed.difference(self.last_config_frame): 192 | if key.code in _numbers: 193 | digit = (key.code - KC.N1.code + 1) % 10 194 | if self.status == SequenceStatus.SET_REPEPITIONS: 195 | self.current_slot.repetitions = ( 196 | self.current_slot.repetitions * 10 + digit 197 | ) 198 | elif self.status == SequenceStatus.SET_INTERVAL: 199 | self.current_slot.interval = self.current_slot.interval * 10 + digit 200 | 201 | elif key.code == KC.ENTER.code: 202 | self.stop_config() 203 | 204 | self.last_config_frame = keyboard.keys_pressed.copy() 205 | keyboard.hid_pending = False # Disable typing 206 | 207 | if not check_deadline(ticks_ms(), self.start_time, self.timeout): 208 | self.stop_config() 209 | 210 | # Finish configuring repetitions 211 | def stop_config(self): 212 | self.current_slot.sequence_data[-1] = SequenceFrame( 213 | self.current_slot.sequence_data[-1].keys_pressed, 214 | self.current_slot.sequence_data[-2].timestamp 215 | + self.current_slot.interval * 1000, 216 | ) 217 | self.current_slot.repetitions = max(self.current_slot.repetitions, 1) 218 | self.status = SequenceStatus.STOPPED 219 | 220 | def on_runtime_enable(self, keyboard): 221 | return 222 | 223 | def on_runtime_disable(self, keyboard): 224 | return 225 | 226 | def during_bootup(self, keyboard): 227 | return 228 | 229 | def before_matrix_scan(self, keyboard): 230 | return 231 | 232 | def after_matrix_scan(self, keyboard): 233 | return 234 | 235 | def before_hid_send(self, keyboard): 236 | 237 | if not self.status: 238 | return 239 | 240 | elif self.status == SequenceStatus.RECORDING: 241 | self.record_frame(keyboard.keys_pressed) 242 | 243 | elif self.status == SequenceStatus.PLAYING: 244 | self.play_frame(keyboard) 245 | 246 | elif ( 247 | self.status == SequenceStatus.SET_REPEPITIONS 248 | or self.status == SequenceStatus.SET_INTERVAL 249 | ): 250 | self.config_mode(keyboard) 251 | 252 | def after_hid_send(self, keyboard): 253 | return 254 | 255 | def on_powersave_enable(self, keyboard): 256 | return 257 | 258 | def on_powersave_disable(self, keyboard): 259 | return 260 | -------------------------------------------------------------------------------- /code/kmk/modules/mouse_keys.py: -------------------------------------------------------------------------------- 1 | from supervisor import ticks_ms 2 | 3 | from kmk.hid import HID_REPORT_SIZES, HIDReportTypes 4 | from kmk.keys import make_key 5 | from kmk.modules import Module 6 | 7 | 8 | class PointingDevice: 9 | MB_LMB = 1 10 | MB_RMB = 2 11 | MB_MMB = 4 12 | _evt = bytearray(HID_REPORT_SIZES[HIDReportTypes.MOUSE] + 1) 13 | 14 | def __init__(self): 15 | self.key_states = {} 16 | self.hid_pending = False 17 | self.report_device = memoryview(self._evt)[0:1] 18 | self.report_device[0] = HIDReportTypes.MOUSE 19 | self.button_status = memoryview(self._evt)[1:2] 20 | self.report_x = memoryview(self._evt)[2:3] 21 | self.report_y = memoryview(self._evt)[3:4] 22 | self.report_w = memoryview(self._evt)[4:] 23 | 24 | 25 | class MouseKeys(Module): 26 | def __init__(self): 27 | self.pointing_device = PointingDevice() 28 | self._nav_key_activated = 0 29 | self._up_activated = False 30 | self._down_activated = False 31 | self._left_activated = False 32 | self._right_activated = False 33 | self.max_speed = 10 34 | self.ac_interval = 100 # Delta ms to apply acceleration 35 | self._next_interval = 0 # Time for next tick interval 36 | self.move_step = 1 37 | 38 | make_key( 39 | names=('MB_LMB',), 40 | on_press=self._mb_lmb_press, 41 | on_release=self._mb_lmb_release, 42 | ) 43 | make_key( 44 | names=('MB_MMB',), 45 | on_press=self._mb_mmb_press, 46 | on_release=self._mb_mmb_release, 47 | ) 48 | make_key( 49 | names=('MB_RMB',), 50 | on_press=self._mb_rmb_press, 51 | on_release=self._mb_rmb_release, 52 | ) 53 | make_key( 54 | names=('MW_UP',), 55 | on_press=self._mw_up_press, 56 | on_release=self._mw_up_release, 57 | ) 58 | make_key( 59 | names=( 60 | 'MW_DOWN', 61 | 'MW_DN', 62 | ), 63 | on_press=self._mw_down_press, 64 | on_release=self._mw_down_release, 65 | ) 66 | make_key( 67 | names=('MS_UP',), 68 | on_press=self._ms_up_press, 69 | on_release=self._ms_up_release, 70 | ) 71 | make_key( 72 | names=( 73 | 'MS_DOWN', 74 | 'MS_DN', 75 | ), 76 | on_press=self._ms_down_press, 77 | on_release=self._ms_down_release, 78 | ) 79 | make_key( 80 | names=( 81 | 'MS_LEFT', 82 | 'MS_LT', 83 | ), 84 | on_press=self._ms_left_press, 85 | on_release=self._ms_left_release, 86 | ) 87 | make_key( 88 | names=( 89 | 'MS_RIGHT', 90 | 'MS_RT', 91 | ), 92 | on_press=self._ms_right_press, 93 | on_release=self._ms_right_release, 94 | ) 95 | 96 | def during_bootup(self, keyboard): 97 | return 98 | 99 | def matrix_detected_press(self, keyboard): 100 | return keyboard.matrix_update is None 101 | 102 | def before_matrix_scan(self, keyboard): 103 | return 104 | 105 | def after_matrix_scan(self, keyboard): 106 | if self._nav_key_activated: 107 | if self._next_interval <= ticks_ms(): 108 | # print("hello: ") 109 | # print(ticks_ms()) 110 | self._next_interval = ticks_ms() + self.ac_interval 111 | # print(self._next_interval) 112 | if self.move_step < self.max_speed: 113 | self.move_step = self.move_step + 1 114 | if self._right_activated: 115 | self.pointing_device.report_x[0] = self.move_step 116 | if self._left_activated: 117 | self.pointing_device.report_x[0] = 0xFF & (0 - self.move_step) 118 | if self._up_activated: 119 | self.pointing_device.report_y[0] = 0xFF & (0 - self.move_step) 120 | if self._down_activated: 121 | self.pointing_device.report_y[0] = self.move_step 122 | self.pointing_device.hid_pending = True 123 | return 124 | 125 | def before_hid_send(self, keyboard): 126 | if self.pointing_device.hid_pending and keyboard._hid_send_enabled: 127 | keyboard._hid_helper.hid_send(self.pointing_device._evt) 128 | self.pointing_device.hid_pending = False 129 | return 130 | 131 | def after_hid_send(self, keyboard): 132 | return 133 | 134 | def on_powersave_enable(self, keyboard): 135 | return 136 | 137 | def on_powersave_disable(self, keyboard): 138 | return 139 | 140 | def _mb_lmb_press(self, key, keyboard, *args, **kwargs): 141 | self.pointing_device.button_status[0] |= self.pointing_device.MB_LMB 142 | self.pointing_device.hid_pending = True 143 | 144 | def _mb_lmb_release(self, key, keyboard, *args, **kwargs): 145 | self.pointing_device.button_status[0] &= ~self.pointing_device.MB_LMB 146 | self.pointing_device.hid_pending = True 147 | 148 | def _mb_mmb_press(self, key, keyboard, *args, **kwargs): 149 | self.pointing_device.button_status[0] |= self.pointing_device.MB_MMB 150 | self.pointing_device.hid_pending = True 151 | 152 | def _mb_mmb_release(self, key, keyboard, *args, **kwargs): 153 | self.pointing_device.button_status[0] &= ~self.pointing_device.MB_MMB 154 | self.pointing_device.hid_pending = True 155 | 156 | def _mb_rmb_press(self, key, keyboard, *args, **kwargs): 157 | self.pointing_device.button_status[0] |= self.pointing_device.MB_RMB 158 | self.pointing_device.hid_pending = True 159 | 160 | def _mb_rmb_release(self, key, keyboard, *args, **kwargs): 161 | self.pointing_device.button_status[0] &= ~self.pointing_device.MB_RMB 162 | self.pointing_device.hid_pending = True 163 | 164 | def _mw_up_press(self, key, keyboard, *args, **kwargs): 165 | self.pointing_device.report_w[0] = self.move_step 166 | self.pointing_device.hid_pending = True 167 | 168 | def _mw_up_release(self, key, keyboard, *args, **kwargs): 169 | self.pointing_device.report_w[0] = 0 170 | self.pointing_device.hid_pending = True 171 | 172 | def _mw_down_press(self, key, keyboard, *args, **kwargs): 173 | self.pointing_device.report_w[0] = 0xFF 174 | self.pointing_device.hid_pending = True 175 | 176 | def _mw_down_release(self, key, keyboard, *args, **kwargs): 177 | self.pointing_device.report_w[0] = 0 178 | self.pointing_device.hid_pending = True 179 | 180 | # Mouse movement 181 | def _reset_next_interval(self): 182 | if self._nav_key_activated == 1: 183 | self._next_interval = ticks_ms() + self.ac_interval 184 | self.move_step = 1 185 | 186 | def _check_last(self): 187 | if self._nav_key_activated == 0: 188 | self.move_step = 1 189 | 190 | def _ms_up_press(self, key, keyboard, *args, **kwargs): 191 | self._nav_key_activated += 1 192 | self._reset_next_interval() 193 | self._up_activated = True 194 | self.pointing_device.report_y[0] = 0xFF & (0 - self.move_step) 195 | self.pointing_device.hid_pending = True 196 | 197 | def _ms_up_release(self, key, keyboard, *args, **kwargs): 198 | self._up_activated = False 199 | self._nav_key_activated -= 1 200 | self._check_last() 201 | self.pointing_device.report_y[0] = 0 202 | self.pointing_device.hid_pending = False 203 | 204 | def _ms_down_press(self, key, keyboard, *args, **kwargs): 205 | self._nav_key_activated += 1 206 | self._reset_next_interval() 207 | self._down_activated = True 208 | # if not self.x_activated and not self.y_activated: 209 | # self.next_interval = ticks_ms() + self.ac_intervalle 210 | self.pointing_device.report_y[0] = self.move_step 211 | self.pointing_device.hid_pending = True 212 | 213 | def _ms_down_release(self, key, keyboard, *args, **kwargs): 214 | self._down_activated = False 215 | self._nav_key_activated -= 1 216 | self._check_last() 217 | self.pointing_device.report_y[0] = 0 218 | self.pointing_device.hid_pending = False 219 | 220 | def _ms_left_press(self, key, keyboard, *args, **kwargs): 221 | self._nav_key_activated += 1 222 | self._reset_next_interval() 223 | self._left_activated = True 224 | self.pointing_device.report_x[0] = 0xFF & (0 - self.move_step) 225 | self.pointing_device.hid_pending = True 226 | 227 | def _ms_left_release(self, key, keyboard, *args, **kwargs): 228 | self._nav_key_activated -= 1 229 | self._left_activated = False 230 | self._check_last() 231 | self.pointing_device.report_x[0] = 0 232 | self.pointing_device.hid_pending = False 233 | 234 | def _ms_right_press(self, key, keyboard, *args, **kwargs): 235 | self._nav_key_activated += 1 236 | self._reset_next_interval() 237 | self._right_activated = True 238 | self.pointing_device.report_x[0] = self.move_step 239 | self.pointing_device.hid_pending = True 240 | 241 | def _ms_right_release(self, key, keyboard, *args, **kwargs): 242 | self._nav_key_activated -= 1 243 | self._right_activated = False 244 | self._check_last() 245 | self.pointing_device.report_x[0] = 0 246 | self.pointing_device.hid_pending = False 247 | -------------------------------------------------------------------------------- /code/kmk/modules/holdtap.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | 3 | from kmk.keys import KC, make_argumented_key 4 | from kmk.modules import Module 5 | from kmk.utils import Debug 6 | 7 | debug = Debug(__name__) 8 | 9 | 10 | class ActivationType: 11 | PRESSED = const(0) 12 | RELEASED = const(1) 13 | HOLD_TIMEOUT = const(2) 14 | INTERRUPTED = const(3) 15 | REPEAT = const(4) 16 | 17 | 18 | class HoldTapRepeat: 19 | NONE = const(0) 20 | TAP = const(1) 21 | HOLD = const(2) 22 | ALL = const(3) 23 | 24 | 25 | class HoldTapKeyState: 26 | def __init__(self, timeout_key, *args, **kwargs): 27 | self.timeout_key = timeout_key 28 | self.args = args 29 | self.kwargs = kwargs 30 | self.activated = ActivationType.PRESSED 31 | 32 | 33 | class HoldTapKeyMeta: 34 | def __init__( 35 | self, 36 | tap, 37 | hold, 38 | prefer_hold=True, 39 | tap_interrupted=False, 40 | tap_time=None, 41 | repeat=HoldTapRepeat.NONE, 42 | ): 43 | self.tap = tap 44 | self.hold = hold 45 | self.prefer_hold = prefer_hold 46 | self.tap_interrupted = tap_interrupted 47 | self.tap_time = tap_time 48 | self.repeat = repeat 49 | 50 | 51 | class HoldTap(Module): 52 | tap_time = 300 53 | 54 | def __init__(self): 55 | self.key_buffer = [] 56 | self.key_states = {} 57 | if not KC.get('HT'): 58 | make_argumented_key( 59 | validator=HoldTapKeyMeta, 60 | names=('HT',), 61 | on_press=self.ht_pressed, 62 | on_release=self.ht_released, 63 | ) 64 | 65 | def during_bootup(self, keyboard): 66 | return 67 | 68 | def before_matrix_scan(self, keyboard): 69 | return 70 | 71 | def after_matrix_scan(self, keyboard): 72 | return 73 | 74 | def process_key(self, keyboard, key, is_pressed, int_coord): 75 | '''Handle holdtap being interrupted by another key press/release.''' 76 | current_key = key 77 | send_buffer = False 78 | append_buffer = False 79 | 80 | for key, state in self.key_states.items(): 81 | if key == current_key: 82 | continue 83 | if state.activated != ActivationType.PRESSED: 84 | continue 85 | 86 | # holdtap is interrupted by another key event. 87 | if (is_pressed and not key.meta.tap_interrupted) or ( 88 | not is_pressed and key.meta.tap_interrupted and self.key_buffer 89 | ): 90 | 91 | keyboard.cancel_timeout(state.timeout_key) 92 | self.key_states[key].activated = ActivationType.INTERRUPTED 93 | self.ht_activate_on_interrupt( 94 | key, keyboard, *state.args, **state.kwargs 95 | ) 96 | send_buffer = True 97 | 98 | # if interrupt on release: store interrupting keys until one of them 99 | # is released. 100 | if ( 101 | key.meta.tap_interrupted 102 | and is_pressed 103 | and not isinstance(current_key.meta, HoldTapKeyMeta) 104 | ): 105 | append_buffer = True 106 | 107 | # apply changes with 'side-effects' on key_states or the loop behaviour 108 | # outside the loop. 109 | if append_buffer: 110 | self.key_buffer.append((int_coord, current_key, is_pressed)) 111 | current_key = None 112 | 113 | elif send_buffer: 114 | self.send_key_buffer(keyboard) 115 | keyboard.resume_process_key(self, current_key, is_pressed, int_coord) 116 | current_key = None 117 | 118 | return current_key 119 | 120 | def before_hid_send(self, keyboard): 121 | return 122 | 123 | def after_hid_send(self, keyboard): 124 | return 125 | 126 | def on_powersave_enable(self, keyboard): 127 | return 128 | 129 | def on_powersave_disable(self, keyboard): 130 | return 131 | 132 | def ht_pressed(self, key, keyboard, *args, **kwargs): 133 | '''Unless in repeat mode, do nothing yet, action resolves when key is released, timer expires or other key is pressed.''' 134 | if key in self.key_states: 135 | state = self.key_states[key] 136 | keyboard.cancel_timeout(self.key_states[key].timeout_key) 137 | 138 | if state.activated == ActivationType.RELEASED: 139 | state.activated = ActivationType.REPEAT 140 | self.ht_activate_tap(key, keyboard, *args, **kwargs) 141 | elif state.activated == ActivationType.HOLD_TIMEOUT: 142 | self.ht_activate_hold(key, keyboard, *args, **kwargs) 143 | elif state.activated == ActivationType.INTERRUPTED: 144 | self.ht_activate_on_interrupt(key, keyboard, *args, **kwargs) 145 | return 146 | 147 | if key.meta.tap_time is None: 148 | tap_time = self.tap_time 149 | else: 150 | tap_time = key.meta.tap_time 151 | timeout_key = keyboard.set_timeout( 152 | tap_time, 153 | lambda: self.on_tap_time_expired(key, keyboard, *args, **kwargs), 154 | ) 155 | self.key_states[key] = HoldTapKeyState(timeout_key, *args, **kwargs) 156 | return keyboard 157 | 158 | def ht_released(self, key, keyboard, *args, **kwargs): 159 | '''On keyup, release mod or tap key.''' 160 | if key not in self.key_states: 161 | return keyboard 162 | 163 | state = self.key_states[key] 164 | keyboard.cancel_timeout(state.timeout_key) 165 | repeat = key.meta.repeat & HoldTapRepeat.TAP 166 | 167 | if state.activated == ActivationType.HOLD_TIMEOUT: 168 | # release hold 169 | self.ht_deactivate_hold(key, keyboard, *args, **kwargs) 170 | repeat = key.meta.repeat & HoldTapRepeat.HOLD 171 | elif state.activated == ActivationType.INTERRUPTED: 172 | # release tap 173 | self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs) 174 | if key.meta.prefer_hold: 175 | repeat = key.meta.repeat & HoldTapRepeat.HOLD 176 | elif state.activated == ActivationType.PRESSED: 177 | # press and release tap because key released within tap time 178 | self.ht_activate_tap(key, keyboard, *args, **kwargs) 179 | self.send_key_buffer(keyboard) 180 | self.ht_deactivate_tap(key, keyboard, *args, **kwargs) 181 | state.activated = ActivationType.RELEASED 182 | self.send_key_buffer(keyboard) 183 | elif state.activated == ActivationType.REPEAT: 184 | state.activated = ActivationType.RELEASED 185 | self.ht_deactivate_tap(key, keyboard, *args, **kwargs) 186 | 187 | # don't delete the key state right now in this case 188 | if repeat: 189 | if key.meta.tap_time is None: 190 | tap_time = self.tap_time 191 | else: 192 | tap_time = key.meta.tap_time 193 | state.timeout_key = keyboard.set_timeout( 194 | tap_time, lambda: self.key_states.pop(key) 195 | ) 196 | else: 197 | del self.key_states[key] 198 | 199 | return keyboard 200 | 201 | def on_tap_time_expired(self, key, keyboard, *args, **kwargs): 202 | '''When tap time expires activate hold if key is still being pressed. 203 | Remove key if ActivationType is RELEASED.''' 204 | try: 205 | state = self.key_states[key] 206 | except KeyError: 207 | if debug.enabled: 208 | debug(f'on_tap_time_expired: no such key {key}') 209 | return 210 | 211 | if self.key_states[key].activated == ActivationType.PRESSED: 212 | # press hold because timer expired after tap time 213 | self.key_states[key].activated = ActivationType.HOLD_TIMEOUT 214 | self.ht_activate_hold(key, keyboard, *args, **kwargs) 215 | self.send_key_buffer(keyboard) 216 | elif state.activated == ActivationType.RELEASED: 217 | self.ht_deactivate_tap(key, keyboard, *args, **kwargs) 218 | del self.key_states[key] 219 | 220 | def send_key_buffer(self, keyboard): 221 | if not self.key_buffer: 222 | return 223 | 224 | for (int_coord, key, is_pressed) in self.key_buffer: 225 | keyboard.resume_process_key(self, key, is_pressed, int_coord) 226 | 227 | self.key_buffer.clear() 228 | 229 | def ht_activate_hold(self, key, keyboard, *args, **kwargs): 230 | if debug.enabled: 231 | debug('ht_activate_hold') 232 | keyboard.resume_process_key(self, key.meta.hold, True) 233 | 234 | def ht_deactivate_hold(self, key, keyboard, *args, **kwargs): 235 | if debug.enabled: 236 | debug('ht_deactivate_hold') 237 | keyboard.resume_process_key(self, key.meta.hold, False) 238 | 239 | def ht_activate_tap(self, key, keyboard, *args, **kwargs): 240 | if debug.enabled: 241 | debug('ht_activate_tap') 242 | keyboard.resume_process_key(self, key.meta.tap, True) 243 | 244 | def ht_deactivate_tap(self, key, keyboard, *args, **kwargs): 245 | if debug.enabled: 246 | debug('ht_deactivate_tap') 247 | keyboard.resume_process_key(self, key.meta.tap, False) 248 | 249 | def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs): 250 | if debug.enabled: 251 | debug('ht_activate_on_interrupt') 252 | if key.meta.prefer_hold: 253 | self.ht_activate_hold(key, keyboard, *args, **kwargs) 254 | else: 255 | self.ht_activate_tap(key, keyboard, *args, **kwargs) 256 | 257 | def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs): 258 | if debug.enabled: 259 | debug('ht_deactivate_on_interrupt') 260 | if key.meta.prefer_hold: 261 | self.ht_deactivate_hold(key, keyboard, *args, **kwargs) 262 | else: 263 | self.ht_deactivate_tap(key, keyboard, *args, **kwargs) 264 | -------------------------------------------------------------------------------- /kicad files/picohat.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "design_settings": { 4 | "defaults": { 5 | "board_outline_line_width": 0.09999999999999999, 6 | "copper_line_width": 0.19999999999999998, 7 | "copper_text_italic": false, 8 | "copper_text_size_h": 1.5, 9 | "copper_text_size_v": 1.5, 10 | "copper_text_thickness": 0.3, 11 | "copper_text_upright": false, 12 | "courtyard_line_width": 0.049999999999999996, 13 | "dimension_precision": 4, 14 | "dimension_units": 3, 15 | "dimensions": { 16 | "arrow_length": 1270000, 17 | "extension_offset": 500000, 18 | "keep_text_aligned": true, 19 | "suppress_zeroes": false, 20 | "text_position": 0, 21 | "units_format": 1 22 | }, 23 | "fab_line_width": 0.09999999999999999, 24 | "fab_text_italic": false, 25 | "fab_text_size_h": 1.0, 26 | "fab_text_size_v": 1.0, 27 | "fab_text_thickness": 0.15, 28 | "fab_text_upright": false, 29 | "other_line_width": 0.15, 30 | "other_text_italic": false, 31 | "other_text_size_h": 1.0, 32 | "other_text_size_v": 1.0, 33 | "other_text_thickness": 0.15, 34 | "other_text_upright": false, 35 | "pads": { 36 | "drill": 0.762, 37 | "height": 1.524, 38 | "width": 1.524 39 | }, 40 | "silk_line_width": 0.15, 41 | "silk_text_italic": false, 42 | "silk_text_size_h": 1.0, 43 | "silk_text_size_v": 1.0, 44 | "silk_text_thickness": 0.15, 45 | "silk_text_upright": false, 46 | "zones": { 47 | "45_degree_only": false, 48 | "min_clearance": 0.09999999999999999 49 | } 50 | }, 51 | "diff_pair_dimensions": [], 52 | "drc_exclusions": [], 53 | "meta": { 54 | "version": 2 55 | }, 56 | "rule_severities": { 57 | "annular_width": "error", 58 | "clearance": "error", 59 | "copper_edge_clearance": "error", 60 | "courtyards_overlap": "error", 61 | "diff_pair_gap_out_of_range": "error", 62 | "diff_pair_uncoupled_length_too_long": "error", 63 | "drill_out_of_range": "error", 64 | "duplicate_footprints": "warning", 65 | "extra_footprint": "warning", 66 | "footprint_type_mismatch": "error", 67 | "hole_clearance": "error", 68 | "hole_near_hole": "error", 69 | "invalid_outline": "error", 70 | "item_on_disabled_layer": "error", 71 | "items_not_allowed": "error", 72 | "length_out_of_range": "error", 73 | "malformed_courtyard": "error", 74 | "microvia_drill_out_of_range": "error", 75 | "missing_courtyard": "ignore", 76 | "missing_footprint": "warning", 77 | "net_conflict": "warning", 78 | "npth_inside_courtyard": "ignore", 79 | "padstack": "error", 80 | "pth_inside_courtyard": "ignore", 81 | "shorting_items": "error", 82 | "silk_over_copper": "warning", 83 | "silk_overlap": "warning", 84 | "skew_out_of_range": "error", 85 | "through_hole_pad_without_hole": "error", 86 | "too_many_vias": "error", 87 | "track_dangling": "warning", 88 | "track_width": "error", 89 | "tracks_crossing": "error", 90 | "unconnected_items": "error", 91 | "unresolved_variable": "error", 92 | "via_dangling": "warning", 93 | "zone_has_empty_net": "error", 94 | "zones_intersect": "error" 95 | }, 96 | "rules": { 97 | "allow_blind_buried_vias": false, 98 | "allow_microvias": false, 99 | "max_error": 0.005, 100 | "min_clearance": 0.0, 101 | "min_copper_edge_clearance": 0.0, 102 | "min_hole_clearance": 0.25, 103 | "min_hole_to_hole": 0.25, 104 | "min_microvia_diameter": 0.19999999999999998, 105 | "min_microvia_drill": 0.09999999999999999, 106 | "min_silk_clearance": 0.0, 107 | "min_through_hole_diameter": 0.3, 108 | "min_track_width": 0.19999999999999998, 109 | "min_via_annular_width": 0.049999999999999996, 110 | "min_via_diameter": 0.39999999999999997, 111 | "solder_mask_clearance": 0.0, 112 | "solder_mask_min_width": 0.0, 113 | "use_height_for_length_calcs": true 114 | }, 115 | "track_widths": [], 116 | "via_dimensions": [], 117 | "zones_allow_external_fillets": false, 118 | "zones_use_no_outline": true 119 | }, 120 | "layer_presets": [] 121 | }, 122 | "boards": [], 123 | "cvpcb": { 124 | "equivalence_files": [] 125 | }, 126 | "erc": { 127 | "erc_exclusions": [], 128 | "meta": { 129 | "version": 0 130 | }, 131 | "pin_map": [ 132 | [ 133 | 0, 134 | 0, 135 | 0, 136 | 0, 137 | 0, 138 | 0, 139 | 1, 140 | 0, 141 | 0, 142 | 0, 143 | 0, 144 | 2 145 | ], 146 | [ 147 | 0, 148 | 2, 149 | 0, 150 | 1, 151 | 0, 152 | 0, 153 | 1, 154 | 0, 155 | 2, 156 | 2, 157 | 2, 158 | 2 159 | ], 160 | [ 161 | 0, 162 | 0, 163 | 0, 164 | 0, 165 | 0, 166 | 0, 167 | 1, 168 | 0, 169 | 1, 170 | 0, 171 | 1, 172 | 2 173 | ], 174 | [ 175 | 0, 176 | 1, 177 | 0, 178 | 0, 179 | 0, 180 | 0, 181 | 1, 182 | 1, 183 | 2, 184 | 1, 185 | 1, 186 | 2 187 | ], 188 | [ 189 | 0, 190 | 0, 191 | 0, 192 | 0, 193 | 0, 194 | 0, 195 | 1, 196 | 0, 197 | 0, 198 | 0, 199 | 0, 200 | 2 201 | ], 202 | [ 203 | 0, 204 | 0, 205 | 0, 206 | 0, 207 | 0, 208 | 0, 209 | 0, 210 | 0, 211 | 0, 212 | 0, 213 | 0, 214 | 2 215 | ], 216 | [ 217 | 1, 218 | 1, 219 | 1, 220 | 1, 221 | 1, 222 | 0, 223 | 1, 224 | 1, 225 | 1, 226 | 1, 227 | 1, 228 | 2 229 | ], 230 | [ 231 | 0, 232 | 0, 233 | 0, 234 | 1, 235 | 0, 236 | 0, 237 | 1, 238 | 0, 239 | 0, 240 | 0, 241 | 0, 242 | 2 243 | ], 244 | [ 245 | 0, 246 | 2, 247 | 1, 248 | 2, 249 | 0, 250 | 0, 251 | 1, 252 | 0, 253 | 2, 254 | 2, 255 | 2, 256 | 2 257 | ], 258 | [ 259 | 0, 260 | 2, 261 | 0, 262 | 1, 263 | 0, 264 | 0, 265 | 1, 266 | 0, 267 | 2, 268 | 0, 269 | 0, 270 | 2 271 | ], 272 | [ 273 | 0, 274 | 2, 275 | 1, 276 | 1, 277 | 0, 278 | 0, 279 | 1, 280 | 0, 281 | 2, 282 | 0, 283 | 0, 284 | 2 285 | ], 286 | [ 287 | 2, 288 | 2, 289 | 2, 290 | 2, 291 | 2, 292 | 2, 293 | 2, 294 | 2, 295 | 2, 296 | 2, 297 | 2, 298 | 2 299 | ] 300 | ], 301 | "rule_severities": { 302 | "bus_definition_conflict": "error", 303 | "bus_entry_needed": "error", 304 | "bus_label_syntax": "error", 305 | "bus_to_bus_conflict": "error", 306 | "bus_to_net_conflict": "error", 307 | "different_unit_footprint": "error", 308 | "different_unit_net": "error", 309 | "duplicate_reference": "error", 310 | "duplicate_sheet_names": "error", 311 | "extra_units": "error", 312 | "global_label_dangling": "warning", 313 | "hier_label_mismatch": "error", 314 | "label_dangling": "error", 315 | "lib_symbol_issues": "warning", 316 | "multiple_net_names": "warning", 317 | "net_not_bus_member": "warning", 318 | "no_connect_connected": "warning", 319 | "no_connect_dangling": "warning", 320 | "pin_not_connected": "error", 321 | "pin_not_driven": "error", 322 | "pin_to_pin": "warning", 323 | "power_pin_not_driven": "error", 324 | "similar_labels": "warning", 325 | "unannotated": "error", 326 | "unit_value_mismatch": "error", 327 | "unresolved_variable": "error", 328 | "wire_dangling": "error" 329 | } 330 | }, 331 | "libraries": { 332 | "pinned_footprint_libs": [], 333 | "pinned_symbol_libs": [] 334 | }, 335 | "meta": { 336 | "filename": "picohat.kicad_pro", 337 | "version": 1 338 | }, 339 | "net_settings": { 340 | "classes": [ 341 | { 342 | "bus_width": 12.0, 343 | "clearance": 0.2, 344 | "diff_pair_gap": 0.25, 345 | "diff_pair_via_gap": 0.25, 346 | "diff_pair_width": 0.2, 347 | "line_style": 0, 348 | "microvia_diameter": 0.3, 349 | "microvia_drill": 0.1, 350 | "name": "Default", 351 | "pcb_color": "rgba(0, 0, 0, 0.000)", 352 | "schematic_color": "rgba(0, 0, 0, 0.000)", 353 | "track_width": 0.25, 354 | "via_diameter": 0.8, 355 | "via_drill": 0.4, 356 | "wire_width": 6.0 357 | } 358 | ], 359 | "meta": { 360 | "version": 2 361 | }, 362 | "net_colors": null 363 | }, 364 | "pcbnew": { 365 | "last_paths": { 366 | "gencad": "", 367 | "idf": "", 368 | "netlist": "picohat.net", 369 | "specctra_dsn": "", 370 | "step": "", 371 | "vrml": "" 372 | }, 373 | "page_layout_descr_file": "" 374 | }, 375 | "schematic": { 376 | "annotate_start_num": 0, 377 | "drawing": { 378 | "default_line_thickness": 6.0, 379 | "default_text_size": 50.0, 380 | "field_names": [], 381 | "intersheets_ref_own_page": false, 382 | "intersheets_ref_prefix": "", 383 | "intersheets_ref_short": false, 384 | "intersheets_ref_show": false, 385 | "intersheets_ref_suffix": "", 386 | "junction_size_choice": 3, 387 | "label_size_ratio": 0.375, 388 | "pin_symbol_size": 25.0, 389 | "text_offset_ratio": 0.15 390 | }, 391 | "legacy_lib_dir": "", 392 | "legacy_lib_list": [], 393 | "meta": { 394 | "version": 1 395 | }, 396 | "net_format_name": "", 397 | "ngspice": { 398 | "fix_include_paths": true, 399 | "fix_passive_vals": false, 400 | "meta": { 401 | "version": 0 402 | }, 403 | "model_mode": 0, 404 | "workbook_filename": "" 405 | }, 406 | "page_layout_descr_file": "", 407 | "plot_directory": "", 408 | "spice_adjust_passive_values": false, 409 | "spice_external_command": "spice \"%I\"", 410 | "subpart_first_id": 65, 411 | "subpart_id_separator": 0 412 | }, 413 | "sheets": [ 414 | [ 415 | "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55", 416 | "" 417 | ] 418 | ], 419 | "text_variables": {} 420 | } 421 | -------------------------------------------------------------------------------- /code/kmk/hid.py: -------------------------------------------------------------------------------- 1 | import supervisor 2 | import usb_hid 3 | from micropython import const 4 | 5 | from storage import getmount 6 | 7 | from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey 8 | 9 | try: 10 | from adafruit_ble import BLERadio 11 | from adafruit_ble.advertising.standard import ProvideServicesAdvertisement 12 | from adafruit_ble.services.standard.hid import HIDService 13 | except ImportError: 14 | # BLE not supported on this platform 15 | pass 16 | 17 | 18 | class HIDModes: 19 | NOOP = 0 # currently unused; for testing? 20 | USB = 1 21 | BLE = 2 22 | 23 | ALL_MODES = (NOOP, USB, BLE) 24 | 25 | 26 | class HIDReportTypes: 27 | KEYBOARD = 1 28 | MOUSE = 2 29 | CONSUMER = 3 30 | SYSCONTROL = 4 31 | 32 | 33 | class HIDUsage: 34 | KEYBOARD = 0x06 35 | MOUSE = 0x02 36 | CONSUMER = 0x01 37 | SYSCONTROL = 0x80 38 | 39 | 40 | class HIDUsagePage: 41 | CONSUMER = 0x0C 42 | KEYBOARD = MOUSE = SYSCONTROL = 0x01 43 | 44 | 45 | HID_REPORT_SIZES = { 46 | HIDReportTypes.KEYBOARD: 8, 47 | HIDReportTypes.MOUSE: 4, 48 | HIDReportTypes.CONSUMER: 2, 49 | HIDReportTypes.SYSCONTROL: 8, # TODO find the correct value for this 50 | } 51 | 52 | 53 | class AbstractHID: 54 | REPORT_BYTES = 8 55 | 56 | def __init__(self, **kwargs): 57 | self._prev_evt = bytearray(self.REPORT_BYTES) 58 | self._evt = bytearray(self.REPORT_BYTES) 59 | self.report_device = memoryview(self._evt)[0:1] 60 | self.report_device[0] = HIDReportTypes.KEYBOARD 61 | 62 | # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view 63 | # is "reserved" and evidently (mostly?) unused. However, other modes (or 64 | # at least consumer, so far) will use this byte, which is the main reason 65 | # this view exists. For KEYBOARD, use report_mods and report_non_mods 66 | self.report_keys = memoryview(self._evt)[1:] 67 | 68 | self.report_mods = memoryview(self._evt)[1:2] 69 | self.report_non_mods = memoryview(self._evt)[3:] 70 | 71 | self.post_init() 72 | 73 | def __repr__(self): 74 | return f'{self.__class__.__name__}(REPORT_BYTES={self.REPORT_BYTES})' 75 | 76 | def post_init(self): 77 | pass 78 | 79 | def create_report(self, keys_pressed): 80 | self.clear_all() 81 | 82 | consumer_key = None 83 | for key in keys_pressed: 84 | if isinstance(key, ConsumerKey): 85 | consumer_key = key 86 | break 87 | 88 | reporting_device = self.report_device[0] 89 | needed_reporting_device = HIDReportTypes.KEYBOARD 90 | 91 | if consumer_key: 92 | needed_reporting_device = HIDReportTypes.CONSUMER 93 | 94 | if reporting_device != needed_reporting_device: 95 | # If we are about to change reporting devices, release 96 | # all keys and close our proverbial tab on the existing 97 | # device, or keys will get stuck (mostly when releasing 98 | # media/consumer keys) 99 | self.send() 100 | 101 | self.report_device[0] = needed_reporting_device 102 | 103 | if consumer_key: 104 | self.add_key(consumer_key) 105 | else: 106 | for key in keys_pressed: 107 | if key.code >= FIRST_KMK_INTERNAL_KEY: 108 | continue 109 | 110 | if isinstance(key, ModifierKey): 111 | self.add_modifier(key) 112 | else: 113 | self.add_key(key) 114 | 115 | if key.has_modifiers: 116 | for mod in key.has_modifiers: 117 | self.add_modifier(mod) 118 | 119 | return self 120 | 121 | def hid_send(self, evt): 122 | # Don't raise a NotImplementedError so this can serve as our "dummy" HID 123 | # when MCU/board doesn't define one to use (which should almost always be 124 | # the CircuitPython-targeting one, except when unit testing or doing 125 | # something truly bizarre. This will likely change eventually when Bluetooth 126 | # is added) 127 | pass 128 | 129 | def send(self): 130 | if self._evt != self._prev_evt: 131 | self._prev_evt[:] = self._evt 132 | self.hid_send(self._evt) 133 | 134 | return self 135 | 136 | def clear_all(self): 137 | for idx, _ in enumerate(self.report_keys): 138 | self.report_keys[idx] = 0x00 139 | 140 | return self 141 | 142 | def clear_non_modifiers(self): 143 | for idx, _ in enumerate(self.report_non_mods): 144 | self.report_non_mods[idx] = 0x00 145 | 146 | return self 147 | 148 | def add_modifier(self, modifier): 149 | if isinstance(modifier, ModifierKey): 150 | if modifier.code == ModifierKey.FAKE_CODE: 151 | for mod in modifier.has_modifiers: 152 | self.report_mods[0] |= mod 153 | else: 154 | self.report_mods[0] |= modifier.code 155 | else: 156 | self.report_mods[0] |= modifier 157 | 158 | return self 159 | 160 | def remove_modifier(self, modifier): 161 | if isinstance(modifier, ModifierKey): 162 | if modifier.code == ModifierKey.FAKE_CODE: 163 | for mod in modifier.has_modifiers: 164 | self.report_mods[0] ^= mod 165 | else: 166 | self.report_mods[0] ^= modifier.code 167 | else: 168 | self.report_mods[0] ^= modifier 169 | 170 | return self 171 | 172 | def add_key(self, key): 173 | # Try to find the first empty slot in the key report, and fill it 174 | placed = False 175 | 176 | where_to_place = self.report_non_mods 177 | 178 | if self.report_device[0] == HIDReportTypes.CONSUMER: 179 | where_to_place = self.report_keys 180 | 181 | for idx, _ in enumerate(where_to_place): 182 | if where_to_place[idx] == 0x00: 183 | where_to_place[idx] = key.code 184 | placed = True 185 | break 186 | 187 | if not placed: 188 | # TODO what do we do here?...... 189 | pass 190 | 191 | return self 192 | 193 | def remove_key(self, key): 194 | where_to_place = self.report_non_mods 195 | 196 | if self.report_device[0] == HIDReportTypes.CONSUMER: 197 | where_to_place = self.report_keys 198 | 199 | for idx, _ in enumerate(where_to_place): 200 | if where_to_place[idx] == key.code: 201 | where_to_place[idx] = 0x00 202 | 203 | return self 204 | 205 | 206 | class USBHID(AbstractHID): 207 | REPORT_BYTES = 9 208 | 209 | def post_init(self): 210 | self.devices = {} 211 | 212 | for device in usb_hid.devices: 213 | us = device.usage 214 | up = device.usage_page 215 | 216 | if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER: 217 | self.devices[HIDReportTypes.CONSUMER] = device 218 | continue 219 | 220 | if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD: 221 | self.devices[HIDReportTypes.KEYBOARD] = device 222 | continue 223 | 224 | if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE: 225 | self.devices[HIDReportTypes.MOUSE] = device 226 | continue 227 | 228 | if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL: 229 | self.devices[HIDReportTypes.SYSCONTROL] = device 230 | continue 231 | 232 | def hid_send(self, evt): 233 | if not supervisor.runtime.usb_connected: 234 | return 235 | 236 | # int, can be looked up in HIDReportTypes 237 | reporting_device_const = evt[0] 238 | 239 | return self.devices[reporting_device_const].send_report( 240 | evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1] 241 | ) 242 | 243 | 244 | class BLEHID(AbstractHID): 245 | BLE_APPEARANCE_HID_KEYBOARD = const(961) 246 | # Hardcoded in CPy 247 | MAX_CONNECTIONS = const(2) 248 | 249 | def __init__(self, ble_name=str(getmount('/').label), **kwargs): 250 | self.ble_name = ble_name 251 | super().__init__() 252 | 253 | def post_init(self): 254 | self.ble = BLERadio() 255 | self.ble.name = self.ble_name 256 | self.hid = HIDService() 257 | self.hid.protocol_mode = 0 # Boot protocol 258 | 259 | # Security-wise this is not right. While you're away someone turns 260 | # on your keyboard and they can pair with it nice and clean and then 261 | # listen to keystrokes. 262 | # On the other hand we don't have LESC so it's like shouting your 263 | # keystrokes in the air 264 | if not self.ble.connected or not self.hid.devices: 265 | self.start_advertising() 266 | 267 | @property 268 | def devices(self): 269 | '''Search through the provided list of devices to find the ones with the 270 | send_report attribute.''' 271 | if not self.ble.connected: 272 | return {} 273 | 274 | result = {} 275 | 276 | for device in self.hid.devices: 277 | if not hasattr(device, 'send_report'): 278 | continue 279 | us = device.usage 280 | up = device.usage_page 281 | 282 | if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER: 283 | result[HIDReportTypes.CONSUMER] = device 284 | continue 285 | 286 | if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD: 287 | result[HIDReportTypes.KEYBOARD] = device 288 | continue 289 | 290 | if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE: 291 | result[HIDReportTypes.MOUSE] = device 292 | continue 293 | 294 | if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL: 295 | result[HIDReportTypes.SYSCONTROL] = device 296 | continue 297 | 298 | return result 299 | 300 | def hid_send(self, evt): 301 | if not self.ble.connected: 302 | return 303 | 304 | # int, can be looked up in HIDReportTypes 305 | reporting_device_const = evt[0] 306 | 307 | device = self.devices[reporting_device_const] 308 | 309 | report_size = len(device._characteristic.value) 310 | while len(evt) < report_size + 1: 311 | evt.append(0) 312 | 313 | return device.send_report(evt[1 : report_size + 1]) 314 | 315 | def clear_bonds(self): 316 | import _bleio 317 | 318 | _bleio.adapter.erase_bonding() 319 | 320 | def start_advertising(self): 321 | if not self.ble.advertising: 322 | advertisement = ProvideServicesAdvertisement(self.hid) 323 | advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD 324 | 325 | self.ble.start_advertising(advertisement) 326 | 327 | def stop_advertising(self): 328 | self.ble.stop_advertising() 329 | -------------------------------------------------------------------------------- /code/kmk/modules/encoder.py: -------------------------------------------------------------------------------- 1 | # See docs/encoder.md for how to use 2 | 3 | import busio 4 | import digitalio 5 | from supervisor import ticks_ms 6 | 7 | from kmk.modules import Module 8 | 9 | # NB : not using rotaryio as it requires the pins to be consecutive 10 | 11 | 12 | class BaseEncoder: 13 | 14 | VELOCITY_MODE = True 15 | 16 | def __init__(self, is_inverted=False, divisor=4): 17 | 18 | self.is_inverted = is_inverted 19 | self.divisor = divisor 20 | 21 | self._state = None 22 | self._start_state = None 23 | self._direction = None 24 | self._pos = 0 25 | self._button_state = True 26 | self._button_held = None 27 | self._velocity = 0 28 | 29 | self._movement = 0 30 | self._timestamp = ticks_ms() 31 | 32 | # callback functions on events. Need to be defined externally 33 | self.on_move_do = None 34 | self.on_button_do = None 35 | 36 | def get_state(self): 37 | return { 38 | 'direction': self.is_inverted and -self._direction or self._direction, 39 | 'position': self.is_inverted and -self._pos or self._pos, 40 | 'is_pressed': not self._button_state, 41 | 'velocity': self._velocity, 42 | } 43 | 44 | # Called in a loop to refresh encoder state 45 | 46 | def update_state(self): 47 | # Rotation events 48 | new_state = (self.pin_a.get_value(), self.pin_b.get_value()) 49 | 50 | if new_state != self._state: 51 | # encoder moved 52 | self._movement += 1 53 | # false / false and true / true are common half steps 54 | # looking on the step just before helps determining 55 | # the direction 56 | if new_state[0] == new_state[1] and self._state[0] != self._state[1]: 57 | if new_state[1] == self._state[0]: 58 | self._direction = 1 59 | else: 60 | self._direction = -1 61 | 62 | # when the encoder settles on a position (every 2 steps) 63 | if new_state[0] == new_state[1]: 64 | # an encoder returned to the previous 65 | # position halfway, cancel rotation 66 | if ( 67 | self._start_state[0] == new_state[0] 68 | and self._start_state[1] == new_state[1] 69 | and self._movement <= 2 70 | ): 71 | self._movement = 0 72 | self._direction = 0 73 | 74 | # when the encoder made a full loop according to its divisor 75 | elif self._movement >= self.divisor - 1: 76 | # 1 full step is 4 movements (2 for high-resolution encoder), 77 | # however, when rotated quickly, some steps may be missed. 78 | # This makes it behave more naturally 79 | real_movement = self._movement // self.divisor 80 | self._pos += self._direction * real_movement 81 | if self.on_move_do is not None: 82 | for i in range(real_movement): 83 | self.on_move_do(self.get_state()) 84 | 85 | # Rotation finished, reset to identify new movement 86 | self._movement = 0 87 | self._direction = 0 88 | self._start_state = new_state 89 | 90 | self._state = new_state 91 | 92 | # Velocity 93 | self.velocity_event() 94 | 95 | # Button event 96 | self.button_event() 97 | 98 | def velocity_event(self): 99 | if self.VELOCITY_MODE: 100 | new_timestamp = ticks_ms() 101 | self._velocity = new_timestamp - self._timestamp 102 | self._timestamp = new_timestamp 103 | 104 | def button_event(self): 105 | raise NotImplementedError('subclasses must override button_event()!') 106 | 107 | # return knob velocity as milliseconds between position changes (detents) 108 | # for backwards compatibility 109 | def vel_report(self): 110 | # print(self._velocity) 111 | return self._velocity 112 | 113 | 114 | class GPIOEncoder(BaseEncoder): 115 | def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False, divisor=None): 116 | super().__init__(is_inverted) 117 | 118 | # Divisor can be 4 or 2 depending on whether the detent 119 | # on the encoder is defined by 2 or 4 pulses 120 | self.divisor = divisor 121 | 122 | self.pin_a = EncoderPin(pin_a) 123 | self.pin_b = EncoderPin(pin_b) 124 | self.pin_button = ( 125 | EncoderPin(pin_button, button_type=True) if pin_button is not None else None 126 | ) 127 | 128 | self._state = (self.pin_a.get_value(), self.pin_b.get_value()) 129 | self._start_state = self._state 130 | 131 | def button_event(self): 132 | if self.pin_button: 133 | new_button_state = self.pin_button.get_value() 134 | if new_button_state != self._button_state: 135 | self._button_state = new_button_state 136 | if self.on_button_do is not None: 137 | self.on_button_do(self.get_state()) 138 | 139 | 140 | class EncoderPin: 141 | def __init__(self, pin, button_type=False): 142 | self.pin = pin 143 | self.button_type = button_type 144 | self.prepare_pin() 145 | 146 | def prepare_pin(self): 147 | if self.pin is not None: 148 | self.io = digitalio.DigitalInOut(self.pin) 149 | self.io.direction = digitalio.Direction.INPUT 150 | self.io.pull = digitalio.Pull.UP 151 | else: 152 | self.io = None 153 | 154 | def get_value(self): 155 | return self.io.value 156 | 157 | 158 | class I2CEncoder(BaseEncoder): 159 | def __init__(self, i2c, address, is_inverted=False): 160 | 161 | try: 162 | from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw 163 | except ImportError: 164 | print('seesaw missing') 165 | return 166 | 167 | super().__init__(is_inverted) 168 | 169 | self.seesaw = seesaw.Seesaw(i2c, address) 170 | 171 | # Check for correct product 172 | 173 | seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF 174 | if seesaw_product != 4991: 175 | print('Wrong firmware loaded? Expected 4991') 176 | 177 | self.encoder = rotaryio.IncrementalEncoder(self.seesaw) 178 | self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP) 179 | self.switch = digitalio.DigitalIO(self.seesaw, 24) 180 | self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1) 181 | 182 | self._state = self.encoder.position 183 | 184 | def update_state(self): 185 | 186 | # Rotation events 187 | new_state = self.encoder.position 188 | if new_state != self._state: 189 | # it moves ! 190 | self._movement += 1 191 | # false / false and true / true are common half steps 192 | # looking on the step just before helps determining 193 | # the direction 194 | if self.encoder.position > self._state: 195 | self._direction = 1 196 | else: 197 | self._direction = -1 198 | self._state = new_state 199 | self.on_move_do(self.get_state()) 200 | 201 | # Velocity 202 | self.velocity_event() 203 | 204 | # Button events 205 | self.button_event() 206 | 207 | def button_event(self): 208 | if not self.switch.value and not self._button_held: 209 | # Pressed 210 | self._button_held = True 211 | if self.on_button_do is not None: 212 | self.on_button_do(self.get_state()) 213 | 214 | if self.switch.value and self._button_held: 215 | # Released 216 | self._button_held = False 217 | 218 | def get_state(self): 219 | return { 220 | 'direction': self.is_inverted and -self._direction or self._direction, 221 | 'position': self._state, 222 | 'is_pressed': not self.switch.value, 223 | 'is_held': self._button_held, 224 | 'velocity': self._velocity, 225 | } 226 | 227 | 228 | class EncoderHandler(Module): 229 | def __init__(self): 230 | self.encoders = [] 231 | self.pins = None 232 | self.map = None 233 | self.divisor = 4 234 | 235 | def on_runtime_enable(self, keyboard): 236 | return 237 | 238 | def on_runtime_disable(self, keyboard): 239 | return 240 | 241 | def during_bootup(self, keyboard): 242 | if self.pins and self.map: 243 | for idx, pins in enumerate(self.pins): 244 | try: 245 | # Check for busio.I2C 246 | if isinstance(pins[0], busio.I2C): 247 | new_encoder = I2CEncoder(*pins) 248 | 249 | # Else fall back to GPIO 250 | else: 251 | new_encoder = GPIOEncoder(*pins) 252 | # Set default divisor if unset 253 | if new_encoder.divisor is None: 254 | new_encoder.divisor = self.divisor 255 | 256 | # In our case, we need to define keybord and encoder_id for callbacks 257 | new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do( 258 | keyboard, bound_idx, x 259 | ) 260 | new_encoder.on_button_do = ( 261 | lambda x, bound_idx=idx: self.on_button_do( 262 | keyboard, bound_idx, x 263 | ) 264 | ) 265 | self.encoders.append(new_encoder) 266 | except Exception as e: 267 | print(e) 268 | return 269 | 270 | def on_move_do(self, keyboard, encoder_id, state): 271 | if self.map: 272 | layer_id = keyboard.active_layers[0] 273 | # if Left, key index 0 else key index 1 274 | if state['direction'] == -1: 275 | key_index = 0 276 | else: 277 | key_index = 1 278 | key = self.map[layer_id][encoder_id][key_index] 279 | keyboard.tap_key(key) 280 | 281 | def on_button_do(self, keyboard, encoder_id, state): 282 | if state['is_pressed'] is True: 283 | layer_id = keyboard.active_layers[0] 284 | key = self.map[layer_id][encoder_id][2] 285 | keyboard.tap_key(key) 286 | 287 | def before_matrix_scan(self, keyboard): 288 | ''' 289 | Return value will be injected as an extra matrix update 290 | ''' 291 | for encoder in self.encoders: 292 | encoder.update_state() 293 | 294 | return keyboard 295 | 296 | def after_matrix_scan(self, keyboard): 297 | ''' 298 | Return value will be replace matrix update if supplied 299 | ''' 300 | return 301 | 302 | def before_hid_send(self, keyboard): 303 | return 304 | 305 | def after_hid_send(self, keyboard): 306 | return 307 | 308 | def on_powersave_enable(self, keyboard): 309 | return 310 | 311 | def on_powersave_disable(self, keyboard): 312 | return 313 | -------------------------------------------------------------------------------- /code/kmk/modules/pimoroni_trackball.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Extension handles usage of Trackball Breakout by Pimoroni 3 | Product page: https://shop.pimoroni.com/products/trackball-breakout 4 | ''' 5 | from micropython import const 6 | 7 | import math 8 | import struct 9 | 10 | from kmk.keys import make_argumented_key, make_key 11 | from kmk.kmktime import PeriodicTimer 12 | from kmk.modules import Module 13 | from kmk.modules.mouse_keys import PointingDevice 14 | 15 | I2C_ADDRESS = 0x0A 16 | I2C_ADDRESS_ALTERNATIVE = 0x0B 17 | 18 | CHIP_ID = 0xBA11 19 | VERSION = 1 20 | 21 | REG_LED_RED = 0x00 22 | REG_LED_GRN = 0x01 23 | REG_LED_BLU = 0x02 24 | REG_LED_WHT = 0x03 25 | 26 | REG_LEFT = 0x04 27 | REG_RIGHT = 0x05 28 | REG_UP = 0x06 29 | REG_DOWN = 0x07 30 | REG_SWITCH = 0x08 31 | MSK_SWITCH_STATE = 0b10000000 32 | 33 | REG_USER_FLASH = 0xD0 34 | REG_FLASH_PAGE = 0xF0 35 | REG_INT = 0xF9 36 | MSK_INT_TRIGGERED = 0b00000001 37 | MSK_INT_OUT_EN = 0b00000010 38 | REG_CHIP_ID_L = 0xFA 39 | RED_CHIP_ID_H = 0xFB 40 | REG_VERSION = 0xFC 41 | REG_I2C_ADDR = 0xFD 42 | REG_CTRL = 0xFE 43 | MSK_CTRL_SLEEP = 0b00000001 44 | MSK_CTRL_RESET = 0b00000010 45 | MSK_CTRL_FREAD = 0b00000100 46 | MSK_CTRL_FWRITE = 0b00001000 47 | 48 | ANGLE_OFFSET = 0 49 | 50 | 51 | class TrackballHandlerKeyMeta: 52 | def __init__(self, handler=0): 53 | self.handler = handler 54 | 55 | 56 | def layer_key_validator(handler): 57 | return TrackballHandlerKeyMeta(handler=handler) 58 | 59 | 60 | class TrackballMode: 61 | '''Behaviour mode of trackball: mouse movement or vertical scroll''' 62 | 63 | MOUSE_MODE = const(0) 64 | SCROLL_MODE = const(1) 65 | 66 | 67 | class ScrollDirection: 68 | '''Behaviour mode of scrolling: natural or reverse scrolling''' 69 | 70 | NATURAL = const(0) 71 | REVERSE = const(1) 72 | 73 | 74 | class TrackballHandler: 75 | def handle(self, keyboard, trackball, x, y, switch, state): 76 | raise NotImplementedError 77 | 78 | 79 | class PointingHandler(TrackballHandler): 80 | def handle(self, keyboard, trackball, x, y, switch, state): 81 | if x > 0: 82 | trackball.pointing_device.report_x[0] = x 83 | elif x < 0: 84 | trackball.pointing_device.report_x[0] = 0xFF & x 85 | if y > 0: 86 | trackball.pointing_device.report_y[0] = y 87 | elif y < 0: 88 | trackball.pointing_device.report_y[0] = 0xFF & y 89 | 90 | if x != 0 or y != 0: 91 | trackball.pointing_device.hid_pending = True 92 | 93 | if switch == 1: # Button pressed 94 | trackball.pointing_device.button_status[ 95 | 0 96 | ] |= trackball.pointing_device.MB_LMB 97 | trackball.pointing_device.hid_pending = True 98 | 99 | if not state and trackball.previous_state is True: # Button released 100 | trackball.pointing_device.button_status[ 101 | 0 102 | ] &= ~trackball.pointing_device.MB_LMB 103 | trackball.pointing_device.hid_pending = True 104 | 105 | trackball.previous_state = state 106 | 107 | 108 | class ScrollHandler(TrackballHandler): 109 | def __init__(self, scroll_direction=ScrollDirection.NATURAL): 110 | self.scroll_direction = scroll_direction 111 | 112 | def handle(self, keyboard, trackball, x, y, switch, state): 113 | if self.scroll_direction == ScrollDirection.REVERSE: 114 | y = -y 115 | 116 | if y != 0: 117 | pointing_device = trackball.pointing_device 118 | pointing_device.report_w[0] = 0xFF & y 119 | pointing_device.hid_pending = True 120 | 121 | if switch == 1: # Button pressed 122 | pointing_device.button_status[0] |= pointing_device.MB_LMB 123 | pointing_device.hid_pending = True 124 | 125 | if not state and trackball.previous_state is True: # Button released 126 | pointing_device.button_status[0] &= ~pointing_device.MB_LMB 127 | pointing_device.hid_pending = True 128 | 129 | trackball.previous_state = state 130 | 131 | 132 | class KeyHandler(TrackballHandler): 133 | x = 0 134 | y = 0 135 | 136 | def __init__(self, up, right, down, left, press, axis_snap=0.25, steps=8): 137 | self.up = up 138 | self.right = right 139 | self.down = down 140 | self.left = left 141 | self.press = press 142 | self.axis_snap = axis_snap 143 | self.steps = steps 144 | 145 | def handle(self, keyboard, trackball, x, y, switch, state): 146 | if y and abs(x / y) < self.axis_snap: 147 | x = 0 148 | if x and abs(y / x) < self.axis_snap: 149 | y = 0 150 | 151 | self.x += x 152 | self.y += y 153 | x_taps = self.x // self.steps 154 | y_taps = self.y // self.steps 155 | self.x %= self.steps 156 | self.y %= self.steps 157 | for i in range(x_taps, 0, 1): 158 | keyboard.tap_key(self.left) 159 | for i in range(x_taps, 0, -1): 160 | keyboard.tap_key(self.right) 161 | for i in range(y_taps, 0, 1): 162 | keyboard.tap_key(self.up) 163 | for i in range(y_taps, 0, -1): 164 | keyboard.tap_key(self.down) 165 | if switch and state: 166 | keyboard.tap_key(self.press) 167 | 168 | 169 | class Trackball(Module): 170 | '''Module handles usage of Trackball Breakout by Pimoroni''' 171 | 172 | def __init__( 173 | self, 174 | i2c, 175 | mode=TrackballMode.MOUSE_MODE, 176 | address=I2C_ADDRESS, 177 | angle_offset=ANGLE_OFFSET, 178 | handlers=None, 179 | ): 180 | self.angle_offset = angle_offset 181 | if not handlers: 182 | handlers = [PointingHandler(), ScrollHandler()] 183 | if mode == TrackballMode.SCROLL_MODE: 184 | handlers.reverse() 185 | self._i2c_address = address 186 | self._i2c_bus = i2c 187 | 188 | self.pointing_device = PointingDevice() 189 | self.mode = mode 190 | self.previous_state = False # click state 191 | self.handlers = handlers 192 | self.current_handler = self.handlers[0] 193 | self.polling_interval = 20 194 | 195 | chip_id = struct.unpack('= len(self.handlers): 280 | next_index = 0 281 | self.activate_handler(next_index) 282 | 283 | def _clear_pending_hid(self): 284 | self.pointing_device.hid_pending = False 285 | self.pointing_device.report_x[0] = 0 286 | self.pointing_device.report_y[0] = 0 287 | self.pointing_device.report_w[0] = 0 288 | self.pointing_device.button_status[0] = 0 289 | 290 | def _read_raw_state(self): 291 | '''Read up, down, left, right and switch data from trackball.''' 292 | left, right, up, down, switch = self._i2c_rdwr([REG_LEFT], 5) 293 | switch, switch_state = ( 294 | switch & ~MSK_SWITCH_STATE, 295 | (switch & MSK_SWITCH_STATE) > 0, 296 | ) 297 | return up, down, left, right, switch, switch_state 298 | 299 | def _i2c_rdwr(self, data, length=0): 300 | '''Write and optionally read I2C data.''' 301 | while not self._i2c_bus.try_lock(): 302 | pass 303 | 304 | try: 305 | if length > 0: 306 | result = bytearray(length) 307 | self._i2c_bus.writeto_then_readfrom( 308 | self._i2c_address, bytes(data), result 309 | ) 310 | return list(result) 311 | else: 312 | self._i2c_bus.writeto(self._i2c_address, bytes(data)) 313 | 314 | return [] 315 | 316 | finally: 317 | self._i2c_bus.unlock() 318 | 319 | def _tb_handler_press(self, key, keyboard, *args, **kwargs): 320 | self.activate_handler(key.meta.handler) 321 | 322 | def _tb_handler_next_press(self, key, keyboard, *args, **kwargs): 323 | self.next_handler() 324 | 325 | def _calculate_movement(self, raw_x, raw_y): 326 | '''Calculate accelerated movement vector from raw data''' 327 | if raw_x == 0 and raw_y == 0: 328 | return 0, 0 329 | 330 | var_accel = 1 331 | power = 2.5 332 | 333 | angle_rad = math.atan2(raw_y, raw_x) + self.angle_offset 334 | vector_length = math.sqrt(pow(raw_x, 2) + pow(raw_y, 2)) 335 | vector_length = pow(vector_length * var_accel, power) 336 | x = math.floor(vector_length * math.cos(angle_rad)) 337 | y = math.floor(vector_length * math.sin(angle_rad)) 338 | 339 | limit = 127 # hid size limit 340 | x_clamped = max(min(limit, x), -limit) 341 | y_clamped = max(min(limit, y), -limit) 342 | 343 | return x_clamped, y_clamped 344 | -------------------------------------------------------------------------------- /code/kmk/modules/combos.py: -------------------------------------------------------------------------------- 1 | try: 2 | from typing import Optional, Tuple, Union 3 | except ImportError: 4 | pass 5 | from micropython import const 6 | 7 | import kmk.handlers.stock as handlers 8 | from kmk.keys import Key, make_key 9 | from kmk.kmk_keyboard import KMKKeyboard 10 | from kmk.modules import Module 11 | 12 | 13 | class _ComboState: 14 | RESET = const(0) 15 | MATCHING = const(1) 16 | ACTIVE = const(2) 17 | IDLE = const(3) 18 | 19 | 20 | class Combo: 21 | fast_reset = False 22 | per_key_timeout = False 23 | timeout = 50 24 | _remaining = [] 25 | _timeout = None 26 | _state = _ComboState.IDLE 27 | _match_coord = False 28 | 29 | def __init__( 30 | self, 31 | match: Tuple[Union[Key, int], ...], 32 | result: Key, 33 | fast_reset=None, 34 | per_key_timeout=None, 35 | timeout=None, 36 | match_coord=None, 37 | ): 38 | ''' 39 | match: tuple of keys (KC.A, KC.B) 40 | result: key KC.C 41 | ''' 42 | self.match = match 43 | self.result = result 44 | if fast_reset is not None: 45 | self.fast_reset = fast_reset 46 | if per_key_timeout is not None: 47 | self.per_key_timeout = per_key_timeout 48 | if timeout is not None: 49 | self.timeout = timeout 50 | if match_coord is not None: 51 | self._match_coord = match_coord 52 | 53 | def __repr__(self): 54 | return f'{self.__class__.__name__}({[k.code for k in self.match]})' 55 | 56 | def matches(self, key: Key, int_coord: int): 57 | raise NotImplementedError 58 | 59 | def has_match(self, key: Key, int_coord: int): 60 | return self._match_coord and int_coord in self.match or key in self.match 61 | 62 | def insert(self, key: Key, int_coord: int): 63 | if self._match_coord: 64 | self._remaining.insert(0, int_coord) 65 | else: 66 | self._remaining.insert(0, key) 67 | 68 | def reset(self): 69 | self._remaining = list(self.match) 70 | 71 | 72 | class Chord(Combo): 73 | def matches(self, key: Key, int_coord: int): 74 | if not self._match_coord and key in self._remaining: 75 | self._remaining.remove(key) 76 | return True 77 | elif self._match_coord and int_coord in self._remaining: 78 | self._remaining.remove(int_coord) 79 | return True 80 | else: 81 | return False 82 | 83 | 84 | class Sequence(Combo): 85 | fast_reset = True 86 | per_key_timeout = True 87 | timeout = 1000 88 | 89 | def matches(self, key: Key, int_coord: int): 90 | if ( 91 | not self._match_coord and self._remaining and self._remaining[0] == key 92 | ) or ( 93 | self._match_coord and self._remaining and self._remaining[0] == int_coord 94 | ): 95 | self._remaining.pop(0) 96 | return True 97 | else: 98 | return False 99 | 100 | 101 | class Combos(Module): 102 | def __init__(self, combos=[]): 103 | self.combos = combos 104 | self._key_buffer = [] 105 | 106 | make_key( 107 | names=('LEADER', 'LDR'), 108 | on_press=handlers.passthrough, 109 | on_release=handlers.passthrough, 110 | ) 111 | 112 | def during_bootup(self, keyboard): 113 | self.reset(keyboard) 114 | 115 | def before_matrix_scan(self, keyboard): 116 | return 117 | 118 | def after_matrix_scan(self, keyboard): 119 | return 120 | 121 | def before_hid_send(self, keyboard): 122 | return 123 | 124 | def after_hid_send(self, keyboard): 125 | return 126 | 127 | def on_powersave_enable(self, keyboard): 128 | return 129 | 130 | def on_powersave_disable(self, keyboard): 131 | return 132 | 133 | def process_key(self, keyboard, key: Key, is_pressed, int_coord): 134 | if is_pressed: 135 | return self.on_press(keyboard, key, int_coord) 136 | else: 137 | return self.on_release(keyboard, key, int_coord) 138 | 139 | def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]): 140 | # refill potential matches from timed-out matches 141 | if self.count_matching() == 0: 142 | for combo in self.combos: 143 | if combo._state == _ComboState.RESET: 144 | combo._state = _ComboState.MATCHING 145 | 146 | # filter potential matches 147 | for combo in self.combos: 148 | if combo._state != _ComboState.MATCHING: 149 | continue 150 | if combo.matches(key, int_coord): 151 | continue 152 | combo._state = _ComboState.IDLE 153 | if combo._timeout: 154 | keyboard.cancel_timeout(combo._timeout) 155 | combo._timeout = keyboard.set_timeout( 156 | combo.timeout, lambda c=combo: self.reset_combo(keyboard, c) 157 | ) 158 | 159 | match_count = self.count_matching() 160 | 161 | if match_count: 162 | # At least one combo matches current key: append key to buffer. 163 | self._key_buffer.append((int_coord, key, True)) 164 | key = None 165 | 166 | for first_match in self.combos: 167 | if first_match._state == _ComboState.MATCHING: 168 | break 169 | 170 | # Single match left: don't wait on timeout to activate 171 | if match_count == 1 and not any(first_match._remaining): 172 | combo = first_match 173 | self.activate(keyboard, combo) 174 | if combo._timeout: 175 | keyboard.cancel_timeout(combo._timeout) 176 | combo._timeout = None 177 | self._key_buffer = [] 178 | self.reset(keyboard) 179 | 180 | # Start or reset individual combo timeouts. 181 | for combo in self.combos: 182 | if combo._state != _ComboState.MATCHING: 183 | continue 184 | if combo._timeout: 185 | if combo.per_key_timeout: 186 | keyboard.cancel_timeout(combo._timeout) 187 | else: 188 | continue 189 | combo._timeout = keyboard.set_timeout( 190 | combo.timeout, lambda c=combo: self.on_timeout(keyboard, c) 191 | ) 192 | else: 193 | # There's no matching combo: send and reset key buffer 194 | if self._key_buffer: 195 | self._key_buffer.append((int_coord, key, True)) 196 | self.send_key_buffer(keyboard) 197 | self._key_buffer = [] 198 | key = None 199 | 200 | return key 201 | 202 | def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]): 203 | for combo in self.combos: 204 | if combo._state != _ComboState.ACTIVE: 205 | continue 206 | if combo.has_match(key, int_coord): 207 | # Deactivate combo if it matches current key. 208 | self.deactivate(keyboard, combo) 209 | 210 | if combo.fast_reset: 211 | self.reset_combo(keyboard, combo) 212 | self._key_buffer = [] 213 | else: 214 | combo.insert(key, int_coord) 215 | combo._state = _ComboState.MATCHING 216 | 217 | key = combo.result 218 | break 219 | 220 | else: 221 | # Non-active but matching combos can either activate on key release 222 | # if they're the only match, or "un-match" the released key but stay 223 | # matching if they're a repeatable combo. 224 | for combo in self.combos: 225 | if combo._state != _ComboState.MATCHING: 226 | continue 227 | if not combo.has_match(key, int_coord): 228 | continue 229 | 230 | # Combo matches, but first key released before timeout. 231 | elif not any(combo._remaining) and self.count_matching() == 1: 232 | keyboard.cancel_timeout(combo._timeout) 233 | self.activate(keyboard, combo) 234 | self._key_buffer = [] 235 | keyboard._send_hid() 236 | self.deactivate(keyboard, combo) 237 | if combo.fast_reset: 238 | self.reset_combo(keyboard, combo) 239 | else: 240 | combo.insert(key, int_coord) 241 | combo._state = _ComboState.MATCHING 242 | self.reset(keyboard) 243 | 244 | elif not any(combo._remaining): 245 | continue 246 | 247 | # Skip combos that allow tapping. 248 | elif combo.fast_reset: 249 | continue 250 | 251 | # This was the last key released of a repeatable combo. 252 | elif len(combo._remaining) == len(combo.match) - 1: 253 | self.reset_combo(keyboard, combo) 254 | if not self.count_matching(): 255 | self._key_buffer.append((int_coord, key, False)) 256 | self.send_key_buffer(keyboard) 257 | self._key_buffer = [] 258 | key = None 259 | 260 | # Anything between first and last key released. 261 | else: 262 | combo.insert(key, int_coord) 263 | 264 | # Don't propagate key-release events for keys that have been 265 | # buffered. Append release events only if corresponding press is in 266 | # buffer. 267 | pressed = self._key_buffer.count((int_coord, key, True)) 268 | released = self._key_buffer.count((int_coord, key, False)) 269 | if (pressed - released) > 0: 270 | self._key_buffer.append((int_coord, key, False)) 271 | key = None 272 | 273 | # Reset on non-combo key up 274 | if not self.count_matching(): 275 | self.reset(keyboard) 276 | 277 | return key 278 | 279 | def on_timeout(self, keyboard, combo): 280 | # If combo reaches timeout and has no remaining keys, activate it; 281 | # else, drop it from the match list. 282 | combo._timeout = None 283 | 284 | if not any(combo._remaining): 285 | self.activate(keyboard, combo) 286 | # check if the last buffered key event was a 'release' 287 | if not self._key_buffer[-1][2]: 288 | keyboard._send_hid() 289 | self.deactivate(keyboard, combo) 290 | self._key_buffer = [] 291 | self.reset(keyboard) 292 | else: 293 | if self.count_matching() == 1: 294 | # This was the last pending combo: flush key buffer. 295 | self.send_key_buffer(keyboard) 296 | self._key_buffer = [] 297 | self.reset_combo(keyboard, combo) 298 | 299 | def send_key_buffer(self, keyboard): 300 | for (int_coord, key, is_pressed) in self._key_buffer: 301 | keyboard.resume_process_key(self, key, is_pressed, int_coord) 302 | 303 | def activate(self, keyboard, combo): 304 | combo.result.on_press(keyboard) 305 | combo._state = _ComboState.ACTIVE 306 | 307 | def deactivate(self, keyboard, combo): 308 | combo.result.on_release(keyboard) 309 | combo._state = _ComboState.IDLE 310 | 311 | def reset_combo(self, keyboard, combo): 312 | combo.reset() 313 | if combo._timeout is not None: 314 | keyboard.cancel_timeout(combo._timeout) 315 | combo._timeout = None 316 | combo._state = _ComboState.RESET 317 | 318 | def reset(self, keyboard): 319 | for combo in self.combos: 320 | if combo._state != _ComboState.ACTIVE: 321 | self.reset_combo(keyboard, combo) 322 | 323 | def count_matching(self): 324 | match_count = 0 325 | for combo in self.combos: 326 | if combo._state == _ComboState.MATCHING: 327 | match_count += 1 328 | return match_count 329 | --------------------------------------------------------------------------------