Once you've finished setting up your KB2040 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 to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2025 Liz Clark and Noe Ruiz for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import board
import simpleio
import displayio
import i2cdisplaybus
import adafruit_imageload
import rotaryio
import digitalio
import terminalio
import keypad
from digitalio import DigitalInOut, Direction
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
import adafruit_displayio_ssd1306
import usb_midi
import adafruit_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
from adafruit_midi.control_change import ControlChange
from analogio import AnalogIn
from adafruit_debouncer import Button
# midi note names
MIDI_NOTE_NAMES = [
"C-2", "C#-2", "D-2", "D#-2", "E-2", "F-2", "F#-2", "G-2", "G#-2", "A-2", "A#-2", "B-2",
"C-1", "C#-1", "D-1", "D#-1", "E-1", "F-1", "F#-1", "G-1", "G#-1", "A-1", "A#-1", "B-1",
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9"
]
# Data structure to store all control settings
control_settings = {
'pots': [
{'cc_num': 1, 'range_min': 0, 'range_max': 127, 'channel': 0, 'current_val': 0}, # Pot A
{'cc_num': 10, 'range_min': 0, 'range_max': 127, 'channel': 0, 'current_val': 0}, # Pot B
{'cc_num': 11, 'range_min': 0, 'range_max': 127, 'channel': 0, 'current_val': 0}, # Pot C
],
'keys': [
{'note': 61, 'velocity': 120, 'channel': 0}, # Key 1
{'note': 64, 'velocity': 120, 'channel': 0}, # Key 2
{'note': 66, 'velocity': 120, 'channel': 0}, # Key 3
{'note': 68, 'velocity': 120, 'channel': 0}, # Key 4
{'note': 73, 'velocity': 120, 'channel': 0}, # Key 5
]
}
# midi note numbers (will be replaced by control_settings)
midi_notes = [key['note'] for key in control_settings['keys']]
#rotary encoder and button
encoder = rotaryio.IncrementalEncoder(board.D3, board.D2)
last_position = 0
# midi setup
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
# Create keypad object with the pins (removed encoder button)
keys = keypad.Keys(
pins=(board.D4, board.D5, board.D6, board.D7, board.D8),
value_when_pressed=False, # Buttons pull to ground when pressed
pull=True # Enable internal pull-up resistors
)
# Set up encoder button with debouncer
encoder_button_pin = DigitalInOut(board.A3)
encoder_button_pin.direction = Direction.INPUT
encoder_button_pin.pull = digitalio.Pull.UP
encoder_button = Button(encoder_button_pin, value_when_pressed=False, long_duration_ms=500)
# potentiometer setup
pot_A = AnalogIn(board.A2)
pot_B = AnalogIn(board.A1)
pot_C = AnalogIn(board.A0)
#display setup
i2c = board.STEMMA_I2C()
displayio.release_displays()
# oled
oled_reset = board.D9
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset)
WIDTH = 128
HEIGHT = 64
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT)
bitmap, palette = adafruit_imageload.load("/main.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
# Create a TileGrid to hold the bitmap
bitmap_grid = displayio.TileGrid(bitmap, pixel_shader=palette)
# Create main page
maingroup = displayio.Group()
#dictionary for rectangle highlights
rect_dict = [
{'pos': (0, 0), 'dim': (42, 32)},
{'pos': (42, 0), 'dim': (42, 32)},
{'pos': (84, 0), 'dim': (42, 32)},
{'pos': (1, 32), 'dim': (25, 32)},
{'pos': (26, 32), 'dim': (25, 32)},
{'pos': (51, 32), 'dim': (25, 32)},
{'pos': (76, 32), 'dim': (25, 32)},
{'pos': (101, 32), 'dim': (25, 32)},
]
current_index = 0
rect_info = rect_dict[current_index]
x, y = rect_info['pos']
w, h = rect_info['dim']
# Create the rectangle (outline only, no fill)
rectangle = Rect(x, y, w, h, fill=None, outline=0xFFFFFF)
# Add rectangles to the Group
maingroup.append(bitmap_grid)
maingroup.append(rectangle)
#text container
keynote_maingroup = displayio.Group()
font = terminalio.FONT
color = 0x000000
keynotes = [
{'num': control_settings['keys'][0]['note'], 'pos': (14, 42)},
{'num': control_settings['keys'][1]['note'], 'pos': (39, 42)},
{'num': control_settings['keys'][2]['note'], 'pos': (64, 42)},
{'num': control_settings['keys'][3]['note'], 'pos': (89, 42)},
{'num': control_settings['keys'][4]['note'], 'pos': (114, 42)},
]
keynote_labels = []
for keynote in keynotes:
keynote_area = label.Label(terminalio.FONT,
text=MIDI_NOTE_NAMES[keynote['num']],
color=0x000000)
keynote_area.anchor_point = (0.5, 0.0)
keynote_area.anchored_position = (keynote['pos'][0], keynote['pos'][1])
keynote_labels.append(keynote_area)
keynote_maingroup.append(keynote_area)
maingroup.append(keynote_maingroup)
# labels for potentiometers
potval_maingroup = displayio.Group()
potvals = [
{'num': "0", 'pos': (22, 10)},
{'num': "0", 'pos': (64, 10)},
{'num': "0", 'pos': (106, 10)},
]
potvals_labels = []
for potval in potvals:
potval_area = label.Label(
terminalio.FONT,
text=potval['num'],
color=0x000000,
)
potval_area.anchor_point = (0.5, 0.0)
potval_area.anchored_position = (potval['pos'][0], potval['pos'][1])
potvals_labels.append(potval_area)
potval_maingroup.append(potval_area)
maingroup.append(potval_maingroup)
# Create edit page
editgroup = displayio.Group()
# Labels for Edit Page
header_area = label.Label(
terminalio.FONT,
text="",
color=0x000000,
x=0, y=10,
background_color=0xFFFFFF,
padding_left=34,
padding_right=34,
padding_top=2,
padding_bottom=2,
)
header_area.anchor_point = (0.5, 0.0)
header_area.anchored_position = (64, 2)
item1_area = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=4, y=25,)
item2_area = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=4, y=38,)
item3_area = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=4, y=52,)
# Create separate labels for range min and max
range_label = label.Label(terminalio.FONT, text="Range:", color=0xFFFFFF, x=4, y=38,)
range_min_label = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=65, y=38,)
range_dash_label = label.Label(terminalio.FONT, text="-", color=0xFFFFFF, x=85, y=38,)
range_max_label = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=95, y=38,)
item_areas = [item1_area, item2_area, item3_area]
edit_rectangle = Rect(0, 0, 128, 14, fill=None, outline=0xFFFFFF)
range_rectangle = Rect(0, 0, 20, 14, fill=0x000000, outline=0xFFFFFF)
editgroup.append(edit_rectangle)
editgroup.append(header_area)
editgroup.append(item1_area)
editgroup.append(item2_area)
editgroup.append(item3_area)
editgroup.append(range_rectangle)
range_rectangle.hidden = True
# Add the Group to the Display
display.root_group = maingroup
# function to read analog input
def val(pin):
return pin.value
# Function to update edit mode display
def update_edit_display(index):
# First, remove the range components if they exist in editgroup
if range_label in editgroup:
editgroup.remove(range_label)
editgroup.remove(range_min_label)
editgroup.remove(range_dash_label)
editgroup.remove(range_max_label)
if index < 3: # Potentiometer
p = control_settings['pots'][index]
header_area.text = f"EDIT POT {'ABC'[index]}"
item1_area.text = f"CC Number: {p['cc_num']}"
# Clear item2_area text since we'll use separate labels
item2_area.text = ""
# Add range components
editgroup.append(range_label)
editgroup.append(range_min_label)
editgroup.append(range_dash_label)
editgroup.append(range_max_label)
range_min_label.text = str(p['range_min'])
range_max_label.text = str(p['range_max'])
item3_area.text = f"MIDI Channel: {p['channel'] + 1}"
else: # Button
k = index - 3
s = control_settings['keys'][k]
note_name = MIDI_NOTE_NAMES[s['note']]
header_area.text = f"EDIT KEY {k + 1}"
item1_area.text = f"MIDI Note: {note_name}"
item2_area.text = f"Velocity: {s['velocity']}"
item3_area.text = f"MIDI Channel: {s['channel'] + 1}"
# variables for last read value
pot_A_val2 = 0
pot_B_val2 = 0
pot_C_val2 = 0
edit_mode = False
edit_current_index = 0
edit_active = False # Whether we're actively editing a value
range_edit_selection = 0 # 0 for min, 1 for max
range_edit_active = False # New variable to track if we're actively editing a range value
while True:
# Update encoder button state
encoder_button.update()
# Handle encoder button presses
if encoder_button.short_count == 1:
if not edit_mode:
# Short press enters edit mode
update_edit_display(current_index)
edit_current_index = 0
edit_rectangle.x = 0
edit_rectangle.y = item_areas[edit_current_index].y - 7
edit_active = False
range_rectangle.hidden = True
display.root_group = editgroup
edit_mode = True
elif edit_mode and edit_active and current_index < 3 and edit_current_index == 1:
# In range editing mode, short press switches between min/max
range_edit_selection = 1 - range_edit_selection # Toggle between 0 and 1
if range_edit_selection == 0:
range_rectangle.x = range_min_label.x - 2
else:
range_rectangle.x = range_max_label.x - 2
else:
# Short press exits edit mode
display.root_group = maingroup
edit_mode = False
edit_active = False
range_edit_active = False
edit_rectangle.fill = None
item_areas[edit_current_index].color = 0xFFFFFF
if current_index < 3 and edit_current_index == 1:
range_min_label.color = 0xFFFFFF
range_max_label.color = 0xFFFFFF
range_label.color = 0xFFFFFF
range_dash_label.color = 0xFFFFFF
range_rectangle.hidden = True
if encoder_button.short_count == 2:
# send midi panic
panic = ControlChange(123, 120)
# send CC message
midi.send(panic)
# Handle long press in edit mode
if edit_mode and encoder_button.long_press:
if current_index < 3 and edit_current_index == 1:
# We're on the Range line
if not edit_active:
# Not in range edit mode yet, enter it
edit_active = True
range_edit_active = False
edit_rectangle.fill = 0xFFFFFF
# Show the range rectangle
range_rectangle.hidden = False
range_rectangle.fill = None
range_rectangle.outline = 0x000000
range_edit_selection = 0 # Start with min
# Position range rectangle over the min value
range_rectangle.x = range_min_label.x - 2
range_rectangle.y = range_min_label.y - 7
range_label.color = 0x000000
range_dash_label.color = 0x000000
range_max_label.color = 0x000000
range_min_label.color = 0x000000
elif edit_active and not range_edit_active:
# In selection mode, enter value editing mode
range_edit_active = True
range_rectangle.fill = 0x000000
range_min_label.color = 0xFFFFFF
range_max_label.color = 0xFFFFFF
elif edit_active and range_edit_active:
# In value editing mode, exit range editing completely
edit_active = False
range_edit_active = False
edit_rectangle.fill = None
# Hide range rectangle and restore colors
range_rectangle.hidden = True
range_label.color = 0xFFFFFF
range_dash_label.color = 0xFFFFFF
range_min_label.color = 0xFFFFFF
range_max_label.color = 0xFFFFFF
else:
# Non-range items - simple toggle
edit_active = not edit_active
range_edit_active = False # Reset range edit active state
# Change rectangle color to indicate active editing
if edit_active:
edit_rectangle.fill = 0xFFFFFF
# Make text black on white background for other items
item_areas[edit_current_index].color = 0x000000
else:
edit_rectangle.fill = None
# Make text white
item_areas[edit_current_index].color = 0xFFFFFF
event = keys.events.get()
if event:
# event.key_number gives you the index (0-4) of which button
key_index = event.key_number
if event.pressed:
# Button was pressed - send NoteOn using settings
key_settings = control_settings['keys'][key_index]
midi.send(NoteOn(key_settings['note'], key_settings['velocity']))
if event.released:
# Button was released - send NoteOff using settings
key_settings = control_settings['keys'][key_index]
midi.send(NoteOff(key_settings['note'], key_settings['velocity']))
position = encoder.position
# Check if encoder moved
if position != last_position:
if not edit_mode:
# Main menu navigation
if position > last_position:
current_index = (current_index + 1) % len(rect_dict)
else:
current_index = (current_index - 1) % len(rect_dict)
rect_info = rect_dict[current_index]
x, y = rect_info['pos']
w, h = rect_info['dim']
# Remove old rectangle and create new one with updated dimensions
maingroup.remove(rectangle)
rectangle = Rect(x, y, w, h, fill=None, outline=0xFFFFFF)
maingroup.append(rectangle)
last_position = position
elif edit_mode and not edit_active:
# Edit mode navigation - cycle through editable items
# First restore previous item colors
item_areas[edit_current_index].color = 0xFFFFFF
if position > last_position:
edit_current_index = (edit_current_index + 1) % len(item_areas)
else:
edit_current_index = (edit_current_index - 1) % len(item_areas)
# Update rectangle position to highlight current item
edit_rectangle.x = 0
edit_rectangle.y = item_areas[edit_current_index].y - 7
last_position = position
elif edit_mode and edit_active:
# Actively editing a value
direction = 1 if position > last_position else -1
if current_index < 3: # Editing potentiometer
pot_settings = control_settings['pots'][current_index]
if edit_current_index == 0: # CC Number
pot_settings['cc_num'] = (pot_settings['cc_num'] + direction) % 128
item1_area.text = f"CC Number: {pot_settings['cc_num']}"
elif edit_current_index == 1: # Range editing
if range_edit_active:
# Actually edit the value
if range_edit_selection == 0: # Editing min
pot_settings['range_min'] = (pot_settings['range_min'] +
direction) % 128
range_min_label.text = f"{pot_settings['range_min']}"
else: # Editing max
pot_settings['range_max'] = (pot_settings['range_max'] +
direction) % 128
range_max_label.text = f"{pot_settings['range_max']}"
else:
# Special handling for range - switch between min and max
if direction > 0 and range_edit_selection == 0:
# Moving right from min, switch to max
range_edit_selection = 1
range_rectangle.x = range_max_label.x - 2
elif direction < 0 and range_edit_selection == 1:
# Moving left from max, switch to min
range_edit_selection = 0
range_rectangle.x = range_min_label.x - 2
elif edit_current_index == 2: # MIDI Channel
pot_settings['channel'] = (pot_settings['channel'] + direction) % 16
item3_area.text = f"MIDI Channel: {pot_settings['channel'] + 1}"
else: # Editing key
key_index = current_index - 3
key_settings = control_settings['keys'][key_index]
if edit_current_index == 0: # MIDI Note
key_settings['note'] = (key_settings['note'] + direction) % len(MIDI_NOTE_NAMES)
key_note_name = MIDI_NOTE_NAMES[key_settings['note']]
item1_area.text = f"MIDI Note: {key_note_name}"
# Update the main screen label too
keynote_labels[key_index].text = key_note_name
elif edit_current_index == 1: # Velocity
key_settings['velocity'] = (key_settings['velocity'] + direction) % 127
item2_area.text = f"Velocity: {key_settings['velocity']}"
elif edit_current_index == 2: # MIDI Channel
key_settings['channel'] = (key_settings['channel'] + direction) % 16
item3_area.text = f"MIDI Channel: {key_settings['channel'] + 1}"
last_position = position
pot_A_val1 = round(simpleio.map_range(val(pot_A), 65535, 0,
control_settings['pots'][0]['range_min'],
control_settings['pots'][0]['range_max']))
pot_B_val1 = round(simpleio.map_range(val(pot_B), 65535, 0,
control_settings['pots'][1]['range_min'],
control_settings['pots'][1]['range_max']))
pot_C_val1 = round(simpleio.map_range(val(pot_C), 65535, 0,
control_settings['pots'][2]['range_min'],
control_settings['pots'][2]['range_max']))
# if modulation value is updated...
if abs(pot_A_val1 - pot_A_val2) > 1:
# update pot_A_val2
pot_A_val2 = pot_A_val1
# create integer
modulation = int(pot_A_val2)
control_settings['pots'][0]['current_val'] = modulation
potvals_labels[0].text = str(modulation)
# create CC message
pot_settings = control_settings['pots'][0]
modWheel = ControlChange(pot_settings['cc_num'], modulation)
# send CC message
midi.send(modWheel)
if abs(pot_B_val1 - pot_B_val2) > 1:
# update pot_B_val2
pot_B_val2 = pot_B_val1
# create integer
ControllerB = int(pot_B_val2)
control_settings['pots'][1]['current_val'] = ControllerB
potvals_labels[1].text = str(ControllerB)
# create CC message
pot_settings = control_settings['pots'][1]
ControlB = ControlChange(pot_settings['cc_num'], ControllerB)
# send CC message
midi.send(ControlB)
if abs(pot_C_val1 - pot_C_val2) > 1:
# update pot_c_val2
pot_C_val2 = pot_C_val1
# create integer
ControllerC = int(pot_C_val2)
control_settings['pots'][2]['current_val'] = ControllerC
potvals_labels[2].text = str(ControllerC)
# create CC message
pot_settings = control_settings['pots'][2]
ControlC = ControlChange(pot_settings['cc_num'], ControllerC)
# send CC message
midi.send(ControlC)
Upload the Code and Libraries to the KB2040
After downloading the Project Bundle, plug your KB2040 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 KB2040's CIRCUITPY drive.
- lib folder
- code.py
Your KB2040 CIRCUITPY drive should look like this after copying the lib folder and code.py file:
How the CircuitPython Code Works
At the top of the code is a list of all of the MIDI note names as strings. They are in the same index position as their matching MIDI note number.
# midi note names
MIDI_NOTE_NAMES = [
"C-2", "C#-2", "D-2", "D#-2", "E-2", "F-2", "F#-2", "G-2", "G#-2", "A-2", "A#-2", "B-2",
"C-1", "C#-1", "D-1", "D#-1", "E-1", "F-1", "F#-1", "G-1", "G#-1", "A-1", "A#-1", "B-1",
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9"
]
This is followed by a dictionary called control_settings. This dictionary is used for the settings for the potentiometers and keys.
# Data structure to store all control settings
control_settings = {
'pots': [
{'cc_num': 1, 'range_min': 0, 'range_max': 127, 'channel': 0, 'current_val': 0}, # Pot A
{'cc_num': 10, 'range_min': 0, 'range_max': 127, 'channel': 0, 'current_val': 0}, # Pot B
{'cc_num': 11, 'range_min': 0, 'range_max': 127, 'channel': 0, 'current_val': 0}, # Pot C
],
'keys': [
{'note': 61, 'velocity': 120, 'channel': 0}, # Key 1
{'note': 64, 'velocity': 120, 'channel': 0}, # Key 2
{'note': 66, 'velocity': 120, 'channel': 0}, # Key 3
{'note': 68, 'velocity': 120, 'channel': 0}, # Key 4
{'note': 73, 'velocity': 120, 'channel': 0}, # Key 5
]
}
Hardware
The rotary encoder, keys and potentiometers are set up. The keys are instantiated with keypad and the button on the rotary encoder is setup as a debouncer Button to allow for long and short press detection.
# midi note numbers (will be replaced by control_settings)
midi_notes = [key['note'] for key in control_settings['keys']]
#rotary encoder and button
encoder = rotaryio.IncrementalEncoder(board.D3, board.D2)
last_position = 0
# midi setup
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
# Create keypad object with the pins (removed encoder button)
keys = keypad.Keys(
pins=(board.D4, board.D5, board.D6, board.D7, board.D8),
value_when_pressed=False, # Buttons pull to ground when pressed
pull=True # Enable internal pull-up resistors
)
# Set up encoder button with debouncer
encoder_button_pin = DigitalInOut(board.A3)
encoder_button_pin.direction = Direction.INPUT
encoder_button_pin.pull = digitalio.Pull.UP
encoder_button = Button(encoder_button_pin, value_when_pressed=False, long_duration_ms=500)
# potentiometer setup
pot_A = AnalogIn(board.A2)
pot_B = AnalogIn(board.A1)
pot_C = AnalogIn(board.A0)
Display and Bitmap Background
The OLED display is connected over I2C. The main.bmp image file is loaded in with the adafruit_imageload library.
#display setup
i2c = board.STEMMA_I2C()
displayio.release_displays()
# oled
oled_reset = board.D9
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset)
WIDTH = 128
HEIGHT = 64
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT)
bitmap, palette = adafruit_imageload.load("/main.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
# Create a TileGrid to hold the bitmap
bitmap_grid = displayio.TileGrid(bitmap, pixel_shader=palette)
Rectangles are used to highlight the different options on the main menu screen. A dictionary is used to hold the positions and dimensions for these rectangles.
# Create main page
maingroup = displayio.Group()
#dictionary for rectangle highlights
rect_dict = [
{'pos': (0, 0), 'dim': (42, 32)},
{'pos': (42, 0), 'dim': (42, 32)},
{'pos': (84, 0), 'dim': (42, 32)},
{'pos': (1, 32), 'dim': (25, 32)},
{'pos': (26, 32), 'dim': (25, 32)},
{'pos': (51, 32), 'dim': (25, 32)},
{'pos': (76, 32), 'dim': (25, 32)},
{'pos': (101, 32), 'dim': (25, 32)},
]
current_index = 0
rect_info = rect_dict[current_index]
x, y = rect_info['pos']
w, h = rect_info['dim']
# Create the rectangle (outline only, no fill)
rectangle = Rect(x, y, w, h, fill=None, outline=0xFFFFFF)
Value Text
Each of the keys have a text label, pulled from the MIDI_NOTE_NAMES list by passing the assigned MIDI note number as the index.
#text container
keynote_maingroup = displayio.Group()
font = terminalio.FONT
color = 0x000000
keynotes = [
{'num': control_settings['keys'][0]['note'], 'pos': (14, 42)},
{'num': control_settings['keys'][1]['note'], 'pos': (39, 42)},
{'num': control_settings['keys'][2]['note'], 'pos': (64, 42)},
{'num': control_settings['keys'][3]['note'], 'pos': (89, 42)},
{'num': control_settings['keys'][4]['note'], 'pos': (114, 42)},
]
keynote_labels = []
for keynote in keynotes:
keynote_area = label.Label(terminalio.FONT,
text=MIDI_NOTE_NAMES[keynote['num']],
color=0x000000)
keynote_area.anchor_point = (0.5, 0.0)
keynote_area.anchored_position = (keynote['pos'][0], keynote['pos'][1])
keynote_labels.append(keynote_area)
keynote_maingroup.append(keynote_area)
maingroup.append(keynote_maingroup)
Similarly, there are text labels for the values currently being sent from the potentiometers.
# labels for potentiometers
potval_maingroup = displayio.Group()
potvals = [
{'num': "0", 'pos': (22, 10)},
{'num': "0", 'pos': (64, 10)},
{'num': "0", 'pos': (106, 10)},
]
potvals_labels = []
for potval in potvals:
potval_area = label.Label(
terminalio.FONT,
text=potval['num'],
color=0x000000,
)
potval_area.anchor_point = (0.5, 0.0)
potval_area.anchored_position = (potval['pos'][0], potval['pos'][1])
potvals_labels.append(potval_area)
potval_maingroup.append(potval_area)
maingroup.append(potval_maingroup)
The Edit Menu
The editgroup is a second graphics group that holds the graphics elements for editing settings assigned to each key and potentiometer.
# Create edit page
editgroup = displayio.Group()
# Labels for Edit Page
header_area = label.Label(
terminalio.FONT,
text="",
color=0x000000,
x=0, y=10,
background_color=0xFFFFFF,
padding_left=34,
padding_right=34,
padding_top=2,
padding_bottom=2,
)
header_area.anchor_point = (0.5, 0.0)
header_area.anchored_position = (64, 2)
item1_area = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=4, y=25,)
item2_area = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=4, y=38,)
item3_area = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=4, y=52,)
# Create separate labels for range min and max
range_label = label.Label(terminalio.FONT, text="Range:", color=0xFFFFFF, x=4, y=38,)
range_min_label = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=65, y=38,)
range_dash_label = label.Label(terminalio.FONT, text="-", color=0xFFFFFF, x=85, y=38,)
range_max_label = label.Label(terminalio.FONT, text="", color=0xFFFFFF, x=95, y=38,)
item_areas = [item1_area, item2_area, item3_area]
edit_rectangle = Rect(0, 0, 128, 14, fill=None, outline=0xFFFFFF)
range_rectangle = Rect(0, 0, 20, 14, fill=0x000000, outline=0xFFFFFF)
editgroup.append(edit_rectangle)
editgroup.append(header_area)
editgroup.append(item1_area)
editgroup.append(item2_area)
editgroup.append(item3_area)
editgroup.append(range_rectangle)
range_rectangle.hidden = True
The function update_edit_display() is used to update the values in edit mode. There are different text elements when editing the potentiometers versus the keys, so this function handles showing the correct elements when one is selected.
# Function to update edit mode display
def update_edit_display(index):
# First, remove the range components if they exist in editgroup
if range_label in editgroup:
editgroup.remove(range_label)
editgroup.remove(range_min_label)
editgroup.remove(range_dash_label)
editgroup.remove(range_max_label)
if index < 3: # Potentiometer
p = control_settings['pots'][index]
header_area.text = f"EDIT POT {'ABC'[index]}"
item1_area.text = f"CC Number: {p['cc_num']}"
# Clear item2_area text since we'll use separate labels
item2_area.text = ""
# Add range components
editgroup.append(range_label)
editgroup.append(range_min_label)
editgroup.append(range_dash_label)
editgroup.append(range_max_label)
range_min_label.text = str(p['range_min'])
range_max_label.text = str(p['range_max'])
item3_area.text = f"MIDI Channel: {p['channel'] + 1}"
else: # Button
k = index - 3
s = control_settings['keys'][k]
note_name = MIDI_NOTE_NAMES[s['note']]
header_area.text = f"EDIT KEY {k + 1}"
item1_area.text = f"MIDI Note: {note_name}"
item2_area.text = f"Velocity: {s['velocity']}"
item3_area.text = f"MIDI Channel: {s['channel'] + 1}"
The Loop
In the loop, the rotary encoder button helps to navigate between the main menu and edit mode. A long press with an element highlighted on the main menu enters edit mode for that potentiometer or key. Short presses select different parameters to change the value of with the rotary encoder.
Outside of menu navigation, two short presses sends the MIDI panic message if you ever experience stuck notes.
while True:
# Update encoder button state
encoder_button.update()
# Handle encoder button presses
if encoder_button.short_count == 1:
if not edit_mode:
# Short press enters edit mode
update_edit_display(current_index)
edit_current_index = 0
edit_rectangle.x = 0
edit_rectangle.y = item_areas[edit_current_index].y - 7
edit_active = False
range_rectangle.hidden = True
display.root_group = editgroup
edit_mode = True
elif edit_mode and edit_active and current_index < 3 and edit_current_index == 1:
# In range editing mode, short press switches between min/max
range_edit_selection = 1 - range_edit_selection # Toggle between 0 and 1
if range_edit_selection == 0:
range_rectangle.x = range_min_label.x - 2
else:
range_rectangle.x = range_max_label.x - 2
else:
# Short press exits edit mode
display.root_group = maingroup
edit_mode = False
edit_active = False
range_edit_active = False
edit_rectangle.fill = None
item_areas[edit_current_index].color = 0xFFFFFF
if current_index < 3 and edit_current_index == 1:
range_min_label.color = 0xFFFFFF
range_max_label.color = 0xFFFFFF
range_label.color = 0xFFFFFF
range_dash_label.color = 0xFFFFFF
range_rectangle.hidden = True
if encoder_button.short_count == 2:
# send midi panic
panic = ControlChange(123, 120)
# send CC message
midi.send(panic)
# Handle long press in edit mode
if edit_mode and encoder_button.long_press:
if current_index < 3 and edit_current_index == 1:
# We're on the Range line
if not edit_active:
# Not in range edit mode yet, enter it
edit_active = True
range_edit_active = False
edit_rectangle.fill = 0xFFFFFF
# Show the range rectangle
range_rectangle.hidden = False
range_rectangle.fill = None
range_rectangle.outline = 0x000000
range_edit_selection = 0 # Start with min
# Position range rectangle over the min value
range_rectangle.x = range_min_label.x - 2
range_rectangle.y = range_min_label.y - 7
range_label.color = 0x000000
range_dash_label.color = 0x000000
range_max_label.color = 0x000000
range_min_label.color = 0x000000
elif edit_active and not range_edit_active:
# In selection mode, enter value editing mode
range_edit_active = True
range_rectangle.fill = 0x000000
range_min_label.color = 0xFFFFFF
range_max_label.color = 0xFFFFFF
elif edit_active and range_edit_active:
# In value editing mode, exit range editing completely
edit_active = False
range_edit_active = False
edit_rectangle.fill = None
# Hide range rectangle and restore colors
range_rectangle.hidden = True
range_label.color = 0xFFFFFF
range_dash_label.color = 0xFFFFFF
range_min_label.color = 0xFFFFFF
range_max_label.color = 0xFFFFFF
else:
# Non-range items - simple toggle
edit_active = not edit_active
range_edit_active = False # Reset range edit active state
# Change rectangle color to indicate active editing
if edit_active:
edit_rectangle.fill = 0xFFFFFF
# Make text black on white background for other items
item_areas[edit_current_index].color = 0x000000
else:
edit_rectangle.fill = None
# Make text white
item_areas[edit_current_index].color = 0xFFFFFF
Keypad
The keys use the keypad module. When a key is pressed, a NoteOn message with the selected MIDI note and velocity are sent. When the key is released, a matching NoteOff message is sent.
event = keys.events.get()
if event:
# event.key_number gives you the index (0-4) of which button
key_index = event.key_number
if event.pressed:
# Button was pressed - send NoteOn using settings
key_settings = control_settings['keys'][key_index]
midi.send(NoteOn(key_settings['note'], key_settings['velocity']))
if event.released:
# Button was released - send NoteOff using settings
key_settings = control_settings['keys'][key_index]
midi.send(NoteOff(key_settings['note'], key_settings['velocity']))
Rotary Encoder
The rotary encoder lets you navigate around the main menu. When you turn the encoder, the highlight rectangle is deleted and then recreated with the dimensions and position from the dictionary.
In edit mode, the rotary encoder lets you navigate around the different parameters to edit for each key or potentiometer. When a parameter it selected, it is used to change the value.
position = encoder.position
# Check if encoder moved
if position != last_position:
if not edit_mode:
# Main menu navigation
if position > last_position:
current_index = (current_index + 1) % len(rect_dict)
else:
current_index = (current_index - 1) % len(rect_dict)
rect_info = rect_dict[current_index]
x, y = rect_info['pos']
w, h = rect_info['dim']
# Remove old rectangle and create new one with updated dimensions
maingroup.remove(rectangle)
rectangle = Rect(x, y, w, h, fill=None, outline=0xFFFFFF)
maingroup.append(rectangle)
last_position = position
elif edit_mode and not edit_active:
# Edit mode navigation - cycle through editable items
# First restore previous item colors
item_areas[edit_current_index].color = 0xFFFFFF
if position > last_position:
edit_current_index = (edit_current_index + 1) % len(item_areas)
else:
edit_current_index = (edit_current_index - 1) % len(item_areas)
# Update rectangle position to highlight current item
edit_rectangle.x = 0
edit_rectangle.y = item_areas[edit_current_index].y - 7
last_position = position
elif edit_mode and edit_active:
# Actively editing a value
direction = 1 if position > last_position else -1
if current_index < 3: # Editing potentiometer
pot_settings = control_settings['pots'][current_index]
if edit_current_index == 0: # CC Number
pot_settings['cc_num'] = (pot_settings['cc_num'] + direction) % 128
item1_area.text = f"CC Number: {pot_settings['cc_num']}"
elif edit_current_index == 1: # Range editing
if range_edit_active:
# Actually edit the value
if range_edit_selection == 0: # Editing min
pot_settings['range_min'] = (pot_settings['range_min'] +
direction) % 128
range_min_label.text = f"{pot_settings['range_min']}"
else: # Editing max
pot_settings['range_max'] = (pot_settings['range_max'] +
direction) % 128
range_max_label.text = f"{pot_settings['range_max']}"
else:
# Special handling for range - switch between min and max
if direction > 0 and range_edit_selection == 0:
# Moving right from min, switch to max
range_edit_selection = 1
range_rectangle.x = range_max_label.x - 2
elif direction < 0 and range_edit_selection == 1:
# Moving left from max, switch to min
range_edit_selection = 0
range_rectangle.x = range_min_label.x - 2
elif edit_current_index == 2: # MIDI Channel
pot_settings['channel'] = (pot_settings['channel'] + direction) % 16
item3_area.text = f"MIDI Channel: {pot_settings['channel'] + 1}"
else: # Editing key
key_index = current_index - 3
key_settings = control_settings['keys'][key_index]
if edit_current_index == 0: # MIDI Note
key_settings['note'] = (key_settings['note'] + direction) % len(MIDI_NOTE_NAMES)
key_note_name = MIDI_NOTE_NAMES[key_settings['note']]
item1_area.text = f"MIDI Note: {key_note_name}"
# Update the main screen label too
keynote_labels[key_index].text = key_note_name
elif edit_current_index == 1: # Velocity
key_settings['velocity'] = (key_settings['velocity'] + direction) % 127
item2_area.text = f"Velocity: {key_settings['velocity']}"
elif edit_current_index == 2: # MIDI Channel
key_settings['channel'] = (key_settings['channel'] + direction) % 16
item3_area.text = f"MIDI Channel: {key_settings['channel'] + 1}"
last_position = position
Potentiometers
The potentiometer values are mapped to the range defined in the control_settings dictionary for each potentiometer. When each potentiometer is turned, it sends a MIDI CC message out.
pot_A_val1 = round(simpleio.map_range(val(pot_A), 65535, 0,
control_settings['pots'][0]['range_min'],
control_settings['pots'][0]['range_max']))
pot_B_val1 = round(simpleio.map_range(val(pot_B), 65535, 0,
control_settings['pots'][1]['range_min'],
control_settings['pots'][1]['range_max']))
pot_C_val1 = round(simpleio.map_range(val(pot_C), 65535, 0,
control_settings['pots'][2]['range_min'],
control_settings['pots'][2]['range_max']))
# if modulation value is updated...
if abs(pot_A_val1 - pot_A_val2) > 1:
# update pot_A_val2
pot_A_val2 = pot_A_val1
# create integer
modulation = int(pot_A_val2)
control_settings['pots'][0]['current_val'] = modulation
potvals_labels[0].text = str(modulation)
# create CC message
pot_settings = control_settings['pots'][0]
modWheel = ControlChange(pot_settings['cc_num'], modulation)
# send CC message
midi.send(modWheel)
if abs(pot_B_val1 - pot_B_val2) > 1:
# update pot_B_val2
pot_B_val2 = pot_B_val1
# create integer
ControllerB = int(pot_B_val2)
control_settings['pots'][1]['current_val'] = ControllerB
potvals_labels[1].text = str(ControllerB)
# create CC message
pot_settings = control_settings['pots'][1]
ControlB = ControlChange(pot_settings['cc_num'], ControllerB)
# send CC message
midi.send(ControlB)
if abs(pot_C_val1 - pot_C_val2) > 1:
# update pot_c_val2
pot_C_val2 = pot_C_val1
# create integer
ControllerC = int(pot_C_val2)
control_settings['pots'][2]['current_val'] = ControllerC
potvals_labels[2].text = str(ControllerC)
# create CC message
pot_settings = control_settings['pots'][2]
ControlC = ControlChange(pot_settings['cc_num'], ControllerC)
# send CC message
midi.send(ControlC)
Page last edited August 13, 2025
Text editor powered by tinymce.