@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.
- package/CLAUDE.md +176 -0
- package/README.md +146 -0
- package/config.schema.json +143 -0
- package/dist/cli/stick.d.ts +3 -0
- package/dist/cli/stick.d.ts.map +1 -0
- package/dist/cli/stick.js +176 -0
- package/dist/cli/stick.js.map +1 -0
- package/dist/color/cct.d.ts +3 -0
- package/dist/color/cct.d.ts.map +1 -0
- package/dist/color/cct.js +33 -0
- package/dist/color/cct.js.map +1 -0
- package/dist/color/dimmer.d.ts +3 -0
- package/dist/color/dimmer.d.ts.map +1 -0
- package/dist/color/dimmer.js +16 -0
- package/dist/color/dimmer.js.map +1 -0
- package/dist/color/hsvcct.d.ts +3 -0
- package/dist/color/hsvcct.d.ts.map +1 -0
- package/dist/color/hsvcct.js +52 -0
- package/dist/color/hsvcct.js.map +1 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.d.ts.map +1 -0
- package/dist/color/index.js +28 -0
- package/dist/color/index.js.map +1 -0
- package/dist/color/parsers.d.ts +20 -0
- package/dist/color/parsers.d.ts.map +1 -0
- package/dist/color/parsers.js +232 -0
- package/dist/color/parsers.js.map +1 -0
- package/dist/color/rgb.d.ts +3 -0
- package/dist/color/rgb.d.ts.map +1 -0
- package/dist/color/rgb.js +32 -0
- package/dist/color/rgb.js.map +1 -0
- package/dist/color/rgbaw.d.ts +3 -0
- package/dist/color/rgbaw.d.ts.map +1 -0
- package/dist/color/rgbaw.js +48 -0
- package/dist/color/rgbaw.js.map +1 -0
- package/dist/color/rgbw.d.ts +3 -0
- package/dist/color/rgbw.d.ts.map +1 -0
- package/dist/color/rgbw.js +37 -0
- package/dist/color/rgbw.js.map +1 -0
- package/dist/color/rgbww.d.ts +3 -0
- package/dist/color/rgbww.d.ts.map +1 -0
- package/dist/color/rgbww.js +54 -0
- package/dist/color/rgbww.js.map +1 -0
- package/dist/color/types.d.ts +64 -0
- package/dist/color/types.d.ts.map +1 -0
- package/dist/color/types.js +91 -0
- package/dist/color/types.js.map +1 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +191 -0
- package/dist/config.js.map +1 -0
- package/dist/controller.d.ts +32 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +128 -0
- package/dist/controller.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +96 -0
- package/dist/platform.js.map +1 -0
- package/dist/platformAccessory.d.ts +18 -0
- package/dist/platformAccessory.d.ts.map +1 -0
- package/dist/platformAccessory.js +81 -0
- package/dist/platformAccessory.js.map +1 -0
- package/dist/settings.d.ts +14 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +15 -0
- package/dist/settings.js.map +1 -0
- package/dist/stick/frame.d.ts +16 -0
- package/dist/stick/frame.d.ts.map +1 -0
- package/dist/stick/frame.js +51 -0
- package/dist/stick/frame.js.map +1 -0
- package/dist/stick/kdf.d.ts +23 -0
- package/dist/stick/kdf.d.ts.map +1 -0
- package/dist/stick/kdf.js +90 -0
- package/dist/stick/kdf.js.map +1 -0
- package/dist/stick/protocol.d.ts +28 -0
- package/dist/stick/protocol.d.ts.map +1 -0
- package/dist/stick/protocol.js +78 -0
- package/dist/stick/protocol.js.map +1 -0
- package/dist/stick/session.d.ts +50 -0
- package/dist/stick/session.d.ts.map +1 -0
- package/dist/stick/session.js +301 -0
- package/dist/stick/session.js.map +1 -0
- package/examples/dmx.yaml +65 -0
- package/package.json +51 -0
- package/src/cli/stick.ts +198 -0
- package/src/color/cct.ts +40 -0
- package/src/color/dimmer.ts +24 -0
- package/src/color/hsvcct.ts +59 -0
- package/src/color/index.ts +34 -0
- package/src/color/parsers.ts +232 -0
- package/src/color/rgb.ts +33 -0
- package/src/color/rgbaw.ts +46 -0
- package/src/color/rgbw.ts +36 -0
- package/src/color/rgbww.ts +56 -0
- package/src/color/types.ts +150 -0
- package/src/config.ts +248 -0
- package/src/controller.ts +136 -0
- package/src/index.ts +9 -0
- package/src/platform.ts +118 -0
- package/src/platformAccessory.ts +102 -0
- package/src/settings.ts +18 -0
- package/src/stick/frame.ts +70 -0
- package/src/stick/kdf.ts +99 -0
- package/src/stick/protocol.ts +90 -0
- package/src/stick/session.ts +348 -0
- package/stick-de3.json +6 -0
- package/tools/aes-hook.c +1404 -0
- package/tools/analyze-pcap.mjs +143 -0
- package/tools/capture-aes-key.lldb +25 -0
- package/tools/capture-and-verify.sh +46 -0
- package/tools/capture-handshake.sh +59 -0
- package/tools/capture-send.sh +39 -0
- package/tools/crack-kdf.mjs +121 -0
- package/tools/crack-key.mjs +170 -0
- package/tools/decode-frame.mjs +110 -0
- package/tools/decrypt-011c.mjs +123 -0
- package/tools/decrypt-dmx.mjs +130 -0
- package/tools/decrypt-ssl2.ts +154 -0
- package/tools/derive-dmx-key.mjs +189 -0
- package/tools/diff-011c.mjs +127 -0
- package/tools/dump-pcap-summary.mjs +54 -0
- package/tools/dump-tcp-stream.mjs +123 -0
- package/tools/extract-011c.mjs +117 -0
- package/tools/extract-frame.mjs +78 -0
- package/tools/find-011c-key.mjs +140 -0
- package/tools/find-iv.mjs +153 -0
- package/tools/ghidra/README.md +136 -0
- package/tools/ghidra/__pycache__/dump-011c-callers.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-011c-handler.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-aes-candidates.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-anchors.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-auth2.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-authenticate.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-block-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-ctor-chain.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-curve.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-encompassing.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-handshake.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-kdf-chain.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-kdf-context.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-profile.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-real-encrypt.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-secret.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-setsoftware.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-stick3-vtable.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-swxref.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/dump-vt-key-getter.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-cipher.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-md5.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-secret-users.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3-crypto.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3-device.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3a-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-stick3anet-ctor.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/find-vtables.cpython-313.pyc +0 -0
- package/tools/ghidra/__pycache__/string-walk.cpython-313.pyc +0 -0
- package/tools/ghidra/dump-011c-callers.py +140 -0
- package/tools/ghidra/dump-011c-handler.py +158 -0
- package/tools/ghidra/dump-aes-candidates.py +99 -0
- package/tools/ghidra/dump-anchors.py +192 -0
- package/tools/ghidra/dump-auth2.py +50 -0
- package/tools/ghidra/dump-authenticate.py +71 -0
- package/tools/ghidra/dump-block-cipher.py +88 -0
- package/tools/ghidra/dump-cipher.py +106 -0
- package/tools/ghidra/dump-ctor-chain.py +107 -0
- package/tools/ghidra/dump-curve.py +112 -0
- package/tools/ghidra/dump-encompassing.py +146 -0
- package/tools/ghidra/dump-handshake.py +76 -0
- package/tools/ghidra/dump-kdf-chain.py +105 -0
- package/tools/ghidra/dump-kdf-context.py +72 -0
- package/tools/ghidra/dump-profile.py +59 -0
- package/tools/ghidra/dump-real-encrypt.py +90 -0
- package/tools/ghidra/dump-secret.py +84 -0
- package/tools/ghidra/dump-setsoftware.py +63 -0
- package/tools/ghidra/dump-stick3-vtable.py +182 -0
- package/tools/ghidra/dump-swxref.py +27 -0
- package/tools/ghidra/dump-vt-key-getter.py +170 -0
- package/tools/ghidra/find-cipher.py +210 -0
- package/tools/ghidra/find-ctor.py +109 -0
- package/tools/ghidra/find-md5.py +97 -0
- package/tools/ghidra/find-secret-users.py +98 -0
- package/tools/ghidra/find-stick3-crypto.py +190 -0
- package/tools/ghidra/find-stick3-device.py +111 -0
- package/tools/ghidra/find-stick3a-ctor.py +87 -0
- package/tools/ghidra/find-stick3anet-ctor.py +76 -0
- package/tools/ghidra/find-vtables.py +165 -0
- package/tools/ghidra/out-011c-callers-run.log +51 -0
- package/tools/ghidra/out-011c-run.log +50 -0
- package/tools/ghidra/out-vt-key-run.log +55 -0
- package/tools/ghidra/string-walk.py +180 -0
- package/tools/hmac-key.sh +51 -0
- package/tools/hwm-entitlements.plist +18 -0
- package/tools/inspect-plaintext.mjs +87 -0
- package/tools/lldb-capture-aes.py +137 -0
- package/tools/lldb_hmac_key.py +93 -0
- package/tools/lldb_pathA_scan.py +341 -0
- package/tools/lldb_pathC_keytrace.py +300 -0
- package/tools/lldb_pathD_kdf_watch.py +281 -0
- package/tools/pathA-launch-hwm.sh +86 -0
- package/tools/pathA-scan.sh +52 -0
- package/tools/pathC-keytrace.sh +49 -0
- package/tools/pathC-run.sh +71 -0
- package/tools/pathD-kdf-watch.sh +51 -0
- package/tools/quick-trigger.mjs +86 -0
- package/tools/run-hooked-hwmanager.sh +51 -0
- package/tools/scan-aes-schedule.mjs +135 -0
- package/tools/send_dmx.mjs +480 -0
- package/tools/stick-status.mjs +57 -0
- package/tools/test-aes-detector.mjs +100 -0
- package/tools/test-tcp-write.ts +274 -0
- package/tools/test-udp-dmx.ts +324 -0
- package/tools/test-x25519.mjs +87 -0
- package/tools/try-decrypt.ts +144 -0
- package/tools/try-hardcoded-key.mjs +86 -0
- package/tools/try-hardcoded-key.ts +103 -0
- package/tools/try-key.mjs +103 -0
- package/tools/verify-011c-key.mjs +121 -0
- package/tools/verify-kdf.mjs +142 -0
- 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
|
+
[](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 @@
|
|
|
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 @@
|
|
|
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"}
|