NA's page keys name the other locations with a problem in a blue band; PROP OUT reads Chronicles
- campuses.remote_summary -> custom variable sp_remote; band flashes until acknowledged there, steady after, "SC?" when silent
- nav_locations_band (NA only); red stays for NA's own problems
- PROP OUT: /Volumes/Location Assets/{date} on NA ProP (Chronicles share)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-3
@@ -935,8 +935,8 @@
|
||||
"pco_module": "pco-plan",
|
||||
"pco_service_type": "275974",
|
||||
"projects": "~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/Shared drives/Waters Creative/Ableton/Projects",
|
||||
"prop_bundle_folder": "~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/Shared drives/Location Assets/{date}",
|
||||
"prop_bundle_host": "ingest",
|
||||
"prop_bundle_folder": "/Volumes/Location Assets/{date}",
|
||||
"prop_bundle_host": "prop",
|
||||
"prop_bundle_pattern": "*.probundle",
|
||||
"prop_host": "prop",
|
||||
"prop_lists": [
|
||||
@@ -2298,5 +2298,6 @@
|
||||
"command": "sp_cmd",
|
||||
"heartbeat": "sp_hb",
|
||||
"state": "sp_state"
|
||||
}
|
||||
},
|
||||
"nav_locations_band": true
|
||||
}
|
||||
|
||||
@@ -265,6 +265,28 @@ def nav_templates(cfg):
|
||||
raise FileNotFoundError("no nav key templates for Companion %s" % version)
|
||||
|
||||
|
||||
REMOTE_VAR = "sp_remote" # North Attleboro's summary of the other locations (settings/campuses.py writes it)
|
||||
BAND_BLUE, BAND_DARK = 0x1E6FFF, 0x0B2A66
|
||||
|
||||
|
||||
def locations_band(flash_var):
|
||||
"""A blue band across the bottom of NA's page-number key naming the other locations with a problem
|
||||
("SC", "SC WS"): it flashes until that location acknowledges, then stays. Red stays NA's own
|
||||
(2026-10-21, the user: "i dont want the NA page buttons to flash red, that should he reserved for on site
|
||||
problems")."""
|
||||
r = "$(custom:%s)" % REMOTE_VAR
|
||||
on = "$(internal:time_unix) - jsonpath(%s, '$.t') < 30 && jsonpath(%s, '$.n') > 0" % (r, r)
|
||||
color = "jsonpath(%s, '$.f') == 1 && $(custom:%s) == 1 ? %d : %d" % (r, flash_var, BAND_DARK, BAND_BLUE)
|
||||
box = {"id": stable_id("nav", "band_box"), "name": "Locations", "usage": "auto", "type": "box", "enabled": prop(on, True),
|
||||
"opacity": prop(100), "x": prop(0), "y": prop(60), "width": prop(100), "height": prop(40), "rotation": prop(0),
|
||||
"color": prop(color, True), "borderWidth": prop(0), "borderColor": prop(0), "borderPosition": prop("inside")}
|
||||
text = {"id": stable_id("nav", "band_text"), "name": "Locations text", "usage": "auto", "type": "text", "enabled": prop(on, True),
|
||||
"opacity": prop(100), "x": prop(0), "y": prop(60), "width": prop(100), "height": prop(40), "rotation": prop(0),
|
||||
"text": prop("jsonpath(%s, '$.x')" % r, True), "color": prop(0xFFFFFF), "halign": prop("center"), "valign": prop("center"),
|
||||
"fontsize": prop("auto"), "fontsizeAllowShrink": prop(True), "font": prop("companion-sans"), "outlineColor": prop(4278190080)}
|
||||
return [box, text]
|
||||
|
||||
|
||||
def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1, cfg=None):
|
||||
"""Companion's own UP / page number / DOWN key, coloured like the ALL button.
|
||||
|
||||
@@ -291,6 +313,8 @@ def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1, cfg=None):
|
||||
layer["borderColor"] = prop(fg, True)
|
||||
paint(layer.get("children", []))
|
||||
paint(key["style"]["layers"])
|
||||
if kind == "pagenum" and (cfg.get("nav_locations_band") or False):
|
||||
key["style"]["layers"] += locations_band(opts["flash"])
|
||||
if kind == "pagenum" and status_page:
|
||||
step = key["steps"]["0"]
|
||||
tap = step["action_sets"]["down"]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -257,6 +257,9 @@ field("tiles[].id", "id", None, "Used inside Companion.", "Keys", advanced=True,
|
||||
field("tiles[].label", "str", None, "The first line on the key.", "Keys", required=True, apply="pages")
|
||||
field("tiles[].pos", "list", None, "[row, column]; absent = no key of its own (shown inside a group).", "Keys",
|
||||
apply="pages")
|
||||
field("nav_locations_band", "bool", False, "North Attleboro: a blue band on the page-number keys names the other locations "
|
||||
"with a problem (flashing until acknowledged there). Red stays for this location's own problems.", "Keys", advanced=True,
|
||||
apply="pages")
|
||||
field("tiles[].kind", "enum", None, "A special key: master (ALL), service (ALARMS), network (NET), ready (READY: "
|
||||
"opens the READY page and sums up its checklist), autofix (AUTO-FIX: its own switch, so ALARMS only "
|
||||
"silences), bypass (hold: hand a device to an operator - its MIDI links out, its Companion connections off).",
|
||||
|
||||
+26
-1
@@ -104,6 +104,7 @@ def http_json(url, body=None, timeout=3):
|
||||
# --- the pages (the hub only) -----------------------------------------------------------
|
||||
|
||||
VIEW = {"state": "sp_state_view", "heartbeat": "sp_hb_view"}
|
||||
REMOTE = make_page.REMOTE_VAR # which other locations have a problem: the blue band on NA's page-number keys
|
||||
SELECT = "sp_view"
|
||||
VIEW_CMD = "sp_view_cmd" # a tap or hold on the LOCATION page, passed on to that location's own watchdog
|
||||
MODE = "sp_view_mode" # "" = that location's status page, "RESTART" = its Restart page
|
||||
@@ -205,7 +206,7 @@ def ensure_variables(cfg, ids):
|
||||
from settings import campus
|
||||
have, _ = campus.companion_state(cfg)
|
||||
want = [n for cid in ids for n in ("sp_state_" + cid, "sp_hb_" + cid) if n not in have]
|
||||
want += [n for n in (VIEW["state"], VIEW["heartbeat"], SELECT, VIEW_CMD, MODE) if n not in have]
|
||||
want += [n for n in (VIEW["state"], VIEW["heartbeat"], SELECT, VIEW_CMD, MODE, REMOTE) if n not in have]
|
||||
if not want:
|
||||
return
|
||||
comp = cfg.get("companion") or {}
|
||||
@@ -385,6 +386,8 @@ class Mirror:
|
||||
threads.append(t)
|
||||
for t in threads:
|
||||
t.join(2)
|
||||
if (data.get("pages") or {}).get("overview"):
|
||||
self.site.comp.set_custom(REMOTE, json.dumps(remote_summary(others, self.remote, time.time()), separators=(",", ":")))
|
||||
# The LOCATION page shows the tapped location: its keys moved onto their cells.
|
||||
pick = (self.site.comp.get_custom(SELECT) or "").strip().lower() # held as "WS": the back key shows it
|
||||
if pick not in others and others:
|
||||
@@ -404,6 +407,28 @@ class Mirror:
|
||||
self.site.comp.set_custom(VIEW["heartbeat"], str(st.get("hb") or "0"))
|
||||
|
||||
|
||||
def remote_summary(others, remote, now, stale=30):
|
||||
"""{"t", "n", "x", "f"}: the other locations with something down (their ALL key not "ALL OK", or not
|
||||
answering: "SC?"), named by id; "f" = 1 while any of them is still flashing (not acknowledged there)."""
|
||||
names, flashing = [], False
|
||||
for cid in sorted(others):
|
||||
st = remote.get(cid)
|
||||
tag = cid.upper()
|
||||
if not st or now - float(st.get("hb") or 0) > stale:
|
||||
names.append(tag + "?")
|
||||
flashing = True
|
||||
continue
|
||||
try:
|
||||
all_ = json.loads(st.get("state") or "{}").get("all") or {}
|
||||
except ValueError:
|
||||
all_ = {}
|
||||
if not all_ or str(all_.get("x") or "").startswith("ALL OK"):
|
||||
continue
|
||||
names.append(tag)
|
||||
flashing = flashing or all_.get("b") != all_.get("b2")
|
||||
return {"t": int(now), "n": len(names), "x": " ".join(names), "f": 1 if flashing else 0}
|
||||
|
||||
|
||||
def campus_command(keys, text, now, restart=False):
|
||||
"""The LOCATION page's taps and holds ("tap 1 3 <time>;hold 0 2 <time>") as that location's own
|
||||
commands ("ack hd1", "restart hd1 <time>"; on its Restart page a hold is "force rs_x <time>" and a
|
||||
|
||||
Binary file not shown.
@@ -578,3 +578,27 @@ class TestHelperJoinsLater(unittest.TestCase):
|
||||
self.assertTrue(any((c.get("params") or {}).get("exe") == "rtpMIDI.exe" for c in midi["checks"]), added)
|
||||
self.assertTrue(any("RTPMIDI" in a for a in added), added)
|
||||
self.assertEqual(schema.validate(new)[0], [])
|
||||
|
||||
|
||||
class TestLocationsBand(unittest.TestCase):
|
||||
"""NA's page keys name the other locations with a problem in blue; red stays NA's own (2026-10-21)."""
|
||||
|
||||
def test_the_summary_names_locations_down_and_flashes_until_acknowledged(self):
|
||||
from settings import campuses as cv
|
||||
now = 1000.0
|
||||
st = lambda x, b, b2: {"hb": now - 1, "state": json.dumps({"all": {"x": x, "b": b, "b2": b2}})}
|
||||
remote = {"ws": st("ALL OK", 1, 1), "sc": st("2 DOWN\nPROP LIGHTS", 0xFF0000, 0)}
|
||||
self.assertEqual(cv.remote_summary(["sc", "ws", "ab"], remote, now), {"t": 1000, "n": 2, "x": "AB? SC", "f": 1})
|
||||
remote["sc"] = st("1 DOWN\nX", 0x8B0000, 0x8B0000) # acknowledged at SC: steady
|
||||
self.assertEqual(cv.remote_summary(["sc", "ws"], remote, now), {"t": 1000, "n": 1, "x": "SC", "f": 0})
|
||||
remote["sc"] = dict(remote["sc"], hb=now - 60) # stopped answering
|
||||
self.assertEqual(cv.remote_summary(["sc"], remote, now)["x"], "SC?")
|
||||
|
||||
def test_only_a_page_key_set_for_it_carries_the_band(self):
|
||||
na = json.load(open(os.path.join(ROOT, "config.json")))
|
||||
names = lambda cfg: [l.get("name") for l in make_page.nav_key("pagenum", cfg["variables"], 19, cfg=cfg)["style"]["layers"]]
|
||||
self.assertIn("Locations", names(dict(na, nav_locations_band=True)))
|
||||
self.assertNotIn("Locations", names(dict(na, nav_locations_band=False)))
|
||||
band = [l for l in make_page.nav_key("pagenum", na["variables"], 19, cfg=dict(na, nav_locations_band=True))["style"]["layers"]
|
||||
if l.get("name") == "Locations"][0]
|
||||
self.assertNotIn(str(0xFF0000), json.dumps(band)) # never red
|
||||
|
||||
Reference in New Issue
Block a user