"Location", never "campus", in everything people read; SC FOH joined with its MIDI links
- pages (NA 98 Locations / 99 Location, re-imported via PAGES_VERSION), keys, settings page, setup and install messages, alerts, docs, the Apollo Beach kit; python3 -m settings.location and /location/... addresses (the old names still answer for installed Macs) - Apply reads the before settings from the file: an in-place edit skipped the watchdog restart - SC: SUPERRACK (+SoundGrid) and X32 EDIT from SC FOH; FOH TRACKS and TRACKS LIGHTS MIDI links watched Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+47
-42
@@ -1,23 +1,23 @@
|
||||
"""Every campus's status, on North Attleboro's Companion only.
|
||||
"""Every location's status, on North Attleboro's Companion only.
|
||||
|
||||
Each other campus's settings page tells North Attleboro's (the hub: a campus with no "home") where it
|
||||
is, and learns nothing back: no other campus ever sees another campus (2026-10-20, the user: "NA
|
||||
Each other location's settings page tells North Attleboro's (the hub: a location with no "home") where it
|
||||
is, and learns nothing back: no other location ever sees another location (2026-10-20, the user: "NA
|
||||
should be the only one that sees every single location's status"). Only the hub, every two seconds,
|
||||
copies each campus's key states (what that campus's watchdog draws, /api/state) into its own
|
||||
copies each location's key states (what that location's watchdog draws, /api/state) into its own
|
||||
Companion as custom variables sp_state_<id> / sp_hb_<id>, and keeps two kinds of page there:
|
||||
|
||||
CAMPUSES page one key per campus: that campus's ALL key, with its name; tap = its status page
|
||||
CAMPUS page whichever campus was tapped, as its own status page looks (same places, same
|
||||
colours, live), view only: holds and presses are for that campus's own deck.
|
||||
The ALARMS corner goes back to the CAMPUSES page.
|
||||
LOCATIONS page one key per location: that location's ALL key, with its name; tap = its status page
|
||||
LOCATION page whichever location was tapped, as its own status page looks (same places, same
|
||||
colours, live), view only: holds and presses are for that location's own deck.
|
||||
The ALARMS corner goes back to the LOCATIONS page.
|
||||
|
||||
Two pages, whatever the number of campuses (NA's Companion has 97 pages of its own; Companion stops
|
||||
at 99): the CAMPUS page's keys are cells ("c_1_3"), and the hub fills them from the tapped campus's
|
||||
layout (sp_view says which). A campus that stops answering goes purple. Pages are only written
|
||||
Two pages, whatever the number of locations (NA's Companion has 97 pages of its own; Companion stops
|
||||
at 99): the LOCATION page's keys are cells ("c_1_3"), and the hub fills them from the tapped location's
|
||||
layout (sp_view says which). A location that stops answering goes purple. Pages are only written
|
||||
where nothing a person made sits (push_page.safe_target): an empty page, else one added at the
|
||||
end. State: campuses.json in the data folder.
|
||||
|
||||
A standalone campus (Apollo Beach, 2026-10-21: "they want to take care of it on their own ... i dont
|
||||
A standalone location (Apollo Beach, 2026-10-21: "they want to take care of it on their own ... i dont
|
||||
need to monitor AB from NA") has "standalone" set instead of a home: it tells nobody, is no hub, and
|
||||
lists only itself.
|
||||
"""
|
||||
@@ -70,17 +70,22 @@ def me(cfg, url=None):
|
||||
import pwd
|
||||
site = cfg.get("site") or {}
|
||||
# The account this panel runs as: the hub logs in with it for the nightly backup.
|
||||
return {"id": site.get("id") or "new", "name": site.get("name") or "This campus", "url": url or my_url(cfg),
|
||||
return {"id": site.get("id") or "new", "name": site.get("name") or "This location", "url": url or my_url(cfg),
|
||||
"user": pwd.getpwuid(os.getuid()).pw_name}
|
||||
|
||||
|
||||
# Bumped when the pages are built differently (2: "campus" became "location", 2026-10-21), so the hub
|
||||
# re-imports them.
|
||||
PAGES_VERSION = 2
|
||||
|
||||
|
||||
def layout_sha(cfg):
|
||||
keys = [(t.get("id"), t.get("label"), t.get("pos"), t.get("kind"), t.get("members")) for t in cfg.get("tiles", [])]
|
||||
return hashlib.sha256(json.dumps([keys, cfg.get("page_grid"), cfg.get("variables")], sort_keys=True, default=str).encode()).hexdigest()[:16]
|
||||
return hashlib.sha256(json.dumps([keys, cfg.get("page_grid"), cfg.get("variables"), PAGES_VERSION], sort_keys=True, default=str).encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def state_now(site):
|
||||
"""What /api/state answers: this campus's key states as its watchdog draws them, right now."""
|
||||
"""What /api/state answers: this location's key states as its watchdog draws them, right now."""
|
||||
cfg = site.load()
|
||||
names = cfg.get("variables") or {}
|
||||
comp = site.comp
|
||||
@@ -100,8 +105,8 @@ def http_json(url, body=None, timeout=3):
|
||||
|
||||
VIEW = {"state": "sp_state_view", "heartbeat": "sp_hb_view"}
|
||||
SELECT = "sp_view"
|
||||
VIEW_CMD = "sp_view_cmd" # a tap or hold on the CAMPUS page, passed on to that campus's own watchdog
|
||||
MODE = "sp_view_mode" # "" = that campus's status page, "RESTART" = its Restart page
|
||||
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
|
||||
CELLS = [(r, c) for r in range(4) for c in range(1, 8)]
|
||||
|
||||
|
||||
@@ -110,7 +115,7 @@ def cell_id(r, c):
|
||||
|
||||
|
||||
def view_state(state, keys):
|
||||
"""A campus's key states, moved onto the cells its keys sit in (empty cells blank)."""
|
||||
"""A location's key states, moved onto the cells its keys sit in (empty cells blank)."""
|
||||
out = {cell_id(r, c): {"x": "", "b": 0, "b2": 0, "f": 0xFFFFFF, "f2": 0xFFFFFF, "s": 14} for r, c in CELLS}
|
||||
for k in keys:
|
||||
r, c = k["pos"]
|
||||
@@ -120,8 +125,8 @@ def view_state(state, keys):
|
||||
|
||||
|
||||
def build_view(local_cfg, page, overview_page):
|
||||
"""The CAMPUS page: a key per cell, drawn from sp_state_view; tap = acknowledge, hold = fix, at
|
||||
that campus (2026-10-20: the user wants to test and fix Woonsocket from NA). The ALARMS corner goes back."""
|
||||
"""The LOCATION page: a key per cell, drawn from sp_state_view; tap = acknowledge, hold = fix, at
|
||||
that location (2026-10-20: the user wants to test and fix Woonsocket from NA). The ALARMS corner goes back."""
|
||||
opts = make_page.settings(local_cfg)
|
||||
controls = make_page.nav_controls(local_cfg)
|
||||
hold_ms = opts["hold_ms"]
|
||||
@@ -132,17 +137,17 @@ def build_view(local_cfg, page, overview_page):
|
||||
make_page.place(controls, (r, c), {
|
||||
"type": "button-layered", "style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile 'campus view %d/%d' - the tapped campus's key here. Press: acknowledge it there. "
|
||||
"Hold %g s: fix it there, like a hold on that campus's own deck." % (r, c, hold_ms / 1000.0)},
|
||||
"notes": "STATUS tile 'location view %d/%d' - the tapped location's key here. Press: acknowledge it there. "
|
||||
"Hold %g s: fix it there, like a hold on that location's own deck." % (r, c, hold_ms / 1000.0)},
|
||||
"feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": [tap], str(hold_ms): [hold]},
|
||||
"options": {"runWhileHeld": [hold_ms]}}}, "localVariables": []})
|
||||
back = make_page.static_button("campus_back", "ALL\\nSITES", 0x37474F, 0xFFFFFF, overview_page,
|
||||
"Tap: back to the CAMPUSES page (page %d)." % overview_page, "STATUS jump key")
|
||||
"Tap: back to the LOCATIONS page (page %d)." % overview_page, "STATUS jump key")
|
||||
for layer in back["style"]["layers"]:
|
||||
if layer["type"] == "text": # which campus this page shows, then the way back
|
||||
if layer["type"] == "text": # which location this page shows, then the way back
|
||||
layer["text"] = make_page.prop("`${$(custom:%s)} ${$(custom:%s)}\\nALL SITES`" % (SELECT, MODE), True)
|
||||
make_page.place(controls, (3, 0), back)
|
||||
return make_page.page_export("%d\\nCampus" % page, page, controls, local_cfg)
|
||||
return make_page.page_export("%d\\nLocation" % page, page, controls, local_cfg)
|
||||
|
||||
|
||||
def select_action(cid, mode=""):
|
||||
@@ -153,7 +158,7 @@ def select_action(cid, mode=""):
|
||||
|
||||
|
||||
def campus_key(cid, name, names, actions, note, local_cfg):
|
||||
"""One CAMPUSES key: that campus's ALL key with its name on top."""
|
||||
"""One LOCATIONS key: that location's ALL key with its name on top."""
|
||||
opts = make_page.settings(local_cfg)
|
||||
tag = short(cid, name)
|
||||
stale_text = "`%s\\n?`" % tag
|
||||
@@ -164,13 +169,13 @@ def campus_key(cid, name, names, actions, note, local_cfg):
|
||||
text["fontsize"] = make_page.prop("auto")
|
||||
return {"type": "button-layered", "style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile 'campus %s' - %s's ALL key. %s" % (cid, name, note)},
|
||||
"notes": "STATUS tile 'location %s' - %s's ALL key. %s" % (cid, name, note)},
|
||||
"feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": actions}, "options": {"runWhileHeld": []}}},
|
||||
"localVariables": []}
|
||||
|
||||
|
||||
def build_overview(local_cfg, campuses, page, view_page):
|
||||
"""Row 0: each campus's ALL key (tap: its status page). Row 1: its RESTART key (tap: its Restart page)."""
|
||||
"""Row 0: each location's ALL key (tap: its status page). Row 1: its RESTART key (tap: its Restart page)."""
|
||||
controls = make_page.nav_controls(local_cfg)
|
||||
mine = (local_cfg.get("site") or {}).get("id")
|
||||
rp = (local_cfg.get("restart_page") or {}).get("page")
|
||||
@@ -181,7 +186,7 @@ def build_overview(local_cfg, campuses, page, view_page):
|
||||
key = campus_key(cid, name, local_cfg["variables"], [make_page.set_page_action("campus_" + cid, "go", local_cfg["page"])],
|
||||
"Tap: its status page (page %d)." % local_cfg["page"], local_cfg)
|
||||
restart = make_page.static_button("campus_rs_" + cid, "%s\\nRESTART" % tag, 0x37474F, 0xFFFFFF, rp,
|
||||
"Tap: this campus's Restart page (page %s)." % rp, "STATUS jump key") if rp else None
|
||||
"Tap: this location's Restart page (page %s)." % rp, "STATUS jump key") if rp else None
|
||||
else:
|
||||
key = campus_key(cid, name, {"state": "sp_state_" + cid, "heartbeat": "sp_hb_" + cid},
|
||||
select_action(cid) + [make_page.set_page_action("campus_" + cid, "go", view_page)],
|
||||
@@ -193,7 +198,7 @@ def build_overview(local_cfg, campuses, page, view_page):
|
||||
make_page.place(controls, (0, col), key)
|
||||
if restart:
|
||||
make_page.place(controls, (1, col), restart)
|
||||
return make_page.page_export("%d\\nCampuses" % page, page, controls, local_cfg)
|
||||
return make_page.page_export("%d\\nLocations" % page, page, controls, local_cfg)
|
||||
|
||||
|
||||
def ensure_variables(cfg, ids):
|
||||
@@ -232,10 +237,10 @@ def free_page(cfg, taken, start=21):
|
||||
# None empty: a new page at the end (Companion's own page keys on it), never in between.
|
||||
last = max(p for p in range(1, 100) if exists(host, port, p))
|
||||
if last >= 99:
|
||||
raise RuntimeError("Companion has 99 pages and none is empty: free one for the campuses")
|
||||
raise RuntimeError("Companion has 99 pages and none is empty: free one for the locations")
|
||||
ws = push_page.WebSocket(host, port, "/trpc")
|
||||
try:
|
||||
push_page.Trpc(ws).mutation("pages.insert", {"asPageNumber": last + 1, "pageNames": ["CAMPUSES"]})
|
||||
push_page.Trpc(ws).mutation("pages.insert", {"asPageNumber": last + 1, "pageNames": ["LOCATIONS"]})
|
||||
finally:
|
||||
ws.close()
|
||||
return last + 1
|
||||
@@ -278,7 +283,7 @@ class Mirror:
|
||||
return not data.get("home") and not data.get("standalone")
|
||||
|
||||
def sync_registry(self):
|
||||
"""A campus: tell the hub where this settings page is (it learns nothing back). The hub: know itself."""
|
||||
"""A location: tell the hub where this settings page is (it learns nothing back). The hub: know itself."""
|
||||
cfg = self.site.load()
|
||||
data = load(self.site.data_dir)
|
||||
mine = me(cfg)
|
||||
@@ -300,7 +305,7 @@ class Mirror:
|
||||
return {p for p in t if p}
|
||||
|
||||
def refresh_pages(self):
|
||||
"""The hub: each campus's layout (when it changed), the CAMPUS page once, the CAMPUSES page when the roster changes."""
|
||||
"""The hub: each location's layout (when it changed), the LOCATION page once, the LOCATIONS page when the roster changes."""
|
||||
cfg = self.site.load()
|
||||
data = self.sync_registry()
|
||||
if not self.is_hub(data):
|
||||
@@ -308,7 +313,7 @@ class Mirror:
|
||||
mine = (cfg.get("site") or {}).get("id")
|
||||
others = {cid: e for cid, e in data.get("list", {}).items() if cid != mine}
|
||||
if not others and not (data.get("pages") or {}).get("overview"):
|
||||
return data # no other campus yet: no pages needed
|
||||
return data # no other location yet: no pages needed
|
||||
pages = data.setdefault("pages", {})
|
||||
layouts = data.setdefault("layouts", {})
|
||||
for cid, e in sorted(others.items()):
|
||||
@@ -337,7 +342,7 @@ class Mirror:
|
||||
return data
|
||||
|
||||
def pass_on(self, pick, others, restart=False):
|
||||
"""A tap or hold on the CAMPUS page goes to the shown campus's watchdog, through its Companion."""
|
||||
"""A tap or hold on the LOCATION page goes to the shown location's watchdog, through its Companion."""
|
||||
text = (self.site.comp.get_custom(VIEW_CMD) or "").strip()
|
||||
if not text:
|
||||
return
|
||||
@@ -351,12 +356,12 @@ class Mirror:
|
||||
remote = sp.Companion(host, 8000, timeout=3)
|
||||
before = (remote.get_custom("sp_cmd") or "").strip()
|
||||
remote.set_custom("sp_cmd", ";".join([before] + cmds if before else cmds))
|
||||
log.info("CAMPUS %s: %s (from %s's CAMPUS page)", pick, "; ".join(cmds), (self.site.cfg.get("site") or {}).get("name", "here"))
|
||||
log.info("LOCATION %s: %s (from %s's LOCATION page)", pick, "; ".join(cmds), (self.site.cfg.get("site") or {}).get("name", "here"))
|
||||
except OSError as e:
|
||||
log.warning("CAMPUS %s: could not pass on %s: %s", pick, cmds, e)
|
||||
log.warning("LOCATION %s: could not pass on %s: %s", pick, cmds, e)
|
||||
|
||||
def poll(self):
|
||||
"""The hub: copy each other campus's key states into this Companion."""
|
||||
"""The hub: copy each other location's key states into this Companion."""
|
||||
cfg = self.site.load()
|
||||
data = load(self.site.data_dir)
|
||||
if not self.is_hub(data):
|
||||
@@ -380,10 +385,10 @@ class Mirror:
|
||||
threads.append(t)
|
||||
for t in threads:
|
||||
t.join(2)
|
||||
# The CAMPUS page shows the tapped campus: its keys moved onto their cells.
|
||||
# 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:
|
||||
# Nothing tapped yet (or a campus that left): show the first one rather than a purple page.
|
||||
# Nothing tapped yet (or a location that left): show the first one rather than a purple page.
|
||||
pick = sorted(others)[0]
|
||||
self.site.comp.set_custom(SELECT, pick.upper())
|
||||
restart = (self.site.comp.get_custom(MODE) or "").strip().upper() == "RESTART"
|
||||
@@ -400,7 +405,7 @@ class Mirror:
|
||||
|
||||
|
||||
def campus_command(keys, text, now, restart=False):
|
||||
"""The CAMPUS page's taps and holds ("tap 1 3 <time>;hold 0 2 <time>") as that campus's own
|
||||
"""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
|
||||
tap does nothing, as on its own deck), for the keys at those cells. Stale ones are dropped."""
|
||||
at = {tuple(k["pos"]): k["id"] for k in keys or []}
|
||||
|
||||
Reference in New Issue
Block a user