diff --git a/snakeswitch.py b/snakeswitch.py index 797b2c9..f09a937 100644 --- a/snakeswitch.py +++ b/snakeswitch.py @@ -1,30 +1,69 @@ +""" +`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 gc import busio import neopixel from adafruit_hid.keyboard import Keyboard -from adafruit_hid.keycode import Keycode -from adafruit_hid.mouse import Mouse from adafruit_neotrellis.neotrellis import NeoTrellis +# pylint: enable=import-error -OFF = (5, 0, 5) -LIT = (40, 20, 0) -UNDEF = (1, 1, 1) -PRESSED = (80, 40, 0) +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) @@ -34,13 +73,19 @@ class SnakeSwitch: 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 - self.toggle_layout(sorted(self.layouts.keys())[0]) + # 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: @@ -48,46 +93,79 @@ class SnakeSwitch: 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 key: - self.trellis.pixels[event.number] = PRESSED + # 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: + # On release, activate a button if it has a defined layout (or fire + # any defined key events): if event.number in self.layouts: - self.toggle_layout(event.number) + 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): - print(time.monotonic(), 'switching layout: ', 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] = LIT + self.trellis.pixels[layout_button] = COLOR_ACTIVE_LAYOUT elif layout_button in self.layouts: - # Layout is defined, but deactivate: - self.trellis.pixels[layout_button] = OFF + 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] = UNDEF + self.trellis.pixels[layout_button] = COLOR_UNDEFINED def advance_frame(self): - # XXX: trellis sync may need to happen less frequently + """ + 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() - current_layouts = [self.layouts[layout] for layout, active in self.active_layouts.items() if active] + # 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]: - print(time.monotonic(), 'marking switch', switch, ' inactive') - keys = self.get_keys_for_switch(switch, current_layouts) + diag('marking switch', switch, ' inactive') + keys = get_keys_for_switch(switch, current_layouts) for key in keys: - print(time.monotonic(), 'release key:', key) + diag('release key:', key) self.kbd.release(key) self.active_switches[switch] = False continue @@ -99,19 +177,9 @@ class SnakeSwitch: # If switch is pressed, it's pulled low. Debounce by waiting for bounce time: time.sleep(BOUNCE_SECS) - print(time.monotonic(), 'marking switch', switch, ' active') + diag('marking switch', switch, ' active') self.active_switches[switch] = True - for key in self.get_keys_for_switch(switch, current_layouts): - print(time.monotonic(), 'press key:', key) + for key in get_keys_for_switch(switch, current_layouts): + diag('press key:', key) self.kbd.press(key) - - def get_keys_for_switch(self, switch, current_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 diff --git a/switch_config.py b/switch_config.py index fa5debd..18e3b5d 100644 --- a/switch_config.py +++ b/switch_config.py @@ -1,4 +1,15 @@ -"""Configuration for SnakeSwitch keys.""" +""" +SnakeSwitch Configuration +========================= + +This file should define a dictionary called LAYOUTS. + +The keys of this dictionary should be integers corresponding to +NeoTrelllis button numbers, 0-15. Values may be either a +dictionary defining a layout, or a tuple containing keycodes +to fire immediately. +""" + from adafruit_hid.keycode import Keycode # Define a modifier key here for easy changing if window manager @@ -6,19 +17,21 @@ from adafruit_hid.keycode import Keycode MOD_KEY = Keycode.LEFT_GUI LAYOUTS = { + # A default layout - just the modifier key on the 0th switch: 0: { 0: MOD_KEY, }, - # Add this for left and right arrows on the other two switches: + # The second button toggles left and right arrows on the other two + # switches: 1: { 1: Keycode.LEFT_ARROW, 2: Keycode.RIGHT_ARROW, }, - # Chorded mod-left, mod-right - workspace switching in my XMonad - # setup: + # The third button toggles chorded mod-left, mod-right - workspace + # switching in my XMonad setup: 2: { 1: (MOD_KEY, Keycode.LEFT_ARROW), 2: (MOD_KEY, Keycode.RIGHT_ARROW), @@ -30,7 +43,7 @@ LAYOUTS = { }, # These add some common chords to the primary mod key if used - # in combination with layout 0. + # in combination with layout 0: # Mod-Shift-G - brings up a list of active windows: 4: { @@ -42,39 +55,18 @@ LAYOUTS = { 0: Keycode.TAB }, - # Add buttons for binding instant workspace switching in combination - # with layout 0. - 8: { - 0: Keycode.ONE - }, - - 9: { - 0: Keycode.TWO - }, - - 10: { - 0: Keycode.THREE - }, - - 11: { - 0: Keycode.FOUR - }, - - 12: { - 0: Keycode.FIVE - }, - - 13: { - 0: Keycode.SIX - }, - - 14: { - 0: Keycode.SEVEN - }, - - # Note window toggling: - 15: { - 0: (Keycode.SHIFT, Keycode.N) - }, + # Instead of toggling layouts for the footswitches, pressing and + # releasing buttons 8-15 will instantly fire keyboard events, in + # this case switching between workspaces: + 15: (MOD_KEY, Keycode.ONE), + 14: (MOD_KEY, Keycode.TWO), + 13: (MOD_KEY, Keycode.THREE), + 12: (MOD_KEY, Keycode.FOUR), + 11: (MOD_KEY, Keycode.FIVE), + 10: (MOD_KEY, Keycode.SIX), + 9: (MOD_KEY, Keycode.SEVEN), + + # Toggle note window: + 8: (MOD_KEY, Keycode.SHIFT, Keycode.N) }