Skip to main content

Documentation

Control Server

6 min readEdit on GitHub

The custom control server runs on the Pi at port 5000 and provides a richer interface on top of QLC+'s native web UI.

Current version: v2.13.2

Overview

Access it at http://lights.local:5000

The control server provides:

  • Chat (new default tab) — Full agentic Claude/ChatGPT-style conversation with access to ~39 MCP tools. The LLM plans multi-step operations ("set up three-point lighting and save it as a scene") and executes them server-side. Anthropic or OpenAI; Ollama not yet supported.
  • Quick Commands (was AI Control) — One-shot natural language commands ("make it warmer", "party mode", "fade to black over 5 seconds")
  • Virtual Console — Per-fixture channel sliders with correct labels from .qxf definitions, plus an Identify button per fixture (pulses it for physical location)
  • Scene Management — Activate, rename, duplicate, delete saved scenes via a hover-menu on each scene tile
  • Cue Lists (new tab) — Build audio-synced shows, GO/STOP playback, live elapsed-time indicator
  • Chases (new tab) — Build QLC+ chases from a scene picker, start/stop via the native chase engine
  • Quick Tools (new tab) — Kelvin slider for white balance, strobe rate slider, palette assignment
  • Diagnostics (new tab) — Pi health (CPU temp / load / memory / disk / uptime / USB / service status), test_dmx button, systemd log tail
  • Fixture Groups — Create / rename / delete groups inline from the drawer, organize fixtures into named zones, target commands to specific groups
  • BLACKOUT — Always-visible header button. Zeros every channel including strobe/macro state.
  • Health Dashboard — Live status of QLC+, WebSocket connection, AI provider, and workspace

Visual Identity (v2.13)

The UI was redesigned in v2.13 around a minimal, console-inspired aesthetic:

  • Geist Sans + Geist Mono for body and code respectively
  • CSS token system on :root--ink, --paper, --rule, --amber-tungsten, --signal-* — single source of truth for colors
  • Amber-tungsten accent (#ffa257) for active/selected states, evoking a warm filament
  • Filament dot indicator in the header (live status pulse)
  • Animated tab indicator slides under the active tab
  • Eyebrow labels on tool panels via data-eyebrow attribute (small uppercase context labels)
  • BLACKOUT button redesigned — always-visible header button, high-contrast red, single click zeros every channel including strobe and macro state

Tab Persistence (v2.13.2)

The active tab is mirrored to the URL as ?tab=<name> via history.replaceState, so:

  • Refresh lands you back on the same tab
  • Tab URLs are bookmarkable and shareable
  • The default landing tab (when no ?tab= is present) is Quick Commands — changed from Chat in v2.13.1

Valid values: ?tab=chat, ai, console, cuelists, chases, tools, diag.

Mobile + PWA

The UI is mobile-first and installable as a Progressive Web App on iOS and Android. See Mobile-and-PWA for installation steps, what works offline, and the mobile-specific UX (44px touch targets, full-screen modals, horizontal-scroll tabs row, edge-to-edge chat composer).

Installation

bash
./lightsctl.sh control-install

This creates a Python venv on the Pi, installs dependencies, and sets up lighting-control.service.

Architecture

The server maintains a single persistent WebSocket to QLC+ on a dedicated background thread. All HTTP requests dispatch channel commands through this one connection. This avoids the CLOSE_WAIT socket leak that occurs when each request opens its own short-lived connection (QLC+ 4.14.x has a hard limit of ~50 concurrent WebSocket clients).

See CONTROL_SERVER_ARCHITECTURE.md for the full technical deep-dive.

API Endpoints

MethodPathPurpose
POST/api/commandAI natural-language command (one-shot interpreter)
POST/api/chatAgentic chat — full tool-use loop, ~39 tools, Anthropic or OpenAI
POST/api/actionStructured action dispatch (used by MCP-Server, bypasses AI interpreter)
POST/api/batchExecute an ordered list of actions in one request
POST/api/blackoutInstant kill-all — zeroes every channel on targeted fixtures
GET/api/statusMulti-service health JSON
GET/api/scenesList workspace scenes
GET/api/scenes/<id>Describe a saved scene (per-fixture channel breakdown)
POST/api/scenes/<id>/activateApply a scene live
POST/api/scenes/saveSave scene XML to workspace permanently
POST/api/scenes/snapshotCapture current state as a new scene
POST/api/scenes/<id>/duplicateClone a saved scene under a new name
PATCH/api/scenes/<id>Rename a scene (and/or move its Path folder)
DELETE/api/scenes/<id>Delete a saved scene
GET/api/fixturesList fixtures with channel_info
GET/api/fixture_channels/<id>Per-fixture channel breakdown
POST/api/fixtures/<id>/identifyFlash a fixture so you can locate it physically
GET/api/groupsList fixture groups
POST/api/groupsCreate a new group
PATCH/api/groups/<name>Rename / re-describe / replace fixture list
DELETE/api/groups/<name>Delete a group
POST/api/groups/<name>/fixturesAppend fixtures to a group
DELETE/api/groups/<name>/fixturesRemove fixtures from a group
POST/api/channelSet a single channel value
GET/api/channel_valuesLive DMX values from QLC+
POST/api/diagnostics/test_dmxR → G → B → restore sweep across fixtures
GET/api/diagnostics/logs/<service>Tail systemd journal for an allowlisted service
GET/api/diagnostics/systemPi-level health JSON
GET/api/chasesList chases in the workspace
GET/api/chases/<id>Describe a chase (steps, scene names, timing)
POST/api/chasesCreate a new chase
DELETE/api/chases/<id>Remove a chase
POST/api/chases/<id>/startStart chase playback
POST/api/chases/<id>/stopStop chase playback
GET/api/cue_listsList saved cue lists (with runtime status)
GET/api/cue_lists/activeCurrently-playing cue lists, with elapsed time
GET/api/cue_lists/<id>Describe a cue list
POST/api/cue_listsCreate a new cue list
PATCH/api/cue_lists/<id>Rename / re-describe / replace cues array
DELETE/api/cue_lists/<id>Remove (stops playback first if running)
POST/api/cue_lists/<id>/goGO — start playback
POST/api/cue_lists/<id>/stopHalt running cue list

Saving Scenes

The save feature lets you persist any AI-generated scene (or the current live state) into the QLC+ workspace permanently:

bash
1# Via API
2curl -X POST http://lights.local:5000/api/scenes/save \
3  -H "Content-Type: application/json" \
4  -d '{"name": "My Scene", "snapshot": true}'
5
6# Or with scene XML from a previous generate
7curl -X POST http://lights.local:5000/api/scenes/save \
8  -H "Content-Type: application/json" \
9  -d '{"name": "Warm Sunset", "scene_xml": "<Function Type=\"Scene\" ...>...</Function>"}'

Saved scenes appear in the Scenes tab immediately and persist through reboots.

Fixture Definitions

The server reads QLC+ .qxf fixture definition files to resolve each channel's semantic role. This means:

  • Channel sliders show correct labels (e.g. "Warm White" not "Ch 2")
  • The AI knows which channels are color vs strobe vs configuration
  • Color commands drive the right channels per fixture type

The parser checks /usr/share/qlcplus/fixtures/ (system) and ~/.qlcplus/fixtures/ (user overrides).

LLM Agent Access

To expose the same control surface to MCP-capable LLM agents (Claude Desktop, ChatGPT, Cursor, custom clients), install the sibling MCP server — see MCP-Server. It runs at port 5001 and calls back into this server's /api/action endpoint.

Management Commands

bash
1./lightsctl.sh control-status    # Service status
2./lightsctl.sh control-logs      # Recent logs
3./lightsctl.sh control-restart   # Restart the server
4./lightsctl.sh control-uninstall # Remove the server
5./lightsctl.sh env-sync          # Push .env to Pi and restart

Was this page helpful?