@ecopoesis/homebridge-dmx 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CLAUDE.md +1 -0
  2. package/ENCRYPTION.md +207 -0
  3. package/PROTOCOL.md +277 -0
  4. package/README.md +25 -2
  5. package/config.schema.json +53 -5
  6. package/dist/config.d.ts +5 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +13 -2
  9. package/dist/config.js.map +1 -1
  10. package/dist/controller.d.ts +16 -4
  11. package/dist/controller.d.ts.map +1 -1
  12. package/dist/controller.js +41 -5
  13. package/dist/controller.js.map +1 -1
  14. package/dist/dmxController.d.ts +11 -0
  15. package/dist/dmxController.d.ts.map +1 -0
  16. package/dist/dmxController.js +4 -0
  17. package/dist/dmxController.js.map +1 -0
  18. package/dist/platform.d.ts.map +1 -1
  19. package/dist/platform.js +5 -2
  20. package/dist/platform.js.map +1 -1
  21. package/dist/platformAccessory.d.ts +2 -2
  22. package/dist/platformAccessory.d.ts.map +1 -1
  23. package/dist/platformAccessory.js.map +1 -1
  24. package/dist/sacn.d.ts +29 -0
  25. package/dist/sacn.d.ts.map +1 -0
  26. package/dist/sacn.js +142 -0
  27. package/dist/sacn.js.map +1 -0
  28. package/dist/settings.d.ts +23 -0
  29. package/dist/settings.d.ts.map +1 -1
  30. package/dist/settings.js +24 -0
  31. package/dist/settings.js.map +1 -1
  32. package/dist/zoneAccessory.d.ts +2 -2
  33. package/dist/zoneAccessory.d.ts.map +1 -1
  34. package/dist/zoneAccessory.js.map +1 -1
  35. package/examples/dmx.yaml +13 -4
  36. package/package.json +1 -1
  37. package/src/config.ts +18 -3
  38. package/src/controller.ts +41 -6
  39. package/src/dmxController.ts +14 -0
  40. package/src/platform.ts +8 -4
  41. package/src/platformAccessory.ts +2 -2
  42. package/src/sacn.ts +149 -0
  43. package/src/settings.ts +32 -0
  44. package/src/zoneAccessory.ts +2 -2
  45. package/tools/stick-power-cycle.py +121 -0
package/dist/settings.js CHANGED
@@ -8,8 +8,32 @@ export const CHARACTERISTIC_UPDATE_DELAY_MS = 50;
8
8
  * this long before spawning the send_dmx subprocess. Coalesces a fast
9
9
  * slider drag into one transaction. */
10
10
  export const DEBOUNCE_MS = 750;
11
+ /** Idle keepalive. The Stick-DE3 drifts into a wedged state after hours
12
+ * without traffic: sessions and the crypto handshake still succeed but
13
+ * live DMX output goes dark (observed 2026-08-22..25; only a hard power
14
+ * cycle recovers it). Re-sending the last-known state whenever the
15
+ * controller has been idle this long keeps the device exercised and
16
+ * re-asserts state after an out-of-band power-cycle. 0 disables. */
17
+ export const REFRESH_MINUTES_DEFAULT = 30;
18
+ /** How often the controller checks whether a keepalive refresh is due. */
19
+ export const REFRESH_CHECK_MS = 60_000;
11
20
  /** Frame interval (40ms = 25Hz, matches HWM exactly). Unused by the
12
21
  * subprocess controller — send_dmx.mjs's own FRAME_HZ env knob is what
13
22
  * matters now. */
14
23
  export const FRAME_INTERVAL_MS = 40;
