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