MIDI hold and auto-fix move a link joined by name to the owning Mac's wired address
- WS 2026-10-21: FOH Mac mini joined ProPresenter's session by name; hold said "no automatic fix". Now the address comes from the link's own check, else the Mac that owns that session (wired SSH / ingest address + session port); auto-fix does it too, never while a HyperDeck records - WS: FOH Mac mini <-> ProPresenter watched (FOH PROP); auto-fix moved it to 10.2.1.12:5004 by itself Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -537,6 +537,14 @@
|
|||||||
"tag": "PROP LIGHTS",
|
"tag": "PROP LIGHTS",
|
||||||
"type": "midi_peer"
|
"type": "midi_peer"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "midi_peer",
|
||||||
|
"session": "ProPresenter",
|
||||||
|
"peer": "WS FOH Mac mini",
|
||||||
|
"address": "10.2.1.12:5004",
|
||||||
|
"fix": "midi_reconnect",
|
||||||
|
"tag": "FOH PROP"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fix": "midi_reconnect",
|
"fix": "midi_reconnect",
|
||||||
"nets": [
|
"nets": [
|
||||||
|
|||||||
+30
-5
@@ -687,6 +687,27 @@ def by_name(address):
|
|||||||
return str(address or "").startswith("\t")
|
return str(address or "").startswith("\t")
|
||||||
|
|
||||||
|
|
||||||
|
def owner_wired_address(cfg, snap, session_name, nets):
|
||||||
|
""""ip:port" of the Mac whose network MIDI session is called `session_name`, at its wired address:
|
||||||
|
this Mac (its wired ingest address), or another computer the settings know (its SSH address), using
|
||||||
|
that session's own port. None when no known Mac owns it or none of its addresses is wired."""
|
||||||
|
def wired(ip):
|
||||||
|
try:
|
||||||
|
return any(ipaddress.ip_address(ip) in n for n in nets)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if snap.midi and session_name in snap.midi:
|
||||||
|
ip = next((i for i in (cfg.get("site") or {}).get("ingest_ips") or [] if wired(i)), None)
|
||||||
|
if ip:
|
||||||
|
return "%s:%s" % (ip, snap.midi[session_name].get("port") or 5004)
|
||||||
|
for alias, sessions in (snap.remote_midi or {}).items():
|
||||||
|
if session_name in (sessions or {}):
|
||||||
|
ip = str((cfg.get("hosts") or {}).get(alias, {}).get("ssh", "")).split("@")[-1]
|
||||||
|
if wired(ip):
|
||||||
|
return "%s:%s" % (ip, sessions[session_name].get("port") or 5004)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def wired_nets(cfg):
|
def wired_nets(cfg):
|
||||||
"""The wired network(s) the MIDI key's wired check names (empty: no rule)."""
|
"""The wired network(s) the MIDI key's wired check names (empty: no rule)."""
|
||||||
nets = []
|
nets = []
|
||||||
@@ -1602,16 +1623,20 @@ class Engine:
|
|||||||
target = chk["conn"]
|
target = chk["conn"]
|
||||||
if fix_kind == "midi_connect" and chk["type"] == "midi_wired":
|
if fix_kind == "midi_connect" and chk["type"] == "midi_wired":
|
||||||
# A peer off the wired network or joined by name: join it again at the wired
|
# A peer off the wired network or joined by name: join it again at the wired
|
||||||
# address its own MIDI check names. One no check names (ProdDash's own) is left
|
# address its own MIDI check names, else at the wired address of the Mac whose
|
||||||
# to its program: the key says which.
|
# session it is (WS, 2026-10-21: FOH Mac mini joined ProPresenter by name and a hold
|
||||||
if errors_only:
|
# did nothing). Moving a working link drops it for a second: auto-fix does it too,
|
||||||
continue # moving a working link drops it a second: a hold only
|
# but never while a HyperDeck records. A peer no known Mac owns (ProdDash's own) is
|
||||||
|
# left to its program: the key says which.
|
||||||
|
if errors_only and self.any_recording:
|
||||||
|
continue
|
||||||
host, session, peer = (sub.split("|", 2) + ["", ""])[:3]
|
host, session, peer = (sub.split("|", 2) + ["", ""])[:3]
|
||||||
nets = wired_nets(self.cfg)
|
nets = wired_nets(self.cfg)
|
||||||
want = next((c.get("address") for t2 in all_tiles(self.cfg) for c in t2.get("checks", [])
|
want = next((c.get("address") for t2 in all_tiles(self.cfg) for c in t2.get("checks", [])
|
||||||
if c.get("type") == "midi_peer" and c.get("host", "") == host and c.get("session") == session
|
if c.get("type") == "midi_peer" and c.get("host", "") == host and c.get("session") == session
|
||||||
and c.get("peer") == peer and peer_ip(c.get("address")) is not None
|
and c.get("peer") == peer and peer_ip(c.get("address")) is not None
|
||||||
and any(peer_ip(c["address"]) in n for n in nets)), None)
|
and any(peer_ip(c["address"]) in n for n in nets)), None) \
|
||||||
|
or owner_wired_address(self.cfg, snap, peer, nets)
|
||||||
if not want:
|
if not want:
|
||||||
continue
|
continue
|
||||||
chk = dict({"host": host} if host else {}, type="midi_peer", session=session, peer=peer, address=want, repoint=True)
|
chk = dict({"host": host} if host else {}, type="midi_peer", session=session, peer=peer, address=want, repoint=True)
|
||||||
|
|||||||
@@ -1293,7 +1293,10 @@ class TestFixes(unittest.TestCase):
|
|||||||
self.assertEqual([(c["host"], c["session"], c["peer"], c["address"]) for c in moves],
|
self.assertEqual([(c["host"], c["session"], c["peer"], c["address"]) for c in moves],
|
||||||
[("tracks", "NA Tracks", "Studio B Mac Studio", "10.1.13.175:5004")]) # ProdDash's own: left to it
|
[("tracks", "NA Tracks", "Studio B Mac Studio", "10.1.13.175:5004")]) # ProdDash's own: left to it
|
||||||
auto = engine.fix_plan("midi", self.snap, SUNDAY + 30, 0, True)
|
auto = engine.fix_plan("midi", self.snap, SUNDAY + 30, 0, True)
|
||||||
self.assertFalse(any(c.get("repoint") for _, _, _, c in auto)) # never by itself: a hold
|
self.assertTrue(any(c.get("repoint") for _, _, _, c in auto)) # auto-fix moves it too (user, 2026-10-21) ...
|
||||||
|
engine.any_recording = True
|
||||||
|
auto = engine.fix_plan("midi", self.snap, SUNDAY + 30, 0, True)
|
||||||
|
self.assertFalse(any(c.get("repoint") for _, _, _, c in auto)) # ... but never while a HyperDeck records
|
||||||
|
|
||||||
def test_an_off_pc_is_woken(self):
|
def test_an_off_pc_is_woken(self):
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@@ -2663,3 +2666,36 @@ class TestMidiNoState(unittest.TestCase):
|
|||||||
self.assertEqual(self.check(s, 110.0).msg, "MIDI CONNECTING") # put back: not trusted yet
|
self.assertEqual(self.check(s, 110.0).msg, "MIDI CONNECTING") # put back: not trusted yet
|
||||||
self.assertEqual(self.check(s, 125.0).msg, "MIDI CONNECTING")
|
self.assertEqual(self.check(s, 125.0).msg, "MIDI CONNECTING")
|
||||||
self.assertEqual(self.check(s, 131.0).level, sp.OK) # stayed 20 s: it answered
|
self.assertEqual(self.check(s, 131.0).level, sp.OK) # stayed 20 s: it answered
|
||||||
|
|
||||||
|
|
||||||
|
class TestByNameMove(unittest.TestCase):
|
||||||
|
"""WS 2026-10-21: FOH Mac mini joined ProPresenter's session by name; a hold said "no automatic fix"."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.cfg = sp.load_config(os.path.join(ROOT, "sites", "ws", "config.json"))
|
||||||
|
self.engine = sp.Engine(self.cfg)
|
||||||
|
self.snap = healthy_snapshot(SUNDAY)
|
||||||
|
self.snap.midi = {"ProPresenter": {"port": 5004, "peers": [
|
||||||
|
{"name": "WS FOH Mac mini", "address": "\tWS FOH Mac mini\tlocal.", "state": "running"},
|
||||||
|
{"name": "Lighting", "address": "10.2.2.2:5004", "state": "running"}]}}
|
||||||
|
self.snap.midi_time = SUNDAY
|
||||||
|
foh = next(a for a, h in self.cfg["hosts"].items() if str(h.get("ssh", "")).endswith("@10.2.1.12"))
|
||||||
|
self.snap.remote_midi = {foh: {"WS FOH Mac mini": {"port": 5004, "peers": []}}}
|
||||||
|
self.snap.remote_midi_time = {foh: SUNDAY}
|
||||||
|
self.midi = next(t["id"] for t in self.cfg["tiles"] if t["label"] == "MIDI")
|
||||||
|
|
||||||
|
def moves(self, errors_only):
|
||||||
|
run_steps(self.engine, self.snap, SUNDAY, 3)
|
||||||
|
return [chk for _, kind, what, chk in self.engine.fix_plan(self.midi, self.snap, now=SUNDAY + 3, errors_only=errors_only)
|
||||||
|
if chk.get("repoint")]
|
||||||
|
|
||||||
|
def test_a_hold_moves_it_to_the_owning_macs_wired_address(self):
|
||||||
|
moves = self.moves(False)
|
||||||
|
self.assertEqual([(c["session"], c["peer"], c["address"]) for c in moves],
|
||||||
|
[("ProPresenter", "WS FOH Mac mini", "10.2.1.12:5004")])
|
||||||
|
|
||||||
|
def test_auto_fix_moves_it_too_but_never_while_recording(self):
|
||||||
|
self.assertEqual(len(self.moves(True)), 1)
|
||||||
|
self.engine.any_recording = True # a HyperDeck records: left alone
|
||||||
|
plan = self.engine.fix_plan(self.midi, self.snap, now=SUNDAY + 3, errors_only=True)
|
||||||
|
self.assertEqual([c for _, k, w, c in plan if c.get("repoint")], [])
|
||||||
|
|||||||
Reference in New Issue
Block a user