217 lines
14 KiB
Markdown
217 lines
14 KiB
Markdown
# companion-module-digico-s31 — v2 design (generic S-Series module)
|
||||
|
|
|
|||
|
|
Status: decided 2026-10-15. This is the brief for the v2 implementation. Where this document and
|
|||
|
|
`tools/catalog/s31-catalog.md` (what the real console answers) disagree, the catalog wins and the
|
|||
|
|
design note is updated.
|
|||
|
|
|
|||
|
|
## Goals
|
|||
|
|
|
|||
|
|
1. Generic: nothing church-specific in settings, names, help or defaults. No "Studio Mon".
|
|||
|
|
2. Minimal settings: console address/ports, relay, debug. Everything else is discovered.
|
|||
|
|
3. Rich, discoverable control: every option that identifies a channel/buss/matrix/CG/snapshot is a
|
|||
|
|
**dropdown populated with the live names** from the console, never a bare integer field.
|
|||
|
|
4. Encoder-friendly: relative "adjust" actions for every level, and presets that use rotary steps.
|
|||
|
|
5. Feedback everywhere: boolean feedbacks for every switch, dynamic-text and bar feedbacks for
|
|||
|
|
every level, and presets wired with them.
|
|||
|
|
|
|||
|
|
## Settings (connection config) — the complete list
|
|||
|
|
|
|||
|
|
| id | type | default | label |
|
|||
|
|
| --- | --- | --- | --- |
|
|||
|
|
| `host` | textinput (Regex.IP) | `` | Console IP |
|
|||
|
|
| `consolePort` | number | 8000 | Console receive port (module sends here) |
|
|||
|
|
| `localPort` | number | 9000 | Local receive port (console sends here; = console "Send port") |
|
|||
|
|
| `relayEnable` | checkbox | true | Enable iPad relay |
|
|||
|
|
| `relayPort` | number | 8000 | Relay port (iPad apps send here) |
|
|||
|
|
| `relayClientPort` | number | 9000 | iPad receive port (0 = reply to the app's source port) |
|
|||
|
|
| `relayTimeout` | number | 180 | Forget an iPad after this many idle seconds |
|
|||
|
|
| `debugLog` | checkbox | false | Debug logging (log every new address pattern + all traffic at debug level) |
|
|||
|
|
|
|||
|
|
Two `static-text` blocks explain the console-side iPad extension settings and the iPad-app settings
|
|||
|
|
(generic wording: "this computer", no IPs). Nothing else. Keepalive is always on. Dialect is fixed to
|
|||
|
|
the DiGiCo Pad / SD tree (the only one the iPad extension speaks). On/off values are written as
|
|||
|
|
int 0/1, levels and pans as floats (DiGiCo SD OSC list).
|
|||
|
|
|
|||
|
|
## Topology discovery
|
|||
|
|
|
|||
|
|
Maxima probed (S31 limits; S21 simply answers fewer): inputs 60, busses 25 (`Aux_Outputs` and
|
|||
|
|
`Group_Outputs` are the same 25 busses, named on the side matching the buss mode), matrix outputs
|
|||
|
|
8, matrix inputs 10, control groups 10, graphic EQs 16. On connect the module queries the counts
|
|||
|
|
(`/Console/Input_Channels/?` etc.; the S31 answers these only sometimes) and the **name** of every
|
|||
|
|
index; an entity whose name query never answered within the load is treated as absent and hidden
|
|||
|
|
from dropdowns, variables and presets. Buss mode: `group` when the Group side name is non-empty
|
|||
|
|
and the Aux side is empty, otherwise `aux`. Stereo/mono from `/Console/Aux_Outputs/modes`.
|
|||
|
|
|
|||
|
|
Use exactly the addresses the catalog marks as answered. Never send an address the catalog does
|
|||
|
|
not list as answered (except `raw_osc`).
|
|||
|
|
|
|||
|
|
## State store (`src/state.js`)
|
|||
|
|
|
|||
|
|
Maps keyed by entity index: `inputs[n] = {name, fader, mute, solo, pan}`, `busses[n] = {name,
|
|||
|
|
mode, stereo, fader, mute, solo}`, `matrix[n]`, `cgs[n]`, `geqs[n]`; `sends["ch:buss"] = {level,
|
|||
|
|
on, pan, prepost}`; `groups["ch:buss"] = bool`; `matrixSends["in:out"]`; `snapshots = {current,
|
|||
|
|
list: Map(index -> {name, cue})}`; `macros`; `console = {name}`. One `applyMessage(address, args)`
|
|||
|
|
entry point returning the normalised event; one `choices` section that builds dropdown choice arrays:
|
|||
|
|
|
|||
|
|
- inputs: `{ id: n, label: "n · Name" }` (name falls back to `Input n`)
|
|||
|
|
- busses: `"n · Name (aux)"` / `"n · Name (group)"`; filtered variants `auxChoices`, `groupChoices`
|
|||
|
|
- matrix / cgs / geqs: `"n · Name"`
|
|||
|
|
- snapshots: `"index · Name"` (+ ` · cue X` when a cue number exists)
|
|||
|
|
|
|||
|
|
Definitions are rebuilt (actions, feedbacks, variables, presets) whenever a name, mode or the
|
|||
|
|
snapshot list changes, debounced ~750 ms, and once when the initial load finishes. Option ids stay
|
|||
|
|
numeric so existing buttons survive renames.
|
|||
|
|
|
|||
|
|
## Actions
|
|||
|
|
|
|||
|
|
Option conventions: `channel` = inputs dropdown; `channels` = inputs multidropdown (for bulk
|
|||
|
|
mute/solo); `buss` = busses dropdown (aux-only or group-only where it matters); `state` = dropdown
|
|||
|
|
on/off/toggle; `level` = number dB (−150..10, step 0.5); `delta` = number dB (−30..30, default 1,
|
|||
|
|
for encoders); `pan` = number 0..1 (0.5 centre), `pdelta` = number.
|
|||
|
|
|
|||
|
|
| id | options | notes |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `input_mute` | channels, state | |
|
|||
|
|
| `input_solo` | channels, state | if solo answered in the catalog |
|
|||
|
|
| `input_fader_set` | channel, level | |
|
|||
|
|
| `input_fader_adjust` | channel, delta | clamps; from −∞ the first step lands at −60 dB |
|
|||
|
|
| `input_pan_set` / `input_pan_adjust` | channel, pan / pdelta | |
|
|||
|
|
| `input_name_set` | channel, name (textinput, useVariables) | |
|
|||
|
|
| `send_on` | channel, buss(aux), state | |
|
|||
|
|
| `send_level_set` / `send_level_adjust` | channel, buss(aux), level / delta | |
|
|||
|
|
| `send_pan_set` / `send_pan_adjust` | channel, buss(aux), pan / pdelta | |
|
|||
|
|
| `send_prepost` | channel, buss(aux), mode pre/post/toggle | if answered |
|
|||
|
|
| `group_assign` | channel, buss(group), state | |
|
|||
|
|
| `buss_mute` / `buss_solo` | busses (multi), state | |
|
|||
|
|
| `buss_fader_set` / `buss_fader_adjust` | buss, level / delta | |
|
|||
|
|
| `matrix_mute` / `matrix_fader_set` / `matrix_fader_adjust` | matrix, … | |
|
|||
|
|
| `matrix_send_on` / `matrix_send_level_set` / `matrix_send_level_adjust` | matrixIn, matrixOut, … | only if the catalog shows Matrix_Send answering |
|
|||
|
|
| `cg_mute` / `cg_solo` / `cg_fader_set` / `cg_fader_adjust` | cg, … | |
|
|||
|
|
| `geq_band_set` / `geq_band_adjust` / `geq_on` | geq, band, gain | only if answered |
|
|||
|
|
| `snapshot_fire` | snapshot (dropdown, allowCustom for a raw number) | |
|
|||
|
|
| `snapshot_next` / `snapshot_prev` | — | |
|
|||
|
|
| `macro_press` | macro | only if macros answer |
|
|||
|
|
| `reload` | — | re-query everything |
|
|||
|
|
| `raw_osc` | address, argType, value | experiments |
|
|||
|
|
| `relay_forget` | — | |
|
|||
|
|
|
|||
|
|
Toggle semantics need the current value: if unknown, query it first and apply the toggle when the
|
|||
|
|
reply arrives (or default to "on"). Every write is followed by a query of the same address so the
|
|||
|
|
state is confirmed even if the console does not echo.
|
|||
|
|
|
|||
|
|
## Feedbacks
|
|||
|
|
|
|||
|
|
Boolean (with sensible `defaultStyle`): `input_muted`, `input_solo`, `send_on`, `group_assigned`,
|
|||
|
|
`send_pre` (pre-fade), `buss_muted`, `buss_solo`, `matrix_muted`, `cg_muted`, `snapshot_current`
|
|||
|
|
(snapshot dropdown), `level_compare` (entity kind dropdown input/buss/matrix/cg + index + above/below
|
|||
|
|
+ threshold), `send_level_compare`, `connected`, `relay_clients`.
|
|||
|
|
|
|||
|
|
Value feedbacks (`type: 'value'`, return a JsonValue) feed preset **local variables** so button
|
|||
|
|
text can show live numbers without thousands of module variables: `input_fader_value` (dB string,
|
|||
|
|
`-inf` at the floor), `input_pan_value`, `send_level_value`, `send_on_value` (`On`/`Off`),
|
|||
|
|
`buss_fader_value`, `matrix_fader_value`, `cg_fader_value`, `snapshot_name_value`,
|
|||
|
|
`input_meter_value`. Presets declare `localVariables: [{ variableName: 'level', variableType:
|
|||
|
|
'feedback', feedbackId: 'send_level_value', options: {…} }]` and use `$(local:level)` in the text.
|
|||
|
|
|
|||
|
|
Advanced (bar): `input_fader_bar`, `send_level_bar`, `buss_fader_bar` — returns an `imageBuffer`
|
|||
|
|
(base64 RGBA, `imageBufferEncoding: { pixelFormat: 'RGBA' }`, `imageBufferPosition` sized from
|
|||
|
|
`event.image`) drawing a horizontal bar along the bottom edge of the button proportional to the
|
|||
|
|
level (−60..+10 mapped to 0..100 %), colour option. Implemented with a small RGBA helper in
|
|||
|
|
`src/image.js`. Verify bytes-per-pixel on the real Companion (4 for RGBA).
|
|||
|
|
|
|||
|
|
Base 2.1 has **no feedback `subscribe`**. Send-pair state is loaded on first use instead: when a
|
|||
|
|
feedback or action callback finds the pair's state unknown, enqueue the queries for that pair
|
|||
|
|
(de-duplicated per pair) and return the unknown state; the console's reply updates state and
|
|||
|
|
`checkFeedbacks` re-renders. Sends are never bulk-loaded (60 × 25 × 3 queries would take a minute).
|
|||
|
|
|
|||
|
|
Number options for levels use `showMinAsNegativeInfinity: true`; every entity dropdown sets
|
|||
|
|
`minChoicesForSearch: 0` so lists are searchable. Visibility of conditional options uses
|
|||
|
|
`isVisibleExpression` strings (the only form in base 2.1).
|
|||
|
|
|
|||
|
|
## Variables
|
|||
|
|
|
|||
|
|
Only for entities that exist: `input_N_name|fader|mute|pan`, `buss_N_name|mode|stereo|fader|mute`,
|
|||
|
|
`matrix_N_name|fader|mute`, `cg_N_name|fader|mute`, `snapshot_index|name|cue|count`,
|
|||
|
|
`console_name`, `connected`, `relay_clients`, `relay_client_ips`, `last_change`. No per-send
|
|||
|
|
variables (use the advanced text feedbacks).
|
|||
|
|
|
|||
|
|
## Presets (type `simple`; names carry the live entity name; rebuilt when names load)
|
|||
|
|
|
|||
|
|
Structure groups and their definitions:
|
|||
|
|
|
|||
|
|
1. **Inputs** — per input: *Mute* (toggle; feedback muted = red; text `$(label:input_N_name)`),
|
|||
|
|
*Fader (knob)* (rotary: left/right = `input_fader_adjust` ∓1 dB; press = `input_mute` toggle;
|
|||
|
|
feedbacks: `input_text` with `{name}\n{fader}`, `input_fader_bar`, muted = red), *Solo*.
|
|||
|
|
2. **Busses** — per buss: *Master mute*, *Master fader (knob)* (rotary ±1 dB, press = mute toggle,
|
|||
|
|
text + bar).
|
|||
|
|
3. **Sends to <aux name>** — one group per aux buss, per input: *On/off* (toggle; green when on;
|
|||
|
|
`send_text` `{name}\n{level}`), *Level (knob)* (rotary ±1 dB, press toggles on/off; text + bar).
|
|||
|
|
4. **Group <name>** — one group per group buss, per input: *Assign* (toggle, green when assigned).
|
|||
|
|
5. **Matrix** — per output: mute, fader knob. **Control groups** — per CG: mute, fader knob.
|
|||
|
|
6. **Snapshots** — per snapshot: *Fire* (green when current, text = name); *Next*, *Previous*,
|
|||
|
|
*Current* (display only, `snapshot_text`).
|
|||
|
|
7. **Console** — Reload, Connection status, Relay status (count).
|
|||
|
|
|
|||
|
|
Rotary presets set `options.rotaryActions: true` and use the `rotate_left` / `rotate_right` step
|
|||
|
|
arrays (verify exact field names in `@companion-module/base` 2.1.3 types).
|
|||
|
|
|
|||
|
|
## Loading, keepalive, relay
|
|||
|
|
|
|||
|
|
As in v1: fixed local port; keepalive `/Console/Name/?` every 5 s; 20 s silence → Disconnected;
|
|||
|
|
reconnect → full reload; paced query queue (20 ms); learn log of new address patterns when debug
|
|||
|
|
is on; verbatim relay with fan-out and idle timeout. The console pushes changes made on the surface
|
|||
|
|
unsolicited, so after the initial load the state stays current without polling.
|
|||
|
|
|
|||
|
|
## Protocol facts learned from a live iPad session (2026-10-15)
|
|||
|
|
|
|||
|
|
Addresses the DiGiCo S app itself uses on this console (all verified answering):
|
|||
|
|
|
|||
|
|
- `/Console/Aux_Outputs/types/?` → 25 ints, **1 = aux, 0 = group**: the authoritative buss-mode
|
|||
|
|
source (use it; fall back to the name heuristic only if it does not answer).
|
|||
|
|
`/Console/Input_Channels/modes/?` → 60 ints (stereo modes per input), `/Console/Group_Outputs/modes/?`.
|
|||
|
|
- `/Console/Session/Filename/?` → session file name (expose as `session_name`).
|
|||
|
|
- `/Input_Channels/N/Panner/pan` (float 0..1) is the pan address (not `/Input_Channels/N/pan`).
|
|||
|
|
- `/Input_Channels/N/solo` (float 0/1); `/Input_Channels/N/CGs_level`, `/CGs_mute` (control-group
|
|||
|
|
contribution, read-only display); `/Input_Channels/N/Channel_Input/main_alt_in`, `stereo_mode`,
|
|||
|
|
`analog_gain`, `trim`; `/Input_Channels/N/total_gain` (read-only, pushed with fader moves).
|
|||
|
|
- `/Control_Groups/N/fader|mute|solo` — the iPad queried `Control_Groups/0`, so CG indexing may be
|
|||
|
|
**0-based**; the catalog decides (probe both 0 and 1).
|
|||
|
|
- `/Group_Outputs/N/fader|mute|solo`, `/Aux_Outputs/N/fader|mute`, `/Matrix_Outputs/N/fader`.
|
|||
|
|
- `/Snapshots/name/? [i]` (int arg) returns one snapshot's name; `/Snapshots/Surface_Snapshot`.
|
|||
|
|
- `/Layout/Layout/Banks` (surface bank layout; read-only, ignore).
|
|||
|
|
- The console pushes every change made at the surface unsolicited (fader, mute, solo, sends, EQ…).
|
|||
|
|
|
|||
|
|
## Metering (captured from a live iPad session through the relay, 2026-10-15)
|
|||
|
|
|
|||
|
|
The console streams meters only to subscribed ids, using the same iPad leg:
|
|||
|
|
|
|||
|
|
- Subscribe: send `/<Entity>/N/<meter point> ,ii [meterId, kind]` where the **client chooses
|
|||
|
|
`meterId`** (int) and `kind` selects the meter: seen `/Input_Channels/N/Channel_Input/post_meter
|
|||
|
|
[id, 1]` (post-fader input level) and `/Input_Channels/N/Dynamics/GR_meter [id, 1]` plus
|
|||
|
|
`[id2, 4]` (two gain-reduction meters per channel, presumably gate and compressor). The iPad uses
|
|||
|
|
ids 0-19 (its visible bank), 200+, and 300+.
|
|||
|
|
- Stream: `/Meters/values ,ii… [id, value, id, value, …]` — only ids whose value changed, several
|
|||
|
|
times a second; values are integers 0..127 (127 seen as full scale / peak).
|
|||
|
|
- `/Meters/clear` (no args) clears every subscription on the console, including ours. The iPad
|
|||
|
|
sends it on connect. When the relay sees an iPad send `/Meters/clear`, the module must re-send its
|
|||
|
|
own subscriptions after ~500 ms. Never send `/Meters/clear` from the module.
|
|||
|
|
- The module uses ids from **5000** upward (5000 + input index for post meters; 6000 + index for
|
|||
|
|
buss output meters if `/Aux_Outputs/N/.../post_meter`-style addresses answer — the builder probes
|
|||
|
|
the mock only; the real address for buss meters is unknown and must be learned from the iPad log)
|
|||
|
|
so they never collide with an iPad's ids. Subscriptions are made lazily: the first time a meter
|
|||
|
|
feedback for an entity runs, subscribe; re-subscribe after reconnect and after `/Meters/clear`.
|
|||
|
|
- Exposed as: value feedback `input_meter_value` (0-127 and percent), boolean `input_signal`
|
|||
|
|
(value above a threshold option, default 5) and `input_peak` (≥ 127, with a hold time option),
|
|||
|
|
advanced `input_meter_bar` (vertical bar at the right edge, green/amber/red zones), and the same
|
|||
|
|
trio for busses once their address is known. Meter updates must NOT trigger definition rebuilds
|
|||
|
|
or variable updates for all channels; only `checkFeedbacks` for the meter feedback ids, throttled
|
|||
|
|
to at most ~10 Hz.
|
|||
|
|
|
|||
|
|
Known relay limitation: the console holds one meter subscription table; two iPads subscribing the
|
|||
|
|
same ids for different banks will both receive mixed data. Document it; nothing to fix in v2.
|
|||
|
|
|
|||
|
|
## Out of scope for v2
|
|||
|
|
|
|||
|
|
Per-channel EQ/dynamics/filters (available via `raw_osc`), S-Series OSC Control dialect
|
|||
|
|
(removed), snapshot editing.
|