Browse Source

composable input layers bound to buttons; press/release of keys

This adds a config file format which allows input layers to be defined for
each NeoTrellis button.  Buttons with a layer attached will glow an
indicator color.  Layers can be toggled independently, and all the keys
defined for a given switch will be sent.

Keys are now held until the switch is released, which allows for chording
and combining them with actual keyboard input.

TODO:

- Order matters within key definitions, and there's probably some implicit
ordering of composed layers that could get weird.  Maybe higher-number
layers should just automatically come first?

- The code still feels pretty confusing and could definitely be optimized
in a couple of places.

- I occasionally get weird behavior from the NeoTrellis, suspect that
might be because I'm reading it too often.
Brennen Bearnes 2 months ago
parent
commit
e948cb28b2
2 changed files with 90 additions and 34 deletions
  1. 6
    2
      code.py
  2. 84
    32
      snakeswitch.py

+ 6
- 2
code.py View File

@@ -5,8 +5,12 @@ https://github.com/adafruit/snakeswitch
5 5
 
6 6
 * Author(s): Brennen Bearnes
7 7
 """
8
-
8
+import board
9 9
 from snakeswitch import SnakeSwitch
10
-ss = SnakeSwitch()
10
+from switch_config import LAYOUTS
11
+
12
+SWITCH_PINS = (board.D5, board.D6, board.D9)
13
+
14
+ss = SnakeSwitch(SWITCH_PINS, LAYOUTS)
11 15
 while True:
12 16
     ss.advance_frame()

+ 84
- 32
snakeswitch.py View File

@@ -9,51 +9,103 @@ from adafruit_hid.keycode import Keycode
9 9
 from adafruit_hid.mouse import Mouse
10 10
 from adafruit_neotrellis.neotrellis import NeoTrellis
11 11
 
12
-OFF = (1, 0, 0)
12
+OFF = (5, 0, 5)
13 13
 LIT = (40, 20, 0)
14
-SWITCH_PINS = (board.D5, board.D6, board.D9, board.D10, board.D11, board.D12, board.A1)
15
-BOUNCE_SECS = 0.100
14
+UNDEF = (1, 1, 1)
15
+PRESSED = (80, 40, 0)
16
+
17
+BOUNCE_SECS = 0.250
18
+NEOTRELLIS_COUNT = 16
16 19
 
17 20
 class SnakeSwitch:
18
-    def __init__(self):
21
+    def __init__(self, switch_pins, layouts):
19 22
         i2c_bus = busio.I2C(board.SCL, board.SDA)
20 23
         self.trellis = NeoTrellis(i2c_bus)
24
+        self.kbd = Keyboard()
21 25
 
22
-        self.switch_ins = tuple(digitalio.DigitalInOut(pin) for pin in SWITCH_PINS)
23
-        for switch_in in self.switch_ins:
24
-            switch_in.switch_to_input(pull=digitalio.Pull.UP)
26
+        self.switch_inputs = tuple(digitalio.DigitalInOut(pin) for pin in switch_pins)
27
+        for switch_input in self.switch_inputs:
28
+            switch_input.switch_to_input(pull=digitalio.Pull.UP)
25 29
 
26
-        self.kbd = Keyboard()
27
-        self.states = {}
30
+        self.layouts = layouts
31
+        self.active_layouts = {}
32
+        self.active_switches = {}
28 33
 
29
-        for i in range(16):
30
-            self.states[i] = False
34
+        for i in range(NEOTRELLIS_COUNT):
35
+            self.active_layouts[i] = False
31 36
             # activate rising / falling edge events on all keys, set up callback
32 37
             self.trellis.activate_key(i, NeoTrellis.EDGE_RISING)
33 38
             self.trellis.activate_key(i, NeoTrellis.EDGE_FALLING)
34
-            self.trellis.callbacks[i] = self.toggle
39
+            self.trellis.callbacks[i] = self.handle_neotrellis_event
40
+
41
+        self.toggle_layout(sorted(self.layouts.keys())[0])
42
+        self.paint_trellis()
35 43
 
36
-    def toggle(self, event):
37
-        # turn the LED on when a rising edge is detected
44
+    def handle_neotrellis_event(self, event):
38 45
         if event.edge == NeoTrellis.EDGE_RISING:
39
-            self.states[event.number] = not self.states[event.number]
46
+            # Highlight the currently pressed key:
47
+            self.trellis.pixels[event.number] = PRESSED
48
+        elif event.edge == NeoTrellis.EDGE_FALLING:
49
+            # On release, activate a button if it has a defined layout:
50
+            if event.number in self.layouts:
51
+                self.toggle_layout(event.number)
52
+            self.paint_trellis()
40 53
 
41
-            for state, value in self.states.items():
42
-                if value:
43
-                    self.trellis.pixels[state] = LIT
44
-                else:
45
-                    self.trellis.pixels[state] = OFF
54
+    def toggle_layout(self, layout_id):
55
+        print(time.monotonic(), 'switching layout: ', layout_id)
56
+        self.active_layouts[layout_id] = not self.active_layouts[layout_id]
57
+
58
+    def paint_trellis(self):
59
+        for layout_button, active in self.active_layouts.items():
60
+            if active:
61
+                # Layout is currently activated:
62
+                self.trellis.pixels[layout_button] = LIT
63
+            elif layout_button in self.layouts:
64
+                # Layout is defined, but deactivate:
65
+                self.trellis.pixels[layout_button] = OFF
66
+            else:
67
+                # Layout isn't defined in the config file:
68
+                self.trellis.pixels[layout_button] = UNDEF
46 69
 
47 70
     def advance_frame(self):
48
-        for switch, switch_in in enumerate(self.switch_ins):
49
-            self.trellis.sync()
50
-            if not switch_in.value:
51
-                # If switch is pressed, it's pulled low. Debounce by waiting for bounce time.
52
-                time.sleep(BOUNCE_SECS)
53
-                print('switch: ', switch)
54
-                print(gc.mem_free())
55
-                self.kbd.send(Keycode.LEFT_GUI, Keycode.TAB)
56
-
57
-                # Wait for switch to be released.
58
-                while not switch_in.value:
59
-                    pass
71
+        # XXX: trellis sync may need to happen less frequently
72
+        self.trellis.sync()
73
+
74
+        current_layouts = [self.layouts[layout] for layout, active in self.active_layouts.items() if active]
75
+
76
+        for switch, switch_input in enumerate(self.switch_inputs):
77
+            # If switch is un-pressed, it's pulled high.  Make sure to release
78
+            # any keys attached to it and mark it non-active.
79
+            if switch_input.value:
80
+                if switch in self.active_switches and self.active_switches[switch]:
81
+                    print(time.monotonic(), 'marking switch', switch, ' inactive')
82
+                    keys = self.get_keys_for_switch(switch, current_layouts)
83
+                    for key in keys:
84
+                        print(time.monotonic(), 'release key:', key)
85
+                        self.kbd.release(key)
86
+                    self.active_switches[switch] = False
87
+                continue
88
+
89
+            if switch in self.active_switches and self.active_switches[switch]:
90
+                # If we're already marked active, do nothing.
91
+                continue
92
+
93
+            # If switch is pressed, it's pulled low. Debounce by waiting for bounce time:
94
+            time.sleep(BOUNCE_SECS)
95
+
96
+            print(time.monotonic(), 'marking switch', switch, ' active')
97
+            self.active_switches[switch] = True
98
+
99
+            for key in self.get_keys_for_switch(switch, current_layouts):
100
+                print(time.monotonic(), 'press key:', key)
101
+                self.kbd.press(key)
102
+
103
+    def get_keys_for_switch(self, switch, current_layouts):
104
+        keys = []
105
+        for layout in current_layouts:
106
+            if switch in layout:
107
+                if isinstance(layout[switch], tuple):
108
+                    keys.extend(layout[switch])
109
+                else:
110
+                    keys.append(layout[switch])
111
+        return keys