v2: generic S-Series module with live discovery, dynamic options and meters
Rewrite of the module around what the console itself reports: - Settings cut to console address/ports, the iPad relay and debug logging; nothing site-specific remains. An upgrade script carries v1 configs and remaps v1 actions/feedbacks whose ids were reused. - On connect the module reads every name, buss type (aux/group), stereo mode, fader/mute/solo/pan and the snapshot list; entities that never answer are hidden. Definitions rebuild (debounced, skipped when identical) when names, buss types or snapshots change. - Every entity option is a searchable dropdown of live names. 70 actions cover inputs, aux sends, group assigns, busses, matrix outputs and sends, control groups, graphic EQs, channel-strip switches, trim/gain/filters and snapshots, each level with set and encoder-friendly adjust variants. - Boolean, value and bar feedbacks for every state, input level and gain-reduction meters via the console's meter subscription protocol, and about 2800 presets including rotary knob presets with live level text. - A write guard keeps fast encoder turns from being undone by delayed echoes; feedbacks are re-checked per instance. - Relay: the client table survives config changes, idle iPads are kept for an hour by default (0 = forever), bind failures retry and show in status. npm test: 326 checks pass on Node 25 and on Companion's Node 22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { fmtCue, fmtDb, fmtPan } from '../protocol.js'
|
||||
|
||||
const ENTITY_VARS = {
|
||||
input: ['name', 'fader', 'mute', 'pan'],
|
||||
buss: ['name', 'mode', 'stereo', 'fader', 'mute'],
|
||||
matrix: ['name', 'fader', 'mute'],
|
||||
cg: ['name', 'fader', 'mute'],
|
||||
}
|
||||
|
||||
const VAR_LABEL = {
|
||||
name: 'name',
|
||||
fader: 'fader level (dB)',
|
||||
mute: 'mute state',
|
||||
pan: 'pan',
|
||||
mode: 'mode (Aux/Group)',
|
||||
stereo: 'Stereo/Mono',
|
||||
}
|
||||
|
||||
const ENT_LABEL = { input: 'Input', buss: 'Buss', matrix: 'Matrix output', cg: 'Control group' }
|
||||
|
||||
/** Variables exist only for entities the console reported (all possible ones until the first load finishes). */
|
||||
export function getVariableDefinitions(inst) {
|
||||
const defs = {
|
||||
console_name: { name: 'Console name' },
|
||||
session_name: { name: 'Session file name' },
|
||||
connected: { name: 'Console link alive (Yes/No)' },
|
||||
relay_clients: { name: 'iPad apps connected through the relay' },
|
||||
relay_client_ips: { name: 'iPad client IPs' },
|
||||
last_change: { name: 'Last change reported by the console' },
|
||||
snapshot_index: { name: 'Current snapshot index' },
|
||||
snapshot_name: { name: 'Current snapshot name' },
|
||||
snapshot_cue: { name: 'Current snapshot cue number' },
|
||||
snapshot_count: { name: 'Number of snapshots' },
|
||||
}
|
||||
for (const ent of Object.keys(ENTITY_VARS)) {
|
||||
for (const n of inst.state.numbers(ent)) {
|
||||
for (const field of ENTITY_VARS[ent]) defs[`${ent}_${n}_${field}`] = { name: `${ENT_LABEL[ent]} ${n} ${VAR_LABEL[field]}` }
|
||||
}
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
const muteText = (m) => (m === undefined ? '' : m ? 'Muted' : 'Unmuted')
|
||||
|
||||
/** All current values of one entity, keyed by variable id. */
|
||||
export function entityVariableValues(inst, ent, n) {
|
||||
const s = inst.state
|
||||
const p = `${ent}_${n}_`
|
||||
const out = { [`${p}name`]: s.displayName(ent, n), [`${p}fader`]: fmtDb(s.getFader(ent, n)), [`${p}mute`]: muteText(s.getMute(ent, n)) }
|
||||
if (ent === 'input') out[`${p}pan`] = fmtPan(s.getPan(n))
|
||||
if (ent === 'buss') {
|
||||
out[`${p}mode`] = s.bussMode(n) === 'group' ? 'Group' : 'Aux'
|
||||
const st = s.busses.get(n)?.stereo
|
||||
out[`${p}stereo`] = st === undefined ? '' : st ? 'Stereo' : 'Mono'
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function snapshotVariableValues(inst) {
|
||||
const s = inst.state.snapshots
|
||||
const cur = s.list.get(s.current)
|
||||
return {
|
||||
snapshot_index: s.current ?? '',
|
||||
snapshot_name: cur?.name ?? '',
|
||||
snapshot_cue: cur ? fmtCue(cur.cue) : '',
|
||||
snapshot_count: s.count ?? s.list.size,
|
||||
}
|
||||
}
|
||||
|
||||
/** Everything, after the definitions were rebuilt. */
|
||||
export function getAllVariableValues(inst) {
|
||||
const s = inst.state
|
||||
const out = { console_name: s.console.name, session_name: s.console.session, connected: inst.connectedFlag ? 'Yes' : 'No', ...snapshotVariableValues(inst) }
|
||||
for (const ent of Object.keys(ENTITY_VARS)) for (const n of s.numbers(ent)) Object.assign(out, entityVariableValues(inst, ent, n))
|
||||
return out
|
||||
}
|
||||
|
||||
/** Variable values affected by one event; {} when none. */
|
||||
export function getEventVariableValues(inst, ev) {
|
||||
switch (ev.kind) {
|
||||
case 'consoleName':
|
||||
return { console_name: ev.value }
|
||||
case 'session':
|
||||
return { session_name: ev.value }
|
||||
case 'name':
|
||||
case 'fader':
|
||||
case 'mute':
|
||||
case 'pan':
|
||||
return entityVariableValues(inst, ev.ent, ev.n)
|
||||
case 'snapCurrent':
|
||||
case 'snapName':
|
||||
case 'snapCount':
|
||||
return snapshotVariableValues(inst)
|
||||
case 'bussTypes':
|
||||
case 'bussModes': {
|
||||
// stereo / mono (and aux / group) for every buss; no definition rebuild for a stereo flag alone
|
||||
const out = {}
|
||||
for (const n of inst.state.existing('buss')) Object.assign(out, entityVariableValues(inst, 'buss', n))
|
||||
return out
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/** Short text for last_change, or '' when the event is not worth showing. */
|
||||
export function describeChange(inst, ev) {
|
||||
const s = inst.state
|
||||
const who = (ent, n) => `${ENT_LABEL[ent]} ${n} (${s.displayName(ent, n)})`
|
||||
switch (ev.kind) {
|
||||
case 'fader':
|
||||
return `${who(ev.ent, ev.n)} fader ${fmtDb(ev.value)}`
|
||||
case 'mute':
|
||||
return `${who(ev.ent, ev.n)} ${ev.value ? 'muted' : 'unmuted'}`
|
||||
case 'solo':
|
||||
return `${who(ev.ent, ev.n)} solo ${ev.value ? 'on' : 'off'}`
|
||||
case 'pan':
|
||||
return `${who(ev.ent, ev.n)} pan ${fmtPan(ev.value)}`
|
||||
case 'send':
|
||||
return `Input ${ev.ch} send ${ev.buss} ${ev.field} ${ev.field === 'level' ? fmtDb(ev.value) : ev.value}`
|
||||
case 'group':
|
||||
return `Input ${ev.ch} group ${ev.buss} ${ev.value ? 'assigned' : 'unassigned'}`
|
||||
case 'snapCurrent':
|
||||
return `Snapshot ${ev.value}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user