-
Notifications
You must be signed in to change notification settings - Fork 3
Subsystem PC Daemon
host/pythings/pc_daemon/pc_daemon.py is the Python process that:
- Holds the only TCP connection to the serial bridge (and therefore to the dongle).
- Reads/writes show data and receiver definitions in SQLite.
- Runs the show schedule.
- Coordinates OTA flashes.
- Publishes a JSON state snapshot for the WS server / browser to consume.
It runs under supervisord inside the container as the firework-daemon program. There is exactly one instance per Backyard Hero install.
FireworkDaemon.run() starts five long-lived threads:
| Thread | What it does |
|---|---|
poll_command_dir |
Polls /tmp/d_cmd/ at 2 Hz for JSON command files dropped by the API. Reads, deletes, dispatches via handle_command(cmd). |
read_from_tcp |
Reads bytes from the TCP socket to the serial bridge. Splits on \n, dispatches each line to the protocol handler. |
monitor_switch |
At 10 Hz, reads the latest GPIO state (start/stop, arm, manual fire) from gpio_handler (which is fed by gpio_status JSON from the dongle), enforces interlocks. |
state_flusher |
Coalesces state-dirty signals into snapshot writes (file + Unix-socket push). Debounces ~10 ms; emits at most every ~10 ms during bursts, plus a 1 s heartbeat. |
(run_show worker) |
Spawned on demand from start_schedule. Runs the precheck → countdown → schedule loop. |
handle_command(cmd) dispatches on cmd['type']. The full list:
| Type | Body | Effect |
|---|---|---|
serial |
{ data: "..." } |
Pass data straight to the dongle (debug). |
manual_fire |
{ data: { zone, target } } |
Manually fire one cue (with arm + start + manual-fire interlock checks). |
db_query |
{ query: "..." } |
Run a raw SQL query (debug). |
delegate_launch |
{ do_it: bool } |
Toggle the delegate_start_to_client flag. |
start_show |
(none) | Start the loaded schedule (delegate-aware). |
stop_show |
(none) | Stop the schedule. |
pause_show |
(none) | Pause; transition to DELEGATE_WAIT if delegate is on. |
schedule |
{ schedule: [...] } |
Override the current schedule (rare; debug). |
stop_schedule |
(none) | Same as stop_show. |
load_show |
{ id: <showId> } |
Load show from SQLite onto the receivers. |
unload_show |
(none) | Send reset to all receivers; clear local state. |
select_serial |
{ device, baud } |
Reconfigure the bridge to talk to a different port. |
set_brightness |
{ brightness } |
Set the LED brightness on the dongle. |
set_receiver_timeout |
{ timeout_ms } |
Set the dongle's "how long without RX = offline" threshold. |
set_command_response_timeout |
{ timeout_ms } |
Set the dongle's per-command response timeout. |
set_clock_sync_interval |
{ interval_ms } |
Set the dongle's clock-sync poll cadence. |
set_debug_mode |
{ debug_mode } |
Toggle dongle debug verbosity. |
set_fire_repeat |
{ repeat_ct } |
Set the per-cue fire-repeat count. |
reload_receivers |
(none) | Re-read Receivers table; reconcile dongle's poll list. Refused while a show is loaded. |
retry_receiver |
{ ident } |
Re-send sync for one receiver. |
fetch_receiver_config |
{ ident?, fire_duration_ms? } |
Send rxcfg <ident> (CONFIG_QUERY). Optionally write a new fire duration first. |
set_rf_channel |
{ channel } (0..125) |
Hot-swap the dongle's RF channel. Refused while a show is loaded or armed. |
ota_flash_start |
{ ident, image_path, rate } |
Begin OTA flash. |
ota_flash_abort |
(none) | Abort in-flight OTA. |
scan_radio |
{ passes, ch_start, ch_end } |
Start an RF spectrum scan. |
See Daemon command reference for the exact JSON shapes of each.
protocol_handler/BYHProtocolHandler.py (~1500 lines) is where most of the show logic lives.
On init (and on reload_receivers):
- Read every enabled row from the
Receiverstable. - Build the in-memory
receiversdict keyed by ident:{ label, type, cues, enabled, metadata, configuration_version }. - For each non-Bilusocn receiver, send
sync <ident> 0 1to the dongle. This forces the dongle to add the receiver to its TDMA poll list. - (Bilusocn receivers are skipped — there's no bidirectional link to maintain.)
The handler maintains two per-receiver state pieces:
-
status: live telemetry (battery, lmt, loadComplete, startReady, continuity, lat, successPercent, fwVersion, boardVersion, numBoards, cuesAvailable, fireDurationMs). -
drift: most recent host-vs-dongle clock offset (used to project receiver timestamps into host wall time).
Two paths populate them:
-
process_status_msgon the dongle's per-second aggregatestatusJSON. Walks thereceivers[]array, merges each entry into the per-receiver dict, recomputes the rolling latency average over 20 samples. -
process_rxupd_msgon the dongle's "fast path" per-receiver update. Fires every time the dongle gets new state from one receiver. Critical detail: if thex(latency sample) field is absent, the dongle had a TX failure on this receiver — so the handler does not advancelmt. This avoids "fake fresh" status when the radio is failing.
receiver_is_connected(ident) returns true if now_ms - status.lmt < 8000 (LATENCY_TO_CONSIDER_ONLINE_MS).
load_show(firing_array, show_id):
- For each item, resolve
(zone, target)→ device id by walkingreceivers[*].cues. If duplicates (same zone:target across receivers) — error. - Annotate each item with
device_id,type,async_fire(true for everything except Bilusocn). - Group async items by device →
async_device_load_dict. - Call
load_async_fire_targets:-
startload <ident> <count> <show_id>. Wait 50 ms. - Send
showloadnchunks (up to 6 cues per line, 30 ms apart). Time values areround(startTime * 1000)ms; positions aretarget - 1(0-based on the wire).
-
- Set
load_waiting = True. ReturnFalse. - Subsequent
process_rxupd_msgcalls updateloadComplete. Once every async target reportsloadComplete=truefor this show id,signal_show_loadedflips state toLOADED.
run_show():
-
Precheck (
run_precheck):- Battery: every cue's target must report
battery >= min_battery_to_fire_pct(default 30, configurable viaprotocols.BKYD_TS_HYBRID.config). - Continuity (if
require_continuityis true): the receiver'scontinuity[2]masks (two 64-bit values for up to 128 cues) must have bit(target - 1)set. - Returns a list of error strings; non-empty → abort.
- Battery: every cue's target must report
- Pick
show_start_time = now_ms + SHOW_START_TIME_SECONDS(25 s). -
Broadcast
showstart(×6 repeats) to all preloaded receivers viasend_to_active_nodes. -
Wait for
startReady=truefrom every preloaded receiver. Reissueshowstartto stragglers every 6 iterations. AbortABORT_PRE_START_SECONDS(10 s) before T-0 if anyone still isn't ready. -
Countdown. While
now < show_start_time, sendplay 0to all preloaded receivers every 3 s. -
Schedule loop. For each item in the firing array, busy-wait until
(time.time() - start_time_epoch_sms) >= delay + pause_offset, then callfire_item(item):-
async_fire = True→ no-op (the receiver fires it from its preloaded schedule). -
async_fire = False(Bilusocn) →433fire <BSC_pkg> xto the dongle.
-
-
End. When the schedule's last item has been processed, transition
RUN_STATEtoSTOPPED.
-
Pause (UI button or start-switch UP mid-show): set
schedule_pause_event, sendpause 0to all receivers. Receivers freeze their local schedule. The host accumulatespause_offsetso resumed cues are correctly delayed. -
Stop (Abort button or both switches UP): set
schedule_stop_event, sendstop 0. Receivers immediately quit firing.
protocol_handler/OtaFlashDriver.py. Single-threaded driver started by start_ota_flash(ident, image_path, rate).
Lifecycle:
- Read the entire
.bininto memory (on the API thread, so the OTA worker doesn't do disk I/O). - Compute CRC32.
-
flash_begin <ident> <total_size> <total_chunks> <crc_hex> <rate>to the dongle. Wait forbegin_okevent (parsed byfeed_eventfrom compactOA/ONlines). -
For each 29-byte chunk:
flash_data <idx> <hex>. Wait forack(idx matches) ornack(retry). - Strict ACK gate — never have two chunks in flight. Avoids saturating the dongle's 128-deep command queue.
-
flash_end. Wait fordone(ortimeoutafter 20 s of silence). - The dongle then
serviceOtaRejoins the receiver — pinging it withCLOCK_SYNCuntil aRECEIVER_STATUSfrom the new firmware arrives, or 30 s elapses.
Retries:
- Up to 12 host-side attempts per chunk, with backoff.
- After attempts 3, 6, 9: send
flash_recover <idx> <level>(level 0 = REPLAY, 1 = SOFT, 2 = FULL). The dongle drops/reopens the radio at the appropriate level to recover from a wedged link.
Gating:
-
start_ota_flashrefuses if a show is loaded, the system is armed, the receiver is offline, the receiver isBILUSOCN_433_TX_ONLY, or the image is unreadable.
update_state_file() is called by the state_flusher thread. It builds a JSON dict containing:
- Daemon health (
device_running,device_found,daemon_lup). - Show state (
show_loaded,loaded_show_name,loaded_show_id,show_running,device_is_transmitting). - Switch state (
device_is_armed,manual_fire_active,start_sw_active). - Errors (
fire_check_failures,proto_handler_errors,proto_handler_status,active_protocol). - Delegate flag (
dstc,waiting_for_client_start), show start time (sst). - Per-receiver
receiversdict. - OTA snapshot (from
OtaFlashDriver.get_ota_state()). - Settings (LED brightness, fire repeat count, RF channel, last RF scan summary, command queue depth/capacity, …).
Writes via:
-
First: best-effort UDP send to
/tmp/byh_state.sock(the WS server's Unix datagram listener). Sub-millisecond delivery to subscribers. -
Then: atomic file write to
/data/stateviamkstemp+os.replace. Acts as the durable record and a fallback for any reader that polls instead of subscribing.
Config is loaded from /config/systemcfg.json on startup. The fields the daemon cares about:
-
system.dongle_port— the TCP bridge configures the serial port from this. -
system.dongle_baud— fixed 115200. -
protocols.BKYD_TS_HYBRID.config—min_battery_to_fire_pct,require_continuity. Re-read on everyrun_precheck.
The Receivers table is the source of truth for receivers. The legacy receivers block in systemcfg.json is only used to seed the table the very first time the daemon starts on an empty database (seedReceiversFromSystemCfgIfEmpty() in sqldb.js).
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