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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user