@ecopoesis/homebridge-dmx 0.5.5 → 0.6.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 +1 -0
- package/README.md +67 -0
- package/config.schema.json +104 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/controller.d.ts +3 -2
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +7 -4
- package/dist/controller.js.map +1 -1
- package/dist/dmxController.d.ts +9 -1
- package/dist/dmxController.d.ts.map +1 -1
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +37 -2
- package/dist/platform.js.map +1 -1
- package/dist/sacn.d.ts +3 -2
- package/dist/sacn.d.ts.map +1 -1
- package/dist/sacn.js +7 -4
- package/dist/sacn.js.map +1 -1
- package/dist/settings.d.ts +13 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +14 -0
- package/dist/settings.js.map +1 -1
- package/dist/show/colors.d.ts +25 -0
- package/dist/show/colors.d.ts.map +1 -0
- package/dist/show/colors.js +216 -0
- package/dist/show/colors.js.map +1 -0
- package/dist/show/dsl.d.ts +56 -0
- package/dist/show/dsl.d.ts.map +1 -0
- package/dist/show/dsl.js +326 -0
- package/dist/show/dsl.js.map +1 -0
- package/dist/show/engine.d.ts +50 -0
- package/dist/show/engine.d.ts.map +1 -0
- package/dist/show/engine.js +264 -0
- package/dist/show/engine.js.map +1 -0
- package/dist/showAccessory.d.ts +9 -0
- package/dist/showAccessory.d.ts.map +1 -0
- package/dist/showAccessory.js +33 -0
- package/dist/showAccessory.js.map +1 -0
- package/dist/stateRegistry.d.ts +8 -3
- package/dist/stateRegistry.d.ts.map +1 -1
- package/dist/stateRegistry.js +4 -4
- package/dist/stateRegistry.js.map +1 -1
- package/examples/dmx.yaml +76 -0
- package/package.json +1 -1
- package/src/config.ts +22 -1
- package/src/controller.ts +8 -5
- package/src/dmxController.ts +10 -1
- package/src/platform.ts +35 -2
- package/src/sacn.ts +8 -4
- package/src/settings.ts +19 -0
- package/src/show/colors.ts +224 -0
- package/src/show/dsl.ts +370 -0
- package/src/show/engine.ts +280 -0
- package/src/showAccessory.ts +46 -0
- package/src/stateRegistry.ts +11 -5
package/dist/show/dsl.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Show DSL — parameterized step lists, resolved at config load.
|
|
2
|
+
//
|
|
3
|
+
// Vocabulary borrows from the DMX chaser convention shared by QLC+, the
|
|
4
|
+
// Nicolaudie ESA/Sunlite scene editor and node-dmx's Animation: a show is
|
|
5
|
+
// a list of STEPS; each step names a LOOK (targets → colors) and the FADE
|
|
6
|
+
// time to crossfade into it plus a HOLD time to sit on it. Unmentioned
|
|
7
|
+
// targets TRACK their previous value (theatrical cue-list tracking). When
|
|
8
|
+
// the last step ends the show loops back to the first (default) or stops.
|
|
9
|
+
//
|
|
10
|
+
// palette:
|
|
11
|
+
// surprise pink: "#f2a1ff" # optional user color names
|
|
12
|
+
//
|
|
13
|
+
// shows:
|
|
14
|
+
// - id: crossfade # template: params without defaults
|
|
15
|
+
// params: { a: ~, b: ~, secs: 10 } # → no HomeKit switch
|
|
16
|
+
// steps:
|
|
17
|
+
// - look: { up: $a, down: $b }
|
|
18
|
+
// fade: $secs
|
|
19
|
+
// - look: { up: $b, down: $a }
|
|
20
|
+
// fade: $secs
|
|
21
|
+
//
|
|
22
|
+
// - id: texture # instance: all params bound
|
|
23
|
+
// name: Texture # → one HomeKit Switch
|
|
24
|
+
// extends: crossfade
|
|
25
|
+
// params: { a: indigo, b: surprise pink, secs: 20 }
|
|
26
|
+
//
|
|
27
|
+
// Look targets are fixture ids, zone ids, group ids (top-level `groups:`,
|
|
28
|
+
// named fixture sets with no HomeKit tile) or `all`; a key may list several
|
|
29
|
+
// separated by commas. Later keys override earlier ones. `$params` are
|
|
30
|
+
// substituted in values only, never in target keys. Color specs: see
|
|
31
|
+
// colors.ts. Times are seconds (number) or "500ms" / "10s" / "2m".
|
|
32
|
+
import { colorKey, parseColor } from './colors.js';
|
|
33
|
+
export const EASES = ['linear', 'sine'];
|
|
34
|
+
export function parseDuration(v, what) {
|
|
35
|
+
if (v == null)
|
|
36
|
+
return 0;
|
|
37
|
+
if (typeof v === 'number') {
|
|
38
|
+
if (!Number.isFinite(v) || v < 0)
|
|
39
|
+
throw new Error(`${what}: must be >= 0 seconds`);
|
|
40
|
+
return Math.round(v * 1000);
|
|
41
|
+
}
|
|
42
|
+
if (typeof v === 'string') {
|
|
43
|
+
const m = v.trim().toLowerCase().match(/^(\d*\.?\d+)\s*(ms|s|sec|secs|m|min|mins)?$/);
|
|
44
|
+
if (m) {
|
|
45
|
+
const n = Number(m[1]);
|
|
46
|
+
const unit = m[2] ?? 's';
|
|
47
|
+
if (unit === 'ms')
|
|
48
|
+
return Math.round(n);
|
|
49
|
+
if (unit.startsWith('m'))
|
|
50
|
+
return Math.round(n * 60_000);
|
|
51
|
+
return Math.round(n * 1000);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`${what}: bad duration "${v}" (use seconds, or "500ms" / "10s" / "2m")`);
|
|
55
|
+
}
|
|
56
|
+
export function parsePalette(raw) {
|
|
57
|
+
const pal = new Map();
|
|
58
|
+
if (raw == null)
|
|
59
|
+
return pal;
|
|
60
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
61
|
+
throw new Error('palette: must be a map of name → color');
|
|
62
|
+
}
|
|
63
|
+
for (const [name, spec] of Object.entries(raw)) {
|
|
64
|
+
try {
|
|
65
|
+
pal.set(colorKey(name), parseColor(spec, pal));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new Error(`palette "${name}": ${e.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return pal;
|
|
72
|
+
}
|
|
73
|
+
// ── parameter substitution ─────────────────────────────────────────────────
|
|
74
|
+
const WHOLE_PARAM = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/;
|
|
75
|
+
const INLINE_PARAM = /\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g;
|
|
76
|
+
/** Deep-substitute `$name` / `${name}` in `value`. A string that is exactly
|
|
77
|
+
* one reference becomes the param's value with its own type (so
|
|
78
|
+
* `fade: $secs` stays numeric); otherwise references interpolate as text. */
|
|
79
|
+
export function substitute(value, params, where) {
|
|
80
|
+
if (typeof value === 'string') {
|
|
81
|
+
const whole = value.match(WHOLE_PARAM);
|
|
82
|
+
if (whole)
|
|
83
|
+
return lookup(whole[1]);
|
|
84
|
+
return value.replace(INLINE_PARAM, (_m, name) => String(lookup(name)));
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(value))
|
|
87
|
+
return value.map((v, i) => substitute(v, params, `${where}[${i}]`));
|
|
88
|
+
if (value && typeof value === 'object') {
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const [k, v] of Object.entries(value)) {
|
|
91
|
+
out[k] = substitute(v, params, `${where}.${k}`);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
function lookup(name) {
|
|
97
|
+
if (!(name in params))
|
|
98
|
+
throw new Error(`${where}: unknown param "$${name}"`);
|
|
99
|
+
const v = params[name];
|
|
100
|
+
if (v == null)
|
|
101
|
+
throw new Error(`${where}: param "$${name}" has no value`);
|
|
102
|
+
return v;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Collect the param names referenced anywhere in a value tree. */
|
|
106
|
+
function referencedParams(value, out = new Set()) {
|
|
107
|
+
if (typeof value === 'string') {
|
|
108
|
+
for (const m of value.matchAll(INLINE_PARAM))
|
|
109
|
+
out.add(m[1]);
|
|
110
|
+
}
|
|
111
|
+
else if (Array.isArray(value)) {
|
|
112
|
+
for (const v of value)
|
|
113
|
+
referencedParams(v, out);
|
|
114
|
+
}
|
|
115
|
+
else if (value && typeof value === 'object') {
|
|
116
|
+
for (const v of Object.values(value))
|
|
117
|
+
referencedParams(v, out);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Merge a show onto its `extends` chain: child params/name/loop/defaults
|
|
122
|
+
* override, steps come from the nearest ancestor that defines them. */
|
|
123
|
+
function flatten(spec, byId, trail = []) {
|
|
124
|
+
if (trail.includes(spec.id)) {
|
|
125
|
+
throw new Error(`show "${spec.id}": extends cycle ${[...trail, spec.id].join(' → ')}`);
|
|
126
|
+
}
|
|
127
|
+
let base = {
|
|
128
|
+
id: spec.id, name: spec.id, params: {}, steps: [], loop: true, switch: true,
|
|
129
|
+
};
|
|
130
|
+
if (spec.extends) {
|
|
131
|
+
const parent = byId.get(spec.extends);
|
|
132
|
+
if (!parent)
|
|
133
|
+
throw new Error(`show "${spec.id}": extends unknown show "${spec.extends}"`);
|
|
134
|
+
base = flatten(parent, byId, [...trail, spec.id]);
|
|
135
|
+
}
|
|
136
|
+
if (spec.params != null && (typeof spec.params !== 'object' || Array.isArray(spec.params))) {
|
|
137
|
+
throw new Error(`show "${spec.id}": params must be a map`);
|
|
138
|
+
}
|
|
139
|
+
if (spec.steps != null && !Array.isArray(spec.steps)) {
|
|
140
|
+
throw new Error(`show "${spec.id}": steps must be an array`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id: spec.id,
|
|
144
|
+
name: spec.name ?? spec.id,
|
|
145
|
+
params: { ...base.params, ...(spec.params ?? {}) },
|
|
146
|
+
steps: spec.steps ?? base.steps,
|
|
147
|
+
loop: spec.loop ?? base.loop,
|
|
148
|
+
fade: spec.fade ?? base.fade,
|
|
149
|
+
hold: spec.hold ?? base.hold,
|
|
150
|
+
ease: spec.ease ?? base.ease,
|
|
151
|
+
switch: spec.switch ?? base.switch,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parseEase(v, what) {
|
|
155
|
+
if (v == null)
|
|
156
|
+
return 'linear';
|
|
157
|
+
if (typeof v === 'string' && EASES.includes(v))
|
|
158
|
+
return v;
|
|
159
|
+
throw new Error(`${what}: ease must be one of ${EASES.join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
/** Named fixture sets for show targets: `groups: { odd: [id, id, ...] }`.
|
|
162
|
+
* Unlike zones they create no HomeKit accessory. Ids may be fixture ids
|
|
163
|
+
* or zone ids; group ids must not collide with either. */
|
|
164
|
+
export function parseGroups(raw, fixtures, zones) {
|
|
165
|
+
const groups = new Map();
|
|
166
|
+
if (raw == null)
|
|
167
|
+
return groups;
|
|
168
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
169
|
+
throw new Error('groups: must be a map of group id → [fixture or zone ids]');
|
|
170
|
+
}
|
|
171
|
+
const fixturesById = new Map(fixtures.map((f) => [f.id, f]));
|
|
172
|
+
const zonesById = new Map(zones.map((z) => [z.id, z]));
|
|
173
|
+
for (const [id, members] of Object.entries(raw)) {
|
|
174
|
+
if (fixturesById.has(id) || zonesById.has(id)) {
|
|
175
|
+
throw new Error(`group "${id}": collides with a fixture or zone id`);
|
|
176
|
+
}
|
|
177
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
178
|
+
throw new Error(`group "${id}": must be a non-empty array of fixture or zone ids`);
|
|
179
|
+
}
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const m of members) {
|
|
182
|
+
const fx = fixturesById.get(String(m));
|
|
183
|
+
const z = zonesById.get(String(m));
|
|
184
|
+
if (fx)
|
|
185
|
+
out.push(fx);
|
|
186
|
+
else if (z)
|
|
187
|
+
out.push(...z.members);
|
|
188
|
+
else
|
|
189
|
+
throw new Error(`group "${id}": unknown fixture or zone "${m}"`);
|
|
190
|
+
}
|
|
191
|
+
groups.set(id, out);
|
|
192
|
+
}
|
|
193
|
+
return groups;
|
|
194
|
+
}
|
|
195
|
+
export function loadShows(specs, fixtures, zones, groups, palette) {
|
|
196
|
+
const byId = new Map();
|
|
197
|
+
for (const s of specs) {
|
|
198
|
+
if (!s || typeof s !== 'object' || !s.id)
|
|
199
|
+
throw new Error('show: missing id');
|
|
200
|
+
if (byId.has(s.id))
|
|
201
|
+
throw new Error(`show: duplicate id "${s.id}"`);
|
|
202
|
+
byId.set(s.id, s);
|
|
203
|
+
}
|
|
204
|
+
const fixturesById = new Map(fixtures.map((f) => [f.id, f]));
|
|
205
|
+
const targetsById = new Map();
|
|
206
|
+
for (const z of zones)
|
|
207
|
+
targetsById.set(z.id, z.members);
|
|
208
|
+
for (const [id, members] of groups)
|
|
209
|
+
targetsById.set(id, members);
|
|
210
|
+
const shows = [];
|
|
211
|
+
for (const spec of specs) {
|
|
212
|
+
const flat = flatten(spec, byId);
|
|
213
|
+
const tag = `show "${flat.id}"`;
|
|
214
|
+
if (flat.steps.length === 0)
|
|
215
|
+
throw new Error(`${tag}: no steps (define steps or extend a show that has them)`);
|
|
216
|
+
// Params: every reference must be declared; unbound ones make the show
|
|
217
|
+
// a template (no switch) rather than an error, so a base show can
|
|
218
|
+
// carry `params: { a: ~ }` purely as documentation of its inputs.
|
|
219
|
+
const referenced = referencedParams({ steps: flat.steps, fade: flat.fade, hold: flat.hold, ease: flat.ease });
|
|
220
|
+
for (const p of referenced) {
|
|
221
|
+
if (!(p in flat.params))
|
|
222
|
+
throw new Error(`${tag}: references undeclared param "$${p}"`);
|
|
223
|
+
}
|
|
224
|
+
const unbound = Object.entries(flat.params).filter(([, v]) => v == null).map(([k]) => k);
|
|
225
|
+
const runnable = unbound.length === 0;
|
|
226
|
+
if (!runnable) {
|
|
227
|
+
shows.push({
|
|
228
|
+
id: flat.id, name: flat.name, loop: flat.loop, runnable, unboundParams: unbound,
|
|
229
|
+
exposeSwitch: false, fixtures: [], steps: [], cycleMs: 0,
|
|
230
|
+
});
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const stepsRaw = substitute(flat.steps, flat.params, `${tag}.steps`);
|
|
234
|
+
const defFade = parseDuration(substitute(flat.fade, flat.params, `${tag}.fade`), `${tag}.fade`);
|
|
235
|
+
const defHold = parseDuration(substitute(flat.hold, flat.params, `${tag}.hold`), `${tag}.hold`);
|
|
236
|
+
const defEase = parseEase(substitute(flat.ease, flat.params, `${tag}.ease`), `${tag}.ease`);
|
|
237
|
+
// Pass 1: each step's explicitly-named fixtures.
|
|
238
|
+
const explicit = [];
|
|
239
|
+
const involved = new Map();
|
|
240
|
+
const parsed = [];
|
|
241
|
+
stepsRaw.forEach((st, i) => {
|
|
242
|
+
const stag = `${tag} step ${i + 1}`;
|
|
243
|
+
if (!st || typeof st !== 'object')
|
|
244
|
+
throw new Error(`${stag}: must be a map`);
|
|
245
|
+
if (!st.look || typeof st.look !== 'object' || Array.isArray(st.look)) {
|
|
246
|
+
throw new Error(`${stag}: needs a "look" map of target → color`);
|
|
247
|
+
}
|
|
248
|
+
const look = new Map();
|
|
249
|
+
for (const [targetKey, colorSpec] of Object.entries(st.look)) {
|
|
250
|
+
let color;
|
|
251
|
+
try {
|
|
252
|
+
color = parseColor(colorSpec, palette);
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
throw new Error(`${stag} look "${targetKey}": ${e.message}`);
|
|
256
|
+
}
|
|
257
|
+
for (const fx of expandTargets(targetKey, fixturesById, targetsById, stag)) {
|
|
258
|
+
look.set(fx.id, color);
|
|
259
|
+
involved.set(fx.id, fx);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
explicit.push(look);
|
|
263
|
+
parsed.push({
|
|
264
|
+
fadeMs: st.fade == null ? defFade : parseDuration(st.fade, `${stag}.fade`),
|
|
265
|
+
holdMs: st.hold == null ? defHold : parseDuration(st.hold, `${stag}.hold`),
|
|
266
|
+
ease: st.ease == null ? defEase : parseEase(st.ease, `${stag}.ease`),
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
// Pass 2: tracking. Walk the steps twice for looping shows so step 0
|
|
270
|
+
// inherits from the tail of the cycle; once for one-shots.
|
|
271
|
+
const carry = new Map();
|
|
272
|
+
const passes = flat.loop ? 2 : 1;
|
|
273
|
+
const targets = explicit.map(() => new Map());
|
|
274
|
+
for (let p = 0; p < passes; p++) {
|
|
275
|
+
explicit.forEach((look, i) => {
|
|
276
|
+
for (const [id, c] of look)
|
|
277
|
+
carry.set(id, c);
|
|
278
|
+
targets[i] = new Map(carry);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (involved.size === 0)
|
|
282
|
+
throw new Error(`${tag}: no step names any fixture`);
|
|
283
|
+
const steps = parsed.map((p, i) => ({ targets: targets[i], ...p }));
|
|
284
|
+
const cycleMs = steps.reduce((s, st) => s + st.fadeMs + st.holdMs, 0);
|
|
285
|
+
if (cycleMs <= 0)
|
|
286
|
+
throw new Error(`${tag}: needs some fade or hold time`);
|
|
287
|
+
shows.push({
|
|
288
|
+
id: flat.id,
|
|
289
|
+
name: flat.name,
|
|
290
|
+
loop: flat.loop,
|
|
291
|
+
runnable: true,
|
|
292
|
+
unboundParams: [],
|
|
293
|
+
exposeSwitch: flat.switch,
|
|
294
|
+
fixtures: [...involved.values()],
|
|
295
|
+
steps,
|
|
296
|
+
cycleMs,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return shows;
|
|
300
|
+
}
|
|
301
|
+
function expandTargets(key, fixturesById, targetsById, // zones + groups
|
|
302
|
+
where) {
|
|
303
|
+
const out = [];
|
|
304
|
+
for (const raw of key.split(/[,\s]+/)) {
|
|
305
|
+
const t = raw.trim();
|
|
306
|
+
if (!t)
|
|
307
|
+
continue;
|
|
308
|
+
const fx = fixturesById.get(t);
|
|
309
|
+
if (fx) {
|
|
310
|
+
out.push(fx);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const set = targetsById.get(t);
|
|
314
|
+
if (set) {
|
|
315
|
+
out.push(...set);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (t === 'all' || t === '*') {
|
|
319
|
+
out.push(...fixturesById.values());
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`${where}: unknown target "${t}" (not a fixture, zone or group id, or "all")`);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=dsl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsl.js","sourceRoot":"","sources":["../../src/show/dsl.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Logger } from 'homebridge';
|
|
2
|
+
import { Fixture } from '../config.js';
|
|
3
|
+
import { HomeKitLightState } from '../color/types.js';
|
|
4
|
+
import { DmxController } from '../dmxController.js';
|
|
5
|
+
import { StateRegistry } from '../stateRegistry.js';
|
|
6
|
+
import { LookColor } from './colors.js';
|
|
7
|
+
import { Show } from './dsl.js';
|
|
8
|
+
export type ShowListener = (runningId: string | null) => void;
|
|
9
|
+
export declare class ShowEngine {
|
|
10
|
+
private readonly controllers;
|
|
11
|
+
private readonly registry;
|
|
12
|
+
private readonly log;
|
|
13
|
+
private run;
|
|
14
|
+
private listeners;
|
|
15
|
+
private lastEmitted;
|
|
16
|
+
private shows;
|
|
17
|
+
constructor(shows: Show[], controllers: Map<string, DmxController>, registry: StateRegistry, log: Logger);
|
|
18
|
+
running(): string | null;
|
|
19
|
+
onChange(cb: ShowListener): () => void;
|
|
20
|
+
start(id: string): void;
|
|
21
|
+
/** Stop the running show, freezing the lights where they are: the
|
|
22
|
+
* registry gets the last frame that actually went on the wire. `except`
|
|
23
|
+
* names a fixture whose registry state must NOT be overwritten (it was
|
|
24
|
+
* just set from HomeKit and is the reason we're stopping). */
|
|
25
|
+
stop(reason?: string, except?: string): void;
|
|
26
|
+
/** `notify: false` on a handoff inside start(): the switches hear one
|
|
27
|
+
* transition (A → B) instead of A → nothing → B, which would bounce
|
|
28
|
+
* the tapped tile off and back on. */
|
|
29
|
+
private stopRun;
|
|
30
|
+
shutdown(): void;
|
|
31
|
+
/** Notify switches of the running show — only on an actual transition,
|
|
32
|
+
* so the handoff inside start() (stop A, start B) doesn't bounce B's
|
|
33
|
+
* tile off and back on. */
|
|
34
|
+
private emit;
|
|
35
|
+
private fixture;
|
|
36
|
+
private tick;
|
|
37
|
+
private applyFinal;
|
|
38
|
+
/** Where every fixture is at wall-clock `now`. Null once a one-shot show
|
|
39
|
+
* has run past its end. */
|
|
40
|
+
private frameAt;
|
|
41
|
+
}
|
|
42
|
+
export declare function fromState(s: HomeKitLightState): LookColor;
|
|
43
|
+
/** Shape a look for a specific fixture: only the characteristics its color
|
|
44
|
+
* model exposes, HomeKit-style on/brightness. A dark look parks the
|
|
45
|
+
* brightness at 100 so the next HomeKit "on" restores full (mirrors
|
|
46
|
+
* hsvcct.parse). `precise` keeps fractional values — the wire path
|
|
47
|
+
* renders 16-bit intensity, so fades stay smooth below 1 % steps; the
|
|
48
|
+
* registry (HomeKit) gets integers. */
|
|
49
|
+
export declare function toState(look: LookColor, fx: Fixture, precise?: boolean): HomeKitLightState;
|
|
50
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/show/engine.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,MAAM,MAAM,YAAY,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;AAgB9D,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IATtB,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,KAAK,CAA2B;gBAGtC,KAAK,EAAE,IAAI,EAAE,EACI,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EACvC,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,MAAM;IAY9B,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB,QAAQ,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM,IAAI;IAKtC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IA0CvB;;;mEAG+D;IAC/D,IAAI,CAAC,MAAM,SAAiB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAIpD;;2CAEuC;IACvC,OAAO,CAAC,OAAO;IAgBf,QAAQ,IAAI,IAAI;IAIhB;;gCAE4B;IAC5B,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,IAAI;IAiCZ,OAAO,CAAC,UAAU;IAalB;gCAC4B;IAC5B,OAAO,CAAC,OAAO;CAoChB;AAID,wBAAgB,SAAS,CAAC,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAMzD;AAED;;;;;wCAKwC;AACxC,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,UAAQ,GAAG,iBAAiB,CAexF"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// ShowEngine — runs at most one show at a time.
|
|
2
|
+
//
|
|
3
|
+
// Every frame (the involved controllers' fastest frame interval, 25 Hz on
|
|
4
|
+
// sACN) the engine works out where each fixture is between the previous
|
|
5
|
+
// step's look and the current one, renders it through the controller
|
|
6
|
+
// (quietly — no per-frame logging) and moves on. The registry only hears
|
|
7
|
+
// about it at step boundaries (throttled) and when the show stops, so
|
|
8
|
+
// HomeKit tiles track the show without a 25 Hz event storm.
|
|
9
|
+
//
|
|
10
|
+
// Rules:
|
|
11
|
+
// - starting a show stops whichever one was running (its switch flips off)
|
|
12
|
+
// - the first fade starts from the fixtures' live registry state, so a
|
|
13
|
+
// show fades IN from whatever the lights were doing
|
|
14
|
+
// - stopping freezes the lights on the current frame and writes that
|
|
15
|
+
// frame to the registry (so tiles are accurate)
|
|
16
|
+
// - a HomeKit change to any fixture in the running show stops the show;
|
|
17
|
+
// the fixture that was touched keeps the user's new value
|
|
18
|
+
import { SHOW_FRAME_MS_DEFAULT, SHOW_KEYFRAME_MIN_MS } from '../settings.js';
|
|
19
|
+
import { mixLook } from './colors.js';
|
|
20
|
+
export class ShowEngine {
|
|
21
|
+
controllers;
|
|
22
|
+
registry;
|
|
23
|
+
log;
|
|
24
|
+
run = null;
|
|
25
|
+
listeners = new Set();
|
|
26
|
+
lastEmitted = null;
|
|
27
|
+
shows = new Map();
|
|
28
|
+
constructor(shows, controllers, registry, log) {
|
|
29
|
+
this.controllers = controllers;
|
|
30
|
+
this.registry = registry;
|
|
31
|
+
this.log = log;
|
|
32
|
+
for (const s of shows)
|
|
33
|
+
this.shows.set(s.id, s);
|
|
34
|
+
// Any HomeKit change to a fixture in the running show ends the show.
|
|
35
|
+
registry.onChange((id, source) => {
|
|
36
|
+
if (source !== 'homekit' || !this.run)
|
|
37
|
+
return;
|
|
38
|
+
if (this.run.fixtures.has(id)) {
|
|
39
|
+
this.stop(`"${id}" changed from HomeKit`, id);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
running() {
|
|
44
|
+
return this.run?.show.id ?? null;
|
|
45
|
+
}
|
|
46
|
+
onChange(cb) {
|
|
47
|
+
this.listeners.add(cb);
|
|
48
|
+
return () => this.listeners.delete(cb);
|
|
49
|
+
}
|
|
50
|
+
start(id) {
|
|
51
|
+
const show = this.shows.get(id);
|
|
52
|
+
if (!show) {
|
|
53
|
+
this.log.error(`show "${id}": unknown`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!show.runnable) {
|
|
57
|
+
this.log.error(`show "${id}": unbound params (${show.unboundParams.join(', ')})`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (this.run?.show.id === id)
|
|
61
|
+
return;
|
|
62
|
+
if (this.run)
|
|
63
|
+
this.stopRun(`"${id}" starting`, undefined, false);
|
|
64
|
+
const snapshot = new Map();
|
|
65
|
+
for (const fx of show.fixtures)
|
|
66
|
+
snapshot.set(fx.id, fromState(this.registry.get(fx.id)));
|
|
67
|
+
// One timer drives every involved controller, so tick at the SLOWEST
|
|
68
|
+
// frame interval among them: flooding the Stick with 25 Hz sets would
|
|
69
|
+
// keep resetting its debounce and it would never transact at all.
|
|
70
|
+
const frameMs = show.fixtures.reduce((max, fx) => {
|
|
71
|
+
const c = this.controllers.get(fx.controller.id);
|
|
72
|
+
return Math.max(max, c?.frameIntervalMs ?? SHOW_FRAME_MS_DEFAULT);
|
|
73
|
+
}, 0) || SHOW_FRAME_MS_DEFAULT;
|
|
74
|
+
const run = {
|
|
75
|
+
show,
|
|
76
|
+
startedAt: Date.now(),
|
|
77
|
+
timer: setInterval(() => this.tick(), frameMs),
|
|
78
|
+
snapshot,
|
|
79
|
+
fixtures: new Map(show.fixtures.map((f) => [f.id, f])),
|
|
80
|
+
frameMs,
|
|
81
|
+
lastFrame: null,
|
|
82
|
+
lastKeyframeStep: -1,
|
|
83
|
+
lastKeyframeAt: 0,
|
|
84
|
+
};
|
|
85
|
+
run.timer.unref?.();
|
|
86
|
+
this.run = run;
|
|
87
|
+
this.log.info(`show "${show.id}" started: ${show.steps.length} steps, ${show.fixtures.length} fixtures, ` +
|
|
88
|
+
`cycle ${(show.cycleMs / 1000).toFixed(1)} s, ${show.loop ? 'looping' : 'one-shot'}, ` +
|
|
89
|
+
`${frameMs} ms frames`);
|
|
90
|
+
this.tick();
|
|
91
|
+
this.emit();
|
|
92
|
+
}
|
|
93
|
+
/** Stop the running show, freezing the lights where they are: the
|
|
94
|
+
* registry gets the last frame that actually went on the wire. `except`
|
|
95
|
+
* names a fixture whose registry state must NOT be overwritten (it was
|
|
96
|
+
* just set from HomeKit and is the reason we're stopping). */
|
|
97
|
+
stop(reason = 'switched off', except) {
|
|
98
|
+
this.stopRun(reason, except, true);
|
|
99
|
+
}
|
|
100
|
+
/** `notify: false` on a handoff inside start(): the switches hear one
|
|
101
|
+
* transition (A → B) instead of A → nothing → B, which would bounce
|
|
102
|
+
* the tapped tile off and back on. */
|
|
103
|
+
stopRun(reason, except, notify) {
|
|
104
|
+
const run = this.run;
|
|
105
|
+
if (!run)
|
|
106
|
+
return;
|
|
107
|
+
clearInterval(run.timer);
|
|
108
|
+
this.run = null;
|
|
109
|
+
// Before the first tick nothing has been sent; after a one-shot's end
|
|
110
|
+
// the final look is on the wire (see applyFinal).
|
|
111
|
+
const frozen = run.lastFrame ?? run.show.steps[run.show.steps.length - 1].targets;
|
|
112
|
+
for (const [id, look] of frozen) {
|
|
113
|
+
if (id === except)
|
|
114
|
+
continue;
|
|
115
|
+
this.registry.set(id, toState(look, this.fixture(run, id)), 'show');
|
|
116
|
+
}
|
|
117
|
+
this.log.info(`show "${run.show.id}" stopped (${reason})`);
|
|
118
|
+
if (notify)
|
|
119
|
+
this.emit();
|
|
120
|
+
}
|
|
121
|
+
shutdown() {
|
|
122
|
+
if (this.run) {
|
|
123
|
+
clearInterval(this.run.timer);
|
|
124
|
+
this.run = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Notify switches of the running show — only on an actual transition,
|
|
128
|
+
* so the handoff inside start() (stop A, start B) doesn't bounce B's
|
|
129
|
+
* tile off and back on. */
|
|
130
|
+
emit() {
|
|
131
|
+
const id = this.running();
|
|
132
|
+
if (id === this.lastEmitted)
|
|
133
|
+
return;
|
|
134
|
+
this.lastEmitted = id;
|
|
135
|
+
for (const l of this.listeners)
|
|
136
|
+
l(id);
|
|
137
|
+
}
|
|
138
|
+
fixture(run, id) {
|
|
139
|
+
return run.fixtures.get(id);
|
|
140
|
+
}
|
|
141
|
+
tick() {
|
|
142
|
+
const run = this.run;
|
|
143
|
+
if (!run)
|
|
144
|
+
return;
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const frame = this.frameAt(run, now);
|
|
147
|
+
if (!frame) {
|
|
148
|
+
// One-shot show ran off the end: land on the final look and stop.
|
|
149
|
+
this.applyFinal(run);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const [id, look] of frame.looks) {
|
|
153
|
+
const fx = this.fixture(run, id);
|
|
154
|
+
const c = this.controllers.get(fx.controller.id);
|
|
155
|
+
c?.setFixture(fx, toState(look, fx, true), { quiet: true });
|
|
156
|
+
}
|
|
157
|
+
run.lastFrame = frame.looks;
|
|
158
|
+
// Keyframe: on entering a new step, tell the registry where the lights
|
|
159
|
+
// are heading — the step's destination, not the in-between value, so
|
|
160
|
+
// a long fade shows its target for the whole fade. Throttled so fast
|
|
161
|
+
// chases don't flood HomeKit; on a sub-second chase the tiles lag by
|
|
162
|
+
// an unbounded amount, which is fine for a chase.
|
|
163
|
+
if (now - run.lastKeyframeAt >= SHOW_KEYFRAME_MIN_MS) {
|
|
164
|
+
run.lastKeyframeAt = now;
|
|
165
|
+
if (frame.step !== run.lastKeyframeStep) {
|
|
166
|
+
run.lastKeyframeStep = frame.step;
|
|
167
|
+
const targets = run.show.steps[frame.step].targets;
|
|
168
|
+
for (const [id, look] of targets) {
|
|
169
|
+
this.registry.set(id, toState(look, this.fixture(run, id)), 'show');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
applyFinal(run) {
|
|
175
|
+
clearInterval(run.timer);
|
|
176
|
+
this.run = null; // before any registry write, same as stop()
|
|
177
|
+
const last = run.show.steps[run.show.steps.length - 1];
|
|
178
|
+
for (const [id, look] of last.targets) {
|
|
179
|
+
const fx = this.fixture(run, id);
|
|
180
|
+
this.controllers.get(fx.controller.id)?.setFixture(fx, toState(look, fx, true), { quiet: true });
|
|
181
|
+
this.registry.set(id, toState(look, fx), 'show');
|
|
182
|
+
}
|
|
183
|
+
this.log.info(`show "${run.show.id}" finished`);
|
|
184
|
+
this.emit();
|
|
185
|
+
}
|
|
186
|
+
/** Where every fixture is at wall-clock `now`. Null once a one-shot show
|
|
187
|
+
* has run past its end. */
|
|
188
|
+
frameAt(run, now) {
|
|
189
|
+
const { show, snapshot } = run;
|
|
190
|
+
let e = now - run.startedAt;
|
|
191
|
+
let firstCycle = true;
|
|
192
|
+
if (show.loop) {
|
|
193
|
+
if (e >= show.cycleMs) {
|
|
194
|
+
firstCycle = false;
|
|
195
|
+
e = e % show.cycleMs;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (e >= show.cycleMs) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// Locate the step and the position within its fade/hold.
|
|
202
|
+
let i = 0;
|
|
203
|
+
let t = 1;
|
|
204
|
+
let acc = 0;
|
|
205
|
+
for (; i < show.steps.length; i++) {
|
|
206
|
+
const st = show.steps[i];
|
|
207
|
+
const len = st.fadeMs + st.holdMs;
|
|
208
|
+
if (e < acc + len || i === show.steps.length - 1) {
|
|
209
|
+
const local = e - acc;
|
|
210
|
+
t = st.fadeMs > 0 ? Math.min(1, local / st.fadeMs) : 1;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
acc += len;
|
|
214
|
+
}
|
|
215
|
+
const st = show.steps[i];
|
|
216
|
+
const from = i > 0 ? show.steps[i - 1].targets
|
|
217
|
+
: firstCycle ? null : show.steps[show.steps.length - 1].targets;
|
|
218
|
+
const eased = st.ease === 'sine' ? (1 - Math.cos(Math.PI * t)) / 2 : t;
|
|
219
|
+
const looks = new Map();
|
|
220
|
+
for (const fx of show.fixtures) {
|
|
221
|
+
const start = snapshot.get(fx.id);
|
|
222
|
+
const a = from?.get(fx.id) ?? start;
|
|
223
|
+
const b = st.targets.get(fx.id) ?? start;
|
|
224
|
+
looks.set(fx.id, mixLook(a, b, eased));
|
|
225
|
+
}
|
|
226
|
+
return { step: i, looks };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ── LookColor ↔ HomeKitLightState ──────────────────────────────────────────
|
|
230
|
+
export function fromState(s) {
|
|
231
|
+
const out = { brightness: s.on ? s.brightness : 0 };
|
|
232
|
+
if (s.hue != null)
|
|
233
|
+
out.hue = s.hue;
|
|
234
|
+
if (s.saturation != null)
|
|
235
|
+
out.saturation = s.saturation;
|
|
236
|
+
if (s.colorTemperatureMireds != null)
|
|
237
|
+
out.mireds = s.colorTemperatureMireds;
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
/** Shape a look for a specific fixture: only the characteristics its color
|
|
241
|
+
* model exposes, HomeKit-style on/brightness. A dark look parks the
|
|
242
|
+
* brightness at 100 so the next HomeKit "on" restores full (mirrors
|
|
243
|
+
* hsvcct.parse). `precise` keeps fractional values — the wire path
|
|
244
|
+
* renders 16-bit intensity, so fades stay smooth below 1 % steps; the
|
|
245
|
+
* registry (HomeKit) gets integers. */
|
|
246
|
+
export function toState(look, fx, precise = false) {
|
|
247
|
+
const chars = fx.profile.model.characteristics;
|
|
248
|
+
const r = precise ? (n) => n : Math.round;
|
|
249
|
+
const on = look.brightness > 0.5;
|
|
250
|
+
const s = {
|
|
251
|
+
on,
|
|
252
|
+
brightness: on ? Math.max(1, r(look.brightness)) : 100,
|
|
253
|
+
};
|
|
254
|
+
const colorMode = look.hue != null && (look.saturation ?? 0) > 0;
|
|
255
|
+
if (chars.includes('Hue') && look.hue != null)
|
|
256
|
+
s.hue = r(look.hue);
|
|
257
|
+
if (chars.includes('Saturation'))
|
|
258
|
+
s.saturation = r(look.saturation ?? 0);
|
|
259
|
+
if (chars.includes('ColorTemperature') && look.mireds != null && !colorMode) {
|
|
260
|
+
s.colorTemperatureMireds = r(look.mireds);
|
|
261
|
+
}
|
|
262
|
+
return s;
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=engine.js.map
|