Skip to content

Wire Protocol Reference

OneSeventyFour edited this page May 14, 2026 · 2 revisions

Wire protocol reference

The serial protocol between the daemon (host) and the dongle. All lines are \n-terminated. Lines beginning with { are JSON; other lines are space-separated text commands. The OTA hot-path text commands (flash_data, flash_recover, flash_ping) are recognized before the general parser.

Host → dongle

Text commands

Command Form Meaning
msync msync 0 <epoch_ms> Set the dongle's wall clock for cue scheduling. Sent on init and on bounce().
sync sync <ident> 0 1 Register a receiver in the dongle's poll list (forces TDMA poll start).
forget forget <ident> Drop a receiver from the poll list. Scrubs queued commands too.
scan scan <passes> <ch_start> <ch_end> Run an RF spectrum scan (250 ms – 1 s).
433fire 433fire >>BITS:N<< x Bit-bang BITS on the 433 MHz line, N repetitions.
fire fire <ident> <position> Manual fire one cue on one BYH receiver. position is 0-based.
startload startload <ident> <count> <showId> Begin loading a show on one receiver.
showloadn showloadn <ident> <count> <t1_ms> <pos1> ... <repeat> Load up to 6 cues per line. Times in ms; positions 0-based.
showload showload <ident> <t1> <p1> <t2> <p2> 2 Legacy 2-cue load.
showstart showstart <ident> <epoch_ms> 0 <showId> <repeat> Tell receiver when to start firing.
play play 0 Resume / begin firing.
pause pause 0 Freeze receiver schedule.
stop stop 0 Stop firing immediately.
reset reset <ident> 0 Clear loaded show state on one receiver.
rxcfg rxcfg <ident> or rxcfg <ident> fd <50..5000> Request CONFIG_RESPONSE; optionally write fire duration first.

JSON config (parseLedJSON)

The daemon sends a single JSON object containing any subset of these fields:

{
  "led_brightness": 50,
  "daemon_act": 1,
  "web_act_state": 1,
  "tx_active": 2,
  "show_load_state": 1,
  "show_run_state": 5,
  "error_state": 0,
  "arm_state": 1,
  "receiver_timeout_ms": 30000,
  "response_timeout_ms": 100,
  "clock_sync_interval_ms": 2000,
  "debug_mode": 0,
  "rf_channel": 76,
  "rf_system_id": 0
}

Fields:

Key Type Range / values Effect
led_brightness int 1–100 NeoPixel master brightness.
daemon_act int 0/1 LED 0 state (daemon activity).
web_act_state int 0–7 LED 1 state (browser activity).
tx_active int 0–3 LED 2 state (TX activity).
show_load_state int 0–3 LED 3 state.
show_run_state int 0–8 LED 4 state — RUN_STATE enum.
error_state int 0–4 LED 5 state — ERR_STATE enum.
arm_state int 0/1 LED 6 state — pulses red when armed.
receiver_timeout_ms int 60000–21600000 "No contact = offline" threshold.
response_timeout_ms int 5–500 Per-command response timeout.
clock_sync_interval_ms int 50–30000 Polling cadence; clamped before use.
debug_mode int 0/1 Verbose logging on the dongle.
rf_channel int 0–125 Hot-swap nRF24 channel.
rf_system_id int 0–255 Address salt; reflashing receivers required.

OTA hot-path

Command Form Meaning
flash_begin flash_begin <ident> <total_size> <total_chunks> <crc32_hex> <rate> Allocate Update partition. rate 0/1/2 = 250k/1M/2M.
flash_data flash_data <idx> <hex> Send one 29-byte chunk (hex-encoded).
flash_recover flash_recover <idx> <level> Escalate recovery: 0 REPLAY, 1 SOFT, 2 FULL.
flash_ping flash_ping Liveness check; expects OP reply.
flash_end flash_end Finalize and reboot the receiver.
flash_abort flash_abort Abort the in-flight transfer.

Dongle → host

Aggregate status (every ~1 s)

{
  "timestamp": 1715680000123,
  "q": 12,
  "qmax": 128,
  "fw": 16,
  "ch": 76,
  "csim": 2000,
  "l": 4,
  "receivers": [
    {
      "i": "RX146", "n": 146, "b": 88, "s": 42,
      "l": 1, "r": 1, "t": 1715680000050,
      "x": 4, "sp": 99.5,
      "c": [18446744073709551615, 0],
      "fw": 23, "bv": 9, "nb": 1, "nbd": false, "ca": 8, "fd": 1000
    }
  ]
}

Top-level keys

Key Meaning
timestamp Dongle's millis() at frame build time.
q Command queue depth.
qmax Command queue capacity (128).
fw Dongle firmware version (currently 16).
ch Active RF channel.
csim Post-clamp clock-sync interval ms.
l Aggregate average latency (ms) over recent samples.
receivers[] Per-receiver telemetry, abbreviated keys (see below).

Per-receiver keys (also in rxupd)

Abbrev Field Meaning
i ident Receiver ident, e.g. RX146.
n node NODE_ID.
b battery Scaled 5..253.
s showId Currently-loaded show id (lower 14 bits).
l loadComplete bool.
r startReady bool.
t lmt Last contact time (dongle ms; host adjusts to wall ms).
x lat Latency sample (ms). Absent when contact failed.
sp successPercent Rolling success percentage.
c continuity [u64, u64] — continuity bitmask, cues 0..63 / 64..127.
fw fwVersion Receiver FW version (FW v22+).
bv boardVersion Receiver PCB version.
nb numBoards Detected cue board count.
nbd noBoardsDetected True if board ladder reads ~0.
ca cuesAvailable Physically-usable cues.
fd fireDurationMs Active fire duration.

Per-receiver fast-path push (rxupd)

Same shape as one element of receivers[], with "type": "rxupd". Emitted whenever the dongle gets new state from one receiver, without waiting for the next aggregate frame.

{ "type": "rxupd", "i": "RX146", "n": 146, "b": 88, ... }

Per-receiver config response (rxcfg)

After rxcfg <ident> or rxcfg <ident> fd <ms>:

{
  "type": "rxcfg",
  "i": "RX146", "n": 146,
  "fw": 23, "bv": 9, "nb": 1, "nbd": false,
  "ca": 8, "fd": 1000,
  "t": 1715680000050
}

Persisted to Receivers.fw_version, board_version, cues_available, config_data by the daemon's _persist_rxcfg_to_db.

Scan result (scan_result)

After scan <passes> <ch_start> <ch_end>:

{
  "type": "scan_result",
  "fw": 16,
  "passes": 10,
  "ch_start": 0,
  "ch_end": 125,
  "current_ch": 85,
  "started_ms": 1715680000123,
  "duration_ms": 487,
  "results": [
    { "ch": 0, "hits": 0 },
    { "ch": 1, "hits": 1 },
    ...
  ]
}

The host writes the full payload to /data/last_scan.json and a summary (recommended channel, peak, etc.) into /data/state for the UI.

GPIO state (gpio_status)

When any switch changes (or every 10 s as a heartbeat):

{
  "gpio": "gpio_status",
  "start_stop": 1,
  "armed": 0,
  "man_fire": 1
}

Values are raw digitalRead results: INPUT_PULLUP → HIGH (1) when open, LOW (0) when closed.

Bridge / config replies

When the daemon sends {"type":"config_serial",...} to the bridge (not the dongle), it gets back:

{ "tcpstatus": true, "serial_config": { "port": "/dev/tty.usbmodem01", "baud": 115200 } }

Or on failure:

{ "type": "config_response", "error": "Could not configure port: ..." }

These are bridge-level messages, not dongle messages, but they look the same on the wire because the bridge transparently relays everything.

OTA events

The dongle uses a mix of compact text and JSON for OTA progress. Compact lines minimize byte count for the high-frequency hot path:

Line Meaning
OA <idx> <state> <bytes_received> <attempts> Chunk ACK.
ON <idx> <rf_ok> <got_ack> <state> <err> <last> <bytes_received> <fatal> Chunk NACK.
OS ... OTA heartbeat / throughput / phase update.
OP Pong reply to flash_ping.

Lifecycle JSON via emitOtaEvent:

{ "type": "ota", "phase": "begin_ok" }
{ "type": "ota", "phase": "end_sent" }
{ "type": "ota", "phase": "done" }
{ "type": "ota", "phase": "timeout", "msg": "..." }
{ "type": "ota", "phase": "aborted" }

The host's OtaFlashDriver.feed_event handles both compact and JSON forms.

Validation / debug strings

The dongle emits human-readable strings like:

  • "M+ (RF24 Master Hub Online v4: ACK-payloads)" on boot.
  • "C+ msync 1715680000123 ok" on successful command.
  • "C+ forget RX146" on receiver removal.
  • "CV 433" on a malformed 433fire.
  • "C? rxcfg RX999 not in poll list".
  • "ERR: Command queue full.".
  • "WARN: ..." for soft errors.
  • "INFO: ..." for general logging.

Most of these are diagnostic — the daemon ignores them but they're useful when watching the serial line directly during development.

See also

Clone this wiki locally