-
Notifications
You must be signed in to change notification settings - Fork 3
Show Lifecycle
A temporal walkthrough of "what happens between selecting a show and watching the last cue go off". Read Glossary and terms first if any vocabulary is unclear.
┌──────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ ┌────────┐
│ Design │ -> │ Stage │ -> │ Load │ -> │ Arm │ -> │ Pre-check + T-N │ -> │ Run │
└──────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────────────┘ └────────┘
│
Stop / Abort │
<───────────────────────┘
Each phase has its own UI mode badge and its own daemon RUN_STATE. The interlocks below describe the exact transitions.
You build the show in the Editor (Show Builder). You drop inventory items onto a timeline, optionally synced to one or more audio tracks, assign each item to a zone:target (a receiver and a cue number), and set per-show metadata (name, duration, authorization code, receiver list, optional spatial layout).
The show lives only in the SQL Show row at this point. Nothing has been preloaded. The receivers don't know about it.
Mode badge: Design (default when no show is loaded).
Daemon RUN_STATE: OFF.
You go to the Console page, search/pick the show you want to fire, and click Stage. The browser stores stagedShowId in localStorage; the Console UI refreshes to show the staged show's timeline (read-only), receiver health, and a big Load show button.
Staging is a UI concept only — no daemon involvement, no state change in the backend. You can stage a show without the dongle even being plugged in.
Mode badge: Ready once a show is staged (and the daemon is healthy).
Clicking Load show prompts for the show's authorization code. Once you type it, the UI does:
-
Pre-flight check. Verifies every receiver in the show is enabled, online, and has enough cues (the
useShowReceiverVerificationhook). If any check fails, the button switches to Resolve receivers and won't proceed. -
POST
/api/system/cmd_daemonwith{ "type": "load_show", "id": <showId> }. The handler writes/tmp/d_cmd/<ts>.json. - The daemon's
poll_command_dirthread reads the file, callsBYHProtocolHandler.load_show(showId).
What load_show does:
- Reads the show row from SQLite, decompresses
display_payload, runsprocess_display_payloadto convert it into a sorted firing array (each entry is{ startTime, zone, target, id }). - For every cue with a 2.4 GHz receiver (
async_fire = True), groups by receiver and callsload_async_fire_targets. - For each target receiver:
- Sends
startload <ident> <count> <showId>to the dongle. - Waits 50 ms.
- Sends chunked
showloadnlines (up to 6 cues per line, 30 ms apart) — eachshowloadnis dynamically packed into a single 32-byte nRF24 frame.
- Sends
- Sets
load_waiting = Trueand returns. The "loaded" status only flips once every receiver reportsloadComplete = truefor the requestedshowIdin its next ACK payload.
The UI shows Loading with a spinner while the receivers are still confirming. Within ~1–2 seconds (depending on receiver count and cue density) the spinner clears and the mode badge flips to Loaded.
Mode badge: Loading → Loaded.
Daemon RUN_STATE: OFF → OFF (load is independent of run state).
Daemon LOAD_STATE: LOADING → LOADED.
If anything goes wrong — a receiver doesn't ACK, the protocol handler runs into an error, the start switch is wrongly engaged — the UI surfaces the failure as a red banner and the daemon writes it to /data/log/daemon.err.
433 MHz cues are not preloaded. They're stored on the host and bit-banged on the dongle's 433 MHz frontend at the cue's exact time during
run_show. Bilusocn receivers are TX-only: they don't have memory and they don't know what time it is.
Two physical switches on the dongle gate the next phase:
| Switch | Required position |
|---|---|
| Arm | DOWN (ARMED) |
| Start / stop | UP (STOP) |
The daemon's monitor_switch thread reads the switch state from the dongle's gpio_status JSON and updates is_armed / start_sw_active. The UI shows them in the status bar.
The interlock matters: if the start switch is already down ("START") when you try to load, the daemon refuses (Cannot load a show when the START button is active). You have to flip it up first, load, then arm and flip the start switch.
Mode badge: Loaded → Armed once both interlocks are satisfied.
Daemon RUN_STATE: OFF → ARMED.
This is where the delegate-to-client mechanism kicks in.
By default, the daemon flag delegate_start_to_client (a.k.a. dstc) is on. With it on:
- You click Launch in the Console UI.
- Browser POSTs
{ "type": "start_show" }→ daemon receives, callsstart_schedule(). - Because
delegate_start_to_clientis true, the daemon does not start the show. It setswaiting_for_client_start = True, transitionsRUN_STATEtoDELEGATE_WAIT, and waits. - The UI sees
waiting_for_client_startanddstcand presents a second Launch button, this time labeledLaunchwith the live styling. - Clicking it again POSTs
{ "type": "start_show" }again, which the daemon now interprets as the operator's confirmation. It entersstart_schedule(from_delegate=True).
Equivalently — and this is the intended workflow — flipping the start switch on the dongle to DOWN (START) drops a follow-up start_schedule from the switch monitor. The operator's hand on the box is the second confirmation.
Why two clicks / a switch? Loading a show, picking a start time, and broadcasting "go" all happen on the laptop, but the actual go-no-go decision is a hardware concept. We mirror what big firing systems do: the box has to physically agree.
Once start_schedule(True) runs:
-
Pre-check (
run_precheck):- For every cue in the firing array, check the target receiver's reported
battery≥min_battery_to_fire_pct. - If
require_continuityis on, check the receiver's continuity mask and confirm bit(target - 1)is set. - Any failure →
fire_check_failurespopulated,RUN_STATEgoes toSTOPPEDwithERR_STATE.DAEMON. The UI shows the errors.
- For every cue in the firing array, check the target receiver's reported
-
Pick
show_start_timeasnow_ms + 25 s. Broadcastshowstart <ident> <show_start_time> 0 <show_id>(repeat 6) to every preloaded receiver. -
Wait for
startReady=truefrom every receiver. Reissueshowstartevery 6 seconds to stragglers. If any receiver isn't ready byshow_start_time - 10 s, abort with errors. -
Countdown. While
now < show_start_time, sendplayto all async receivers every 3 seconds (keepalive in case a few packets dropped). The UI shows a bigT-NN.Ntimer.
Mode badge: Pre-check → Countdown.
Daemon RUN_STATE: ARMED → PRECHECK → COUNTDOWN.
At T-0:
-
2.4 GHz cues fire from the receivers' own preloaded schedules. The host doesn't have to send anything per cue. Each receiver computes
getSynchronizedTime() - showStartTimeand walks its sorted cue table. -
433 MHz cues are timed by the host's schedule loop. When
(time.time() - start_time_epoch_sms) >= cue.delay, the daemon sends433fire <BSC_pkg> xto the dongle, which bit-bangs it on the 433 MHz line.
During the show:
- The receivers'
RECEIVER_STATUSACK payloads keep flowing back. Continuity bits flip from "needed" (red) to "fired" (blue), then to "fired-out" (off) on the cue strip. Battery telemetry stays current. - The daemon's
time_cursorincrements and is published to/tmp/fw_cursor, which the WS server pushes to the browser. The Console timeline cursor sweeps left-to-right. - The dongle's command queue depth fluctuates with traffic. If you're firing many 433 cues simultaneously, you might see the queue depth spike.
Pause: flipping the start switch to UP mid-show. The daemon sends pause to all receivers; they freeze their local schedule. The host accumulates pause_offset so when you resume (flip start back DOWN) cues continue from where they left off.
Stop / Abort: clicking Abort in the UI, or flipping start UP and then off arming, sends stop to all receivers. The receivers immediately quit firing.
Lost host: if a receiver doesn't hear from the dongle for the configured timeout (~8 s floor, adaptive based on the receiver's own measured poll cadence), it triggers a recovery flash on its main strip but continues firing the show on schedule. This is intentional: once preloaded and started, the show is committed. The radio dropping is not a reason to leave half a show un-fired in mortar tubes. (Stop has to come from a deliberate operator action.)
Mode badge: Live.
Daemon RUN_STATE: RUNNING.
When the schedule's last cue has fired and the configured tail time has elapsed, the daemon sets RUN_STATE back to STOPPED. The show stays loaded — you can hit Unload to clear it from the receivers (sends reset <ident> 0 to each).
If you want to fire the same show again, you have to re-load, re-arm, and re-launch. Same flow as before.
A quick reference of what the top-bar mode badge shows at each phase:
| Phase | Mode badge | UI accent |
|---|---|---|
| no show staged | Design |
neutral |
| show staged, daemon healthy, not loaded | Ready |
neutral |
| show loading | Loading |
neutral |
| show loaded | Loaded |
neutral |
| arm switch flipped | Armed |
red rail |
| pre-check running | Pre-check |
red rail |
| pre-check passed, in countdown | Countdown |
red rail |
| at/after T-0, schedule running | Live |
red rail + barber-pole |
| schedule paused | Paused |
red rail |
| stop | Stopped |
red rail |
| manual fire mode engaged | Manual fire |
red rail |
| daemon offline | Disconnected |
yellow |
- End-to-end show example — concrete run with two receivers and four cues.
- UI Console page — the operator's view of every phase.
-
PC daemon — the implementation of
load_show/run_show. -
BYH protocol handler —
load_async_fire_targets,run_precheck,run_show.
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