Once you've finished setting up your Metro RP2350 with CircuitPython, you can access the code, MIDI files 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 for Adafruit Industries # # SPDX-License-Identifier: MIT import os from random import randint import board import usb_midi import keypad from adafruit_mcp230xx.mcp23017 import MCP23017 import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff import adafruit_midi_parser # music_box plays back MIDI files on CP drive # set to false for live MIDI over USB control music_box = True # define the notes that correspond to each solenoid notes = [48, 50, 52, 53, 55, 57, 59, 60] key = keypad.Keys((board.BUTTON,), value_when_pressed=False, pull=True) i2c = board.STEMMA_I2C() mcp = MCP23017(i2c) noids = [] for i in range(8): noid = mcp.get_pin(i) noid.switch_to_output(value=False) noids.append(noid) # pylint: disable=used-before-assignment, unused-argument, global-statement, no-self-use if not music_box: midi = adafruit_midi.MIDI( midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0 ) else: midi_files = [] for filename in os.listdir('/'): if filename.lower().endswith('.mid') and not filename.startswith('.'): midi_files.append("/"+filename) print(midi_files) class Custom_Player(adafruit_midi_parser.MIDIPlayer): def on_note_on(self, note, velocity, channel): # noqa: PLR6301 for z in range(len(notes)): if notes[z] == note: print(f"Playing note: {note}") noids[z].value = True def on_note_off(self, note, velocity, channel): # noqa: PLR6301 for z in range(len(notes)): if notes[z] == note: noids[z].value = False def on_end_of_track(self, track): # noqa: PLR6301 print(f"End of track {track}") for z in range(8): noids[z].value = False def on_playback_complete(self): # noqa: PLR6301 global now_playing now_playing = False for z in range(8): noids[z].value = False parser = adafruit_midi_parser.MIDIParser() parser.parse(midi_files[randint(0, (len(midi_files) - 1))]) player = Custom_Player(parser) new_file = False now_playing = False while True: if music_box: event = key.events.get() if event: if event.pressed: now_playing = not now_playing if now_playing: new_file = True if new_file: parser.parse(midi_files[randint(0, (len(midi_files) - 1))]) print(f"Successfully parsed! Found {len(parser.events)} events.") print(f"BPM: {parser.bpm:.1f}") print(f"Note Count: {parser.note_count}") new_file = False if now_playing: player.play(loop=False) else: msg = midi.receive() if msg is not None: for i in range(8): noid_output = noids[i] notes_played = notes[i] if isinstance(msg, NoteOn) and msg.note == notes_played: noid_output.value = True elif isinstance(msg, NoteOff) and msg.note == notes_played: noid_output.value = False
Upload the Code and Libraries to the Metro RP2350
After downloading the Project Bundle, plug your Metro RP2350 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 Metro RP2350's CIRCUITPY drive.
- lib folder
- code.py
- song_1.mid
- song_2.mid
- song_3.mid
- song_4.mid
Your Metro RP2350 CIRCUITPY drive should look like this after copying the lib folder, MIDI files and code.py file:

How the Code Works
At the top of the code is the music_box
variable. This determines the mode for the robot xylophone. If music_box
is set to True
, then it will playback MIDI files (.mid) files on the CIRCUITPY drive. If it is set to False
, then it will act as a USB MIDI output device. The MIDI note numbers assigned to each solenoid are defined in the notes
array.
# music_box plays back MIDI files on CP drive # set to false for live MIDI over USB control music_box = True # define the notes that correspond to each solenoid notes = [48, 50, 52, 53, 55, 57, 59, 60]
Button and I2C
The BOOT button is passed as a keypad
button. Then, the MCP23017 is instantiated over I2C and the 8 solenoid pins are created and added to the noids
array.
key = keypad.Keys((board.BUTTON,), value_when_pressed=False, pull=True) i2c = board.STEMMA_I2C() mcp = MCP23017(i2c) noids = [] for i in range(8): noid = mcp.get_pin(i) noid.switch_to_output(value=False) noids.append(noid)
if not music_box: midi = adafruit_midi.MIDI( midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0 )
Music Box Mode
If music_box
is True
, then the Metro is setup to playback MIDI files on the CIRCUITPY drive. First, all of the .mid files on the drive are found and added to the midi_files
array. Then, a MIDIPlayer
class is created to define what is done in the code when certain MIDI messages are read from a MIDI file. In the case of the xylophone, if a NoteOn message is received, the solenoid is triggered. When a NoteOff message is received, the solenoid retracts. At the end of the MIDI file, all solenoids are set to False
to make sure none of them are stuck on.
else: midi_files = [] for filename in os.listdir('/'): if filename.lower().endswith('.mid') and not filename.startswith('.'): midi_files.append("/"+filename) print(midi_files) class Custom_Player(adafruit_midi_parser.MIDIPlayer): def on_note_on(self, note, velocity, channel): # noqa: PLR6301 for z in range(len(notes)): if notes[z] == note: print(f"Playing note: {note}") noids[z].value = True def on_note_off(self, note, velocity, channel): # noqa: PLR6301 for z in range(len(notes)): if notes[z] == note: noids[z].value = False def on_end_of_track(self, track): # noqa: PLR6301 print(f"End of track {track}") for z in range(8): noids[z].value = False def on_playback_complete(self): # noqa: PLR6301 global now_playing now_playing = False for z in range(8): noids[z].value = False parser = adafruit_midi_parser.MIDIParser() parser.parse(midi_files[randint(0, (len(midi_files) - 1))]) player = Custom_Player(parser) new_file = False now_playing = False
The Loop
When you press the BOOT button in music box mode, a MIDI file on the CIRCUITPY drive is chosen and queued up to be parsed. As it is read back, the file is played on the xylophone.
while True: if music_box: event = key.events.get() if event: if event.pressed: now_playing = not now_playing if now_playing: new_file = True if new_file: parser.parse(midi_files[randint(0, (len(midi_files) - 1))]) print(f"Successfully parsed! Found {len(parser.events)} events.") print(f"BPM: {parser.bpm:.1f}") print(f"Note Count: {parser.note_count}") new_file = False if now_playing: player.play(loop=False)
In live MIDI mode, whenever a NoteOn or NoteOff message is received, the solenoids strike a note.
else: msg = midi.receive() if msg is not None: for i in range(8): noid_output = noids[i] notes_played = notes[i] if isinstance(msg, NoteOn) and msg.note == notes_played: noid_output.value = True elif isinstance(msg, NoteOff) and msg.note == notes_played: noid_output.value = False
Page last edited May 20, 2025
Text editor powered by tinymce.