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>
Developer tools
Two small, dependency-free Node.js tools (Node 22+) for developing the S31 module
without a console. They share osc-lite.js, a tiny OSC encoder/decoder.
tools/package.json pins this folder to CommonJS, because the module's own
package.json is "type": "module" and would otherwise make Node load these
files as ESM. Keep it.
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 (tools/catalog/s31-catalog.md); this is the dialect the Companion module uses. |
mock-s31.js - fake S31 console
Emulates the console's OSC extension. It listens on one UDP port and sends all feedback to one configured controller IP:port, whoever asked and 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]
[--delay MS] [--types] [--quiet]
| Flag | Meaning |
|---|---|
--dialect |
sseries (default) or sd. |
--listen |
UDP port to bind (the console's receive port). Default 8000. |
--reply-to |
Fixed feedback destination, e.g. 127.0.0.1:9000. If omitted, locks onto the first sender's IP with port 9000 and prints that. |
--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. |
Traffic is printed as <- ADDRESS [args] from IP:PORT and -> ADDRESS [args].
Whole-number floats print with a decimal (-10.0) so you can tell int from float.
sseries dialect
No args at a readable address = query (reply with the value); with an arg =
set (store, then echo to the controller). /console/resend dumps the whole
state at ~2 ms per packet. Values are clamped (fader -150..10, send level
-90..10, pan -1..1). Addresses: /console/{ping,channel/counts,resend},
/channel/N/{name,fader,mute,solo,pan}, /channel/N/total/gain (send-only,
sent with fader changes), /channel/N/send/M/{level,enabled,pan}, and
/digico/snapshots/fire[/next|/previous] (logged only). Inputs are 1-60, aux
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. 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.
| Address | Reply type |
|---|---|
/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); 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) |
/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) |
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.
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.)
fake-ipad.js - stand-in for the DiGiCo iPad app
Binds the receive port, sends its opening requests, repeats a keepalive every 5 s, prints everything it receives and a packet-count summary every 10 s.
node tools/fake-ipad.js --console IP --send-port 8000 --receive-port 9000
[--dialect sseries|sd] [--name A] [--types] [--quiet]
sseries: sends/console/ping, then/console/resend; keepalive/console/ping;/console/pongcounts as a pong.sd: sends/Console/Name/?,/Console/Input_Channels/?,/Console/Aux_Outputs/?,/Console/Aux_Outputs/modes/?, names of inputs 1-8 and aux outputs 1-4, then/Snapshots/names/?(20 ms apart); keepalive/Console/Name/?; a/Console/Namereply counts as a pong.
Type ADDRESS [arg ...] lines on stdin to send your own messages
(true/false = boolean, 5 = int, 5.0 or -12.5 = float, anything else or
"quoted" = string; quit exits):
/channel/3/send/2/level -12.5 # sseries set
/Input_Channels/3/Aux_Send/2/send_on 1 # sd set (echo comes back as ,f [1])
/Input_Channels/3/Aux_Send/2/send_level/? # sd query
/channel/3/name "Lead Vox"
End-to-end loops on one machine
The module sits between the console and the iPad: the console's feedback goes
to the module (port 9000), and the module relays to and from the iPad. Use the
ports the module is actually configured with; 9100 and 9200 below are just
examples for "the relay's iPad-facing port" and "where the relay forwards
feedback to the iPad".
S-Series dialect
# 1. Mock console: listens on 8000, replies to whatever listens on 9000
node tools/mock-s31.js --listen 8000 --reply-to 127.0.0.1:9000
# 2. Start the Companion module (or any relay) listening on 9000, sending to 127.0.0.1:8000
# 3. Fake iPad pointed at the relay (sends to 9100, receives the relayed feedback on 9200)
node tools/fake-ipad.js --console 127.0.0.1 --send-port 9100 --receive-port 9200
SD dialect (what the real S31 speaks)
# 1. Mock console in SD mode: listens on 8000, replies to 127.0.0.1:9000
node tools/mock-s31.js --dialect sd --listen 8000 --reply-to 127.0.0.1:9000 --types
# 2. Start the Companion module (or any relay) listening on 9000, sending to 127.0.0.1:8000
# 3. Fake iPad (SD) pointed at the relay
node tools/fake-ipad.js --dialect sd --console 127.0.0.1 --send-port 9100 --receive-port 9200 --types
No module yet? Skip step 2 and point the iPad straight at the mock to check the
tools themselves, e.g.
node tools/fake-ipad.js --dialect sd --console 127.0.0.1 --send-port 8000 --receive-port 9000.