MIDI alarm when the Lighting PC drops out of NA Tracks' session (FX lights)
Tracks sends the FX lighting cues to the Lighting PC through NA Tracks' network MIDI session. A midi_peer check may now name a "host": the watchdog reads that Mac's sessions over SSH every 5 s, sending this folder's coremidi.py over stdin (nothing installed there). Unreachable = no verdict; the Tracks Mac's own ping check covers that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+40
-2
@@ -133,6 +133,7 @@ class Snapshot:
|
||||
self.conns_time = 0.0
|
||||
self.vars = {} # (label, name) -> str or None
|
||||
self.midi = {} # session name -> {"peers": [...]}
|
||||
self.remote_midi = {} # host -> the same, read on that Mac over SSH (None: unreachable)
|
||||
self.midi_time = 0.0
|
||||
self.mdns = set() # Bonjour _apple-midi._udp instance names
|
||||
self.mdns_time = 0.0
|
||||
@@ -400,9 +401,12 @@ def check_disk(chk, snap, ctx):
|
||||
|
||||
|
||||
def check_midi_peer(chk, snap, ctx):
|
||||
if not snap.midi_time:
|
||||
# "host": a session on another Mac (e.g. NA Tracks -> Lighting PC, the FX lighting cues),
|
||||
# read over SSH. Unreachable or not read yet = no verdict (that Mac's own ping says if it is down).
|
||||
sessions = snap.remote_midi.get(chk["host"]) if chk.get("host") else (snap.midi if snap.midi_time else None)
|
||||
if sessions is None:
|
||||
return [("peer", Result(OK))]
|
||||
sess = snap.midi.get(chk["session"])
|
||||
sess = sessions.get(chk["session"])
|
||||
if sess is None:
|
||||
return [("peer", Result(ERR, "NO SESSION"))]
|
||||
for p in sess["peers"]:
|
||||
@@ -1135,6 +1139,39 @@ def poll_midi(snap, cfg):
|
||||
|
||||
# --- Other machines: SSH checks and fixes ------------------------------------
|
||||
|
||||
def remote_midi_hosts(cfg):
|
||||
return sorted({c["host"] for t in cfg["tiles"] for c in t.get("checks", [])
|
||||
if c["type"] == "midi_peer" and c.get("host")})
|
||||
|
||||
|
||||
def remote_midi_script():
|
||||
"""Python for /usr/bin/python3 on another Mac: this folder's coremidi.py, then print its sessions.
|
||||
Nothing is installed there; the code goes over stdin each time."""
|
||||
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "coremidi.py"), encoding="utf-8").read()
|
||||
return ("import json\nns = {'__name__': 'spcm'}\nexec(compile(%r, 'coremidi.py', 'exec'), ns)\n"
|
||||
"print('SPMIDI' + json.dumps(ns['sessions']()))\n" % src)
|
||||
|
||||
|
||||
def parse_remote_midi(out):
|
||||
for line in (out or "").splitlines():
|
||||
if line.startswith("SPMIDI"):
|
||||
try:
|
||||
return json.loads(line[6:])
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def poll_remote_midi(snap, cfg):
|
||||
script = remote_midi_script()
|
||||
for host in remote_midi_hosts(cfg):
|
||||
try:
|
||||
r = subprocess.run(ssh_cmd(cfg, host) + ["/usr/bin/python3", "-"], input=script,
|
||||
capture_output=True, text=True, timeout=20)
|
||||
snap.remote_midi[host] = parse_remote_midi(r.stdout) if r.returncode == 0 else None
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
snap.remote_midi[host] = None
|
||||
|
||||
def ssh_cmd(cfg, host):
|
||||
"""ssh to a machine in config "hosts", with NA Ingest's status-panel key; one shared connection per host."""
|
||||
return ["/usr/bin/ssh", "-i", os.path.expanduser(cfg.get("ssh_key", "~/.ssh/id_status_panel")),
|
||||
@@ -1477,6 +1514,7 @@ def start_pollers(cfg, snap, stop, data_dir=None):
|
||||
Periodic("bonjour", 15.0, lambda: poll_mdns(snap), stop),
|
||||
Periodic("http", 5.0, lambda: poll_http(snap, http_checks(cfg)), stop),
|
||||
Periodic("remote", 10.0, lambda: poll_remote(snap, cfg), stop),
|
||||
Periodic("remote-midi", 5.0, lambda: poll_remote_midi(snap, cfg), stop),
|
||||
Periodic("dante-map", 30.0, lambda: poll_dante_map(snap, cfg, c4, dante_map_path(data_dir)), stop),
|
||||
]
|
||||
for t in threads:
|
||||
|
||||
Reference in New Issue
Block a user