@ecopoesis/homebridge-dmx 0.1.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 (226) hide show
  1. package/CLAUDE.md +176 -0
  2. package/README.md +146 -0
  3. package/config.schema.json +143 -0
  4. package/dist/cli/stick.d.ts +3 -0
  5. package/dist/cli/stick.d.ts.map +1 -0
  6. package/dist/cli/stick.js +176 -0
  7. package/dist/cli/stick.js.map +1 -0
  8. package/dist/color/cct.d.ts +3 -0
  9. package/dist/color/cct.d.ts.map +1 -0
  10. package/dist/color/cct.js +33 -0
  11. package/dist/color/cct.js.map +1 -0
  12. package/dist/color/dimmer.d.ts +3 -0
  13. package/dist/color/dimmer.d.ts.map +1 -0
  14. package/dist/color/dimmer.js +16 -0
  15. package/dist/color/dimmer.js.map +1 -0
  16. package/dist/color/hsvcct.d.ts +3 -0
  17. package/dist/color/hsvcct.d.ts.map +1 -0
  18. package/dist/color/hsvcct.js +52 -0
  19. package/dist/color/hsvcct.js.map +1 -0
  20. package/dist/color/index.d.ts +7 -0
  21. package/dist/color/index.d.ts.map +1 -0
  22. package/dist/color/index.js +28 -0
  23. package/dist/color/index.js.map +1 -0
  24. package/dist/color/parsers.d.ts +20 -0
  25. package/dist/color/parsers.d.ts.map +1 -0
  26. package/dist/color/parsers.js +232 -0
  27. package/dist/color/parsers.js.map +1 -0
  28. package/dist/color/rgb.d.ts +3 -0
  29. package/dist/color/rgb.d.ts.map +1 -0
  30. package/dist/color/rgb.js +32 -0
  31. package/dist/color/rgb.js.map +1 -0
  32. package/dist/color/rgbaw.d.ts +3 -0
  33. package/dist/color/rgbaw.d.ts.map +1 -0
  34. package/dist/color/rgbaw.js +48 -0
  35. package/dist/color/rgbaw.js.map +1 -0
  36. package/dist/color/rgbw.d.ts +3 -0
  37. package/dist/color/rgbw.d.ts.map +1 -0
  38. package/dist/color/rgbw.js +37 -0
  39. package/dist/color/rgbw.js.map +1 -0
  40. package/dist/color/rgbww.d.ts +3 -0
  41. package/dist/color/rgbww.d.ts.map +1 -0
  42. package/dist/color/rgbww.js +54 -0
  43. package/dist/color/rgbww.js.map +1 -0
  44. package/dist/color/types.d.ts +64 -0
  45. package/dist/color/types.d.ts.map +1 -0
  46. package/dist/color/types.js +91 -0
  47. package/dist/color/types.js.map +1 -0
  48. package/dist/config.d.ts +60 -0
  49. package/dist/config.d.ts.map +1 -0
  50. package/dist/config.js +191 -0
  51. package/dist/config.js.map +1 -0
  52. package/dist/controller.d.ts +32 -0
  53. package/dist/controller.d.ts.map +1 -0
  54. package/dist/controller.js +128 -0
  55. package/dist/controller.js.map +1 -0
  56. package/dist/index.d.ts +4 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +7 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/platform.d.ts +17 -0
  61. package/dist/platform.d.ts.map +1 -0
  62. package/dist/platform.js +96 -0
  63. package/dist/platform.js.map +1 -0
  64. package/dist/platformAccessory.d.ts +18 -0
  65. package/dist/platformAccessory.d.ts.map +1 -0
  66. package/dist/platformAccessory.js +81 -0
  67. package/dist/platformAccessory.js.map +1 -0
  68. package/dist/settings.d.ts +14 -0
  69. package/dist/settings.d.ts.map +1 -0
  70. package/dist/settings.js +15 -0
  71. package/dist/settings.js.map +1 -0
  72. package/dist/stick/frame.d.ts +16 -0
  73. package/dist/stick/frame.d.ts.map +1 -0
  74. package/dist/stick/frame.js +51 -0
  75. package/dist/stick/frame.js.map +1 -0
  76. package/dist/stick/kdf.d.ts +23 -0
  77. package/dist/stick/kdf.d.ts.map +1 -0
  78. package/dist/stick/kdf.js +90 -0
  79. package/dist/stick/kdf.js.map +1 -0
  80. package/dist/stick/protocol.d.ts +28 -0
  81. package/dist/stick/protocol.d.ts.map +1 -0
  82. package/dist/stick/protocol.js +78 -0
  83. package/dist/stick/protocol.js.map +1 -0
  84. package/dist/stick/session.d.ts +50 -0
  85. package/dist/stick/session.d.ts.map +1 -0
  86. package/dist/stick/session.js +301 -0
  87. package/dist/stick/session.js.map +1 -0
  88. package/examples/dmx.yaml +65 -0
  89. package/package.json +51 -0
  90. package/src/cli/stick.ts +198 -0
  91. package/src/color/cct.ts +40 -0
  92. package/src/color/dimmer.ts +24 -0
  93. package/src/color/hsvcct.ts +59 -0
  94. package/src/color/index.ts +34 -0
  95. package/src/color/parsers.ts +232 -0
  96. package/src/color/rgb.ts +33 -0
  97. package/src/color/rgbaw.ts +46 -0
  98. package/src/color/rgbw.ts +36 -0
  99. package/src/color/rgbww.ts +56 -0
  100. package/src/color/types.ts +150 -0
  101. package/src/config.ts +248 -0
  102. package/src/controller.ts +136 -0
  103. package/src/index.ts +9 -0
  104. package/src/platform.ts +118 -0
  105. package/src/platformAccessory.ts +102 -0
  106. package/src/settings.ts +18 -0
  107. package/src/stick/frame.ts +70 -0
  108. package/src/stick/kdf.ts +99 -0
  109. package/src/stick/protocol.ts +90 -0
  110. package/src/stick/session.ts +348 -0
  111. package/stick-de3.json +6 -0
  112. package/tools/aes-hook.c +1404 -0
  113. package/tools/analyze-pcap.mjs +143 -0
  114. package/tools/capture-aes-key.lldb +25 -0
  115. package/tools/capture-and-verify.sh +46 -0
  116. package/tools/capture-handshake.sh +59 -0
  117. package/tools/capture-send.sh +39 -0
  118. package/tools/crack-kdf.mjs +121 -0
  119. package/tools/crack-key.mjs +170 -0
  120. package/tools/decode-frame.mjs +110 -0
  121. package/tools/decrypt-011c.mjs +123 -0
  122. package/tools/decrypt-dmx.mjs +130 -0
  123. package/tools/decrypt-ssl2.ts +154 -0
  124. package/tools/derive-dmx-key.mjs +189 -0
  125. package/tools/diff-011c.mjs +127 -0
  126. package/tools/dump-pcap-summary.mjs +54 -0
  127. package/tools/dump-tcp-stream.mjs +123 -0
  128. package/tools/extract-011c.mjs +117 -0
  129. package/tools/extract-frame.mjs +78 -0
  130. package/tools/find-011c-key.mjs +140 -0
  131. package/tools/find-iv.mjs +153 -0
  132. package/tools/ghidra/README.md +136 -0
  133. package/tools/ghidra/__pycache__/dump-011c-callers.cpython-313.pyc +0 -0
  134. package/tools/ghidra/__pycache__/dump-011c-handler.cpython-313.pyc +0 -0
  135. package/tools/ghidra/__pycache__/dump-aes-candidates.cpython-313.pyc +0 -0
  136. package/tools/ghidra/__pycache__/dump-anchors.cpython-313.pyc +0 -0
  137. package/tools/ghidra/__pycache__/dump-auth2.cpython-313.pyc +0 -0
  138. package/tools/ghidra/__pycache__/dump-authenticate.cpython-313.pyc +0 -0
  139. package/tools/ghidra/__pycache__/dump-block-cipher.cpython-313.pyc +0 -0
  140. package/tools/ghidra/__pycache__/dump-cipher.cpython-313.pyc +0 -0
  141. package/tools/ghidra/__pycache__/dump-ctor-chain.cpython-313.pyc +0 -0
  142. package/tools/ghidra/__pycache__/dump-curve.cpython-313.pyc +0 -0
  143. package/tools/ghidra/__pycache__/dump-encompassing.cpython-313.pyc +0 -0
  144. package/tools/ghidra/__pycache__/dump-handshake.cpython-313.pyc +0 -0
  145. package/tools/ghidra/__pycache__/dump-kdf-chain.cpython-313.pyc +0 -0
  146. package/tools/ghidra/__pycache__/dump-kdf-context.cpython-313.pyc +0 -0
  147. package/tools/ghidra/__pycache__/dump-profile.cpython-313.pyc +0 -0
  148. package/tools/ghidra/__pycache__/dump-real-encrypt.cpython-313.pyc +0 -0
  149. package/tools/ghidra/__pycache__/dump-secret.cpython-313.pyc +0 -0
  150. package/tools/ghidra/__pycache__/dump-setsoftware.cpython-313.pyc +0 -0
  151. package/tools/ghidra/__pycache__/dump-stick3-vtable.cpython-313.pyc +0 -0
  152. package/tools/ghidra/__pycache__/dump-swxref.cpython-313.pyc +0 -0
  153. package/tools/ghidra/__pycache__/dump-vt-key-getter.cpython-313.pyc +0 -0
  154. package/tools/ghidra/__pycache__/find-cipher.cpython-313.pyc +0 -0
  155. package/tools/ghidra/__pycache__/find-ctor.cpython-313.pyc +0 -0
  156. package/tools/ghidra/__pycache__/find-md5.cpython-313.pyc +0 -0
  157. package/tools/ghidra/__pycache__/find-secret-users.cpython-313.pyc +0 -0
  158. package/tools/ghidra/__pycache__/find-stick3-crypto.cpython-313.pyc +0 -0
  159. package/tools/ghidra/__pycache__/find-stick3-device.cpython-313.pyc +0 -0
  160. package/tools/ghidra/__pycache__/find-stick3a-ctor.cpython-313.pyc +0 -0
  161. package/tools/ghidra/__pycache__/find-stick3anet-ctor.cpython-313.pyc +0 -0
  162. package/tools/ghidra/__pycache__/find-vtables.cpython-313.pyc +0 -0
  163. package/tools/ghidra/__pycache__/string-walk.cpython-313.pyc +0 -0
  164. package/tools/ghidra/dump-011c-callers.py +140 -0
  165. package/tools/ghidra/dump-011c-handler.py +158 -0
  166. package/tools/ghidra/dump-aes-candidates.py +99 -0
  167. package/tools/ghidra/dump-anchors.py +192 -0
  168. package/tools/ghidra/dump-auth2.py +50 -0
  169. package/tools/ghidra/dump-authenticate.py +71 -0
  170. package/tools/ghidra/dump-block-cipher.py +88 -0
  171. package/tools/ghidra/dump-cipher.py +106 -0
  172. package/tools/ghidra/dump-ctor-chain.py +107 -0
  173. package/tools/ghidra/dump-curve.py +112 -0
  174. package/tools/ghidra/dump-encompassing.py +146 -0
  175. package/tools/ghidra/dump-handshake.py +76 -0
  176. package/tools/ghidra/dump-kdf-chain.py +105 -0
  177. package/tools/ghidra/dump-kdf-context.py +72 -0
  178. package/tools/ghidra/dump-profile.py +59 -0
  179. package/tools/ghidra/dump-real-encrypt.py +90 -0
  180. package/tools/ghidra/dump-secret.py +84 -0
  181. package/tools/ghidra/dump-setsoftware.py +63 -0
  182. package/tools/ghidra/dump-stick3-vtable.py +182 -0
  183. package/tools/ghidra/dump-swxref.py +27 -0
  184. package/tools/ghidra/dump-vt-key-getter.py +170 -0
  185. package/tools/ghidra/find-cipher.py +210 -0
  186. package/tools/ghidra/find-ctor.py +109 -0
  187. package/tools/ghidra/find-md5.py +97 -0
  188. package/tools/ghidra/find-secret-users.py +98 -0
  189. package/tools/ghidra/find-stick3-crypto.py +190 -0
  190. package/tools/ghidra/find-stick3-device.py +111 -0
  191. package/tools/ghidra/find-stick3a-ctor.py +87 -0
  192. package/tools/ghidra/find-stick3anet-ctor.py +76 -0
  193. package/tools/ghidra/find-vtables.py +165 -0
  194. package/tools/ghidra/out-011c-callers-run.log +51 -0
  195. package/tools/ghidra/out-011c-run.log +50 -0
  196. package/tools/ghidra/out-vt-key-run.log +55 -0
  197. package/tools/ghidra/string-walk.py +180 -0
  198. package/tools/hmac-key.sh +51 -0
  199. package/tools/hwm-entitlements.plist +18 -0
  200. package/tools/inspect-plaintext.mjs +87 -0
  201. package/tools/lldb-capture-aes.py +137 -0
  202. package/tools/lldb_hmac_key.py +93 -0
  203. package/tools/lldb_pathA_scan.py +341 -0
  204. package/tools/lldb_pathC_keytrace.py +300 -0
  205. package/tools/lldb_pathD_kdf_watch.py +281 -0
  206. package/tools/pathA-launch-hwm.sh +86 -0
  207. package/tools/pathA-scan.sh +52 -0
  208. package/tools/pathC-keytrace.sh +49 -0
  209. package/tools/pathC-run.sh +71 -0
  210. package/tools/pathD-kdf-watch.sh +51 -0
  211. package/tools/quick-trigger.mjs +86 -0
  212. package/tools/run-hooked-hwmanager.sh +51 -0
  213. package/tools/scan-aes-schedule.mjs +135 -0
  214. package/tools/send_dmx.mjs +480 -0
  215. package/tools/stick-status.mjs +57 -0
  216. package/tools/test-aes-detector.mjs +100 -0
  217. package/tools/test-tcp-write.ts +274 -0
  218. package/tools/test-udp-dmx.ts +324 -0
  219. package/tools/test-x25519.mjs +87 -0
  220. package/tools/try-decrypt.ts +144 -0
  221. package/tools/try-hardcoded-key.mjs +86 -0
  222. package/tools/try-hardcoded-key.ts +103 -0
  223. package/tools/try-key.mjs +103 -0
  224. package/tools/verify-011c-key.mjs +121 -0
  225. package/tools/verify-kdf.mjs +142 -0
  226. package/tsconfig.json +19 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,176 @@
1
+ # homebridge-wac-dmx
2
+
3
+ Homebridge plugin to control WAC Lighting CUBE Architectural DC-WD05 fixtures via a Nicolaudie Stick-DE3 DMX controller.
4
+
5
+ ## Architecture
6
+
7
+ - 18x WAC DC-WD05 fixtures on a DMX chain
8
+ - Nicolaudie Stick-DE3 wall-mount DMX controller at 192.168.96.2
9
+ - Stick-DE3 exposes a TCP interface (port 2431) that sends raw DMX values
10
+ - This plugin bridges HomeKit ↔ DMX via Homebridge
11
+
12
+ ## Fixture Configuration
13
+
14
+ - RDM Personality: 4 (Enhanced Tuning, 5 channels per fixture)
15
+ - DMX addresses: 1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 76, 81, 86
16
+
17
+ ### Physical fixture map
18
+
19
+ 9 positions (A–I) on the boat, each with an **up** and a **down** fixture
20
+ (9 × 2 = 18). Nautical terms (bow = front, stern = back, port/stbd) for clarity.
21
+
22
+ ```
23
+ stern
24
+ G H
25
+ I
26
+ stbd F D port
27
+ E B
28
+ C A
29
+ bow
30
+ ```
31
+
32
+ | DMX | Pos | DMX | Pos | DMX | Pos |
33
+ |-----|-----|-----|-----|-----|-----|
34
+ | 1 | B up | 31 | E down | 61 | I down |
35
+ | 6 | A down | 36 | C down | 66 | B down |
36
+ | 11 | E up | 41 | C up | 71 | I up |
37
+ | 16 | G up | 46 | H down | 76 | G down |
38
+ | 21 | H up | 51 | F up | 81 | D up |
39
+ | 26 | A up | 56 | F down | 86 | D down |
40
+
41
+ ### Channel Map (per fixture, 5 channels)
42
+
43
+ | Slot | Name | DMX Range | Notes |
44
+ |------|------------------|-----------|------------------------------|
45
+ | 0 | Intensity | 0-255 | Master dimmer (coarse) |
46
+ | 1 | Intensity (fine) | 0-255 | 16-bit fine dimmer |
47
+ | 2 | Color Temp | 0-255 | Maps to 1650K-8000K |
48
+ | 3 | Saturation | 0-255 | 0 = white/CCT mode |
49
+ | 4 | Hue | 0-255 | 0-360° mapped to 0-255 |
50
+
51
+ ## HomeKit ↔ DMX Mapping
52
+
53
+ HomeKit Lightbulb service characteristics:
54
+
55
+ | HomeKit Characteristic | Range | DMX Channel | Conversion |
56
+ |------------------------|------------|-------------|---------------------------------------------------------|
57
+ | On | bool | Ch 0 | on=last brightness, off=0 |
58
+ | Brightness | 0-100% | Ch 0 (+ Ch 1 for fine) | `Math.round(value * 2.55)` |
59
+ | Hue | 0-360° | Ch 4 | `Math.round(value * 255 / 360)` |
60
+ | Saturation | 0-100% | Ch 3 | `Math.round(value * 2.55)` |
61
+ | ColorTemperature | 50-400 mireds | Ch 2 | mireds→K: `1000000/mireds`, then scale 1650-8000K→0-255 |
62
+
63
+ When saturation is 0, the fixture is in white/CCT mode using the color temp channel.
64
+ When saturation > 0, it's in color mode using hue + saturation.
65
+
66
+ ## Stick-DE3 Network
67
+
68
+ - Static IP: 192.168.96.2 (on dedicated /30 VLAN, ID 96)
69
+ - The Stick changes its MAC address on every settings update (this is "a feature")
70
+
71
+ ### Protocol (observed via packet capture)
72
+
73
+ - **TCP/2431** — control + per-session crypto handshake. Client sends
74
+ `LSAG_ALL\x00\x00\x15\x00\x00\x00`; Stick replies
75
+ `Stick_3A\x00\xc9\x00DEFAULT\x00…` (carries device name/identity). Short
76
+ probe connections close in ~30 ms (normal); one long-lived control session.
77
+ - **UDP → 192.168.96.2:2431** (src port 2430) — the live DMX stream:
78
+ **576-byte frames = 32-byte clear header + 544-byte AES-128 body**
79
+ (CBC or CFB mode selectable by a flag in the cipher state; mode for the
80
+ live Stick3 path TBD). Custom-inlined AES (S-box @ `DAT_1007c0010`), not
81
+ Gladman library. See `tools/ghidra/README.md` and
82
+ `memory/stick-cipher-is-stream-not-aes.md` for the static-RE findings.
83
+ - **UDP/2430 broadcast** — device discovery only.
84
+
85
+ ### Required device setup (CRITICAL — fixes the multi-minute lag)
86
+
87
+ Symptom (now resolved): every DMX change — from any source (HWM faders, ESA
88
+ Pro 2, raw stream) — applied a **consistent ~2–3 min late, then snapped**.
89
+
90
+ Root cause: the Stick's **DMX output wire-timing** was set to absurd values
91
+ (MBB 15000 / Break 1000 / MAB 1500 / **MBS 400**). MBS is applied between all
92
+ 512 slots every frame, so the output engine emitted frames so slowly that live
93
+ input backed up in the device's FIFO for minutes. NOT a network/AES/show/fade
94
+ problem — all of those were investigated and ruled out.
95
+
96
+ Fix: **USB-connect Hardware Manager** → the DMX/fader screen → set
97
+ **Standard/Recommended**:
98
+
99
+ | Param | Good value |
100
+ |-------|-----------|
101
+ | MBB (Mark Before Break) | 100 |
102
+ | Break | 180 |
103
+ | MAB (Mark After Break) | 20 |
104
+ | MBS (Mark Between Slots) | 4 |
105
+
106
+ (≈ 40 Hz / ~348 fps figure shown). Lights then change instantly, over USB
107
+ **and** the network.
108
+
109
+ Gotchas:
110
+ - This DMX-timing screen exists **only in USB-connected Hardware Manager** —
111
+ it is NOT in the network config, and a **factory Reset does NOT fix it**
112
+ (Reset wipes network + showfile but not these toward sane values).
113
+ - After a Reset you must re-enter the static network *and* re-enable **WAN
114
+ access** for network HWM control.
115
+ - fw **3.08** is the newest *version*, but builds differ: installed build is
116
+ dated **2024-08-08**; a newer Hardware Manager (≈May 2026) bundles **3.08
117
+ built 2026-03-31** — a silent same-version rebuild (no changelog line will
118
+ reflect it). It *may* fix the absurd DMX-timing defaults and/or MAC/remote
119
+ bugs — unknown without Nicolaudie support or testing.
120
+ - **Landmine:** the device works only because the DMX timing was hand-fixed;
121
+ any factory Reset / settings corruption (it rewrites settings+MAC constantly)
122
+ reintroduces the absurd defaults and the ~2–3 min lag. A planned update to
123
+ the 2026-03-31 build *might* remove this permanently — do it deliberately,
124
+ not reactively, and **first archive the currently-installed Hardware Manager
125
+ app/installer**: it is the ONLY firmware-rollback path (Nicolaudie publishes
126
+ no old firmware). When updating: record config, flash, then read the DMX
127
+ screen *before* changing anything (defaults may already be sane).
128
+
129
+ ### Other notes
130
+
131
+ - Hardware Manager / ESA Pro 2 send a tame, steady **25 Hz** stream — *not* a
132
+ flood. A pacing relay/bridge was built, deployed, and **disproven**; do not
133
+ revisit it.
134
+ - First Connect after launching Hardware Manager often fails **XHL 17**; the
135
+ retry succeeds → stale single TCP/2431 session slot freed by the failed
136
+ attempt. The plugin should auto-retry and close its session cleanly.
137
+ - `tools/quick-trigger.mjs` — spec-verified UDP/2430 Quick Trigger (scene-level
138
+ only; not actioned on fw3.08 unless "Security for Cloud Access" is off).
139
+ `tools/stick-status.mjs` — reads the TCP/2431 status packet (Live-Mode flag).
140
+ - Per-fixture HomeKit control still requires sending the **encrypted 576-byte
141
+ DMX stream**; see `tools/ghidra/README.md` and
142
+ `memory/stick-cipher-is-stream-not-aes.md`. (The latency fix above is
143
+ independent and a prerequisite for that to be usable.)
144
+
145
+ ### Plugin architecture constraints (observed)
146
+
147
+ - **Latch IS supported — on clean disconnect** (corrected 2026-05-20; the
148
+ earlier "no latch / must stream" note was wrong). The Stick holds DMX
149
+ values after a *clean* TCP disconnect; output only drops when the session
150
+ is dirty-dropped (TCP timeout, process kill, the current 2025-09-23
151
+ EsaPro2 HWM's bugged disconnect). Implication: the plugin can be
152
+ **transactional** — connect → ECDH handshake → send DMX → clean
153
+ disconnect → Stick holds the value. No persistent streaming needed.
154
+ Trade-off: a few hundred ms wall-controller blink per change. See
155
+ `memory/stick-actually-latches-on-clean-disconnect.md`.
156
+ - **Single session, mutually exclusive with HWM.** Only one TCP/2431 session;
157
+ while Hardware Manager is connected the Stick is unusable by anything else
158
+ (and vice-versa). Fine for production — HWM is commissioning-only; the
159
+ plugin owns the session at runtime. They cannot run simultaneously.
160
+ - **Graceful-fallback option:** load a real (non-empty) standalone show with a
161
+ sensible "lights on" look so a stream/plugin outage degrades to that look
162
+ instead of full blackout. Trade-off vs. the empty-show baseline.
163
+ - **Hard dependency:** the continuous stream is the **encrypted 576-byte DMX**
164
+ (32-byte clear header + 544-byte AES-128 body, custom-inlined AES with
165
+ S-box at `DAT_1007c0010`). Mode is CBC or CFB, selected by a byte in the
166
+ cipher state (live Stick3 path mode TBD). Quick Trigger is scene-level,
167
+ can't do per-fixture. So the persistent sender is blocked on (1) recovering
168
+ the per-session AES key from the TCP/2431 handshake and (2) the first-block
169
+ IV setup. See `tools/ghidra/README.md` and
170
+ `memory/stick-cipher-is-stream-not-aes.md`.
171
+
172
+ ## Reference
173
+
174
+ - Existing plugin by same author for style reference: https://github.com/ecopoesis/homebridge-unifi-ap-rgb/tree/main
175
+ - WAC fixture spec: RGBWW, 1650K-8000K CCT range
176
+ - Manufacturer ID (RDM): 0x0776
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # homebridge-dmx
2
+
3
+ Homebridge platform plugin that exposes DMX fixtures as HomeKit Lightbulb
4
+ accessories. The DMX controller is abstracted so additional controllers can be
5
+ plugged in without rewriting your patch; the supported controller today is the
6
+ **Nicolaudie Stick-DE3** (TCP/2431 + AES-256-CBC-encrypted 576-byte UDP DMX
7
+ stream, reverse-engineered from the ESA2 Hardware Manager).
8
+
9
+ [![npm](https://img.shields.io/npm/v/@ecopoesis/homebridge-dmx.svg)](https://www.npmjs.com/package/@ecopoesis/homebridge-dmx)
10
+
11
+ ## Status
12
+
13
+ | Piece | State |
14
+ | --- | --- |
15
+ | Stick-DE3 control (auth, ECDH, encrypted DMX stream) | ✅ working |
16
+ | Color models: dimmer, cct, rgb, rgbw, rgbww, rgbaw, hsvcct | ✅ |
17
+ | YAML and Homebridge-UI config | ✅ |
18
+ | Multiple controllers in one patch | ✅ (only `StickDE3` type supported today) |
19
+ | Universe B on the Stick | 🚧 plumbed but the wire encoding is unknown |
20
+ | Stick-DE3 password / "Cloud Access" auth | 🚧 not implemented |
21
+ | State read-back | 🚧 Homebridge holds the canonical state; local drift accepted |
22
+
23
+ ## How it works
24
+
25
+ Each HomeKit change is rendered into DMX bytes by the fixture's color model
26
+ (HSV → channel values for the appropriate model), written into a per-universe
27
+ buffer, and dispatched via a debounced controller. After 750 ms of quiet (or
28
+ during a slider drag, continuously), the controller spawns
29
+ **`tools/send_dmx.mjs`** as a child process which runs the full Stick-DE3
30
+ handshake + 750 ms of 25 Hz encrypted UDP frames + a dirty close (RST → Stick
31
+ latches the last values).
32
+
33
+ The subprocess-per-transaction architecture is an empirical workaround:
34
+ running the same protocol in-process in a long-lived plugin only worked for
35
+ the first transaction; sessions 2+ silently failed to drive output. Running
36
+ each transaction in a fresh node process bypasses whatever in-process state
37
+ poisons subsequent sessions. Each transaction costs ~1.5–2 s total.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install -g @ecopoesis/homebridge-dmx
43
+ ```
44
+
45
+ Or, in the Homebridge UI: **Plugins** → search for "DMX" → install.
46
+
47
+ ## Config
48
+
49
+ Two flavours: inline JSON via the Homebridge UI, or external YAML pointed to by
50
+ `yamlPath`. YAML is recommended for anything beyond a couple of fixtures.
51
+
52
+ Minimal inline config:
53
+
54
+ ```json
55
+ "platforms": [
56
+ {
57
+ "platform": "DMX",
58
+ "name": "DMX",
59
+ "controllers": [
60
+ { "id": "main", "type": "StickDE3", "ip": "192.168.96.2" }
61
+ ],
62
+ "profiles": [
63
+ {
64
+ "name": "WAC",
65
+ "colormodel": "hsvcct",
66
+ "channel_order": [
67
+ "Intensity", "Intensity (Fine)",
68
+ "ColorTemp 1650-8000",
69
+ "Saturation", "Hue"
70
+ ]
71
+ }
72
+ ],
73
+ "patch": [
74
+ { "id": "a_down", "name": "A Down", "type": "WAC", "controller": "main", "start": 6 }
75
+ ]
76
+ }
77
+ ]
78
+ ```
79
+
80
+ External YAML form (recommended):
81
+
82
+ ```json
83
+ "platforms": [
84
+ { "platform": "DMX", "yamlPath": "/homebridge/dmx.yaml" }
85
+ ]
86
+ ```
87
+
88
+ See [`examples/dmx.yaml`](examples/dmx.yaml) for a starting point.
89
+
90
+ ## Color models
91
+
92
+ | Model | Channels | HomeKit characteristics |
93
+ | --- | --- | --- |
94
+ | `dimmer` | 1 (Intensity, +optional Fine) | On, Brightness |
95
+ | `cct` | 2 (Intensity, ColorTemp `KMin-KMax`) | On, Brightness, ColorTemperature |
96
+ | `rgb` | 3 (Red, Green, Blue — any order) | On, Brightness, Hue, Saturation |
97
+ | `rgbw` | 4 (+ White; W = min(R,G,B)) | On, Brightness, Hue, Saturation |
98
+ | `rgbww` | 5 (+ WarmWhite, CoolWhite — split by CCT) | On, Brightness, Hue, Saturation, ColorTemperature |
99
+ | `rgbaw` | 5 (+ Amber, White) | On, Brightness, Hue, Saturation |
100
+ | `hsvcct` | 5 (Intensity, Intensity (Fine), ColorTemp, Saturation, Hue) | All five characteristics; HomeKit-native (the WAC DC-WD05 layout) |
101
+
102
+ Channel-name syntax:
103
+
104
+ - `(Fine)` appended → 16-bit companion to the preceding channel
105
+ (e.g. `Intensity (Fine)` is the low byte of a 16-bit intensity).
106
+ - CCT channels carry a Kelvin range: `ColorTemp 1650-8000`,
107
+ `WarmWhite 2700`, `CoolWhite 6500`.
108
+ - Names are case-insensitive and recognise common aliases (`R`/`Red`,
109
+ `WW`/`Warm White`/`WarmWhite`, etc.).
110
+
111
+ ## CLI
112
+
113
+ A `stick` CLI is included for hand-testing without Homebridge:
114
+
115
+ ```bash
116
+ # Set channel 6 of universe A to 255 on a Stick at 192.168.96.2
117
+ npx -p @ecopoesis/homebridge-dmx stick 192.168.96.2 uA,6=255
118
+
119
+ # Named fixture from your YAML
120
+ stick --config ~/.config/dmx.yaml 192.168.96.2 a_down=#ff8800
121
+ ```
122
+
123
+ Value forms: `#rrggbb`, `#rgb`, `hsl(120, 100%, 50%)`, `hwb(180 10% 20%)`,
124
+ `rgb(255,128,0)`, `kelvin(2700)`, `0..255` (dimmer profiles), `on`, `off`,
125
+ `50%`.
126
+
127
+ ## Required Stick-DE3 device setup
128
+
129
+ Brand-new (or factory-reset) Stick-DE3 devices ship with pathological DMX
130
+ output timing. Symptom: every DMX change applies ~2–3 minutes late then snaps.
131
+ Fix via **USB-connected Hardware Manager** → DMX/fader screen → set
132
+ **Standard/Recommended** (MBB 100 / Break 180 / MAB 20 / MBS 4). This screen
133
+ is USB-only; a factory Reset does NOT fix it. See `CLAUDE.md` →
134
+ "Required device setup" for the full landmine.
135
+
136
+ ## Single-session lock
137
+
138
+ While the plugin is mid-transaction (~1.5 s per change), the Stick is unusable
139
+ from Hardware Manager (single TCP/2431 session, mutually exclusive). For an
140
+ idle Homebridge the Stick is free for HWM commissioning at all other times.
141
+ The latch model (clean disconnect leaves the last value lit) is what lets us
142
+ be polite about this.
143
+
144
+ ## License
145
+
146
+ Apache-2.0
@@ -0,0 +1,143 @@
1
+ {
2
+ "pluginAlias": "DMX",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Homebridge platform for DMX controllers. Currently supports the Nicolaudie Stick-DE3; the controller abstraction is structured so additional DMX controllers can be added later without rewriting your patch.",
6
+ "footerDisplay": "For larger setups, point **yamlPath** at an external YAML file containing the controllers + profiles + patch. The YAML replaces the inline JSON below entirely.",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Platform Name",
12
+ "type": "string",
13
+ "default": "DMX",
14
+ "required": false
15
+ },
16
+ "yamlPath": {
17
+ "title": "YAML config path (optional)",
18
+ "type": "string",
19
+ "description": "Absolute or relative path to a YAML file containing the full config. When set, the YAML replaces the JSON fields below.",
20
+ "required": false
21
+ },
22
+ "controllers": {
23
+ "title": "DMX Controllers",
24
+ "type": "array",
25
+ "required": false,
26
+ "description": "One entry per physical controller. Patch entries below reference these by id.",
27
+ "items": {
28
+ "type": "object",
29
+ "properties": {
30
+ "id": {
31
+ "title": "Controller id",
32
+ "type": "string",
33
+ "required": true,
34
+ "description": "kebab-case identifier; referenced from patch entries."
35
+ },
36
+ "type": {
37
+ "title": "Controller type",
38
+ "type": "string",
39
+ "required": true,
40
+ "default": "StickDE3",
41
+ "oneOf": [
42
+ { "title": "Nicolaudie Stick-DE3", "enum": ["StickDE3"] }
43
+ ]
44
+ },
45
+ "ip": {
46
+ "title": "IP address",
47
+ "type": "string",
48
+ "format": "ipv4",
49
+ "required": true,
50
+ "description": "Static IP of the controller on its DMX VLAN (e.g. 192.168.96.2). Discovery is not supported."
51
+ }
52
+ }
53
+ }
54
+ },
55
+ "profiles": {
56
+ "title": "Fixture Profiles",
57
+ "type": "array",
58
+ "required": false,
59
+ "items": {
60
+ "type": "object",
61
+ "properties": {
62
+ "name": {
63
+ "title": "Profile name",
64
+ "type": "string",
65
+ "required": true,
66
+ "description": "Used as the 'type' in patch entries below."
67
+ },
68
+ "colormodel": {
69
+ "title": "Color model",
70
+ "type": "string",
71
+ "required": true,
72
+ "oneOf": [
73
+ { "title": "Dimmer (1ch)", "enum": ["dimmer"] },
74
+ { "title": "CCT (2ch intensity + colortemp)", "enum": ["cct"] },
75
+ { "title": "RGB (3ch)", "enum": ["rgb"] },
76
+ { "title": "RGBW (4ch + white)", "enum": ["rgbw"] },
77
+ { "title": "RGBWW (5ch + warm/cool white)", "enum": ["rgbww"] },
78
+ { "title": "RGBAW (5ch + amber + white)", "enum": ["rgbaw"] },
79
+ { "title": "HSV+CCT (5ch — WAC DC-WD05 native)", "enum": ["hsvcct"] }
80
+ ]
81
+ },
82
+ "channel_order": {
83
+ "title": "Channel order",
84
+ "type": "array",
85
+ "required": true,
86
+ "description": "Names in DMX-slot order. Append ' (Fine)' for a 16-bit companion, e.g. 'Intensity (Fine)'. CCT channels include a K range: 'ColorTemp 1650-8000'.",
87
+ "items": { "type": "string" }
88
+ }
89
+ }
90
+ }
91
+ },
92
+ "patch": {
93
+ "title": "Patch",
94
+ "type": "array",
95
+ "required": false,
96
+ "items": {
97
+ "type": "object",
98
+ "properties": {
99
+ "id": {
100
+ "title": "Fixture id",
101
+ "type": "string",
102
+ "required": true,
103
+ "description": "kebab-case identifier; the fixture's stable handle."
104
+ },
105
+ "name": {
106
+ "title": "HomeKit name",
107
+ "type": "string",
108
+ "required": true
109
+ },
110
+ "type": {
111
+ "title": "Profile",
112
+ "type": "string",
113
+ "required": true,
114
+ "description": "Must match a Profile name above."
115
+ },
116
+ "controller": {
117
+ "title": "Controller",
118
+ "type": "string",
119
+ "required": false,
120
+ "description": "Controller id this fixture is wired to. Optional when only one controller is configured."
121
+ },
122
+ "universe": {
123
+ "title": "DMX universe",
124
+ "type": "string",
125
+ "default": "A",
126
+ "oneOf": [
127
+ { "title": "A", "enum": ["A"] },
128
+ { "title": "B (untested)", "enum": ["B"] }
129
+ ]
130
+ },
131
+ "start": {
132
+ "title": "DMX start channel",
133
+ "type": "integer",
134
+ "minimum": 1,
135
+ "maximum": 512,
136
+ "required": true
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=stick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stick.d.ts","sourceRoot":"","sources":["../../src/cli/stick.ts"],"names":[],"mappings":""}
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // stick — set DMX channels on a Stick-DE3, transactionally.
3
+ //
4
+ // stick [--config <yaml>] <ip> <assignment> [<assignment>...]
5
+ //
6
+ // Assignment forms:
7
+ // <fixture-id>=<value> named fixture from config
8
+ // value: "#FFFFFF", "hsl(120,50%,50%)",
9
+ // "hwb(180 10% 20%)", "rgb(255,128,0)",
10
+ // "kelvin(2700)", "50%", "128", "on", "off"
11
+ // u<universe>,<channel>=<byte> raw single-channel write (e.g. "uA,6=255")
12
+ //
13
+ // Env knobs (same as the StickSession opts):
14
+ // STREAM_MS, CHATTER_MS, SETTLE_2E_MS, FRAME_HZ, SECTORS, RUN_PROBE,
15
+ // PROBE_GAP_MS, KEY_DUMP
16
+ //
17
+ // Config search order if --config not given:
18
+ // $STICK_CONFIG, ./stick-de3.yaml, ~/.config/stick-de3.yaml.
19
+ // A config is OPTIONAL when ONLY raw u<universe>,<channel>=<byte> assignments
20
+ // are used (and the ip is on the command line).
21
+ import fs from 'node:fs';
22
+ import os from 'node:os';
23
+ import path from 'node:path';
24
+ import yaml from 'js-yaml';
25
+ import { loadConfig } from '../config.js';
26
+ import { parseValue } from '../color/parsers.js';
27
+ import { StickSession } from '../stick/session.js';
28
+ function findDefaultConfig() {
29
+ const candidates = [
30
+ process.env.STICK_CONFIG,
31
+ path.resolve(process.cwd(), 'stick-de3.yaml'),
32
+ path.resolve(os.homedir(), '.config/stick-de3.yaml'),
33
+ ].filter(Boolean);
34
+ for (const c of candidates) {
35
+ try {
36
+ fs.accessSync(c, fs.constants.R_OK);
37
+ return c;
38
+ }
39
+ catch { /* miss */ }
40
+ }
41
+ return null;
42
+ }
43
+ function usage() {
44
+ console.error('usage: stick [--config <yaml>] <ip> <assignment> [<assignment>...]\n' +
45
+ ' assignments:\n' +
46
+ ' <fixture-id>=<value> named fixture (config required)\n' +
47
+ ' u<universe>,<channel>=<n> raw, e.g. uA,6=255');
48
+ process.exit(1);
49
+ }
50
+ function parseArgs(argv) {
51
+ let configPath = null;
52
+ const rest = [];
53
+ for (let i = 0; i < argv.length; i++) {
54
+ if (argv[i] === '--config' || argv[i] === '-c') {
55
+ configPath = argv[++i];
56
+ }
57
+ else if (argv[i] === '-h' || argv[i] === '--help') {
58
+ usage();
59
+ }
60
+ else {
61
+ rest.push(argv[i]);
62
+ }
63
+ }
64
+ if (rest.length < 2)
65
+ usage();
66
+ return { configPath, ip: rest[0], assigns: rest.slice(1) };
67
+ }
68
+ function loadOptionalConfig(configPath, ip) {
69
+ const p = configPath ?? findDefaultConfig();
70
+ if (!p)
71
+ return null;
72
+ const cwd = path.dirname(p);
73
+ const text = fs.readFileSync(p, 'utf8');
74
+ const raw = p.endsWith('.yaml') || p.endsWith('.yml') ? yaml.load(text) : JSON.parse(text);
75
+ // CLI convenience: if the config doesn't define controllers, synthesize
76
+ // one from the cmdline ip (or a legacy top-level `ip:` field).
77
+ if (!raw.controllers || raw.controllers.length === 0) {
78
+ raw.controllers = [{ id: 'cli', type: 'StickDE3', ip: raw.ip ?? ip }];
79
+ }
80
+ return loadConfig(raw, cwd);
81
+ }
82
+ // Build a Map<universe, Uint8Array(512)> of channel targets from CLI args.
83
+ function resolveAssignments(assigns, cfg) {
84
+ const universes = new Map();
85
+ const slot = (u) => {
86
+ let arr = universes.get(u);
87
+ if (!arr) {
88
+ arr = new Uint8Array(512);
89
+ universes.set(u, arr);
90
+ }
91
+ return arr;
92
+ };
93
+ for (const a of assigns) {
94
+ const eq = a.indexOf('=');
95
+ if (eq < 0)
96
+ throw new Error(`bad assignment "${a}": expected key=value`);
97
+ const lhs = a.slice(0, eq).trim();
98
+ const rhs = a.slice(eq + 1).trim();
99
+ // Raw: u<universe>,<channel>=<byte>
100
+ const raw = /^u([ab]),(\d+)$/i.exec(lhs);
101
+ if (raw) {
102
+ const u = raw[1].toUpperCase() === 'A' ? 0 : 1;
103
+ const ch = Number(raw[2]);
104
+ const byte = Number(rhs);
105
+ if (!Number.isInteger(ch) || ch < 1 || ch > 512) {
106
+ throw new Error(`raw "${a}": channel out of 1..512`);
107
+ }
108
+ if (!Number.isInteger(byte) || byte < 0 || byte > 255) {
109
+ throw new Error(`raw "${a}": value out of 0..255`);
110
+ }
111
+ slot(u)[ch - 1] = byte;
112
+ continue;
113
+ }
114
+ // Named fixture
115
+ if (!cfg) {
116
+ throw new Error(`assignment "${a}" needs a config; use --config or set STICK_CONFIG`);
117
+ }
118
+ const fx = cfg.fixtures.find((f) => f.id === lhs);
119
+ if (!fx) {
120
+ throw new Error(`unknown fixture id "${lhs}". Known: ${cfg.fixtures.map((f) => f.id).join(', ')}`);
121
+ }
122
+ const state = parseValue(rhs);
123
+ const bytes = fx.profile.model.render(state, fx.profile.channels);
124
+ const arr = slot(fx.universe);
125
+ for (let i = 0; i < bytes.length; i++) {
126
+ arr[fx.startCh - 1 + i] = bytes[i];
127
+ }
128
+ }
129
+ return universes;
130
+ }
131
+ async function main() {
132
+ const { configPath, ip, assigns } = parseArgs(process.argv.slice(2));
133
+ const cfg = loadOptionalConfig(configPath, ip);
134
+ // CLI ip arg overrides config ip if both present
135
+ const target = ip;
136
+ const universes = resolveAssignments(assigns, cfg);
137
+ if (universes.size === 0) {
138
+ console.error('no channels to set');
139
+ process.exit(1);
140
+ }
141
+ // Summarise what we're sending
142
+ for (const [u, arr] of universes) {
143
+ const lit = [...arr.entries()].filter(([, v]) => v).map(([i, v]) => `ch${i + 1}=${v}`);
144
+ console.log(` universe ${u ? 'B' : 'A'}: ${lit.length} channel${lit.length === 1 ? '' : 's'}: ${lit.join(' ')}`);
145
+ }
146
+ const session = await StickSession.connect(target, {
147
+ log: (...a) => console.log(' ', ...a),
148
+ });
149
+ // Optional key dump (for offline pcap decryption)
150
+ try {
151
+ const keyPath = process.env.KEY_DUMP || `${os.homedir()}/.send_dmx-key.txt`;
152
+ // Access the key indirectly: it's a private field of StickSession, so we
153
+ // dump the most recent value we know about. Since we don't expose it,
154
+ // skip the dump here — the diagnostic tools in tools/ already do this.
155
+ void keyPath;
156
+ }
157
+ catch { /* silent */ }
158
+ const STREAM_MS = Number(process.env.STREAM_MS || 750);
159
+ const FRAME_HZ = Number(process.env.FRAME_HZ || 25);
160
+ const frameMs = Math.max(1, Math.round(1000 / FRAME_HZ));
161
+ console.log(` streaming at ${FRAME_HZ} Hz (${frameMs}ms) for ${STREAM_MS}ms`);
162
+ let nFrames = 0;
163
+ const timer = setInterval(() => {
164
+ for (const [u, arr] of universes) {
165
+ session.send(arr, u);
166
+ nFrames++;
167
+ }
168
+ }, frameMs);
169
+ await new Promise((res) => setTimeout(res, STREAM_MS));
170
+ clearInterval(timer);
171
+ console.log(` streamed ${nFrames} frames`);
172
+ session.destroy();
173
+ console.log(' disconnected (RST) — Stick should hold the last value');
174
+ }
175
+ main().catch((e) => { console.error('failed:', e); process.exit(1); });
176
+ //# sourceMappingURL=stick.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stick.js","sourceRoot":"","sources":["../../src/cli/stick.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { ColorModel } from './types.js';
2
+ export declare const cct: ColorModel;
3
+ //# sourceMappingURL=cct.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cct.d.ts","sourceRoot":"","sources":["../../src/color/cct.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,UAAU,EAMX,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,GAAG,EAAE,UA0BjB,CAAC"}