Transmitter / Timecode

Remote Firmware

The master clock that broadcasts encrypted timecode at 10Hz, keeping all receivers synchronized.

Overview

10 Hz
Broadcast Rate
915 MHz
ISM Band
AES-128
Encryption
11 B
Packet Size

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.

Config/Stop (GPIO 3) Hold on boot → USB mode; Press → Stop & reset
Play/Pause (GPIO 15) Toggle between playing and paused
Cue A-D (GPIO 6-9) Jump to cue time and start playing

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. 1 In PicoLume Studio, position the playhead and press Shift+1/2/3/4 to set Cue A/B/C/D
  2. 2 Export show.bin — it will include the CUE1 block
  3. 3 Upload to remote via USB mode (hold Config/Stop on boot)
  4. 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.bin CUE1 block (via FatFS).
  • Initialize RF69 and apply RF_BITRATE, RF69_FREQ, and ENCRYPT_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.

Setting Must Match?
Frequency Yes (RF69_FREQ)
Bitrate / modem config Yes (RF_BITRATE)
Encryption key Yes (ENCRYPT_KEY, 16 chars)
RF Bitrate Options
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)

4B packetCounter
4B masterTime (ms)
1B state (0=STOP, 1=PLAY)
1B hopCount
1B sourceID
Why This Matters

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.

Quick Checklist
  • LCD shows time increasing when playing.
  • RF69_FREQ matches receivers.
  • RF_BITRATE matches receivers.
  • ENCRYPT_KEY is 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 + 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: GPIO28 (CONFIG_BUTTON_PIN)
  • LED data out: GPIO22 (LED_PIN)
Remote
  • 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.