RESI LIVE NOT REC: every deck with a disk must record while Resi plays; a hold starts the idle decks (never touches a recording one)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 13:03:44 -04:00
co-authored by Claude Opus 5.5
parent 12a9e506e1
commit 6c78ed3221
5 changed files with 205 additions and 9 deletions
+99
View File
@@ -285,6 +285,47 @@ def hyperdeck_recording(snap, label):
return (snap.var(label, "status") or "").strip().lower() == "record"
def rec_name(chk):
"""The clip name a deck gets when a hold starts it (the Director REC key's names: CAM3, PGM2...)."""
return chk.get("rec_name") or chk["conn"]
def hyperdeck_record(ip, name, port=9993, timeout=4):
"""Start recording on a HyperDeck over its own protocol (beside Companion's connection): sends
"record: name: <name>" only if the deck is not recording already. True when it then reports record."""
import socket
def ask(sock, cmd):
sock.sendall(cmd.encode() + b"\r\n")
data = b""
while True: # the answer, until the deck goes quiet
try:
chunk = sock.recv(4096)
except socket.timeout:
return data.decode("utf-8", "replace")
if not chunk:
return data.decode("utf-8", "replace")
data += chunk
def status(sock):
m = re.search(r"status: (\w+)", ask(sock, "transport info"))
return m.group(1) if m else ""
try:
with socket.create_connection((ip, port), timeout=timeout) as sock:
sock.settimeout(1.0)
try:
sock.recv(4096) # the greeting (500 connection info)
except socket.timeout:
pass
if status(sock) == "record":
return True
ask(sock, "record: name: %s" % name)
time.sleep(1.0)
return status(sock) == "record"
except OSError:
return False
def check_hyperdeck(chk, snap, ctx):
label = chk["conn"]
base = conn_result(snap, label, ctx["now"])
@@ -313,6 +354,9 @@ def check_hyperdeck(chk, snap, ctx):
if chk.get("rec_sync", True):
if ctx["any_recording"] and not recording:
out.append(("rec", Result(ERR, "NOT REC")))
elif ctx.get("resi_live") and not recording:
# Resi is playing and nothing records (2026-10-27: "i dont want anyone to forget these recordings")
out.append(("rec", Result(ERR, "RESI LIVE NOT REC")))
else:
out.append(("rec", Result(OK)))
out.insert(0, ("conn", Result(OK, info=info)))
@@ -993,6 +1037,9 @@ def needed_variables(cfg):
elif t == "psm":
for ch in range(1, chk.get("channels", 1) + 1):
slow += [(label, "ch_%d_name" % ch), (label, "ch_%d_rf_mute" % ch)]
rw = cfg.get("rec_watch") or {}
if rw.get("conn"):
fast.append((rw["conn"], rw.get("var", "playback_state")))
dedupe = lambda xs: list(dict.fromkeys(xs))
return dedupe(fast), dedupe(slow)
@@ -1413,6 +1460,9 @@ class Engine:
self.auto_muted = False # ALARMS muted last round (nothing fixes itself then)
self.auto_last = 0.0
self.deck_ips = {} # HyperDeck connection -> last address Companion reported
self.resi_since = None # rec_watch: when Resi's playback was first seen PLAYING (None: not playing)
self.resi_live = False
self.record_executor = hyperdeck_record # tests swap it
self.now = 0.0
@property
@@ -1507,6 +1557,7 @@ class Engine:
if conn_result(snap, chk["conn"], now).level == OK and hyperdeck_recording(snap, chk["conn"]):
ctx["any_recording"] = True
self.any_recording = ctx["any_recording"]
ctx["resi_live"] = self.resi_live = self.resi_playing(snap, now)
raw = {}
for t in all_tiles(self.cfg):
for i, chk in enumerate(t.get("checks", [])):
@@ -1905,6 +1956,50 @@ class Engine:
log.info("BYPASS %s: auto-fix hours %s", t["label"], "began" if want else "ended")
self.toggle_bypass(tid, comp, now)
def resi_playing(self, snap, now):
"""config "rec_watch": Resi's playback (resi-studio-cues "playback_state") has read PLAYING for
after_seconds, with the module itself connected. The decks must record then."""
rw = self.cfg.get("rec_watch") or {}
if not rw.get("conn"):
return False
value = (snap.var(rw["conn"], rw.get("var", "playback_state")) or "").strip().upper()
if value != rw.get("playing", "PLAYING").upper() or conn_result(snap, rw["conn"], now).level != OK:
self.resi_since = None
return False
self.resi_since = self.resi_since or now
return now - self.resi_since >= rw.get("after_seconds", 60)
def idle_decks(self, tile_id, snap, now):
"""HyperDeck checks on this key (or its members) that could record but do not: connected, a disk in."""
tile = self.tiles.get(tile_id, {})
out = []
for tid in tile.get("members") or [tile_id]:
for chk in self.tiles.get(tid, {}).get("checks", []):
if chk["type"] != "hyperdeck" or not chk.get("rec_sync", True):
continue
if conn_result(snap, chk["conn"], now).level != OK or hyperdeck_recording(snap, chk["conn"]):
continue
if (snap.var(chk["conn"], "slotId") or "").strip() in ("", "-"):
continue
ip = chk.get("ip") or snap.var(chk["conn"], "ip") or self.deck_ips.get(chk["conn"])
if ip:
out.append(dict(chk, ip=ip))
return out
def start_recording(self, tile_id, decks, now):
"""Hold while Resi is live: start recording on the idle decks only (a recording deck is never sent
anything), named like the Director page's REC key: <rec_name>-YYYYMMDD_HHMMSS-."""
log.info("HOLD %s: Resi is live - start recording on %s", tile_id, ", ".join(d["conn"] for d in decks))
self.note(tile_id, "STARTING\nREC", "busy", now, 6)
stamp = time.strftime("%Y%m%d_%H%M%S", time.localtime(now))
def job():
for chk in decks:
ok = self.record_executor(chk["ip"], "%s-%s-" % (rec_name(chk), stamp))
log.info("REC %s (%s) -> %s", chk["conn"], chk["ip"], "recording" if ok else "FAILED")
threading.Thread(target=job, daemon=True).start()
return []
def run_fixes(self, tile_id, snap, comp, now, cooldown=None):
cooldown = self.cooldown if cooldown is None else cooldown
if self.stopping:
@@ -1932,6 +2027,10 @@ class Engine:
self.conn_drops.clear()
self.note(tile_id, "CLEARED", "ok", now, 3)
return []
if self.resi_live:
decks = self.idle_decks(tile_id, snap, now)
if decks:
return self.start_recording(tile_id, decks, now)
plan = self.fix_plan(tile_id, snap)
always = self.tiles.get(tile_id, {}).get("hold_always")
if not plan and always: