Headless Raspberry Pi lighting controller for studio environments. Control DMX fixtures from any device on your network via QLC+'s web interface, the custom Virtual Console at port
5000, natural-language voice/chat commands powered by OpenAI / Anthropic / Ollama, or any MCP-compatible LLM agent over the Streamable HTTP MCP server at port5001.
Core Stack: QLC+ • ENTTEC DMX USB Pro • Raspberry Pi OS • Flask Control Server • MCP Server
- Quick Start
- System Architecture
- Hardware Requirements
- Command Reference
- Workflow Examples
- Configuration
- Natural Language Control
- MCP Server (LLM Agent Access)
- Fixture Groups / Zones
- Project Structure
- Troubleshooting
For the deeper internals of the natural-language control layer and the single-WebSocket QLC+ bridge, see docs/CONTROL_SERVER_ARCHITECTURE.md. For the MCP server (LLM agent access), see docs/MCP_SERVER.md.
Download Raspberry Pi Imager and configure:
| Setting | Value |
|---|---|
| OS | Raspberry Pi OS Lite (64-bit for Pi 4, 32-bit for Pi 3) |
| Hostname | lights |
| Enable SSH | Yes |
| Username | pi |
| WiFi | Your network |
⚠️ Important:
- Hostname must match
PI_HOSTNAMEin.envfor mDNS to work- Use 64-bit OS for Pi 4, 32-bit OS for Pi 3 (better performance on older hardware)
cp .env.example .env
# Edit .env with your settings# Full setup (recommended for new Pi)
# The script will prompt for Pi model (3 or 4) and apply appropriate optimizations
WIFI1_SSID="SetupNet" WIFI1_PSK="setup-pass" \
WIFI2_SSID="StudioNet" WIFI2_PSK="studio-pass" \
./lightsctl.sh setup-full
# Or specify Pi model explicitly to skip prompt
PI_MODEL=3 WIFI1_SSID="SetupNet" WIFI1_PSK="setup-pass" \
WIFI2_SSID="StudioNet" WIFI2_PSK="studio-pass" \
./lightsctl.sh setup-full
# Verify installation
./lightsctl.sh doctor
./lightsctl.sh test-dmx# One-command SSL setup with locally-trusted certificates
./lightsctl.sh setup-ssl
# Access via HTTPS with no browser warnings
# https://lights.local/ → Landing page
# https://lights.local/qlc/ → QLC+ interface./lightsctl.sh open-web
# Opens http://lights.local:9999 (or https://lights.local/qlc/ if SSL configured) Browser / phone / voice MCP-aware LLM agents ┌─────────── AI providers ──────────┐
│ (Claude Desktop, etc.) │ OpenAI • Anthropic • Ollama (local) │
│ 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 │
│ • /api/action structured dispatch (for MCP) │
└──────────┬────────────────────────────────────┘
│ persistent WebSocket
▼
┌──────────────────────────┐
│ QLC+ headless (port 9999)│
│ + .qxf fixture defs │
└──────────┬───────────────┘
│ USB
▼
ENTTEC DMX USB Pro
│ DMX
▼
DMX Fixtures (rig)
Software Stack
| Software | Purpose |
|---|---|
| Raspberry Pi OS (Bookworm) | Lightweight OS |
| QLC+ 4.14.x | Lighting control engine + DMX output |
lighting-control (Flask + Flask-SocketIO) |
Custom control server (port 5000): AI chat, live UI, group/fixture API, .qxf-derived channel labels |
lighting-mcp (FastMCP + httpx) |
MCP server (port 5001): Streamable HTTP endpoint exposing lights as tools/resources to LLM agents — see MCP_SERVER.md |
websockets (Python) |
Single persistent QLC+ WebSocket on a dedicated asyncio loop — see CONTROL_SERVER_ARCHITECTURE.md |
| Avahi | mDNS (lights.local) |
| nginx | Landing page + SSL reverse proxy |
| stunnel4 (optional) | SSL/TLS termination alternative |
| ufw | Firewall |
| systemd | Service management (qlcplus-web.service, lighting-control.service, lighting-mcp.service) |
| Device | Purpose |
|---|---|
| Raspberry Pi 3B+ or 4 | Lighting controller host |
| MicroSD Card (16–32 GB) | OS and configuration |
| ENTTEC DMX USB Pro | USB → DMX interface |
| DMX fixtures | Any QLC+-compatible fixture |
| Wireless DMX system (optional) | Cable-free fixture control |
| DMX cables (110 Ω) | Daisy-chaining wired fixtures |
Note: Pi 3 is supported with automatic performance optimizations. Pi 4 is recommended for larger setups with many fixtures.
| Command | Description |
|---|---|
validate |
Pre-flight validation of config and connectivity |
doctor |
Comprehensive health check with recommendations |
health |
Quick status check (service, web UI, USB, resources) |
test-dmx |
Verify ENTTEC USB and DMX output capability |
backup |
Pull QLC+ config to local storage |
restore <file> |
Restore QLC+ config from backup |
📦 Provisioning Commands
setup-full # Full provisioning: setup then harden (recommended)
setup # Base install (requires WIFI1_SSID/PSK, WIFI2_SSID/PSK)
harden # Firewall, watchdog, unattended upgrades, udev rule
add-key [pubkey] # Install local SSH public key on the Pi
disable-password-auth # Disable SSH password login (run add-key first)
static-ip <ip/prefix> <gw> # Write static IP to /etc/dhcpcd.conf and restart
update # apt update && apt upgrade on the Pi
update-qlc # Upgrade only the qlcplus package and restart serviceWhat setup does:
- Detects or prompts for Pi model (3 or 4)
- Sets hostname and installs required packages
- Configures dual WiFi (studio network takes priority)
- Installs QLC+ with automatic retry on network hiccups
- Creates and enables
qlcplus-web.servicewith headless Qt - Adds Pi user to
dialoutgroup for ENTTEC USB access - Configures persistent systemd journal logs
- Pi 3 only: Applies performance optimizations (see below)
Pi 3 Performance Optimizations: When Pi 3 is detected or selected, the following optimizations are automatically applied:
- Reduces GPU memory to 16MB (more RAM for QLC+)
- Disables Bluetooth (saves CPU and memory)
- Disables HDMI output (saves power)
- Increases swap to 512MB (better for low memory)
- Limits journal size to 50MB (reduces SD card wear)
- Sets CPU governor to performance mode (better for real-time lighting)
- Disables unnecessary services
Note: A reboot is recommended after setup to apply all Pi 3 optimizations.
What harden does:
- Installs
ufwand opens only SSH (22) and QLC+ web port - Configures unattended security upgrades
- Enables hardware watchdog (auto-reboots on kernel hang)
- Creates udev rule so ENTTEC always appears at
/dev/dmx0
🔍 Service Management Commands
status # systemd status for qlcplus-web.service
restart # Restart qlcplus-web.service
logs # Last 80 lines from the service journal
logs-errors # Show only ERROR and WARN lines from logs
tail # Follow service logs live
health # Service + web UI + USB + disk + memory + CPU temp
diagnose # Full diagnostic dump (health + logs + wifi + uptime)
check # Ping + SSH pre-flight connectivity check
validate # Pre-flight validation (config, connectivity, dependencies)
doctor # Comprehensive health check with recommendations
perf [seconds] # Monitor CPU, memory, network usage over time (default: 10s)
benchmark # Test system performance (web UI latency, network speed)💡 QLC+ Commands
qlc-version # Run qlcplus --version on the Pi
qlc-headless # Push Qt platform fix (sets QT_QPA_PLATFORM=minimal)
deploy-workspace <file.qxw> # Upload workspace to Pi and restart service
pull-workspace [output.qxw] # Download current workspace from Pi
list-fixtures # Show installed fixture definitions
install-fixture <file.qxf> # Upload and install custom fixture definition
test-dmx # Verify ENTTEC USB and DMX output capability
open-web # Open the web UI in the default browser🌐 Network / WiFi Commands
wifi # Dump /etc/wpa_supplicant/wpa_supplicant.conf
wifi-list # List all configured and available WiFi networks
wifi-add-network <ssid> <pass> [priority] # Add a new WiFi network
wifi-connect <ssid> # Connect to a specific WiFi network
wifi-test # End-to-end connectivity test (IP, gateway, DNS, internet)
wifi-reconf # Run wpa_cli -i wlan0 reconfigure
wifi-status # Show current SSID and wlan0 address
wifi-diagnose # Comprehensive WiFi diagnostics
wifi-reconnect # Force disconnect and reconnect to best network
wifi-edit # Edit the Wi-Fi config in $EDITOR
wifi-watchdog-install # Install auto-recovery watchdog (checks every 2 min)
wifi-watchdog-status # Show watchdog timer status
wifi-watchdog-logs # Show watchdog log history
wifi-watchdog-uninstall # Remove the watchdog
scan [--deep] # Scan network for Raspberry Pi devicesWiFi Watchdog:
The WiFi watchdog is a systemd timer that runs every 2 minutes on the Pi. It checks if wlan0 has an IP and can reach the default gateway. If connectivity is lost, it automatically reconnects. After 3 consecutive failures, it restarts NetworkManager entirely. Install it once and forget about it:
./lightsctl.sh wifi-watchdog-installWiFi Connectivity Test: Quick end-to-end test that verifies interface status, IP assignment, SSID connection, signal strength, gateway reachability, DNS resolution, and internet access:
./lightsctl.sh wifi-test🖥️ System Commands
backup # Pull QLC+ config dirs to BACKUP_STORAGE
restore <backup.tar.gz> # Restore QLC+ config from backup and restart service
lsusb # Show USB devices (ENTTEC should appear)
os-version # Show Raspberry Pi OS and kernel version
hdmi-disable # Disable HDMI output to save power
reboot # Reboot the Pi
poweroff # Shut down the Pi
ssh # Open an interactive shell on the Pi
edit <path> # Edit an arbitrary file on the Pi🔒 TLS/SSL Commands
setup-ssl # Complete SSL setup: mkcert cert + nginx config (recommended!)
gen-cert [days] # Generate self-signed cert/key in certs/ (default: 730 days)
gen-cert-mkcert # Generate locally-trusted cert using mkcert (no browser warnings)
ssl-nginx [cert] [key] # Configure nginx with SSL + reverse proxy to QLC+
ssl-proxy [cert] [key] # Install stunnel, redirect 443 → QLC_PORT (simpler alternative)SSL Setup Options:
-
Recommended: One-command setup with mkcert
./lightsctl.sh setup-ssl
- Installs mkcert (via Homebrew) if needed
- Generates locally-trusted certificate
- Configures nginx with SSL + reverse proxy
- No browser warnings!
- Access:
https://lights.local/andhttps://lights.local/qlc/
-
Manual: Self-signed certificate
./lightsctl.sh gen-cert 365 ./lightsctl.sh ssl-nginx
- Creates self-signed certificate
- Browser will show security warning (expected)
- Still encrypted, just not trusted by default
-
Alternative: stunnel (simpler)
./lightsctl.sh gen-cert ./lightsctl.sh ssl-proxy
- Uses stunnel instead of nginx
- Less flexible but easier setup
- Landing page only (QLC+ stays on port 9999)
🌐 Landing Page Commands
landing-setup # Install nginx and deploy the landing page (first time)
landing-deploy # Push updated landing/index.html (no nginx reinstall)Serves a simple branded page at http://lights.local (or https://lights.local if SSL configured) with a button linking to the QLC+ web UI.
Customization:
Set these variables in .env to customize the landing page:
LANDING_TITLE- Browser titleLANDING_STUDIO_NAME- Studio name displayedLANDING_SUBTITLE- Subtitle textLANDING_BUTTON_TEXT- Button textLANDING_FOOTER_TEXT- Footer textQLC_URL- Button destination (e.g.,https://lights.local/qlc/orhttp://lights.local:9999/)
All commands have make shortcuts for convenience:
View Makefile Examples
# Provisioning
make setup-full
make setup
make harden
make add-key
make static-ip IP=192.168.1.50/24 GW=192.168.1.1
# Service Management
make status
make restart
make logs
make logs-errors
make health
make diagnose
make validate
make doctor
make perf DURATION=30
make benchmark
# QLC+
make deploy WS=workspaces/studio.qxw
make set-default WS=workspaces/studio.qxw
make pull OUTPUT=custom.qxw
make list-fixtures
make install-fixture FIXTURE=path/to/fixture.qxf
make test-dmx
make open
# Network
make wifi-status
make scan
# System
make backup
make restore BACKUP=backups/qlcplus-backup-20260305T203838Z.tar.gz
make os-version
make reboot
# Landing Page
make landing-setup
make landing-deploy# 1. Validate your local environment
./lightsctl.sh validate
# 2. Run full provisioning
WIFI1_SSID="SetupNet" WIFI1_PSK="setup-pass" \
WIFI2_SSID="StudioNet" WIFI2_PSK="studio-pass" \
./lightsctl.sh setup-full
# 3. Set up HTTPS (recommended)
./lightsctl.sh setup-ssl
# 4. Deploy landing page
./lightsctl.sh landing-setup
# 5. Verify everything is working
./lightsctl.sh doctor
./lightsctl.sh test-dmx# Check system health
./lightsctl.sh health
# View recent errors
./lightsctl.sh logs-errors
# Deploy a workspace
./lightsctl.sh deploy-workspace workspaces/studio.qxw
# Pull workspace after making changes in web UI
./lightsctl.sh pull-workspace workspaces/studio-updated.qxw
# Create backup
./lightsctl.sh backup# Run comprehensive diagnostics
./lightsctl.sh doctor
# Monitor performance in real-time
./lightsctl.sh perf 30
# Test network and system performance
./lightsctl.sh benchmark
# Find Pi on network if hostname not resolving
./lightsctl.sh scan
# Check DMX hardware and configuration
./lightsctl.sh test-dmx
⚠️ ./lightsctl.sh updatedoes NOT deploy lights-pi code. It only runssudo apt update && apt upgradeon the Pi. To push your latestcontrol-server/changes from the workstation, use:bash scripts/deploy.shThat rsyncs
control-server/,scripts/, andlightsctl.shto the Pi and restartslighting-control.service. Without it, the Pi keeps running whatever code was last deployed.
A few related gotchas worth knowing:
- MCP server installs separately.
./lightsctl.sh mcp-installis not part of initial provisioning. Until you run it, Pi-health will reportlighting-mcp: not_installed(post-v2.13.1). See docs/MCP_SERVER.md. - Hard-refresh the browser after deploy. ⌘⇧R (macOS) or Ctrl⇧R (Win/Linux) to bust the cached CSS/JS — otherwise you'll keep seeing the old UI even after the service restarts.
- Don't edit
.envon the workstation expecting it to deploy.deploy.shexcludes.envso production secrets on the Pi aren't clobbered. Edit it on the Pi directly (./lightsctl.sh ssh, thennano ~/control-server/.env).
Create .env from .env.example:
cp .env.example .envView Configuration Options
| Variable | Default | Description |
|---|---|---|
PI_HOST |
lights.local |
Pi hostname or IP |
PI_USER |
pi |
SSH username (customize per rig) |
PI_HOSTNAME |
lights |
mDNS hostname set on the Pi |
QLC_PORT |
9999 |
QLC+ web UI port |
SSH_KEY |
(none) | Path to SSH private key |
BACKUP_STORAGE |
./backups |
Local backup destination |
SSL_CERT |
certs/qlc.crt |
TLS certificate for ssl-proxy |
SSL_KEY |
certs/qlc.key |
TLS private key for ssl-proxy |
QLC_URL |
http://lights.local:9999/ |
Landing page button destination |
LANDING_TITLE |
Lighting Controller |
Landing page browser title |
LANDING_STUDIO_NAME |
Your Studio |
Studio name displayed on landing page |
LANDING_SUBTITLE |
Lighting Controller |
Subtitle text on landing page |
LANDING_BUTTON_TEXT |
Lighting Control |
Button text on landing page |
LANDING_FOOTER_TEXT |
lights.local |
Footer text on landing page |
AI_PROVIDER |
openai |
AI backend: openai, anthropic, or ollama |
AI_API_KEY |
(none) | API key (not needed for ollama) |
AI_MODEL |
gpt-4.1 |
Model name for the chosen provider |
AI_SCENE_STYLE |
complete |
Default scene style |
AI_SCENE_VARIATIONS |
1 |
Default number of variations |
Note: Use
PI_HOSTNAME, notHOSTNAME— the latter is a macOS shell builtin that will silently override your value.
./lightsctl.sh static-ip 192.168.1.50/24 192.168.1.1
# or: make static-ip IP=192.168.1.50/24 GW=192.168.1.1Recommended: One-command setup with mkcert
./lightsctl.sh setup-sslThis will:
- Install mkcert (via Homebrew) if not already installed
- Install the mkcert local CA (makes your system trust the certs)
- Generate a locally-trusted certificate for
lights.local - Upload the certificate to your Pi
- Configure nginx with SSL and reverse proxy to QLC+
After setup, access your lighting controller securely:
https://lights.local/→ Landing pagehttps://lights.local/qlc/→ QLC+ web interface
No browser warnings - the certificate is fully trusted!
Alternative: Self-signed certificate
./lightsctl.sh gen-cert # Generate self-signed certificate
./lightsctl.sh ssl-nginx # Configure nginx with SSLSimple option: stunnel
./lightsctl.sh gen-cert # Generate certificate
./lightsctl.sh ssl-proxy # Install stunnelControl where the landing page button links by setting QLC_URL in .env:
# For SSL reverse proxy (after running setup-ssl)
QLC_URL=https://lights.local/qlc/
# For direct port access
QLC_URL=http://lights.local:9999/Then redeploy the landing page:
./lightsctl.sh landing-deployPlug into the Pi, then verify detection:
./lightsctl.sh test-dmx # Comprehensive DMX hardware check
./lightsctl.sh lsusb # Expect: FTDI DMX USB PRO
./lightsctl.sh health # Confirms service + web UI + USB all greenAfter harden is run, the device gets a stable symlink at /dev/dmx0 via udev, so QLC+ always finds it regardless of which USB port it's in.
# List installed fixture definitions
./lightsctl.sh list-fixtures
# Install a custom fixture definition
./lightsctl.sh install-fixture path/to/custom-fixture.qxfAccess the QLC+ designer at http://lights.local:9999 to configure fixtures, DMX addresses, and workspace layout.
# Deploy workspace to Pi
./lightsctl.sh deploy-workspace workspaces/studio.qxw
# Pull current workspace from Pi (after making changes in web UI)
./lightsctl.sh pull-workspace workspaces/studio-updated.qxw# Create backup
./lightsctl.sh backup
# Restore from backup
./lightsctl.sh restore backups/qlcplus-backup-20260305T203838Z.tar.gz
# List available backups
ls -lh backups/Backups include .config/qlcplus and .qlcplus directories from the Pi.
lights-pi/
├── lightsctl.sh # Main CLI interface (all Pi management commands)
├── Makefile # Convenience shortcuts
├── .env # Local configuration (gitignored)
├── control-server/ # Flask control server (deployed to Pi at port 5000)
│ ├── app.py # Routes, persistent QLC+ WebSocket, AI dispatch
│ ├── fixture_definitions.py # .qxf parser → authoritative channel roles
│ ├── templates/index.html # Live control UI (AI chat, virtual console, groups)
│ └── requirements.txt # Python dependencies
├── scripts/
│ ├── lib/ # Utility libraries (sourced by lightsctl.sh)
│ │ ├── ai_scene.sh # AI scene-generation prompts and API calls
│ │ ├── ai_scene_mock.sh # Mock generator for offline testing
│ │ ├── backup.sh # Backup/restore and system updates
│ │ ├── extract_fixtures.py # Enriched fixture JSON for AI prompts (.qxf-aware)
│ │ ├── fixture_groups.sh # Group CRUD + group-scoped scene generation
│ │ ├── network.sh # Network scanning and Pi discovery
│ │ ├── qlc.sh # QLC+ operations (workspace, fixtures, DMX)
│ │ ├── scene_templates.sh # Pre-defined template generation
│ │ ├── system.sh # System monitoring and diagnostics
│ │ ├── tls.sh # Certificate generation and SSL proxy
│ │ ├── wifi.sh # WiFi configuration management
│ │ └── workspace.sh # Inject/extract scenes from .qxw workspaces
│ ├── provisioning/ # One-time setup scripts (run on Pi)
│ │ ├── setup.sh # Base installation
│ │ └── harden.sh # Security hardening
│ ├── services/ # Service deployment scripts
│ │ ├── control_server.sh # Control server installer
│ │ ├── landing.sh # Landing page setup
│ │ └── wifi-watchdog.sh # WiFi auto-recovery watchdog
│ └── debug/ # One-off diagnostic scripts
├── landing/ # Landing page HTML (deployed to nginx)
├── scenes/examples/ # Reference AI-generated scene XML
├── docs/ # Architecture and feature documentation
├── backups/ # QLC+ configuration backups (gitignored)
└── certs/ # TLS certificates (gitignored)
./lightsctl.sh doctor # Full health check with recommendations
./lightsctl.sh validate # Pre-flight validation
./lightsctl.sh diagnose # Detailed diagnostic dumpENTTEC not detected
./lightsctl.sh test-dmx # Comprehensive DMX hardware check
./lightsctl.sh lsusb # Verify USB device detection
./lightsctl.sh health # Check overall system statusIf harden has been run, the device should appear at /dev/dmx0 — replug it to trigger the udev rule. If the Pi user can't access the device, confirm they are in the dialout group:
./lightsctl.sh ssh
groups $USER # should include dialoutA logout/login (or reboot) is required for group changes to take effect.
QLC+ service fails to start
./lightsctl.sh logs # View recent logs
./lightsctl.sh logs-errors # Filter for errors onlyIf logs show Qt platform errors, run ./lightsctl.sh qlc-headless to apply the QT_QPA_PLATFORM=minimal drop-in. The service has a crash loop guard (StartLimitBurst=5 in 60 s) — if it keeps restarting, check logs for the root cause before it stops trying.
Performance Issues
Monitor system performance:
./lightsctl.sh perf 30 # Real-time monitoring for 30 seconds
./lightsctl.sh benchmark # Run performance testsQLC+ web interface hangs or doesn't respond
If the web interface connects but never loads (spinning wheel), the service may have the --operate flag enabled which causes it to hang:
./lightsctl.sh ssh
sudo sed -i 's/--operate//' /etc/systemd/system/qlcplus-web.service
sudo systemctl daemon-reload
sudo systemctl restart qlcplus-webThe --operate flag puts QLC+ into operate mode immediately, which can make the web interface unresponsive. Remove it to allow normal web UI access.
HTTPS not working or certificate warnings
If using mkcert:
# Reinstall the local CA
mkcert -install
# Regenerate certificates
rm certs/qlc.crt certs/qlc.key
./lightsctl.sh gen-cert-mkcert
./lightsctl.sh ssl-nginxIf using self-signed certificates: Browser warnings are expected. Click "Advanced" and "Proceed to lights.local" to accept the certificate.
Check nginx configuration:
./lightsctl.sh ssh
sudo nginx -t # Test config syntax
sudo systemctl status nginx # Check if nginx is running
sudo tail -f /var/log/nginx/error.log # View error logsVerify certificate files:
./lightsctl.sh ssh
ls -la /etc/ssl/qlc/
openssl x509 -in /etc/ssl/qlc/qlc.crt -text -noout # View cert detailsReverse proxy /qlc/ path not working
Check nginx configuration and logs:
./lightsctl.sh ssh
cat /etc/nginx/sites-available/lights # View config
sudo nginx -t # Test config
sudo systemctl reload nginx # Reload config
sudo tail -f /var/log/nginx/error.log # Watch for errorsVerify QLC+ is running and accessible locally:
./lightsctl.sh ssh
curl -I http://127.0.0.1:9999/ # Should connect
sudo systemctl status qlcplus-web # Check service statusWeb UI connects but browser hangs
If the port is open (TCP connects) but the browser never loads a page, the QLC+ web server thread has stalled:
./lightsctl.sh restartThe service recovers cleanly on restart. If it keeps stalling, check ./lightsctl.sh logs-errors for errors.
Can't find Pi on network
./lightsctl.sh scan # Scan for Pi devices on network
./lightsctl.sh check # Test connectivity to configured hostEnsure the Pi is powered on, connected to the same network, and that the hostname in .env matches what was set during SD card preparation.
WiFi keeps dropping or Pi unreachable at studio
Quick diagnosis:
./lightsctl.sh wifi-test # End-to-end connectivity check
./lightsctl.sh wifi-diagnose # Full WiFi diagnosticsCommon fixes:
- Install the WiFi watchdog for automatic recovery:
./lightsctl.sh wifi-watchdog-install
- Add all network band variants (2.4GHz and 5GHz):
./lightsctl.sh wifi-add-network "StudioNet-2G" "password" 30 ./lightsctl.sh wifi-add-network "StudioNet-5G" "password" 40
- Set connection priorities so preferred networks connect first:
# Higher number = higher priority ssh pi@lights.local sudo nmcli connection modify "MyNetwork" connection.autoconnect-priority 100
- SSH "Too many authentication failures" — if your SSH agent has many keys loaded, add
IdentitiesOnly yesto~/.ssh/config:Host lights.local User pi IdentityFile ~/.ssh/id_rsa IdentitiesOnly yes
Check watchdog logs:
./lightsctl.sh wifi-watchdog-logsLights not responding
- Confirm universe output is enabled in QLC+ under Inputs/Outputs
- Confirm ENTTEC is selected as the output plugin for the correct universe
- Verify fixture DMX addresses match their DIP switch or menu settings
- If using wireless DMX, confirm the transmitter is in Transmit mode and channels match
DNS fails during setup
pi_lights_setup.sh automatically waits up to 60 s for DNS to recover after WiFi reconfiguration and injects nameserver 1.1.1.1 if still failing. To fix manually on the Pi:
echo 'nameserver 1.1.1.1' | sudo tee -a /etc/resolv.conf
sudo apt-get updateNetwork interface commands not found
Raspberry Pi OS Lite does not include ifup/ifdown or the networking systemd unit. Use instead:
sudo ip link set wlan0 down && sudo ip link set wlan0 up
sudo systemctl restart dhcpcd5See docs/ROADMAP.md for the complete product roadmap.
Completed:
- ✅ Auto-load default workspace
- ✅ AI scene generation with complete/modular styles
- ✅ Natural language control server (port 5000)
- ✅ Fixture groups/zones with per-group scene targeting
- ✅
.qxf-aware fixture channel parsing (no more guessing RGB vs warm/cool) - ✅ Persistent WebSocket architecture (no CLOSE_WAIT leaks)
- ✅ WiFi watchdog for auto-recovery
Planned:
- Multi-device fleet management
- Studio ecosystem integration (camera, video, audio)
- Advanced AI features (scene evolution, video analysis)
- Professional show management tools
- Plugin system and API access
For detailed information on AI scene generation, see docs/AI_SCENE_GENERATION.md. For the control server architecture, see docs/CONTROL_SERVER_ARCHITECTURE.md. For WiFi reliability and troubleshooting, see docs/WIFI_RELIABILITY.md.
Generate QLC+ scenes from natural language descriptions using AI. The system understands your fixture inventory and creates appropriate DMX values to match your desired mood or effect.
# Generate a scene
./lightsctl.sh generate-scene "warm sunset ambiance" --preview
# Generate with specific style
./lightsctl.sh generate-scene "party mode" --style modular --add-to-workspace
# Generate multiple variations and choose the best one
./lightsctl.sh generate-scene "dramatic spotlight" --variations 3 --preview
# Save to file
./lightsctl.sh generate-scene "dramatic spotlight" --output scenes/dramatic.xmlSupported Styles:
- Complete: Self-contained, ready-to-use scenes
- Modular: Composable layers (color, intensity, position)
- Timeline: Time-based sequences with keyframes
- Reactive: Audio/sensor-responsive scenes
Scene Variations: Generate multiple variations of a scene and interactively select the best one:
# Generate 3 variations with interactive selection
./lightsctl.sh generate-scene "warm sunset" --variations 3
# Works with all options
./lightsctl.sh generate-scene "party lights" --variations 5 --style modular --add-to-workspaceScene Templates: Fast, predictable scenes from pre-defined templates (no AI required):
# List available templates
./lightsctl.sh list-templates
# Generate from template
./lightsctl.sh generate-from-template youtube-studio --preview
./lightsctl.sh generate-from-template party --add-to-workspace
./lightsctl.sh generate-from-template warm-white --output scenes/warm.xmlAvailable templates:
youtube-studio- Bright neutral white for video recordingparty- Vibrant alternating colors with fast transitionsambient- Soft warm glow at low intensityspotlight- Single fixture at full, others offwork-light- Bright neutral white for task lightingwarm-white- Warm white (2700K-3000K color temperature)cool-white- Cool white (5000K-6500K color temperature)
Configuration:
Add to your .env file:
AI_PROVIDER=anthropic # anthropic, openai, or ollama
AI_API_KEY=sk-ant-... # Your API key (not needed for ollama)
AI_MODEL=claude-3-5-sonnet-20241022
AI_SCENE_STYLE=complete # Default style
AI_SCENE_VARIATIONS=1 # Default number of variationsSee docs/AI_SCENE_GENERATION.md for complete documentation.
The web UI and platform layer have evolved quickly since the original v1.x roadmap. Below is a quick orientation to the most recent ground covered — see the full release notes on GitHub for everything else.
v2.13.2 — Tab URL persistence. Active tab now syncs to a ?tab=<name>
query param via history.replaceState, so tab links are bookmarkable and the
back button stays clean.
v2.13.1 — Boot init + diagnostics fix. Restores loadScenes(),
checkStatus(), and the 15-second status poll on page load (regression from
v2.13.0). Pi-health now distinguishes not_installed from inactive via a
new _systemd_unit_state helper that inspects LoadState before ActiveState.
v2.13.0 — Visual identity pass. Geist Sans + Mono via Google Fonts; a
:root CSS token system (--ink, --paper, --rule, --amber-tungsten,
--signal-*); amber-tungsten replaces white for active/selected states; a
filament dot in the header; animated tab indicator; eyebrow labels on tool
panels; redesigned BLACKOUT button with theatrical hover; hairlines instead
of literal greys.
v2.12.0 — Mobile audit, installable PWA, tests + CI.
- Mobile redesign: 44 px touch targets, 16 px form inputs (prevents iOS zoom), safe-area insets, horizontal-scroll tabs, full-screen modals on phones.
- Installable PWA:
/manifest.json,/icon.svg,/sw.jsservice worker, Apple-touch meta tags. iOS and Android "Add to Home Screen" gives a chromeless app shell. - Test suite + CI: ~195 pytest tests cover the pure helpers under
control-server/tests/..github/workflows/test.ymlruns pytest on Python 3.11 + 3.12,node --checkon inline JS, and an HTML tag-balance check on the single-page template.
The web UI's Chat tab is a full Claude/ChatGPT-style conversation where the LLM has access to ~39 of the MCP tools and can plan multi-step lighting operations autonomously:
"Set up three-point lighting with tungsten key, daylight fill, magenta back, and save it as 'photoshoot'."
The agent calls
list_groups(sees no groups yet) →list_fixtures→create_group(key-lights)→create_group(fill-lights)→create_group(back-lights)→palette({...})→snapshot_scene('photoshoot')→ reports back.
Implementation lives server-side in POST /api/chat — runs an Anthropic
tool_use or OpenAI function-calling loop, dispatches tools via the same
endpoints the MCP server uses. Stateless; the client owns the conversation
history (persisted in localStorage).
Set AI_PROVIDER=anthropic or openai in .env plus your AI_API_KEY.
Ollama tool-calling isn't supported yet — it varies too much by model.
Control your lights in real-time using natural language commands via a web interface.
# Install control server
./lightsctl.sh control-install
# Access at http://lights.local:5000Features:
- 🎤 Voice and text input
- 🤖 AI-powered command interpretation
- ⚡ Real-time lighting adjustments
- 📱 Works on any device
Example Commands:
- "Make it brighter"
- "Add more blue"
- "Party mode"
- "Warm sunset"
- "Fade to black over 5 seconds"
See control-server/README.md for complete documentation.
Expose the lighting rig as a Model Context Protocol endpoint so any MCP-capable agent — Claude Desktop, ChatGPT, Cursor, custom clients — can discover fixtures, scenes, and groups and issue commands without hand-rolling REST integrations.
# Install the MCP server (sibling systemd service, port 5001)
./lightsctl.sh mcp-install
# Endpoint
# http://lights.local:5001/mcp ← Streamable HTTP transportArchitecture. The MCP server is a thin FastMCP wrapper that calls the
control server's REST API over localhost — the Flask app remains the single
writer to QLC+, so the MCP process is stateless and crash-safe to restart.
LLM agent ──MCP/HTTP──▶ lighting-mcp.service ──HTTP──▶ lighting-control.service ──WS──▶ QLC+
:5001/mcp :5000 :9999
Tools exposed:
- Discovery:
get_status,list_fixtures,get_fixture_channels,list_groups,list_scenes,list_templates,get_channel_values - Actions:
activate_scene,apply_template,adjust_brightness,adjust_color,color_temperature,palette,strobe,fade,generate_scene,set_channel,save_scene,snapshot_scene,blackout,batch_action,identify_fixture - Group management:
create_group,delete_group,update_group,add_fixtures_to_group,remove_fixtures_from_group - Scene management:
describe_scene,delete_scene,rename_scene,duplicate_scene - Chase management:
list_chases,describe_chase,create_chase,delete_chase,start_chase,stop_chase - Cue lists (audio-synced shows):
list_cue_lists,describe_cue_list,get_active_cue_lists,create_cue_list,update_cue_list,delete_cue_list,go_cue_list,stop_cue_list - Diagnostics:
test_dmx,get_logs,get_system_info - Resource:
lights://workspace— one-shot snapshot of fixtures, groups, scenes, templates, and status for LLM context
Wire it up in Claude Desktop / Cursor:
{
"mcpServers": {
"qlc-lights": { "url": "http://lights.local:5001/mcp" }
}
}Commands:
./lightsctl.sh mcp-install # install + enable + start
./lightsctl.sh mcp-status # systemctl status
./lightsctl.sh mcp-logs # journalctl -u lighting-mcp.service
./lightsctl.sh mcp-restart # restart after config changes
./lightsctl.sh mcp-uninstall # remove service + firewall ruleSee docs/MCP_SERVER.md and mcp-server/README.md for the full tool/resource reference and auth hooks.
Organize fixtures into named groups for easier control and management.
# Create groups
./lightsctl.sh group-create "key-lights" "0,3" "Main key lights"
./lightsctl.sh group-create "fill-lights" "4,5" "Fill and background"
# List all groups
./lightsctl.sh group-list
# Apply template to specific group
./lightsctl.sh group-template "key-lights" warm-white --add-to-workspace
# Generate scene for specific group
./lightsctl.sh group-scene "fill-lights" "soft blue ambient" --previewFeatures:
- Name and organize fixtures into logical groups
- Control groups independently
- Apply templates to specific groups
- Generate AI scenes for specific groups only
- Persistent storage in
~/.qlcplus/fixture_groups.json
Common Use Cases:
- Three-point lighting: "key", "fill", "back"
- Zones: "stage-left", "stage-right", "audience"
- Function: "overhead", "floor", "accent"
- Color: "rgb-pars", "white-spots"
QLC+ Workspace Integration:
# Import groups from QLC+ workspace
./lightsctl.sh group-import
# Export groups back to QLC+ workspace
./lightsctl.sh group-export --deployGroups are bidirectionally synced with QLC+ workspace, making them visible in QLC+'s UI and usable in Virtual Console!
The control server ships a pytest suite for its pure-function helpers (time parsing, CCT-to-RGB, palette / cue / strobe normalization, run-order parsing). About 180 tests, runs in under a second.
cd control-server
pip install -r requirements-dev.txt
pytest -vCI runs the same suite on every PR via .github/workflows/test.yml —
matrix on Python 3.11 + 3.12, plus ast.parse on both servers, node --check
on the inline JS, and HTML tag-balance verification on the Flask template.
MIT License - see LICENSE for details