Files
companion-module-digico-s31/tools/README.md
T
michaelandClaude Fable 5.1 131cdfd81f Companion module for the DiGiCo S31 with an iPad relay (v1)
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>
2026-10-15 15:55:18 -04:00

7.5 KiB

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/pong counts 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/Name reply 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.