- Work in the currently open local repository checkout and branch by default.
- Do not create or use separate worktrees or child worktree sessions unless the user explicitly asks for that.
- If orchestration is requested, prefer coordinating work in the active branch/session when possible; ask before spawning separate worktrees.
- Never add a
Co-authored-by: Copilottrailer to commits in this repository. - When working with DMX/light hardware or running tests for this project, the agent must execute the commands itself (start/stop processes, run test/sequencer scripts, check logs) rather than only handing the user manual instructions to run.
This repository uses two different package managers depending on the subtree:
dmx-bridge/(Node/TypeScript DMX bridge, sequencer, clock watcher): use pnpm for everything —pnpm install,pnpm typecheck,pnpm build,pnpm dev:*, etc. Do not runyarnornpminsidedmx-bridge/.- Repo root (Angular app /
osc-server): use yarn — e.g.yarn build. Do not runpnpmat the repo root.
Never mix the two in the same subtree (e.g. do not run yarn install inside dmx-bridge/, or pnpm install at the repo root) — it will produce a second, inconsistent lockfile/node_modules tree.
The active DMX bridge lives in dmx-bridge/. It controls the Enttec Open DMX USB interface through the native FTDI helper and receives OSC on 127.0.0.1:9000.
cd dmx-bridge
pnpm install
pnpm typecheck
pnpm build
cd native
make
./open-dmx-ftdi --list
cd ..Expected FTDI device:
manufacturer="FTDI" product="FT232R USB UART" serial="B0011U7V"
The native helper binary is generated at dmx-bridge/native/open-dmx-ftdi and is intentionally ignored by git.
When the user asks to start the DMX server, prefer the supervised TypeScript runtime rather than the bridge alone. The DMX supervisor starts the OSC-to-DMX bridge, the Ableton Link clock watcher, and the clocked sequencer, while reporting child PIDs, restart counts, ports, serial, and recent logs.
cd dmx-bridge
pnpm dev:dmx:supervisedThis starts:
- Ableton Link clock watcher:
pnpm dev:clock:link - OSC-to-DMX bridge watcher:
pnpm dev:bridge:open-ftdi - Clocked HSI/Mode 6 sequencer watcher:
pnpm dev:seq:hsi:clocked - OSC bridge listener:
127.0.0.1:9000 - Link clock OSC output:
127.0.0.1:9101 - FTDI serial:
B0011U7V - Live runtime config:
dmx-bridge/config.dmx.json
The legacy concurrently command remains available if the supervisor itself is being debugged:
cd dmx-bridge
pnpm example:dev:dmx:rawFor safe development without DMX hardware output:
cd dmx-bridge
pnpm dev:dmx:supervised:dryIf starting the stack from an agent as a detached process, write logs to a file so failures are visible:
cd dmx-bridge
mkdir -p logs
nohup pnpm dev:dmx:supervised > logs/dmx-dev-dmx.log 2>&1 &
tail -f logs/dmx-dev-dmx.logUseful verification commands:
ps -ax -o pid,ppid,command | grep -E 'dmx-supervisor|dev:dmx|concurrently|tsx.*(index|link-clock|clocked-hsi)|open-dmx-ftdi|src/index\.ts|link-clock-to-osc\.ts|clocked-hsi-sequencer\.ts' | grep -v grep
lsof -nP -iUDP:9000 2>/dev/null
lsof -nP -iTCP:8081 -sTCP:LISTEN 2>/dev/null
lsof -nP -iTCP:4200 -sTCP:LISTEN 2>/dev/null
tail -80 logs/dmx-dev-dmx.logHealthy bridge log lines include:
[bridge] OSC listening on 127.0.0.1:9000
[bridge] DMX universe "main" using driver "open-dmx-ftdi"
With pnpm dev:dmx:supervised running, the clocked HSI sequencer for fixture 3 at DMX address 015 is already supervised. For manual sequencer-only testing:
cd dmx-bridge
pnpm seq:hsi:clocked 127.0.0.1 9101 127.0.0.1 9000 15 4 0.20 200 0 --ignore-playUse --ignore-play when Ableton Link transport is stopped but you still want visible light changes.
Successful output appears in the bridge log as /dmx/setmany updates on channels 15-20, for example:
[bridge] /dmx/setmany [15,...,20,...] -> {"15":...,"20":...}
When replacing a running stack, kill only the specific PIDs from the verification command. Do not use pkill or killall.
Example:
kill <pnpm-dev-dmx-pid> <concurrently-pid> <tsx-pid> <node-runtime-pid> <open-dmx-ftdi-pid>cd dmx-bridge
pnpm dmx:supervisor:check
pnpm typecheck
pnpm buildThe Angular app can still be validated from the repository root with:
yarn buildThis bug has been fixed and has regressed at least once before — treat any related change with extra caution.
Symptoms reported by the user:
- The sequencer/lights sometimes keep playing (continue advancing/rendering) even after the user has stopped Ableton Live's transport.
- On restart of the supervised stack, the system sometimes incorrectly assumes it is in the "stopped" state even though Ableton Link is already playing (or vice versa), so lights don't start until an unrelated transport toggle happens.
Relevant code (read both before touching play/stop logic):
dmx-bridge/scripts/link-clock-to-osc.ts— the Ableton Link follower. Note the--startup-assume-play-msoption (startupAssumePlayMs) which force-overrides the reported play state totruefor a window after process start if Link reports stopped. This kind of startup-state assumption is a likely source of the "wrongly assumes stopped/playing on restart" symptom.dmx-bridge/scripts/sequencer/transport.ts—TransportGateclass, which tracksisPlayingState/waitingForDownbeatStateand gates step advancement based on play/stop transitions and downbeat sync. This is the core state machine responsible for actually holding or advancing playback; the "keeps playing after stop" symptom would show up here or in howruntime.tsconsumesconsumePlayState/shouldHoldStep.
Anti-regression checklist — run all of these before declaring this bug fixed, not just one:
- Start the supervised stack (
pnpm dev:dmx:supervised) while Ableton Live is already playing. Confirm the supervisor/bridge logs reportplay, notstop, and lights react immediately (no need to toggle transport first). - With the stack running and Live playing, stop Live's transport. Confirm the bridge log emits
clock transport=stopand the physical lights actually go static/stop reacting — not just that a log line appeared. - Start Live's transport again. Confirm
clock transport=playappears and lights resume. - Repeat stop → start → stop → start at least 3 times in a row without restarting the Node process. Confirm state never gets "stuck" (e.g., lights ignoring a subsequent stop, or a delayed/missed play transition).
- Kill and restart the supervised stack while Live is mid-playback (transport already running before the process starts). Confirm it does not incorrectly render as stopped.
- Kill and restart the supervised stack while Live's transport is stopped. Confirm it does not incorrectly render as playing (watch out for
--startup-assume-play-msmasking this). - Only after all six checks pass, and only after the physical-confirmation rule below has been satisfied, consider the bug closed.
Never declare a DMX/light/audio-reactive behavior "fixed", "working", or "done" based on logs, code review, or dry-run output alone. Before closing any task in this category:
- Explicitly ask the user to confirm they physically observed the expected behavior on the real fixture (not
--driver null/ dry mode). - Do not mark the task complete until that confirmation is given. If the user cannot test immediately, say the fix is implemented but unverified on hardware — do not call it "fixed".
- This matters because fixes declared complete from logs/dry-run alone have previously turned out broken or regressive on the real light (e.g. contrast/brightness controls lost after a supposed fix).
dmx-bridge/scripts/sequencer/config.ts and hsi-renderer.ts define several hardcoded millisecond defaults with no explanatory comment in code, commit history, or prior docs:
hsiIntensityAttackMs= 35hsiIntensityReleaseMs= 180hsiHueSlewMs= 180hsiSaturationSlewMs= 120hsiLiveAccentReleaseMs= defaults tohsiIntensityReleaseMs
Plausible reasons these particular values exist (perceptual smoothing/anti-flicker, DMX/FTDI update-rate or latency limits, avoiding visible stepping) were considered, but none could be confirmed from the existing code, comments, or git history. Do not present any of these as the confirmed rationale.
If asked to explain or change these values: say the original reasoning is unconfirmed and ask the user before assuming a rationale or changing the defaults.