"""
|
|
`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)
|