24
+ // ── sACN (E1.31) controller ────────────────────────────────────────────────
25
+ /** UDP port sACN receivers listen on (ANSI E1.31). */
26
+ export const SACN_PORT = 5568;
27
+ /** Coalescing window for the burst of setFixture calls a zone set
28
+ * produces: one packet per universe per burst. */
29
+ export const SACN_COALESCE_MS = 25;
30
+ /** Keepalive stream rate. E1.31 receivers drop a source after 2.5 s of
31
+ * silence; a 1 Hz re-send keeps the gateway holding our look and
32
+ * re-asserts state cheaply. */
33
+ export const SACN_KEEPALIVE_MS = 1000;
34
+ /** Default E1.31 priority (spec range 0-200, 100 = default). Only matters
35
+ * if a second source ever targets the same universe. */
36
+ export const SACN_PRIORITY_DEFAULT = 100;
37
+ /** Source name advertised in every E1.31 framing layer (max 63 chars). */
38
+ export const SACN_SOURCE_NAME = 'homebridge-dmx';
15
39
  //# sourceMappingURL=settings.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC;AACnC,MAAM,CAAC,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAEvD;6EAC6E;AAC7E,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD;;wCAEwC;AACxC,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAE/B;;mBAEmB;AACnB,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC;AACnC,MAAM,CAAC,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAEvD;6EAC6E;AAC7E,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD;;wCAEwC;AACxC,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAE/B;;;;;qEAKqE;AACrE,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAE1C,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEvC;;mBAEmB;AACnB,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,8EAA8E;AAE9E,sDAAsD;AACtD,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC;AAE9B;mDACmD;AACnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC;;gCAEgC;AAChC,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAEtC;yDACyD;AACzD,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,gBAAgB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import type { PlatformAccessory } from 'homebridge';
2
2
  import { Fixture, Zone } from './config.js';
3
- import { StickController } from './controller.js';
3
+ import { DmxController } from './dmxController.js';
4
4
  import type { DmxPlatform } from './platform.js';
5
5
  import { StateRegistry } from './stateRegistry.js';
6
6
  export declare class ZoneFixture {
@@ -11,7 +11,7 @@ export declare class ZoneFixture {
11
11
  private pending;
12
12
  private refreshPending;
13
13
  private bulb;
14
- constructor(platform: DmxPlatform, accessory: PlatformAccessory, zone: Zone, controllers: Map<string, StickController>, registry: StateRegistry);
14
+ constructor(platform: DmxPlatform, accessory: PlatformAccessory, zone: Zone, controllers: Map<string, DmxController>, registry: StateRegistry);
15
15
  /** Compute majority state across members, per characteristic. */
16
16
  private majorityState;
17
17
  /** A HomeKit set on the zone: build the new state from the current
@@ -1 +1 @@
1
- {"version":3,"file":"zoneAccessory.d.ts","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAgB,MAAM,oBAAoB,CAAC;AAEjE,qBAAa,WAAW;IAMpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAT3B,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,IAAI,CAAU;gBAGH,QAAQ,EAAE,WAAW,EACtC,SAAS,EAAE,iBAAiB,EACX,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,QAAQ,EAAE,aAAa;IAmD1C,iEAAiE;IACjE,OAAO,CAAC,aAAa;IAWrB;;;mDAG+C;IAC/C,OAAO,CAAC,cAAc;IAWtB;qDACiD;IACjD,OAAO,CAAC,gBAAgB;IAuBxB;8CAC0C;IAC1C,OAAO,CAAC,eAAe;CAqBxB;AAGD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC"}
1
+ {"version":3,"file":"zoneAccessory.d.ts","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAgB,MAAM,oBAAoB,CAAC;AAEjE,qBAAa,WAAW;IAMpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAT3B,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,IAAI,CAAU;gBAGH,QAAQ,EAAE,WAAW,EACtC,SAAS,EAAE,iBAAiB,EACX,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EACvC,QAAQ,EAAE,aAAa;IAmD1C,iEAAiE;IACjE,OAAO,CAAC,aAAa;IAWrB;;;mDAG+C;IAC/C,OAAO,CAAC,cAAc;IAWtB;qDACiD;IACjD,OAAO,CAAC,gBAAgB;IAuBxB;8CAC0C;IAC1C,OAAO,CAAC,eAAe;CAqBxB;AAGD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"zoneAccessory.js","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"zoneAccessory.js","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"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"}
package/examples/dmx.yaml CHANGED
@@ -10,13 +10,22 @@
10
10
  # }]
11
11
 
12
12
  controllers:
13
- # One entry per physical DMX controller. Currently only StickDE3 is
14
- # supported; the abstraction is here so other controller types can be
15
- # added later without rewriting your patch.
13
+ # One entry per physical DMX controller. Supported types:
14
+ # sACN — any E1.31 gateway (e.g. ENTTEC DIN Ethergate Mk2). Packets
15
+ # are unicast to `ip`; patch universes A/B map to sACN
16
+ # universes 1/2, so set the gateway's ports to match.
17
+ # Optional `priority` (0-200, default 100) is the E1.31
18
+ # source priority.
19
+ # StickDE3 — Nicolaudie Stick-DE3 (legacy; subprocess transactions).
20
+ # Optional `refreshMinutes` idle keepalive (default 30).
16
21
  - id: main
17
- type: StickDE3
22
+ type: sACN
18
23
  ip: 192.168.96.2
19
24
 
25
+ # - id: stick
26
+ # type: StickDE3
27
+ # ip: 192.168.96.2
28
+
20
29
  profiles:
21
30
  # Fixture types. Each profile names a color model + the order its DMX
22
31
  # channels appear in. "(Fine)" makes a channel the low byte of a 16-bit
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ecopoesis/homebridge-dmx",
3
3
  "displayName": "DMX",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "Homebridge platform for DMX controllers (currently Nicolaudie Stick-DE3)",
6
6
  "license": "Apache-2.0",
7
7
  "type": "module",
package/src/config.ts CHANGED
@@ -38,13 +38,16 @@ import path from 'node:path';
38
38
  import yaml from 'js-yaml';
39
39
 
40
40
  import { ChannelDef, ColorModel, HKCharacteristic } from './color/types.js';
41
+ import { REFRESH_MINUTES_DEFAULT, SACN_PRIORITY_DEFAULT } from './settings.js';
41
42
  import { getColorModel } from './color/index.js';
42
43
  import { parseChannelName } from './color/parsers.js';
43
44
 
44
45
  export interface ControllerSpec {
45
46
  id: string; // unique handle, referenced from patch entries
46
- type: string; // 'StickDE3' (only one supported today)
47
+ type: string; // 'StickDE3' | 'sACN'
47
48
  ip: string; // controller's static IP
49
+ refreshMinutes?: number; // StickDE3 idle keepalive: re-send state every N min (0 = off)
50
+ priority?: number; // sACN only: E1.31 priority 0-200 (default 100)
48
51
  }
49
52
 
50
53
  export interface ProfileSpec {
@@ -83,13 +86,15 @@ export interface Profile {
83
86
  channels: ChannelDef[];
84
87
  }
85
88
 
86
- export const CONTROLLER_TYPES = ['StickDE3'] as const;
89
+ export const CONTROLLER_TYPES = ['StickDE3', 'sACN'] as const;
87
90
  export type ControllerType = typeof CONTROLLER_TYPES[number];
88
91
 
89
92
  export interface Controller {
90
93
  id: string;
91
94
  type: ControllerType;
92
95
  ip: string;
96
+ refreshMinutes: number;
97
+ priority: number; // E1.31 priority; only the sACN controller reads it
93
98
  }
94
99
 
95
100
  export interface Fixture {
@@ -159,7 +164,17 @@ export function loadConfig(rawJson: RawConfig, cwd?: string): LoadedConfig {
159
164
  if (!cs.ip || typeof cs.ip !== 'string') {
160
165
  throw new Error(`controller "${cs.id}": required field "ip" missing`);
161
166
  }
162
- controllersById.set(cs.id, { id: cs.id, type: cs.type as ControllerType, ip: cs.ip });
167
+ const refreshMinutes = cs.refreshMinutes ?? REFRESH_MINUTES_DEFAULT;
168
+ if (typeof refreshMinutes !== 'number' || refreshMinutes < 0) {
169
+ throw new Error(`controller "${cs.id}": refreshMinutes must be a number >= 0 (0 disables)`);
170
+ }
171
+ const priority = cs.priority ?? SACN_PRIORITY_DEFAULT;
172
+ if (!Number.isInteger(priority) || priority < 0 || priority > 200) {
173
+ throw new Error(`controller "${cs.id}": priority must be an integer 0..200`);
174
+ }
175
+ controllersById.set(cs.id, {
176
+ id: cs.id, type: cs.type as ControllerType, ip: cs.ip, refreshMinutes, priority,
177
+ });
163
178
  }
164
179
 
165
180
  // Profiles
package/src/controller.ts CHANGED
@@ -15,7 +15,8 @@ import path from 'node:path';
15
15
 
16
16
  import { Fixture } from './config.js';
17
17
  import { HomeKitLightState } from './color/types.js';
18
- import { DEBOUNCE_MS } from './settings.js';
18
+ import { DmxController } from './dmxController.js';
19
+ import { DEBOUNCE_MS, REFRESH_CHECK_MS } from './settings.js';
19
20
  import { sleep } from './stick/protocol.js';
20
21
 
21
22
  const __filename = fileURLToPath(import.meta.url);
@@ -23,10 +24,12 @@ const __dirname = path.dirname(__filename);
23
24
  // dist/controller.js → ../tools/send_dmx.mjs
24
25
  const SEND_DMX_PATH = path.resolve(__dirname, '..', 'tools', 'send_dmx.mjs');
25
26
 
26
- export class StickController {
27
+ export class StickController implements DmxController {
27
28
  private targets = new Map<number, Uint8Array>();
28
29
  private lastChangeAt = 0;
30
+ private lastTxEndAt = 0;
29
31
  private streaming = false;
32
+ private refreshTimer?: NodeJS.Timeout;
30
33
  /** Tracks whether the very first subprocess in this controller's
31
34
  * lifetime should set RUN_PROBE=1. After a Stick power-cycle (which
32
35
  * we can't detect in-band) the probe is required to (re-)register
@@ -37,7 +40,31 @@ export class StickController {
37
40
  private readonly ip: string,
38
41
  private readonly log: Logger,
39
42
  private readonly debounceMs: number = DEBOUNCE_MS,
40
- ) {}
43
+ private readonly refreshMs: number = 0,
44
+ ) {
45
+ if (refreshMs > 0) {
46
+ this.refreshTimer = setInterval(() => this.maybeRefresh(), REFRESH_CHECK_MS);
47
+ this.refreshTimer.unref?.();
48
+ }
49
+ }
50
+
51
+ /** Idle keepalive: the Stick wedges (sessions succeed, output dark) after
52
+ * hours without traffic. Re-send the current universe state whenever the
53
+ * controller has been idle for refreshMs. Also re-asserts state after an
54
+ * out-of-band power-cycle of the Stick. */
55
+ private maybeRefresh(): void {
56
+ if (this.streaming || this.targets.size === 0) return;
57
+ const idleMs = Date.now() - Math.max(this.lastTxEndAt, this.lastChangeAt);
58
+ if (idleMs < this.refreshMs) return;
59
+ this.log.info(`keepalive refresh after ${Math.round(idleMs / 60_000)}m idle`);
60
+ this.needsProbe = true; // the Stick may have power-cycled while we were idle
61
+ this.streaming = true;
62
+ this.run().catch((e) => {
63
+ this.log.error('keepalive refresh crashed:', (e as Error).message);
64
+ }).finally(() => {
65
+ this.streaming = false;
66
+ });
67
+ }
41
68
 
42
69
  /** Stage a fixture's bytes into the universe target buffer and ensure
43
70
  * the streaming loop is running. */
@@ -67,7 +94,9 @@ export class StickController {
67
94
  }
68
95
 
69
96
  /** Build the CLI args from current targets. Only non-zero channels are
70
- * passed — send_dmx initialises the universe to 0 so omission == 0. */
97
+ * passed — send_dmx initialises the universe to 0 so omission == 0.
98
+ * When everything is 0 (all lights off), send an explicit zero so
99
+ * send_dmx still has at least one assignment to transact. */
71
100
  private buildArgs(): string[] {
72
101
  const args: string[] = [SEND_DMX_PATH, this.ip];
73
102
  for (const [u, arr] of this.targets) {
@@ -75,6 +104,10 @@ export class StickController {
75
104
  if (arr[i] !== 0) args.push(`${u},${i + 1}=${arr[i]}`);
76
105
  }
77
106
  }
107
+ if (args.length === 2 && this.targets.size > 0) {
108
+ const firstU = this.targets.keys().next().value as number;
109
+ args.push(`${firstU},1=0`);
110
+ }
78
111
  return args;
79
112
  }
80
113
 
@@ -115,6 +148,7 @@ export class StickController {
115
148
  });
116
149
  });
