# OpenSpec Change: enterprise-dmx - **Change ID**: enterprise-dmx - **Package**: `@suaveplan/dmx` - **Category**: universal - **Target path**: `universal/media/dmx` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/dmx.md` --- ## Summary Deliver `@suaveplan/dmx` on the genesis branch from scratch at enterprise quality. The package is a DMX512 lighting control engine: universe management (512 channels per universe), fixture definition and addressing, scene/chase/cue building, fade interpolation with injectable clock, transport-agnostic output (serial, WebSocket, UDP/Art-Net), and a chase sequencer with BPM clock sync. Pure TypeScript with no platform hard deps — transport is an injectable adapter. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. The main-branch package had 99.28% coverage. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `DmxError`; `DMX.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injectable clock | | `@suaveplan/events` (`workspace:^`) | Event bus for universe-update notifications | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Public API ### Core types ```ts class DmxError extends SuaveplanError const DMX_UNIVERSE_SIZE = 512 const DMX_MIN_VALUE = 0 const DMX_MAX_VALUE = 255 type DmxChannel = number // 1–512 type DmxValue = number // 0–255 type DmxUniverse = Uint8Array // length 512 interface FixtureDefinition { name: string channelCount: number channels: FixtureChannel[] } interface FixtureChannel { offset: number // 0-based offset within fixture name: string defaultValue?: DmxValue } interface FixtureInstance { id: string definition: FixtureDefinition startChannel: DmxChannel universeId: string } interface Scene { id: string values: Map // fixtureId:channelName → value } interface FadeOptions { durationMs: number easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' } interface DmxTransport { send(universe: DmxUniverse, universeId: string): Promise connect?(): Promise disconnect?(): Promise } interface DmxEngineOptions { transport: DmxTransport clock?: Clock logger?: LoggerLike refreshRateHz?: number // default: 40 (25ms frame) } class DmxEngine { constructor(options: DmxEngineOptions) addUniverse(id: string): void removeUniverse(id: string): void addFixture(instance: FixtureInstance): void removeFixture(id: string): void setChannel(universeId: string, channel: DmxChannel, value: DmxValue): void getChannel(universeId: string, channel: DmxChannel): DmxValue getUniverse(id: string): Readonly applyScene(scene: Scene, fade?: FadeOptions): Promise blackout(universeId?: string): void start(): void stop(): void } function createDmxEngine(options: DmxEngineOptions): DmxEngine function buildScene(id: string, values: Record): Scene function interpolate(from: DmxValue, to: DmxValue, t: number): DmxValue ``` ### Chase sequencer ```ts interface CueStep { scene: Scene holdMs: number fade?: FadeOptions } class ChaseSequencer { constructor(engine: DmxEngine, clock: Clock) addStep(step: CueStep): void start(looping?: boolean): void stop(): void jumpToStep(index: number): void readonly currentStep: number readonly running: boolean } ``` ### Error codes ```ts type DmxErrorCode = | 'DMX_UNIVERSE_NOT_FOUND' | 'DMX_FIXTURE_NOT_FOUND' | 'DMX_CHANNEL_OUT_OF_RANGE' | 'DMX_VALUE_OUT_OF_RANGE' | 'DMX_TRANSPORT_FAILED' | 'DMX_ADDRESS_CONFLICT' const DMX_ERROR_METADATA: Readonly> function registerDmxErrorCodes(): void ``` ### Zod schemas ```ts FixtureDefinitionSchema FixtureInstanceSchema SceneSchema FadeOptionsSchema DmxEngineOptionsSchema CueStepSchema ``` --- ## New additions for genesis 1. **`DmxTransport` interface** — injectable; `NullTransport` ships for tests (captures send calls). 2. **`ChaseSequencer`** — BPM-based step timing; uses injectable `Clock`; emits events via `@suaveplan/events`. 3. **Fade interpolation** — four easing functions; frame-rate controlled by `refreshRateHz`. 4. **`blackout`** — sets all channels in one or all universes to 0 immediately, bypassing any active fade. 5. **Address-conflict detection** — `addFixture` throws `DmxError(DMX_ADDRESS_CONFLICT)` if channel range overlaps an existing fixture in the same universe. 6. **OTel instrumentation** — counter `dmx.frame_total`, histogram `dmx.frame_render_ms`, counter `dmx.scene_apply_total`. 7. **Property-based tests** — channel value clamping invariant, fade interpolation monotonicity, scene application round-trip. 8. **Benchmark suite** — single-universe 512-channel render loop, 8-fixture scene apply. --- ## File layout ```text universal/media/dmx/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts types/ common.ts common.md core/ dmx-engine/ dmx-engine.ts dmx-engine.md dmx-engine.types.ts dmx-engine.test.ts index.ts chase-sequencer/ chase-sequencer.ts chase-sequencer.md chase-sequencer.types.ts chase-sequencer.test.ts index.ts transports/ null-transport/ null-transport.ts null-transport.md null-transport.test.ts index.ts utils/ interpolate.ts interpolate.md interpolate.test.ts scene.ts scene.md scene.test.ts __benchmarks__/ dmx.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream**: `@suaveplan/r3f-dmx` (React layer), any live-event lighting integrations. **Breaking changes**: none — fresh build. **Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events` published to Verdaccio.