diff --git a/docs/plans/2026-02-08-display-system-design.md b/docs/plans/2026-02-08-display-system-design.md new file mode 100644 index 0000000..6e6fd64 --- /dev/null +++ b/docs/plans/2026-02-08-display-system-design.md @@ -0,0 +1,307 @@ +# Display System Design + +## Overview + +PVM venues need to push content (tournament clocks, waitlists, announcements) to multiple screens. This design covers the hardware, software, and architecture for a managed digital signage system built on cheap ARM SBCs. + +## System Topology + +``` + ┌─────────────┐ + │ Core Server│ (Hetzner) + │ PVM Backend│ + └──────┬──────┘ + │ VPN tunnel (WireGuard) + │ - data sync (messaging pipeline) + │ - remote management (SSH, updates) + │ - content push (templates, media) + │ - remote screen management + │ + ┌──────┴──────┐ + │ Orange Pi 5 │ (1 per venue, ~€65) + │ "Leaf Node" │ + │ │ + │ • PVM local backend + │ • Screen management API + │ • Video generation pipeline + │ • Display node orchestrator + │ • mDNS discovery service + │ • Local web server (display content) + └──────┬──────┘ + │ Local network (LAN/WiFi) + │ mDNS auto-discovery + │ WebSocket for live updates + │ + ┌────────────┼────────────┐ + │ │ │ + ┌─────┴─────┐ ┌───┴───┐ ┌─────┴─────┐ + │ ROCK 4D │ │ROCK 4D│ │ ROCK 4D │ (~€34 each) + │ Screen 1 │ │Scrn 2 │ │ Screen N │ + │ Kiosk │ │Kiosk │ │ Kiosk │ + └───────────┘ └───────┘ └───────────┘ +``` + +### Key Principles + +- Orange Pi 5 is fully autonomous — venue operates with no internet. +- Display nodes are stateless thin clients — boot, discover, display. +- All display content served locally — no cloud dependency for screens. +- CEC gives power/input/volume control over connected TVs. +- Conflict resolution: leaf wins, timestamps for ordering. + +## Hardware + +### Orange Pi 5 (Leaf Node) + +- **SoC:** Rockchip RK3588S +- **CPU:** 4x Cortex-A76 @ 2.4GHz + 4x Cortex-A55 @ 1.8GHz +- **GPU:** Mali-G610 MP4 +- **RAM:** 4GB (minimum, 8GB recommended) +- **HDMI:** 2.1, 8K@60Hz, CEC supported +- **Video decode:** H.265/VP9 8K@60fps, AV1 4K@60fps, H.264 8K@30fps +- **Video encode:** H.265/H.264 8K@30fps, 16x 1080p@30fps parallel +- **Price:** ~€65 +- **Role:** Runs all venue-local services. Optional video generation pipeline via ffmpeg-rockchip (hardware-accelerated encode). + +### Radxa ROCK 4D (Display Node) + +- **SoC:** Rockchip RK3576 +- **CPU:** 4x Cortex-A72 @ 2.2GHz + 4x Cortex-A53 @ 1.8GHz +- **GPU:** Mali-G52 MC3 +- **RAM:** 4GB +- **HDMI:** 2.1, 4K@60Hz, CEC supported +- **Video decode:** H.265/VP9/AV1 8K@30fps / 4K@120fps, H.264 4K@60fps +- **Video encode:** H.265/H.264 4K@60fps (not used for display, but available) +- **Price:** ~€34 +- **Role:** Thin client. Boots into kiosk Chromium, renders assigned URL at 4K@60Hz. + +### Bill of Materials (Per Venue) + +| Item | Unit Cost | Qty | Notes | +|------|-----------|-----|-------| +| Orange Pi 5 (4GB) | ~€65 | 1 | Leaf node | +| Radxa ROCK 4D (4GB) | ~€34 | N | One per screen | +| SD cards / eMMC | ~€5-10 | N+1 | Storage per device | +| Power supplies | ~€5-8 | N+1 | USB-C PSU per device | + +Minimum venue cost: ~€110 (1 leaf + 1 screen). Typical small venue (4 screens): ~€250. Large venue (12 screens): ~€550. + +## Display Node Lifecycle + +### Boot Sequence + +1. Power on, custom Linux image boots (Armbian BSP, ~5-8s). +2. Loads last provisioned background from local storage (immediate visual, no blank screen). +3. Agent daemon starts, announces via mDNS (`_pvm-display._tcp`). +4. Broadcasts: device ID (persistent, generated on first boot), MAC, IP, display capabilities. +5. Waits for assignment from the Orange Pi 5. +6. Once assigned, kiosk Chromium opens the assigned URL: `http://leaf.local:3000/display/{screen-id}/{stream-slug}`. +7. WebSocket connection established for live updates + heartbeat. + +### Steady State + +- Renders assigned webpage at 4K@60Hz. +- WebSocket receives live data pushes (clock ticks, table updates, announcements). +- Heartbeat ping every 5s back to leaf node (health monitoring). +- CEC daemon listens for commands (power, volume, input switching). + +### Reassignment + +Staff drags screen to a new stream in the web UI. Leaf node sends WebSocket message to the display node: `{ type: "navigate", url: "/display/{screen-id}/{new-stream}" }`. Chromium navigates to new URL — instant switch, no reboot. + +### Failure Modes + +- **Leaf node unreachable:** display continues showing last content (static). WebSocket reconnects with exponential backoff. +- **Display node reboots:** boots to last background, re-announces via mDNS, leaf re-assigns automatically. +- **Network loss:** same as leaf unreachable — last content persists. + +## Power Management + +### Default: Always-On + +- DPMS disabled at boot (`xset -dpms; xset s off; xset s noblank`). +- Screen blanking disabled in Chromium flags (`--disable-screen-saver`). +- Kernel: `consoleblank=0` boot param. +- No screensaver, no idle timeout — screen stays on 24/7 unless the management UI says otherwise. + +### Managed Power Schedules (via Web UI) + +- Per-screen or per-group schedules (e.g., "Screen 1: on at 17:00, standby at 03:00"). +- Uses CEC `power:standby` / `power:on` to control the TV. +- The ROCK 4D stays powered on (agent daemon running) even when the TV is in standby — ready to wake it instantly. +- Override button in web UI: "Wake all screens now" / "Sleep all screens now". + +### CEC Power State Tracking + +- Agent polls TV power state periodically (every 30s). +- If TV is manually turned off by someone with a remote, leaf node knows and flags it in the UI ("Screen 3: TV powered off manually"). +- Optional: auto-wake if TV is turned off outside scheduled sleep window. + +## CEC Commands + +Sent from the leaf node to display nodes via their agent daemon HTTP API: + +- `power:on` / `power:off` / `power:standby` +- `volume:set:{level}` / `volume:mute` / `volume:unmute` +- `input:switch` (force TV to the ROCK 4D's HDMI input) +- `status:query` (returns TV power state) + +## Stream Model + +A stream is a URL. The leaf node serves its own pages (tournament clock, waitlist, etc.) as URLs, and can also point a screen at any external URL. + +Each stream has: + +- **Name:** friendly label (e.g., "Tournament Clock", "Welcome Screen") +- **URL:** the content source +- **Default audio level:** 0-100, muted by default +- **Priority:** for scheduled overrides (e.g., "announcement" stream temporarily takes over all screens) +- **Fallback:** URL to show if the primary stream source fails + +Future upgrade: stream a native app's output by capturing the app framebuffer on the Orange Pi 5, encoding via ffmpeg-rockchip to HLS, and displaying in the browser on display nodes. + +## Screen Management API + +The leaf node exposes a RESTful API. All management features are API-first. The web UI and future tablet app are consumers of this API. + +Auth: Bearer token (PVM session token). + +### Screens (Display Nodes) + +``` +GET /api/v1/screens # list all discovered screens +GET /api/v1/screens/:id # screen details + status +PATCH /api/v1/screens/:id # rename, assign group, set default stream +DELETE /api/v1/screens/:id # forget/unpair a screen +``` + +### Screen Control + +``` +POST /api/v1/screens/:id/assign # { streamUrl } +POST /api/v1/screens/:id/power # { action: on|standby|off } +POST /api/v1/screens/:id/volume # { level: 0-100, mute: bool } +POST /api/v1/screens/:id/navigate # { url } — raw URL override +POST /api/v1/screens/:id/reload # force browser refresh +``` + +### Bulk Operations + +``` +POST /api/v1/screens/bulk/assign # { screenIds[], streamUrl } +POST /api/v1/screens/bulk/power # { screenIds[], action } +POST /api/v1/screens/bulk/volume # { screenIds[], level, mute } +``` + +### Streams (Saved URL Presets) + +``` +GET /api/v1/streams # list all configured streams +POST /api/v1/streams # { name, url, defaultAudio } +PATCH /api/v1/streams/:id # update +DELETE /api/v1/streams/:id # remove +``` + +### Groups (Logical Screen Grouping) + +``` +GET /api/v1/groups # list groups +POST /api/v1/groups # { name } +PATCH /api/v1/groups/:id # rename, set default stream +POST /api/v1/groups/:id/assign # assign stream to all screens in group +POST /api/v1/groups/:id/power # power control all screens in group +``` + +### Schedules + +``` +GET /api/v1/screens/:id/schedules # list schedules for a screen +POST /api/v1/screens/:id/schedules # { streamUrl, cron, priority } +DELETE /api/v1/screens/:id/schedules/:sid + +GET /api/v1/groups/:id/schedules # list schedules for a group +POST /api/v1/groups/:id/schedules # { streamUrl, cron, priority } +DELETE /api/v1/groups/:id/schedules/:sid +``` + +### WebSocket Events + +``` +WS /api/v1/events +``` + +Pushes real-time state changes to connected management clients: + +- `screen:discovered` — new device on network +- `screen:online` / `screen:offline` — heartbeat lost/restored +- `screen:tv_power_changed` — CEC detected TV state change +- `screen:assigned` — stream changed (from another client) + +## Display Node Agent API + +HTTP server running on each ROCK 4D, called by the leaf node: + +``` +POST /navigate { url } — switch displayed URL +POST /power { action } — CEC power command to TV +POST /volume { level, mute } — CEC volume command +POST /reload — force browser refresh +GET /status — device health, TV power state, current URL +POST /update { imageUrl } — trigger OTA firmware update +``` + +## Orange Pi 5 Software Stack + +**OS:** Armbian (Debian-based) with Rockchip BSP kernel (6.1 LTS) — needed for MPP/CEC driver support. + +| Service | Role | Tech | +|---------|------|------| +| PVM Backend | Tournament/player data, local DB | Rust (axum), libsql | +| Display API | Screen management REST API + WebSocket events | Rust (axum) | +| Content Server | Serves display pages (tournament clock, waitlist, etc.) | SvelteKit (SSR) | +| Discovery Daemon | mDNS listener for `_pvm-display._tcp`, registers new screens | Rust, `mdns-sd` crate | +| Scheduler | Cron-like engine for time-based stream assignments | Rust, in-process | +| CEC Proxy | Sends CEC commands to display nodes via their agent daemon | Rust, HTTP calls to ROCK 4D agents | +| VPN Client | WireGuard tunnel back to core | `wg-quick` / systemd | +| Sync Agent | Messaging pipeline — pushes data packets to core | Rust | +| Video Pipeline | (Optional) ffmpeg-rockchip for rendering content to HLS | ffmpeg, triggered on demand | + +## Display Node (ROCK 4D) Software Stack + +**OS:** Armbian BSP (minimal), custom image. + +| Service | Role | Tech | +|---------|------|------| +| Agent daemon | mDNS announcement, heartbeat, receives commands from leaf | Rust (cross-compiled aarch64) | +| Chromium kiosk | Fullscreen browser, no UI chrome, renders assigned URL | Chromium, kiosk mode | +| CEC daemon | Controls TV via libcec, receives commands from agent | libcec | +| Power manager | Disables DPMS/blanking, enforces always-on unless told otherwise | systemd + xset | +| OTA updater | Pulls firmware/image updates from leaf node on boot | Rust, in agent daemon | + +## Data Architecture + +``` +┌─────────────────────┐ ┌──────────────────────┐ +│ Core (Hetzner) │ │ Leaf (Orange Pi 5) │ +│ │ VPN + │ │ +│ PostgreSQL │◄────────│ libsql │ +│ • All venues │ msg │ • PVM domain data │ +│ • Full history │ pipeline│ • Screen mgmt state │ +│ • Analytics │ │ • Schedules/groups │ +│ │ │ │ +└─────────────────────┘ └──────────────────────┘ +``` + +- **Leaf (libsql):** Single database, all local state. PVM domain data (tournaments, players, waitlists) and screen management state (devices, streams, groups, schedules). +- **Core (PostgreSQL):** Aggregated data from all venues. Full historical record. Analytics, reporting, billing. Screen management config for remote management. +- **Sync:** Leaf to core via existing messaging pipeline. Eventual consistency with causal ordering and leaf-wins conflict resolution. + +## Monetization + +Charge a small fee per ROCK 4D display device. Covers hardware cost + margin + funds ongoing development. Transparent with early customers that it's to cover initial backend and development costs. + +## Future Upgrades + +- **App streaming:** Capture native app framebuffer on Orange Pi 5, encode via ffmpeg-rockchip to HLS, display in browser on ROCK 4D nodes. +- **AI-generated slides:** Instead of plain text on black background — 3D animated gold text with subtle fire effects, rendered as a webpage. Venue infotainment that scales with customer size. +- **Tablet management app:** Different UI, same API. Drag-and-drop screen management from a tablet on the venue floor.