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:
2026-10-27 13:45:09 -04:00
co-authored by Claude Opus 5.5
parent 26804e4ce2
commit 5d0263a9b9
11 changed files with 490 additions and 165 deletions
+19 -1
View File
@@ -109,8 +109,12 @@ class TestServer(unittest.TestCase):
json.dump(live, open(os.path.join(self.data, "machines-live.json"), "w"))
snap = json.load(open(os.path.join(self.data, "snapshot.json")))
snap.update({"conns": {"ATEM": {"category": "good", "enabled": True}, "x32": {"category": "error", "enabled": True},
"BibleTV": {"category": "good", "enabled": True}, "RESI_Decoder": {"category": "good", "enabled": True},
"HyperDeck_1": {"category": "good", "enabled": True}, "tcp-HyperDeck-1": {"category": "good", "enabled": True}},
"conn_hosts": {"ATEM": "10.1.10.2", "x32": "10.1.1.112", "HyperDeck_1": "10.1.10.11", "tcp-HyperDeck-1": "10.1.10.11"},
"conn_hosts": {"ATEM": "10.1.10.2", "x32": "10.1.1.112", "HyperDeck_1": "10.1.10.11", "tcp-HyperDeck-1": "10.1.10.11",
"BibleTV": "10.6.7.13", "RESI_Decoder": "10.1.10.203"},
"conn_info": {"BibleTV": {"module": "generic-osc", "port": 3333}, "RESI_Decoder": {"module": "resi-decoders", "port": None}},
"rosstalk": {"listen": True, "clients": [{"ip": "10.1.14.225", "cmd": ""}], "targets": ["10.1.10.203"], "time": time.time()},
"remote_midi": {"tracks": {"NA Tracks": {"peers": [{"address": "10.1.13.140:5004", "name": "SuperRack-LiveBox", "state": "running"}]}}},
"dante_map": {"NA-Tracks": {"switch": "10.66.1.100", "port": "0/6", "seen": time.time()},
"FOH-SuperRack-LiveBox": {"switch": "10.66.1.102", "port": "0/9", "seen": time.time()},
@@ -134,6 +138,9 @@ class TestServer(unittest.TestCase):
self.assertIn(("dante", "h:tracks", "sw:10.66.1.100", "ok"), edges) # NA-Tracks is the Tracks Mac
self.assertIn("FOH-SuperRack-LiveBox", nodes["d:10.1.13.140"]["names"]) # same box as its MIDI peer
self.assertIn("d:studiobmon", nodes) # a monitor, not the Studio B Mac
self.assertIn(("osc", "h:ingest", "h:bibletv", "ok"), edges) # OSC to the Bible TV PC
self.assertIn(("rosstalk", "h:ingest", "d:10.1.10.203", "ok"), edges) # RossTalk to the Resi decoder
self.assertIn(("rosstalk", "h:prop", "h:ingest", "ok"), edges) # ProPresenter's cues into Companion
self.assertEqual({t["id"] for t in w["types"]} >= {"companion", "midi", "watch", "dante"}, True)
def test_gear_and_keys_and_pages(self):
@@ -416,6 +423,17 @@ class TestBindingAndProbes(unittest.TestCase):
tasks={"\\StatusPanel\\Start Vista": "Running", "\\StatusPanel\\Start rtpMIDI": "Ready"})
self.assertEqual([(x["name"], x["running"]) for x in win], [("Start Vista", True), ("start-vista.ps1", True), ("Start rtpMIDI", False)])
def test_the_page_script_has_no_two_functions_with_one_name(self):
# 2026-10-27: a second wireLayout (the Wiring screen's) replaced the Keys screen's and broke it ("null errors"),
# and a second setupHtml had broken Computers > Setup the same way.
import collections
import re
js = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "settings", "ui", "app.js")).read()
# the page's own names: two spaces in, inside its one (function () { ... })
names = collections.Counter(re.findall(r"^ (?:async )?function\s+([A-Za-z0-9_$]+)\s*\(", js, re.M))
names.update(re.findall(r"^ (?:const|let)\s+([A-Za-z0-9_$]+)\s*=", js, re.M))
self.assertEqual([n for n, c in names.items() if c > 1], [])
def test_probe_commands_never_carry_names_from_the_settings(self):
cfg = json.loads(json.dumps(CONFIG))
cfg["hosts"]["tracks"]["runs"][1]["process"] = "x; rm -rf ~" # a hostile name in the settings
+32
View File
@@ -677,6 +677,38 @@ class TestHyperDeckRecord(unittest.TestCase):
self.assertFalse(sp.hyperdeck_record("127.0.0.1", "x", port=1, timeout=1))
class TestRossTalk(unittest.TestCase):
"""Companion's RossTalk port (2026-10-27): ProPresenter's cues come in on 7788, Companion talks to the Resi decoder."""
NA = ("COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n"
"node 925 Studio 37u IPv6 0x1 0t0 TCP *:7788 (LISTEN)\n"
"node 925 Studio 155u IPv6 0x2 0t0 TCP 10.1.1.1:7788->10.1.14.225:61156 (ESTABLISHED)\n"
"node 1058 Studio 22u IPv4 0x3 0t0 TCP 10.1.1.1:49697->10.1.10.203:7788 (ESTABLISHED)\n")
WS = ("COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n"
"node 973 wsprop 38u IPv6 0x1 0t0 TCP *:7788 (LISTEN)\n"
"node 973 wsprop 43u IPv6 0x2 0t0 TCP 127.0.0.1:7788->127.0.0.1:53750 (ESTABLISHED)\n"
"node 17116 wsprop 22u IPv4 0x3 0t0 TCP 10.2.2.210:57966->10.2.1.166:7788 (ESTABLISHED)\n"
"ProPresen 53124 wsprop 77u IPv4 0x4 0t0 TCP 127.0.0.1:53750->127.0.0.1:7788 (ESTABLISHED)\n")
def check(self, chk, rt, now=1000.0):
snap = sp.Snapshot()
snap.rosstalk = dict(rt, time=now) if rt is not None else None
return dict(sp.check_rosstalk(chk, snap, {"now": now}))["rosstalk"]
def test_parsing(self):
self.assertEqual(sp.parse_rosstalk(self.NA), {"listen": True, "clients": [{"ip": "10.1.14.225", "cmd": ""}], "targets": ["10.1.10.203"]})
self.assertEqual(sp.parse_rosstalk(self.WS), {"listen": True, "clients": [{"ip": "127.0.0.1", "cmd": "ProPresen"}], "targets": ["10.2.1.166"]})
def test_prop_link(self):
na, ws = sp.parse_rosstalk(self.NA), sp.parse_rosstalk(self.WS)
self.assertEqual(self.check({"address": "10.1.14.225", "name": "PROP"}, na).level, sp.OK)
self.assertEqual(self.check({"address": "local", "process": "ProPresenter", "name": "PROP"}, ws).level, sp.OK)
gone = sp.parse_rosstalk("\n".join(l for l in self.NA.splitlines() if "61156" not in l))
self.assertEqual(self.check({"address": "10.1.14.225", "name": "PROP"}, gone).msg, "PROP ROSSTALK LOST")
off = sp.parse_rosstalk("COMMAND\n")
self.assertEqual(self.check({"address": "10.1.14.225", "name": "PROP"}, off).msg, "ROSSTALK OFF")
self.assertEqual(self.check({"address": "10.1.14.225"}, None).level, sp.OK) # not looked yet
class TestTextSize(unittest.TestCase):
def test_no_word_is_split(self):
for text in ("TASCAM\nOK", "SERVICE\nON\nmanual", "OBS\nOFFLINE", "INGEST\n97GB free",