新增 WMS 搬运规则与任务管理
支持搬运规则维护、候选预览、任务生成、预占、下发、取消和完成,并完善仓储管理前端交互。
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user