# 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/?`, `/Matrix_Inputs/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. Names are padded with trailing spaces and runs of spaces, as the surface reports them (matrix input names are read-only here) | | `/{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). A matrix send floors at -90 dB (verified on an S31): level writes are clamped to -90..10, never -150 as for faders and aux sends | | `/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/?`, `/Matrix_Inputs/N/Channel_Input/{trim,phase}/?` | `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), `/Input_Channels/N/Dynamics/GR_meter ,ii [id, 1|4]` (gate / compressor gain reduction) or `/{Aux_Outputs,Group_Outputs,Matrix_Outputs}/N/Output/meter ,ii [id, 1]` (output level), where the client chooses `id`. The scale is inverted, as on the real console (verified on an S31, approximate): a level meter reads about the dB below full scale, so **silence is 127** and the mock's live channels (inputs 1-3, aux 1, group 9, matrix output 1) read 20-60; a gain reduction meter reads the dB of reduction, 0-12, now and then 127 (inactive). Every subscribed id gets its value about five times a second, as `/Meters/values ,ii... [id, v, id, v, ...]` (only ids whose value changed, so a silent channel reports 127 once). `/Meters/clear` drops every subscription, like the real console. Control groups and matrix inputs have no meter address. 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/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 ```sh # 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) ```sh # 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`.