Tune key at every location: the key the tracks send must reach each location's FOH (SuperRack) in that location's own Planning Center key

NA works out each location's expected Waves Tune programs (campus_tune) and serves them at /api/tune/<id>; locations
fetch their list, read ProPresenter through Companion's connection, and compare with a MIDI witness on the FOH Mac
(SC FOH now; WS FOH needs the Command Line Tools first). FOH KEY WRONG / FOH GOT NO KEY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 11:51:09 -04:00
co-authored by Claude Opus 5.5
parent 5d0263a9b9
commit e4a33ff660
9 changed files with 140 additions and 11 deletions
+50 -9
View File
@@ -191,6 +191,8 @@ class Snapshot:
self.disk = {} # path -> free GB
self.listeners = {} # MIDI source name -> coremidi.Listener
self.witness = {} # host ("" here) -> midi_witness.py's file, with "read": when it was read
self.tune_expect = None # a location: {song: {"programs", "key", "clips"}} fetched from the hub (/api/tune/<id>)
self.tune_expect_time = 0.0
self.prop_song = None # {"name", "since", "changes"}: the song ProPresenter shows, for the key checks
self.remote = {} # (host, command) -> True / False / None (could not ask)
self.dante_map = {} # Dante device name -> {"ip", "mac", "switch", "port", "shared", "poe", "seen"}
@@ -579,7 +581,8 @@ def poll_tune(snap, cfg):
if data is not None:
data["read"] = time.time()
snap.witness = dict(snap.witness, **{host: data})
url = ((cfg.get("ready") or {}).get("prop_url") or "").rstrip("/")
chk = next((c for t in all_tiles(cfg) for c in t.get("checks", []) if c["type"] == "tune_rx"), {})
url = ((cfg.get("ready") or {}).get("prop_url") or "").rstrip("/") or tune_location_inputs(snap, chk, (cfg.get("site") or {}).get("id") or "")
if not url:
return
try:
@@ -599,6 +602,26 @@ def poll_tune(snap, cfg):
snap.prop_song = dict(song, changes=song["changes"] + 1, slide=idx)
def tune_location_inputs(snap, chk, site_id):
"""A location's Tune check (2026-10-28): fetch from the hub, once a minute, the programs its songs must arrive
in ("expect": "hub"), and return its ProPresenter's address as Companion reaches it ("prop_conn")."""
if chk.get("expect") == "hub" and time.time() - snap.tune_expect_time > 60:
snap.tune_expect_time = time.time()
try:
here = os.path.dirname(os.path.abspath(__file__)) # installed beside campuses.json
campuses = json.load(open(os.path.join(here, "campuses.json"))) or {}
home = (campuses.get("home") or "").rstrip("/")
with urllib.request.urlopen("%s/api/tune/%s" % (home, urllib.parse.quote(site_id)), timeout=5) as r:
snap.tune_expect = json.loads(r.read()).get("songs")
except (OSError, ValueError, AttributeError):
pass # the hub not answering: keep the last list
if not chk.get("prop_conn"):
return ""
host = snap.conn_hosts.get(chk["prop_conn"])
port = (snap.conn_info.get(chk["prop_conn"]) or {}).get("port")
return "http://%s:%s" % (host, port) if host and port else ""
def check_tune_rx(chk, snap, ctx):
"""The key the tracks send (the Waves Tune clip's program at the song's start) really arrived on a Mac (Studio
B's Pro Tools), judged against the song ProPresenter shows: a program change received during the song must be
@@ -613,22 +636,27 @@ def check_tune_rx(chk, snap, ctx):
return [("tune", Result(OK))]
try:
import readiness
sent = readiness.LAST.get("tune_sent") or {}
exp = next((v for k, v in sent.items() if readiness.same(k, song["name"])), None)
if snap.tune_expect is not None: # a location: its own plan's keys, from the hub
exp = next((v for k, v in snap.tune_expect.items() if not k.startswith("_") and readiness.same(k, song["name"])), None)
else: # the hub: what its own set sends
sent = readiness.LAST.get("tune_sent") or {}
exp = next((v for k, v in sent.items() if readiness.same(k, song["name"])), None)
exp = dict(exp, programs=[exp["program"]]) if exp and exp.get("program") is not None else None
except Exception: # noqa: BLE001 - no READY page here
exp = None
if not exp or exp.get("program") is None:
if not exp or not exp.get("programs"):
return [("tune", Result(OK))]
what = exp.get("clip") or "/".join(exp.get("clips") or []) or exp.get("key", "")
pcs = [p for p in w.get("pcs") or [] if p[0] >= song["since"] - 5]
if pcs:
got = pcs[-1][3]
if got != exp["program"]:
return [("tune", Result(ERR, "%s KEY WRONG" % name, info="%s got program %d; %s's clip %s sends %d" % (
name, got, song["name"], exp.get("clip"), exp["program"]), named=True))]
return [("tune", Result(OK, info="%s KEY %s" % (name, (exp.get("clip") or "").upper())))]
if got not in exp["programs"]:
return [("tune", Result(ERR, "%s KEY WRONG" % name, info="%s got program %d; %s should get %s (program %s)" % (
name, got, song["name"], what, "/".join(str(x) for x in exp["programs"])), named=True))]
return [("tune", Result(OK, info="%s KEY %s" % (name, (exp.get("clip") or exp.get("key") or "").upper())))]
if ctx["now"] - song["since"] >= 45 and song.get("changes", 0) >= 2:
return [("tune", Result(ERR, "%s GOT NO KEY" % name, info="%s has run %d s and %s received no key (%s)" % (
song["name"], ctx["now"] - song["since"], name, exp.get("clip")), named=True))]
song["name"], ctx["now"] - song["since"], name, what), named=True))]
return [("tune", Result(OK))]
@@ -4138,6 +4166,18 @@ def start_pollers(cfg, snap, stop, data_dir=None):
return threads
def tune_expect_now(snap):
"""The hub: each location's expected Tune programs (readiness works them out from that location's plan);
a location: its own list, as fetched from the hub."""
if snap.tune_expect is not None:
return {"here": snap.tune_expect}
try:
import readiness
return readiness.LAST.get("tune_expect")
except Exception: # noqa: BLE001 - no READY page here
return None
def snapshot_summary(snap, engine, now):
"""Everything the watchdog knows right now, in plain JSON (the settings page reads it instead of
asking the gear again): what Companion reports, pings, MIDI sessions here and on the other Macs,
@@ -4182,6 +4222,7 @@ def snapshot_summary(snap, engine, now):
"cleared": engine.burst["cleared"]},
"live": list(engine.live_now(now)),
"ready": snap.ready,
"tune_expect": tune_expect_now(snap),
"faults": {},
}
for key, tr in engine.trackers.items():