Skip to content

gfargo/lights-pi

Repository files navigation

Raspberry Pi Studio Lighting Controller

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 port 5001.

Core Stack: QLC+ • ENTTEC DMX USB Pro • Raspberry Pi OS • Flask Control Server • MCP Server

License: MIT


📋 Table of Contents

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.


🚀 Quick Start

1. Prepare SD Card

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_HOSTNAME in .env for mDNS to work
  • Use 64-bit OS for Pi 4, 32-bit OS for Pi 3 (better performance on older hardware)

2. Configure Environment

cp .env.example .env
# Edit .env with your settings

3. Provision Pi

# 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

4. Set Up HTTPS (Optional but Recommended)

# 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

5. Access Web UI

./lightsctl.sh open-web
# Opens http://lights.local:9999 (or https://lights.local/qlc/ if SSL configured)

🏗️ System Architecture

   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)

🔧 Hardware Requirements

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 Reference

Core Commands

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 service

What 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.service with headless Qt
  • Adds Pi user to dialout group 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 ufw and 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 devices

WiFi 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-install

WiFi 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:

  1. 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/ and https://lights.local/qlc/
  2. 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
  3. 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 title
  • LANDING_STUDIO_NAME - Studio name displayed
  • LANDING_SUBTITLE - Subtitle text
  • LANDING_BUTTON_TEXT - Button text
  • LANDING_FOOTER_TEXT - Footer text
  • QLC_URL - Button destination (e.g., https://lights.local/qlc/ or http://lights.local:9999/)

Makefile Shortcuts

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

💼 Workflow Examples

Initial Setup

# 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

Daily Operations

# 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

Troubleshooting

# 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

Updating the Control Server — Common Pitfalls

⚠️ ./lightsctl.sh update does NOT deploy lights-pi code. It only runs sudo apt update && apt upgrade on the Pi. To push your latest control-server/ changes from the workstation, use:

bash scripts/deploy.sh

That rsyncs control-server/, scripts/, and lightsctl.sh to the Pi and restarts lighting-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-install is not part of initial provisioning. Until you run it, Pi-health will report lighting-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 .env on the workstation expecting it to deploy. deploy.sh excludes .env so production secrets on the Pi aren't clobbered. Edit it on the Pi directly (./lightsctl.sh ssh, then nano ~/control-server/.env).

⚙️ Configuration

Environment Variables

Create .env from .env.example:

cp .env.example .env
View 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, not HOSTNAME — the latter is a macOS shell builtin that will silently override your value.

Static IP Configuration

./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.1

HTTPS Setup

Recommended: One-command setup with mkcert

./lightsctl.sh setup-ssl

This will:

  1. Install mkcert (via Homebrew) if not already installed
  2. Install the mkcert local CA (makes your system trust the certs)
  3. Generate a locally-trusted certificate for lights.local
  4. Upload the certificate to your Pi
  5. Configure nginx with SSL and reverse proxy to QLC+

After setup, access your lighting controller securely:

  • https://lights.local/ → Landing page
  • https://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 SSL

Simple option: stunnel

./lightsctl.sh gen-cert        # Generate certificate
./lightsctl.sh ssl-proxy       # Install stunnel

Landing Page Button URL

Control 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-deploy

🔌 Hardware Setup

Connecting ENTTEC DMX USB Pro

Plug 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 green

After 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.


🎛️ Fixture & Workspace Management

Managing Fixtures

# List installed fixture definitions
./lightsctl.sh list-fixtures

# Install a custom fixture definition
./lightsctl.sh install-fixture path/to/custom-fixture.qxf

Managing Workspaces

Access 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

Backups

# 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.


📁 Project Structure

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)

🔧 Troubleshooting

Quick Diagnostics

./lightsctl.sh doctor      # Full health check with recommendations
./lightsctl.sh validate    # Pre-flight validation
./lightsctl.sh diagnose    # Detailed diagnostic dump
ENTTEC not detected
./lightsctl.sh test-dmx    # Comprehensive DMX hardware check
./lightsctl.sh lsusb       # Verify USB device detection
./lightsctl.sh health      # Check overall system status

If 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 dialout

A 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 only

If 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 tests
QLC+ 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-web

The --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-nginx

If 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 logs

Verify certificate files:

./lightsctl.sh ssh
ls -la /etc/ssl/qlc/
openssl x509 -in /etc/ssl/qlc/qlc.crt -text -noout  # View cert details
Reverse 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 errors

Verify 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 status
Web 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 restart

The 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 host

Ensure 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 diagnostics

Common fixes:

  1. Install the WiFi watchdog for automatic recovery:
    ./lightsctl.sh wifi-watchdog-install
  2. 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
  3. 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
  4. SSH "Too many authentication failures" — if your SSH agent has many keys loaded, add IdentitiesOnly yes to ~/.ssh/config:
    Host lights.local
      User pi
      IdentityFile ~/.ssh/id_rsa
      IdentitiesOnly yes
    

Check watchdog logs:

./lightsctl.sh wifi-watchdog-logs
Lights 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 update
Network 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 dhcpcd5

🚀 Future Enhancements

See 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.


🤖 AI Scene Generation (Beta)

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.xml

Supported 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-workspace

Scene 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.xml

Available templates:

  • youtube-studio - Bright neutral white for video recording
  • party - Vibrant alternating colors with fast transitions
  • ambient - Soft warm glow at low intensity
  • spotlight - Single fixture at full, others off
  • work-light - Bright neutral white for task lighting
  • warm-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 variations

See docs/AI_SCENE_GENERATION.md for complete documentation.


🆕 Recent Releases

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.js service 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.yml runs pytest on Python 3.11 + 3.12, node --check on inline JS, and an HTML tag-balance check on the single-page template.

💬 Agentic Chat (v2.11+)

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_fixturescreate_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.


🎙️ Natural Language Control (Beta)

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:5000

Features:

  • 🎤 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.


🤖 MCP Server (LLM Agent Access)

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 transport

Architecture. 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 rule

See docs/MCP_SERVER.md and mcp-server/README.md for the full tool/resource reference and auth hooks.


🎯 Fixture Groups/Zones

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" --preview

Features:

  • 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 --deploy

Groups are bidirectionally synced with QLC+ workspace, making them visible in QLC+'s UI and usable in Virtual Console!


🧪 Tests

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 -v

CI 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.


📄 License

MIT License - see LICENSE for details

About

Headless Raspberry Pi DMX lighting controller with AI-powered natural language scene generation. QLC+ • ENTTEC DMX USB Pro • Flask control server with persistent WebSocket bridge.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors