Restart page (23), force restarts, Tascam reconnect, ProdCom, Resi encoder
- Page 23 "Restart": hold a key 2 s to restart DVS (4 Macs), ProPresenter,
Ableton, Church Translate, ProdCom, ProdDash, captions, the bridge or the
Tascam connection even when the status page says it is fine ("force",
shared 60 s guard). RESTART PAGE key on page 19, STATUS PAGE back.
- Holds carry Companion's clock; ones queued while the watchdog was down
are dropped instead of firing together later.
- TASCAM hold always reconnects and checks again after 20 s: HUNG - POWER
CYCLE IT (pings, control port hung), NO NETWORK, or turns a disabled
connection on. Tascam pinged at its Companion address.
- INGEST: ProdCom (:24480), ProdDash's page (:24500), the caption screen;
DVS on Ingest restarts ProdCom afterwards. RESI: the encoder (10.1.10.202).
- Windows starts check the app really started (FIX FAILED otherwise); OBS's
start task runs elevated (OBS is "Run as administrator"); setup script
does that for any such app and gives sshd restart-on-failure.
- push_page.py checks what is on the target page, not its name (pages 22
and 97 are named "PAGE" but hold buttons); --which restart.
- Companion custom variables are written in the request body (no URL limit).
Reviewed by a 3-lens review with per-finding verification; 7 confirmed
issues fixed. Tested live: OBS hold opens OBS; Restart page TASCAM key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+108
-20
@@ -13,9 +13,10 @@ watchdog can never leave a frozen green page behind.
|
||||
|
||||
Buttons write commands into sp_cmd, which status_panel.py reads:
|
||||
press ack <tile> (the master acks everything)
|
||||
hold 2 seconds restart <tile> (restarts that tile's Companion connections)
|
||||
hold 2 seconds restart <tile> (fixes what that tile is failing on)
|
||||
Restart page force <key> (hold 2 s: restart that thing even when it looks fine)
|
||||
|
||||
python3 make_page.py -> status-page.companionconfig
|
||||
python3 make_page.py -> status-page.companionconfig and restart-page.companionconfig
|
||||
"""
|
||||
import gzip
|
||||
import hashlib
|
||||
@@ -96,6 +97,57 @@ def command_action(tid, role, cmd_var, command):
|
||||
}
|
||||
|
||||
|
||||
def set_page_action(tid, role, page):
|
||||
return {"type": "action", "id": stable_id(tid, role), "definitionId": "set_page", "connectionId": "internal",
|
||||
"options": {"surfaceId": prop("self"), "page": prop(page)}, "children": {}}
|
||||
|
||||
|
||||
def static_button(tid, text, bg, fg, page, note):
|
||||
"""A plain key (no watchdog state): tap goes to `page`."""
|
||||
geometry = {"enabled": prop(True), "opacity": prop(100), "x": prop(0), "y": prop(0),
|
||||
"width": prop(100), "height": prop(100), "rotation": prop(0)}
|
||||
layers = [
|
||||
{"id": "canvas", "name": "Canvas", "usage": "auto", "type": "canvas",
|
||||
"decoration": prop("border"), "showStatusIcons": prop("default")},
|
||||
dict({"id": "box0", "name": "Background", "usage": "auto", "type": "box"}, **geometry,
|
||||
color=prop(bg), borderWidth=prop(0), borderColor=prop(0), borderPosition=prop("inside")),
|
||||
dict({"id": "text0", "name": "Text", "usage": "auto", "type": "text"}, **geometry,
|
||||
text=prop(text), color=prop(fg), halign=prop("center"), valign=prop("center"),
|
||||
fontsize=prop("auto"), fontsizeAllowShrink=prop(True), font=prop("companion-sans"),
|
||||
outlineColor=prop(4278190080)),
|
||||
]
|
||||
return {
|
||||
"type": "button-layered",
|
||||
"style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
|
||||
"canModifyStyleInApis": False, "notes": note},
|
||||
"feedbacks": [],
|
||||
"steps": {"0": {"action_sets": {"down": [], "up": [set_page_action(tid, "go", page)]},
|
||||
"options": {"runWhileHeld": []}}},
|
||||
"localVariables": [],
|
||||
}
|
||||
|
||||
|
||||
def restart_key(key, cfg):
|
||||
"""A Restart page key: drawn by the watchdog like a tile; hold 2 s = "force <id>"."""
|
||||
names = cfg["variables"]
|
||||
stale_text = "'%s\\n?'" % key["label"]
|
||||
return {
|
||||
"type": "button-layered",
|
||||
"style": {"layers": layers(key["id"], key, names, stale_text)},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
|
||||
"canModifyStyleInApis": True,
|
||||
"notes": "RESTART key '%s' - hold 2 s: restart it even when the status page says it is fine "
|
||||
"(status_panel.py on NA Ingest runs fix '%s')." % (key["id"], key["fix"])},
|
||||
"feedbacks": [],
|
||||
"steps": {"0": {"action_sets": {"down": [], "up": [],
|
||||
str(HOLD_MS): [command_action(key["id"], "hold", names["command"],
|
||||
"force %s $(internal:time_unix)" % key["id"])]},
|
||||
"options": {"runWhileHeld": [HOLD_MS]}}},
|
||||
"localVariables": [],
|
||||
}
|
||||
|
||||
|
||||
def button(tid, tile, cfg):
|
||||
names = cfg["variables"]
|
||||
kind = tile.get("kind")
|
||||
@@ -114,13 +166,17 @@ def button(tid, tile, cfg):
|
||||
press = "ack " + tid
|
||||
fixable = "members" in tile or any((c["type"] in RESTARTABLE and c.get("conn")) or c.get("fix")
|
||||
for c in tile.get("checks", []))
|
||||
hold = ("restart " + tid) if fixable else None
|
||||
note = "Press: acknowledge." + (" Hold 2 s: fix what this button is failing on (nothing if it is green)."
|
||||
if hold else "")
|
||||
hold = ("restart " + tid) if fixable or tile.get("hold_always") else None
|
||||
if tile.get("hold_always"):
|
||||
note = "Press: acknowledge. Hold 2 s: %s." % cfg["fixes"][tile["hold_always"]]["label"]
|
||||
else:
|
||||
note = "Press: acknowledge." + (" Hold 2 s: fix what this button is failing on (nothing if it is green)."
|
||||
if hold else "")
|
||||
action_sets = {"down": [], "up": [command_action(tid, "press", names["command"], press)]}
|
||||
run_while_held = []
|
||||
if hold:
|
||||
action_sets[str(HOLD_MS)] = [command_action(tid, "hold", names["command"], hold)]
|
||||
# Stamped with Companion's clock, so a hold made while the watchdog was down is dropped later.
|
||||
action_sets[str(HOLD_MS)] = [command_action(tid, "hold", names["command"], hold + " $(internal:time_unix)")]
|
||||
run_while_held = [HOLD_MS]
|
||||
return {
|
||||
"type": "button-layered",
|
||||
@@ -177,41 +233,73 @@ def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1):
|
||||
return key
|
||||
|
||||
|
||||
def build(cfg):
|
||||
def nav_controls(cfg):
|
||||
controls = {}
|
||||
coloured = cfg.get("nav_keys", list(NAV_LOCATIONS)) # which nav keys follow the ALL button
|
||||
for kind, (r, c) in NAV_LOCATIONS.items():
|
||||
controls.setdefault(r, {})[c] = (nav_key(kind, cfg["variables"], cfg["page"], cfg.get("nav_hold_ms", 1000),
|
||||
cfg.get("nav_tap_page", 1))
|
||||
if kind in coloured else {"type": kind})
|
||||
for tile in cfg["tiles"]:
|
||||
if "pos" not in tile:
|
||||
continue
|
||||
r, c = (str(n) for n in tile["pos"])
|
||||
if controls.get(r, {}).get(c):
|
||||
raise ValueError("two buttons at row %s column %s" % (r, c))
|
||||
controls.setdefault(r, {})[c] = button(tile["id"], tile, cfg)
|
||||
return controls
|
||||
|
||||
|
||||
def place(controls, pos, control):
|
||||
r, c = (str(n) for n in pos)
|
||||
if controls.get(r, {}).get(c):
|
||||
raise ValueError("two buttons at row %s column %s" % (r, c))
|
||||
controls.setdefault(r, {})[c] = control
|
||||
|
||||
|
||||
def page_export(name, number, controls):
|
||||
return {
|
||||
"version": EXPORT_VERSION,
|
||||
"type": "page",
|
||||
"companionBuild": COMPANION_BUILD,
|
||||
"page": {"id": stable_id("page", cfg["page_name"]), "name": cfg["page_name"],
|
||||
"controls": controls, "gridSize": GRID},
|
||||
"page": {"id": stable_id("page", name), "name": name, "controls": controls, "gridSize": GRID},
|
||||
"instances": {},
|
||||
"connectionCollections": [],
|
||||
"oldPageNumber": cfg["page"],
|
||||
"oldPageNumber": number,
|
||||
"imageLibrary": [],
|
||||
"imageLibraryCollections": [],
|
||||
}
|
||||
|
||||
|
||||
def build(cfg):
|
||||
controls = nav_controls(cfg)
|
||||
for tile in cfg["tiles"]:
|
||||
if "pos" in tile:
|
||||
place(controls, tile["pos"], button(tile["id"], tile, cfg))
|
||||
rp = cfg.get("restart_page")
|
||||
if rp:
|
||||
place(controls, rp["jump_pos"], static_button("restart_jump", "RESTART\\nPAGE", 0x37474F, 0xFFFFFF,
|
||||
rp["page"], "Tap: open the Restart page (page %d)." % rp["page"]))
|
||||
return page_export(cfg["page_name"], cfg["page"], controls)
|
||||
|
||||
|
||||
def build_restart(cfg):
|
||||
rp = cfg["restart_page"]
|
||||
controls = nav_controls(cfg)
|
||||
place(controls, rp["back_pos"], static_button("restart_back", "STATUS\\nPAGE", 0x37474F, 0xFFFFFF, cfg["page"],
|
||||
"Tap: back to the status page (page %d)." % cfg["page"]))
|
||||
for key in rp["keys"]:
|
||||
place(controls, key["pos"], restart_key(key, cfg))
|
||||
return page_export(rp["page_name"], rp["page"], controls)
|
||||
|
||||
|
||||
def write(path, export):
|
||||
with gzip.open(path, "wb") as f: # Companion's own exports are gzipped JSON
|
||||
f.write(json.dumps(export, indent=1).encode())
|
||||
|
||||
|
||||
def main():
|
||||
cfg = load_config(os.path.join(HERE, "config.json"))
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "status-page.companionconfig")
|
||||
data = json.dumps(build(cfg), indent=1).encode()
|
||||
with gzip.open(out, "wb") as f: # Companion's own exports are gzipped JSON
|
||||
f.write(data)
|
||||
write(out, build(cfg))
|
||||
print("wrote %s (%d buttons)" % (out, sum(1 for t in cfg["tiles"] if "pos" in t)))
|
||||
if cfg.get("restart_page") and len(sys.argv) <= 1:
|
||||
out = os.path.join(HERE, "restart-page.companionconfig")
|
||||
write(out, build_restart(cfg))
|
||||
print("wrote %s (%d restart keys)" % (out, len(cfg["restart_page"]["keys"])))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user