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:
2026-10-15 17:11:17 -04:00
co-authored by Claude Opus 5.5
parent 8693fe924a
commit dad8010239
20 changed files with 5102 additions and 1327 deletions
+41 -25
View File
@@ -12,7 +12,7 @@ Both tools speak two OSC dialects, chosen with `--dialect`:
| Dialect | What it is |
| --- | --- |
| `sseries` (default) | The lowercase S-Series command list: `/console/ping`, `/channel/N/fader`, ... |
| `sd` | The "DiGiCo Pad" dialect the **real S31 iPad extension actually speaks**: `/Console/Name/?`, `/Input_Channels/N/fader/?`, ... Verified against a live console. |
| `sd` | The "DiGiCo Pad" dialect the **real S31 iPad extension actually speaks**: `/Console/Name/?`, `/Input_Channels/N/fader/?`, ... Verified against a live console (`tools/catalog/s31-catalog.md`); this is the dialect the Companion module uses. |
## `mock-s31.js` - fake S31 console
@@ -23,7 +23,7 @@ from whichever source port, like the real desk.
```
node tools/mock-s31.js [--dialect sseries|sd] [--listen 8000] [--reply-to IP:PORT]
[--channels N] [--auxes N] [--bool-int] [--live]
[--types] [--quiet]
[--delay MS] [--types] [--quiet]
```
| Flag | Meaning |
@@ -34,6 +34,7 @@ node tools/mock-s31.js [--dialect sseries|sd] [--listen 8000] [--reply-to IP:POR
| `--channels` | Input count, 1-60. Default 48 (`sseries`) or 60 (`sd`). |
| `--auxes` | Aux busses / sends per channel. Default 16, max 16 (`sseries`); default 25, max 25 (`sd`). |
| `--bool-int` | `sseries` only: send booleans as int 0/1 instead of OSC `T`/`F`. |
| `--delay MS` | `sd`: answer and echo every message MS milliseconds late (0-5000, default 0), to test a slow console and stale echoes. |
| `--live` | Every 3 s, change a random send level or send on/off and emit it. |
| `--types` | Also print each message's OSC type tags, e.g. `/Input_Channels/1/fader ,f [-1.4375]`. |
| `--quiet` | Don't print every message. |
@@ -52,32 +53,47 @@ sent with fader changes), `/channel/N/send/M/{level,enabled,pan}`, and
busses 70..70+auxes-1.
### sd dialect
A **query** is the address plus a trailing `/?` and no args; the reply is the
same address without `/?` plus the value. A **set** is the address without `/?`
and one arg; it is stored and echoed back in the console's reply types.
A **query** is the address plus a trailing `/?` and no args; the reply is the same address without `/?`
plus the value. A **set** is the address without `/?` and one arg; it is stored and echoed back in the
console's reply types. The mock answers the addresses listed in `tools/catalog/s31-catalog.md` (the real
console's answers) with the catalog's reply types. Entity numbers are 1-based, **control groups
included** (`/Control_Groups/1/name` is the first); snapshot indexes are 0-based. `--channels N` limits the
inputs, `--auxes N` the busses (default 25), and the matrix (8 outputs, 10 inputs), 10 control groups and
16 graphic EQs are fixed.
| Query | Reply type |
| Address | Reply type |
| --- | --- |
| `/Console/Name/?` | `s` ("S31 AB") |
| `/Console/{Input_Channels,Aux_Outputs,Group_Outputs,Control_Groups,Matrix_Outputs,Matrix_Inputs,Graphic_EQ,Talkback_Outputs,Multis}/?` | `i` |
| `/Console/Aux_Outputs/modes/?` | one `i` per aux (2 = stereo, 1 = mono) |
| `/Input_Channels/N/Channel_Input/name/?`, `/Aux_Outputs/N/Buss_Trim/name/?`, `/Group_Outputs/N/Buss_Trim/name/?` | `s` (read-only). The 25 busses appear under both Aux_Outputs and Group_Outputs; a buss reports its name on the side matching its mode and `""` on the other (captured names). |
| `/Input_Channels/N/fader/?`, `/Aux_Outputs/N/fader/?`, `/Group_Outputs/N/fader/?` | `f`, dB, -150 = -inf, max +10 (group 24 = -1.6875) |
| `/Input_Channels/N/mute/?`, `/Aux_Outputs/N/mute/?`, `/Group_Outputs/N/mute/?` | `f` 0/1 (a float, not an int) |
| `/Input_Channels/N/Aux_Send/M/send_level/?` | `f`, dB, -150..10 |
| `/Input_Channels/N/Aux_Send/M/send_on/?` | `f` 0/1 |
| `/Input_Channels/N/Aux_Send/M/send_pan/?` | `f`, 0..1, 0.5 = centre |
| `/Input_Channels/N/Aux_Send/M/send_pre-post/?` | `i` (captured as an int, default 0; settable, clamped 0..1 - range is an assumption) |
| `/Input_Channels/N/Group_Send/M/group/?` | `f` 0/1 group assign, M = 1..25. Default: inputs 1-8 and 18-21 on group 24, nothing else. The reply type is the single constant `SD_GROUP_ASSIGN_WIRE_TYPE` in `mock-s31.js` (`'f'`, assumed like mute/send_on; set `'i'` if the real console reports an int). |
| `/Snapshots/Current_Snapshot/?` | `i` |
| `/Snapshots/names/?` | one `/Snapshots/name ,iiis [index, cue*100, 0, "name"]` per snapshot |
| `/Console/Name/?`, `/Console/Session/Filename/?` | `s` |
| `/Console/{Matrix_Outputs,Matrix_Inputs,Control_Groups,Graphic_EQ,Talkback_Outputs,Multis}/?` | `i` |
| `/Console/{Input_Channels,Aux_Outputs,Group_Outputs}/?` | **silent**, like the real console. All nine counts are pushed once, ~200 ms after the first datagram it receives |
| `/Console/Aux_Outputs/types/?` | one `i` per buss, 1 = aux, 0 = group (a buss named only on the Group side is a group) |
| `/Console/{Aux_Outputs,Group_Outputs}/modes/?`, `/Console/Input_Channels/modes/?` | one `i` per buss / input (2 = stereo, 1 = mono, captured) |
| `/Input_Channels/N/Channel_Input/name/?`, `/Aux_Outputs/N/Buss_Trim/name/?`, `/Group_Outputs/N/Buss_Trim/name/?`, `/Matrix_Outputs/N/Buss_Trim/name/?`, `/Control_Groups/N/name/?`, `/Graphic_EQ/N/name/?` | `s`. Only input names accept a write here (not verified on the real console); the others are read-only. The 25 busses appear under both Aux_Outputs and Group_Outputs; a buss reports its name on the side matching its mode and `""` on the other. Matrix inputs have no name address |
| `/{Input_Channels,Aux_Outputs,Group_Outputs,Matrix_Outputs,Control_Groups}/N/fader/?` | `f`, dB, -150 = -inf, max +10 |
| `/{...}/N/mute/?`, `/{...}/N/solo/?` | `f` 0/1 (a float, not an int) |
| `/Input_Channels/N/Panner/pan/?` | `f`, 0..1, 0.5 = centre |
| `/Input_Channels/N/Aux_Send/M/{send_level,send_on,send_pan}/?` | `f` (level dB -150..10, on 0/1, pan 0..1) |
| `/Input_Channels/N/Aux_Send/M/send_pre-post/?` | `i` (captured as an int, default 0; settable 0..1, which value means "pre" is an assumption; the Companion module does not use it) |
| `/Input_Channels/N/Group_Send/M/group/?` | `f` 0/1 group assign. Default: inputs 1-8 and 18-21 on group 24. The wire type is the constant `SD_GROUP_ASSIGN_WIRE_TYPE` |
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}/?` | `f` (default -90 and 0, captured) |
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}/?` | `f` |
| `/Input_Channels/N/{Channel_Input/{trim,phase,phantom,analog_gain,input_pad},Filters/{hi,lo}_filter_{in,freq},Channel_Delay/{delay_on,delay},EQ/eq_in,Dynamics/{comp_in,gate_in},Insert/insert_{A,B}_in}/?` | `f` (switches 0/1) |
| `/{Aux_Outputs,Group_Outputs}/N/{Buss_Trim/{trim,phase},EQ/eq_in,Dynamics/{comp_in,gate_in},Channel_Delay/{delay_on,delay},Insert/insert_{A,B}_in}/?`, `/Matrix_Outputs/N/Buss_Trim/trim/?` | `f` |
| `/Snapshots/Current_Snapshot/?`, `/Snapshots/count/?` | `i` |
| `/Snapshots/names/?` | one `/Snapshots/name ,iiis [index, cue*100, 0, "name"]` per snapshot. `/Snapshots/name/?` without an argument stays silent (unreliable on the real console) |
| `/Snapshots/Fire_Snapshot_number [i]`, `/Snapshots/Fire_Next_Snapshot`, `/Snapshots/Fire_Prev_Snapshot` | write-only triggers; the mock moves its current snapshot and pushes `/Snapshots/Current_Snapshot` |
| `/Macros/Buttons/press [i]` | logged only (no macro query is answered) |
Sets accept int or float for `mute`/`send_on` (any non-zero = on) and clamp
`fader`/`send_level` to -150..10 and `send_pan` to 0..1. Like the real console
it stays **silent** on everything else: all lowercase S-Series addresses,
`/Console/Resend`, `/Console/Channels/?`, `/Input_Channels/N/Aux_Send/M/?`,
`/Input_Channels/N/Aux_Send/?`, `/Macros/Buttons/?`, out-of-range channel/send
numbers, and sets of read-only names. (`--bool-int` has no effect in `sd`.)
**Meters.** Subscribe with `/Input_Channels/N/Channel_Input/post_meter ,ii [id, 1]` (post-fader level) or
`/Input_Channels/N/Dynamics/GR_meter ,ii [id, 1|4]` (gate / compressor gain reduction), where the client
chooses `id`. Every subscribed id then gets a random value 0..127 about five times a second, as
`/Meters/values ,ii... [id, v, id, v, ...]` (only ids whose value changed). `/Meters/clear` drops every
subscription, like the real console.
Sets accept int or float for the 0/1 switches (any non-zero = on) and clamp levels, pans and values to
their ranges. Like the real console the mock stays **silent** on everything else: all lowercase S-Series
addresses, `/Console/Resend`, `/Console/Channels/?`, whole-send queries, out-of-range entity numbers,
sets of read-only names. (`--bool-int` has no effect in `sd`.)
## `fake-ipad.js` - stand-in for the DiGiCo iPad app