Import the Libraries

First, the CircuitPython libraries are imported.

import time
import board
import displayio
import terminalio
import adafruit_aw9523
import busio
import adafruit_ssd1327
import digitalio
from adafruit_display_text import label
from import Circle
from adafruit_display_shapes.rect import Rect
import usb_midi
import adafruit_midi
from adafruit_midi.note_on          import NoteOn
from adafruit_midi.note_off         import NoteOff

I2C and MIDI Setup

I2C is setup to use the Pico's GP0 and GP1 pins. You have two I2C devices in this project: the Grayscale 1.5" 128x128 OLED Display and the AW9523 GPIO Expander and LED Driver. 

midi is also setup to act as a USB MIDI output device. midi_out sends notes out from the device.

# i2c setup, higher frequency for display refresh
i2c = busio.I2C(board.GP1, board.GP0, frequency=1000000)
#  i2c display setup
display_bus = displayio.I2CDisplay(i2c, device_address=0x3D)
#  i2c AW9523 GPIO expander setup
aw = adafruit_aw9523.AW9523(i2c)
#  MIDI setup as MIDI out device
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)

Display Setup

This project utilizes a graphical user interface (GUI) to let you change the MIDI note numbers assigned to each of the arcade buttons. Each button is represented on the display as a circle. The code uses the Circle object from the adafruit_display_shapes library to easily draw circles on the display without having to import a bitmap.

spots holds the list of coordinates for each of the circles and the for statement creates each circle and assigns them to the correct coordinate.

A rectangle is also created using the Rect object from the adafruit_display_shapes library. This rectangle is used to highlight the currently selected circle on the display.

# display dimensions
WIDTH = 128
HEIGHT = 128
#  display setup
display = adafruit_ssd1327.SSD1327(display_bus, width=WIDTH, height=HEIGHT, brightness = 0.01)

#  main display group, shows default GUI menu
splash = displayio.Group()
#  group for circle icons
circle_group = displayio.Group()
#  group for text labels on circles
text_group = displayio.Group()

#  list of circle positions
spots = (
    (16, 16),
    (48, 16),
    (80, 16),
    (112, 16),
    (16, 48),
    (48, 48),
    (80, 48),
    (112, 48),
    (16, 80),
    (48, 80),
    (80, 80),
    (112, 80),
    (16, 112),
    (48, 112),
    (80, 112),
    (112, 112),

#  creating the circles & pulling in positions from spots
for spot in spots:
    circle = Circle(x0=spot[0], y0=spot[1], r=14, fill=0x888888)
	#  adding circles to their display group
#  square to show position on menu
rect = Rect(0, 0, 33, 33, fill=None, outline=0x00FF00, stroke = 3)


MIDI Note Labels

Each circle has text that shows the currently assigned MIDI note number for each arcade button. This information is stored in texts along with the coordinates for each string's location. The for statement creates each text object, pulling this information from texts, and stores them in the text_labels array.

#  strings and positions for the MIDI note text labels
texts = [
    {'num': "60", 'pos': (12, 16)},
    {'num': "61", 'pos': (44, 16)},
    {'num': "62", 'pos': (76, 16)},
    {'num': "63", 'pos': (108, 16)},
    {'num': "64", 'pos': (12, 48)},
    {'num': "65", 'pos': (44, 48)},
    {'num': "66", 'pos': (76, 48)},
    {'num': "67", 'pos': (108, 48)},
    {'num': "68", 'pos': (12, 80)},
    {'num': "69", 'pos': (44, 80)},
    {'num': "70", 'pos': (76, 80)},
    {'num': "71", 'pos': (108, 80)},
    {'num': "72", 'pos': (12, 112)},
    {'num': "73", 'pos': (44, 112)},
    {'num': "74", 'pos': (76, 112)},
    {'num': "75", 'pos': (108, 112)},
text_labels = []

for text in texts:
    text_area = label.Label(terminalio.FONT, text=text['num'], color=0xFFFFFF)
    text_area.x = text['pos'][0]
    text_area.y = text['pos'][1]

Secondary GUI Menu

In addition to the main GUI, there is a secondary GUI. When you select an arcade button's MIDI note to edit, the display shows a large circle with large text showing the MIDI note number that you're editing. This secondary GUI is stored in big_splash.

#  secondary display group, shows large circle when button is selected
big_splash = displayio.Group()
#  large circle to fill display
big_circle = Circle(x0=64, y0=64, r=62, fill=0x888888)
#  large text to fill circle
big_text = label.Label(terminalio.FONT, text='   ', color=0xFFFFFF)
big_text.x = 43
big_text.y = 62
big_text.scale = 4

LEDs with the AW9523

The arcade button's LEDs are controlled with the AW9523 GPIO expander. The I/O of the AW9523 is accessed with aw.get_pin(pin_number). The pin numbers are stored in the led_pins array and the for statement sets up the pins to be outputs.

#  array for LEDs on AW9523
leds = []
led_pins = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
#  setup to create the AW9523 outputs for LEDs
for led in led_pins:
    led_pin = aw.get_pin(led)
    led_pin.direction = digitalio.Direction.OUTPUT

Arcade Button Pins

The pins used for the arcade buttons are stored in the note_pins array. They are setup to be digital inputs in the for statement and are then stored in the note_buttons array. 

Each arcade button has a state setup for debouncing. These states are stored in the note_states array.

#  button pins, all pins in order skipping GP15
note_pins = [board.GP7, board.GP8, board.GP9, board.GP10, board.GP11,
             board.GP12, board.GP13, board.GP14, board.GP16, board.GP17,
             board.GP18, board.GP19, board.GP20, board.GP21, board.GP22, board.GP26]

note_buttons = []

for pin in note_pins:
    note_pin = digitalio.DigitalInOut(pin)
    note_pin.direction = digitalio.Direction.INPUT
    note_pin.pull = digitalio.Pull.UP

#  note states
note0_pressed = False
note1_pressed = False
note2_pressed = False
note3_pressed = False
note4_pressed = False
note5_pressed = False
note6_pressed = False
note7_pressed = False
note8_pressed = False
note9_pressed = False
note10_pressed = False
note11_pressed = False
note12_pressed = False
note13_pressed = False
note14_pressed = False
note15_pressed = False
#  array of note states
note_states = [note0_pressed, note1_pressed, note2_pressed, note3_pressed,
               note4_pressed, note5_pressed, note6_pressed, note7_pressed,
               note8_pressed, note9_pressed, note10_pressed, note11_pressed,
               note12_pressed, note13_pressed, note14_pressed, note15_pressed]

5-Way Navigation Switch

The GUI is navigated with a 5-way switch. This allows you to move in all directions around the screen and select the arcade button that you want to edit. The digital pins for the 5-way switch are stored in the joystick array and are setup as inputs in the for statement.

#  pins for 5-way switch
select = digitalio.DigitalInOut(board.GP6)
up = digitalio.DigitalInOut(board.GP5)
down = digitalio.DigitalInOut(board.GP4)
left = digitalio.DigitalInOut(board.GP3)
right = digitalio.DigitalInOut(board.GP2)
#  array for 5-way switch
joystick = [select, up, down, left, right]

for joy in joystick:
    joy.direction = digitalio.Direction.INPUT
    joy.pull = digitalio.Pull.UP

State Machines

There are a few state machines used in the code. Each pin for the 5-way switch has a state for debouncing. The other states' functionality is commented below.

#  states for 5-way switch
select_state = None
up_state = None
down_state = None
left_state = None
right_state = None
midi_state = None

#  coordinates for navigating main GUI
select_x = [0, 32, 64, 96]
select_y = [0, 32, 64, 96]

#  y coordinate for 5-way switch navigation
y_pos = 0
#  x coordinate for 5-way switch navigation
x_pos = 0
sub_state = False
#  default midi number
midi_num = 60
#  default MIDI button
button_num = 0
#  default MIDI button position
button_pos = 0
#  check for blinking LED
led_check = None
#  time.monotonic() device
clock = time.monotonic()

GUI Navigation Setup

The navigation for the GUI works by counting the number of times each directional input from the 5-way switch is pressed. The combinations of these counts are stored in the switch_coordinates array to act as x and y coordinates on the GUI. It's helpful to think of the arcade buttons as a 4x4 grid.

#  coordinates for tracking location of 5-way switch
up_scroll = 0
down_scroll = 0
left_scroll = 0
right_scroll = 0
switch_coordinates = [(0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (1, 1), (2, 1), (3, 1), (0, 2),
            (1, 2), (2, 2), (3, 2), (0, 3), (1, 3), (2, 3), (3, 3)]

MIDI Note Array

The midi_notes array holds the default MIDI notes that are assigned to the arcade buttons. If you want to change the MIDI notes that are loaded after powering the MIDI Fighter, you'll want to edit this array.

#  array of default MIDI notes
midi_notes = [60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75]

The Loop

Switch Debouncing

The loop begins by debouncing the five inputs of the 5-way switch.

while True:

    #  debouncing for 5-way switch positions
    if up.value and up_state == "pressed":
        print("Button pressed.")
        up_state = None
    if down.value and down_state == "pressed":
        print("Button pressed.")
        down_state = None
    if left.value and left_state == "pressed":
        print("Button pressed.")
        left_state = None
    if right.value and right_state == "pressed":
        print("Button pressed.")
        right_state = None
    if select.value and select_state == "pressed":
        print("Button pressed.")
        select_state = None

MIDI Input

The arcade buttons send their assigned MIDI note number out with a MIDI NoteOn message when they are pressed. Additionally, when you press an arcade button, its LED lights up with the AW9523. When the arcade button is released, a NoteOff message is sent and the LED is turned off.

#  MIDI input
    for i in range(16):
        buttons = note_buttons[i]
        #  if button is pressed...
        if not buttons.value and note_states[i] is False:
            #  send the MIDI note and light up the LED
            midi.send(NoteOn(midi_notes[i], 120))
            note_states[i] = True
            leds[i].value = True
        #  if the button is released...
        if buttons.value and note_states[i] is True:
            #  stop sending the MIDI note and turn off the LED
            midi.send(NoteOff(midi_notes[i], 120))
            note_states[i] = False
            leds[i].value = False

Main GUI Navigation

The main GUI is navigated using the 5-way switch. Every time you press up, down, left or right, the values of up_scroll, down_scroll, left_scroll or right_scroll are updated with a count between 0 and 3. These values are used as coordinates to track where you are on the GUI. 

y_pos and x_pos also hold these values and are used as array indexes to update the highlighting square's position on the GUI.

#  if we're on the main GUI page
    if not sub_state:
        #  if you press up on the 5-way switch...
        if not up.value and up_state is None:
            up_state = "pressed"
            #  track the switch's position
            up_scroll -= 1
            if up_scroll < 0:
                up_scroll = 3
            y_pos = up_scroll
            down_scroll = up_scroll
        #  if you press down on the 5-way switch...
        if not down.value and down_state is None:
            down_state = "pressed"
            #  track the switch's position
            down_scroll += 1
            if down_scroll > 3:
                down_scroll = 0
            y_pos = down_scroll
            up_scroll = down_scroll
        #  if you press left on the 5-way switch...
        if not left.value and left_state is None:
            # print("scroll", down_scroll)
            left_state = "pressed"
            #  track the switch's position
            left_scroll -= 1
            if left_scroll < 0:
                left_scroll = 3
            x_pos = left_scroll
            right_scroll = left_scroll
        #  if you press right on the 5-way switch...
        if not right.value and right_state is None:
            # print("scroll", down_scroll)
            right_state = "pressed"
            #  track the switch's position
            right_scroll += 1
            if right_scroll > 3:
                right_scroll = 0
            x_pos = right_scroll
            left_scroll = right_scroll

        #  update square's position on the GUI
        rect.y = select_y[y_pos]
        rect.x = select_x[x_pos]

Track the Button

In order to keep track of which button on the GUI is highlighted, the x_pos and y_pos values are compared to the switch_coordinates array to track which button is highlighted on the screen. This is how that button's value can then be affected in the secondary GUI.

button_num is used to track the MIDI note number for the currently selected button.

#  update the currently highlighted button on the GUI
        for coords in switch_coordinates:
            if x_pos == coords[0] and y_pos == coords[1]:
                button_pos = switch_coordinates.index(coords)
                #  print(button_pos)
        button_num = text_labels[button_pos].text

Selecting a Button to Edit

When you have navigated to your chosen arcade button's position, you can press select on the 5-way switch to enter the editing mode for that button. midi_num grabs the highlighted button's MIDI note number so that you'll be able to edit and update that number.

#  if you press select on the 5-way switch...
        if not select.value and select_state is None:
            select_state = "pressed"
            #  grab the selected button's MIDI note
            midi_num = int(button_num)
            #  change into the secondary GUI menu
            sub_state = True

Secondary GUI: Edit the Arcade Button's MIDI Note Number

When you enter the editing mode, the secondary GUI is displayed. Your selected button's LED will also blink on and off until you exit this mode. The blinking is done using time.monotonic() so that it doesn't interrupt anything else happening in the loop.

#  if an arcade button is selected to change the MIDI note...
    if sub_state:
        #  display the secondary GUI menu
        display.root_group = big_splash
        #  display the selected button's MIDI note
        big_text.text = midi_num

        #  blink the selected button's LED without pausing the loop
        if (time.monotonic() > (clock + 1)) and led_check is None:
            leds[button_pos].value = True
            led_check = True
            clock = time.monotonic()
        if (time.monotonic() > (clock + 1)) and led_check is True:
            leds[button_pos].value = False
            led_check = None
            clock = time.monotonic()

MIDI Note Number Range

A MIDI note range is setup so that you don't go below 0 or above 128.

#  blocks the MIDI number from being set above 128
        if midi_num >= 128:
            midi_num = 128
        #  blocks the MIDI number from being set below 0
        if midi_num <= 0:
            midi_num = 0

Adjusting the MIDI Note

The MIDI note number can be increased by pressing up or right with the 5-way switch and decreased by pressing down or left with the 5-way switch. The value of midi_num is either increased or decreased by 1 depending on the input.

#  if you press right on the 5-way switch...
        if not right.value and right_state is None:
            #  increase the MIDI number
            midi_num += 1
            right_state = "pressed"
        #  if you press up on the 5-way switch...
        if not up.value and up_state is None:
            #  increase the MIDI number
            midi_num += 1
            up_state = "pressed"
        #  if you press left on the 5-way switch...
        if not left.value and left_state is None:
            #  decrease the MIDI number
            midi_num -= 1
            left_state = "pressed"
        #  if you press down on the 5-way switch...
        if not down.value and down_state is None:
            #  decrease the MIDI number
            midi_num -= 1
            down_state = "pressed"

Update the MIDI Note

The value of the selected arcade button's MIDI note is adjusted in real time. This allows you to play the note while you're adjusting to make sure it's the correct note.

#  update arcade button's MIDI note
        #  allows you to check note while you're adjusting it
        midi_notes[button_pos] = midi_num

Save the New MIDI Note

After deciding on your MIDI note, you can press select again on the 5-way switch to save your choice. This updates the text label on the main GUI, stops the LED from blinking and brings you back to the main GUI on the display.

#  if you press select on the 5-way switch...
        if not select.value and select_state is None:
            select_state = "pressed"
            #  change back to main menu mode
            sub_state = False
            #  update new MIDI number text label
            text_labels[button_pos].text = midi_num
            #  show main GUI display
            display.root_group = splash
            #  turn off blinking LED
            leds[button_pos].value = False

This guide was first published on Mar 10, 2021. It was last updated on Jun 15, 2024.

This page (CircuitPython Code Walkthrough) was last updated on Mar 28, 2024.

Text editor powered by tinymce.