Overview
Build a MIDI visualizer with NeoPixel LEDs and the PropMaker Feather RP2040. A 3D printed grid display is inspired by the light billboard from the movie, Close Encounters of the Third Kind.
When connected with a MIDI keyboard and DAW (digital audio workstation), as you play, multi-colored LEDs light up the display with each MIDI note mapped to an LED NeoPixel.
The NeoPixel LED matrix is a 6x12 arrangement that features a grid of rectangular segments. A piece of acrylic is overlaid over the grid to diffuse the LEDs.
When a MIDI note is received, a corresponding segment is lit up in the grid. A total of 72 LEDs are mapped to MIDI notes C1-C6. Each row of NeoPixels is mapped to an octave, with a total of 6 available octaves.
Page last edited April 01, 2026
Text editor powered by tinymce.
Circuit Diagram
The diagram below provides a general visual reference for wiring of the components once you get to the Assembly page. This diagram was created using the software package Fritzing.
Adafruit Library for Fritzing
Adafruit uses the Adafruit Fritzing parts library to create circuit diagrams for projects. You can download the library or just grab individual parts. Get the library and parts from GitHub - Adafruit Fritzing Parts.
Page last edited April 01, 2026
Text editor powered by tinymce.
CircuitPython
CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.
CircuitPython Quickstart
Follow this step-by-step to quickly get CircuitPython running on your board.
Click the link above to download the latest CircuitPython UF2 file.
Save it wherever is convenient for you.
To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in red or blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!
If the drive does not appear, release all the buttons, and then repeat the process above.
You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.
A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.
You will see a new disk drive appear called RPI-RP2.
Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.
The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.
That's it, you're done! :)
Safe Mode
You want to edit your code.py or modify the files on your CIRCUITPY drive, but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.
Safe mode in CircuitPython does not run any user code on startup, and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.
Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.
To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)
In Safe Mode
If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.
If you connect to the serial console, you'll find the following message.
Auto-reload is off. Running in safe mode! Not running saved code. CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode. Press any key to enter the REPL. Use CTRL-D to reload.
You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.
Flash Resetting UF2
If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RPI-RP2. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.
Page last edited April 01, 2026
Text editor powered by tinymce.
Code
Code the MIDI NeoPixel Visualizer
Once you've finished setting up your Feather Prop Maker 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: 2026 Noe Ruiz for Adafruit Industries
# SPDX-License-Identifier: MIT
import time
import board
import usb_midi
import adafruit_midi
from digitalio import DigitalInOut, Direction
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
import neopixel
# enable external power pin
# provides power to the external components
external_power = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value = True
# NeoPixel LED Setup
NUMPIXELS = 72
BRIGHTNESS = 1
PIN = board.EXTERNAL_NEOPIXELS
ORDER = neopixel.BGR
pixels = neopixel.NeoPixel(PIN, NUMPIXELS, brightness=BRIGHTNESS,
auto_write=False, pixel_order=ORDER)
# Matrix layout
MATRIX_ROWS = 6
MATRIX_COLS = 12 # One full chromatic octave per row
# MIDI setup - listen on channels 1 and 2 (0-indexed: 0 and 1)
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
midi_in=usb_midi.ports[0], in_channel=(0, 1), midi_out=usb_midi.ports[1], out_channel=0
)
# MIDI note number that maps to Pixel 0 (top-left of matrix).
# 24 = C1, 36 = C2, 48 = C3
NOTE_OFFSET = 24
# Chromatic note names used for print statements
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
# Base color palette - 24 colors spanning two octaves of the chromatic scale.
# The first 12 colors cover one octave, the next 12 cover the second octave
# with a distinct shifted palette. Colors then tile across all 6 rows.
BASE_COLORS = [
# Octave A - Warm to Cool spectrum
(255, 0, 0), # C - Red
(255, 45, 0), # C# - Red-Orange
(255, 90, 0), # D - Orange
(255, 145, 0), # D# - Amber
(255, 200, 0), # E - Yellow-Orange
(255, 255, 0), # F - Yellow
(128, 255, 0), # F# - Yellow-Green
(0, 255, 0), # G - Green
(0, 255, 128), # G# - Spring Green
(0, 255, 255), # A - Cyan
(0, 128, 255), # A# - Sky Blue
(0, 0, 255), # B - Blue
# Octave B - Rich and saturated shifted palette
(64, 0, 255), # C - Indigo
(128, 0, 255), # C# - Violet
(200, 0, 255), # D - Purple
(255, 0, 200), # D# - Magenta
(255, 0, 128), # E - Hot Pink
(255, 0, 64), # F - Deep Rose
(255, 64, 64), # F# - Salmon
(255, 128, 128), # G - Light Coral
(255, 200, 128), # G# - Peach
(255, 255, 128), # A - Pale Yellow
(128, 255, 128), # A# - Mint
(128, 255, 255), # B - Ice Blue
]
# Expand BASE_COLORS to a full 72-entry list by repeating the 24-color pattern.
PIXEL_COLORS = [BASE_COLORS[i % len(BASE_COLORS)] for i in range(NUMPIXELS)]
FADE_DURATION = 0.1 # Total fade duration in seconds
FADE_STEPS = 5 # Number of steps in the fade
# Non-blocking fade state per pixel:
# pixel_index -> {"color": (r,g,b), "step": int, "last_time": float}
fading_pixels = {}
def note_to_matrix(note):
"""
Map a MIDI note to a (row, col) position in the matrix.
Each row is one octave (12 notes). C always starts at column 0.
NOTE_OFFSET determines which note maps to (row=0, col=0).
"""
offset_note = note - NOTE_OFFSET
r = (offset_note // MATRIX_COLS) % MATRIX_ROWS
c = offset_note % MATRIX_COLS
return r, c
def matrix_to_pixel(r, c):
"""
Convert a (row, col) matrix position to a physical pixel index,
accounting for zigzag wiring.
Even rows (0, 2, 4) run left to right.
Odd rows (1, 3, 5) run right to left.
"""
if r % 2 == 0:
return r * MATRIX_COLS + c # Left to right
else:
return r * MATRIX_COLS + (MATRIX_COLS - 1 - c) # Right to left
def note_to_pixel(note):
"""Map a MIDI note directly to a physical pixel index via the matrix."""
r, c = note_to_matrix(note)
return matrix_to_pixel(r, c)
def note_to_name(note):
"""Return a human-readable note name and octave, e.g. 'C2' for note 36."""
name = NOTE_NAMES[note % 12]
octave = (note // 12) - 1
return f"{name}{octave}"
def color_for_note(note):
"""
Look up the color for a note based on its position across two octaves.
The 24-color palette tiles every two octaves across the matrix rows.
"""
offset_note = note - NOTE_OFFSET
return BASE_COLORS[offset_note % len(BASE_COLORS)]
def color_wipe(c, delay=0.01):
"""Wipe a color across the strip one LED at a time."""
for i in range(NUMPIXELS):
pixels[i] = c
pixels.show()
time.sleep(delay)
def boot_sequence():
"""Animated boot sequence using color wipes."""
color_wipe((255, 0, 0), 0.01) # Red wipe
color_wipe((0, 255, 0), 0.01) # Green wipe
color_wipe((0, 0, 255), 0.01) # Blue wipe
color_wipe((0, 0, 0), 0.01) # Wipe off
def update_fades():
"""Call this every loop iteration to advance any active fades."""
now = time.monotonic()
completed = []
for pix_index, state in fading_pixels.items():
step_delay = FADE_DURATION / FADE_STEPS
if now - state["last_time"] >= step_delay:
state["step"] += 1
state["last_time"] = now
if state["step"] >= FADE_STEPS:
pixels[pix_index] = (0, 0, 0)
pixels.show()
completed.append(pix_index)
else:
r, g, b = state["color"]
factor = (FADE_STEPS - state["step"]) / FADE_STEPS
pixels[pix_index] = (int(r * factor), int(g * factor), int(b * factor))
pixels.show()
for pix_index in completed:
del fading_pixels[pix_index]
# Run boot sequence on startup
boot_sequence()
while True:
msg = midi.receive()
if isinstance(msg, NoteOn) and msg.velocity > 0:
pixel_index = note_to_pixel(msg.note)
color = color_for_note(msg.note)
note_name = note_to_name(msg.note)
row, col = note_to_matrix(msg.note)
# Immediately cancel any active fade on this pixel
if pixel_index in fading_pixels:
del fading_pixels[pixel_index]
pixels[pixel_index] = color
pixels.show()
print(f"Note ON: {note_name} ({msg.note}) Pixel {pixel_index}, Color {color}")
elif isinstance(msg, NoteOff) or (isinstance(msg, NoteOn) and msg.velocity == 0):
pixel_index = note_to_pixel(msg.note)
color = color_for_note(msg.note)
note_name = note_to_name(msg.note)
row, col = note_to_matrix(msg.note)
fading_pixels[pixel_index] = {
"color": color,
"step": 0,
"last_time": time.monotonic()
}
print(f"Note OFF: {note_name} ({msg.note}) Pixel {pixel_index} fading")
# Advance all active fades each loop iteration
update_fades()
Upload the Code and Libraries to the Feather Prop Maker
After downloading the Project Bundle, plug your Feather Prop Maker 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 Prop Maker's CIRCUITPY drive.
- lib folder
- code.py
Your Feather Prop Maker 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, the EXTERNAL_POWER pin is enabled to power up the peripherals on the Feather.
# enable external power pin # provides power to the external components external_power = DigitalInOut(board.EXTERNAL_POWER) external_power.direction = Direction.OUTPUT external_power.value = True
NeoPixels
The NeoPixels are set up and use the board.EXTERNAL_NEOPIXELS pin that is on the PropMaker Feather's screw-block terminal. The NeoPixel Pebble/Seed strands uses a special RGB color order, neopixel.BGR which is called in the ORDER variable.
# NeoPixel LED Setup
NUMPIXELS = 72
BRIGHTNESS = 1
PIN = board.EXTERNAL_NEOPIXELS
ORDER = neopixel.BGR
pixels = neopixel.NeoPixel(PIN, NUMPIXELS, brightness=BRIGHTNESS,
auto_write=False, pixel_order=ORDER)
MIDI Setup
Next is the initialization of the USB MIDI ports. This will listen on MIDI channels 1 and 2. NOTE_OFFSET is set to 24 which will map the first NeoPixel LED in the strand to MIDI note C1.
Chromatic note names are in the NOTE_NAMES array that is used for print statments.
# MIDI setup - listen on channels 1 and 2 (0-indexed: 0 and 1)
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
midi_in=usb_midi.ports[0], in_channel=(0, 1), midi_out=usb_midi.ports[1], out_channel=0
)
# MIDI note number that maps to Pixel 0 (top-left of matrix).
# 24 = C1, 36 = C2, 48 = C3
NOTE_OFFSET = 24
# Chromatic note names used for print statements
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
Base Color Palette
A total of 24 colors will span across two octaves of the chromatic scale. The first 12 colors cover one octave, the next 12 for the second with a shifted palette. Colors then tile across all 6 rows.
BASE_COLORS = [
# Octave A - Warm to Cool spectrum
(255, 0, 0), # C - Red
(255, 45, 0), # C# - Red-Orange
(255, 90, 0), # D - Orange
(255, 145, 0), # D# - Amber
(255, 200, 0), # E - Yellow-Orange
(255, 255, 0), # F - Yellow
(128, 255, 0), # F# - Yellow-Green
(0, 255, 0), # G - Green
(0, 255, 128), # G# - Spring Green
(0, 255, 255), # A - Cyan
(0, 128, 255), # A# - Sky Blue
(0, 0, 255), # B - Blue
# Octave B - Rich and saturated shifted palette
(64, 0, 255), # C - Indigo
(128, 0, 255), # C# - Violet
(200, 0, 255), # D - Purple
(255, 0, 200), # D# - Magenta
(255, 0, 128), # E - Hot Pink
(255, 0, 64), # F - Deep Rose
(255, 64, 64), # F# - Salmon
(255, 128, 128), # G - Light Coral
(255, 200, 128), # G# - Peach
(255, 255, 128), # A - Pale Yellow
(128, 255, 128), # A# - Mint
(128, 255, 255), # B - Ice Blue
]
Mapping MIDI Notes to Pixels and Colors
A few functions are created to map the MIDI notes to positions in a matrix, 6 rows and 12 columns. Each row is one octave which always starts with a C note. The matrix is in a zigzag arrangement, where the function matrix_to_pixel handles the pixel indexing.
def note_to_matrix(note):
"""
Map a MIDI note to a (row, col) position in the matrix.
Each row is one octave (12 notes). C always starts at column 0.
NOTE_OFFSET determines which note maps to (row=0, col=0).
"""
offset_note = note - NOTE_OFFSET
r = (offset_note // MATRIX_COLS) % MATRIX_ROWS
c = offset_note % MATRIX_COLS
return r, c
def matrix_to_pixel(r, c):
"""
Convert a (row, col) matrix position to a physical pixel index,
accounting for zigzag wiring.
Even rows (0, 2, 4) run left to right.
Odd rows (1, 3, 5) run right to left.
"""
if r % 2 == 0:
return r * MATRIX_COLS + c # Left to right
else:
return r * MATRIX_COLS + (MATRIX_COLS - 1 - c) # Right to left
def note_to_pixel(note):
"""Map a MIDI note directly to a physical pixel index via the matrix."""
r, c = note_to_matrix(note)
return matrix_to_pixel(r, c)
def note_to_name(note):
"""Return a human-readable note name and octave, e.g. 'C2' for note 36."""
name = NOTE_NAMES[note % 12]
octave = (note // 12) - 1
return f"{name}{octave}"
def color_for_note(note):
"""
Look up the color for a note based on its position across two octaves.
The 24-color palette tiles every two octaves across the matrix rows.
"""
offset_note = note - NOTE_OFFSET
return BASE_COLORS[offset_note % len(BASE_COLORS)]
Boot Animation Sequence
Two functions are created to make an LED animation run as soon as the board is connected. This is used to check if all of the NeoPixel LEDs in the strand are working. The color_wipe function will wipe a color across the strand, one LED at a time. The boot_sequence function defines three colors and then black to turn them off. The speed of the animation can be adjusted by changing the delay value.
def color_wipe(c, delay=0.01):
"""Wipe a color across the strip one LED at a time."""
for i in range(NUMPIXELS):
pixels[i] = c
pixels.show()
time.sleep(delay)
def boot_sequence():
"""Animated boot sequence using color wipes."""
color_wipe((255, 0, 0), 0.01) # Red wipe
color_wipe((0, 255, 0), 0.01) # Green wipe
color_wipe((0, 0, 255), 0.01) # Blue wipe
color_wipe((0, 0, 0), 0.01) # Wipe off
MIDI Notes Fade Off
Whenever a MIDI Note Off message is received, the NeoPixel color will produce a fade off animation. update_fades() loops through every pixel currently fading, and on each call checks if enough time has elapsed since the last step — if so, it dims the pixel by multiplying its original RGB values by a decreasing factor and advances the step counter. Once the step counter reaches FADE_STEPS, the pixel is set to black and removed from the fading_pixels dictionary.
def update_fades():
"""Call this every loop iteration to advance any active fades."""
now = time.monotonic()
completed = []
for pix_index, state in fading_pixels.items():
step_delay = FADE_DURATION / FADE_STEPS
if now - state["last_time"] >= step_delay:
state["step"] += 1
state["last_time"] = now
if state["step"] >= FADE_STEPS:
pixels[pix_index] = (0, 0, 0)
pixels.show()
completed.append(pix_index)
else:
r, g, b = state["color"]
factor = (FADE_STEPS - state["step"]) / FADE_STEPS
pixels[pix_index] = (int(r * factor), int(g * factor), int(b * factor))
pixels.show()
for pix_index in completed:
del fading_pixels[pix_index]
The Loop
The while True loop is an endless cycle that does three things in rapid succession: it checks if any new MIDI messages have arrived from the Feather, reacts to them by either instantly lighting up the appropriate LED or registering it to start fading, and then nudges any currently-fading LEDs one tiny step closer to off. This cycle repeats so fast that the fading appears smooth and MIDI input feels instantaneous, even though the program is only ever doing one thing at a time.
while True:
msg = midi.receive()
if isinstance(msg, NoteOn) and msg.velocity > 0:
pixel_index = note_to_pixel(msg.note)
color = color_for_note(msg.note)
note_name = note_to_name(msg.note)
row, col = note_to_matrix(msg.note)
# Immediately cancel any active fade on this pixel
if pixel_index in fading_pixels:
del fading_pixels[pixel_index]
pixels[pixel_index] = color
pixels.show()
print(f"Note ON: {note_name} ({msg.note}) Pixel {pixel_index}, Color {color}")
elif isinstance(msg, NoteOff) or (isinstance(msg, NoteOn) and msg.velocity == 0):
pixel_index = note_to_pixel(msg.note)
color = color_for_note(msg.note)
note_name = note_to_name(msg.note)
row, col = note_to_matrix(msg.note)
fading_pixels[pixel_index] = {
"color": color,
"step": 0,
"last_time": time.monotonic()
}
print(f"Note OFF: {note_name} ({msg.note}) Pixel {pixel_index} fading")
# Advance all active fades each loop iteration
update_fades()
Page last edited April 01, 2026
Text editor powered by tinymce.
CAD Files
Matrix CAD Assembly
The matrix frame is two 4x6 grids that are joined together with connectors. The grids are fitted into the two frames with a diffuser panel fitted in between the two parts. Leg stands are fitted on both sides of the frame with a slight angle.
CAD Parts
Individual 3MF files for 3D printing are oriented and ready to print on FDM machines using PLA/PETG filament. Original design source files may be downloaded using the links below. Make copies of the following:
- 2x Frame
- 2x Grid
- 2x Connector
- 2x Diffuser
Feather Case CAD
The Feather PropMaker RP2040 is secured to a three-piece snap fit enclosure. The PCB is secured under corner clips, with no hardware fasteners on the bottom cover. A frame is fitted over the bottom cover with cutouts for the USB-C port and the screw block terminals. The top cover snaps over the frame with access to the STEMMA QT port and the two on-board buttons, boot and reset.
Diffuser Options
You have the option to choose the material for the diffuser panel. If you want to 3D print the panel, a white translucent filament works well.
If you want a more saturated, matte black finish, choose the Black LED acrylic material. You will have to cut the acrylic down to size using the included template.
Page last edited April 01, 2026
Text editor powered by tinymce.
Wiring Assembly
First NeoPixel
Locate the first NeoPixel in the strand. The connector should match the one in the photo. You'll need to replace it with the silicon ribbon cable so it can be connected to the screw-block terminal on the PropMaker Feather.
Create the Cable
Using the 26AWG silicone ribbon cable, create a 3-wire cable and cut it to a desired length. In this project, 12-inches (30cm) was suffice.
Using wire strippers, remove a bit of insulation from each wire on both ends. Then, twist and tin the exposed wire using a bit of solder - This helps to prevent the wires from fraying when soldering to the NeoPixel strand.
Solder Wires
Cut the cable from the first NeoPixel in the strand and strip the three wires for power, data in, and ground.
The power wire is denoted with small white dot markings, followed by the data wire, and then the ground wire.
Cut three short pieces of heat shrink tubbing and slip them over the silicone ribbon wires.
Solder the three wires from the strand to the silicone ribbon wires.
Wired NeoPixel Strip
Double check that the solder joints are solid. Slip heat shrink over them and applying heat to the tubing to set them in place.
LED Count
This project only needs 72 NeoPixel LEDs. You can either manually count them out, one by one, or light them up! The code features a boot sequence that lights up each NeoPixel LED - useful for testing out the strand before assembling the rest of the project.
If you choose to test them first, you can connect the wires from the strand to the screw-block terminal on the PropMaker Feather.
Cut Strip
Locate the 72nd NeoPixel in the strand and cut the cable in between the 72nd and 73rd NeoPixel using wire cutters.
Save the remaining strand (that's 128 NeoPixels!) for another project.
Page last edited April 01, 2026
Text editor powered by tinymce.
Matrix Assembly
Install Grid to Frames
Get the frame and grid parts ready.
Orient the frame so the printed bottom surface is laid face down. Orient the grid so the printed bottom surface is facing up.
Insert the grid into the frame so the matching groove and dovetails are matting. Push the grid until it sits flush with the frame.
Repeat the installation for the second frame and grid parts.
Diffuser Panels
Get the diffuser panels ready to install into the assembled grids.
This tutorial will opt for the black LED acrylic.
Install Diffuser Panel
Insert the diffuser panel through the opening on the side of the frame grid assembly.
If using a 3D printed panel, note the orientation. The panel has a thin base layer and a thick lip - This accommodates for stand 1/8in thick acrylic material.
Install Connectors
Bring the two frame grid assemblies together so the slotted ends are facing each other.
Get the two connectors ready to install onto the frame grids.
While holding the two together, slide the connector over the dovetails on the two frames. Push the connector until it sits flush with the frames.
Repeat the installation with the second connector.
Assembled Frames
Take a moment to inspect the assembled frames, making sure the side connectors are flush with the frame and the diffuser panels are properly installed.
Install LEDs
Get the NeoPixel Pebble/Seed strand ready to install into the grid.
Orient the frame grid assembly so the bottom surface of the grids are facing up.
Starting with the first NeoPixel LED, firmly press it into pill shaped cutout in the lower right side of the grid.
The NeoPixel LED should sit just below the thickness of the grids surface. If it's pushed in too far, the light will create a sharp spotlight, whereas the ideal effect is for the light to evenly fill the full rectangular cell in the grid.
Install 1st Row
Continuing with the installation, going from right to left, press fit more NeoPixel LEDs across the first row of the grids.
Take your time, making sure each LED is fitted relatively the same depth as the first.
If the LED doesn't quite fit, you can pinch and flex the wires to get the LED to insert, then pull the wire to seat the LED at the desired depth.
Data Flow
The NeoPixel LEDs are arranged in a zigzag pattern. On the 12th LED, go up one row, and then continue with the installation going from left to right.
At the end of the row, at the 24th LED, go up another row, then continue going from right to left.
Repeat the installation process for all 72 LEDs.
Installed NeoPixels
With all 72 NeoPixels installed, take a moment to inspect each LED, making sure they're all inserted with relatively the same depth.
Install Leg Stands
Get the two leg stands ready to install onto the frame.
Starting with the left leg, line it up with the dovetail on the left side of the frame.
Insert and slide the left leg so it fully seats with the frame.
Repeat the installation process for the right leg stand.
Frame Standing
The left leg features a dedicated wire channel for fitting the silicon ribbon cable from the NeoPixel strand.
Press the 3-wire silicone ribbon cable into the routed channel following the leg stand.
Install Feather to Case
Orient the Feather with the bottom of the case. The end with the rectangular cutout should line up with the screw-block terminals on the Feather.
Insert the Feather into the bottom cover so the edge of the PCB is fitted underneath the two corner clips.
Slightly flex the bottom cover so the other side of the Feather can be fitted under the second set of corner clips.
Assemble Case
Orient the case frame with the bottom cover and snap fit it over. The shorter cutout should be lined up with the Feather USB-C port.
Orient the top cover to match the Feather; two buttons, screw-blocks and the STEMMA QT port. Then snap fit it over the frame.
Connect NeoPixels to Feather
Use a small slotted screwdriver to unscrew the three inserts on the screw block (they're labeled 5V, Neo and G on the bottom of the Feather PCB).
Insert the three wires from the NeoPixel strand to their corresponding pins on the Feather screw block.
While holding the wires in place, use the screwdriver to secure the wires to the terminals.
Page last edited April 01, 2026
Text editor powered by tinymce.
Usage
Connect the PropMaker Feather RP2040 to your computer with a USB C cable. Then, open up your DAW or other software that supports MIDI out.
USB MIDI Device
The Feather RP2040 Prop-Maker will show up as a USB MIDI device on your computer. It features a USB MIDI port appropriately named CircuitPython usb_midi.
External MIDI Device
In your DAW software, an external MIDI instrument will allow you to choose the Feather RP2040 Prop-Maker as the destination on all MIDI channels. You'll use this track to output the MIDI notes from a MIDI controller or piano roll from your DAW.
Playing with the MIDI NeoPixel Visualizer
With the Feather RP2040 PropMaker setup as an external MIDI device in your DAW, you can use a MIDI controller such as a keyboard piano or control pads to light up the LEDs.
When a single MIDI note is played, a corresponding NeoPixel LED will light up. When multiple MIDI notes are played, as a chord, multiple LEDs will light up.
Page last edited April 01, 2026
Text editor powered by tinymce.