from enum import Enum from typing import Any from uuid import uuid4 from pydantic import BaseModel, Field, HttpUrl from led_platform.core.clock import epoch_ms class CommandType(str, Enum): HELLO = "hello" STATE = "state" PREPARE_SCENE = "prepare_scene" COMMIT_SCENE = "commit_scene" COMPONENT_ACTION = "component_action" CLOCK_PING = "clock_ping" CLOCK_PONG = "clock_pong" TELEMETRY = "telemetry" HEARTBEAT = "heartbeat" ACK = "ack" ERROR = "error" class AckStatus(str, Enum): PREPARED = "prepared" COMMITTED = "committed" ACTION_COMMITTED = "action_committed" LATE = "late" ERROR = "error" class NodeRole(str, Enum): ADMIN = "admin" OUTPUT = "output" RENDERER = "renderer" class NodeStatus(str, Enum): STARTING = "starting" READY = "ready" DEGRADED = "degraded" ERROR = "error" class PhysicalOutput(BaseModel): id: str index: int x: int y: int width: int = 3840 height: int = 2160 connector: str | None = None class Tile(BaseModel): id: str name: str x: int y: int width: int height: int wall_width: int wall_height: int desktop_width: int desktop_height: int physical_outputs: list[PhysicalOutput] = Field(default_factory=list) @property def right(self) -> int: return self.x + self.width class Scene(BaseModel): id: str name: str url: str | HttpUrl = "/wall-app" view: str description: str | None = None preload: list[str] = Field(default_factory=list) class SceneState(BaseModel): active_scene_id: str active_view: str active_url: str version: int scene_started_at_ms: int apply_at_ms: int | None = None action: str | None = None class CommandEnvelope(BaseModel): type: CommandType command_id: str = Field(default_factory=lambda: uuid4().hex) payload: dict[str, Any] = Field(default_factory=dict) sent_at_ms: int = Field(default_factory=epoch_ms) class OutputNode(BaseModel): node_id: str tile_id: str status: NodeStatus = NodeStatus.READY connected_at_ms: int = Field(default_factory=epoch_ms) last_seen_ms: int = Field(default_factory=epoch_ms) app_version: str | None = None user_agent: str | None = None clock_offset_ms: float | None = None rtt_ms: float | None = None fps: float | None = None frame_time_ms: float | None = None dropped_frames: int | None = None gpu_hint: str | None = None class PerformanceSample(BaseModel): node_id: str tile_id: str fps: float frame_time_ms: float dropped_frames: int = 0 at_ms: int = Field(default_factory=epoch_ms) class CommandRecord(BaseModel): command_id: str command_type: CommandType target_tiles: list[str] apply_at_ms: int created_at_ms: int = Field(default_factory=epoch_ms) expires_at_ms: int released_at_ms: int | None = None payload: dict[str, Any] = Field(default_factory=dict) acks: list[dict[str, Any]] = Field(default_factory=list) @property def prepared(self) -> bool: prepared_tiles = { ack.get("tile_id") for ack in self.acks if ack.get("status") in {AckStatus.PREPARED, AckStatus.COMMITTED} } return set(self.target_tiles).issubset({str(tile) for tile in prepared_tiles if tile}) @property def complete(self) -> bool: completed = { ack.get("tile_id") for ack in self.acks if ack.get("status") in {AckStatus.COMMITTED, AckStatus.ACTION_COMMITTED} } return set(self.target_tiles).issubset({str(tile) for tile in completed if tile})