Write on/off switches as float 0/1, as the S31 requires

Verified on a live S31 against an unused, silent input (restored after):
a float fader write is accepted and echoed, but a mute written as int 0/1
or as an OSC True/False is silently ignored, while a float 1.0/0.0 works.
DiGiCo's 2014 SD command list says int, which is wrong for the S31.

Every switch (mute, solo, send on, group assign, EQ/dynamics/filter/
insert/delay switches, phantom, phase, pad, GEQ in) now goes out as a
float. The mock console now ignores int and T/F switch writes the same
way the console does, so a regression to int fails 38 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 17:32:17 -04:00
co-authored by Claude Opus 5.5
parent dad8010239
commit 57645be1e9
6 changed files with 87 additions and 40 deletions
+5 -2
View File
@@ -70,7 +70,7 @@ inputs, `--auxes N` the busses (default 25), and the matrix (8 outputs, 10 input
| `/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) |
| `/{...}/N/mute/?`, `/{...}/N/solo/?` | `f` 0/1 (a float, not an int); a SET must be a float too, see below |
| `/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) |
@@ -90,7 +90,10 @@ chooses `id`. Every subscribed id then gets a random value 0..127 about five tim
`/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
A 0/1 switch (mute, solo, send on, group assign, matrix send on, `geq_in` and every channel-strip switch) is both
reported **and written as a float** 0.0 / 1.0, verified on a real S31 on 2026-10-15 (the 2014 DiGiCo list says
int). Exactly like the console, the mock accepts only a float write (stored, echoed as `,f`) and silently
ignores an int `i` or `T`/`F` write: no echo, no state change. Levels, pans and values are clamped 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`.)
+10 -7
View File
@@ -23,7 +23,8 @@
* 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 argument; it is stored and echoed using the console's reply
* types (mute, solo and send_on come back as floats 0/1). It also streams
* types (mute, solo and send_on come back as floats 0/1, and a 0/1
* switch only accepts a SET as a float: int and T/F writes are ignored). It also streams
* meters to subscribed ids and implements the write-only snapshot triggers.
* Anything the real console does not answer (lowercase S-Series addresses,
* /Console/Resend, /Console/Channels/?, whole-send queries, ...) gets no
@@ -165,8 +166,8 @@ function mulberry32(seed) {
* s string
* f float32 with min/max clamp
* b boolean (sseries; sent as T/F, or int 0/1 with --bool-int)
* flag 0/1 number (sd; accepted as int, float or boolean; reported as a float,
* or as an int when the spec says wire: 'i')
* flag 0/1 number (sd; a SET is accepted only as a float 0.0 / 1.0, exactly like the S31: int and
* T/F writes are silently ignored; reported as a float, or as an int when the spec says wire: 'i')
* int integer with min/max clamp (sd)
*/
@@ -803,12 +804,12 @@ function sdResolve(address) {
return null;
}
function handleSd(address, args) {
function handleSd(address, args, types) {
if (address.endsWith('/?')) {
if (args.length !== 0) return ignore(`query ${address} carries arguments ${formatArgs(args)}`);
return sdQuery(address.slice(0, -2), address);
}
return sdSet(address, args);
return sdSet(address, args, types);
}
/** The nine counts the console pushes once, shortly after first contact (catalog: unsolicited, order as captured). */
@@ -859,7 +860,7 @@ function sdFireSnapshot(index) {
send('/Snapshots/Current_Snapshot', [int(sdSnapshot)]);
}
function sdSet(address, args) {
function sdSet(address, args, types) {
// Write-only trigger nodes
if (address === '/Snapshots/Fire_Snapshot_number') {
if (typeof args[0] !== 'number' || args[0] < 0 || args[0] >= SD_SNAPSHOTS.length) return ignore(`snapshot fire with ${formatArgs(args)}`);
@@ -895,6 +896,8 @@ function sdSet(address, args) {
if (!r) return ignore(`no reply for ${address} (unknown or unsupported address)`);
const spec = r.spec;
if (spec.readOnly) return ignore(`${address} is read-only`);
// Verified on a real S31 (2026-10-15): a 0/1 switch only takes a float write; ints and T/F are ignored without any reply.
if (spec.kind === 'flag' && types !== 'f') return ignore(`${address} is a switch: only a float 0.0 / 1.0 is accepted, not ,${types} (no reply, no change)`);
if (args.length !== 1) return ignore(`${address} needs exactly one argument (got ${formatArgs(args)})`);
const value = coerce(spec, args[0]);
if (value === undefined) return ignore(`unusable value ${formatArgs(args)} for ${address}`);
@@ -963,7 +966,7 @@ socket.on('message', (msg, rinfo) => {
setTimeout(() => startPaced('count burst', sdCountBurst(), { append: true }), SD_BURST_DELAY_MS);
}
if (packet.raw === undefined) handle(packet.address, packet.args);
if (packet.raw === undefined) handle(packet.address, packet.args, packet.types);
});
socket.on('error', (err) => {