> Both tracks. The DMX feature lands on top of XOSC's existing logging infrastructure — don't invent a parallel one.
Logger
XOSC has one logger: electron/logger.ts (main) + useStore.pushLog (renderer). Levels: info, warn, error, debug. Sources: "input", "osc", "serial", "system".
Add "dmx" as a source. Update:
electron/shared/types.ts—LogEntry["source"]adds"dmx".src/types/api.ts— same.electron/logger.ts— make sure the formatter handles it (it should already; just verify).src/features/debug/DebugPanel.tsx— the source filter dropdown adds a"dmx"chip.
What to log
| Event | Level | Example message |
|---|---|---|
| Transmitter open | info | DMX open art-net://192.168.1.50:6454 universe=u1 (univ#1) |
| Transmitter close | info | DMX close art-net://... |
| Transmitter error | error | DMX art-net://... send failed: EHOSTUNREACH (rate-limited) |
| Frame drops > 30% | warn | DMX universe u1 dropping frames (45% over last 5s) |
| Sequence start | info | Pad start "Warm Wash" (slot=ambient priority=12) |
| Sequence stop | info | Pad stop "Warm Wash" (layer-id=...) reason=user |
| Blackout | warn | BLACKOUT (was 3 layers) |
| Tap tempo | debug | Tap clock "main" bpm=128.4 |
| Mixer slot conflict | debug | Slot "ambient" silenced layer-id=... (priority=5 < 12) |
Rate-limit error spam: at most one error log per (source, message) per 200 ms (TagTable's pattern in osc.js:240+).
Sink-rate gauge
DmxOutNode renders the existing <SinkRateGauge nodeId={node.id} />. It already shows events/s in amber > 60 / red > 200 — useful when a continuous source is flooding a DMX channel. No changes needed.
Cycle detector
src/store/cycleDetect.ts walks edges + virtual ones (Resolume parameter equality). Adding DMX:
- A
dmx-outwriting universeUchannelCis virtually fed by anydmx-inreading universeUchannelCon the same(bindHost, port). Implement as the existing Resolume-virtual-edge pattern: enumerate dmx-out / dmx-in pairs and synthesise a virtual edge for cycle detection.
dmx-in:c1 → transform → dmx-out:c1 would loop silently. The existing depth cap in the router still terminates it; the cycle warning UI just makes the loop visible.
Test coverage
- One Vitest test per transmitter that builds packets and snapshots the bytes (TagTable doesn't ship tests; we write minimal ones since DMX wire format is fragile).
- One Vitest test for
composeFramecovering: graph-only, pad-only, both, slot exclusivity, mix modes. - One Vitest test for
applyFixturecovering all 8 built-in profiles.
src/features/dmx/__tests__/ and electron/dmx/__tests__/.
What's measured for the diagnostics row
The DMX settings panel surfaces a per-transmitter diagnostics row:
kind • config(e.g.Art-Net • 192.168.1.50:6454 universe 1)- FPS (computed from
framesSent / uptime_s) - Drop rate %
- Health badge (good / warning / critical)
- Last-frame age (now - lastFrameTime)
- Connection status
Theme-driven colors:
var(--ok), var(--warn), var(--err) for the health badge.