Hold MIDI reconnects a dropped peer from the Mac side
macOS's MIDINetworkSession API only reaches the default session (blank and disabled on NA Ingest and NA Tracks), so coremidi.connect_peer writes the named session's "apple.midirtp.session" settings with the peer added, as Audio MIDI Setup's Connect does. Existing peers are passed back unchanged. The watchdog remembers each peer's last address (config "address" is the fallback) and runs it locally for NA Ingest's session or over SSH on the Tracks Mac for NA Tracks' (FX lights). Checked live: both Macs answer "already" while the Lighting PC is connected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+45
-5
@@ -134,6 +134,7 @@ class Snapshot:
|
||||
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.peer_addr = {} # (host or "", session, peer) -> address it was last seen at
|
||||
self.midi_time = 0.0
|
||||
self.mdns = set() # Bonjour _apple-midi._udp instance names
|
||||
self.mdns_time = 0.0
|
||||
@@ -801,14 +802,25 @@ class Engine:
|
||||
continue
|
||||
# One run per fix, except a HyperDeck fix, which is per deck. The deck's
|
||||
# address comes from its Companion connection (remembered if it goes away).
|
||||
per_deck = self.cfg["fixes"][fix_id].get("kind") == "hyperdeck"
|
||||
fix_kind = self.cfg["fixes"][fix_id].get("kind")
|
||||
per_deck = fix_kind == "hyperdeck"
|
||||
target = ""
|
||||
if per_deck:
|
||||
ip = chk.get("ip") or snap.var(chk["conn"], "ip") or self.deck_ips.get(chk["conn"])
|
||||
if not ip:
|
||||
continue
|
||||
self.deck_ips[chk["conn"]] = ip
|
||||
chk = dict(chk, ip=ip)
|
||||
ident = ("fix", fix_id, chk["conn"] if per_deck else "")
|
||||
target = chk["conn"]
|
||||
if fix_kind == "midi_connect":
|
||||
# Reconnect the peer where it was last seen (or the config's address).
|
||||
key = (chk.get("host", ""), chk["session"], chk["peer"])
|
||||
address = snap.peer_addr.get(key) or chk.get("address")
|
||||
if not address:
|
||||
continue
|
||||
chk = dict(chk, address=address)
|
||||
target = "|".join(key)
|
||||
ident = ("fix", fix_id, target)
|
||||
if ident not in seen:
|
||||
seen.add(ident)
|
||||
plan.append((ident, "fix", fix_id, chk))
|
||||
@@ -1125,9 +1137,18 @@ def poll_http(snap, checks):
|
||||
snap.http = result
|
||||
|
||||
|
||||
def remember_peers(snap, host, sessions):
|
||||
"""Keep each peer's last address, so a hold can reconnect it after it has gone."""
|
||||
for name, sess in (sessions or {}).items():
|
||||
for p in sess.get("peers", []):
|
||||
if p.get("address"):
|
||||
snap.peer_addr[(host, name, p["name"])] = p["address"]
|
||||
|
||||
|
||||
def poll_midi(snap, cfg):
|
||||
import coremidi
|
||||
snap.midi, snap.midi_time = coremidi.sessions(), time.time()
|
||||
remember_peers(snap, "", snap.midi)
|
||||
for t in cfg["tiles"]:
|
||||
for c in t.get("checks", []):
|
||||
if c["type"] == "midi_rx":
|
||||
@@ -1144,12 +1165,12 @@ def remote_midi_hosts(cfg):
|
||||
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.
|
||||
def remote_midi_script(call="ns['sessions']()"):
|
||||
"""Python for /usr/bin/python3 on another Mac: this folder's coremidi.py, then print `call`'s result.
|
||||
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)
|
||||
"print('SPMIDI' + json.dumps(%s))\n" % (src, call))
|
||||
|
||||
|
||||
def parse_remote_midi(out):
|
||||
@@ -1169,6 +1190,7 @@ def poll_remote_midi(snap, cfg):
|
||||
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
|
||||
remember_peers(snap, host, snap.remote_midi[host])
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
snap.remote_midi[host] = None
|
||||
|
||||
@@ -1371,6 +1393,24 @@ def execute_fix(cfg, fix_id, chk):
|
||||
ok = chk["conn"] in ids and comp.restart_connection(ids[chk["conn"]])
|
||||
log.info("FIX %s -> Companion connection %s restarted %s", label, chk["conn"], "ok" if ok else "FAILED")
|
||||
return r.returncode == 0
|
||||
if kind == "midi_connect":
|
||||
# Like Connect in Audio MIDI Setup: add the peer back to the session it dropped out of.
|
||||
args = (chk["session"], chk["peer"], chk["address"])
|
||||
if chk.get("host"):
|
||||
r = subprocess.run(ssh_cmd(cfg, chk["host"]) + ["/usr/bin/python3", "-"],
|
||||
input=remote_midi_script("ns['connect_peer'](%r, %r, %r)" % args),
|
||||
capture_output=True, text=True, timeout=30)
|
||||
result = parse_remote_midi(r.stdout) if r.returncode == 0 else None
|
||||
if result is None:
|
||||
log.info("FIX %s -> %s on %s failed: %s", label, chk["peer"], chk["host"],
|
||||
(r.stdout + r.stderr).strip().replace("\n", " ")[-200:])
|
||||
return False
|
||||
else:
|
||||
import coremidi
|
||||
result = coremidi.connect_peer(*args)
|
||||
log.info("FIX %s -> %s at %s, session %s%s: %s", label, chk["peer"], chk["address"], chk["session"],
|
||||
(" on " + chk["host"]) if chk.get("host") else "", result)
|
||||
return result in ("asked", "already")
|
||||
if kind == "netgear_poe":
|
||||
# Netgear AV switch on the Dante network: power-cycle one PoE port (netgear.py).
|
||||
import netgear
|
||||
|
||||
Reference in New Issue
Block a user