Overview
A receiver is the brains inside each prop. You give it a unique ID, load a show file, and from then on it knows exactly what to do at every moment in the performance. It's not waiting for instructions from the remote — it already has the whole choreography memorized. The remote just tells it what time it is.
- 224 unique prop IDs
- Per-prop hardware config (V3)
- USB mass storage upload
- 17 built-in effects
- OLED status display
- Auto-timeout (30 min)
Role
Here's the mindset that makes the firmware click: the receiver is not "remote-controlled LEDs." It's a tiny computer that already has the whole plan, and the remote just tells it what time it is.
This is a subtle but powerful distinction. Because each prop carries its own show data, the radio link only needs to carry timing info — a few bytes every 100ms. No video stream, no effect parameters, no "do this now" commands. Just "the show clock says 12,345 milliseconds."
The practical benefit? Dozens or even hundreds of props can sync to one remote without drowning in radio traffic. And if a prop misses a packet or two, it can coast on its local clock until the next update arrives.
Boot Sequence
Boot is basically a checklist that tries to get the device into a safe, visible state quickly. OLED comes up early so you can see what’s happening even if file loading or RF setup fails.
setup():
(1) Detect USB-mode button hold early
(2) Init EEPROM (prop ID)
(3) Init OLED early so failures are visible
(4) Init NeoPixel strip with safe defaults (temporary)
(5) If USB mode requested: runUSBMode()
(6) Mount FatFS, load show.bin (with retry), unmount
(7) Configure strip from PropConfig (type/order/count/brightness)
(8) Init RF69 radio (freq/bitrate/encryption)
(9) Show status on OLED enterUSBMode, loadShowFromFlashWithRetry, strip.updateType.
Setup
In real life, receiver setup usually boils down to two things: give it a unique prop ID, and upload a new show.bin.
The firmware supports both without needing a laptop.
Setting Prop ID
- 1 Power on the device
- 2 Hold config button 3+ seconds for Setup Mode
- 3 Short press to increment ID (1-224)
- 4 Long press (3 sec) to save and reboot
USB Mass Storage Mode
- 1 Hold config button while powering on
- 2 Device appears as USB drive
- 3 Copy
show.binto the drive - 4 Eject drive (auto-reboots)
Test Mode
Here's a handy trick for checking your LED strips: a short press of the CONFIG button toggles test mode. The prop runs a rainbow chase animation so you can verify all your LEDs are wired up and responding — no remote or show file needed.
This is particularly useful when you're first building props or troubleshooting a strip that seems dead. If the test animation runs smoothly, you know the hardware is good and you can focus on software or radio issues.
LED Type Support
Not all LED strips speak the same language. The receiver supports six common LED chipsets, each with their own timing quirks, plus all six RGB color orderings.
This flexibility is configured per-prop in the show.bin file, so you can mix different LED types across props in the same show.
Different manufacturers wire RGB in different orders. If your colors look wrong (red showing as green, etc.), you probably just need to change the color order setting.
show.bin Loading
The receiver is intentionally strict when it reads show.bin.
If the magic or version is wrong, it bails instead of trying to “make sense” of random bytes.
loadShowFromFlash():
read header, validate magic "PICO"
validate version == 3
read PropConfig for THIS prop ID:
offset = sizeof(header) + (propID - 1) * sizeof(PropConfig)
myConfig = bytes[8]
seek past full LUT to events:
sizeof(header) + 224 * sizeof(PropConfig)
read up to eventCount events into showSchedule[] The fun part is the LUT: each receiver seeks directly to “its” 8-byte PropConfig entry based on prop ID, so one show file can support mixed LED counts/types/orders across props. Full layout details live on the show.bin page.
Timebase + Sync
Timecode packets show up about every 100ms. When one arrives, the receiver treats masterTime as truth and sets
currentShowTime to match. Between packets it “coasts” using local delta
so animations stay smooth during brief RF hiccups.
Scheduler
The scheduler is where the receiver turns “time” into “behavior”. It asks: what event is active right now, and does that event actually target this prop?
bucketIndex = (propID - 1) / 32
bitMask = 1 << ((propID - 1) % 32)
for event in showSchedule:
if t in [start, start+duration):
if event.targetMask[bucketIndex] & bitMask:
choose event
break
else:
OFF Sometimes the show literally schedules an OFF event. Other times OFF is just “nothing matches right now.” In both cases, the firmware has to reliably clear the LEDs — no mystery glow between cues.
Effects Engine
At this point it’s basically “paint pixels.” Each effect is a function that fills a NeoPixel buffer based on a local effect time:
localTime = currentShowTime - currentEffectStart.
localTime = currentShowTime - effectStart
switch (effectType):
SOLID => fill(color)
STROBE => toggle on/off by localTime
RAINBOW => per-pixel hue by localTime
... A nice touch: most renderers use strip.numPixels(), so the same effect scales across different LED counts without special cases.
OLED Status UI
The OLED is the receiver’s “talking to you” channel. It’s a small dashboard that answers the questions you ask in the field: who am I (prop ID), did I load a show (event count), am I hearing the remote (RSSI), what mode am I in, and what time is it.
When you’re troubleshooting on a football field or backstage, this is the difference between guessing and knowing.
Safety + Timeouts
The receiver is built to prefer “off and predictable” over “on and confusing.” A few guardrails make it behave nicely as real hardware.
- Packet timeout: if packets stop arriving, UI reports NO SIGNAL.
- Standby: when not playing, LEDs are kept OFF for reliability.
- Show end: after the last targeted event, LEDs are forced OFF.
- Strip timeout: auto-timeout exists to prevent long-running battery drain/overheat scenarios.
Built-in Effects
17 effects available out of the box. Each effect can be customized with color, speed, and duration in your show file.
All LEDs off
Static color fill
Brief white flash every 500ms
Fast on/off at ~30Hz
Moving rainbow pattern
Static rainbow gradient
Moving color band
Progressive fill
Larson scanner (Knight Rider)
Falling streak with tail
Flickering fire simulation
Pulsing heartbeat pattern
Random flicker/dropout
Dual sine wave pattern
Random white sparkles on color
Smooth fade in/out
Even/odd pixel pattern
Dependencies
PicoLume firmware builds on a small set of well-established Arduino libraries for RF, displays, and LEDs. These dependencies are pulled in by your Arduino toolchain when building from source.
Firmware is licensed under GNU GPL v3.0. Third-party libraries retain their own licenses.
When redistributing binaries, review each dependency's license terms.
Arduino-Pico
Remote + ReceiverRP2040 Arduino core (USB MSC, FatFS, EEPROM, timing).
Source + licenseFlashing Firmware
For now, the recommended way to update PicoLume firmware is to build and flash it with the Arduino IDE.
Pre-built .uf2 binaries are in progress and will be linked here once they're available.
Arduino IDE (Recommended)
- 1 Install Arduino IDE and add the Arduino-Pico (RP2040) board package.
- 2 Open the sketch you want to flash: picolume_receiver/picolume_receiver.ino or picolume_remote/picolume_remote.ino.
- 3 Update your hardware pin settings near the top of the sketch (buttons + LED data pin) if needed.
- 4 Put the RP2040 into bootloader mode: hold BOOTSEL while plugging in USB (or while tapping reset), then click Upload.
Default Pin Map
- CONFIG button: GPIO3 (CONFIG_BUTTON_PIN)
- ENTER button: GPIO15 (ENTER_BUTTON_PIN)
- LED data out: GPIO14 (PIN_LED_DATA)
- Cycle (Play/Pause): GPIO6 (CYCLE_BUTTON_PIN)
- Off (Stop/Reset): GPIO7 (OFF_BUTTON_PIN)
Pre-built binaries: in progress. A Releases link will be added here when ready.