Skip to main content

Documentation

Architecture

3 min readEdit on GitHub

Technical overview of how Lights Pi components fit together.

System Diagram

snippet
1   Browser / phone / voice    MCP-aware LLM agents    ┌─────────── AI providers ──────────┐
2              │              (Claude Desktop, etc.)   │  OpenAI • Anthropic • Ollama       │
3              │  WiFi / HTTPS        │                └─────────────┬─────────────────────┘
4              ▼                      ▼                              │
5       Raspberry Pi                                                 │
6   ┌─────────────────────────────┐  ┌──────────────────────────┐    │
7   │ nginx (80/443, optional)    │  │ MCP server (port 5001)   │    │
8   │ landing page + reverse proxy│  │  Streamable HTTP @ /mcp  │    │
9   └──────────┬──────────────────┘  └───────────┬──────────────┘    │
10              │                                 │ HTTP              │
11   ┌──────────▼─────────────────────────────────▼──┐ HTTP+WebSocket │
12   │ Flask control server (port 5000)              │◄───────────────┘
13   │  • AI chat → DMX                              │
14   │  • Live virtual console                       │
15   │  • Fixture groups                             │
16   │  • .qxf-aware channels                        │
17   │  • Scene save/snapshot                        │
18   │  • /api/action structured dispatch (for MCP)  │
19   └──────────┬────────────────────────────────────┘
20              │ persistent WebSocket
2122   ┌──────────────────────────┐
23   │ QLC+ headless (port 9999)│
24   │  + .qxf fixture defs     │
25   └──────────┬───────────────┘
26              │ USB
2728       ENTTEC DMX USB Pro
29              │  DMX
3031        DMX Fixtures (rig)

Services on the Pi

ServicePortPurpose
qlcplus-web.service9999QLC+ headless with web UI
lighting-control.service5000Flask control server
lighting-mcp.service5001MCP server (Streamable HTTP, LLM agent endpoint) — see MCP-Server
nginx80/443Landing page + optional HTTPS reverse proxy
wifi-watchdog.timerAuto-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

PathPurpose
/home/<user>/.qlcplus/default.qxwActive workspace (loaded by QLC+ on boot)
/home/<user>/.qlcplus/fixture_groups.jsonPersisted 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.shCLI entry point (deployed from workstation)
/home/<user>/scripts/Supporting scripts and libraries

Was this page helpful?