Skip to content

Show Lifecycle

OneSeventyFour edited this page May 14, 2026 · 1 revision

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.

The phases

  ┌──────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────────────┐    ┌────────┐
  │  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.


Phase 1 — Design

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.


Phase 2 — Stage

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).


Phase 3 — Load

Clicking Load show prompts for the show's authorization code. Once you type it, the UI does:

  1. Pre-flight check. Verifies every receiver in the show is enabled, online, and has enough cues (the useShowReceiverVerification hook). If any check fails, the button switches to Resolve receivers and won't proceed.
  2. POST /api/system/cmd_daemon with { "type": "load_show", "id": <showId> }. The handler writes /tmp/d_cmd/<ts>.json.
  3. The daemon's poll_command_dir thread reads the file, calls BYHProtocolHandler.load_show(showId).

What load_show does:

  1. Reads the show row from SQLite, decompresses display_payload, runs process_display_payload to convert it into a sorted firing array (each entry is { startTime, zone, target, id }).
  2. For every cue with a 2.4 GHz receiver (async_fire = True), groups by receiver and calls load_async_fire_targets.
  3. For each target receiver:
    • Sends startload <ident> <count> <showId> to the dongle.
    • Waits 50 ms.
    • Sends chunked showloadn lines (up to 6 cues per line, 30 ms apart) — each showloadn is dynamically packed into a single 32-byte nRF24 frame.
  4. Sets load_waiting = True and returns. The "loaded" status only flips once every receiver reports loadComplete = true for the requested showId in 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: LoadingLoaded. Daemon RUN_STATE: OFFOFF (load is independent of run state). Daemon LOAD_STATE: LOADINGLOADED.

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.


Phase 4 — Arm

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: LoadedArmed once both interlocks are satisfied. Daemon RUN_STATE: OFFARMED.


Phase 5 — Pre-check + countdown

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:

  1. You click Launch in the Console UI.
  2. Browser POSTs { "type": "start_show" } → daemon receives, calls start_schedule().
  3. Because delegate_start_to_client is true, the daemon does not start the show. It sets waiting_for_client_start = True, transitions RUN_STATE to DELEGATE_WAIT, and waits.
  4. The UI sees waiting_for_client_start and dstc and presents a second Launch button, this time labeled Launch with the live styling.
  5. Clicking it again POSTs { "type": "start_show" } again, which the daemon now interprets as the operator's confirmation. It enters start_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:

  1. Pre-check (run_precheck):
    • For every cue in the firing array, check the target receiver's reported batterymin_battery_to_fire_pct.
    • If require_continuity is on, check the receiver's continuity mask and confirm bit (target - 1) is set.
    • Any failure → fire_check_failures populated, RUN_STATE goes to STOPPED with ERR_STATE.DAEMON. The UI shows the errors.
  2. Pick show_start_time as now_ms + 25 s. Broadcast showstart <ident> <show_start_time> 0 <show_id> (repeat 6) to every preloaded receiver.
  3. Wait for startReady=true from every receiver. Reissue showstart every 6 seconds to stragglers. If any receiver isn't ready by show_start_time - 10 s, abort with errors.
  4. Countdown. While now < show_start_time, send play to all async receivers every 3 seconds (keepalive in case a few packets dropped). The UI shows a big T-NN.N timer.

Mode badge: Pre-checkCountdown. Daemon RUN_STATE: ARMEDPRECHECKCOUNTDOWN.


Phase 6 — Run

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() - showStartTime and 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 sends 433fire <BSC_pkg> x to the dongle, which bit-bangs it on the 433 MHz line.

During the show:

  • The receivers' RECEIVER_STATUS ACK 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_cursor increments 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.


Phase 7 — End

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.


Mode badge timeline

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

See also

Clone this wiki locally