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

6.2 KiB
Raw Blame History

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

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

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

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

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

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.