Sfoglia il codice sorgente

allow adding simple key triggers to some trellis buttons

Allows for adding utilities like workspace switching to the NeoTrellis pad
itself.

Also adds a diag() function, slightly refactors code layout, and adds
some docstrings.
master
Brennen Bearnes 2 anni fa
parent
commit
fc7eee31a6
2 ha cambiato i file con 134 aggiunte e 74 eliminazioni
  1. +103
    -35
      snakeswitch.py
  2. +31
    -39
      switch_config.py

+ 103
- 35
snakeswitch.py Vedi File

@ -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

+ 31
- 39
switch_config.py Vedi File

@ -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)
}

Caricamento…
Annulla
Salva