Files
suaveplan/openspec/changes/enterprise-dmx/proposal.md
2026-05-27 23:29:53 +01:00

232 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<string, DmxValue> // fixtureId:channelName → value
}
interface FadeOptions {
durationMs: number
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
}
interface DmxTransport {
send(universe: DmxUniverse, universeId: string): Promise<void>
connect?(): Promise<void>
disconnect?(): Promise<void>
}
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<DmxUniverse>
applyScene(scene: Scene, fade?: FadeOptions): Promise<void>
blackout(universeId?: string): void
start(): void
stop(): void
}
function createDmxEngine(options: DmxEngineOptions): DmxEngine
function buildScene(id: string, values: Record<string, DmxValue>): 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<Record<DmxErrorCode, CodeMetadata>>
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.