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

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