Files
NODEDC_MISSION_CORE/src/k1link/fleet/sensors.py
T

130 lines
5.1 KiB
Python

"""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",
"replay",
"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}