Skip to content

System Architecture

OneSeventyFour edited this page May 14, 2026 · 1 revision

System architecture

A high-level map of every process and every piece of hardware in a running Backyard Hero install. If a section sparks a question, the Subsystems and Hardware pages have the deep dives.

The stack at a glance

┌──────────────────────────────────────────────────────────────────────┐
│                       HOST COMPUTER                                  │
│                                                                      │
│   ┌────────────────┐       ┌──────────────────────┐                  │
│   │    Browser     │◄────► │      Web App         │                  │
│   │  (operator)    │  HTTP │  Next.js, port 1776  │                  │
│   └────────────────┘       └──────────┬───────────┘                  │
│           ▲                           │                              │
│           │                           ▼                              │
│           │ WebSocket          ┌────────────┐                        │
│           │   :8090            │  SQLite    │                        │
│           │                    │  /data/    │                        │
│           │                    │ backyard…  │                        │
│           │                    └─────▲──────┘                        │
│           │                          │                               │
│           ▼                          │                               │
│   ┌────────────────┐                 │                               │
│   │  WS Server     │                 │                               │
│   │  ws_server.py  │                 │                               │
│   └────────┬───────┘                 │                               │
│            ▲                         │                               │
│            │ /tmp/byh_state.sock     │                               │
│            │ + /data/state file      │                               │
│            │                         │                               │
│   ┌────────┴────────────┐            │                               │
│   │     PC Daemon       │ ──────────►│  /tmp/d_cmd/*.json            │
│   │   pc_daemon.py      │ ◄──────────│   (drop box from API)         │
│   │  + BYHProtoHandler  │            │                               │
│   │  + OtaFlashDriver   │                                            │
│   └─────────┬───────────┘                                            │
│             │ TCP localhost:9000                                     │
│             ▼                                                        │
│   ┌─────────────────────┐                                            │
│   │ tcp_serial_bridge   │  ◄─── runs OUTSIDE Docker                  │
│   │   (Python pyserial) │       (so the container can reach USB)     │
│   └─────────┬───────────┘                                            │
└─────────────┼────────────────────────────────────────────────────────┘
              │ USB-CDC, 115200 8N1
              ▼
       ┌─────────────┐
       │   Dongle    │  ESP32-S2 + nRF24L01+ (2.4 GHz) + 433 MHz TX
       │ os4_dongle  │  3 switches: start/stop, arm, manual fire
       └──────┬──────┘
              │
        ┌─────┴────────────┬────────────────────┬────────────────┐
        │ 2.4 GHz nRF24    │ 2.4 GHz nRF24      │ 433 MHz one-way│
        ▼                  ▼                    ▼                │
   ┌──────────┐       ┌──────────┐       ┌──────────────────┐    │
   │ Receiver │       │ Receiver │  ...  │ Bilusocn 4-ch    │    │
   │ RX146    │       │ RX152    │       │ (TX-only legacy) │    │
   └────┬─────┘       └────┬─────┘       └──────────────────┘    │
        │                  │                                     │
        ▼                  ▼                                     │
   8-cue modules     8-cue modules                               │
   (chained 1–16)    (chained 1–16)                              │
                                                                 │

The host components in dashed Docker box are all packaged in one image (os4ivmb/backyardhero). The serial bridge runs natively on the host so it can see USB.

Each component, what it does

Web app — Next.js, port 1776

host/byh_app/backyardhero/

A single-page React app served by Next.js. The whole UI is rendered behind one Next.js route (/) — every "page" in the nav (Console, Receivers, Editor, Loadout, Inventory, Manual, Settings) is a tab inside MainNav.jsx.

The browser does two things over the network:

  • HTTP to the Next.js server for REST routes under /api/* (CRUD on shows, inventory, racks, receivers; trigger daemon commands; upload OTA firmware).
  • WebSocket to ws://<host>:8090 for live state (every receiver's telemetry, daemon status, time cursor, error log).

See Subsystem: Web app.

PC daemon — Python, supervisord-managed

host/pythings/pc_daemon/pc_daemon.py

The heart of the system. It:

  • Holds the TCP connection to the serial bridge.
  • Parses the dongle's per-second status frames into a per-receiver telemetry dict.
  • Loads shows from SQLite, transforms the timeline into firing arrays, preloads them onto receivers via the dongle.
  • Runs the show schedule, issues the play / stop / pause transport commands, handles 433 MHz cues itself.
  • Polls /tmp/d_cmd/*.json for commands dropped by the web app's API routes.
  • Watches three logical switches (start/stop, arm, manual fire) reported by the dongle, enforces the interlocks.
  • Publishes a JSON state snapshot to /data/state (debounced ~10 ms) and pushes the same JSON to the WS server's Unix datagram socket for low-latency delivery.

See Subsystem: PC daemon.

WebSocket server — Python, port 8090

host/pythings/websock_server/ws_server.py

A thin fan-out layer. Listens on a Unix datagram socket for state pushes from the daemon, additionally watches /data/state, /tmp/fw_cursor, and /tmp/fw_firing via watchfiles, merges everything (plus host CPU/memory/temp via psutil) into one payload, and broadcasts to every connected browser at up to 30 Hz with 5 s heartbeats.

See Subsystem: WS server.

TCP-to-serial bridge — Python, port 9000

host/tcp_serial_bridge/tcp_serial_bridge.py

Runs as a host-native process (not in the container) so it can hold the USB serial port. Forwards bytes both ways between TCP localhost:9000 and the dongle's USB-CDC at 115200 8N1, with auto-reconnect when the dongle reboots.

See Subsystem: Serial bridge.

SQLite database

host/data/backyardhero.db (mounted into the container at /data/)

Tables: Show, inventory, inventoryFiringProfile, racks, Receivers. Created and migrated automatically by src/util/sqldb.js on every Next.js start. See Subsystem: Database for the full schema.

Configuration files

host/config/systemcfg.json, mounted at /config/. Holds the active dongle port + baud, the active protocol, and protocol/type metadata. See Subsystem: Config.

Catalog crawl + firing-profile generator

host/pythings/inv_crawl/crawl_catalog.py and host/pythings/fp_gen/process_firing_profiles.py. Optional services launched on demand from API routes:

  • The crawl pulls a curated shell catalog JSON from backyard-hero.com.
  • The firing-profile generator downloads the audio of a YouTube link and detects shot timings using librosa + scipy.signal.find_peaks.

How a click in the UI becomes a fired cue

  1. Operator clicks Launch in the Console.
  2. Browser POSTs { "type": "start_show" } to /api/system/cmd_daemon.
  3. The Next.js handler writes that JSON to /tmp/d_cmd/<timestamp>.json.
  4. The daemon's poll_command_dir thread reads the file (~500 ms), deletes it, and routes it to BYHProtocolHandler.run_show.
  5. The handler runs the precheck, picks show_start_time = now_ms + 25 s, sends showstart RX146 <epoch_ms> 0 <showId> 6 to the dongle for every preloaded receiver.
  6. The dongle queues a CLOCK_SYNC + SHOW_START packet for each receiver, transmitting on its scheduled poll slot.
  7. Each receiver records showStartTime, sets startReady=true, returns the new state in its next ACK payload.
  8. The dongle pushes that as a rxupd JSON line up to the daemon, which publishes the new state to the WS server, which pushes it to the browser. The Console UI shows "T-25.0".
  9. At T-0, every receiver fires its first cue autonomously from its preloaded schedule. The host doesn't have to send anything.
  10. Telemetry (battery, continuity, success %) keeps flowing back at ~1 Hz via the per-second status aggregate.

That's the entire control loop.

File system layout (inside the container)

Path Meaning
/app/byh_app/ Next.js app (built; npm run start)
/app/pythings/pc_daemon/ Daemon source
/app/pythings/websock_server/ WS server source
/data/ Persistent volume (./data on host): SQLite DB, state file, log files, OTA staging
/config/ Config volume (./config on host): systemcfg.json
/tmp/d_cmd/ Drop-box for command files from the API
/tmp/fw_cursor, /tmp/fw_firing Show playback markers
/tmp/byh_state.sock Unix datagram socket for daemon → WS push

Ports

Port Bound by Purpose
1776 Next.js (byh-app) Web UI + REST API
8090 WS server WebSocket state stream
9000 tcp_serial_bridge (host-native) TCP framing for USB serial

docker-compose.yml exposes 1776 and 8090 on the host. 9000 is not exposed — it's reachable inside the container as host.docker.internal:9000.

Where to go next

Clone this wiki locally