HyperDeck IPs from Companion, Vista Art-Net heartbeat, quiet Tascam module
- The hold-to-fix deck probe takes each HyperDeck's IP from its Companion connection's "ip" variable, so the config no longer repeats them. - APPS shows "VISTA SILENT" when the Lighting PC's Art-Net poll (every 0.5 s) is not heard for 5 s: a crashed or closed Vista shows in seconds, not the minute macOS takes to drop a network MIDI peer. - tascam-quiet/ builds the Tascam DA-6400 module 1.3.1-quiet: the recorder's "Illegal Status" warning is logged once a minute, not 5 times a second. - README: hold-to-fix table fixed and updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+55
-3
@@ -147,6 +147,8 @@ class Snapshot:
|
||||
self.remote = {} # (host, command) -> True / False / None (could not ask)
|
||||
self.dante_map = {} # Dante device name -> {"ip", "mac", "switch", "port", "shared", "poe", "seen"}
|
||||
self.dante_map_time = 0.0 # last successful survey of the Dante switches
|
||||
self.artnet_last = {} # source IP -> time its last Art-Net packet arrived
|
||||
self.artnet_since = None # when the Art-Net listener started (None: not listening)
|
||||
|
||||
def var(self, label, name):
|
||||
return self.vars.get((label, name))
|
||||
@@ -434,6 +436,18 @@ def check_midi_rx(chk, snap, ctx):
|
||||
return [("rx", Result(OK, info="%s %s" % (name, fmt_age(age))))]
|
||||
|
||||
|
||||
def check_artnet(chk, snap, ctx):
|
||||
"""A lighting program's Art-Net traffic (Vista polls for nodes every 0.5 s): its heartbeat."""
|
||||
limit = chk.get("max_silence_seconds", 5)
|
||||
since = snap.artnet_since
|
||||
if since is None or ctx["now"] - since < limit: # not listening, or only just started
|
||||
return [("artnet", Result(OK))]
|
||||
last = snap.artnet_last.get(chk["source"])
|
||||
if last is None or ctx["now"] - last > limit:
|
||||
return [("artnet", Result(ERR, chk.get("msg", "ART-NET SILENT"), named=True))]
|
||||
return [("artnet", Result(OK))]
|
||||
|
||||
|
||||
def remote_key(chk):
|
||||
return "%s\0%s" % (chk["host"], chk["cmd"])
|
||||
|
||||
@@ -449,7 +463,7 @@ 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,
|
||||
"mdns": check_mdns, "midi_rx": check_midi_rx, "remote": check_remote,
|
||||
"mdns": check_mdns, "midi_rx": check_midi_rx, "remote": check_remote, "artnet": check_artnet,
|
||||
}
|
||||
|
||||
# Checks whose connection a long press may restart (Dante is shared, so not that one).
|
||||
@@ -471,6 +485,7 @@ def needed_variables(cfg):
|
||||
t, label = chk["type"], chk.get("conn")
|
||||
if t == "hyperdeck":
|
||||
fast += [(label, "status"), (label, "slotId"), (label, "recordingTime")]
|
||||
slow.append((label, "ip")) # the deck's address, from Companion
|
||||
elif t == "dante":
|
||||
fast.append((label, "devices"))
|
||||
elif t == "tascam":
|
||||
@@ -607,6 +622,7 @@ class Engine:
|
||||
self.executor = execute_fix
|
||||
self.poe_executor = execute_poe
|
||||
self.notes = {} # tile id -> hold feedback {"text", "level", "until"}
|
||||
self.deck_ips = {} # HyperDeck connection -> last address Companion reported
|
||||
self.now = 0.0
|
||||
|
||||
# raw results for every check key
|
||||
@@ -779,9 +795,16 @@ class Engine:
|
||||
if fix_id:
|
||||
if chk["type"] == "hyperdeck" and hyperdeck_recording(snap, chk["conn"]):
|
||||
continue
|
||||
# One run per fix, except a HyperDeck reboot, which is per deck.
|
||||
# One run per fix, except a HyperDeck fix, which is per deck. The deck's
|
||||
# address comes from its Companion connection (remembered if it goes away).
|
||||
per_deck = self.cfg["fixes"][fix_id].get("kind") == "hyperdeck"
|
||||
ident = ("fix", fix_id, chk.get("ip", "") if per_deck else "")
|
||||
if per_deck:
|
||||
ip = chk.get("ip") or snap.var(chk["conn"], "ip") or self.deck_ips.get(chk["conn"])
|
||||
if not ip:
|
||||
continue
|
||||
self.deck_ips[chk["conn"]] = ip
|
||||
chk = dict(chk, ip=ip)
|
||||
ident = ("fix", fix_id, chk["conn"] if per_deck else "")
|
||||
if ident not in seen:
|
||||
seen.add(ident)
|
||||
plan.append((ident, "fix", fix_id, chk))
|
||||
@@ -1311,6 +1334,33 @@ def execute_fix(cfg, fix_id, chk):
|
||||
return False
|
||||
|
||||
|
||||
def start_artnet_listener(snap, stop, port=6454):
|
||||
"""Note when each source last sent Art-Net. Shares the port (SO_REUSEPORT) so it never
|
||||
keeps another program from listening too. Passive: nothing is ever sent."""
|
||||
import select as _select
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"):
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
try:
|
||||
s.bind(("", port))
|
||||
except OSError as e:
|
||||
log.warning("Art-Net listener could not start on port %d: %s", port, e)
|
||||
return None
|
||||
|
||||
def loop():
|
||||
snap.artnet_since = time.time()
|
||||
while not stop.is_set():
|
||||
if _select.select([s], [], [], 0.5)[0]:
|
||||
data, (src, _) = s.recvfrom(2048)
|
||||
if data[:8] == b"Art-Net\x00":
|
||||
snap.artnet_last = dict(snap.artnet_last, **{src: time.time()})
|
||||
s.close()
|
||||
t = threading.Thread(target=loop, name="artnet", daemon=True)
|
||||
t.start()
|
||||
return t
|
||||
|
||||
|
||||
class Periodic(threading.Thread):
|
||||
def __init__(self, name, interval, fn, stop):
|
||||
super().__init__(name=name, daemon=True)
|
||||
@@ -1400,6 +1450,8 @@ def start_pollers(cfg, snap, stop, data_dir=None):
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
if any(c["type"] == "artnet" for t in cfg["tiles"] for c in t.get("checks", [])):
|
||||
start_artnet_listener(snap, stop)
|
||||
return threads
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user