@ -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 ] = CO LOR_ACT IVE_LAYOU T
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