Once you've finished setting up your Feather RP2040 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.
# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT
import time
import asyncio
import board
import digitalio
from rainbowio import colorwheel
import keypad
import displayio
import i2cdisplaybus
import busio
import adafruit_seesaw.seesaw
import adafruit_seesaw.neopixel
import adafruit_seesaw.rotaryio
import adafruit_seesaw.digitalio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
import adafruit_displayio_ssd1306
import adafruit_midi
from adafruit_midi.control_change import ControlChange
import neopixel
# default MIDI channel (1-16)
midi_in_channel = 2
midi_out_channel = 2
# MIDI CC messages, values and names assigned to each encoder
cc_values = [
{'cc_val': (0, 127), 'cc_message': (14), 'cc_name': "Volume"},
{'cc_val': (0, 127), 'cc_message': (15), 'cc_name': "Repeats"},
{'cc_val': (0, 127), 'cc_message': (16), 'cc_name': "Size"},
{'cc_val': (0, 127), 'cc_message': (17), 'cc_name': "Mod"},
{'cc_val': (0, 127), 'cc_message': (18), 'cc_name': "Spread"},
{'cc_val': (0, 127), 'cc_message': (19), 'cc_name': "Scan"},
{'cc_val': (0, 127), 'cc_message': (20), 'cc_name': "Ramp"},
{'cc_val': (1, 3), 'cc_message': (21), 'cc_name': "Mod Number"},
{'cc_val': (1, 3), 'cc_message': (22), 'cc_name': "Mod Bank"},
{'cc_val': (1, 3), 'cc_message': (23), 'cc_name': "Mode"},
{'cc_val': (0, 1), 'cc_message': (102), 'cc_name': "Bypass/Engage"},
{'cc_val': (0, 127), 'cc_message': (93), 'cc_name': "Tap Tempo"},
{'cc_val': (0, 1), 'cc_message': (24), 'cc_name': "Loop (R Hold)"},
{'cc_val': (0, 1), 'cc_message': (25), 'cc_name': "Scan (L Hold)"},
{'cc_val': (0, 127), 'cc_message': (26), 'cc_name': "Clear (Both Hold)"},
{'cc_val': (0, 1), 'cc_message': (51), 'cc_name': "MIDI Clock Ignore"}
]
displayio.release_displays()
oled_reset = board.D13
i2c = board.STEMMA_I2C()
# STEMMA OLED setup
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
splash = displayio.Group()
display.root_group = splash
font = bitmap_font.load_font('/OCRA_small.pcf')
# main label/MIDI message name text; centered
main_area = label.Label(
font, text="4x4 MIDI Messenger", color=0xFFFFFF)
main_area.anchor_point = (0.5, 0.0)
main_area.anchored_position = (display.width / 2, 0)
# MIDI message number text
msg_area = label.Label(
font, text="CC Msg: 10", color=0xFFFFFF)
msg_area.anchor_point = (0.0, 0.5)
msg_area.anchored_position = (0, display.height / 2)
# MIDI message value text
val_area = label.Label(
font, text="CC Val: 50", color=0xFFFFFF)
val_area.anchor_point = (0.0, 1.0)
val_area.anchored_position = (0, display.height)
# MIDI message status text
status_area = label.Label(
font, text="Sent!", color=0xFFFFFF)
status_area.anchor_point = (1.0, 1.0)
status_area.anchored_position = (display.width, display.height)
splash.append(main_area)
splash.append(msg_area)
splash.append(val_area)
splash.append(status_area)
# MIDI over UART setup for MIDI FeatherWing
uart = busio.UART(board.TX, board.RX, baudrate=31250, timeout=0.001)
midi = adafruit_midi.MIDI(
midi_in=uart,
midi_out=uart,
in_channel=(midi_in_channel - 1),
out_channel=(midi_out_channel - 1),
debug=False,
)
# quad rotary encoder setup
ss0 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x49)
ss1 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4A)
ss2 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4B)
ss3 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4C)
# button pins for the encoders
pins = [12, 14, 17, 9]
# interrupts for the button pins. pins are passed as a bitmask
ss0.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
ss1.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
ss2.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
ss3.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
# arrays for the encoders and switches
enc0 = []
enc1 = []
enc2 = []
enc3 = []
sw0 = []
sw1 = []
sw2 = []
sw3 = []
# creating encoders and switches, enabling interrupts for encoders
for i in range(4):
enc0.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss0, i))
enc1.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss1, i))
enc2.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss2, i))
enc3.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss3, i))
sw0.append(adafruit_seesaw.digitalio.DigitalIO(ss0, pins[i]))
sw0[i].switch_to_input(digitalio.Pull.UP)
sw1.append(adafruit_seesaw.digitalio.DigitalIO(ss1, pins[i]))
sw1[i].switch_to_input(digitalio.Pull.UP)
sw2.append(adafruit_seesaw.digitalio.DigitalIO(ss2, pins[i]))
sw2[i].switch_to_input(digitalio.Pull.UP)
sw3.append(adafruit_seesaw.digitalio.DigitalIO(ss3, pins[i]))
sw3[i].switch_to_input(digitalio.Pull.UP)
ss0.enable_encoder_interrupt(encoder=i)
ss1.enable_encoder_interrupt(encoder=i)
ss2.enable_encoder_interrupt(encoder=i)
ss3.enable_encoder_interrupt(encoder=i)
# neopixels on each PCB
pix0 = adafruit_seesaw.neopixel.NeoPixel(ss0, 18, 4, auto_write = True)
pix0.brightness = 0.5
pix1 = adafruit_seesaw.neopixel.NeoPixel(ss1, 18, 4, auto_write = True)
pix1.brightness = 0.5
pix2 = adafruit_seesaw.neopixel.NeoPixel(ss2, 18, 4, auto_write = True)
pix2.brightness = 0.5
pix3 = adafruit_seesaw.neopixel.NeoPixel(ss3, 18, 4, auto_write = True)
pix3.brightness = 0.5
# onboard Feather neopixel
pix_feather = neopixel.NeoPixel(board.NEOPIXEL, 1, auto_write = True)
pix_feather.brightness = 0.5
# encoder position arrays
last_pos0 = [60, 60, 60, 60]
last_pos1 = [60, 60, 60, 0]
last_pos2 = [0, 0, 0, 120]
last_pos3 = [0, 0, 0, 0]
pos0 = [60, 60, 60, 60]
pos1 = [60, 60, 60, 0]
pos2 = [0, 0, 0, 120]
pos3 = [0, 0, 0, 0]
# color arrays for the neopixels
c0 = [0, 16, 32, 48]
c1 = [64, 80, 96, 112]
c2 = [128, 144, 160, 176]
c3 = [192, 208, 224, 240]
# setting starting colors for neopixels
for r in range(4):
pix0[r] = colorwheel(c0[r])
pix1[r] = colorwheel(c1[r])
pix2[r] = colorwheel(c2[r])
pix3[r] = colorwheel(c3[r])
# feather neopixel color
c_feather = 0
pix_feather[0] = colorwheel(c_feather)
# array of all 16 encoder positions
encoder_posititions = [60, 60, 60, 60, 60, 60, 60, 60, 0, 0, 0, 120, 0, 0, 0, 0]
class MIDI_Messages:
# tracks sending a message and index 0-15
def __init__(self):
self.send_msg = False
self.midi_index = 0
class NeoPixel_Attributes:
# tracks color, neopixel index and seesaw
def __init__(self):
self.color = c0
self.index = 0
self.strip = pix0
self.feather_color = c_feather
async def send_midi(midi_msg):
# sends MIDI message if send_msg is True/button pressed
while True:
if midi_msg.send_msg is True:
m = midi_msg.midi_index
main_area.text = f"{cc_values[m]['cc_name']}"
msg_area.text = f"CC Msg: {cc_values[m]['cc_message']}"
val_area.text = f"CC Val: {encoder_posititions[m]}"
midi.send(ControlChange(cc_values[m]['cc_message'], encoder_posititions[m]))
status_area.text = "Sent!"
print(f"sending midi: {m}, {encoder_posititions[m]}, {cc_values[m]['cc_message']}")
time.sleep(1)
midi_msg.send_msg = False
else:
status_area.text = " "
await asyncio.sleep(0)
async def rainbows(the_color):
# Updates colors of the neopixels to scroll through rainbow
while True:
the_color.feather_color += 8
the_color.strip[the_color.index] = colorwheel(the_color.color[the_color.index])
pix_feather[0] = colorwheel(the_color.feather_color)
await asyncio.sleep(0)
async def monitor_interrupts(pin0, pin1, pin2, pin3, the_color, midi_msg): #pylint: disable=too-many-statements
# function to keep encoder value pinned between CC value range
def normalize(val, min_v, max_v):
return max(min(max_v, val), min_v)
# read encoder function
def read_encoder(enc_group, pos, last_pos, pix, colors, index_diff):
# check all four encoders if interrupt is detected
for p in range(4):
pos[p] = enc_group[p].position
if pos[p] != last_pos[p]:
main_index = p + index_diff
# update CC value
if pos[p] > last_pos[p]:
colors[p] += 8
encoder_posititions[main_index] = encoder_posititions[main_index] + 1
else:
colors[p] -= 8
encoder_posititions[main_index] = encoder_posititions[main_index] - 1
encoder_posititions[main_index] = normalize(encoder_posititions[main_index],
cc_values[main_index]['cc_val'][0],
cc_values[main_index]['cc_val'][1])
colors[p] = (colors[p] + 256) % 256 # wrap around to 0-256
print(main_index, encoder_posititions[main_index])
main_area.text = f"{cc_values[main_index]['cc_name']}"
msg_area.text = f"CC Msg: {cc_values[main_index]['cc_message']}"
val_area.text = f"CC Val: {encoder_posititions[main_index]}"
last_pos[p] = pos[p]
# update NeoPixel colors
the_color.color = colors
the_color.index = p
the_color.strip = pix
# function to read button press
def press_switches(sw, index):
if not sw[index].value:
# signals that a MIDI message should be sent
midi_msg.send_msg = True
midi_msg.midi_index = index
print(f"button {index} pressed")
# interrupt pins are passed as a keypad
with keypad.Keys(
(pin0, pin1, pin2, pin3,), value_when_pressed=False, pull=True
) as keys:
while True:
key_event = keys.events.get()
if key_event and key_event.pressed:
key_number = key_event.key_number
# seesaw 0
if key_number == 0:
read_encoder(enc0, pos0, last_pos0, pix0, c0, 0)
press_switches(sw0, 0)
press_switches(sw0, 1)
press_switches(sw0, 2)
press_switches(sw0, 3)
# seesaw 1
elif key_number == 1:
read_encoder(enc1, pos1, last_pos1, pix1, c1, 4)
press_switches(sw1, 0)
press_switches(sw1, 1)
press_switches(sw1, 2)
press_switches(sw1, 3)
# update index to 4-7
midi_msg.midi_index = midi_msg.midi_index + 4
# seesaw 2
elif key_number == 2:
read_encoder(enc2, pos2, last_pos2, pix2, c2, 8)
press_switches(sw2, 0)
press_switches(sw2, 1)
press_switches(sw2, 2)
press_switches(sw2, 3)
# update index 8-11
midi_msg.midi_index = midi_msg.midi_index + 8
# seesaw 3
else:
read_encoder(enc3, pos3, last_pos3, pix3, c3, 12)
press_switches(sw3, 0)
press_switches(sw3, 1)
press_switches(sw3, 2)
press_switches(sw3, 3)
# update index 12-15
midi_msg.midi_index = midi_msg.midi_index + 12
# clear interrupt flag to reset interrupt pin
ss0.get_GPIO_interrupt_flag()
ss1.get_GPIO_interrupt_flag()
ss2.get_GPIO_interrupt_flag()
ss3.get_GPIO_interrupt_flag()
await asyncio.sleep(0)
async def main():
the_color = NeoPixel_Attributes()
midi_msg = MIDI_Messages()
# interrupt listener task
interrupt_task = asyncio.create_task(monitor_interrupts(board.D5, board.D6, board.D9,
board.D10, the_color, midi_msg))
# neopixel task
pixels_task = asyncio.create_task(rainbows(the_color))
# midi task
midi_task = asyncio.create_task(send_midi(midi_msg))
await asyncio.gather(interrupt_task, pixels_task, midi_task)
asyncio.run(main())
Upload the Code and Libraries to the Feather RP2040
After downloading the Project Bundle, plug your Feather RP2040 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather RP2040's CIRCUITPY drive:
- lib folder
- code.py
- OCRA_small.pcf
Your Feather RP2040 CIRCUITPY drive should look like this after copying the lib folder, the font file and the code.py file:
How the CircuitPython Code Works
This code uses asyncio, which lets you queue up tasks that take turns running. Combined with using the interrupt pins on the rotary encoder breakouts, this makes reading 16 rotary encoders and button presses over I2C much faster than if you were to try and do it with a traditional single while True: loop.
The code begins with a dictionary that contains the MIDI CC information assigned to each rotary encoder. You can edit this for whatever device you are controlling. cc_val has the minimum and maximum CC values, cc_message has the MIDI CC message number and cc_name has the name of each command. The cc_name is displayed on the OLED.
# MIDI CC messages, values and names assigned to each encoder
cc_values = [
{'cc_val': (0, 127), 'cc_message': (14), 'cc_name': "Volume"},
{'cc_val': (0, 127), 'cc_message': (15), 'cc_name': "Repeats"},
{'cc_val': (0, 127), 'cc_message': (16), 'cc_name': "Size"},
{'cc_val': (0, 127), 'cc_message': (17), 'cc_name': "Mod"},
{'cc_val': (0, 127), 'cc_message': (18), 'cc_name': "Spread"},
{'cc_val': (0, 127), 'cc_message': (19), 'cc_name': "Scan"},
{'cc_val': (0, 127), 'cc_message': (20), 'cc_name': "Ramp"},
{'cc_val': (1, 3), 'cc_message': (21), 'cc_name': "Mod Number"},
{'cc_val': (1, 3), 'cc_message': (22), 'cc_name': "Mod Bank"},
{'cc_val': (1, 3), 'cc_message': (23), 'cc_name': "Mode"},
{'cc_val': (0, 1), 'cc_message': (102), 'cc_name': "Bypass/Engage"},
{'cc_val': (60, 200), 'cc_message': (93), 'cc_name': "Tap Tempo"},
{'cc_val': (0, 1), 'cc_message': (24), 'cc_name': "Loop (R Hold)"},
{'cc_val': (0, 1), 'cc_message': (25), 'cc_name': "Scan (L Hold)"},
{'cc_val': (0, 127), 'cc_message': (26), 'cc_name': "Clear (Both Hold)"},
{'cc_val': (0, 1), 'cc_message': (51), 'cc_name': "MIDI Clock Ignore"}
]
Display
Next is the display setup for the OLED. There are four text elements. The main_area will show the name of the CC message that you have selected. The msg_area shows the MIDI CC message number and the val_area shows the CC value that is currently assigned to the rotary encoder. status_area will show "Sent!" when you send the MIDI CC message.
# STEMMA OLED setup
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
splash = displayio.Group()
display.root_group = splash
font = bitmap_font.load_font('/OCRA_small.pcf')
# main label/MIDI message name text; centered
main_area = label.Label(
font, text="4x4 MIDI Messenger", color=0xFFFFFF)
main_area.anchor_point = (0.5, 0.0)
main_area.anchored_position = (display.width / 2, 0)
# MIDI message number text
msg_area = label.Label(
font, text="CC Msg: 10", color=0xFFFFFF)
msg_area.anchor_point = (0.0, 0.5)
msg_area.anchored_position = (0, display.height / 2)
# MIDI message value text
val_area = label.Label(
font, text="CC Val: 50", color=0xFFFFFF)
val_area.anchor_point = (0.0, 1.0)
val_area.anchored_position = (0, display.height)
# MIDI message status text
status_area = label.Label(
font, text="Sent!", color=0xFFFFFF)
status_area.anchor_point = (1.0, 1.0)
status_area.anchored_position = (display.width, display.height)
splash.append(main_area)
splash.append(msg_area)
splash.append(val_area)
splash.append(status_area)
The MIDI FeatherWing uses MIDI over UART to send and receive MIDI messages.
# MIDI over UART setup for MIDI FeatherWing
uart = busio.UART(board.TX, board.RX, baudrate=31250, timeout=0.001)
midi_in_channel = 1
midi_out_channel = 1
midi = adafruit_midi.MIDI(
midi_in=uart,
midi_out=uart,
in_channel=(midi_in_channel - 1),
out_channel=(midi_out_channel - 1),
debug=False,
)
seesaw Setup
Four seesaw objects are created for each of the quad rotary encoder breakouts. Each of the breakouts use the same pins for the switches on the rotary encoders (12, 14, 17 and 9).
# quad rotary encoder setup ss0 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x49) ss1 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4A) ss2 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4B) ss3 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4C) # button pins for the encoders pins = [12, 14, 17, 9]
Interrupts
Interrupts are setup for the switch pins by passing the pins as a bitmask. As a result, whenever you engage one of the switches, you'll see the INT LED turn on on the breakout.
# interrupts for the button pins. pins are passed as a bitmask ss0.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) ss1.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) ss2.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True) ss3.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
Encoders, Switches and More Interrupts
A for statement instantiates four encoders and four switches on each of the breakouts. Interrupts are also enabled on each of the encoders.
# arrays for the encoders and switches
enc0 = []
enc1 = []
enc2 = []
enc3 = []
sw0 = []
sw1 = []
sw2 = []
sw3 = []
# creating encoders and switches, enabling interrupts for encoders
for i in range(4):
enc0.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss0, i))
enc1.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss1, i))
enc2.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss2, i))
enc3.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss3, i))
sw0.append(adafruit_seesaw.digitalio.DigitalIO(ss0, pins[i]))
sw0[i].switch_to_input(digitalio.Pull.UP)
sw1.append(adafruit_seesaw.digitalio.DigitalIO(ss1, pins[i]))
sw1[i].switch_to_input(digitalio.Pull.UP)
sw2.append(adafruit_seesaw.digitalio.DigitalIO(ss2, pins[i]))
sw2[i].switch_to_input(digitalio.Pull.UP)
sw3.append(adafruit_seesaw.digitalio.DigitalIO(ss3, pins[i]))
sw3[i].switch_to_input(digitalio.Pull.UP)
ss0.enable_encoder_interrupt(encoder=i)
ss1.enable_encoder_interrupt(encoder=i)
ss2.enable_encoder_interrupt(encoder=i)
ss3.enable_encoder_interrupt(encoder=i)
NeoPixels
The four NeoPixels on each breakout are instantiated. Then, each NeoPixel is assigned a color value from its corresponding color array.
# neopixels on each PCB
pix0 = adafruit_seesaw.neopixel.NeoPixel(ss0, 18, 4, auto_write = True)
pix0.brightness = 0.5
pix1 = adafruit_seesaw.neopixel.NeoPixel(ss1, 18, 4, auto_write = True)
pix1.brightness = 0.5
pix2 = adafruit_seesaw.neopixel.NeoPixel(ss2, 18, 4, auto_write = True)
pix2.brightness = 0.5
pix3 = adafruit_seesaw.neopixel.NeoPixel(ss3, 18, 4, auto_write = True)
pix3.brightness = 0.5
# color arrays for the neopixels
c0 = [0, 16, 32, 48]
c1 = [64, 80, 96, 112]
c2 = [128, 144, 160, 176]
c3 = [192, 208, 224, 240]
# setting starting colors for neopixels
for r in range(4):
pix0[r] = colorwheel(c0[r])
pix1[r] = colorwheel(c1[r])
pix2[r] = colorwheel(c2[r])
pix3[r] = colorwheel(c3[r])
Tracking Encoder Positions
Each encoder position and last position are tracked in arrays. There is also one large array that tracks all sixteen encoders.
# encoder position arrays last_pos0 = [60, 60, 60, 60] last_pos1 = [60, 60, 60, 0] last_pos2 = [0, 0, 0, 120] last_pos3 = [0, 0, 0, 0] pos0 = [60, 60, 60, 60] pos1 = [60, 60, 60, 0] pos2 = [0, 0, 0, 120] pos3 = [0, 0, 0, 0] # array of all 16 encoder positions encoder_posititions = [60, 60, 60, 60, 60, 60, 60, 60, 0, 0, 0, 120, 0, 0, 0, 0]
Classes
There are two classes that are used for the asyncio portion of the code. These classes hold values that are passed between the async tasks. The MIDI_Messages class tracks the selected index and whether or not a message should be sent. The NeoPixel_Attributes class tracks the current color, index and NeoPixel object.
class MIDI_Messages:
# tracks sending a message and index 0-15
def __init__(self):
self.send_msg = False
self.midi_index = 0
class NeoPixel_Attributes:
# tracks color, neopixel index and seesaw
def __init__(self):
self.color = c0
self.index = 0
self.strip = pix0
Tasks
There are three async tasks. send_midi sends a MIDI message if the send_msg attribute is True.
async def send_midi(midi_msg):
# sends MIDI message if send_msg is True/button pressed
while True:
if midi_msg.send_msg is True:
m = midi_msg.midi_index
main_area.text = f"{cc_values[m]['cc_name']}"
msg_area.text = f"CC Msg: {cc_values[m]['cc_message']}"
val_area.text = f"CC Val: {encoder_posititions[m]}"
midi.send(ControlChange(cc_values[m]['cc_message'], encoder_posititions[m]))
status_area.text = "Sent!"
print(f"sending midi: {m}, {encoder_posititions[m]}, {cc_values[m]['cc_message']}")
time.sleep(1)
midi_msg.send_msg = False
else:
status_area.text = " "
await asyncio.sleep(0)
rainbows() changes the color of the NeoPixels.
async def rainbows(the_color):
# Updates colors of the neopixels to scroll through rainbow
while True:
the_color.strip[the_color.index] = colorwheel(the_color.color[the_color.index])
await asyncio.sleep(0)
The main task is monitor_interrupts(). The pins that the interrupt pins are attached to are passed as Keypad keys. If an interrupt is triggered, then I2C is scanned on the corresponding seesaw board for changes in the rotary encoder position or the switch status.
The rotary encoders change the MIDI CC value and engaging a switch triggers sending a MIDI message with send_midi(). These values are displayed on the OLED. The selected encoder or switch corresponds with the index tracked by the MIDI_Messages class.
Before the task ends, the interrupt flag is reset with get_GPIO_interrupt_flag() for each seesaw.
async def monitor_interrupts(pin0, pin1, pin2, pin3, the_color, midi_msg): #pylint: disable=too-many-statements
# function to keep encoder value pinned between CC value range
def normalize(val, min_v, max_v):
return max(min(max_v, val), min_v)
# read encoder function
def read_encoder(enc_group, pos, last_pos, pix, colors, index_diff):
# check all four encoders if interrupt is detected
for p in range(4):
pos[p] = enc_group[p].position
if pos[p] != last_pos[p]:
main_index = p + index_diff
# update CC value
if pos[p] > last_pos[p]:
colors[p] += 8
encoder_posititions[main_index] = encoder_posititions[main_index] + 1
else:
colors[p] -= 8
encoder_posititions[main_index] = encoder_posititions[main_index] - 1
encoder_posititions[main_index] = normalize(encoder_posititions[main_index],
cc_values[main_index]['cc_val'][0],
cc_values[main_index]['cc_val'][1])
colors[p] = (colors[p] + 256) % 256 # wrap around to 0-256
print(main_index, encoder_posititions[main_index])
main_area.text = f"{cc_values[main_index]['cc_name']}"
msg_area.text = f"CC Msg: {cc_values[main_index]['cc_message']}"
val_area.text = f"CC Val: {encoder_posititions[main_index]}"
last_pos[p] = pos[p]
# update NeoPixel colors
the_color.color = colors
the_color.index = p
the_color.strip = pix
# function to read button press
def press_switches(sw, index):
if not sw[index].value:
# signals that a MIDI message should be sent
midi_msg.send_msg = True
midi_msg.midi_index = index
print(f"button {index} pressed")
# interrupt pins are passed as a keypad
with keypad.Keys(
(pin0, pin1, pin2, pin3,), value_when_pressed=False, pull=True
) as keys:
while True:
key_event = keys.events.get()
if key_event and key_event.pressed:
key_number = key_event.key_number
# seesaw 0
if key_number == 0:
read_encoder(enc0, pos0, last_pos0, pix0, c0, 0)
press_switches(sw0, 0)
press_switches(sw0, 1)
press_switches(sw0, 2)
press_switches(sw0, 3)
# seesaw 1
elif key_number == 1:
read_encoder(enc1, pos1, last_pos1, pix1, c1, 4)
press_switches(sw1, 0)
press_switches(sw1, 1)
press_switches(sw1, 2)
press_switches(sw1, 3)
# update index to 4-7
midi_msg.midi_index = midi_msg.midi_index + 4
# seesaw 2
elif key_number == 2:
read_encoder(enc2, pos2, last_pos2, pix2, c2, 8)
press_switches(sw2, 0)
press_switches(sw2, 1)
press_switches(sw2, 2)
press_switches(sw2, 3)
# update index 8-11
midi_msg.midi_index = midi_msg.midi_index + 8
# seesaw 3
else:
read_encoder(enc3, pos3, last_pos3, pix3, c3, 12)
press_switches(sw3, 0)
press_switches(sw3, 1)
press_switches(sw3, 2)
press_switches(sw3, 3)
# update index 12-15
midi_msg.midi_index = midi_msg.midi_index + 12
# clear interrupt flag to reset interrupt pin
ss0.get_GPIO_interrupt_flag()
ss1.get_GPIO_interrupt_flag()
ss2.get_GPIO_interrupt_flag()
ss3.get_GPIO_interrupt_flag()
await asyncio.sleep(0)
Run!
In the main() function, the tasks are created and gathered. After that, they are run in a loop with run().
async def main():
the_color = NeoPixel_Attributes()
midi_msg = MIDI_Messages()
# interrupt listener task
interrupt_task = asyncio.create_task(monitor_interrupts(board.D5, board.D6, board.D9,
board.D10, the_color, midi_msg))
# neopixel task
pixels_task = asyncio.create_task(rainbows(the_color))
# midi task
midi_task = asyncio.create_task(send_midi(midi_msg))
await asyncio.gather(interrupt_task, pixels_task, midi_task)
asyncio.run(main())
Page last edited January 21, 2025
Text editor powered by tinymce.