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:
2026-10-08 15:54:53 -04:00
co-authored by Claude Opus 5.5
parent 74fbe73a4b
commit 80fd89d03b
5 changed files with 161 additions and 14 deletions
+45 -5
View File
@@ -527,11 +527,52 @@ class TestFixes(unittest.TestCase):
self.assertEqual(self.plan("obs"), [("fix", "obs")])
self.assertEqual(self.plan("arena"), [("fix", "arena")])
self.assertEqual(self.plan("apps"), [("fix", "vista")])
# Restarting rtpMIDI drops every session and it does not rejoin them (2026-10-08): no auto fix.
self.assertEqual(self.plan("midi"), [])
# Restarting rtpMIDI drops every session and does not rejoin them (2026-10-08): the hold
# reconnects the peer from the Mac's side instead, like Connect in Audio MIDI Setup.
self.assertEqual(self.plan("midi"), [("fix", "midi_reconnect")])
for fix_id in ("obs", "arena", "vista"): # never closes a running app
self.assertTrue(CONFIG["fixes"][fix_id]["run"].startswith("tasklist "), fix_id)
def test_midi_reconnect_where_the_peer_was_last_seen(self):
tracks = {"NA Tracks": {"port": 5004, "peers": [
{"name": "Lighting PC", "address": "10.1.10.77:5004", "state": "running"}]}}
sp.remember_peers(self.snap, "tracks", tracks) # seen at a new address earlier
tracks["NA Tracks"]["peers"] = []
self.snap.remote_midi["tracks"] = tracks
self.snap.midi["NA Ingest"]["peers"] = [p for p in self.snap.midi["NA Ingest"]["peers"]
if p["name"] != "Lighting PC"]
run_steps(self.engine, self.snap, SUNDAY, 4)
plan = self.engine.fix_plan("midi", self.snap)
got = sorted((c.get("host", ""), c["session"], c["address"]) for _, kind, what, c in plan)
self.assertEqual(got, [("", "NA Ingest", "10.1.10.206:5004"), # never seen: config address
("tracks", "NA Tracks", "10.1.10.77:5004")]) # seen: last address
self.assertEqual(self.plan("show"), [("fix", "midi_reconnect")] * 2) # one per session
def test_midi_reconnect_runs_on_the_right_mac(self):
from unittest import mock
chk = {"type": "midi_peer", "host": "tracks", "session": "NA Tracks", "peer": "Lighting PC",
"address": "10.1.10.206:5004"}
done = mock.Mock(returncode=0, stdout='SPMIDI"asked"\n', stderr="")
with mock.patch.object(sp.subprocess, "run", return_value=done) as run:
self.assertTrue(sp.execute_fix(CONFIG, "midi_reconnect", chk))
argv, kw = run.call_args.args[0], run.call_args.kwargs
self.assertIn("waterschurchmedia@10.1.3.227", argv)
self.assertIn("ns['connect_peer']('NA Tracks', 'Lighting PC', '10.1.10.206:5004')", kw["input"])
failed = mock.Mock(returncode=255, stdout="", stderr="ssh: timed out")
with mock.patch.object(sp.subprocess, "run", return_value=failed):
self.assertFalse(sp.execute_fix(CONFIG, "midi_reconnect", chk))
def test_reconnect_leaves_other_peers_alone(self):
import coremidi
sess = {"dns-sd-name": "NA Tracks", "flags": 1, "join-policy": 2, "port": 5004, "peers": [
{"address": "10.1.13.140:5004", "name": "SuperRack-LiveBox", "state": "running"},
{"address": "\tStudio B Mac Studio\tlocal.", "name": "Studio B Mac Studio", "state": "running"}]}
new = coremidi.with_peer(sess, "Lighting PC", "10.1.10.206:5004")
self.assertEqual(new["peers"][:2], sess["peers"])
self.assertEqual(new["peers"][2], {"address": "10.1.10.206:5004", "name": "Lighting PC"})
self.assertEqual({k: v for k, v in new.items() if k != "peers"}, {k: v for k, v in sess.items() if k != "peers"})
self.assertIsNone(coremidi.with_peer(new, "Lighting PC", "10.1.10.206:5004")) # already there
def test_an_off_pc_is_woken(self):
from unittest import mock
woke = []
@@ -711,8 +752,7 @@ class TestFixes(unittest.TestCase):
self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices)
self.assertEqual(self.plan("dante"), [("fix", "dvs_ingest")])
# A failure with no fix must not claim there is nothing wrong.
self.snap.midi["NA Ingest"]["peers"] = [p for p in self.snap.midi["NA Ingest"]["peers"]
if p["name"] != "SuperRack-LiveBox"]
self.snap.mdns = self.snap.mdns - {"NA Tracks"} # an advert gone: nothing to reconnect
_, t = run_steps(self.engine, self.snap, SUNDAY, 4)
self.engine.run_fixes("midi", self.snap, None, t)
self.assertEqual(self.engine.notes["midi"]["text"], "NO AUTO\nFIX")
@@ -733,7 +773,7 @@ class TestFixes(unittest.TestCase):
def test_every_fix_is_valid(self):
for f in CONFIG["fixes"].values():
self.assertIn(f.get("kind", "ssh"), ("ssh", "local", "hyperdeck"))
self.assertIn(f.get("kind", "ssh"), ("ssh", "local", "hyperdeck", "midi_connect"))
class FakeDeck: