feat(x4): add per-camera manual wake and hide power action when connected

This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 16:27:48 +03:00
parent db14ee5435
commit 99f29218d7
27 changed files with 992 additions and 126 deletions
+50
View File
@@ -26,6 +26,7 @@ class Recovery:
self.absent = {}
self.stable = {}
self.jobs = {}
self.wakeable = set()
self.stopping = threading.Event()
self.pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="x4-recovery")
for path in sorted(root.glob("instax4_*.json"))[:500]:
@@ -62,8 +63,50 @@ class Recovery:
"enabled": state.get("enabled", False),
"preview_wanted": state.get("preview_wanted", False),
"phase": state.get("phase", "disabled"),
"wake_available": ident in self.wakeable,
}
def sync_registry(self):
values = request(HELPER, "/registry", {}, timeout=4)["items"]
if not isinstance(values, list) or len(values) > 500:
raise ValueError("Invalid wake registry")
for ident in values:
self.broker.validate_identifier(ident)
with self.lock:
self.wakeable = set(values)
for ident in values:
if ident not in self.states:
self.states[ident] = self.default()
self.save(ident, self.states[ident])
def wake_once(self, ident, command):
# Explicit operator action. Keep the automatic policy and preview intent intact.
deadline = time.monotonic() + 52
result = request(
HELPER,
"/wake-once",
{"device_id": ident, "attempt": command["operation_id"][3:]},
timeout=52,
)
state = result.get("state")
# A cold X4 can enumerate after the 20-second beacon has already ended.
# Observe the installed supervisor only; never send a second wake beacon.
if state == "unavailable":
while time.monotonic() < deadline and not self.stopping.is_set():
if any(x["id"] == ident and x.get("online") for x in self.broker.snapshots()):
state = "connected"
break
self.stopping.wait(1)
if state == "connected":
return {"state": "complete", "result": {"usb_connected": True}}
errors = {
"busy": "wake_adapter_busy",
"disabled": "wake_identity_missing",
"identity_conflict": "wake_identity_conflict",
"exhausted": "wake_rate_limited",
}
return {"state": "error", "error": errors.get(state, "wake_not_confirmed")}
def snapshot(self, ident):
with self.lock:
current = self.current.get(ident)
@@ -246,6 +289,11 @@ class Recovery:
while not self.stopping.wait(2):
try:
snapshots = self.broker.snapshots()
try:
self.sync_registry()
except (OSError, ValueError, RuntimeError, KeyError, TypeError):
with self.lock:
self.wakeable = set()
with self.lock:
for ident, future in list(self.jobs.items()):
if future.done():
@@ -270,6 +318,8 @@ class RecoveryCalls:
self.recovery, self.ident, self.command = recovery, ident, command
def call(self, action, params):
if action == "power.wake":
return self.recovery.wake_once(self.ident, self.command)
if action == "recovery.configure":
return self.recovery.configure(self.ident, params["enabled"])
if action == "preview.stop":