""" `snakeswitch` ============= CircuitPython implementation of a foot-switch and NeoTrellis device for triggering keyboard events. See code.py for a demo of the usage. * Author(s): Brennen Bearnes """ # pylint: disable=import-error import time import board import digitalio import busio import neopixel from adafruit_hid.keyboard import Keyboard from adafruit_neotrellis.neotrellis import NeoTrellis # pylint: enable=import-error COLOR_ACTIVE_LAYOUT = (40, 20, 0) COLOR_INACTIVE_LAYOUT = (5, 0, 5) COLOR_PRESSED = (80, 40, 0) COLOR_TRIGGER = (0, 5, 0) COLOR_UNDEFINED = (0, 0, 0) BOUNCE_SECS = 0.250 NEOTRELLIS_COUNT = 16 def get_keys_for_switch(switch, current_layouts): """ Return a list of keycodes that are triggered by a switch in the given set of layouts. """ keys = [] for layout in current_layouts: if switch in layout: if isinstance(layout[switch], tuple): keys.extend(layout[switch]) else: keys.append(layout[switch]) return keys def diag(*message): """Print a diagnostic message to the console.""" print(time.monotonic(), message) class SnakeSwitch: """ Class modeling a SnakeSwitch. Expects a tuple listing the pins that switches are connected to, and a dictionary of layouts - see switch_config.py for a sample configuration. """ def __init__(self, switch_pins, layouts): i2c_bus = busio.I2C(board.SCL, board.SDA) self.trellis = NeoTrellis(i2c_bus) self.kbd = Keyboard() diag('initializing pins', switch_pins) self.switch_inputs = tuple(digitalio.DigitalInOut(pin) for pin in switch_pins) for switch_input in self.switch_inputs: switch_input.switch_to_input(pull=digitalio.Pull.UP) self.layouts = layouts self.active_layouts = {} self.active_switches = {} for i in range(NEOTRELLIS_COUNT): # Mark any mapped to each button as currently inactive: self.active_layouts[i] = False # Activate rising / falling edge events on all keys, set up callback: self.trellis.activate_key(i, NeoTrellis.EDGE_RISING) self.trellis.activate_key(i, NeoTrellis.EDGE_FALLING) self.trellis.callbacks[i] = self.handle_neotrellis_event # Activate first defined layout that isn't just a key trigger: for layout_id, layout in self.layouts.items(): if isinstance(layout, dict): self.toggle_layout(layout_id) break self.paint_trellis() # Turn off NeoPixel on the Feather M4: onboard_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1) onboard_pixel[0] = (0, 0, 0) def handle_neotrellis_event(self, event): """A callback for handling NeoTrelllis rising/falling edges.""" if event.edge == NeoTrellis.EDGE_RISING: # Highlight the currently pressed button: self.trellis.pixels[event.number] = COLOR_PRESSED elif event.edge == NeoTrellis.EDGE_FALLING: # On release, activate a button if it has a defined layout (or fire # any defined key events): if event.number in self.layouts: action_for_button = self.layouts[event.number] if isinstance(action_for_button, int): # It's a key code, press it: diag("pressing and releasing keycode: ", action_for_button) self.kbd.press(self.layouts[event.number]) self.kbd.release_all() elif isinstance(action_for_button, tuple): # It's a tuple with multiple key codes - fire them all: diag("pressing and releasing keycode combo: ", action_for_button) for k in action_for_button: self.kbd.press(k) self.kbd.release_all() else: # Toggle a layout: self.toggle_layout(event.number) self.paint_trellis() def toggle_layout(self, layout_id): """Toggle the state of a given layout.""" diag('switching layout: ', layout_id) self.active_layouts[layout_id] = not self.active_layouts[layout_id] def paint_trellis(self): """Set state of trellis LEDs based on defined and active layouts.""" for layout_button, active in self.active_layouts.items(): if active: # Layout is currently activated: self.trellis.pixels[layout_button] = COLOR_ACTIVE_LAYOUT elif layout_button in self.layouts: if isinstance(self.layouts[layout_button], (tuple, int)): # Button is actually a keycode or set of keycodes to fire on press: self.trellis.pixels[layout_button] = COLOR_TRIGGER else: # Layout is defined, but deactivated: self.trellis.pixels[layout_button] = COLOR_INACTIVE_LAYOUT else: # Layout isn't defined in the config file: self.trellis.pixels[layout_button] = COLOR_UNDEFINED def advance_frame(self): """ Advance the frame, synchronizing the trellis (which will call handle_neotrellis_event() as necessary) and pressing / releasing keys where appropriate for the currently pressed switch and currently active layout. """ # Trellis sync may need to happen less frequently than this? self.trellis.sync() # Accumulate currently-active switch layouts: current_layouts = [ self.layouts[layout] for layout, active in self.active_layouts.items() if active ] for switch, switch_input in enumerate(self.switch_inputs): # If switch is un-pressed, it's pulled high. Make sure to release # any keys attached to it and mark it non-active. if switch_input.value: if switch in self.active_switches and self.active_switches[switch]: diag('marking switch', switch, ' inactive') keys = get_keys_for_switch(switch, current_layouts) for key in keys: diag('release key:', key) self.kbd.release(key) self.active_switches[switch] = False continue if switch in self.active_switches and self.active_switches[switch]: # If we're already marked active, do nothing. continue # If switch is pressed, it's pulled low. Debounce by waiting for bounce time: time.sleep(BOUNCE_SECS) diag('marking switch', switch, ' active') self.active_switches[switch] = True for key in get_keys_for_switch(switch, current_layouts): diag('press key:', key) self.kbd.press(key)