Files
Migu2.0/SimpleLite/model/agv_glb/playback_hub.py
T

183 lines
6.3 KiB
Python
Raw Normal View History

2026-06-25 11:02:22 +08:00
# -*- coding: utf-8 -*-
"""播放命令总线:HTTP 写入期望状态,播放页通过 SSE 订阅并执行。"""
from __future__ import annotations
import queue
import threading
import time
from typing import Any
REMOTE_METHODS = frozenset({"playAction", "stopAction", "clearActions"})
def _as_string(v: Any) -> str:
return v.strip() if isinstance(v, str) else ""
def _as_string_list(v: Any) -> list[str]:
if isinstance(v, list):
return [_as_string(x) for x in v if _as_string(x)]
if isinstance(v, str):
return [s for s in v.replace("+", ",").split(",") if s.strip()]
return []
class PlaybackHub:
def __init__(self) -> None:
self._lock = threading.Lock()
self._revision = 0
self._state: dict[str, Any] = {"modelId": None, "activeActions": []}
self._subscribers: list[queue.Queue] = []
def _snapshot_locked(self) -> dict[str, Any]:
return {
"modelId": self._state.get("modelId"),
"activeActions": list(self._state.get("activeActions") or []),
"revision": self._revision,
"updatedAt": self._state.get("updatedAt"),
}
def get_state(self) -> dict[str, Any]:
with self._lock:
return self._snapshot_locked()
def subscriber_count(self) -> int:
with self._lock:
return len(self._subscribers)
def subscribe(self) -> queue.Queue:
q: queue.Queue = queue.Queue(maxsize=128)
with self._lock:
self._subscribers.append(q)
return q
def unsubscribe(self, q: queue.Queue) -> None:
with self._lock:
if q in self._subscribers:
self._subscribers.remove(q)
def _bump_locked(self) -> int:
self._revision += 1
self._state["updatedAt"] = time.time()
return self._revision
def _broadcast_locked(self, message: dict[str, Any]) -> None:
dead: list[queue.Queue] = []
for sub in self._subscribers:
try:
sub.put_nowait(message)
except queue.Full:
dead.append(sub)
for sub in dead:
if sub in self._subscribers:
self._subscribers.remove(sub)
def _set_model(self, model_id: str) -> None:
if self._state.get("modelId") != model_id:
self._state["activeActions"] = []
self._state["modelId"] = model_id
def _require_model(self, params: dict[str, Any]) -> str:
model_id = _as_string(params.get("modelId") or params.get("model"))
if not model_id:
raise ValueError("params.modelId 必填")
self._set_model(model_id)
return model_id
def _apply(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
params = params if isinstance(params, dict) else {}
if method == "playAction":
model_id = self._require_model(params)
active = list(self._state.get("activeActions") or [])
ids = _as_string_list(params.get("actionIds") or params.get("actions"))
if not ids:
one = _as_string(params.get("actionId") or params.get("action"))
if one:
ids = [one]
if not ids:
raise ValueError("params.actionId 必填")
for action_id in ids:
if action_id not in active:
active.append(action_id)
self._state["activeActions"] = active
return {"modelId": model_id, "played": ids, "activeActions": active}
if method == "stopAction":
model_id = self._require_model(params)
active = list(self._state.get("activeActions") or [])
ids = _as_string_list(params.get("actionIds") or params.get("actions"))
if not ids:
one = _as_string(params.get("actionId") or params.get("action"))
if one:
ids = [one]
if not ids:
raise ValueError("params.actionId 必填")
active = [a for a in active if a not in ids]
self._state["activeActions"] = active
return {"modelId": model_id, "stopped": ids, "activeActions": active}
if method == "clearActions":
model_id = self._require_model(params)
self._state["activeActions"] = []
return {"modelId": model_id, "activeActions": []}
raise ValueError(f"不支持的 method: {method}")
def _emit_locked(self, method: str, params: dict[str, Any], req_id: Any, source_client_id: str | None) -> dict[str, Any]:
data = self._apply(method, params)
revision = self._bump_locked()
state = self._snapshot_locked()
self._broadcast_locked(
{
"version": 1,
"id": req_id,
"revision": revision,
"method": method,
"params": params,
"state": state,
"sourceClientId": source_client_id,
}
)
return {
"dispatched": True,
"subscribers": len(self._subscribers),
"revision": revision,
"state": state,
**data,
}
def dispatch(self, method: str, params: dict[str, Any] | None, req_id: Any = None) -> dict[str, Any]:
if method not in REMOTE_METHODS:
raise ValueError(f"method「{method}」不可远程调度")
with self._lock:
return self._emit_locked(method, params or {}, req_id, None)
def report_from_client(
self,
client_id: str,
model_id: str | None,
active_actions: list[str] | None,
) -> dict[str, Any]:
with self._lock:
if model_id:
self._state["modelId"] = model_id
if active_actions is not None:
self._state["activeActions"] = list(active_actions)
revision = self._bump_locked()
state = self._snapshot_locked()
self._broadcast_locked(
{
"version": 1,
"revision": revision,
"method": "syncState",
"params": {},
"state": state,
"sourceClientId": client_id,
}
)
return {"revision": revision, "state": state, "subscribers": len(self._subscribers)}
HUB = PlaybackHub()