From c0c83459e565a38d963d254fe039dbd4d6f8d6fa Mon Sep 17 00:00:00 2001 From: christian Date: Thu, 8 Oct 2026 09:22:38 -0400 Subject: [PATCH] Regroup Dante, MIDI and apps into one button each, and fix the morning Bonjour check. DANTE now alarms for all 19 devices by name. MIDI covers the Lighting PC and SuperRack peers plus the Lighting PC and NA Tracks sessions; APPS covers ProPresenter, Ableton, the tracks Mac and the Bible TV app. Five buttons became three, and a check's tag names the device in the message. dns-sd pads hours before 10 AM with a space, which the Bonjour parser did not allow for, so every session read as gone each morning. Parsing is now its own function with a test for both cases. Co-Authored-By: Claude Opus 5.5 --- README.md | 39 ++++++++++++++++++++++++++++++-- config.json | 43 ++++++++++++++---------------------- status-page.companionconfig | Bin 4785 -> 4378 bytes status_panel.py | 28 ++++++++++++++--------- tests/test_status_panel.py | 37 +++++++++++++++++++++++-------- 5 files changed, 100 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 6d9f937..fddc7c9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ There are two parts: |--------|-------|-------|-------|-------|-------|-------|-------| | row 0 | **ALL** | REC | AUDIO | RF | SHOW | VIDEO | STREAM | | row 1 | HD1 | HD2 | HD3 | HD4 | HD5 | HD6 | TASCAM | -| row 2 | X32 | DANTE | TRACKS | LIGHT PC | RACK (SuperRack) | BIBLE TV | PROP (ProPresenter) | +| row 2 | X32 | DANTE | MIDI | APPS | | | | | row 3 | ATEM | OBS | RESI | NET | INGEST | LED | ARENA | Column 0 holds page up / page number / page down, plus **SERVICE** at row 3. @@ -46,7 +46,42 @@ Row 0 holds the group tiles. Each one shows the first problem in its group, plus | | 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 | | X32, ATEM, ARENA, LED, OBS | Companion connection | -| DANTE | Shows all 19 watched Dante devices as "19/19" (matches Dante Controller). Alarms for 16 of them: DiGiCo DMIs, WING, DQ rack, AD4Qs, Studio A X32 / Resi AJA / Ingest, Translation, and Studio B (RedNet TNX, TB, TB MON, MON, SOLO MON, Tascam). NA-Tracks, SuperRack and the ProPresenter Mac are on their own buttons. | +| DANTE | All 19 Dante devices ("19/19"), named when one drops: "WING GONE", "TRACKS GONE", "PROP MAC GONE". | +| MIDI | Lighting PC and SuperRack still joined to NA Ingest's network MIDI session; the Lighting PC and NA Tracks MIDI sessions still on the network. A clean disconnect shows within seconds; a crash or pulled cable takes about a minute (macOS's own timeout, measured). | +| APPS | ProPresenter API; Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC. Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections. | +| RESI | NET | INGEST | LED | ARENA | + +Column 0 holds page up / page number / page down, plus **SERVICE** at row 3. + +Row 0 holds the group tiles. Each one shows the first problem in its group, plus "+N" if there are more. RF covers all six Shure units: AD4Q ×2, ULXD4, and PSM1000 ×3. + +## Colours + +| Tile | Meaning | +|---|---| +| Green | OK. The second line shows something useful, like `28h` of record time or `10/10` Dante devices. | +| **Blinking red** | Show-stopping, during service mode. | +| Solid dark red | Acknowledged. It stays red until the problem clears, then goes green. A new failure blinks again. | +| Amber | Warning: low disk, battery at 2 bars, RF interference, a device that keeps dropping out ("WING GONE x3"), a network blip in the last 3 minutes ("BLIP 10:21"). | +| Dim red, not blinking | A fault outside service mode. | +| Grey | Expected off outside service: OBS, a HyperDeck with no disk, Bible TV. | +| **Purple** | The watchdog has stopped. The master shows `WATCHDOG DOWN` and Companion's own error count. | + +## Buttons + +- **Press any tile:** acknowledge its alarms (stops the blinking). Press **ALL** to acknowledge everything. +- **Hold a device tile for 2 seconds:** restart that tile's *Companion connection*. This is the fix for a connection stuck after a network blip. It only restarts Companion's side and never reboots the device. +- **SERVICE:** service mode turns on automatically **Sunday 07:00–12:30** (`service_windows` in `config.json`). Press SERVICE to switch it on or off by hand, for example for a Wednesday event. The manual setting lasts until the next scheduled start or end. + +## What each tile checks + +| Tile | Checks | +|---|---| +| HD1–HD6 | Companion connection; **NO DISK**; record time **LOW** (<30 min, amber) / **FULL** (<5 min); **NOT REC** when another deck is recording and this one isn't | +| | 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 | +| X32, ATEM, ARENA, LED, OBS | Companion connection | +| DANTE | All 19 Dante devices ("19/19"), named when one drops: "WING GONE", "TRACKS GONE", "PROP MAC GONE". | | TRACKS | NA-Tracks Dante device; tracks Mac pings; "NA Tracks" MIDI session still on the network; Ableton OSC connection | | LIGHT PC | Lighting PC still joined to NA Ingest's network MIDI session; its MIDI session still on the network | | RACK | SuperRack-LiveBox still joined to the MIDI session; its Dante device | diff --git a/config.json b/config.json index 8017403..e1fcd3b 100644 --- a/config.json +++ b/config.json @@ -25,9 +25,9 @@ "tiles": [ { "id": "all", "label": "ALL", "pos": [0, 1], "kind": "master" }, { "id": "rec", "label": "REC", "pos": [0, 2], "members": ["hd1", "hd2", "hd3", "hd4", "hd5", "hd6", "tascam"] }, - { "id": "audio", "label": "AUDIO", "pos": [0, 3], "members": ["x32", "dante", "superrack"] }, + { "id": "audio", "label": "AUDIO", "pos": [0, 3], "members": ["x32", "dante"] }, { "id": "rf", "label": "RF", "pos": [0, 4], "members": ["axient1", "axient2", "ulxd", "psm1", "psm2", "psm3"] }, - { "id": "show", "label": "SHOW", "pos": [0, 5], "members": ["propres", "lightpc", "tracks", "bibletv"] }, + { "id": "show", "label": "SHOW", "pos": [0, 5], "members": ["midi", "apps"] }, { "id": "video", "label": "VIDEO", "pos": [0, 6], "members": ["atem", "led", "arena"] }, { "id": "stream", "label": "STREAM", "pos": [0, 7], "members": ["obs", "resi"] }, @@ -41,36 +41,27 @@ { "id": "x32", "label": "X32", "pos": [2, 1], "checks": [{ "type": "conn", "conn": "x32" }] }, { "id": "dante", "label": "DANTE", "pos": [2, 2], "checks": [ - { "type": "dante", "conn": "audinate-dantecontroller", "count_all": true, "devices": { + { "type": "dante", "conn": "audinate-dantecontroller", "devices": { "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", "STUDIO-A-Resi-AJA-SDI": "RESI AJA", "STUDIO-A-Ingest": "INGEST", "NA-Translation": "TRANSL", "STUDIO-B-RedNet-TNX": "STB TNX", "STUDIO-B-TB": "STB TB", "STUDIO-B-TB-MON": "STB TB MON", - "STUDIO-B-MON": "STB MON", "STUDIO-B-SOLO-MON": "STB SOLO", "STUDIO-B-Tascam": "STB TASCAM" } } + "STUDIO-B-MON": "STB MON", "STUDIO-B-SOLO-MON": "STB SOLO", "STUDIO-B-Tascam": "STB TASCAM", + "NA-Tracks": "TRACKS", "FOH-SuperRack-LiveBox": "SUPERRACK", "NA-ProPresenter-Mac": "PROP MAC" } } ] }, - { "id": "tracks", "label": "TRACKS", "pos": [2, 3], "checks": [ - { "type": "dante", "conn": "audinate-dantecontroller", "devices": { "NA-Tracks": "DANTE" } }, - { "type": "ping", "host": "10.1.3.227", "name": "MAC" }, - { "type": "mdns", "instance": "NA Tracks", "name": "MIDI SESS" }, - { "type": "conn", "conn": "ableton" }, - { "type": "midi_rx", "source": "Network NA Ingest Listen", "name": "TRK MIDI" } + { "id": "midi", "label": "MIDI", "pos": [2, 3], "checks": [ + { "type": "midi_peer", "session": "NA Ingest", "peer": "Lighting PC", "tag": "LIGHT PC" }, + { "type": "midi_peer", "session": "NA Ingest", "peer": "SuperRack-LiveBox", "tag": "SUPERRACK" }, + { "type": "mdns", "instance": "Lighting PC", "name": "LIGHT PC MIDI" }, + { "type": "mdns", "instance": "NA Tracks", "name": "TRACKS MIDI" }, + { "type": "midi_rx", "source": "Network NA Ingest Listen", "name": "TRACKS IN" } ] }, - { "id": "lightpc", "label": "LIGHT PC", "pos": [2, 4], "checks": [ - { "type": "midi_peer", "session": "NA Ingest", "peer": "Lighting PC" }, - { "type": "mdns", "instance": "Lighting PC", "name": "MIDI SESS" }, - { "type": "midi_rx", "source": "Network NA Ingest", "name": "MIDI IN" } - ] }, - { "id": "superrack", "label": "RACK", "pos": [2, 5], "checks": [ - { "type": "midi_peer", "session": "NA Ingest", "peer": "SuperRack-LiveBox" }, - { "type": "dante", "conn": "audinate-dantecontroller", "devices": { "FOH-SuperRack-LiveBox": "DANTE" } } - ] }, - { "id": "bibletv", "label": "BIBLE TV", "pos": [2, 6], "checks": [ - { "type": "http", "url": "http://10.6.7.13:8080/", "msg": "APP DOWN", "interval_seconds": 5 }, - { "type": "conn", "conn": "BibleTV" } - ] }, - { "id": "propres", "label": "PROP", "pos": [2, 7], "checks": [ - { "type": "conn", "conn": "Propresenter-API" }, - { "type": "dante", "conn": "audinate-dantecontroller", "devices": { "NA-ProPresenter-Mac": "DANTE" } } + { "id": "apps", "label": "APPS", "pos": [2, 4], "checks": [ + { "type": "conn", "conn": "Propresenter-API", "tag": "PROPRES" }, + { "type": "conn", "conn": "ableton", "tag": "ABLETON" }, + { "type": "ping", "host": "10.1.3.227", "name": "TRACKS MAC" }, + { "type": "http", "url": "http://10.6.7.13:8080/", "msg": "BIBLE APP DOWN", "interval_seconds": 5 }, + { "type": "conn", "conn": "BibleTV", "tag": "BIBLE OSC" } ] }, { "id": "service", "label": "ALARMS", "pos": [3, 0], "kind": "service" }, diff --git a/status-page.companionconfig b/status-page.companionconfig index ca61b4b4b122277144acda2ac9ff2ac71a0af3c2..b9ea3e97df3935a5868c82bbfac29f6f8f71474e 100644 GIT binary patch literal 4378 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 literal 4785 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 K;OWs6Wd8@k+*5=A diff --git a/status_panel.py b/status_panel.py index d2f3fbf..87f474b 100644 --- a/status_panel.py +++ b/status_panel.py @@ -346,7 +346,7 @@ def check_ping(chk, snap, ctx): fails = snap.ping.get(chk["host"]) if fails is not None and fails >= chk.get("failures", 2): msg = chk.get("msg") or chk.get("name", chk["host"]) + " OFFLINE" - return [("ping", Result(ERR, msg, service_only=chk.get("service_only", False)))] + return [("ping", Result(ERR, msg, service_only=chk.get("service_only", False), named=True))] return [("ping", Result(OK))] @@ -370,7 +370,7 @@ def check_process(chk, snap, ctx): def check_http(chk, snap, ctx): if snap.http.get(chk["url"]) is False: - return [("http", Result(ERR, chk.get("msg") or chk.get("name", "WEB") + " UNREACH"))] + return [("http", Result(ERR, chk.get("msg") or chk.get("name", "WEB") + " UNREACH", named=True))] return [("http", Result(OK))] @@ -398,14 +398,14 @@ def check_midi_peer(chk, snap, ctx): # clock sync, then "running". A peer that dies is removed from the list: # at once if it says goodbye, after ~60 s if it crashes or its cable is pulled. if p["state"] in ("running", "connected"): - return [("peer", Result(OK, info="MIDI OK"))] + return [("peer", Result(OK))] return [("peer", Result(ERR, "MIDI " + (p["state"] or "?").upper()[:8]))] return [("peer", Result(ERR, "MIDI LOST"))] def check_mdns(chk, snap, ctx): if snap.mdns_time and chk["instance"] not in snap.mdns: - return [("mdns", Result(ERR, chk.get("name", chk["instance"]) + " GONE"))] + return [("mdns", Result(ERR, chk.get("name", chk["instance"]) + " GONE", named=True))] return [("mdns", Result(OK))] @@ -595,6 +595,9 @@ class Engine: log.exception("check %s/%s failed", t["id"], chk["type"]) results = [("error", Result(WARN, "CHECK BUG"))] for sub, res in results: + # "tag" names the device when one button covers several (APPS, MIDI). + if chk.get("tag") and res.level in (WARN, ERR) and not res.named: + res.msg, res.named = "%s %s" % (chk["tag"], res.msg), True raw["%s:%d:%s" % (t["id"], i, sub)] = (t["id"], res, chk) for sub, res in self.network_results(snap, now): raw["net:%s" % sub] = ("net", res, {"type": "network"}) @@ -877,6 +880,16 @@ def poll_system(snap, cfg): snap.procs = procs +def parse_dns_sd(out): + """Instance names still present in `dns-sd -B` output.""" + names = set() + for line in out.splitlines(): + m = re.match(r"^\s*\S+\s+(Add|Rmv)\s+\d+\s+\d+\s+\S+\s+_apple-midi\._udp\.\s+(.+?)\s*$", line) + if m: + (names.add if m.group(1) == "Add" else names.discard)(m.group(2)) + return names + + def poll_mdns(snap, seconds=3): proc = subprocess.Popen(["/usr/bin/dns-sd", "-B", "_apple-midi._udp", "local."], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) @@ -885,12 +898,7 @@ def poll_mdns(snap, seconds=3): except subprocess.TimeoutExpired: proc.kill() out, _ = proc.communicate() - names = set() - for line in out.splitlines(): - m = re.match(r"^\S+\s+(Add|Rmv)\s+\d+\s+\d+\s+\S+\s+_apple-midi\._udp\.\s+(.+?)\s*$", line) - if m: - (names.add if m.group(1) == "Add" else names.discard)(m.group(2)) - snap.mdns, snap.mdns_time = names, time.time() + snap.mdns, snap.mdns_time = parse_dns_sd(out), time.time() _UNVERIFIED = ssl._create_unverified_context() # reachability only; nothing is sent diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index a1be75e..99ae8d1 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -167,14 +167,14 @@ class TestChecks(unittest.TestCase): out = dict(sp.check_dante({"conn": "audinate-dantecontroller", "devices": ["BOH-WING"]}, self.snap, self.ctx)) self.assertEqual(out["conn"].msg, "NO DATA") - def test_dante_count_includes_other_buttons(self): + def test_dante_button_names_the_missing_device(self): devices = json.loads(self.snap.vars[("audinate-dantecontroller", "devices")]) devices.remove("NA-Tracks") self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 4) - self.assertEqual(tiles["dante"]["x"], "DANTE\n18/19") - self.assertEqual(level(tiles["dante"]), "ok") # the alarm is on TRACKS, not twice - self.assertEqual(tiles["tracks"]["x"], "TRACKS\nDANTE GONE") + self.assertEqual(tiles["dante"]["x"], "DANTE\nTRACKS GONE") + self.assertEqual(level(tiles["dante"]), "err") + self.assertEqual(tiles["all"]["x"], "1 DOWN\nDANTE TRACKS GONE") def test_shure_psm_tascam(self): s = self.snap @@ -204,24 +204,42 @@ class TestChecks(unittest.TestCase): tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 4) self.assertIn("PSM 1 OFFLINE", tiles["rf"]["x"]) + def test_bonjour_parsing_morning_and_afternoon(self): + out = ("Browsing for _apple-midi._udp.local.\n" + " 9:20:51.321 Add 3 13 local. _apple-midi._udp. NA Tracks\n" + "19:20:51.321 Add 3 27 local. _apple-midi._udp. Lighting PC\n" + "19:21:02.001 Add 3 27 local. _apple-midi._udp. Gone Soon\n" + "19:21:09.001 Rmv 1 27 local. _apple-midi._udp. Gone Soon\n") + self.assertEqual(sp.parse_dns_sd(out), {"NA Tracks", "Lighting PC"}) + def test_midi_peer_joining_is_not_an_error(self): for p in self.snap.midi["NA Ingest"]["peers"]: p["state"] = "connected" # joined, clock sync not finished yet tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 8) - self.assertEqual(tiles["lightpc"]["x"], "LIGHT PC\nMIDI OK") + self.assertEqual(tiles["midi"]["x"], "MIDI\nOK") def test_midi_peer_and_bonjour(self): self.snap.midi["NA Ingest"]["peers"] = [p for p in self.snap.midi["NA Ingest"]["peers"] if p["name"] != "Lighting PC"] self.snap.mdns.discard("NA Tracks") tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 4) - self.assertEqual(tiles["lightpc"]["x"], "LIGHT PC\nMIDI LOST") - self.assertEqual(tiles["tracks"]["x"], "TRACKS\nMIDI SESS GONE") + self.assertEqual(tiles["midi"]["x"], "MIDI\nLIGHT PC MIDI LOST\n+1") + self.assertEqual(tiles["show"]["x"], "SHOW\nLIGHT PC MIDI LOST\n+1") # named, no "MIDI" prefix + self.assertNotIn("MIDI TRACKS", tiles["show"]["x"]) + self.snap.midi["NA Ingest"]["peers"].append({"name": "Lighting PC", "address": "", "state": "running"}) + tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 4) + self.assertEqual(tiles["midi"]["x"], "MIDI\nTRACKS MIDI GONE") def test_bible_tv_app_down(self): self.snap.http["http://10.6.7.13:8080/"] = False tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 4) - self.assertEqual(tiles["bibletv"]["x"], "BIBLE TV\nAPP DOWN") + self.assertEqual(tiles["apps"]["x"], "APPS\nBIBLE APP DOWN") + + def test_apps_names_the_app(self): + self.snap.conns["Propresenter-API"]["category"] = "error" + tiles, _ = run_steps(sp.Engine(CONFIG), self.snap, self.now, 4) + self.assertEqual(tiles["apps"]["x"], "APPS\nPROPRES OFFLINE") + self.assertEqual(tiles["all"]["x"], "1 DOWN\nPROPRES OFFLINE") def test_http_intervals(self): checks = dict(sp.http_checks(CONFIG)) @@ -390,7 +408,8 @@ class TestBehaviour(unittest.TestCase): engine = sp.Engine(CONFIG) self.assertEqual(engine.restart_targets("hd3"), ["HyperDeck_3"]) self.assertEqual(engine.restart_targets("led"), ["VX1000", "UHDJr"]) - self.assertEqual(engine.restart_targets("tracks"), ["ableton"]) # not the shared Dante one + self.assertEqual(engine.restart_targets("apps"), ["Propresenter-API", "ableton", "BibleTV"]) + self.assertEqual(engine.restart_targets("dante"), []) # shared connection: never self.assertEqual(engine.restart_targets("ingest"), [])