A big batch: the backend halves of four new control surfaces (MIDI, OSC, cue-list preview/waveform, stage-layout persistence), a round of bugs found in post-merge review of the tap-tempo feature, and an ops fix for a real production incident this release itself walked into.
🎛️ MIDI + OSC backends (PR #100, #99)
Two new inbound control paths, both scoped deliberately narrow: the listener/dispatch logic only, reusing the exact same lighting functions the web UI and MCP already call (set_channel_values, scene activation, chase start/stop, /blackout) — no new lighting logic, just new triggers into existing code.
- MIDI —
python-rtmidilistener thread with device auto-discovery and reconnect-on-hot-plug, CC/note → action mapping persisted tomidi_mappings.json,GET/POST /api/midi/mappings,GET /api/midi/devices,GET /api/midi/state. - OSC — UDP listener (default port 8000) routing
/scene/<name>,/chase/<name>,/fixture/<id>/<channel>,/master,/blackout,/cue/go|pause|stop, plus outbound/state/*feedback on changes.
Both degrade gracefully — the server starts fine with no MIDI hardware or python-osc/python-rtmidi installed, same pattern as the existing audio engine's available flag.
Deliberately not in this release: the "MIDI" web UI tab, drag-to-bind UX, TouchOSC layout file, and verification against real hardware — those need hands-on review with an actual controller/iPad, not something CI can sign off on. Tracked as follow-ups on the original feature issues.
🎚️ Cue-list preview/waveform + stage-layout persistence (PR #102, #101)
Two more backend-only slices, each scoped after checking what already existed:
- Cue lists —
POST /api/cue_lists/<id>/preview(apply whichever cue would be active at a given timestamp, without touching actual playback state — verified side-effect-free against a running cue list) andGET /api/cue_lists/<id>/waveform(peak/RMS data for an associated audio file, display-only). Re-timing a cue turned out to already work via the existingPATCHendpoint, so no new work was needed there. - Stage layout —
GET/POST /api/stage_layoutpersists fixture{x, y}positions tostage_layout.json. Group-creation-from-a-fixture-list already existed too (POST /api/groups), so this is purely position storage.
Same split as MIDI/OSC: the actual timeline/canvas UI is a visual/UX feature pytest can't judge and stays a human-reviewed follow-up.
📡 Wireless DMX transmitter channel, cross-referenced against a live scan (PR #89)
Follow-up to the RF Environment scan from the last release. QLC+ has no visibility into a wireless DMX transmitter's RF channel — that's a hardware-only setting on the transmitter's own display. This adds a small form where the operator reports what their transmitter (D-Fi Hub or similar) is set to, persisted server-side, and the RF scan now cross-references it against the live WiFi survey for a concrete suggestion ("Transmitter channel 9 is sitting near loud WiFi traffic") instead of just a generic quiet-window readout. Verified against Chauvet's actual published manuals rather than guessed.
🐛 Fixes
- Tempo API edge cases (PR #97) —
bpm: NaN/Infinitywas slipping past range validation and hitting a 500 instead of a 400; starting a tap chase with zero playable steps reported false success while silently killing any runner already in progress. Both fixed, plus a parametrized regression suite. - Deploy pipeline never installed new Python dependencies (PR #103) — the first deploy after this batch's
structlog/python-osc/python-rtmidiadditions landed crash-loopedlighting-control.serviceon the live Pi (ModuleNotFoundError: structlog).deploy.shsynced the code but never ranpip install. Fixed live via SSH, then fixed for real:deploy.shnow runspip install -r requirements.txtagainst the venv on every deploy (fast/idempotent when nothing changed) so this surfaces as ordinary install output instead of a silent crash loop next time.
✅ Tests
730 unit tests + 73 integration tests, all green (plus 3 backup-script tests that need bash 4+ and only run in CI's Linux environment, not locally on macOS's stock bash). Every PR in this batch was independently re-verified before merge — diffs read by hand, not just CI trusted — including a cross-PR check that caught a real breakage: an already-merged test called _mock_chase_run() with a signature another PR had changed, which git's merge never would have flagged since the two changes never touched the same lines.
Upgrade
From your Mac:
git pull origin main
bash scripts/deploy.shdeploy.sh now installs any new/changed Python dependencies automatically — no manual pip install step needed even though this release adds three new packages. Hard-refresh the browser (⌘⇧R / Ctrl⇧R) after deploying.
What's next
- PR #60 (multi-workspace switching) is still held, not part of this release — it collides with the boot-restore/last-look system (switching workspaces restarts QLC+, which the crash-restart-restore logic reacts to by replaying the previous workspace's last DMX snapshot onto the new one). Needs that fixed before it's safe to merge.
- The MIDI/OSC web UI tabs, TouchOSC layout, and the cue-list/stage-layout canvas editors are all queued as separate, human-reviewed follow-ups to the backend work in this release.
- Issue #47 (gunicorn/eventlet WSGI migration) and #25's remaining nice-to-haves are still open in the backlog.