Bitfocus Companion 5 connection module (ESM, @companion-module/base ~2.1.3, runtime node22) for the DiGiCo S21/S31. It occupies the console's single iPad slot, controls per-channel sends/assigns to one buss, aux send level/on/pan, mutes, faders and snapshots, and relays any number of DiGiCo iPad apps through to the console byte-for-byte. The S31's iPad extension was probed live: it speaks the SD/DiGiCo Pad OSC tree (console receives on 8000, replies only to the configured iPad port 9000). tools/mock-s31.js emulates the console and test/selftest.js runs the link + relay end to end against it. docs/design-v2.md is the brief for the generic v2 rewrite. Co-Authored-By: Claude Fable 5.1 <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. |
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]
[--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. |
--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.
| Query | 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 |
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.)
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.