-
Notifications
You must be signed in to change notification settings - Fork 3
RF Protocol Details
The radio layer between the dongle and BYH receivers. This page collects the things that span both sides — addressing, channel mechanics, ACK payloads, scan algorithm — without re-explaining what's already covered in Receiver firmware and Dongle firmware.
| Property | Value |
|---|---|
| Chip | Nordic nRF24L01+ |
| Default data rate | 250 kbps |
| OTA data rates | 1 Mbps, 2 Mbps (host can pick at flash time) |
| Modulation | GFSK |
| Frequency band | 2.4 GHz ISM |
| Default RF channel | 85 (2485 MHz) |
| Channel range | 0–125 |
| TX power | RF24_PA_MAX |
| CRC | 16-bit |
| Auto-ACK | Enabled on pipe 0 |
| ACK payloads | Enabled |
| Dynamic payloads | Enabled |
| Max payload size | 32 bytes |
| Retries | 5 retries × 250 µs delay (setRetries(15, 5)) |
| Worst-case TX failure | ~22 ms |
| Best-case successful TX | ~3 ms (single try with ACK) |
Star — one master (dongle), N slaves (receivers).
There is no mesh networking. An earlier prototype used a mesh layer, but the LNA-equipped radios easily reach 1000+ yards line-of-sight, which is more than sufficient for any backyard or club site. Removing the mesh saved a lot of latency and complexity.
There is no broadcast. Every command is unicast to one receiver's address. Commands intended for "everyone" (showstart, play, stop, etc.) are issued by the daemon as N separate unicasts, and the dongle's poll scheduler interleaves them at its TDMA cadence.
Each receiver listens on a unique 64-bit address derived as:
receiver_address = ((rfSystemId << 32) | RECEIVER_BASE) + NODE_ID
With rfSystemId = 0 (default), RECEIVER_BASE = 1, and NODE_ID = 146 → address 0x000000093 (147 in low 32 bits).
The dongle has the inverse: receiverAddress(N) returns the same value. Because the address is deterministic from NODE_ID, the dongle doesn't need a pairing handshake — it just writes to the address corresponding to whichever receiver it wants to reach. The receiver finds the dongle's master address baked into its writing pipe (set up similarly).
rfSystemId is the only knob that distinguishes two independent firing systems on the same channel. Today it's always 0; future use cases (multiple shows in close geographic proximity) would assign different system IDs.
A normal nRF24 transaction is:
- Master TX command on receiver's address.
- Receiver auto-ACK with empty payload.
- Done.
With ACK payloads enabled:
- Master TX command.
- Receiver auto-ACK with up to 32 bytes of data piggybacked.
- Master reads ACK payload, gets receiver state for free.
This halves the round-trip count for "send command, get state" and matters enormously for telemetry latency and command queue throughput. The receiver's RECEIVER_STATUS (32 bytes — battery, show state, ident, continuity bits) lives entirely in the ACK payload of every command the dongle sends. So a periodic CLOCK_SYNC poll naturally also collects the receiver's full state.
The dongle exposes per-receiver state to the host two ways:
-
Aggregate
statusJSON — emitted ~1 Hz, contains every receiver's state in one frame. Used by the UI to render the receiver grid steady-state. -
Per-receiver
rxupdJSON — emitted immediately when one receiver's state changes. The UI uses this for low-latency updates (battery edges down by 1, continuity bit flips, etc.) without waiting for the next aggregate.
The host distinguishes a successful poll (latency sample x present in rxupd) from a failed poll (no x) and only updates the receiver's "last contact" timestamp on success.
Triggered from the UI (Settings → Debug → RF Scan) or directly via daemon command. The dongle:
- Stops the normal RF loop.
- For
passesrounds (default 10, max 50), iterates each channel in the requested range (default 0–125). - On each (pass, channel): switch to that channel, listen for ~180 µs, sample the RPD (Received Power Detector) bit. RPD = 1 if power above the nRF24's threshold during the listen window.
- Sums hits per channel.
- Restores RF config to whatever it was.
- Emits
scan_result.
The host computes a recommended channel as:
score(ch) = hits(ch) + 0.5 × sum_{ch' in [ch-2, ch+2], ch' != ch} hits(ch')
I.e. the channel itself plus half-weighted contributions from each ±2 neighbor. The lowest score wins, with ties broken by higher channel (favoring less-used upper channels).
A 10-pass scan over 0–125 takes about 200–500 ms.
The dongle's channel can be hot-swapped at runtime via {"rf_channel": N} JSON over serial (refused while a show is loaded or armed — mid-show channel changes would deafen all receivers).
Receiver firmware cannot hot-swap. To change the receiver fleet's channel, you'd reflash all receivers with a new rfChannel constant. So in practice: pick a quiet channel once based on a scan, set it on the dongle, leave it there. If you do need to change the fleet:
- Edit
rfChannel = <new value>inos4_receiver.ino. -
devices/utils/build_receiver.sh. - Reflash every receiver — over USB or, if they're still reachable on the old channel, OTA.
- Update the dongle to match:
{"rf_channel": <new value>}.
Channel 85 = 2485 MHz, which is above standard WiFi (2.412–2.484 GHz for 2.4 GHz channels 1–14). On most installs WiFi doesn't interfere meaningfully, even at high traffic. If you do see degradation, try channel 90+ (above all WiFi).
The RPD scan is a fine way to confirm: in a typical residential environment you'll see hits on channels 0–62 (WiFi) and very few hits on 70+.
During an OTA flash, the radio:
- Switches to the host-requested rate (1 or 2 Mbps).
- Disables routine polling — the dongle exclusively talks to the OTA target.
- Strict ACK-gated chunk pacing: chunk N+1 isn't TX'd until chunk N is ACK'd.
The receiver's main loop also pauses runPlayLoop while OTA is active. Both ends return to 250 kbps after flash_end, abort, or 30 s of inactivity (at which point OTA tears down).
The dongle's poll list maxes out at 32 receivers. The TDMA cadence is clockSyncIntervalMs / numReceivers, so with 32 receivers and the default 2 s interval, each receiver gets polled every ~62 ms — still plenty fast for the periodic clock-sync requirement.
If you have more than 32 receivers, the architecture would need a larger array on the dongle and (probably) a longer poll interval to keep latency bounded. Nobody's hit this limit in practice.
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