HyperDeck hold switches its Companion connection off and on
After a deck restarts, Companion's HyperDeck connection does not come back by itself and a plain restart of the connection does not help; switching it off and on does. Each HD key's hold now does exactly that (even when it looks green), waits 15 s and checks: DONE, STILL OFFLINE (deck pings) or NO NETWORK. Decks are no longer rebooted by the panel; a recording deck is left alone. Tested live on HD2 (off/on, reconnected in 17 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+38
-29
@@ -127,6 +127,10 @@ class Companion:
|
||||
status, _ = self._request("POST", "/api/connections/%s/restart" % urllib.parse.quote(conn_id, safe=""))
|
||||
return status == 200
|
||||
|
||||
def disable_connection(self, conn_id):
|
||||
status, _ = self._request("POST", "/api/connections/%s/disable" % 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
|
||||
@@ -906,8 +910,15 @@ class Engine:
|
||||
plan = self.fix_plan(tile_id, snap)
|
||||
always = self.tiles.get(tile_id, {}).get("hold_always")
|
||||
if not plan and always:
|
||||
# e.g. TASCAM: its hold reconnects even when it looks fine (its control port hangs).
|
||||
return self.run_force(tile_id, snap, comp, now, cooldown, fix_id=always)
|
||||
# e.g. TASCAM, HD1-6: the hold reconnects even when the button looks fine.
|
||||
chk = next((c for c in self.tiles[tile_id].get("checks", []) if c.get("conn")), {})
|
||||
if self.cfg["fixes"][always].get("kind") == "hyperdeck":
|
||||
if hyperdeck_recording(snap, chk["conn"]):
|
||||
log.info("HOLD %s: %s is recording, left alone", tile_id, chk["conn"])
|
||||
self.note(tile_id, "RECORDING\nLEFT ALONE", "idle", now, 4)
|
||||
return []
|
||||
chk = dict(chk, ip=chk.get("ip") or snap.var(chk["conn"], "ip") or self.deck_ips.get(chk["conn"]))
|
||||
return self.run_force(tile_id, snap, comp, now, cooldown, fix_id=always, chk=chk)
|
||||
if not plan:
|
||||
if self.failing(tile_id):
|
||||
log.info("HOLD %s: failing, but there is no automatic fix for it", tile_id)
|
||||
@@ -976,7 +987,7 @@ class Engine:
|
||||
threading.Thread(target=work, args=(fn, args, short), daemon=True).start()
|
||||
return plan
|
||||
|
||||
def run_force(self, key_id, snap, comp, now, cooldown=60, fix_id=None):
|
||||
def run_force(self, key_id, snap, comp, now, cooldown=60, fix_id=None, chk=None):
|
||||
"""Run one fix whatever the status says: a Restart page key, or a button's "hold_always".
|
||||
Shares the 60 s guard with holds, so a hold and a restart key never double up."""
|
||||
if fix_id is None:
|
||||
@@ -985,7 +996,8 @@ class Engine:
|
||||
log.warning("FORCE %s: no such restart key", key_id)
|
||||
return None
|
||||
fix_id = key["fix"]
|
||||
ident = ("fix", fix_id, "")
|
||||
per_deck = self.cfg["fixes"][fix_id].get("kind") == "hyperdeck" and chk and chk.get("conn")
|
||||
ident = ("fix", fix_id, chk["conn"] if per_deck else "") # the same guard a failing hold uses
|
||||
if now - self.fix_last.get(ident, 0) < cooldown:
|
||||
log.info("FORCE %s: %s ran less than %ds ago, skipped", key_id, fix_id, cooldown)
|
||||
cur = self.notes.get(key_id)
|
||||
@@ -998,7 +1010,7 @@ class Engine:
|
||||
self.note(key_id, "RESTARTING", "fixing", now, 180)
|
||||
|
||||
def work():
|
||||
r = self.executor(self.cfg, fix_id, {})
|
||||
r = self.executor(self.cfg, fix_id, chk or {})
|
||||
done = time.time()
|
||||
if r == WAKING:
|
||||
self.note(key_id, "WAKING PC", "fixing", done, 120)
|
||||
@@ -1578,40 +1590,37 @@ def execute_fix(cfg, fix_id, chk):
|
||||
log.info("FIX %s -> switch %s port %s power-cycled", label, fix["switch"], fix["port"])
|
||||
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)
|
||||
# Turn Companion's connection to the deck off and on: after a deck restarts, that is what
|
||||
# brings it back green (a plain reconnect does not, 2026-10-11). The deck is never rebooted.
|
||||
return reconnect_and_verify(cfg, fix, label, conn=chk["conn"], host=chk.get("ip"), toggle=True)
|
||||
log.warning("FIX %s: unknown kind %r", label, kind)
|
||||
except Exception as e:
|
||||
log.warning("FIX %s failed: %s", label, e)
|
||||
return False
|
||||
|
||||
|
||||
def reconnect_and_verify(cfg, fix, label, sleep=time.sleep):
|
||||
"""Restart one Companion connection, then look again after "verify_seconds". Returns True if it
|
||||
came back; otherwise what to tell the operator: "hung_note" when the device still answers ping
|
||||
(its control side has hung - only a power-cycle helps), else that it is not on the network."""
|
||||
def reconnect_and_verify(cfg, fix, label, sleep=None, conn=None, host=None, toggle=False):
|
||||
"""Restart one Companion connection (toggle=True: switch it off and on again), then look again
|
||||
after "verify_seconds". Returns True if it came back; otherwise what to tell the operator:
|
||||
"hung_note" when the device still answers ping, else that it is not on the network."""
|
||||
sleep = sleep or time.sleep
|
||||
label_conn = conn or fix["conn"]
|
||||
fix = dict(fix, conn=label_conn)
|
||||
comp = Companion(cfg["companion"].get("host", "127.0.0.1"), cfg["companion"].get("port", 8000))
|
||||
conn = {c["label"]: c for c in comp.connections()}.get(fix["conn"])
|
||||
if conn is None:
|
||||
log.info("FIX %s -> no Companion connection %s", label, fix["conn"])
|
||||
return False
|
||||
if not conn.get("enabled"):
|
||||
if toggle:
|
||||
if conn.get("enabled") and not comp.disable_connection(conn["id"]):
|
||||
log.info("FIX %s -> Companion refused to switch %s off", label, fix["conn"])
|
||||
return False
|
||||
sleep(fix.get("off_seconds", 2))
|
||||
if not comp.enable_connection(conn["id"]):
|
||||
log.info("FIX %s -> Companion did not switch %s back on", label, fix["conn"])
|
||||
return "STILL OFF IN\nCOMPANION"
|
||||
log.info("FIX %s -> %s switched off and on", label, fix["conn"])
|
||||
elif not conn.get("enabled"):
|
||||
# Switched off in Companion: turn it on (never call that a hung device).
|
||||
ok = comp.enable_connection(conn["id"])
|
||||
log.info("FIX %s -> %s was disabled in Companion: turned on %s", label, fix["conn"], "ok" if ok else "FAILED")
|
||||
@@ -1629,7 +1638,7 @@ def reconnect_and_verify(cfg, fix, label, sleep=time.sleep):
|
||||
if status not in ("warning", "error"): # still starting up, or unknown: no verdict yet
|
||||
log.info("FIX %s -> %s is %s after %ss", label, fix["conn"], status, fix.get("verify_seconds", 20))
|
||||
return "STILL\nCONNECTING"
|
||||
host = companion_conn_hosts(companion_db_path(cfg)).get(fix["conn"])
|
||||
host = host or companion_conn_hosts(companion_db_path(cfg)).get(fix["conn"])
|
||||
if host and ping_once(host):
|
||||
log.info("FIX %s -> %s still %s but %s answers ping: control port hung", label, fix["conn"], status, host)
|
||||
return fix.get("hung_note", "STILL DOWN")
|
||||
|
||||
Reference in New Issue
Block a user