|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +import time |
| 6 | +import board |
| 7 | +import keypad |
| 8 | +import supervisor |
| 9 | +import usb_hid |
| 10 | +from adafruit_hid.keyboard import Keyboard |
| 11 | +from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS |
| 12 | + |
| 13 | +# Dictionary of macros for single keys and combinations |
| 14 | +macros = { |
| 15 | + # Single key macros |
| 16 | + (0,): "good", |
| 17 | + (1,): "great", |
| 18 | + (2,): "nice", |
| 19 | + (3,): "awesome", |
| 20 | + (4,): "cool", |
| 21 | + |
| 22 | + # Combination macros |
| 23 | + (0, 2, 4): "looks good to me", |
| 24 | + (0, 2): "be right back", |
| 25 | + (2, 4): "see you soon", |
| 26 | + (1, 3): "sounds good", |
| 27 | +} |
| 28 | + |
| 29 | +KEY_PINS = ( |
| 30 | + board.A1, |
| 31 | + board.A2, |
| 32 | + board.A3, |
| 33 | + board.MISO, |
| 34 | + board.MOSI, |
| 35 | +) |
| 36 | + |
| 37 | +keys = keypad.Keys( |
| 38 | + KEY_PINS, |
| 39 | + value_when_pressed=False, |
| 40 | + pull=True, |
| 41 | + interval=0.01, |
| 42 | + max_events=64, |
| 43 | + debounce_threshold=3 |
| 44 | +) |
| 45 | + |
| 46 | +keyboard = Keyboard(usb_hid.devices) |
| 47 | +keyboard_layout = KeyboardLayoutUS(keyboard) |
| 48 | + |
| 49 | +# need to wait longer for 3 key combos |
| 50 | +LARGER_COMBOS = { |
| 51 | + (0, 2): (0, 2, 4), |
| 52 | + (2, 4): (0, 2, 4), |
| 53 | +} |
| 54 | + |
| 55 | +# How long to wait for possible additional keys in a combo (ms) |
| 56 | +COMBO_WAIT_TIME = 150 # Wait 150ms to see if more keys are coming |
| 57 | + |
| 58 | +# How long to wait for a single key before executing (ms) |
| 59 | +SINGLE_KEY_TIMEOUT_MS = 80 |
| 60 | + |
| 61 | +# Minimum time between macro executions (ms) |
| 62 | +MACRO_COOLDOWN_MS = 300 |
| 63 | + |
| 64 | +# Store the current state of all keys |
| 65 | +key_states = {i: False for i in range(len(KEY_PINS))} |
| 66 | + |
| 67 | +# Create a reusable Event object to avoid memory allocations |
| 68 | +reusable_event = keypad.Event() |
| 69 | + |
| 70 | +# Track timing and state |
| 71 | +last_macro_time = 0 |
| 72 | +key_combo_start_time = 0 |
| 73 | +waiting_for_combo = False |
| 74 | +last_executed_combo = None |
| 75 | + |
| 76 | +while True: |
| 77 | + # Process all events in the queue |
| 78 | + keys_changed = False |
| 79 | + |
| 80 | + while keys.events: |
| 81 | + if keys.events.get_into(reusable_event): |
| 82 | + # Check if key state actually changed |
| 83 | + old_state = key_states[reusable_event.key_number] |
| 84 | + key_states[reusable_event.key_number] = reusable_event.pressed |
| 85 | + |
| 86 | + if old_state != reusable_event.pressed: |
| 87 | + print(f"Key {reusable_event.key_number} " + |
| 88 | + f"{'pressed' if reusable_event.pressed else 'released'}") |
| 89 | + keys_changed = True |
| 90 | + |
| 91 | + # Get currently pressed keys as a sorted tuple |
| 92 | + current_pressed_keys = tuple(sorted(k for k, v in key_states.items() if v)) |
| 93 | + current_time = supervisor.ticks_ms() |
| 94 | + |
| 95 | + # When all keys are released, reset tracking |
| 96 | + if not current_pressed_keys: |
| 97 | + waiting_for_combo = False |
| 98 | + last_executed_combo = None |
| 99 | + time.sleep(0.01) |
| 100 | + continue |
| 101 | + |
| 102 | + # If this is a new key pattern or we just started |
| 103 | + if keys_changed: |
| 104 | + # If we weren't tracking before, start now |
| 105 | + if not waiting_for_combo: |
| 106 | + key_combo_start_time = current_time |
| 107 | + waiting_for_combo = True |
| 108 | + |
| 109 | + # If the pressed keys have changed, update the timer |
| 110 | + if current_pressed_keys != last_executed_combo: |
| 111 | + key_combo_start_time = current_time |
| 112 | + |
| 113 | + # Skip if we've already executed this exact combination |
| 114 | + if current_pressed_keys == last_executed_combo: |
| 115 | + time.sleep(0.01) |
| 116 | + continue |
| 117 | + |
| 118 | + # Determine if we should execute a macro now |
| 119 | + should_execute = False |
| 120 | + wait_more = False |
| 121 | + |
| 122 | + # If this is a potential part of a larger combo, wait longer |
| 123 | + if current_pressed_keys in LARGER_COMBOS: |
| 124 | + # Only wait if we've been waiting less than the combo wait time |
| 125 | + if (current_time - key_combo_start_time) < COMBO_WAIT_TIME: |
| 126 | + wait_more = True |
| 127 | + else: |
| 128 | + # We've waited long enough, go ahead and execute |
| 129 | + should_execute = True |
| 130 | + # Immediate execution for multi-key combinations that aren't potential parts of larger combos |
| 131 | + elif len(current_pressed_keys) > 1: |
| 132 | + should_execute = True |
| 133 | + # Execute single key after timeout |
| 134 | + elif waiting_for_combo and (current_time - key_combo_start_time) >= SINGLE_KEY_TIMEOUT_MS: |
| 135 | + should_execute = True |
| 136 | + |
| 137 | + # If we need to wait more, skip to the next iteration |
| 138 | + if wait_more: |
| 139 | + time.sleep(0.01) |
| 140 | + continue |
| 141 | + |
| 142 | + # Execute the macro if conditions are met |
| 143 | + if should_execute and current_pressed_keys in macros: |
| 144 | + # Only execute if cooldown period has passed |
| 145 | + if current_time - last_macro_time >= MACRO_COOLDOWN_MS: |
| 146 | + print(f"MACRO: {macros[current_pressed_keys]}") |
| 147 | + keyboard_layout.write(macros[current_pressed_keys]) |
| 148 | + last_macro_time = current_time |
| 149 | + last_executed_combo = current_pressed_keys |
| 150 | + |
| 151 | + time.sleep(0.01) |
0 commit comments