You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

185 lines
6.9 KiB

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