Wiring: layouts (Hub, Columns, Groups, Free), All/None, Dante in its own box, gentle trackpad zoom; fix the Keys pages (a second wireLayout) and Computers > Setup (a second setupHtml)
RossTalk and OSC: the watchdog reads Companion's RossTalk port (ROSSTALK OFF, PROP ROSSTALK LOST at NA/WS/SC), Bible TV and Ableton must be listening for Companion's OSC (BIBLE OSC OFF, ABLETON OSC OFF), and both are their own connection types on the Wiring screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+95
-5
@@ -170,6 +170,8 @@ class Snapshot:
|
||||
self.remote_midi_time = {} # host -> when it last reported
|
||||
self.peer_addr = {} # (host or "", session, peer) -> address it was last seen at
|
||||
self.conn_hosts = {} # Companion connection label -> its "host" setting
|
||||
self.conn_info = {} # Companion connection label -> {"module", "port"} (the Wiring screen's protocols)
|
||||
self.rosstalk = None # Companion's RossTalk port: {"listen", "clients": [{"ip", "cmd"}], "targets": [ip], "time"}
|
||||
self.prefs = {} # (preferences domain, key) -> value as "defaults read" prints it
|
||||
self.atem = None # atem_watch.js's latest report
|
||||
self.spotify = None # {"running", "state", "track", "name", "artist"} on NA Ingest
|
||||
@@ -503,6 +505,26 @@ def check_heartbeat(chk, snap, ctx):
|
||||
return [("hb", Result(OK))]
|
||||
|
||||
|
||||
def check_rosstalk(chk, snap, ctx):
|
||||
"""RossTalk into Companion (2026-10-27: "ross talk for companion, resi, prop"): Companion's listener is open and,
|
||||
with "address", that machine is connected to it ("local": a program on this Mac whose name has "process")."""
|
||||
rt = snap.rosstalk
|
||||
if not rt or ctx["now"] - rt.get("time", 0) > 60:
|
||||
return [("rosstalk", Result(OK))] # not looked yet
|
||||
if not rt["listen"]:
|
||||
return [("rosstalk", Result(ERR, chk.get("off_msg", "ROSSTALK OFF")))]
|
||||
addr = chk.get("address")
|
||||
if addr:
|
||||
if addr == "local": # lsof cuts program names to 9 letters ("ProPresen")
|
||||
want = (chk.get("process") or "").lower()[:9]
|
||||
ok = any(c["cmd"] and c["cmd"].lower().startswith(want) for c in rt["clients"])
|
||||
else:
|
||||
ok = any(c["ip"] == addr for c in rt["clients"])
|
||||
if not ok:
|
||||
return [("rosstalk", Result(ERR, chk.get("msg") or "%s ROSSTALK LOST" % chk.get("name", "").upper(), named=True))]
|
||||
return [("rosstalk", Result(OK))]
|
||||
|
||||
|
||||
def check_process(chk, snap, ctx):
|
||||
running = snap.procs.get(chk["match"])
|
||||
if running is False:
|
||||
@@ -976,6 +998,7 @@ CHECKS = {
|
||||
"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, "tune_rx": check_tune_rx, "heartbeat": check_heartbeat,
|
||||
"rosstalk": check_rosstalk,
|
||||
}
|
||||
|
||||
|
||||
@@ -2911,12 +2934,76 @@ def companion_conn_hosts(path):
|
||||
return hosts
|
||||
|
||||
|
||||
def poll_conn_hosts(snap, cfg):
|
||||
if any(c["type"] == "ping" and c.get("conn") for t in all_tiles(cfg) for c in t.get("checks", [])):
|
||||
def companion_conn_info(path):
|
||||
"""{connection label: {"module", "port"}} from Companion's settings database (read-only): which protocol each
|
||||
connection speaks (generic-osc / ableton-osc: OSC; resi-decoders: RossTalk) and the port it sends to."""
|
||||
import sqlite3
|
||||
if not path or not os.path.exists(path):
|
||||
return {}
|
||||
db = sqlite3.connect("file:%s?mode=ro" % urllib.parse.quote(path), uri=True, timeout=2)
|
||||
try:
|
||||
rows = db.execute("SELECT value FROM instances").fetchall()
|
||||
finally:
|
||||
db.close()
|
||||
out = {}
|
||||
for (value,) in rows:
|
||||
try:
|
||||
snap.conn_hosts = companion_conn_hosts(companion_db_path(cfg)) or snap.conn_hosts
|
||||
except Exception as e: # Companion's schema is its own business: fall back to "host"
|
||||
log.warning("could not read connection addresses from Companion: %s", e)
|
||||
inst = json.loads(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if isinstance(inst, dict) and inst.get("label"):
|
||||
conf = inst.get("config") or {}
|
||||
out[inst["label"]] = {"module": inst.get("moduleId") or inst.get("instance_type") or "",
|
||||
"port": conf.get("targetPort") or conf.get("port")}
|
||||
return out
|
||||
|
||||
|
||||
def poll_conn_hosts(snap, cfg):
|
||||
try:
|
||||
path = companion_db_path(cfg)
|
||||
snap.conn_hosts = companion_conn_hosts(path) or snap.conn_hosts
|
||||
snap.conn_info = companion_conn_info(path) or snap.conn_info
|
||||
except Exception as e: # Companion's schema is its own business: fall back to "host"
|
||||
log.warning("could not read connection addresses from Companion: %s", e)
|
||||
|
||||
|
||||
ROSSTALK_PORT = 7788
|
||||
|
||||
|
||||
def parse_rosstalk(out, port=ROSSTALK_PORT):
|
||||
"""`lsof -nP -iTCP:<port>` on the Companion Mac -> {"listen", "clients", "targets"}: whether Companion's RossTalk
|
||||
listener is open, who is connected to it (ProPresenter sends its slide cues this way; a client on this Mac
|
||||
itself is named by its program), and the devices Companion talks RossTalk to (the Resi decoder)."""
|
||||
listen, ends = False, []
|
||||
for line in out.splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) < 9:
|
||||
continue
|
||||
name, state = parts[8], " ".join(parts[9:])
|
||||
if "(LISTEN)" in state and name.endswith(":%d" % port):
|
||||
listen = True
|
||||
elif "->" in name and "ESTABLISHED" in state:
|
||||
local, remote = name.split("->", 1)
|
||||
ends.append((parts[0], local, remote))
|
||||
by_local = {local: cmd for cmd, local, _ in ends}
|
||||
clients, targets = [], []
|
||||
for cmd, local, remote in ends:
|
||||
rip, rport = remote.rsplit(":", 1)
|
||||
if local.endswith(":%d" % port): # someone connected to Companion's listener
|
||||
loop = rip.startswith("127.") or rip == local.rsplit(":", 1)[0]
|
||||
clients.append({"ip": rip, "cmd": by_local.get(remote, "") if loop else ""})
|
||||
elif rport == str(port) and not rip.startswith("127."): # Companion talking RossTalk to a device
|
||||
targets.append(rip)
|
||||
return {"listen": listen, "clients": clients, "targets": sorted(set(targets))}
|
||||
|
||||
|
||||
def poll_rosstalk(snap, cfg):
|
||||
port = int(cfg.get("rosstalk_port", ROSSTALK_PORT))
|
||||
try:
|
||||
out = subprocess.run(["/usr/sbin/lsof", "-w", "-nP", "-iTCP:%d" % port], capture_output=True, text=True, timeout=8).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return
|
||||
snap.rosstalk = dict(parse_rosstalk(out, port), time=time.time())
|
||||
|
||||
|
||||
SPOTIFY_STATE = ('tell application "Spotify" to return (player state as text) & "|" & (id of current track) '
|
||||
@@ -4021,6 +4108,7 @@ def start_pollers(cfg, snap, stop, data_dir=None):
|
||||
Periodic("variables", 1.0, lambda: poll_variables(c2, snap, fast), stop),
|
||||
Periodic("slow-variables", 3.0, lambda: poll_variables(c3, snap, slow), stop),
|
||||
Periodic("conn-hosts", 60.0, lambda: poll_conn_hosts(snap, cfg), stop),
|
||||
Periodic("rosstalk", 5.0, lambda: poll_rosstalk(snap, cfg), stop),
|
||||
Periodic("spotify", 3.0, lambda: poll_spotify(snap, cfg), stop),
|
||||
Periodic("pings", float(PING_INTERVAL), lambda: poll_pings(snap, ping_hosts(cfg, snap), iface), stop),
|
||||
Periodic("fast-pings", 1.0, lambda: poll_pings(snap, ping_hosts(cfg, snap, fast=True), None), stop),
|
||||
@@ -4060,6 +4148,8 @@ def snapshot_summary(snap, engine, now):
|
||||
for label, c in snap.conns.items()},
|
||||
"conns_fresh": snap.conns_fresh(now),
|
||||
"conn_hosts": dict(snap.conn_hosts),
|
||||
"conn_info": dict(snap.conn_info),
|
||||
"rosstalk": snap.rosstalk,
|
||||
"ping": dict(snap.ping),
|
||||
"link": snap.link,
|
||||
"midi": snap.midi if snap.midi_time else None,
|
||||
|
||||
Reference in New Issue
Block a user