Prop Controller

Receiver Firmware

Receivers load show.bin via USB mass storage and play back effects synchronized to the remote’s timecode.

Overview

224
Prop IDs
17
Effects
1000
Max LEDs
USB
Show Upload

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
Search terms: 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. 1 Power on the device
  2. 2 Hold config button 3+ seconds for Setup Mode
  3. 3 Short press to increment ID (1-224)
  4. 4 Long press (3 sec) to save and reboot

USB Mass Storage Mode

  1. 1 Hold config button while powering on
  2. 2 Device appears as USB drive
  3. 3 Copy show.bin to the drive
  4. 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.

Supported Chipsets
WS2812B800 KHz, most common
SK6812800 KHz, WS2812B compatible
SK6812 RGBW4-channel with white
WS2811400 KHz, 12V strips
WS2813800 KHz, backup data line
WS281512V version of WS2813
Color Orders

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.

GRB RGB BRG RBG GBR BGR

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.

One subtle guardrail: if a packet arrived this frame, the code skips adding delta. Otherwise you’d “double advance” time and slowly drift ahead.

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?

Selection Logic (Concept)
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
Two kinds of 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.

Rainbow Flash/Strobe Chase/Motion Fire/Meteor Fade/Breathe Scanner/Pulse
#0 Off

All LEDs off

#1 Solid Color

Static color fill

#2 Camera Flash

Brief white flash every 500ms

#3 Strobe

Fast on/off at ~30Hz

#4 Rainbow Chase

Moving rainbow pattern

#5 Rainbow Hold

Static rainbow gradient

#6 Chase

Moving color band

#9 Wipe

Progressive fill

#10 Scanner

Larson scanner (Knight Rider)

#11 Meteor

Falling streak with tail

#12 Fire

Flickering fire simulation

#13 Heartbeat

Pulsing heartbeat pattern

#14 Glitch

Random flicker/dropout

#15 Energy

Dual sine wave pattern

#16 Sparkle

Random white sparkles on color

#17 Breathe

Smooth fade in/out

#18 Alternate

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 + Receiver

RP2040 Arduino core (USB MSC, FatFS, EEPROM, timing).

Source + license

RadioHead

Remote + Receiver

Packet radio drivers used for RH_RF69 / RFM69 links.

Source + license

Adafruit NeoPixel

Receiver

LED strip control for WS2812/SK6812-style pixels.

Source + license

Adafruit SSD1306

Receiver

OLED display driver used by receiver status screens.

Source + license

Adafruit GFX

Receiver

Graphics primitives used by SSD1306 rendering.

Source + license

LiquidCrystal_I2C

Remote

I2C character LCD support for the remote UI.

Source + license

Flashing 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. 1 Install Arduino IDE and add the Arduino-Pico (RP2040) board package.
  2. 2 Open the sketch you want to flash: picolume_receiver/picolume_receiver.ino or picolume_remote/picolume_remote.ino.
  3. 3 Update your hardware pin settings near the top of the sketch (buttons + LED data pin) if needed.
  4. 4 Put the RP2040 into bootloader mode: hold BOOTSEL while plugging in USB (or while tapping reset), then click Upload.

Default Pin Map

Receiver
  • CONFIG button: GPIO3 (CONFIG_BUTTON_PIN)
  • ENTER button: GPIO15 (ENTER_BUTTON_PIN)
  • LED data out: GPIO14 (PIN_LED_DATA)
Remote
  • 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.