Add D455 sensor host and shared Node/Core preparation surface
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""Fleet projects Node-owned sensor state and forwards SDK operations, never hardware calls."""
|
||||
|
||||
import json
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from missioncore_plugin_sdk.v0alpha2.operations import OperationRequest
|
||||
from missioncore_plugin_sdk.v0alpha2.session import DeviceSessionSnapshot
|
||||
|
||||
from .trust import PairingError
|
||||
|
||||
ACTIONS = {
|
||||
"prepare",
|
||||
"details",
|
||||
"rename",
|
||||
"verify",
|
||||
"start",
|
||||
"stop",
|
||||
"option",
|
||||
"offer",
|
||||
"close-peer",
|
||||
}
|
||||
|
||||
|
||||
def validate_inventory(value, node_id):
|
||||
items = value.get("devices", [])
|
||||
if not isinstance(items, list) or len(items) > 16:
|
||||
raise ValueError("Invalid device inventory")
|
||||
seen = set()
|
||||
for item in items:
|
||||
snapshot = DeviceSessionSnapshot.model_validate(item["snapshot"])
|
||||
if (
|
||||
snapshot.context.execution.node_id != node_id
|
||||
or snapshot.context.device.device_id != item["id"]
|
||||
or item["id"] in seen
|
||||
):
|
||||
raise ValueError("Invalid device execution binding")
|
||||
seen.add(item["id"])
|
||||
state = value.get("sensor_state", {"items": items, "operations": [], "preparation": None})
|
||||
if state.get("items") != items or len(json.dumps(state)) > 262144:
|
||||
raise ValueError("Invalid sensor state")
|
||||
return state
|
||||
|
||||
|
||||
def submit(fleet, vehicle_id, value):
|
||||
command = OperationRequest.model_validate(value)
|
||||
if (
|
||||
command.action_id not in ACTIONS
|
||||
or command.idempotency_key != command.operation_id
|
||||
or len(json.dumps(value)) > 65536
|
||||
):
|
||||
raise PairingError("Неподдерживаемая команда устройства.")
|
||||
with fleet.lock:
|
||||
row = fleet.find(vehicle_id)
|
||||
if fleet.public(row)["connectivity"] != "online":
|
||||
raise PairingError("БК недоступен. Дождитесь связи перед настройкой устройства.")
|
||||
existing = row.setdefault("sensor_commands", {}).get(command.operation_id)
|
||||
if existing:
|
||||
if existing["command"] != value:
|
||||
raise PairingError("Идентификатор операции уже использован.")
|
||||
return existing
|
||||
if (
|
||||
not datetime.now(UTC) < command.deadline_at
|
||||
or (command.deadline_at - command.requested_at).total_seconds() > 360
|
||||
):
|
||||
raise PairingError("Срок команды истёк. Обновите устройство.")
|
||||
item = next(
|
||||
(
|
||||
item
|
||||
for item in row.get("sensor_state", {}).get("items", [])
|
||||
if item["id"] == command.session.device_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if item is None or item["snapshot"]["context"]["session_id"] != command.session.session_id:
|
||||
raise PairingError("Сеанс устройства изменился. Обновите сведения.")
|
||||
row["sensor_commands"] = {
|
||||
k: v for k, v in row["sensor_commands"].items() if time.time() - v["updated_at"] < 600
|
||||
}
|
||||
if len(row["sensor_commands"]) >= 32:
|
||||
raise PairingError("Слишком много запросов к устройству. Повторите позже.")
|
||||
operation = {"command": value, "state": "queued", "updated_at": time.time()}
|
||||
row["sensor_commands"][command.operation_id] = operation
|
||||
fleet.save(row)
|
||||
return operation
|
||||
|
||||
|
||||
def operation(fleet, vehicle_id, identifier):
|
||||
with fleet.lock:
|
||||
value = fleet.find(vehicle_id).get("sensor_commands", {}).get(identifier)
|
||||
if value is None:
|
||||
raise PairingError("Операция не найдена.")
|
||||
if value["state"] in ("queued", "running") and datetime.fromisoformat(
|
||||
value["command"]["deadline_at"]
|
||||
) < datetime.now(UTC):
|
||||
return {
|
||||
**value,
|
||||
"state": "unknown",
|
||||
"error": "Подтверждение не получено. Обновите фактическое состояние устройства.",
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def heartbeat(row, value):
|
||||
commands = row.setdefault("sensor_commands", {})
|
||||
acknowledgements = []
|
||||
results = value.get("sensor_results", [])
|
||||
if not isinstance(results, list) or len(results) > 64:
|
||||
raise ValueError("Invalid sensor operations")
|
||||
for result in results:
|
||||
command = result.get("command", {})
|
||||
identifier = command.get("operation_id")
|
||||
existing = commands.get(identifier)
|
||||
if (
|
||||
existing
|
||||
and existing["command"] == command
|
||||
and result.get("state") in ("running", "complete", "error", "unknown")
|
||||
):
|
||||
commands[identifier] = {**result, "updated_at": time.time()}
|
||||
if result["state"] != "running":
|
||||
acknowledgements.append(identifier)
|
||||
pending = []
|
||||
for entry in commands.values():
|
||||
if entry["state"] == "queued" and datetime.fromisoformat(
|
||||
entry["command"]["deadline_at"]
|
||||
) > datetime.now(UTC):
|
||||
pending.append(entry["command"])
|
||||
return {"sensor_commands": pending[:4], "sensor_acknowledgements": acknowledgements}
|
||||
Reference in New Issue
Block a user