feat(x4): add per-camera manual wake and hide power action when connected
This commit is contained in:
@@ -223,7 +223,7 @@ class Broker:
|
||||
return {"items": [self.item(item, node) for item in snapshots]}
|
||||
if method == "POST" and route == "/operation":
|
||||
identifier = value["session"]["device_id"]
|
||||
if value.get("action_id") == "recovery.configure" and self.recovery:
|
||||
if value.get("action_id") in ("recovery.configure", "power.wake") and self.recovery:
|
||||
return self.recovery.operation(identifier, value)
|
||||
if value.get("action_id") == "preview.stop" and self.recovery:
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
"""Public messages are allowlisted; vendor error text never crosses the boundary."""
|
||||
|
||||
MESSAGES = {
|
||||
"wake_adapter_busy": "Сейчас включается другая камера. Повторите действие позже.",
|
||||
"wake_identity_missing": "Сначала подключите эту камеру по USB и подготовьте её на борту.",
|
||||
"wake_identity_conflict": "Не удалось однозначно выбрать камеру для включения.",
|
||||
"wake_rate_limited": "Лимит попыток включения исчерпан. Повторите через 10 минут.",
|
||||
"wake_not_confirmed": (
|
||||
"Камера не подключилась. Проверьте питание, USB "
|
||||
"и настройку «Пробуждение по Bluetooth» на X4."
|
||||
),
|
||||
"unsupported_camera_parameter": "Параметр или действие недоступны в текущем режиме камеры.",
|
||||
"verification_recording_active": "Для проверки изображения остановите запись на камере.",
|
||||
"preview_no_decodable_image": "Камера не передала декодируемое изображение.",
|
||||
|
||||
@@ -35,6 +35,10 @@ class VideoHeader(ctypes.Structure):
|
||||
|
||||
def parameters(action, value):
|
||||
"""Validate before any SDK call; parameters never select a file or library."""
|
||||
if action == "power.wake":
|
||||
if not isinstance(value, dict) or value:
|
||||
raise ValueError("Wake uses the enrolled camera identity only")
|
||||
return 0, "", 0.0
|
||||
if action == "recovery.configure":
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user