|
| 1 | +# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | + |
| 4 | +import time |
| 5 | +import asyncio |
| 6 | +import board |
| 7 | +import digitalio |
| 8 | +from rainbowio import colorwheel |
| 9 | +import keypad |
| 10 | +import displayio |
| 11 | +import busio |
| 12 | +import adafruit_seesaw.seesaw |
| 13 | +import adafruit_seesaw.neopixel |
| 14 | +import adafruit_seesaw.rotaryio |
| 15 | +import adafruit_seesaw.digitalio |
| 16 | +from adafruit_bitmap_font import bitmap_font |
| 17 | +from adafruit_display_text import label |
| 18 | +import adafruit_displayio_ssd1306 |
| 19 | +import adafruit_midi |
| 20 | +from adafruit_midi.control_change import ControlChange |
| 21 | + |
| 22 | +# MIDI CC messages, values and names assigned to each encoder |
| 23 | +cc_values = [ |
| 24 | + {'cc_val': (0, 127), 'cc_message': (14), 'cc_name': "Volume"}, |
| 25 | + {'cc_val': (0, 127), 'cc_message': (15), 'cc_name': "Repeats"}, |
| 26 | + {'cc_val': (0, 127), 'cc_message': (16), 'cc_name': "Size"}, |
| 27 | + {'cc_val': (0, 127), 'cc_message': (17), 'cc_name': "Mod"}, |
| 28 | + {'cc_val': (0, 127), 'cc_message': (18), 'cc_name': "Spread"}, |
| 29 | + {'cc_val': (0, 127), 'cc_message': (19), 'cc_name': "Scan"}, |
| 30 | + {'cc_val': (0, 127), 'cc_message': (20), 'cc_name': "Ramp"}, |
| 31 | + {'cc_val': (1, 3), 'cc_message': (21), 'cc_name': "Mod Number"}, |
| 32 | + {'cc_val': (1, 3), 'cc_message': (22), 'cc_name': "Mod Bank"}, |
| 33 | + {'cc_val': (1, 3), 'cc_message': (23), 'cc_name': "Mode"}, |
| 34 | + {'cc_val': (0, 1), 'cc_message': (102), 'cc_name': "Bypass/Engage"}, |
| 35 | + {'cc_val': (60, 200), 'cc_message': (93), 'cc_name': "Tap Tempo"}, |
| 36 | + {'cc_val': (0, 1), 'cc_message': (24), 'cc_name': "Loop (R Hold)"}, |
| 37 | + {'cc_val': (0, 1), 'cc_message': (25), 'cc_name': "Scan (L Hold)"}, |
| 38 | + {'cc_val': (0, 127), 'cc_message': (26), 'cc_name': "Clear (Both Hold)"}, |
| 39 | + {'cc_val': (0, 1), 'cc_message': (51), 'cc_name': "MIDI Clock Ignore"} |
| 40 | + ] |
| 41 | + |
| 42 | +displayio.release_displays() |
| 43 | + |
| 44 | +oled_reset = board.D13 |
| 45 | + |
| 46 | +i2c = board.STEMMA_I2C() |
| 47 | +# STEMMA OLED setup |
| 48 | +display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset) |
| 49 | +display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64) |
| 50 | + |
| 51 | +splash = displayio.Group() |
| 52 | +display.show(splash) |
| 53 | +font = bitmap_font.load_font('/OCRA_small.pcf') |
| 54 | +# main label/MIDI message name text; centered |
| 55 | +main_area = label.Label( |
| 56 | + font, text="4x4 MIDI Messager", color=0xFFFFFF) |
| 57 | +main_area.anchor_point = (0.5, 0.0) |
| 58 | +main_area.anchored_position = (display.width / 2, 0) |
| 59 | +# MIDI message number text |
| 60 | +msg_area = label.Label( |
| 61 | + font, text="CC Msg: 10", color=0xFFFFFF) |
| 62 | +msg_area.anchor_point = (0.0, 0.5) |
| 63 | +msg_area.anchored_position = (0, display.height / 2) |
| 64 | +# MIDI message value text |
| 65 | +val_area = label.Label( |
| 66 | + font, text="CC Val: 50", color=0xFFFFFF) |
| 67 | +val_area.anchor_point = (0.0, 1.0) |
| 68 | +val_area.anchored_position = (0, display.height) |
| 69 | +# MIDI message status text |
| 70 | +status_area = label.Label( |
| 71 | + font, text="Sent!", color=0xFFFFFF) |
| 72 | +status_area.anchor_point = (1.0, 1.0) |
| 73 | +status_area.anchored_position = (display.width, display.height) |
| 74 | + |
| 75 | +splash.append(main_area) |
| 76 | +splash.append(msg_area) |
| 77 | +splash.append(val_area) |
| 78 | +splash.append(status_area) |
| 79 | +# MIDI over UART setup for MIDI FeatherWing |
| 80 | +uart = busio.UART(board.TX, board.RX, baudrate=31250, timeout=0.001) |
| 81 | +midi_in_channel = 1 |
| 82 | +midi_out_channel = 1 |
| 83 | +midi = adafruit_midi.MIDI( |
| 84 | + midi_in=uart, |
| 85 | + midi_out=uart, |
| 86 | + in_channel=(midi_in_channel - 1), |
| 87 | + out_channel=(midi_out_channel - 1), |
| 88 | + debug=False, |
| 89 | +) |
| 90 | +# quad rotary encoder setup |
| 91 | +ss0 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x49) |
| 92 | +ss1 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4A) |
| 93 | +ss2 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4B) |
| 94 | +ss3 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4C) |
| 95 | +# button pins for the encoders |
| 96 | +pins = [12, 14, 17, 9] |
| 97 | +# interrupts for the button pins. pins are passed as a bitmask |
| 98 | +ss0.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) |
| 99 | +ss1.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) |
| 100 | +ss2.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) |
| 101 | +ss3.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) |
| 102 | +# arrays for the encoders and switches |
| 103 | +enc0 = [] |
| 104 | +enc1 = [] |
| 105 | +enc2 = [] |
| 106 | +enc3 = [] |
| 107 | +sw0 = [] |
| 108 | +sw1 = [] |
| 109 | +sw2 = [] |
| 110 | +sw3 = [] |
| 111 | +# creating encoders and switches, enabling interrupts for encoders |
| 112 | +for i in range(4): |
| 113 | + enc0.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss0, i)) |
| 114 | + enc1.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss1, i)) |
| 115 | + enc2.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss2, i)) |
| 116 | + enc3.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss3, i)) |
| 117 | + sw0.append(adafruit_seesaw.digitalio.DigitalIO(ss0, pins[i])) |
| 118 | + sw0[i].switch_to_input(digitalio.Pull.UP) |
| 119 | + sw1.append(adafruit_seesaw.digitalio.DigitalIO(ss1, pins[i])) |
| 120 | + sw1[i].switch_to_input(digitalio.Pull.UP) |
| 121 | + sw2.append(adafruit_seesaw.digitalio.DigitalIO(ss2, pins[i])) |
| 122 | + sw2[i].switch_to_input(digitalio.Pull.UP) |
| 123 | + sw3.append(adafruit_seesaw.digitalio.DigitalIO(ss3, pins[i])) |
| 124 | + sw3[i].switch_to_input(digitalio.Pull.UP) |
| 125 | + ss0.enable_encoder_interrupt(encoder=i) |
| 126 | + ss1.enable_encoder_interrupt(encoder=i) |
| 127 | + ss2.enable_encoder_interrupt(encoder=i) |
| 128 | + ss3.enable_encoder_interrupt(encoder=i) |
| 129 | +# neopixels on each PCB |
| 130 | +pix0 = adafruit_seesaw.neopixel.NeoPixel(ss0, 18, 4, auto_write = True) |
| 131 | +pix0.brightness = 0.5 |
| 132 | +pix1 = adafruit_seesaw.neopixel.NeoPixel(ss1, 18, 4, auto_write = True) |
| 133 | +pix1.brightness = 0.5 |
| 134 | +pix2 = adafruit_seesaw.neopixel.NeoPixel(ss2, 18, 4, auto_write = True) |
| 135 | +pix2.brightness = 0.5 |
| 136 | +pix3 = adafruit_seesaw.neopixel.NeoPixel(ss3, 18, 4, auto_write = True) |
| 137 | +pix3.brightness = 0.5 |
| 138 | +# encoder position arrays |
| 139 | +last_pos0 = [60, 60, 60, 60] |
| 140 | +last_pos1 = [60, 60, 60, 0] |
| 141 | +last_pos2 = [0, 0, 0, 120] |
| 142 | +last_pos3 = [0, 0, 0, 0] |
| 143 | +pos0 = [60, 60, 60, 60] |
| 144 | +pos1 = [60, 60, 60, 0] |
| 145 | +pos2 = [0, 0, 0, 120] |
| 146 | +pos3 = [0, 0, 0, 0] |
| 147 | +# color arrays for the neopixels |
| 148 | +c0 = [0, 16, 32, 48] |
| 149 | +c1 = [64, 80, 96, 112] |
| 150 | +c2 = [128, 144, 160, 176] |
| 151 | +c3 = [192, 208, 224, 240] |
| 152 | +# setting starting colors for neopixels |
| 153 | +for r in range(4): |
| 154 | + pix0[r] = colorwheel(c0[r]) |
| 155 | + pix1[r] = colorwheel(c1[r]) |
| 156 | + pix2[r] = colorwheel(c2[r]) |
| 157 | + pix3[r] = colorwheel(c3[r]) |
| 158 | +# array of all 16 encoder positions |
| 159 | +encoder_posititions = [60, 60, 60, 60, 60, 60, 60, 60, 0, 0, 0, 120, 0, 0, 0, 0] |
| 160 | + |
| 161 | +class MIDI_Messages: |
| 162 | + # tracks sending a message and index 0-15 |
| 163 | + def __init__(self): |
| 164 | + self.send_msg = False |
| 165 | + self.midi_index = 0 |
| 166 | + |
| 167 | +class NeoPixel_Attributes: |
| 168 | + # tracks color, neopixel index and seesaw |
| 169 | + def __init__(self): |
| 170 | + self.color = c0 |
| 171 | + self.index = 0 |
| 172 | + self.strip = pix0 |
| 173 | + |
| 174 | +async def send_midi(midi_msg): |
| 175 | + # sends MIDI message if send_msg is True/button pressed |
| 176 | + while True: |
| 177 | + if midi_msg.send_msg is True: |
| 178 | + m = midi_msg.midi_index |
| 179 | + main_area.text = f"{cc_values[m]['cc_name']}" |
| 180 | + msg_area.text = f"CC Msg: {cc_values[i]['cc_message']}" |
| 181 | + val_area.text = f"CC Val: {encoder_posititions[m]}" |
| 182 | + midi.send(ControlChange(cc_values[m]['cc_message'], encoder_posititions[m])) |
| 183 | + status_area.text = "Sent!" |
| 184 | + print(f"sending midi: {m}, {encoder_posititions[m]}, {cc_values[m]['cc_message']}") |
| 185 | + time.sleep(1) |
| 186 | + midi_msg.send_msg = False |
| 187 | + else: |
| 188 | + status_area.text = " " |
| 189 | + await asyncio.sleep(0) |
| 190 | + |
| 191 | +async def rainbows(the_color): |
| 192 | + # Updates colors of the neopixels to scroll through rainbow |
| 193 | + while True: |
| 194 | + the_color.strip[the_color.index] = colorwheel(the_color.color[the_color.index]) |
| 195 | + await asyncio.sleep(0) |
| 196 | + |
| 197 | +async def monitor_interrupts(pin0, pin1, pin2, pin3, the_color, midi_msg): #pylint: disable=too-many-statements |
| 198 | + # function to keep encoder value pinned between CC value range |
| 199 | + def normalize(val, min_v, max_v): |
| 200 | + return max(min(max_v, val), min_v) |
| 201 | + # read encoder function |
| 202 | + def read_encoder(enc_group, pos, last_pos, pix, colors, index_diff): |
| 203 | + # check all four encoders if interrupt is detected |
| 204 | + for p in range(4): |
| 205 | + pos[p] = enc_group[p].position |
| 206 | + if pos[p] != last_pos[p]: |
| 207 | + main_index = p + index_diff |
| 208 | + # update CC value |
| 209 | + if pos[p] > last_pos[p]: |
| 210 | + colors[p] += 8 |
| 211 | + encoder_posititions[main_index] = encoder_posititions[main_index] + 1 |
| 212 | + else: |
| 213 | + colors[p] -= 8 |
| 214 | + encoder_posititions[main_index] = encoder_posititions[main_index] - 1 |
| 215 | + encoder_posititions[main_index] = normalize(encoder_posititions[main_index], |
| 216 | + cc_values[main_index]['cc_val'][0], |
| 217 | + cc_values[main_index]['cc_val'][1]) |
| 218 | + colors[p] = (colors[p] + 256) % 256 # wrap around to 0-256 |
| 219 | + print(main_index, encoder_posititions[main_index]) |
| 220 | + main_area.text = f"{cc_values[main_index]['cc_name']}" |
| 221 | + msg_area.text = f"CC Msg: {cc_values[main_index]['cc_message']}" |
| 222 | + val_area.text = f"CC Val: {encoder_posititions[main_index]}" |
| 223 | + last_pos[p] = pos[p] |
| 224 | + # update NeoPixel colors |
| 225 | + the_color.color = colors |
| 226 | + the_color.index = p |
| 227 | + the_color.strip = pix |
| 228 | + # function to read button press |
| 229 | + def press_switches(sw, index): |
| 230 | + if not sw[index].value: |
| 231 | + # signals that a MIDI message should be sent |
| 232 | + midi_msg.send_msg = True |
| 233 | + midi_msg.midi_index = index |
| 234 | + print(f"button {index} pressed") |
| 235 | + # interrupt pins are passed as a keypad |
| 236 | + with keypad.Keys( |
| 237 | + (pin0, pin1, pin2, pin3,), value_when_pressed=False, pull=True |
| 238 | + ) as keys: |
| 239 | + while True: |
| 240 | + key_event = keys.events.get() |
| 241 | + if key_event and key_event.pressed: |
| 242 | + key_number = key_event.key_number |
| 243 | + # seesaw 0 |
| 244 | + if key_number == 0: |
| 245 | + read_encoder(enc0, pos0, last_pos0, pix0, c0, 0) |
| 246 | + press_switches(sw0, 0) |
| 247 | + press_switches(sw0, 1) |
| 248 | + press_switches(sw0, 2) |
| 249 | + press_switches(sw0, 3) |
| 250 | + # seesaw 1 |
| 251 | + elif key_number == 1: |
| 252 | + read_encoder(enc1, pos1, last_pos1, pix1, c1, 4) |
| 253 | + press_switches(sw1, 0) |
| 254 | + press_switches(sw1, 1) |
| 255 | + press_switches(sw1, 2) |
| 256 | + press_switches(sw1, 3) |
| 257 | + # update index to 4-7 |
| 258 | + midi_msg.midi_index = midi_msg.midi_index + 4 |
| 259 | + # seesaw 2 |
| 260 | + elif key_number == 2: |
| 261 | + read_encoder(enc2, pos2, last_pos2, pix2, c2, 8) |
| 262 | + press_switches(sw2, 0) |
| 263 | + press_switches(sw2, 1) |
| 264 | + press_switches(sw2, 2) |
| 265 | + press_switches(sw2, 3) |
| 266 | + # update index 8-11 |
| 267 | + midi_msg.midi_index = midi_msg.midi_index + 8 |
| 268 | + # seesaw 3 |
| 269 | + else: |
| 270 | + read_encoder(enc3, pos3, last_pos3, pix3, c3, 12) |
| 271 | + press_switches(sw3, 0) |
| 272 | + press_switches(sw3, 1) |
| 273 | + press_switches(sw3, 2) |
| 274 | + press_switches(sw3, 3) |
| 275 | + # update index 12-15 |
| 276 | + midi_msg.midi_index = midi_msg.midi_index + 12 |
| 277 | + # clear interrupt flag to reset interrupt pin |
| 278 | + ss0.get_GPIO_interrupt_flag() |
| 279 | + ss1.get_GPIO_interrupt_flag() |
| 280 | + ss2.get_GPIO_interrupt_flag() |
| 281 | + ss3.get_GPIO_interrupt_flag() |
| 282 | + await asyncio.sleep(0) |
| 283 | + |
| 284 | +async def main(): |
| 285 | + the_color = NeoPixel_Attributes() |
| 286 | + midi_msg = MIDI_Messages() |
| 287 | + # interrupt listener task |
| 288 | + interrupt_task = asyncio.create_task(monitor_interrupts(board.D5, board.D6, board.D9, |
| 289 | + board.D10, the_color, midi_msg)) |
| 290 | + # neopixel task |
| 291 | + pixels_task = asyncio.create_task(rainbows(the_color)) |
| 292 | + # midi task |
| 293 | + midi_task = asyncio.create_task(send_midi(midi_msg)) |
| 294 | + |
| 295 | + await asyncio.gather(interrupt_task, pixels_task, midi_task) |
| 296 | + |
| 297 | +asyncio.run(main()) |
0 commit comments