117
150
  const elapsedMs = Date.now() - startedAt;
151
+ this.lastTxEndAt = Date.now();
118
152
 
119
153
  if (exitCode === 0) {
120
154
  this.needsProbe = false;
@@ -129,8 +163,9 @@ export class StickController {
129
163
  }
130
164
  }
131
165
 
132
- /** Shutdown hook. Subprocess approach has no long-lived state. */
166
+ /** Shutdown hook. Stop the keepalive timer; the subprocess approach has
167
+ * no other long-lived state. */
133
168
  shutdown(): void {
134
- /* no-op */
169
+ if (this.refreshTimer) clearInterval(this.refreshTimer);
135
170
  }
136
171
  }
@@ -0,0 +1,14 @@
1
+ // DmxController — the contract the platform and accessories program against.
2
+ // One implementation per controller `type` (StickDE3, sACN, ...).
3
+
4
+ import { Fixture } from './config.js';
5
+ import { HomeKitLightState } from './color/types.js';
6
+
7
+ export interface DmxController {
8
+ /** Render `state` through the fixture's color model into the universe
9
+ * buffer and (eventually) put it on the wire. Must be safe to call at
10
+ * HomeKit interaction rates. */
11
+ setFixture(fixture: Fixture, state: HomeKitLightState): void;
12
+ /** Homebridge shutdown hook: stop timers, close sockets. */
13
+ shutdown(): void;
14
+ }
package/src/platform.ts CHANGED
@@ -16,10 +16,12 @@ import type {
16
16
 
17
17
  import { Controller, LoadedConfig, loadConfig, RawConfig } from './config.js';
18
18
  import { StickController } from './controller.js';
19
+ import { DmxController } from './dmxController.js';
20
+ import { SacnController } from './sacn.js';
19
21
  import { StickFixture } from './platformAccessory.js';
20
22
  import { StateRegistry } from './stateRegistry.js';
21
23
  import { ZoneFixture } from './zoneAccessory.js';
22
- import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
24
+ import { DEBOUNCE_MS, PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
23
25
 
24
26
  export class DmxPlatform implements DynamicPlatformPlugin {
25
27
  public readonly Service: typeof Service;
@@ -27,7 +29,7 @@ export class DmxPlatform implements DynamicPlatformPlugin {
27
29
  public readonly accessories: PlatformAccessory[] = [];
28
30
 
29
31
  private cfg: LoadedConfig | null = null;
30
- private controllers = new Map<string, StickController>();
32
+ private controllers = new Map<string, DmxController>();
31
33
  private registry = new StateRegistry();
32
34
 
33
35
  constructor(
@@ -65,10 +67,12 @@ export class DmxPlatform implements DynamicPlatformPlugin {
65
67
  });
66
68
  }
67
69
 
68
- private makeController(c: Controller): StickController {
70
+ private makeController(c: Controller): DmxController {
69
71
  switch (c.type) {
70
72
  case 'StickDE3':
71
- return new StickController(c.ip, this.log);
73
+ return new StickController(c.ip, this.log, DEBOUNCE_MS, c.refreshMinutes * 60_000);
74
+ case 'sACN':
75
+ return new SacnController(c.ip, this.log, c.priority);
72
76
  default:
73
77
  throw new Error(`unsupported controller type "${(c as Controller).type}"`);
74
78
  }
@@ -12,7 +12,7 @@ import type {
12
12
 
13
13
  import { Fixture } from './config.js';
14
14
  import { HomeKitLightState } from './color/types.js';
15
- import { StickController } from './controller.js';
15
+ import { DmxController } from './dmxController.js';
16
16
  import type { DmxPlatform } from './platform.js';
17
17
  import { CHARACTERISTIC_UPDATE_DELAY_MS } from './settings.js';
18
18
  import { StateRegistry } from './stateRegistry.js';
@@ -25,7 +25,7 @@ export class StickFixture {
25
25
  private readonly platform: DmxPlatform,
26
26
  private readonly accessory: PlatformAccessory,
27
27
  private readonly fixture: Fixture,
28
- private readonly controller: StickController,
28
+ private readonly controller: DmxController,
29
29
  private readonly registry: StateRegistry,
30
30
  ) {
31
31
  const C = platform.Characteristic;
package/src/sacn.ts ADDED
@@ -0,0 +1,149 @@
1
+ // SacnController — streams DMX to a network gateway over sACN (E1.31).
2
+ //
3
+ // Built for the ENTTEC DIN Ethergate Mk2 (PoE) but speaks plain
4
+ // ANSI E1.31, so any sACN node works. Unlike the Stick there is no
5
+ // session, no crypto, and sends are ~free, so the model is simple:
6
+ //
7
+ // - setFixture() renders into a per-universe 512-byte buffer and marks
8
+ // the universe dirty; a short coalescing timer batches the burst of
9
+ // per-member calls a zone set produces into one packet per universe
10
+ // - a 1 Hz keepalive re-sends every universe we have state for: E1.31
11
+ // receivers drop a source after 2.5 s of silence, and the steady
12
+ // trickle keeps the gateway holding our look
13
+ // - shutdown() sends the spec's stream-terminated packets so the
14
+ // gateway drops the source cleanly (its DMX output holds last values)
15
+ //
16
+ // Universe numbering: patch universes A/B (internal 0/1) map to sACN
17
+ // universes 1/2 — sACN has no universe 0. Set the Ethergate ports to
18
+ // sACN universes 1 and 2. Packets are unicast to the configured ip.
19
+
20
+ import type { Logger } from 'homebridge';
21
+ import dgram from 'node:dgram';
22
+ import { randomBytes } from 'node:crypto';
23
+
24
+ import { Fixture } from './config.js';
25
+ import { HomeKitLightState } from './color/types.js';
26
+ import { DmxController } from './dmxController.js';
27
+ import {
28
+ SACN_COALESCE_MS,
29
+ SACN_KEEPALIVE_MS,
30
+ SACN_PORT,
31
+ SACN_PRIORITY_DEFAULT,
32
+ SACN_SOURCE_NAME,
33
+ } from './settings.js';
34
+
35
+ const PACKET_LEN = 638; // full 512-slot E1.31 data packet
36
+ const OPT_STREAM_TERMINATED = 0x40;
37
+
38
+ export class SacnController implements DmxController {
39
+ private universes = new Map<number, Uint8Array>(); // internal idx (0=A,1=B) → slots
40
+ private seq = new Map<number, number>();
41
+ private dirty = new Set<number>();
42
+ private coalesceTimer: NodeJS.Timeout | null = null;
43
+ private keepaliveTimer: NodeJS.Timeout | null = null;
44
+ private readonly socket: dgram.Socket;
45
+ private readonly cid = randomBytes(16);
46
+ private closed = false;
47
+
48
+ constructor(
49
+ private readonly ip: string,
50
+ private readonly log: Logger,
51
+ private readonly priority: number = SACN_PRIORITY_DEFAULT,
52
+ ) {
53
+ this.socket = dgram.createSocket('udp4');
54
+ this.socket.on('error', (e) => this.log.error('sACN socket error:', e.message));
55
+ }
56
+
57
+ setFixture(fixture: Fixture, state: HomeKitLightState): void {
58
+ const arr = this.universeBuf(fixture.universe);
59
+ const bytes = fixture.profile.model.render(state, fixture.profile.channels);
60
+ for (let i = 0; i < bytes.length; i++) {
61
+ arr[fixture.startCh - 1 + i] = bytes[i];
62
+ }
63
+ this.dirty.add(fixture.universe);
64
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
65
+ this.log.info(`setFixture ${fixture.id}@DMX${fixture.startCh}/U${fixture.universe + 1}=${hex}`);
66
+ this.scheduleSend();
67
+ if (!this.keepaliveTimer) {
68
+ this.keepaliveTimer = setInterval(() => this.sendAll(), SACN_KEEPALIVE_MS);
69
+ this.keepaliveTimer.unref?.();
70
+ }
71
+ }
72
+
73
+ private universeBuf(u: number): Uint8Array {
74
+ let arr = this.universes.get(u);
75
+ if (!arr) { arr = new Uint8Array(512); this.universes.set(u, arr); }
76
+ return arr;
77
+ }
78
+
79
+ /** Coalesce the burst of setFixture calls a zone set produces into one
80
+ * packet per universe. */
81
+ private scheduleSend(): void {
82
+ if (this.coalesceTimer) return;
83
+ this.coalesceTimer = setTimeout(() => {
84
+ this.coalesceTimer = null;
85
+ for (const u of this.dirty) this.sendUniverse(u);
86
+ this.dirty.clear();
87
+ }, SACN_COALESCE_MS);
88
+ }
89
+
90
+ private sendAll(): void {
91
+ for (const u of this.universes.keys()) this.sendUniverse(u);
92
+ }
93
+
94
+ private sendUniverse(u: number, options = 0): void {
95
+ if (this.closed) return;
96
+ const slots = this.universes.get(u);
97
+ if (!slots) return;
98
+ const seq = (this.seq.get(u) ?? 0) & 0xff;
99
+ this.seq.set(u, seq + 1);
100
+ const pkt = this.buildPacket(u + 1, seq, slots, options);
101
+ this.socket.send(pkt, SACN_PORT, this.ip, (e) => {
102
+ if (e) this.log.error(`sACN send U${u + 1} failed:`, e.message);
103
+ });
104
+ }
105
+
106
+ /** ANSI E1.31 data packet: root layer + framing layer + DMP layer. */
107
+ private buildPacket(sacnUniverse: number, seq: number, slots: Uint8Array, options: number): Buffer {
108
+ const buf = Buffer.alloc(PACKET_LEN);
109
+ // Root layer
110
+ buf.writeUInt16BE(0x0010, 0); // RLP preamble size
111
+ buf.writeUInt16BE(0x0000, 2); // RLP postamble size
112
+ buf.write('ASC-E1.17', 4, 'ascii'); // ACN packet identifier (12 bytes, zero-padded)
113
+ buf.writeUInt16BE(0x7000 | (PACKET_LEN - 16), 16); // flags + length
114
+ buf.writeUInt32BE(0x00000004, 18); // VECTOR_ROOT_E131_DATA
115
+ this.cid.copy(buf, 22);
116
+ // Framing layer
117
+ buf.writeUInt16BE(0x7000 | (PACKET_LEN - 38), 38);
118
+ buf.writeUInt32BE(0x00000002, 40); // VECTOR_E131_DATA_PACKET
119
+ buf.write(SACN_SOURCE_NAME.slice(0, 63), 44, 'utf8');
120
+ buf.writeUInt8(this.priority, 108);
121
+ buf.writeUInt16BE(0x0000, 109); // sync address (none)
122
+ buf.writeUInt8(seq, 111);
123
+ buf.writeUInt8(options, 112);
124
+ buf.writeUInt16BE(sacnUniverse, 113);
125
+ // DMP layer
126
+ buf.writeUInt16BE(0x7000 | (PACKET_LEN - 115), 115);
127
+ buf.writeUInt8(0x02, 117); // VECTOR_DMP_SET_PROPERTY
128
+ buf.writeUInt8(0xa1, 118); // address & data type
129
+ buf.writeUInt16BE(0x0000, 119); // first property address
130
+ buf.writeUInt16BE(0x0001, 121); // address increment
131
+ buf.writeUInt16BE(1 + 512, 123); // property value count
132
+ buf.writeUInt8(0x00, 125); // DMX start code
133
+ buf.set(slots, 126);
134
+ return buf;
135
+ }
136
+
137
+ shutdown(): void {
138
+ if (this.coalesceTimer) clearTimeout(this.coalesceTimer);
139
+ if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
140
+ // Spec-polite goodbye: three stream-terminated packets per universe.
141
+ // The gateway drops the source immediately instead of waiting out the
142
+ // 2.5 s network-loss timeout; its DMX output holds the last values.
143
+ for (const u of this.universes.keys()) {
144
+ for (let i = 0; i < 3; i++) this.sendUniverse(u, OPT_STREAM_TERMINATED);
145
+ }
146
+ this.closed = true;
147
+ setTimeout(() => this.socket.close(), 100).unref?.();
148
+ }
149
+ }
package/src/settings.ts CHANGED
@@ -12,7 +12,39 @@ export const CHARACTERISTIC_UPDATE_DELAY_MS = 50;
12
12
  * slider drag into one transaction. */
13
13
  export const DEBOUNCE_MS = 750;
14
14
 
15
+ /** Idle keepalive. The Stick-DE3 drifts into a wedged state after hours
16
+ * without traffic: sessions and the crypto handshake still succeed but
17
+ * live DMX output goes dark (observed 2026-08-22..25; only a hard power
18
+ * cycle recovers it). Re-sending the last-known state whenever the
19
+ * controller has been idle this long keeps the device exercised and
20
+ * re-asserts state after an out-of-band power-cycle. 0 disables. */
21
+ export const REFRESH_MINUTES_DEFAULT = 30;
22
+
23
+ /** How often the controller checks whether a keepalive refresh is due. */
24
+ export const REFRESH_CHECK_MS = 60_000;
25
+
15
26
  /** Frame interval (40ms = 25Hz, matches HWM exactly). Unused by the
16
27
  * subprocess controller — send_dmx.mjs's own FRAME_HZ env knob is what
17
28
  * matters now. */
18
29
  export const FRAME_INTERVAL_MS = 40;
30
+
31
+ // ── sACN (E1.31) controller ────────────────────────────────────────────────
32
+
33
+ /** UDP port sACN receivers listen on (ANSI E1.31). */
34
+ export const SACN_PORT = 5568;
35
+
36
+ /** Coalescing window for the burst of setFixture calls a zone set
37
+ * produces: one packet per universe per burst. */
38
+ export const SACN_COALESCE_MS = 25;
39
+
40
+ /** Keepalive stream rate. E1.31 receivers drop a source after 2.5 s of
41
+ * silence; a 1 Hz re-send keeps the gateway holding our look and
42
+ * re-asserts state cheaply. */
43
+ export const SACN_KEEPALIVE_MS = 1000;
44
+
45
+ /** Default E1.31 priority (spec range 0-200, 100 = default). Only matters
46
+ * if a second source ever targets the same universe. */
47
+ export const SACN_PRIORITY_DEFAULT = 100;
48
+
49
+ /** Source name advertised in every E1.31 framing layer (max 63 chars). */
50
+ export const SACN_SOURCE_NAME = 'homebridge-dmx';
@@ -23,7 +23,7 @@ import type {
23
23
 
24
24
  import { Fixture, Zone } from './config.js';
25
25
  import { HKCharacteristic, HomeKitLightState } from './color/types.js';
26
- import { StickController } from './controller.js';
26
+ import { DmxController } from './dmxController.js';
27
27
  import type { DmxPlatform } from './platform.js';
28
28
  import { CHARACTERISTIC_UPDATE_DELAY_MS } from './settings.js';
29
29
  import { StateRegistry, pickMajority } from './stateRegistry.js';
@@ -37,7 +37,7 @@ export class ZoneFixture {
37
37
  private readonly platform: DmxPlatform,
38
38
  accessory: PlatformAccessory,
39
39
  private readonly zone: Zone,
40
- private readonly controllers: Map<string, StickController>,
40
+ private readonly controllers: Map<string, DmxController>,
41
41
  private readonly registry: StateRegistry,
42
42
  ) {
43
43
  const C = platform.Characteristic;