|
| 1 | +""" |
| 2 | +A fairly straightforward macro/hotkey program for Adafruit MACROPAD. |
| 3 | +Macro key setups are stored in the /macros folder (configurable below), |
| 4 | +load up just the ones you're likely to use. Plug into computer's USB port, |
| 5 | +use dial to select an application macro set, press MACROPAD keys to send |
| 6 | +key sequences. |
| 7 | +""" |
| 8 | + |
| 9 | +# pylint: disable=import-error, unused-import, too-few-public-methods, eval-used |
| 10 | + |
| 11 | +import os |
| 12 | +import time |
| 13 | +import board |
| 14 | +import digitalio |
| 15 | +import displayio |
| 16 | +import neopixel |
| 17 | +import rotaryio |
| 18 | +import terminalio |
| 19 | +import usb_hid |
| 20 | +from adafruit_display_shapes.rect import Rect |
| 21 | +from adafruit_display_text import label |
| 22 | +from adafruit_hid.keyboard import Keyboard |
| 23 | +from adafruit_hid.keycode import Keycode |
| 24 | +from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS |
| 25 | + |
| 26 | + |
| 27 | +# CONFIGURABLES ------------------------ |
| 28 | + |
| 29 | +MACRO_FOLDER = '/macros' |
| 30 | + |
| 31 | + |
| 32 | +# CLASSES AND FUNCTIONS ---------------- |
| 33 | + |
| 34 | +class Key: |
| 35 | + """ Class representing the physical hardware of each MACROPAD key. """ |
| 36 | + DEBOUNCE_TIME = 1 / 50 |
| 37 | + |
| 38 | + def __init__(self, keyname): |
| 39 | + self.pin = digitalio.DigitalInOut(keyname) |
| 40 | + self.pin.direction = digitalio.Direction.INPUT |
| 41 | + self.pin.pull = digitalio.Pull.UP |
| 42 | + self.last_value = self.pin.value # Initial state |
| 43 | + self.last_time = time.monotonic() |
| 44 | + |
| 45 | + def debounce(self): |
| 46 | + """ Read a key's current state (hardware pin value), filtering out |
| 47 | + any "bounce" noise. This function needs to be called frequently, |
| 48 | + once for each key on pad, plus encoder switch. """ |
| 49 | + value = self.pin.value |
| 50 | + if value != self.last_value: |
| 51 | + now = time.monotonic() |
| 52 | + elapsed = now - self.last_time |
| 53 | + if elapsed >= self.DEBOUNCE_TIME: |
| 54 | + self.last_value = value |
| 55 | + self.last_time = now |
| 56 | + return value |
| 57 | + return None |
| 58 | + |
| 59 | +class App: |
| 60 | + """ Class representing a host-side application, for which we have a set |
| 61 | + of macro sequences. """ |
| 62 | + def __init__(self, appdata): |
| 63 | + self.name = appdata['name'] |
| 64 | + self.macros = appdata['macros'] |
| 65 | + |
| 66 | + def switch(self): |
| 67 | + """ Activate application settings; update OLED labels and LED |
| 68 | + colors. """ |
| 69 | + GROUP[13].text = self.name # Application name |
| 70 | + for i in range(12): |
| 71 | + if i < len(self.macros): # Key in use, set label + LED color |
| 72 | + PIXELS[i] = self.macros[i][0] |
| 73 | + GROUP[i].text = self.macros[i][1] |
| 74 | + else: # Key not in use, no label or LED |
| 75 | + PIXELS[i] = 0 |
| 76 | + GROUP[i].text = '' |
| 77 | + PIXELS.show() |
| 78 | + DISPLAY.refresh() |
| 79 | + |
| 80 | + |
| 81 | +# INITIALIZATION ----------------------- |
| 82 | + |
| 83 | +DISPLAY = board.DISPLAY |
| 84 | +DISPLAY.auto_refresh = False |
| 85 | +ENCODER = rotaryio.IncrementalEncoder(board.ENCODER_B, board.ENCODER_A) |
| 86 | +PIXELS = neopixel.NeoPixel(board.NEOPIXEL, 12, auto_write=False) |
| 87 | +KEYBOARD = Keyboard(usb_hid.devices) |
| 88 | +LAYOUT = KeyboardLayoutUS(KEYBOARD) |
| 89 | + |
| 90 | +GROUP = displayio.Group(max_size=14) |
| 91 | +for KEY_INDEX in range(12): |
| 92 | + x = KEY_INDEX % 3 |
| 93 | + y = KEY_INDEX // 3 |
| 94 | + GROUP.append(label.Label(terminalio.FONT, text='', color=0xFFFFFF, |
| 95 | + anchored_position=((DISPLAY.width - 1) * x / 2, |
| 96 | + DISPLAY.height - 1 - |
| 97 | + (3 - y) * 12), |
| 98 | + anchor_point=(x / 2, 1.0), max_glyphs=15)) |
| 99 | +GROUP.append(Rect(0, 0, DISPLAY.width, 12, fill=0xFFFFFF)) |
| 100 | +GROUP.append(label.Label(terminalio.FONT, text='', color=0x000000, |
| 101 | + anchored_position=(DISPLAY.width//2, -2), |
| 102 | + anchor_point=(0.5, 0.0), max_glyphs=30)) |
| 103 | +DISPLAY.show(GROUP) |
| 104 | + |
| 105 | +KEYS = [] |
| 106 | +for pin in (board.KEY1, board.KEY2, board.KEY3, board.KEY4, board.KEY5, |
| 107 | + board.KEY6, board.KEY7, board.KEY8, board.KEY9, board.KEY10, |
| 108 | + board.KEY11, board.KEY12, board.ENCODER_SWITCH): |
| 109 | + KEYS.append(Key(pin)) |
| 110 | + |
| 111 | +# Load all the macro key setups from .py files in MACRO_FOLDER |
| 112 | +APPS = [] |
| 113 | +FILES = os.listdir(MACRO_FOLDER) |
| 114 | +FILES.sort() |
| 115 | +for FILENAME in FILES: |
| 116 | + if FILENAME.endswith('.py'): |
| 117 | + module = __import__(MACRO_FOLDER + '/' + FILENAME[:-3]) |
| 118 | + APPS.append(App(module.app)) |
| 119 | + |
| 120 | +if not APPS: |
| 121 | + print('No valid macro files found') |
| 122 | + while True: |
| 123 | + pass |
| 124 | + |
| 125 | +LAST_POSITION = None |
| 126 | +APP_INDEX = 0 |
| 127 | +APPS[APP_INDEX].switch() |
| 128 | + |
| 129 | + |
| 130 | +# MAIN LOOP ---------------------------- |
| 131 | + |
| 132 | +while True: |
| 133 | + POSITION = ENCODER.position |
| 134 | + if POSITION != LAST_POSITION: |
| 135 | + APP_INDEX = POSITION % len(APPS) |
| 136 | + APPS[APP_INDEX].switch() |
| 137 | + LAST_POSITION = POSITION |
| 138 | + |
| 139 | + for KEY_INDEX, KEY in enumerate(KEYS[0: len(APPS[APP_INDEX].macros)]): |
| 140 | + action = KEY.debounce() |
| 141 | + if action is not None: |
| 142 | + sequence = APPS[APP_INDEX].macros[KEY_INDEX][2] |
| 143 | + if action is False: # Macro key pressed |
| 144 | + if KEY_INDEX < 12: |
| 145 | + PIXELS[KEY_INDEX] = 0xFFFFFF |
| 146 | + PIXELS.show() |
| 147 | + for item in sequence: |
| 148 | + if isinstance(item, int): |
| 149 | + if item >= 0: |
| 150 | + KEYBOARD.press(item) |
| 151 | + else: |
| 152 | + KEYBOARD.release(item) |
| 153 | + else: |
| 154 | + LAYOUT.write(item) |
| 155 | + elif action is True: # Macro key released |
| 156 | + # Release any still-pressed modifier keys |
| 157 | + for item in sequence: |
| 158 | + if isinstance(item, int) and item >= 0: |
| 159 | + KEYBOARD.release(item) |
| 160 | + if KEY_INDEX < 12: |
| 161 | + PIXELS[KEY_INDEX] = APPS[APP_INDEX].macros[KEY_INDEX][0] |
| 162 | + PIXELS.show() |
0 commit comments