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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user