"""Private Unix plugin host; the Go broker is the only UI/Core authority.""" import asyncio import hashlib import json import os import re import uuid from datetime import UTC, datetime from pathlib import Path import pyrealsense2 as rs from aiohttp import web from device import Device, atomic, device_id from media import Peers from missioncore_plugin_sdk.v0alpha2.operations import OperationRequest ROOT = Path("/var/lib/mission-core-sensors") SOCKET = Path("/run/mission-core-sensors/driver.sock") class Host: def __init__(self, root=ROOT): self.root = root self.context = rs.context() self.devices = {} self.execution = None self.peers = Peers() self.scan_lock = asyncio.Lock() self.operation_locks = {} async def scan(self): if self.execution is None: return async with self.scan_lock: def work(): found = set() for dev in self.context.query_devices(): if dev.get_info(rs.camera_info.product_id).lower() != "0b5c": continue serial = dev.get_info(rs.camera_info.serial_number) ident = device_id(serial) found.add(ident) if ident not in self.devices: self.devices[ident] = Device(serial, self.root, self.execution) self.devices[ident].refresh(dev) for ident, device in self.devices.items(): if ident not in found: device.online = False await asyncio.to_thread(work) async def inventory(self, request): node_id = request.headers.get("X-Node-Id", "") if not re.fullmatch(r"node_[0-9a-f]{64}", node_id): raise web.HTTPForbidden() if self.execution is None: self.execution = { "node_id": node_id, "agent_instance_id": "driver_" + uuid.uuid4().hex, "platform": "linux", } if self.execution["node_id"] != node_id: raise web.HTTPForbidden() await self.scan() return web.json_response( {"items": [device.snapshot(False) for device in self.devices.values()]} ) async def operation(self, request): value = await request.json() command = OperationRequest.model_validate(value) identifier = command.operation_id if not re.fullmatch(r"op_[0-9a-f]{32}", identifier): raise ValueError("Некорректный идентификатор операции.") path = self.root / (identifier + ".json") digest = hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() lock = self.operation_locks.setdefault(identifier, asyncio.Lock()) async with lock: if path.exists(): previous = json.loads(path.read_text()) if previous["digest"] != digest: raise ValueError("Идентификатор операции уже использован.") return web.json_response(previous["result"]) if command.deadline_at <= datetime.now(UTC): raise ValueError("Срок команды истёк. Состояние камеры не изменено.") device = self.devices.get(command.session.device_id) if device is None or command.session.session_id != device.session: raise ValueError("Сеанс камеры изменился. Обновите сведения.") # Persist uncertainty before any side effect. A crash must not replay START. receipt = { "digest": digest, "result": { "state": "unknown", "error": "Результат операции пока неизвестен. Обновите состояние устройства.", }, } atomic(path, receipt) try: action, params = command.action_id, dict(command.parameters) if action == "details": result = device.snapshot() elif action == "rename": result = device.rename(params.get("name")) elif action == "verify": result = await asyncio.to_thread(device.verify) elif action == "start": if type(params.get("record", False)) is not bool: raise ValueError("Некорректный режим записи.") await asyncio.to_thread( device.start, params.get("profiles"), params.get("record", False) ) result = {"ok": True} elif action == "stop": result = await asyncio.to_thread(device.stop) elif action == "option": result = await asyncio.to_thread( device.set_option, params.get("id"), params.get("value") ) elif action == "offer": result = await self.peers.offer(device, params) elif action == "close-peer": result = await self.peers.close(params.get("peer_id")) else: raise ValueError("Неподдерживаемая операция устройства.") receipt["result"] = {"state": "complete", "result": result} except (ValueError, RuntimeError) as error: receipt["result"] = {"state": "error", "error": str(error)[:400]} atomic(path, receipt) return web.json_response(receipt["result"]) async def cleanup(self, app): for peer in list(self.peers.items): await self.peers.close(peer) for device in self.devices.values(): await asyncio.to_thread(device.stop, True) @web.middleware async def errors(request, handler): try: return await handler(request) except (ValueError, KeyError, TypeError): return web.json_response({"error": "Некорректная команда устройства."}, status=400) except RuntimeError: return web.json_response( {"error": "Драйвер не смог выполнить запрос. Проверьте подключение камеры."}, status=409 ) def main(): os.umask(0o007) host = Host() app = web.Application(client_max_size=65536, middlewares=[errors]) app.router.add_get("/inventory", host.inventory) app.router.add_post("/operation", host.operation) app.on_cleanup.append(host.cleanup) if SOCKET.exists(): SOCKET.unlink() web.run_app(app, path=str(SOCKET), print=None, access_log=None, shutdown_timeout=20)