MIDI key: every network MIDI peer must come over the wired network

The Macs are also on Wi-Fi and the Dante networks, and Bonjour lists each MIDI session once per
network. ProdDash's Vista Cues module had Tracks and Companion set to their Wi-Fi addresses, so
lighting MIDI rode Wi-Fi. A midi_wired check now names any peer, in NA Ingest's session or the
other Macs', whose address is outside the wired network (10.1.0.0/16 at NA).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 10:09:34 -04:00
co-authored by Claude Opus 5.5
parent b41b8a49d2
commit 79318976c5
6 changed files with 84 additions and 2 deletions
+6
View File
@@ -1799,6 +1799,12 @@
"name": "TRACKS IN",
"source": "Network NA Ingest Listen",
"type": "midi_rx"
},
{
"nets": [
"10.1.0.0/16"
],
"type": "midi_wired"
}
],
"id": "midi",
+14
View File
@@ -9,6 +9,7 @@ must be declared here, every default in the code must equal the one here).
Paths: "companion.host", "tiles[].checks[].conn" (a list element), "fixes.*.run" (a map entry).
"""
import copy
import ipaddress
import re
import status_panel as sp
@@ -298,6 +299,7 @@ field("tiles[].checks[].warn_gb", "int", 20, "Amber below this many GB free.", "
field("tiles[].checks[].err_gb", "int", 5, "Red below this many GB free.", "Keys", advanced=True)
field("tiles[].checks[].session", "str", None, "The network MIDI session.", "Keys")
field("tiles[].checks[].peer", "str", None, "The peer that must be in it.", "Keys")
field("tiles[].checks[].nets", "list", None, "The wired network(s) every MIDI peer must come over, e.g. 10.1.0.0/16.", "Keys")
field("tiles[].checks[].address", "str", None, "Where to reconnect the peer if it was never seen.", "Keys", advanced=True)
field("tiles[].checks[].domain", "str", None, "The macOS preference domain.", "Keys")
field("tiles[].checks[].key", "str", None, "The preference key.", "Keys")
@@ -423,6 +425,8 @@ CHECKS = {
"question": "Is a setting right?", "words": "Free space on a disk."},
"midi_peer": {"fields": ["session", "peer", "host", "address"], "subs": ["peer"], "identity": ["host", "session", "peer"],
"module": None, "question": "Is a MIDI link up?", "words": "A peer is in a network MIDI session (here or on another Mac)."},
"midi_wired": {"fields": ["nets"], "subs": ["wired", "<host|session|peer>"], "identity": [], "module": None,
"question": "Is a MIDI link up?", "words": "Every network MIDI peer comes over the wired network (not Wi-Fi or Dante)."},
"pref": {"fields": ["domain", "key", "want", "msg", "template", "params"], "subs": ["pref"], "identity": ["domain", "key"],
"module": None, "question": "Is a setting right?", "words": "A macOS preference has the value it should."},
"spotify": {"fields": ["playlist", "fix"], "subs": ["spotify"], "identity": [], "module": None,
@@ -611,6 +615,16 @@ def validate(cfg, live=None):
errors.append("%s: a ping needs an address or a connection" % cw)
if c["type"] == "midi_peer" and not (c.get("session") and c.get("peer")):
errors.append("%s: a MIDI link needs a session and a peer" % cw)
if c["type"] == "midi_wired":
nets = c.get("nets")
if not (isinstance(nets, list) and nets):
errors.append("%s: a wired-MIDI check needs the wired network(s), e.g. 10.1.0.0/16" % cw)
else:
for n in nets:
try:
ipaddress.ip_network(str(n), strict=False)
except ValueError:
errors.append("%s: %r is not a network (like 10.1.0.0/16)" % (cw, n))
if c["type"] == "remote" and not (c.get("host") and c.get("cmd") and c.get("msg")):
errors.append("%s: a remote check needs a computer, a command and the words to show" % cw)
fx = c.get("fix")
+2
View File
@@ -69,6 +69,8 @@ def check_sentence(chk, cfg):
if t == "midi_peer":
where = ("on %s" % who(chk["host"])) if chk.get("host") else "on this Mac"
return "the network MIDI session %s %s has %s in it" % (e(chk["session"]), where, e(chk["peer"]))
if t == "midi_wired":
return "every peer in the network MIDI sessions (here and on the other Macs) comes over the wired network %s, not Wi-Fi or Dante" % e(", ".join(chk.get("nets") or []))
if t == "pref":
return "the setting %s in %s is %s" % (e(chk["key"]), e(chk["domain"]), e(chk["want"]))
if t == "spotify":
+1 -1
View File
@@ -501,7 +501,7 @@
const checks = (t.checks || []).map((chk, j) => {
const spec = state.schema.checks[chk.type] || { fields: [], question: "" };
const sentence = (kt.checks[j] || {}).sentence || chk.type;
const what = chk.type === "midi_peer" ? `${chk.peer || "?"} <span class="muted">in ${esc(chk.session || "?")}</span>` : esc(chk.conn || chk.name || (chk.params || {}).process || chk.msg || chk.host || chk.label || chk.url || chk.session || short(sentence, 40));
const what = chk.type === "midi_peer" ? `${chk.peer || "?"} <span class="muted">in ${esc(chk.session || "?")}</span>` : chk.type === "midi_wired" ? `every MIDI peer wired <span class="muted">${esc((chk.nets || []).join(", "))}</span>` : esc(chk.conn || chk.name || (chk.params || {}).process || chk.msg || chk.host || chk.label || chk.url || chk.session || short(sentence, 40));
const q = spec.question || chk.type;
const header = q !== lastQ ? `<div class="chkgroup">${esc(q)}</div>` : ""; lastQ = q;
const summary = `<summary>${onOff(`${list}.${i}.checks.${j}`, chk, "this check")} <b class="${chk.disabled ? "muted" : ""}">${what}</b>${hint(sentence)}</summary>`;
+35 -1
View File
@@ -18,6 +18,7 @@ import argparse
import calendar
import hashlib
import http.client
import ipaddress
import json
import logging
import math
@@ -507,6 +508,39 @@ def check_midi_peer(chk, snap, ctx):
return [("peer", Result(ERR, "MIDI LOST"))]
def peer_ip(address):
"""The IPv4 address of a MIDI peer ("10.1.1.1:5040"), or None for a Bonjour name or IPv6."""
host = str(address or "").strip().rsplit(":", 1)[0]
try:
ip = ipaddress.ip_address(host)
except ValueError:
return None
return ip if ip.version == 4 else None
def check_midi_wired(chk, snap, ctx):
"""Every peer in the network MIDI sessions (here and on the other Macs) comes over the wired
network. The Macs are also on Wi-Fi and the Dante networks, and Bonjour lists each session once
per network: a peer joined through the wrong copy works until Wi-Fi hiccups mid-service
(2026-10-20: ProdDash joined Tracks over both Macs' Wi-Fi). Names the peer and the Mac."""
nets = [ipaddress.ip_network(n, strict=False) for n in chk["nets"]]
groups = [("", snap.midi if snap.midi_time else None)]
groups += [(h, s) for h, s in snap.remote_midi.items() if ctx["now"] - snap.remote_midi_time.get(h, 0) <= 12]
out, seen = [], set()
for host, sessions in groups:
for sname, sess in (sessions or {}).items():
for p in sess.get("peers", []):
ip = peer_ip(p.get("address"))
if ip is None or ip.is_loopback or any(ip in n for n in nets) or (p["name"], str(ip)) in seen:
continue
seen.add((p["name"], str(ip)))
name = p["name"].upper()
name = name if len(name) <= 14 else name[:13] + "."
out.append(("%s|%s|%s" % (host, sname, p["name"]),
Result(ERR, "%s NOT WIRED" % name, named=True)))
return out or [("wired", Result(OK))]
def check_mdns(chk, snap, ctx):
if snap.mdns_time and chk["instance"] not in snap.mdns:
return [("mdns", Result(ERR, chk.get("name", chk["instance"]) + " GONE", named=True))]
@@ -597,7 +631,7 @@ READY_ACTIONS = ("recheck", "lineup_sync", "led_cross", "copy_tracks", "ltc_to_p
CHECKS = {
"conn": check_conn, "hyperdeck": check_hyperdeck, "dante": check_dante, "shure": check_shure,
"psm": check_psm, "tascam": check_tascam, "ping": check_ping, "launchd": check_launchd,
"process": check_process, "http": check_http, "disk": check_disk, "midi_peer": check_midi_peer, "pref": check_pref, "spotify": check_spotify,
"process": check_process, "http": check_http, "disk": check_disk, "midi_peer": check_midi_peer, "midi_wired": check_midi_wired, "pref": check_pref, "spotify": check_spotify,
"mdns": check_mdns, "midi_rx": check_midi_rx, "remote": check_remote, "artnet": check_artnet,
"ready": check_ready, "ready_manual": check_ready_manual,
}
+26
View File
@@ -458,6 +458,32 @@ class TestChecks(unittest.TestCase):
self.assertEqual(sp.check_midi_peer({"type": "midi_peer", "host": "tracks", "session": "NA Tracks",
"peer": "Lighting PC"}, self.snap, self.ctx)[0][1].level, sp.OK)
def test_a_midi_peer_on_wi_fi_is_named(self):
"""2026-10-20: ProdDash joined Tracks' session over both Macs' Wi-Fi."""
chk = {"type": "midi_wired", "nets": ["10.1.0.0/16"]}
self.assertEqual([r.level for _, r in sp.check_midi_wired(chk, self.snap, self.ctx)], [sp.OK])
self.snap.remote_midi["tracks"] = {"NA Tracks": {"port": 5004, "peers": [
{"name": "ProdDash Lighting", "address": "10.6.3.222:5040", "state": "running"},
{"name": "Studio B Mac Studio", "address": "\tStudio B Mac Studio\tlocal.", "state": "running"}, # a name: no verdict
{"name": "NA Ingest", "address": "[fe80::1]:5004", "state": "running"}]}}
self.snap.remote_midi_time["tracks"] = self.now
self.snap.midi["NA Ingest"]["peers"].append({"name": "ProdDash Lighting", "address": "10.6.3.222:5040", "state": "running"})
out = sp.check_midi_wired(chk, self.snap, self.ctx)
self.assertEqual([r.msg for _, r in out], ["PRODDASH LIGH. NOT WIRED"]) # once, not per session
self.snap.remote_midi_time["tracks"] = self.now - 30 # a stale stream: no verdict
self.snap.midi["NA Ingest"]["peers"].pop()
self.assertEqual([r.level for _, r in sp.check_midi_wired(chk, self.snap, self.ctx)], [sp.OK])
cfg = json.loads(json.dumps(CONFIG))
next(t for t in cfg["tiles"] if t["id"] == "midi")["checks"].append(dict(chk))
import schema
self.assertEqual([e for e in schema.validate(cfg)[0] if "wired" in e], [])
next(t for t in cfg["tiles"] if t["id"] == "midi")["checks"][-1]["nets"] = ["wifi"]
self.assertTrue(any("not a network" in e for e in schema.validate(cfg)[0]))
self.snap.midi["NA Ingest"]["peers"].append({"name": "ProdDash Lighting", "address": "10.6.3.222:5040", "state": "running"})
next(t for t in cfg["tiles"] if t["id"] == "midi")["checks"][-1]["nets"] = ["10.1.0.0/16"]
tiles, _ = run_steps(sp.Engine(cfg), self.snap, self.now, 4)
self.assertEqual(tiles["midi"]["x"], "MIDI\nPRODDASH LIGH. NOT WIRED")
def test_midi_peer_joining_is_not_an_error(self):
for p in self.snap.midi["NA Ingest"]["peers"]:
p["state"] = "connected" # joined, clock sync not finished yet