Browse Source

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.
Brennen Bearnes 3 months ago
parent
commit
fc7eee31a6
2 changed files with 134 additions and 74 deletions
  1. 103
    35
      snakeswitch.py
  2. 31
    39
      switch_config.py

+ 103
- 35
snakeswitch.py View File

@@ -1,30 +1,69 @@
1
+"""
2
+`snakeswitch`
3
+=============
4
+
5
+CircuitPython implementation of a foot-switch and NeoTrellis device for
6
+triggering keyboard events.
7
+
8
+See code.py for a demo of the usage.
9
+
10
+* Author(s): Brennen Bearnes
11
+"""
12
+
13
+# pylint: disable=import-error
1 14
 import time
2 15
 import board
3 16
 import digitalio
4
-import gc
5 17
 import busio
6 18
 
7 19
 import neopixel
8 20
 
9 21
 from adafruit_hid.keyboard import Keyboard
10
-from adafruit_hid.keycode import Keycode
11
-from adafruit_hid.mouse import Mouse
12 22
 from adafruit_neotrellis.neotrellis import NeoTrellis
23
+# pylint: enable=import-error
13 24
 
14
-OFF = (5, 0, 5)
15
-LIT = (40, 20, 0)
16
-UNDEF = (1, 1, 1)
17
-PRESSED = (80, 40, 0)
25
+COLOR_ACTIVE_LAYOUT = (40, 20, 0)
26
+COLOR_INACTIVE_LAYOUT = (5, 0, 5)
27
+COLOR_PRESSED = (80, 40, 0)
28
+COLOR_TRIGGER = (0, 5, 0)
29
+COLOR_UNDEFINED = (0, 0, 0)
18 30
 
19 31
 BOUNCE_SECS = 0.250
20 32
 NEOTRELLIS_COUNT = 16
21 33
 
34
+def get_keys_for_switch(switch, current_layouts):
35
+    """
36
+    Return a list of keycodes that are triggered by a switch in the
37
+    given set of layouts.
38
+    """
39
+    keys = []
40
+    for layout in current_layouts:
41
+        if switch in layout:
42
+            if isinstance(layout[switch], tuple):
43
+                keys.extend(layout[switch])
44
+            else:
45
+                keys.append(layout[switch])
46
+    return keys
47
+
48
+def diag(*message):
49
+    """Print a diagnostic message to the console."""
50
+    print(time.monotonic(), message)
51
+
22 52
 class SnakeSwitch:
53
+    """
54
+    Class modeling a SnakeSwitch.
55
+
56
+    Expects a tuple listing the pins that switches are connected to,
57
+    and a dictionary of layouts - see switch_config.py for a sample
58
+    configuration.
59
+    """
60
+
23 61
     def __init__(self, switch_pins, layouts):
24 62
         i2c_bus = busio.I2C(board.SCL, board.SDA)
25 63
         self.trellis = NeoTrellis(i2c_bus)
26 64
         self.kbd = Keyboard()
27 65
 
66
+        diag('initializing pins', switch_pins)
28 67
         self.switch_inputs = tuple(digitalio.DigitalInOut(pin) for pin in switch_pins)
29 68
         for switch_input in self.switch_inputs:
30 69
             switch_input.switch_to_input(pull=digitalio.Pull.UP)
@@ -34,13 +73,19 @@ class SnakeSwitch:
34 73
         self.active_switches = {}
35 74
 
36 75
         for i in range(NEOTRELLIS_COUNT):
76
+            # Mark any mapped to each button as currently inactive:
37 77
             self.active_layouts[i] = False
78
+
38 79
             # Activate rising / falling edge events on all keys, set up callback:
39 80
             self.trellis.activate_key(i, NeoTrellis.EDGE_RISING)
40 81
             self.trellis.activate_key(i, NeoTrellis.EDGE_FALLING)
41 82
             self.trellis.callbacks[i] = self.handle_neotrellis_event
42 83
 
43
-        self.toggle_layout(sorted(self.layouts.keys())[0])
84
+        # Activate first defined layout that isn't just a key trigger:
85
+        for layout_id, layout in self.layouts.items():
86
+            if isinstance(layout, dict):
87
+                self.toggle_layout(layout_id)
88
+                break
44 89
         self.paint_trellis()
45 90
 
46 91
         # Turn off NeoPixel on the Feather M4:
@@ -48,46 +93,79 @@ class SnakeSwitch:
48 93
         onboard_pixel[0] = (0, 0, 0)
49 94
 
50 95
     def handle_neotrellis_event(self, event):
96
+        """A callback for handling NeoTrelllis rising/falling edges."""
51 97
         if event.edge == NeoTrellis.EDGE_RISING:
52
-            # Highlight the currently pressed key:
53
-            self.trellis.pixels[event.number] = PRESSED
98
+            # Highlight the currently pressed button:
99
+            self.trellis.pixels[event.number] = COLOR_PRESSED
100
+
54 101
         elif event.edge == NeoTrellis.EDGE_FALLING:
55
-            # On release, activate a button if it has a defined layout:
102
+            # On release, activate a button if it has a defined layout (or fire
103
+            # any defined key events):
56 104
             if event.number in self.layouts:
57
-                self.toggle_layout(event.number)
105
+                action_for_button = self.layouts[event.number]
106
+                if isinstance(action_for_button, int):
107
+                    # It's a key code, press it:
108
+                    diag("pressing and releasing keycode: ", action_for_button)
109
+                    self.kbd.press(self.layouts[event.number])
110
+                    self.kbd.release_all()
111
+                elif isinstance(action_for_button, tuple):
112
+                    # It's a tuple with multiple key codes - fire them all:
113
+                    diag("pressing and releasing keycode combo: ", action_for_button)
114
+                    for k in action_for_button:
115
+                        self.kbd.press(k)
116
+                    self.kbd.release_all()
117
+                else:
118
+                    # Toggle a layout:
119
+                    self.toggle_layout(event.number)
58 120
             self.paint_trellis()
59 121
 
60 122
     def toggle_layout(self, layout_id):
61
-        print(time.monotonic(), 'switching layout: ', layout_id)
123
+        """Toggle the state of a given layout."""
124
+        diag('switching layout: ', layout_id)
62 125
         self.active_layouts[layout_id] = not self.active_layouts[layout_id]
63 126
 
64 127
     def paint_trellis(self):
128
+        """Set state of trellis LEDs based on defined and active layouts."""
65 129
         for layout_button, active in self.active_layouts.items():
66 130
             if active:
67 131
                 # Layout is currently activated:
68
-                self.trellis.pixels[layout_button] = LIT
132
+                self.trellis.pixels[layout_button] = COLOR_ACTIVE_LAYOUT
69 133
             elif layout_button in self.layouts:
70
-                # Layout is defined, but deactivate:
71
-                self.trellis.pixels[layout_button] = OFF
134
+                if isinstance(self.layouts[layout_button], (tuple, int)):
135
+                    # Button is actually a keycode or set of keycodes to fire on press:
136
+                    self.trellis.pixels[layout_button] = COLOR_TRIGGER
137
+                else:
138
+                    # Layout is defined, but deactivated:
139
+                    self.trellis.pixels[layout_button] = COLOR_INACTIVE_LAYOUT
72 140
             else:
73 141
                 # Layout isn't defined in the config file:
74
-                self.trellis.pixels[layout_button] = UNDEF
142
+                self.trellis.pixels[layout_button] = COLOR_UNDEFINED
75 143
 
76 144
     def advance_frame(self):
77
-        # XXX: trellis sync may need to happen less frequently
145
+        """
146
+        Advance the frame, synchronizing the trellis (which will call
147
+        handle_neotrellis_event() as necessary) and pressing / releasing
148
+        keys where appropriate for the currently pressed switch and
149
+        currently active layout.
150
+        """
151
+        # Trellis sync may need to happen less frequently than this?
78 152
         self.trellis.sync()
79 153
 
80
-        current_layouts = [self.layouts[layout] for layout, active in self.active_layouts.items() if active]
154
+        # Accumulate currently-active switch layouts:
155
+        current_layouts = [
156
+            self.layouts[layout] for layout, active
157
+            in self.active_layouts.items() if active
158
+        ]
81 159
 
82 160
         for switch, switch_input in enumerate(self.switch_inputs):
83 161
             # If switch is un-pressed, it's pulled high.  Make sure to release
84 162
             # any keys attached to it and mark it non-active.
85 163
             if switch_input.value:
86 164
                 if switch in self.active_switches and self.active_switches[switch]:
87
-                    print(time.monotonic(), 'marking switch', switch, ' inactive')
88
-                    keys = self.get_keys_for_switch(switch, current_layouts)
165
+                    diag('marking switch', switch, ' inactive')
166
+                    keys = get_keys_for_switch(switch, current_layouts)
89 167
                     for key in keys:
90
-                        print(time.monotonic(), 'release key:', key)
168
+                        diag('release key:', key)
91 169
                         self.kbd.release(key)
92 170
                     self.active_switches[switch] = False
93 171
                 continue
@@ -99,19 +177,9 @@ class SnakeSwitch:
99 177
             # If switch is pressed, it's pulled low. Debounce by waiting for bounce time:
100 178
             time.sleep(BOUNCE_SECS)
101 179
 
102
-            print(time.monotonic(), 'marking switch', switch, ' active')
180
+            diag('marking switch', switch, ' active')
103 181
             self.active_switches[switch] = True
104 182
 
105
-            for key in self.get_keys_for_switch(switch, current_layouts):
106
-                print(time.monotonic(), 'press key:', key)
183
+            for key in get_keys_for_switch(switch, current_layouts):
184
+                diag('press key:', key)
107 185
                 self.kbd.press(key)
108
-
109
-    def get_keys_for_switch(self, switch, current_layouts):
110
-        keys = []
111
-        for layout in current_layouts:
112
-            if switch in layout:
113
-                if isinstance(layout[switch], tuple):
114
-                    keys.extend(layout[switch])
115
-                else:
116
-                    keys.append(layout[switch])
117
-        return keys

+ 31
- 39
switch_config.py View File

@@ -1,4 +1,15 @@
1
-"""Configuration for SnakeSwitch keys."""
1
+"""
2
+SnakeSwitch Configuration
3
+=========================
4
+
5
+This file should define a dictionary called LAYOUTS.
6
+
7
+The keys of this dictionary should be integers corresponding to
8
+NeoTrelllis button numbers, 0-15.  Values may be either a
9
+dictionary defining a layout, or a tuple containing keycodes
10
+to fire immediately.
11
+"""
12
+
2 13
 from adafruit_hid.keycode import Keycode
3 14
 
4 15
 # Define a modifier key here for easy changing if window manager
@@ -6,19 +17,21 @@ from adafruit_hid.keycode import Keycode
6 17
 MOD_KEY = Keycode.LEFT_GUI
7 18
 
8 19
 LAYOUTS = {
20
+
9 21
     # A default layout - just the modifier key on the 0th switch:
10 22
     0: {
11 23
         0: MOD_KEY,
12 24
     },
13 25
 
14
-    # Add this for left and right arrows on the other two switches:
26
+    # The second button toggles left and right arrows on the other two
27
+    # switches:
15 28
     1: {
16 29
         1: Keycode.LEFT_ARROW,
17 30
         2: Keycode.RIGHT_ARROW,
18 31
     },
19 32
 
20
-    # Chorded mod-left, mod-right - workspace switching in my XMonad
21
-    # setup:
33
+    # The third button toggles chorded mod-left, mod-right - workspace
34
+    # switching in my XMonad setup:
22 35
     2: {
23 36
         1: (MOD_KEY, Keycode.LEFT_ARROW),
24 37
         2: (MOD_KEY, Keycode.RIGHT_ARROW),
@@ -30,7 +43,7 @@ LAYOUTS = {
30 43
     },
31 44
 
32 45
     # These add some common chords to the primary mod key if used
33
-    # in combination with layout 0.
46
+    # in combination with layout 0:
34 47
 
35 48
     # Mod-Shift-G - brings up a list of active windows:
36 49
     4: {
@@ -42,39 +55,18 @@ LAYOUTS = {
42 55
         0: Keycode.TAB
43 56
     },
44 57
 
45
-    # Add buttons for binding instant workspace switching in combination
46
-    # with layout 0.
47
-    8: {
48
-        0: Keycode.ONE
49
-    },
50
-
51
-    9: {
52
-        0: Keycode.TWO
53
-    },
54
-
55
-    10: {
56
-        0: Keycode.THREE
57
-    },
58
-
59
-    11: {
60
-        0: Keycode.FOUR
61
-    },
62
-
63
-    12: {
64
-        0: Keycode.FIVE
65
-    },
66
-
67
-    13: {
68
-        0: Keycode.SIX
69
-    },
70
-
71
-    14: {
72
-        0: Keycode.SEVEN
73
-    },
74
-
75
-    # Note window toggling:
76
-    15: {
77
-        0: (Keycode.SHIFT, Keycode.N)
78
-    },
58
+    # Instead of toggling layouts for the footswitches, pressing and
59
+    # releasing buttons 8-15 will instantly fire keyboard events, in
60
+    # this case switching between workspaces:
61
+    15: (MOD_KEY, Keycode.ONE),
62
+    14: (MOD_KEY, Keycode.TWO),
63
+    13: (MOD_KEY, Keycode.THREE),
64
+    12: (MOD_KEY, Keycode.FOUR),
65
+    11: (MOD_KEY, Keycode.FIVE),
66
+    10: (MOD_KEY, Keycode.SIX),
67
+    9:  (MOD_KEY, Keycode.SEVEN),
68
+
69
+    # Toggle note window:
70
+    8: (MOD_KEY, Keycode.SHIFT, Keycode.N)
79 71
 
80 72
 }

Loading…
Cancel
Save