-
Notifications
You must be signed in to change notification settings - Fork 3
Flashing a Receiver
This is the manual (USB-cable) flashing procedure. If your receiver is already powered on and online in the UI, you can update it without touching it — see OTA flashing instead.
Each receiver needs to know its NODE_ID (1–254, e.g. 146) and its RECEIVER_IDENT (e.g. RX146). These two values get combined into the receiver's nRF24 listening address — that's how the dongle finds it without a pairing handshake.
In firmware versions before v14, every receiver needed its own custom build with its NODE_ID baked in as a #define. From v14 forward, identity lives in the chip's NVS (non-volatile storage) instead. One firmware binary serves the entire fleet, and each receiver gets provisioned (assigned its NODE_ID / ident) over USB serial after flashing. Routine firmware updates preserve NVS, so a receiver keeps its identity through every future update.
- A working USB-C cable (data, not charging-only).
- The receiver, with at least one cue module attached so you can see boot indication.
- A Mac, Linux box, or Windows machine with
arduino-cliinstalled (for building) and Python 3 (for flashing). - Once: install
arduino-cliand the ESP32 core. See Getting Started for your OS.
# Build (one-time, or whenever you bump firmware):
devices/utils/build_receiver.sh
# Flash a brand-new chip and provision it as RX146:
devices/utils/flash_receiver.py --full --node 146
# Routinely update an already-provisioned receiver (preserves its identity):
devices/utils/flash_receiver.py
# Re-number a receiver in the field (no flash, just update its identity):
devices/utils/set_node_id.py --node 200 --ident RX200That's the whole interface. The rest of this page explains what's actually happening.
devices/utils/build_receiver.shThis script:
- Reads the current
FW_VERSIONfromdevices/os4_receiver/os4_receiver.ino(currently23at time of writing). - Runs
arduino-cli compile --fqbn esp32:esp32:lolin_s2_mini. - Copies four
.binfiles intodevices/os4_receiver/bin/:-
os4_receiver_v<N>.bin— the app partition (~340 KB), flashed at0x10000. -
os4_receiver_v<N>.bootloader.bin— chip bootloader, flashed at0x1000during--full. -
os4_receiver_v<N>.partitions.bin— partition table, flashed at0x8000during--full. -
os4_receiver_v<N>.boot_app0.bin— OTA "next-app" pointer, flashed at0xe000on every flash.
-
- Updates
bin/latest.*symlinks pointing at the newest version.
If arduino-cli complains about the ESP32 core, follow the bootstrap commands the script prints (one-time setup):
arduino-cli config init --overwrite
arduino-cli config set directories.data ~/Library/Arduino15
arduino-cli config set board_manager.additional_urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core update-index
arduino-cli core install esp32:esp32Important: the receiver firmware uses additional Arduino libraries (
RF24,Adafruit_NeoPixel). If you've ever built the sketch in the Arduino IDE on this machine they're already installed. Otherwise install them via the Library Manager in the Arduino IDE once.
flash_receiver.py is a Python wrapper around esptool that picks the right binaries, sends the right commands, and provisions the receiver afterwards. It self-bootstraps a Python venv at devices/utils/.venv on first run — no manual pip install needed.
It has three modes:
App-only flash to a chip that's already been flashed before. Writes the app partition at 0x10000 and re-stamps boot_app0.bin at 0xe000. Preserves NVS — the receiver's NODE_ID and ident come back unchanged.
Use this for routine firmware bumps.
Full flash: bootloader + partition table + boot_app0 + app, in one esptool invocation. Use this:
- For a brand-new chip that has never been flashed.
- For recovery after a corrupt partition table.
- Whenever the partition layout itself changes.
After the flash completes, the script prompts for a NODE_ID (1–254) and sends SETID <node_id> RX<node_id> over serial. The default ident is RX<node_id>; pass --ident MYNAME to override.
App-only flash, then interactive SETID. Use this to renumber a receiver without wiping anything else in NVS. Equivalent to flash_receiver.py followed by set_node_id.py --node N --ident IDENT.
devices/utils/flash_receiver.py --full --port /dev/tty.usbmodem01 --node 146
# Use the default ident "RX146", or pass --ident MYNAME to override.The lolin_s2_mini's ESP32-S2 runs the user firmware over USB-CDC, so its ability to respond to esptool's auto-reset signals depends on what the user app is doing the moment esptool tries to talk. flash_receiver.py handles this transparently with a three-attempt cascade:
-
--before default_reset— the normal auto-reset path. -
--before no_reset(silent retry) — automatic, no operator interaction. On the lolin_s2_mini the chip is often already in a download-ready state after a "failed"default_reset, so this catches the common case without a prompt. -
--before no_resetafter manual BOOT+RESET — only reached if steps 1 and 2 both fail. The script prompts you to:- Press and HOLD the
BOOTbutton on the receiver. - While still holding
BOOT, press and releaseRESET. - Release
BOOT. - Press Enter at the prompt.
- Press and HOLD the
You'll usually only see step 1 succeed, or step 2 succeed silently. The manual prompt is uncommon in practice — on a healthy receiver, every flash should complete with no operator input.
If the manual retry also fails:
- Unplug and replug the receiver to recycle the USB endpoint.
- Make sure nothing else is holding the serial port — Arduino IDE Serial Monitor,
screen, another terminal session. - Try a different USB-C cable (charging-only cables look identical and break flashing).
The default ESP32-S2 partition table puts NVS at 0x9000–0xdfff, in the gap between the partition table (0x8000–0x8fff) and boot_app0 (0xe000–0xffff). The flasher never writes to that gap, so NVS is left intact even on --full.
This is why a receiver flashed with flash_receiver.py --full --node 146 keeps NODE_ID = 146 through every subsequent flash_receiver.py (no flag) and through every OTA flash.
You don't need to reflash to change a receiver's NODE_ID. The dedicated helper:
devices/utils/set_node_id.py # prompts for everything
devices/utils/set_node_id.py --node 146 # ident defaults to RX146
devices/utils/set_node_id.py --get # just read the current ID
devices/utils/set_node_id.py --wipe # back to UNPROVISIONEDUnder the hood this opens the receiver's USB serial port at 115200 8N1 and sends one of these commands (which the firmware accepts at any time):
GETID
SETID <node_id> <ident>
WIPEID
SETID and WIPEID reboot the chip after writing.
A brand-new ESP32-S2 with zero NVS boots into UNPROVISIONED mode — radio disabled, slow magenta breathe on the three status LEDs. SETID fixes it. WIPEID returns it to that state.
You'll see this:
- After a full flash on a chip that's never been provisioned.
- After deliberately wiping with
WIPEID. - If NVS is somehow corrupted (rare).
USB-CDC quirks. See Step 3 above.
flash_receiver.py self-bootstraps a venv at devices/utils/.venv (POSIX bin/, Windows Scripts\). If something's wrong with it, delete it and re-run:
# macOS / Linux:
rm -rf devices/utils/.venv
devices/utils/flash_receiver.py # re-run; will rebuild the venv# Windows (PowerShell):
Remove-Item -Recurse -Force devices\utils\.venv
python devices\utils\flash_receiver.pyIf the venv still won't build (locked-down Python), install the deps onto your PATH instead — the script will detect an importable pyserial and skip the venv: python -m pip install "pyserial>=3.5" "esptool>=4.9".
It's UNPROVISIONED. Run:
devices/utils/set_node_id.py --node <pick a number 1-254>Either:
- The receiver's
NODE_IDdoesn't match what you typed in the Receivers page in the UI. They have to match. - The dongle and receiver are on different RF channels. The dongle's current channel is shown in the Settings → Debug → RF Scan panel; the receiver firmware ships with channel 85 hard-coded but can be reflashed if you need to change it.
- Out of RF range (very unlikely with these radios — 1000+ yard line-of-sight is normal).
After a successful build:
devices/os4_receiver/bin/
├── os4_receiver_v23.bin
├── os4_receiver_v23.bootloader.bin
├── os4_receiver_v23.partitions.bin
├── os4_receiver_v23.boot_app0.bin
├── latest.bin -> os4_receiver_v23.bin
├── latest.bootloader.bin -> os4_receiver_v23.bootloader.bin
├── latest.partitions.bin -> os4_receiver_v23.partitions.bin
└── latest.boot_app0.bin -> os4_receiver_v23.boot_app0.bin
flash_receiver.py always picks the highest-versioned matching bin. To force a specific binary, use --bin. Older versions are kept on disk so you can roll back.
-
OTA flashing — same
os4_receiver_v<N>.binover the air, no USB cable required. - Receiver firmware — what the firmware actually does.
- Flashing a Dongle — same idea for the dongle.
Getting started
- Overview
- Desktop installers (macOS / Windows)
- macOS
- Linux
- Windows
- Production vs Development
- Connecting the dongle
- Flash a receiver
- Flash a dongle
- OTA flashing
Raspberry Pi
System overview
Subsystems
Hardware
- Receiver firmware
- Dongle firmware
- RF protocol
- Contributor Portal — BOMs, schematics, and board resources
UI walkthrough
Reference
Downloads
- Firmware
- Installers
Module Build & User Guides
- Cue
- Receiver
- Dongle