Skip to content

Architecture

Griffen Fargo edited this page May 15, 2026 · 2 revisions

Architecture

Technical overview of how Lights Pi components fit together.

System Diagram

   Browser / phone / voice    MCP-aware LLM agents    ┌─────────── AI providers ──────────┐
              │              (Claude Desktop, etc.)   │  OpenAI • Anthropic • Ollama       │
              │  WiFi / HTTPS        │                └─────────────┬─────────────────────┘
              ▼                      ▼                              │
       Raspberry Pi                                                 │
   ┌─────────────────────────────┐  ┌──────────────────────────┐    │
   │ nginx (80/443, optional)    │  │ MCP server (port 5001)   │    │
   │ landing page + reverse proxy│  │  Streamable HTTP @ /mcp  │    │
   └──────────┬──────────────────┘  └───────────┬──────────────┘    │
              │                                 │ HTTP              │
   ┌──────────▼─────────────────────────────────▼──┐ HTTP+WebSocket │
   │ Flask control server (port 5000)              │◄───────────────┘
   │  • AI chat → DMX                              │
   │  • Live virtual console                       │
   │  • Fixture groups                             │
   │  • .qxf-aware channels                        │
   │  • Scene save/snapshot                        │
   │  • /api/action structured dispatch (for MCP)  │
   └──────────┬────────────────────────────────────┘
              │ persistent WebSocket
              ▼
   ┌──────────────────────────┐
   │ QLC+ headless (port 9999)│
   │  + .qxf fixture defs     │
   └──────────┬───────────────┘
              │ USB
              ▼
       ENTTEC DMX USB Pro
              │  DMX
              ▼
        DMX Fixtures (rig)

Services on the Pi

Service Port Purpose
qlcplus-web.service 9999 QLC+ headless with web UI
lighting-control.service 5000 Flask control server
lighting-mcp.service 5001 MCP server (Streamable HTTP, LLM agent endpoint) — see MCP-Server
nginx 80/443 Landing page + optional HTTPS reverse proxy
wifi-watchdog.timer Auto-recovery for dropped WiFi (every 2 min)

Persistent WebSocket

The control server holds exactly one WebSocket to QLC+ for its entire lifetime. This is critical because QLC+ 4.14.x has a hard limit (~50) on concurrent WebSocket clients. Previous architectures that opened a new connection per request would exhaust this limit within minutes.

Key design decisions:

  • Dedicated asyncio event loop in a daemon thread owns the WebSocket
  • All Flask request handlers dispatch via asyncio.run_coroutine_threadsafe
  • A background reader task continuously drains incoming messages
  • On connection drop, the reader explicitly closes the socket (preventing CLOSE_WAIT leak) and the next request lazily reconnects

Fixture Definition Parser

control-server/fixture_definitions.py reads .qxf files and resolves a semantic role for each channel:

  1. <Channel Preset="IntensityRed"> → role = red
  2. <Colour>White</Colour> + name contains "Warm" → role = warm
  3. Exact channel name match ("Strobe" → strobe)
  4. Group classification (Shutter → strobe, Colour → macro)
  5. Channels in Speed/Maintenance/Effect groups → role = null (never driven by color commands)

This metadata flows to:

  • The AI prompt (so it picks correct channels per fixture)
  • The UI (correct slider labels)
  • apply_color_live() (drives only color-role channels, zeros everything else)

Scene Save Flow

  1. User sends AI command → scene XML generated and applied live
  2. scene_xml returned in the API response
  3. User clicks 💾 → frontend sends XML + name to POST /api/scenes/save
  4. Backend injects the <Function> element into the workspace's <Engine>
  5. Scene gets the next available ID and appears in the Scenes tab immediately
  6. Persists through reboots (it's in the .qxw file QLC+ loads on boot)

File Locations on Pi

Path Purpose
/home/<user>/.qlcplus/default.qxw Active workspace (loaded by QLC+ on boot)
/home/<user>/.qlcplus/fixture_groups.json Persisted fixture groups
/home/<user>/control-server/ Flask app source
/home/<user>/control-server-venv/ Python virtual environment (control server)
/home/<user>/mcp-server/ MCP server source
/home/<user>/mcp-server-venv/ Python virtual environment (MCP server)
/usr/share/qlcplus/fixtures/ System fixture definitions (.qxf)
~/.qlcplus/fixtures/ User fixture definition overrides
/home/<user>/lightsctl.sh CLI entry point (deployed from workstation)
/home/<user>/scripts/ Supporting scripts and libraries

Clone this wiki locally