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 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 16:16:31 -04:00
co-authored by Claude Opus 5.5
parent 1d05412359
commit 288c1e6e1a
5 changed files with 47 additions and 8 deletions
+1
View File
@@ -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) |
+3 -1
View File
@@ -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" }
] },
+28 -5
View File
@@ -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 = {}
+6
View File
@@ -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) {
+9 -2
View File
@@ -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