Never reboot a HyperDeck to fix a connection that is switched off; turn connections back on instead.
On 2026-10-08 a hold on HD1, whose Companion connection had been disabled, sent the deck a reboot. Now a disabled connection on any button is fixed by enabling it, before any other fix is considered. The HyperDeck fix asks the deck itself (transport info on port 9993): if it answers - recording or not - only Companion's connection is restarted; only a deck that accepts a connection and says nothing at all is rebooted; an unreachable deck is left alone and the button shows FIX FAILED. Tested against a fake deck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+75
-12
@@ -120,6 +120,10 @@ class Companion:
|
||||
status, _ = self._request("POST", "/api/connections/%s/restart" % urllib.parse.quote(conn_id, safe=""))
|
||||
return status == 200
|
||||
|
||||
def enable_connection(self, conn_id):
|
||||
status, _ = self._request("POST", "/api/connections/%s/enable" % urllib.parse.quote(conn_id, safe=""))
|
||||
return status == 200
|
||||
|
||||
|
||||
# --- Shared snapshot written by the pollers --------------------------------
|
||||
|
||||
@@ -735,6 +739,15 @@ class Engine:
|
||||
tr = self.trackers.get(key)
|
||||
if tid not in ids or not tr or not tr.confirmed or tr.result.level not in (WARN, ERR):
|
||||
continue
|
||||
# A connection switched off in Companion is fixed by switching it on - never by
|
||||
# acting on the device (2026-10-08: a disabled HyperDeck connection got a deck reboot).
|
||||
label = chk.get("conn")
|
||||
if label and label in snap.conns and not snap.conns[label].get("enabled"):
|
||||
ident = ("enable", label)
|
||||
if ident not in seen:
|
||||
seen.add(ident)
|
||||
plan.append((ident, "enable", label, chk))
|
||||
continue
|
||||
fix_id = chk.get("fix")
|
||||
if isinstance(fix_id, dict):
|
||||
fix_id = fix_id.get(sub)
|
||||
@@ -742,7 +755,7 @@ class Engine:
|
||||
if chk["type"] == "hyperdeck" and hyperdeck_recording(snap, chk["conn"]):
|
||||
continue
|
||||
# One run per fix, except a HyperDeck reboot, which is per deck.
|
||||
per_deck = self.cfg["fixes"][fix_id].get("kind") == "hyperdeck_reboot"
|
||||
per_deck = self.cfg["fixes"][fix_id].get("kind") == "hyperdeck"
|
||||
ident = ("fix", fix_id, chk.get("ip", "") if per_deck else "")
|
||||
if ident not in seen:
|
||||
seen.add(ident)
|
||||
@@ -783,7 +796,13 @@ class Engine:
|
||||
waited = True
|
||||
continue
|
||||
self.fix_last[ident] = now
|
||||
if kind == "restart":
|
||||
if kind == "enable":
|
||||
c = snap.conns.get(what)
|
||||
if c:
|
||||
ok = comp.enable_connection(c["id"])
|
||||
log.info("ENABLE %s -> %s", what, "ok" if ok else "failed")
|
||||
names.append("TURN ON")
|
||||
elif kind == "restart":
|
||||
c = snap.conns.get(what)
|
||||
if c and c.get("enabled"):
|
||||
ok = comp.restart_connection(c["id"])
|
||||
@@ -1100,6 +1119,41 @@ def poll_remote(snap, cfg):
|
||||
snap.remote = result
|
||||
|
||||
|
||||
def hyperdeck_probe(ip, timeout=3, port=9993):
|
||||
"""Ask a HyperDeck directly what it is doing, on its control port 9993.
|
||||
|
||||
Returns its transport status ("record", "preview", "stopped", ...), "busy" if it
|
||||
refused another client, "answered" if it greeted but did not report a status,
|
||||
"hung" if it accepted the connection and said nothing at all, or "unreachable".
|
||||
Only "hung" justifies a reboot.
|
||||
"""
|
||||
try:
|
||||
s = socket.create_connection((ip, port), timeout=timeout)
|
||||
except OSError:
|
||||
return "unreachable"
|
||||
buf = b""
|
||||
with s:
|
||||
s.settimeout(timeout)
|
||||
try:
|
||||
s.sendall(b"transport info\r\n")
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
m = re.search(rb"208 transport info:.*?status: (\w+)", buf, re.S)
|
||||
if m:
|
||||
return m.group(1).decode().lower()
|
||||
if b"120 connection rejected" in buf:
|
||||
return "busy"
|
||||
except socket.timeout:
|
||||
pass
|
||||
except OSError:
|
||||
return "unreachable" if not buf else "answered"
|
||||
return "answered" if buf else "hung"
|
||||
|
||||
|
||||
def execute_fix(cfg, fix_id, chk):
|
||||
"""Run one entry from config "fixes" (in its own thread). Only listed commands ever run.
|
||||
Returns True if the command ran and succeeded."""
|
||||
@@ -1113,16 +1167,25 @@ def execute_fix(cfg, fix_id, chk):
|
||||
log.info("FIX %s -> %s%s", label, "ok" if r.returncode == 0 else "exit %d" % r.returncode,
|
||||
(": " + detail) if detail else "")
|
||||
return r.returncode == 0
|
||||
if kind == "hyperdeck_reboot":
|
||||
with socket.create_connection((chk["ip"], 9993), timeout=3) as s:
|
||||
s.settimeout(3)
|
||||
try:
|
||||
s.recv(1024) # the deck's greeting
|
||||
except socket.timeout:
|
||||
pass
|
||||
s.sendall(b"reboot\n")
|
||||
log.info("FIX %s %s -> reboot sent", label, chk["conn"])
|
||||
return True
|
||||
if kind == "hyperdeck":
|
||||
port = chk.get("port", 9993)
|
||||
verdict = hyperdeck_probe(chk["ip"], port=port)
|
||||
if verdict == "hung":
|
||||
with socket.create_connection((chk["ip"], port), timeout=3) as s:
|
||||
s.sendall(b"reboot\r\n")
|
||||
log.info("FIX %s %s -> deck accepted a connection but did not answer: reboot sent",
|
||||
label, chk["conn"])
|
||||
return True
|
||||
if verdict == "unreachable":
|
||||
log.info("FIX %s %s -> deck does not answer on the network: nothing sent", label, chk["conn"])
|
||||
return False
|
||||
# The deck answers (and may be recording): leave it alone, reconnect Companion instead.
|
||||
comp = Companion(cfg["companion"].get("host", "127.0.0.1"), cfg["companion"].get("port", 8000))
|
||||
ids = {c["label"]: c["id"] for c in comp.connections()}
|
||||
ok = chk["conn"] in ids and comp.restart_connection(ids[chk["conn"]])
|
||||
log.info("FIX %s %s -> deck is fine (%s): Companion connection restarted %s",
|
||||
label, chk["conn"], verdict, "ok" if ok else "FAILED")
|
||||
return bool(ok)
|
||||
log.warning("FIX %s: unknown kind %r", label, kind)
|
||||
except Exception as e:
|
||||
log.warning("FIX %s failed: %s", label, e)
|
||||
|
||||
Reference in New Issue
Block a user