From 288c1e6e1a6b8d36fac5bab0a95f04af1459e7a1 Mon Sep 17 00:00:00 2001 From: christian Date: Thu, 8 Oct 2026 16:16:31 -0400 Subject: [PATCH] Start rtpMIDI if it is not running; it also restarts itself if it crashes - Hold MIDI on "LIGHT PC MIDI GONE" starts the rtpMIDI service, then its tray app, only if they are not running (a restart drops every session). - The Lighting PC's rtpMIDI service now has Windows recovery actions (restart after 5 s, 5 s, 30 s); windows-setup.ps1 sets the same. - coremidi.disconnect_peer (Disconnect in Audio MIDI Setup), used to test the alarms: all six links dropped and came back (four by themselves, two by the hold). - Audio MIDI Setup need not be open: quitting it on NA Ingest left the session connected. Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + config.json | 4 +++- coremidi.py | 33 ++++++++++++++++++++++++++++----- remote-access/windows-setup.ps1 | 6 ++++++ tests/test_status_panel.py | 11 +++++++++-- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 808a226..86da83f 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Row 0 holds the group buttons. Each one shows the first problem in its group, pl | ARENA | starts Resolume Arena on the Lighting PC if it is not running, then reconnects Companion | | OBS | starts OBS on the LiveStream PC if it is not running, then reconnects Companion. If the PC is off, it is woken (Wake-on-LAN) and the button shows WAKING PC; hold again once someone has logged in. | | MIDI: LIGHT PC / FX LIGHTS / SUPERRACK MIDI LOST | reconnects that peer to the session it dropped out of, from the Mac's side (NA Ingest's own session, or NA Tracks' over SSH), like Connect in Audio MIDI Setup. Other peers are left alone and nothing is restarted. The address is where the peer was last seen, else `"address"` on the check. Not for a missing Bonjour advert ("... MIDI GONE"). **Keep the Lighting PC's network on Public:** its Bonjour firewall rules only allow Public, and on Private it vanishes from the MIDI directory and cannot rejoin a session (2026-10-08). Never restart rtpMIDI as a fix: it drops every session and does not rejoin them. | + | MIDI: LIGHT PC MIDI GONE | starts rtpMIDI on the Lighting PC if it is not running (its service, then the tray app). Its service also restarts itself if it crashes (Windows service recovery, set by `windows-setup.ps1`). Audio MIDI Setup does not need to be open on the Macs: their sessions run in macOS's MIDI server (tested 2026-10-08: quitting it left NA Ingest's session connected). | | DANTE: PROP MAC / TRACKS / TRANSL / INGEST GONE | starts Dante Virtual Soundcard's engine on that Mac (`remote-access/mac-dvs-helper.sh`) | | DANTE: any device powered by its switch port (today the four Studio B boxes) | power-cycles the port it is on **now**, found by its MAC on the Netgear Dante switches | | DANTE: any other device GONE | restarts Companion's Dante module (hardware: if still gone, it is off or unplugged) | diff --git a/config.json b/config.json index 38b83da..66f971b 100644 --- a/config.json +++ b/config.json @@ -60,6 +60,8 @@ "run": "tasklist /NH /FI \"IMAGENAME eq Arena.exe\" | find /I \"Arena.exe\" >nul || schtasks /Run /TN \"\\StatusPanel\\Start Arena\"" }, "vista": { "host": "lighting", "short": "VISTA", "label": "start Vista on the Lighting PC if it is not running (a running Vista is never closed)", "run": "tasklist /NH /FI \"IMAGENAME eq Vista.exe\" | find /I \"Vista.exe\" >nul || schtasks /Run /TN \"\\StatusPanel\\Start Vista\"" }, + "rtpmidi_start": { "host": "lighting", "short": "RTPMIDI", "label": "start rtpMIDI on the Lighting PC if it is not running (service, then tray app). A running rtpMIDI is never restarted: that drops every session.", + "run": "(sc query rtpMIDIService | find \"RUNNING\" >nul || net start rtpMIDIService) && (tasklist /NH /FI \"IMAGENAME eq rtpMIDI.exe\" | find /I \"rtpMIDI.exe\" >nul || schtasks /Run /TN \"\\StatusPanel\\Start rtpMIDI\")" }, "midi_reconnect": { "kind": "midi_connect", "short": "RECONNECT", "label": "reconnect the peer to the network MIDI session it dropped out of (like Connect in Audio MIDI Setup); the address is where it was last seen, else the check's \"address\"" }, "proddash": { "kind": "local", "short": "PRODDASH", "label": "start ProdDash", "run": "open /Applications/ProdDash.app" }, "hyperdeck": { "kind": "hyperdeck", "short": "HYPERDECK", "label": "reconnect Companion to the HyperDeck, or reboot the deck only if it is hung" } @@ -103,7 +105,7 @@ { "type": "midi_peer", "host": "tracks", "session": "NA Tracks", "peer": "SuperRack-LiveBox", "tag": "TRK LIVEBOX", "fix": "midi_reconnect", "address": "10.1.13.140:5004" }, { "type": "midi_peer", "host": "tracks", "session": "NA Tracks", "peer": "Studio B Mac Studio", "tag": "TRK STUDIO B", "fix": "midi_reconnect", "address": "\tStudio B Mac Studio\tlocal." }, { "type": "midi_peer", "host": "prop", "session": "NA ProP", "peer": "Lighting PC", "tag": "PROP LIGHTS", "fix": "midi_reconnect", "address": "10.1.10.206:5004" }, - { "type": "mdns", "instance": "Lighting PC", "name": "LIGHT PC MIDI" }, + { "type": "mdns", "instance": "Lighting PC", "name": "LIGHT PC MIDI", "fix": "rtpmidi_start" }, { "type": "mdns", "instance": "NA Tracks", "name": "TRACKS MIDI" }, { "type": "midi_rx", "source": "Network NA Ingest Listen", "name": "TRACKS IN" } ] }, diff --git a/coremidi.py b/coremidi.py index 372a604..c4ce24c 100644 --- a/coremidi.py +++ b/coremidi.py @@ -134,16 +134,25 @@ def with_peer(sess, peer_name, address): return new -def connect_peer(session_name, peer_name, address): - """Ask macOS to connect `peer_name` at `address` ("10.1.10.206:5004") to the named session, - like Audio MIDI Setup's Connect. Returns "already" (it is in the session) or "asked".""" +def without_peer(sess, peer_name): + """The session settings without this peer, or None if it is not in the session.""" + peers = list(sess.get("peers", [])) + keep = [p for p in peers if p.get("name") != peer_name] + if len(keep) == len(peers): + return None + new = dict(sess) + new["peers"] = keep + return new + + +def _edit_session(session_name, edit): with _lock: for ent, sess in _session_entities(): if (sess.get("dns-sd-name") or _string_prop(ent, "name")) != session_name: continue - new = with_peer(sess, peer_name, address) + new = edit(sess) if new is None: - return "already" + return "unchanged" err = _set_dict_prop(ent, "apple.midirtp.session", new) if err: raise OSError("MIDIObjectSetDictionaryProperty failed: %d" % err) @@ -151,6 +160,20 @@ def connect_peer(session_name, peer_name, address): raise LookupError("no network MIDI session named %r on this Mac" % session_name) +def connect_peer(session_name, peer_name, address): + """Ask macOS to connect `peer_name` at `address` ("10.1.10.206:5004") to the named session, + like Audio MIDI Setup's Connect. Returns "already" (it is in the session) or "asked".""" + result = _edit_session(session_name, lambda sess: with_peer(sess, peer_name, address)) + return "already" if result == "unchanged" else result + + +def disconnect_peer(session_name, peer_name): + """Drop a peer from the named session, like Audio MIDI Setup's Disconnect (to test the alarms). + Returns "not connected" or "asked".""" + result = _edit_session(session_name, lambda sess: without_peer(sess, peer_name)) + return "not connected" if result == "unchanged" else result + + def sessions(): """{session name: {"port": int, "peers": [{"name", "address", "state"}]}}""" found = {} diff --git a/remote-access/windows-setup.ps1 b/remote-access/windows-setup.ps1 index f0593b0..b241e90 100644 --- a/remote-access/windows-setup.ps1 +++ b/remote-access/windows-setup.ps1 @@ -98,6 +98,12 @@ foreach ($app in $apps) { $tasks += ('{0} = {1}{2}' -f $app.Name, $app.Exe, $(if ($app.AtLogon) { ' (also at login)' } else { '' })) } +# rtpMIDI: if its service ever stops unexpectedly, Windows starts it again (5 s, 5 s, then 30 s). +if (Get-Service rtpMIDIService -ErrorAction SilentlyContinue) { + sc.exe failure rtpMIDIService reset= 86400 actions= restart/5000/restart/5000/restart/30000 | Out-Null + $notes += 'rtpMIDI service restarts itself if it stops' +} + # 4. Wake-on-LAN: let the wired adapter wake the PC (the BIOS setting must be on too) $nics = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object Status -eq 'Up' foreach ($nic in $nics) { diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index cefcdfe..8abd0b4 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -529,8 +529,12 @@ class TestFixes(unittest.TestCase): self.assertEqual(self.plan("arena"), [("fix", "arena")]) self.assertEqual(self.plan("apps"), [("fix", "vista")]) # 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")]) + # starts rtpMIDI only if it is not running, and reconnects the peer from the Mac's side. + self.assertEqual(sorted(self.plan("midi")), [("fix", "midi_reconnect"), ("fix", "rtpmidi_start")]) + run = CONFIG["fixes"]["rtpmidi_start"]["run"] + self.assertTrue(run.startswith("(sc query rtpMIDIService | find"), run) + self.assertNotIn("Restart-Service", run) + self.assertNotIn("net stop", run) for fix_id in ("obs", "arena", "vista"): # never closes a running app self.assertTrue(CONFIG["fixes"][fix_id]["run"].startswith("tasklist "), fix_id) @@ -575,6 +579,9 @@ class TestFixes(unittest.TestCase): 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 + back = coremidi.without_peer(new, "Lighting PC") + self.assertEqual(back, sess) + self.assertIsNone(coremidi.without_peer(sess, "Lighting PC")) # not there def test_an_off_pc_is_woken(self): from unittest import mock