Overview
Think of the remote as the "source of truth" for time. It sends out a tiny heartbeat packet about every 100ms that answers two questions: "what time is it in the show?" and "are we playing or stopped?"
That's really all it does, and that simplicity is intentional. By keeping the remote's job narrow, the whole system stays reliable. No complex show data to sync, no effect parameters to broadcast — just a steady pulse of "the clock says X."
Everything else — the effects, colors, and per-prop behavior — lives on the receivers in show.bin.
The remote's job is to keep everyone in sync, like a conductor keeping time for an orchestra.
Role
The clean mental model is: the remote is a metronome with buttons. It doesn’t try to “drive LEDs” directly. It just runs a clock and tells everyone what that clock says.
In code that clock is masterTime (milliseconds).
When you press play, it advances. When you stop, it freezes. When you reset, it goes back to 0.
- Time unit: milliseconds.
- Authority: receivers treat the remote's time as the show clock.
- Control plane: play/pause toggles time; reset stops and returns time to 0.
Controls
The remote has 6 buttons: one for config/stop, one for play/pause, and four dedicated cue buttons (A-D).
This allows quick jumps to preset cue points loaded from show.bin.
Hold the Config/Stop button on boot to enter USB mode for uploading new show files. During playback, it stops and resets to time 0.
The LCD shows the current state (CUE X PLAY, PLAYING, PAUSED, or STOPPED) and elapsed time. On boot, it displays how many cues are configured (e.g., "Cues: 2/4").
Cue System
Cue points let you jump to specific times in your show. They're configured in PicoLume Studio and stored in the show.bin file as a trailing CUE1 block.
Setting Up Cues
- 1 In PicoLume Studio, position the playhead and press Shift+1/2/3/4 to set Cue A/B/C/D
- 2 Export
show.bin— it will include the CUE1 block - 3 Upload to remote via USB mode (hold Config/Stop on boot)
- 4 Press cue buttons to jump to those times during playback
Undefined cues: If a cue button is pressed for a cue that hasn't been set, the button press is ignored.
How It Works
The firmware is small enough that you can hold the whole flow in your head: initialize hardware, then run an event loop that increments time and broadcasts packets. The interesting parts are mostly about making that loop stable and predictable.
Boot (setup)
- Bring up I2C + LCD (address
0x3F, 16x2). - Set up the 6 buttons (config/stop, play/pause, cue A-D).
- Check if Config/Stop is held — if so, enter USB mass storage mode.
- Load cue times from
show.binCUE1 block (via FatFS). - Initialize RF69 and apply
RF_BITRATE,RF69_FREQ, andENCRYPT_KEY.
Note: Some I2C LCDs use address 0x27. If the screen stays blank, try changing the address in the source.
Main Loop (loop)
now = millis()
delta = now - lastLoopTime
if playing:
masterTime += delta
handle buttons (debounced)
every ~100ms:
send RadioPacket(masterTime, state, counter)
update LCD periodically The subtle win is using delta. If the loop jitters a little (it will), your clock still advances correctly.
RF Config
This is the #1 "it's not working" culprit: RF settings must match between the remote and every receiver. If they don't match, receivers will act like the remote doesn't exist — you'll see "NO SIGNAL" on their OLEDs even if the radios are wired perfectly.
The good news: once you get it right, you typically set it and forget it. The default 19.2 kbps bitrate is a nice sweet spot that balances range and reliability. Only change it if you have a specific reason.
RF69_FREQ) RF_BITRATE) ENCRYPT_KEY, 16 chars) | RF_BITRATE | Mode | Use Case |
|---|---|---|
| 2 | FSK_Rb2Fd5 | Maximum range |
| 19 | GFSK_Rb19_2Fd38_4 | Default (recommended) |
| 57 | GFSK_Rb57_6Fd120 | Balanced |
| 125 | GFSK_Rb125Fd125 | Faster updates |
| 250 | GFSK_Rb250Fd250 | Minimum latency |
Radio Packet
The radio packet is intentionally tiny — just 11 bytes. It's not "instructions"; it's just a clock update plus play/stop state. The receiver uses this to keep its local show timeline aligned.
The hopCount and sourceID fields are there for future mesh networking —
receivers could eventually relay packets to extend range. For now they're just set to 0.
Packet (11 bytes)
This struct is a wire contract. If you change fields or sizes, you must update both the remote and receiver firmware together. If you ever plan to evolve it, treat it like an API: version it, and keep backwards compatibility in mind.
struct RadioPacket {
uint32_t packetCounter;
uint32_t masterTime;
uint8_t state; // 0=STOPPED, 1=PLAYING
uint8_t hopCount;
uint8_t sourceID; // 0=remote
} Debugging
When debugging, start with the "boring" checks. If the clock isn't being broadcast correctly, nothing downstream can look right.
- LCD shows time increasing when playing.
RF69_FREQmatches receivers.RF_BITRATEmatches receivers.ENCRYPT_KEYis exactly 16 chars and matches receivers.- On receivers, RSSI changes when the remote is nearby (sanity check for RF link).
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: GPIO28 (CONFIG_BUTTON_PIN)
- LED data out: GPIO22 (LED_PIN)
- Config/Stop: GPIO3 (CONFIG_STOP_PIN)
- Play/Pause: GPIO15 (PLAY_PAUSE_PIN)
- Cue A-D: GPIO6-9 (CUE_A/B/C/D_PIN)
Pre-built binaries: in progress. A Releases link will be added here when ready.