A DANTE hold on missing hardware restarts Companion's Dante module.

Dante Virtual Soundcard on the four Macs is restarted directly; for hardware
(consoles, racks, cards, AVIOs) nothing can be restarted from here, so a hold
re-checks via a fresh Dante module connection (harmless to audio and routing).
If the device is still gone it is really off or unplugged. A check's fix can
now be "restart-connection", and a fix map can use "*" for every other sub-check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 12:42:00 -04:00
co-authored by Claude Opus 5.5
parent df4da8f42e
commit 7c5f086bd5
3 changed files with 20 additions and 12 deletions
+1 -1
View File
@@ -72,7 +72,7 @@
{ "id": "dante", "label": "DANTE", "pos": [2, 2], "checks": [ { "id": "dante", "label": "DANTE", "pos": [2, 2], "checks": [
{ "type": "dante", "conn": "audinate-dantecontroller", { "type": "dante", "conn": "audinate-dantecontroller",
"fix": { "NA-ProPresenter-Mac": "dvs_prop", "NA-Tracks": "dvs_tracks", "NA-Translation": "dvs_translation", "fix": { "NA-ProPresenter-Mac": "dvs_prop", "NA-Tracks": "dvs_tracks", "NA-Translation": "dvs_translation",
"STUDIO-A-Ingest": "dvs_ingest" }, "STUDIO-A-Ingest": "dvs_ingest", "*": "restart-connection" },
"devices": { "devices": {
"FOH-DiGiCo-DMI-1": "DMI-1", "FOH-DiGiCo-DMI-2": "DMI-2", "BOH-WING": "WING", "BOH-DQ-Rack": "DQ RACK", "FOH-DiGiCo-DMI-1": "DMI-1", "FOH-DiGiCo-DMI-2": "DMI-2", "BOH-WING": "WING", "BOH-DQ-Rack": "DQ RACK",
"BOH-AD4Q-A": "AD4Q-A", "BOH-AD4Q-B": "AD4Q-B", "STUDIO-A-X32": "STA X32", "BOH-AD4Q-A": "AD4Q-A", "BOH-AD4Q-B": "AD4Q-B", "STUDIO-A-X32": "STA X32",
+13 -3
View File
@@ -452,6 +452,9 @@ CHECKS = {
# Checks whose connection a long press may restart (Dante is shared, so not that one). # Checks whose connection a long press may restart (Dante is shared, so not that one).
RESTARTABLE = ("conn", "hyperdeck", "shure", "psm", "tascam") RESTARTABLE = ("conn", "hyperdeck", "shure", "psm", "tascam")
# A check's "fix" may name this instead of an entry in "fixes": restart its own Companion
# connection. The Dante button uses it for hardware, which cannot be restarted from here.
RESTART_CONNECTION = "restart-connection"
def needed_variables(cfg): def needed_variables(cfg):
@@ -750,7 +753,13 @@ class Engine:
continue continue
fix_id = chk.get("fix") fix_id = chk.get("fix")
if isinstance(fix_id, dict): if isinstance(fix_id, dict):
fix_id = fix_id.get(sub) fix_id = fix_id.get(sub, fix_id.get("*")) # "*": every other sub-check
if fix_id == RESTART_CONNECTION and chk.get("conn"):
ident = ("restart", chk["conn"]) # e.g. Companion's Dante module
if ident not in seen:
seen.add(ident)
plan.append((ident, "restart", chk["conn"], chk))
continue
if fix_id: if fix_id:
if chk["type"] == "hyperdeck" and hyperdeck_recording(snap, chk["conn"]): if chk["type"] == "hyperdeck" and hyperdeck_recording(snap, chk["conn"]):
continue continue
@@ -1233,9 +1242,10 @@ def load_config(path):
for c in t.get("checks", []): for c in t.get("checks", []):
fixes = c.get("fix") if isinstance(c.get("fix"), dict) else {"": c.get("fix")} fixes = c.get("fix") if isinstance(c.get("fix"), dict) else {"": c.get("fix")}
for f in fixes.values(): for f in fixes.values():
if f and f not in cfg.get("fixes", {}): if f and f != RESTART_CONNECTION and f not in cfg.get("fixes", {}):
raise ValueError("tile %s: unknown fix %s" % (t["id"], f)) raise ValueError("tile %s: unknown fix %s" % (t["id"], f))
if f and cfg["fixes"][f].get("kind", "ssh") == "ssh" and cfg["fixes"][f]["host"] not in cfg.get("hosts", {}): if (f and f != RESTART_CONNECTION and cfg["fixes"][f].get("kind", "ssh") == "ssh"
and cfg["fixes"][f]["host"] not in cfg.get("hosts", {})):
raise ValueError("fix %s: unknown host" % f) raise ValueError("fix %s: unknown host" % f)
if c["type"] == "remote" and c["host"] not in cfg.get("hosts", {}): if c["type"] == "remote" and c["host"] not in cfg.get("hosts", {}):
raise ValueError("tile %s: unknown host %s" % (t["id"], c["host"])) raise ValueError("tile %s: unknown host %s" % (t["id"], c["host"]))
+6 -8
View File
@@ -461,9 +461,9 @@ class TestFixes(unittest.TestCase):
def test_dante_reopens_dvs_on_the_right_mac(self): def test_dante_reopens_dvs_on_the_right_mac(self):
devices = json.loads(self.snap.vars[("audinate-dantecontroller", "devices")]) devices = json.loads(self.snap.vars[("audinate-dantecontroller", "devices")])
devices.remove("NA-ProPresenter-Mac") devices.remove("NA-ProPresenter-Mac")
devices.remove("BOH-WING") # no fix for a console devices.remove("BOH-WING") # hardware: re-check via Companion's Dante module
self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices)
self.assertEqual(self.plan("dante"), [("fix", "dvs_prop")]) self.assertEqual(self.plan("dante"), [("restart", "audinate-dantecontroller"), ("fix", "dvs_prop")])
def test_rec_reboots_only_hung_decks_not_recording_ones(self): def test_rec_reboots_only_hung_decks_not_recording_ones(self):
for i in (2, 3, 5): for i in (2, 3, 5):
@@ -543,13 +543,11 @@ class TestFixes(unittest.TestCase):
devices.remove("STUDIO-A-Ingest") devices.remove("STUDIO-A-Ingest")
self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices)
self.assertEqual(self.plan("dante"), [("fix", "dvs_ingest")]) self.assertEqual(self.plan("dante"), [("fix", "dvs_ingest")])
# A failure with no fix (a console) must not claim there is nothing wrong. # A failure with no fix must not claim there is nothing wrong.
devices.append("STUDIO-A-Ingest") self.snap.midi["NA Ingest"]["peers"] = []
devices.remove("BOH-WING")
self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices)
_, t = run_steps(self.engine, self.snap, SUNDAY, 4) _, t = run_steps(self.engine, self.snap, SUNDAY, 4)
self.engine.run_fixes("dante", self.snap, None, t) self.engine.run_fixes("midi", self.snap, None, t)
self.assertEqual(self.engine.notes["dante"]["text"], "NO AUTO\nFIX") self.assertEqual(self.engine.notes["midi"]["text"], "NO AUTO\nFIX")
def test_hold_on_a_green_button_says_so(self): def test_hold_on_a_green_button_says_so(self):
_, t = run_steps(self.engine, self.snap, SUNDAY, 2) _, t = run_steps(self.engine, self.snap, SUNDAY, 2)