diff --git a/README.md b/README.md index e6c3381..02b1410 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ There are two parts: Row 0 holds the group buttons. Each one shows the first problem in its group, plus "+N" if there are more: - REC: HD1–6 and the Tascam. -- AUDIO: X32 and Dante. +- AUDIO: X32, DiGiCo and Dante. - RF: all six Shure units (AD4Q ×2, ULXD4, PSM1000 ×3). - SHOW: MIDI, APPS, VISTA and SPOTIFY. SPOTIFY has no key of its own any more; it shows inside SHOW, and its force-play key is on the Restart page. @@ -177,6 +177,7 @@ The **page number key** on every page follows the ALL button. It's green when ev | | HD2 and HD4 are `"optional": true` in `config.json`: with no disk they sit grey with no alarm, and once a disk is in they are watched like the others. Add or remove `"optional": true` on any deck to change that. | | TASCAM | connection; the recorder's own Error / Caution. Use the **1.3.1-quiet** Tascam module (see below) so Companion's log isn't flooded.; pinged at the address on its Companion connection. | | X32, ARENA, OBS | Companion connection | +| DIGICO (inside AUDIO) | Companion's `DiGiCo_FOH` connection; the relay helper `org.waterschurch.digico-relay` on NA Ingest (`~/digico-bridge/relay.py`: Companion and the DiGiCo tablets share the console's one External Control slot through it), **DIGICO RELAY STOPPED** / **NOT LOADED**, hold AUDIO restarts it; the console itself answers ping at 10.1.60.100, **DIGICO NO PING**. Companion's OSC connection reads "good" even with the console off, so the ping is what catches that. | | ATEM | Companion connection; the switcher's **audio** (every Fairlight input's on/off/AFV and fader, e.g. Resi on input 16 ON, TRS off) and **camera settings** (iris, white balance, shutter, gain, detail, ND, lift/gamma/gain/offset, contrast, saturation) must match the restore file `atem-reference.xml` (a copy of `4k plus 2026-10-07 14-59-55.xml` from Google Drive Backup/NA/ATEM): **ATEM AUDIO WRONG** / **ATEM CAMERA WRONG**; ATEM Software Control's *Enable MIDI control* must be off: **ATEM SW MIDI ON**. Hold 2 s (and Sunday auto-fix) sends back only what differs; MIDI is fixed by quitting the app, switching it off and reopening it (the switcher is not affected). `atem_watch.js` keeps one connection to the switcher. New restore file: copy it over `atem-reference.xml` and re-run install.sh. | | LED | VX1000 and UHD Jr: Companion connection **and** each one answers ping (the NovaStar module keeps saying "Connected" after a processor is switched off). "UHDJR NO PING" = processor off or unreachable; "UHDJR OFFLINE" = Companion's connection failed; "UHDJR DISABLED" (amber) = connection turned off in Companion. | | DANTE | All 19 Dante devices ("19/19"), named when one drops: "WING GONE", "TRACKS GONE", "PROP MAC GONE". | diff --git a/config.json b/config.json index 2d4a8ae..cf214ba 100644 --- a/config.json +++ b/config.json @@ -797,6 +797,18 @@ "wait_seconds": 45, "dante": "STUDIO-B-Tascam" } + }, + "digico_relay": { + "kind": "local", + "short": "DIGICO", + "label": "restart the DiGiCo relay (Companion and the tablets to the FOH console)", + "run": "launchctl kickstart -k gui/$(id -u)/org.waterschurch.digico-relay || launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.waterschurch.digico-relay.plist", + "template": "helpers", + "params": { + "labels": [ + "org.waterschurch.digico-relay" + ] + } } }, "atem": { @@ -1337,6 +1349,7 @@ ], "members": [ "x32", + "digico", "dante" ] }, @@ -1542,6 +1555,28 @@ } ] }, + { + "id": "digico", + "label": "DIGICO", + "checks": [ + { + "type": "conn", + "conn": "DiGiCo_FOH", + "tag": "DIGICO" + }, + { + "type": "launchd", + "label": "org.waterschurch.digico-relay", + "name": "RELAY", + "fix": "digico_relay" + }, + { + "type": "ping", + "host": "10.1.60.100", + "name": "DIGICO" + } + ] + }, { "id": "dante", "label": "DANTE", diff --git a/settings/ui/app.js b/settings/ui/app.js index 4daed26..b3ebf38 100644 --- a/settings/ui/app.js +++ b/settings/ui/app.js @@ -420,6 +420,9 @@ const c = cfg(), t = c.tiles[i]; const k = await api("/api/keys"); const kt = k.tiles.find((x) => x.id === t.id) || { says: [], holding: "", checks: [] }; + // A Companion connection is picked from the ones Companion has (typing still works). + const cc = editing() ? await api("/api/companion").catch(() => ({})) : {}; + const connList = `${(cc.connections || []).map((r) => ``).join("")}`; const fixOptions = (cur) => `${Object.entries(c.fixes).map(([id, f]) => ``).join("")}`; const short = (text, n) => { text = String(text || ""); return text.length > n ? text.slice(0, n - 1) + "…" : text; }; let lastQ = null; @@ -432,7 +435,9 @@ const summary = `${what}${hint(sentence)}`; if (!editing()) return `${header}
${summary}

${esc(sentence)}

`; const base = `tiles.${i}.checks.${j}.`; - let fields = (spec.fields || []).filter((f) => !["fix", "type", "template", "params"].includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join(""); + let fields = (spec.fields || []).filter((f) => !["fix", "type", "template", "params"].includes(f)).map((f) => f === "conn" + ? inputHtml(base + f, F("tiles[].checks[]." + f), chk[f], connList).replace(" x.name === ctName); @@ -1013,7 +1018,14 @@ for (const b of main.querySelectorAll("[data-oremove]")) b.onclick = () => confirmBox(`Remove the ${b.dataset.oremove} outlet from the list? Fixes that use it stop working.`, "Remove", async () => { try { await post("/api/outlets", { name: b.dataset.oremove, remove: true }); route(); } catch (e) { alertBox(e.message); } }); for (const td of main.querySelectorAll("[id^='ost-']")) api("/api/outlets/status?name=" + encodeURIComponent(td.id.slice(4))).then((r) => { td.innerHTML = r.ok ? `${r.on ? "on" : "off"}` : `?`; }).catch(() => {}); for (const m of main.querySelectorAll("[data-map]")) m.onchange = () => { if (!m.value || !ensureDraft()) return; const [kind, name] = m.dataset.map.split("|"); (kind === "conn" ? renameConn : renameDante)(name, m.value); saveDraft(); route(); }; - for (const b of main.querySelectorAll("[data-addconn]")) b.onclick = () => { const conn = b.dataset.addconn; if (!ensureDraft()) return; const d = state.draft.config; let id = slug(conn) || "key", n = 2; while (d.tiles.some((t) => t.id === id)) id = slug(conn) + "_" + n++; const t = { id, label: conn.toUpperCase().replace(/[_-]+/g, " ").slice(0, 10).trim(), checks: [{ type: "conn", conn, fix: "restart-connection" }] }; const pos = firstEmpty(d, "status"); if (pos) t.pos = pos; d.tiles.push(t); saveDraft(); location.hash = "#keys/" + id; }; + for (const b of main.querySelectorAll("[data-addconn]")) b.onclick = () => { const conn = b.dataset.addconn; if (!ensureDraft()) return; const d = state.draft.config; let id = slug(conn) || "key", n = 2; while (d.tiles.some((t) => t.id === id)) id = slug(conn) + "_" + n++; const t = { id, label: conn.toUpperCase().replace(/[_-]+/g, " ").slice(0, 10).trim(), checks: [{ type: "conn", conn, fix: "restart-connection" }] }; const pos = firstEmpty(d, "status"); + if (pos) { t.pos = pos; d.tiles.push(t); saveDraft(); location.hash = "#keys/" + id; return; } + // The Status page is full: put it inside a group key, where it shows and a hold on the group fixes it. + const groups = d.tiles.filter((g) => g.members); + dialog(`

No free spot on the Status page

Put ${esc(conn)} inside a group key: its problems show on that key, and holding the key reconnects it.

`, (bx) => { + bx.querySelector("#dcancel").onclick = closeDialog; + bx.querySelector("#dok").onclick = () => { const g = d.tiles.find((x) => x.id === bx.querySelector("#ngroup").value); d.tiles.push(t); if (g) g.members.push(id); saveDraft(); closeDialog(); location.hash = "#keys/" + id; }; + }); }; for (const cb of main.querySelectorAll("[data-autofix]")) cb.onchange = () => { if (!ensureDraft()) return; const d = state.draft.config, id = cb.dataset.autofix, f = d.fixes[id]; const set = new Set(d.auto_fix.fixes || []); if (cb.checked) { set.add(id); if (closes(f)) f.restart_ok = true; if (f.never_auto) delete f.never_auto; } else set.delete(id); setPath("auto_fix.fixes", [...set]); }; for (const b of main.querySelectorAll("[data-rmcheck]")) b.onclick = () => confirmBox("Remove this MIDI link from its key?", "Remove", () => { if (!ensureDraft()) return; const [tid, j] = b.dataset.rmcheck.split("|"); const t = state.draft.config.tiles.find((x) => x.id === tid); t.checks.splice(+j, 1); saveDraft(); route(); }); const ds = q("#decksave"); if (ds) ds.onclick = () => applyDecks(main); diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index 5d62ef7..e36b023 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -68,7 +68,7 @@ def healthy_snapshot(now): snap.ping = {"10.1.3.227": 0, "10.6.7.13": 0, "10.1.0.1": 0} snap.link = True snap.launchd = {"org.waterschurch.resi-propresenter": 884, "org.waterschurch.lobby-captions": 900, - "org.waterschurch.lobby-captions-display": 901} + "org.waterschurch.lobby-captions-display": 901, "org.waterschurch.digico-relay": 902} snap.launchd_time = now snap.procs = {"/Applications/ProdDash.app/Contents/MacOS/ProdDash": True} snap.http = {"https://central.resi.io/": True, "http://10.6.7.13:8080/": True, "http://10.1.14.225:1025/": True}