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:
+3
-2
@@ -166,8 +166,9 @@ different banks both receive the mixed stream. The module never sends `/Meters/c
|
|||||||
## Protocol (condensed)
|
## Protocol (condensed)
|
||||||
|
|
||||||
A query is the address plus `/?` with no argument; the reply is the same address without `/?`. All 0/1
|
A query is the address plus `/?` with no argument; the reply is the same address without `/?`. All 0/1
|
||||||
switches are **reported as floats** but written as integers 0/1; levels, sends and pans are floats. Numbers
|
switches are **reported and written as floats** (0.0 / 1.0), verified on an S31 on 2026-10-15: integer and
|
||||||
are 1-based, snapshot indexes 0-based.
|
OSC `T`/`F` writes to a switch are silently ignored (no echo, no change), unlike the 2014 DiGiCo list.
|
||||||
|
Levels, sends and pans are floats too. Numbers are 1-based, snapshot indexes 0-based.
|
||||||
|
|
||||||
| Address | Reply | Notes |
|
| Address | Reply | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
+4
-2
@@ -29,8 +29,10 @@ design note is updated.
|
|||||||
|
|
||||||
Two `static-text` blocks explain the console-side iPad extension settings and the iPad-app settings
|
Two `static-text` blocks explain the console-side iPad extension settings and the iPad-app settings
|
||||||
(generic wording: "this computer", no IPs). Nothing else. Keepalive is always on. Dialect is fixed to
|
(generic wording: "this computer", no IPs). Nothing else. Keepalive is always on. Dialect is fixed to
|
||||||
the DiGiCo Pad / SD tree (the only one the iPad extension speaks). On/off values are written as
|
the DiGiCo Pad / SD tree (the only one the iPad extension speaks). Every 0/1 switch is reported AND
|
||||||
int 0/1, levels and pans as floats (DiGiCo SD OSC list).
|
written as a float 0.0 / 1.0, as are levels and pans. Verified on an S31 (2026-10-15): int and T/F writes to a
|
||||||
|
switch are silently ignored (no echo, no change), so the 2014 DiGiCo list (which says Int) is wrong for the S31.
|
||||||
|
Snapshot triggers and meter subscriptions keep integer arguments.
|
||||||
|
|
||||||
## Topology discovery
|
## Topology discovery
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -754,7 +754,8 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
}
|
}
|
||||||
on = !cur
|
on = !cur
|
||||||
}
|
}
|
||||||
return this.write(address, [{ i: on ? 1 : 0 }], on ? 1 : 0)
|
// Verified on an S31: switches are written as float 0.0 / 1.0; int and T/F writes are silently ignored.
|
||||||
|
return this.write(address, [on ? 1 : 0], on ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeLevel(address, dB) {
|
writeLevel(address, dB) {
|
||||||
|
|||||||
+63
-26
@@ -483,12 +483,12 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
expectWrite(r, '/Input_Channels/8/fader', ',f', -60, 'input_fader_adjust from -inf lands at -60 dB')
|
expectWrite(r, '/Input_Channels/8/fader', ',f', -60, 'input_fader_adjust from -inf lands at -60 dB')
|
||||||
const mute9 = S.getMute('input', 9)
|
const mute9 = S.getMute('input', 9)
|
||||||
r = await run('input_mute', { channels: [9], state: 'toggle' })
|
r = await run('input_mute', { channels: [9], state: 'toggle' })
|
||||||
expectWrite(r, '/Input_Channels/9/mute', ',i', mute9 ? 0 : 1, 'input_mute toggle (known state)')
|
expectWrite(r, '/Input_Channels/9/mute', ',f', mute9 ? 0 : 1, 'input_mute toggle (known state)')
|
||||||
r = await run('input_mute', { channels: [10, 11], state: 'on' })
|
r = await run('input_mute', { channels: [10, 11], state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/10/mute', ',i', 1, 'input_mute on, first of two inputs')
|
expectWrite(r, '/Input_Channels/10/mute', ',f', 1, 'input_mute on, first of two inputs')
|
||||||
expectWrite(r, '/Input_Channels/11/mute', ',i', 1, 'input_mute on, second of two inputs')
|
expectWrite(r, '/Input_Channels/11/mute', ',f', 1, 'input_mute on, second of two inputs')
|
||||||
r = await run('input_solo', { channels: [12], state: 'on' })
|
r = await run('input_solo', { channels: [12], state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/12/solo', ',i', 1, 'input_solo on')
|
expectWrite(r, '/Input_Channels/12/solo', ',f', 1, 'input_solo on')
|
||||||
r = await run('input_pan_set', { channel: 13, pan: 0.25 })
|
r = await run('input_pan_set', { channel: 13, pan: 0.25 })
|
||||||
expectWrite(r, '/Input_Channels/13/Panner/pan', ',f', 0.25, 'input_pan_set')
|
expectWrite(r, '/Input_Channels/13/Panner/pan', ',f', 0.25, 'input_pan_set')
|
||||||
r = await run('input_pan_adjust', { channel: 13, pdelta: 0.5 })
|
r = await run('input_pan_adjust', { channel: 13, pdelta: 0.5 })
|
||||||
@@ -498,7 +498,7 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
|
|
||||||
// sends
|
// sends
|
||||||
r = await run('send_on', { channel: 2, buss: 3, state: 'on' })
|
r = await run('send_on', { channel: 2, buss: 3, state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/2/Aux_Send/3/send_on', ',i', 1, 'send_on on')
|
expectWrite(r, '/Input_Channels/2/Aux_Send/3/send_on', ',f', 1, 'send_on on')
|
||||||
r = await run('send_level_set', { channel: 2, buss: 3, level: -10 })
|
r = await run('send_level_set', { channel: 2, buss: 3, level: -10 })
|
||||||
expectWrite(r, '/Input_Channels/2/Aux_Send/3/send_level', ',f', -10, 'send_level_set')
|
expectWrite(r, '/Input_Channels/2/Aux_Send/3/send_level', ',f', -10, 'send_level_set')
|
||||||
r = await run('send_level_adjust', { channel: 2, buss: 3, delta: 3 })
|
r = await run('send_level_adjust', { channel: 2, buss: 3, delta: 3 })
|
||||||
@@ -514,25 +514,25 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
const q = r.findIndex((m) => m.address === '/Input_Channels/4/Aux_Send/5/send_on/?')
|
const q = r.findIndex((m) => m.address === '/Input_Channels/4/Aux_Send/5/send_on/?')
|
||||||
const w = r.findIndex((m) => m.address === '/Input_Channels/4/Aux_Send/5/send_on')
|
const w = r.findIndex((m) => m.address === '/Input_Channels/4/Aux_Send/5/send_on')
|
||||||
const replied = r[w]?.args?.[0]
|
const replied = r[w]?.args?.[0]
|
||||||
check(q === 0 && w > q && (replied === 0 || replied === 1) && r[w].tags === ',i', `toggle on unknown state queries first (query #${q}), then writes (${JSON.stringify(r[w]?.args)})`)
|
check(q === 0 && w > q && (replied === 0 || replied === 1) && r[w].tags === ',f', `toggle on unknown state queries first (query #${q}), then writes (${JSON.stringify(r[w]?.args)})`)
|
||||||
await sleep(60)
|
await sleep(60)
|
||||||
check(S.getSend(4, 5).on === (replied === 1), 'state follows the toggle (echo or optimistic)')
|
check(S.getSend(4, 5).on === (replied === 1), 'state follows the toggle (echo or optimistic)')
|
||||||
|
|
||||||
// groups
|
// groups
|
||||||
check(S.getGroup(1, 24) === undefined, 'group assign is not preloaded')
|
check(S.getGroup(1, 24) === undefined, 'group assign is not preloaded')
|
||||||
r = await run('group_assign', { channel: 3, buss: 24, state: 'off' })
|
r = await run('group_assign', { channel: 3, buss: 24, state: 'off' })
|
||||||
expectWrite(r, '/Input_Channels/3/Group_Send/24/group', ',i', 0, 'group_assign off')
|
expectWrite(r, '/Input_Channels/3/Group_Send/24/group', ',f', 0, 'group_assign off')
|
||||||
r = await run('group_assign', { channel: 3, buss: 24, state: 'on' })
|
r = await run('group_assign', { channel: 3, buss: 24, state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/3/Group_Send/24/group', ',i', 1, 'group_assign on')
|
expectWrite(r, '/Input_Channels/3/Group_Send/24/group', ',f', 1, 'group_assign on')
|
||||||
|
|
||||||
// busses: aux side vs group side
|
// busses: aux side vs group side
|
||||||
r = await run('buss_mute', { busses: [3], state: 'on' })
|
r = await run('buss_mute', { busses: [3], state: 'on' })
|
||||||
expectWrite(r, '/Aux_Outputs/3/mute', ',i', 1, 'buss_mute on an aux buss uses Aux_Outputs')
|
expectWrite(r, '/Aux_Outputs/3/mute', ',f', 1, 'buss_mute on an aux buss uses Aux_Outputs')
|
||||||
r = await run('buss_mute', { busses: [9, 24], state: 'on' })
|
r = await run('buss_mute', { busses: [9, 24], state: 'on' })
|
||||||
expectWrite(r, '/Group_Outputs/9/mute', ',i', 1, 'buss_mute on a group buss uses Group_Outputs')
|
expectWrite(r, '/Group_Outputs/9/mute', ',f', 1, 'buss_mute on a group buss uses Group_Outputs')
|
||||||
expectWrite(r, '/Group_Outputs/24/mute', ',i', 1, 'buss_mute second group buss')
|
expectWrite(r, '/Group_Outputs/24/mute', ',f', 1, 'buss_mute second group buss')
|
||||||
r = await run('buss_solo', { busses: [24], state: 'on' })
|
r = await run('buss_solo', { busses: [24], state: 'on' })
|
||||||
expectWrite(r, '/Group_Outputs/24/solo', ',i', 1, 'buss_solo')
|
expectWrite(r, '/Group_Outputs/24/solo', ',f', 1, 'buss_solo')
|
||||||
r = await run('buss_fader_set', { buss: 24, level: -3 })
|
r = await run('buss_fader_set', { buss: 24, level: -3 })
|
||||||
expectWrite(r, '/Group_Outputs/24/fader', ',f', -3, 'buss_fader_set on a group buss')
|
expectWrite(r, '/Group_Outputs/24/fader', ',f', -3, 'buss_fader_set on a group buss')
|
||||||
const auxFader = S.getFader('buss', 1)
|
const auxFader = S.getFader('buss', 1)
|
||||||
@@ -541,13 +541,13 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
|
|
||||||
// matrix
|
// matrix
|
||||||
r = await run('matrix_mute', { matrix: 2, state: 'toggle' })
|
r = await run('matrix_mute', { matrix: 2, state: 'toggle' })
|
||||||
check(writes(r).some((m) => m.address === '/Matrix_Outputs/2/mute' && m.tags === ',i'), 'matrix_mute toggle')
|
check(writes(r).some((m) => m.address === '/Matrix_Outputs/2/mute' && m.tags === ',f'), 'matrix_mute toggle')
|
||||||
r = await run('matrix_fader_set', { matrix: 2, level: -20 })
|
r = await run('matrix_fader_set', { matrix: 2, level: -20 })
|
||||||
expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -20, 'matrix_fader_set')
|
expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -20, 'matrix_fader_set')
|
||||||
r = await run('matrix_fader_adjust', { matrix: 2, delta: 4 })
|
r = await run('matrix_fader_adjust', { matrix: 2, delta: 4 })
|
||||||
expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -16, 'matrix_fader_adjust')
|
expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -16, 'matrix_fader_adjust')
|
||||||
r = await run('matrix_send_on', { matrixIn: 2, matrixOut: 3, state: 'on' })
|
r = await run('matrix_send_on', { matrixIn: 2, matrixOut: 3, state: 'on' })
|
||||||
expectWrite(r, '/Matrix_Inputs/2/Matrix_Send/3/send_on', ',i', 1, 'matrix_send_on')
|
expectWrite(r, '/Matrix_Inputs/2/Matrix_Send/3/send_on', ',f', 1, 'matrix_send_on')
|
||||||
r = await run('matrix_send_level_set', { matrixIn: 2, matrixOut: 3, level: -30 })
|
r = await run('matrix_send_level_set', { matrixIn: 2, matrixOut: 3, level: -30 })
|
||||||
expectWrite(r, '/Matrix_Inputs/2/Matrix_Send/3/send_level', ',f', -30, 'matrix_send_level_set')
|
expectWrite(r, '/Matrix_Inputs/2/Matrix_Send/3/send_level', ',f', -30, 'matrix_send_level_set')
|
||||||
r = await run('matrix_send_level_adjust', { matrixIn: 3, matrixOut: 2, delta: 1 })
|
r = await run('matrix_send_level_adjust', { matrixIn: 3, matrixOut: 2, delta: 1 })
|
||||||
@@ -556,9 +556,9 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
|
|
||||||
// control groups (1-based)
|
// control groups (1-based)
|
||||||
r = await run('cg_mute', { cg: 1, state: 'on' })
|
r = await run('cg_mute', { cg: 1, state: 'on' })
|
||||||
expectWrite(r, '/Control_Groups/1/mute', ',i', 1, 'cg_mute')
|
expectWrite(r, '/Control_Groups/1/mute', ',f', 1, 'cg_mute')
|
||||||
r = await run('cg_solo', { cg: 10, state: 'on' })
|
r = await run('cg_solo', { cg: 10, state: 'on' })
|
||||||
expectWrite(r, '/Control_Groups/10/solo', ',i', 1, 'cg_solo')
|
expectWrite(r, '/Control_Groups/10/solo', ',f', 1, 'cg_solo')
|
||||||
r = await run('cg_fader_set', { cg: 2, level: -6 })
|
r = await run('cg_fader_set', { cg: 2, level: -6 })
|
||||||
expectWrite(r, '/Control_Groups/2/fader', ',f', -6, 'cg_fader_set')
|
expectWrite(r, '/Control_Groups/2/fader', ',f', -6, 'cg_fader_set')
|
||||||
r = await run('cg_fader_adjust', { cg: 2, delta: -2 })
|
r = await run('cg_fader_adjust', { cg: 2, delta: -2 })
|
||||||
@@ -566,7 +566,7 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
|
|
||||||
// graphic EQ
|
// graphic EQ
|
||||||
r = await run('geq_on', { geq: 1, state: 'off' })
|
r = await run('geq_on', { geq: 1, state: 'off' })
|
||||||
expectWrite(r, '/Graphic_EQ/1/geq_in', ',i', 0, 'geq_on off')
|
expectWrite(r, '/Graphic_EQ/1/geq_in', ',f', 0, 'geq_on off')
|
||||||
r = await run('geq_band_set', { geq: 1, band: 5, gain: 3 })
|
r = await run('geq_band_set', { geq: 1, band: 5, gain: 3 })
|
||||||
expectWrite(r, '/Graphic_EQ/1/geq_gain_5', ',f', 3, 'geq_band_set')
|
expectWrite(r, '/Graphic_EQ/1/geq_gain_5', ',f', 3, 'geq_band_set')
|
||||||
r = await run('geq_band_adjust', { geq: 1, band: 5, delta: 1 })
|
r = await run('geq_band_adjust', { geq: 1, band: 5, delta: 1 })
|
||||||
@@ -580,18 +580,18 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
r = await run('input_phantom', { channel: 14, state: 'toggle' })
|
r = await run('input_phantom', { channel: 14, state: 'toggle' })
|
||||||
const pq = r.findIndex((m) => m.address === '/Input_Channels/14/Channel_Input/phantom/?')
|
const pq = r.findIndex((m) => m.address === '/Input_Channels/14/Channel_Input/phantom/?')
|
||||||
const pw = r.findIndex((m) => m.address === '/Input_Channels/14/Channel_Input/phantom')
|
const pw = r.findIndex((m) => m.address === '/Input_Channels/14/Channel_Input/phantom')
|
||||||
check(pq === 0 && pw > pq && r[pw].tags === ',i' && r[pw].args[0] === 1, `input_phantom toggle on an unloaded switch queries first, then writes the opposite of the replied 0 (${JSON.stringify(r[pw]?.args)})`)
|
check(pq === 0 && pw > pq && r[pw].tags === ',f' && r[pw].args[0] === 1, `input_phantom toggle on an unloaded switch queries first, then writes the opposite of the replied 0 (${JSON.stringify(r[pw]?.args)})`)
|
||||||
r = await run('input_lpf_in', { channel: 14, state: 'on' })
|
r = await run('input_lpf_in', { channel: 14, state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/14/Filters/hi_filter_in', ',i', 1, 'input_lpf_in on (hi_filter_in is the low-pass filter)')
|
expectWrite(r, '/Input_Channels/14/Filters/hi_filter_in', ',f', 1, 'input_lpf_in on (hi_filter_in is the low-pass filter)')
|
||||||
r = await run('input_hpf_in', { channel: 14, state: 'on' })
|
r = await run('input_hpf_in', { channel: 14, state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/14/Filters/lo_filter_in', ',i', 1, 'input_hpf_in on (lo_filter_in is the high-pass filter)')
|
expectWrite(r, '/Input_Channels/14/Filters/lo_filter_in', ',f', 1, 'input_hpf_in on (lo_filter_in is the high-pass filter)')
|
||||||
r = await run('input_pad', { channel: 14, state: 'off' })
|
r = await run('input_pad', { channel: 14, state: 'off' })
|
||||||
expectWrite(r, '/Input_Channels/14/Channel_Input/input_pad', ',i', 0, 'input_pad off')
|
expectWrite(r, '/Input_Channels/14/Channel_Input/input_pad', ',f', 0, 'input_pad off')
|
||||||
r = await run('input_phase', { channel: 14, state: 'on' })
|
r = await run('input_phase', { channel: 14, state: 'on' })
|
||||||
expectWrite(r, '/Input_Channels/14/Channel_Input/phase', ',i', 1, 'input_phase on')
|
expectWrite(r, '/Input_Channels/14/Channel_Input/phase', ',f', 1, 'input_phase on')
|
||||||
for (const [id, path] of [['eq_in', 'EQ/eq_in'], ['comp_in', 'Dynamics/comp_in'], ['gate_in', 'Dynamics/gate_in'], ['delay_on', 'Channel_Delay/delay_on'], ['insert_a', 'Insert/insert_A_in'], ['insert_b', 'Insert/insert_B_in']]) {
|
for (const [id, path] of [['eq_in', 'EQ/eq_in'], ['comp_in', 'Dynamics/comp_in'], ['gate_in', 'Dynamics/gate_in'], ['delay_on', 'Channel_Delay/delay_on'], ['insert_a', 'Insert/insert_A_in'], ['insert_b', 'Insert/insert_B_in']]) {
|
||||||
r = await run(`input_${id}`, { channel: 14, state: 'on' })
|
r = await run(`input_${id}`, { channel: 14, state: 'on' })
|
||||||
expectWrite(r, `/Input_Channels/14/${path}`, ',i', 1, `input_${id} on`)
|
expectWrite(r, `/Input_Channels/14/${path}`, ',f', 1, `input_${id} on`)
|
||||||
}
|
}
|
||||||
r = await run('input_hpf_freq_set', { channel: 14, value: 80 })
|
r = await run('input_hpf_freq_set', { channel: 14, value: 80 })
|
||||||
expectWrite(r, '/Input_Channels/14/Filters/lo_filter_freq', ',f', 80, 'input_hpf_freq_set')
|
expectWrite(r, '/Input_Channels/14/Filters/lo_filter_freq', ',f', 80, 'input_hpf_freq_set')
|
||||||
@@ -614,11 +614,11 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
r = await run('matrix_trim_set', { matrix: 2, value: 1.5 })
|
r = await run('matrix_trim_set', { matrix: 2, value: 1.5 })
|
||||||
expectWrite(r, '/Matrix_Outputs/2/Buss_Trim/trim', ',f', 1.5, 'matrix_trim_set')
|
expectWrite(r, '/Matrix_Outputs/2/Buss_Trim/trim', ',f', 1.5, 'matrix_trim_set')
|
||||||
r = await run('buss_eq_in', { buss: 3, state: 'toggle' })
|
r = await run('buss_eq_in', { buss: 3, state: 'toggle' })
|
||||||
check(r[0]?.address === '/Aux_Outputs/3/EQ/eq_in/?' && writes(r).some((m) => m.address === '/Aux_Outputs/3/EQ/eq_in' && m.tags === ',i'), 'buss_eq_in toggle on an unloaded aux buss queries first')
|
check(r[0]?.address === '/Aux_Outputs/3/EQ/eq_in/?' && writes(r).some((m) => m.address === '/Aux_Outputs/3/EQ/eq_in' && m.tags === ',f'), 'buss_eq_in toggle on an unloaded aux buss queries first')
|
||||||
r = await run('buss_insert_a', { buss: 9, state: 'on' })
|
r = await run('buss_insert_a', { buss: 9, state: 'on' })
|
||||||
expectWrite(r, '/Group_Outputs/9/Insert/insert_A_in', ',i', 1, 'buss_insert_a on a group buss')
|
expectWrite(r, '/Group_Outputs/9/Insert/insert_A_in', ',f', 1, 'buss_insert_a on a group buss')
|
||||||
r = await run('buss_phase', { buss: 3, state: 'on' })
|
r = await run('buss_phase', { buss: 3, state: 'on' })
|
||||||
expectWrite(r, '/Aux_Outputs/3/Buss_Trim/phase', ',i', 1, 'buss_phase on')
|
expectWrite(r, '/Aux_Outputs/3/Buss_Trim/phase', ',f', 1, 'buss_phase on')
|
||||||
|
|
||||||
// names, snapshots, raw, relay
|
// names, snapshots, raw, relay
|
||||||
r = await run('snapshot_fire', { snapshot: 5 })
|
r = await run('snapshot_fire', { snapshot: 5 })
|
||||||
@@ -647,6 +647,43 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
check(seq.filter((a) => a === '/Control_Groups/3/fader/?').length === 1 && seq.indexOf('/Control_Groups/3/fader/?') > seq.indexOf('/Control_Groups/3/fader'), 'one confirm query of the same address follows when the write guard ends')
|
check(seq.filter((a) => a === '/Control_Groups/3/fader/?').length === 1 && seq.indexOf('/Control_Groups/3/fader/?') > seq.indexOf('/Control_Groups/3/fader'), 'one confirm query of the same address follows when the write guard ends')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A 0/1 switch takes a FLOAT write only (verified on a real S31): the mock ignores int and T/F writes like the console does
|
||||||
|
{
|
||||||
|
const address = '/Input_Channels/30/mute'
|
||||||
|
const before = S.getMute('input', 30)
|
||||||
|
const want = before ? 0 : 1
|
||||||
|
const seen = []
|
||||||
|
const original = inst.onConsoleMessage.bind(inst)
|
||||||
|
inst.onConsoleMessage = (msg, rinfo) => {
|
||||||
|
if (msg.address === address) seen.push(msg)
|
||||||
|
return original(msg, rinfo)
|
||||||
|
}
|
||||||
|
const raw = dgram.createSocket('udp4')
|
||||||
|
const sendRaw = (args) => new Promise((resolve) => raw.send(encode(address, args), P.mock, '127.0.0.1', resolve))
|
||||||
|
await sendRaw([{ i: want }])
|
||||||
|
await sendRaw([true])
|
||||||
|
await sendRaw([false])
|
||||||
|
await sendRaw([{ i: before ? 0 : 1 }])
|
||||||
|
await sleep(500)
|
||||||
|
check(seen.length === 0 && S.getMute('input', 30) === before, 'int and T/F writes to a switch are ignored by the mock: no echo and no state change')
|
||||||
|
inst.queryNow(address)
|
||||||
|
check(await waitFor(() => seen.length === 1, 2000) && seen[0].tags === ',f' && seen[0].args[0] === (before ? 1 : 0), 'reading it back still gives the old value, as a float')
|
||||||
|
await sendRaw([want])
|
||||||
|
check(await waitFor(() => seen.length === 2, 2000) && seen[1].tags === ',f' && seen[1].args[0] === want && S.getMute('input', 30) === !before, 'a float write is accepted and echoed as a float')
|
||||||
|
await sendRaw([before ? 1 : 0]) // restore
|
||||||
|
await sleep(300)
|
||||||
|
inst.onConsoleMessage = original
|
||||||
|
raw.close()
|
||||||
|
|
||||||
|
// and the module's own switch writes are floats, so the console keeps them (the confirm query after the guard agrees)
|
||||||
|
const mark = sent.length
|
||||||
|
const was31 = S.getMute('input', 31)
|
||||||
|
await run('input_mute', { channels: [31], state: 'toggle' })
|
||||||
|
await sleep(inst.timing.writeGuardMs + 500)
|
||||||
|
const w = sent.slice(mark).find((m) => m.address === '/Input_Channels/31/mute' && !m.address.endsWith('/?'))
|
||||||
|
check(!!w && decodeArgs(w).tags === ',f' && S.getMute('input', 31) === !was31, 'a module switch write goes out as a float and the console accepted it (the confirm query returns the written value)')
|
||||||
|
}
|
||||||
|
|
||||||
// Names: renames rebuild the definitions once (debounced) and the dropdowns pick them up
|
// Names: renames rebuild the definitions once (debounced) and the dropdowns pick them up
|
||||||
{
|
{
|
||||||
const before = ctx.rebuilds
|
const before = ctx.rebuilds
|
||||||
|
|||||||
+5
-2
@@ -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) |
|
| `/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/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 |
|
| `/{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/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_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/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
|
`/Meters/values ,ii... [id, v, id, v, ...]` (only ids whose value changed). `/Meters/clear` drops every
|
||||||
subscription, like the real console.
|
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
|
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,
|
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`.)
|
sets of read-only names. (`--bool-int` has no effect in `sd`.)
|
||||||
|
|||||||
+10
-7
@@ -23,7 +23,8 @@
|
|||||||
* A QUERY is the address plus a trailing "/?" and no args; the reply is the
|
* 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
|
* 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
|
* "/?" 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.
|
* meters to subscribed ids and implements the write-only snapshot triggers.
|
||||||
* Anything the real console does not answer (lowercase S-Series addresses,
|
* Anything the real console does not answer (lowercase S-Series addresses,
|
||||||
* /Console/Resend, /Console/Channels/?, whole-send queries, ...) gets no
|
* /Console/Resend, /Console/Channels/?, whole-send queries, ...) gets no
|
||||||
@@ -165,8 +166,8 @@ function mulberry32(seed) {
|
|||||||
* s string
|
* s string
|
||||||
* f float32 with min/max clamp
|
* f float32 with min/max clamp
|
||||||
* b boolean (sseries; sent as T/F, or int 0/1 with --bool-int)
|
* 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,
|
* flag 0/1 number (sd; a SET is accepted only as a float 0.0 / 1.0, exactly like the S31: int and
|
||||||
* or as an int when the spec says wire: 'i')
|
* 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)
|
* int integer with min/max clamp (sd)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -803,12 +804,12 @@ function sdResolve(address) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSd(address, args) {
|
function handleSd(address, args, types) {
|
||||||
if (address.endsWith('/?')) {
|
if (address.endsWith('/?')) {
|
||||||
if (args.length !== 0) return ignore(`query ${address} carries arguments ${formatArgs(args)}`);
|
if (args.length !== 0) return ignore(`query ${address} carries arguments ${formatArgs(args)}`);
|
||||||
return sdQuery(address.slice(0, -2), address);
|
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). */
|
/** 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)]);
|
send('/Snapshots/Current_Snapshot', [int(sdSnapshot)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sdSet(address, args) {
|
function sdSet(address, args, types) {
|
||||||
// Write-only trigger nodes
|
// Write-only trigger nodes
|
||||||
if (address === '/Snapshots/Fire_Snapshot_number') {
|
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)}`);
|
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)`);
|
if (!r) return ignore(`no reply for ${address} (unknown or unsupported address)`);
|
||||||
const spec = r.spec;
|
const spec = r.spec;
|
||||||
if (spec.readOnly) return ignore(`${address} is read-only`);
|
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)})`);
|
if (args.length !== 1) return ignore(`${address} needs exactly one argument (got ${formatArgs(args)})`);
|
||||||
const value = coerce(spec, args[0]);
|
const value = coerce(spec, args[0]);
|
||||||
if (value === undefined) return ignore(`unusable value ${formatArgs(args)} for ${address}`);
|
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);
|
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) => {
|
socket.on('error', (err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user