191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
import time
|
||
from typing import Dict, List, Optional
|
||
|
||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
|
||
|
||
from uas.uas_control.base_adapter import BaseProtocolAdapter
|
||
from uas.uas_control.base_controller import BaseUASController
|
||
from uas.uas_control.command import CommandResult, Commands
|
||
from uas.uas_control.telemetry import BaseTelemetry
|
||
|
||
|
||
# ═════════════════════════════════════════════════════════════════
|
||
# 多设备统一管理器
|
||
# ═════════════════════════════════════════════════════════════════
|
||
|
||
class UASManager(QObject):
|
||
"""
|
||
无人系统多设备统一管理器
|
||
|
||
职责:
|
||
- 统一注册/注销控制器(支持任意 BaseUASController 子类)
|
||
- 批量连接/断开
|
||
- 广播指令(全部设备 or 按类型筛选)
|
||
- 汇聚遥测数据
|
||
- 设备健康监控(心跳超时检测)
|
||
- 协议热切换(switch_protocol)
|
||
"""
|
||
|
||
device_added = pyqtSignal(str) # device_id
|
||
device_removed = pyqtSignal(str) # device_id
|
||
telemetry_all = pyqtSignal(str, object) # (device_id, telemetry)
|
||
alert = pyqtSignal(str, str) # (device_id, message)
|
||
|
||
HEARTBEAT_TIMEOUT_S = 5.0
|
||
|
||
def __init__(self, parent: QObject = None):
|
||
super().__init__(parent)
|
||
self._controllers: Dict[str, BaseUASController] = {}
|
||
self._last_telem_time: Dict[str, float] = {}
|
||
|
||
# 心跳监控定时器
|
||
self._hb_timer = QTimer(self)
|
||
self._hb_timer.timeout.connect(self._check_heartbeats)
|
||
self._hb_timer.start(2000)
|
||
self.logger = logging.getLogger(
|
||
f"{self.__class__.__name__}"
|
||
)
|
||
|
||
# ── 设备管理 ──────────────────────────────────────────────────
|
||
|
||
def register(self, ctrl: BaseUASController) -> None:
|
||
"""注册控制器"""
|
||
did = ctrl.device_id
|
||
self._controllers[did] = ctrl
|
||
self._last_telem_time[did] = time.time()
|
||
|
||
ctrl.telemetry_updated.connect(
|
||
lambda t, _id=did: self._on_telemetry(_id, t)
|
||
)
|
||
ctrl.error_occurred.connect(
|
||
lambda msg, _id=did: self.alert.emit(_id, msg)
|
||
)
|
||
self.device_added.emit(did)
|
||
self.logger.info(f"设备已注册: {did} [{ctrl.device_type}]")
|
||
|
||
def unregister(self, device_id: str) -> bool:
|
||
"""注销控制器"""
|
||
ctrl = self._controllers.pop(device_id, None)
|
||
if ctrl:
|
||
ctrl.disconnect()
|
||
self._last_telem_time.pop(device_id, None)
|
||
self.device_removed.emit(device_id)
|
||
self.logger.info(f"设备已注销: {device_id}")
|
||
return True
|
||
return False
|
||
|
||
def get(self, device_id: str) -> Optional[BaseUASController]:
|
||
return self._controllers.get(device_id)
|
||
|
||
def get_all(self) -> List[BaseUASController]:
|
||
return list(self._controllers.values())
|
||
|
||
def get_by_type(self, device_type: str) -> List[BaseUASController]:
|
||
return [c for c in self._controllers.values()
|
||
if c.device_type == device_type]
|
||
|
||
@property
|
||
def device_count(self) -> int:
|
||
return len(self._controllers)
|
||
|
||
# ── 批量操作 ──────────────────────────────────────────────────
|
||
|
||
def connect_all(self) -> Dict[str, bool]:
|
||
results = {}
|
||
for did, ctrl in self._controllers.items():
|
||
results[did] = ctrl.connect()
|
||
return results
|
||
|
||
def disconnect_all(self) -> None:
|
||
for ctrl in self._controllers.values():
|
||
ctrl.disconnect()
|
||
|
||
def broadcast(
|
||
self,
|
||
cmd_builder,
|
||
device_type: Optional[str] = None,
|
||
) -> Dict[str, CommandResult]:
|
||
"""
|
||
广播指令
|
||
:param cmd_builder: callable(device_id) → Command
|
||
:param device_type: 仅发送给指定类型(None=全部)
|
||
"""
|
||
targets = (self.get_by_type(device_type)
|
||
if device_type else self.get_all())
|
||
return {
|
||
ctrl.device_id: ctrl.send(cmd_builder(ctrl.device_id))
|
||
for ctrl in targets
|
||
}
|
||
|
||
def emergency_stop_all(self) -> Dict[str, CommandResult]:
|
||
"""紧急停止所有设备"""
|
||
self.logger.warning("⚠️ 紧急停止所有设备!")
|
||
return self.broadcast(lambda did: Commands.emergency_stop(did))
|
||
|
||
def switch_protocol(
|
||
self,
|
||
device_id: str,
|
||
new_adapter: BaseProtocolAdapter,
|
||
) -> bool:
|
||
"""
|
||
为指定设备热切换协议适配器
|
||
例:MAVLink → ROS,无需重启控制器
|
||
"""
|
||
ctrl = self.get(device_id)
|
||
if ctrl is None:
|
||
self.logger.error(f"设备不存在: {device_id}")
|
||
return False
|
||
ctrl.switch_adapter(new_adapter)
|
||
self.logger.info(
|
||
f"协议热切换完成: {device_id} → "
|
||
f"{new_adapter.__class__.__name__}"
|
||
)
|
||
return True
|
||
|
||
# ── 遥测汇聚 ──────────────────────────────────────────────────
|
||
|
||
def get_all_telemetry(self) -> Dict[str, Optional[BaseTelemetry]]:
|
||
return {did: ctrl.get_telemetry()
|
||
for did, ctrl in self._controllers.items()}
|
||
|
||
def _on_telemetry(self, device_id: str,
|
||
telem: BaseTelemetry) -> None:
|
||
self._last_telem_time[device_id] = time.time()
|
||
self.telemetry_all.emit(device_id, telem)
|
||
|
||
# ── 心跳监控 ──────────────────────────────────────────────────
|
||
|
||
def _check_heartbeats(self) -> None:
|
||
now = time.time()
|
||
for did, last_t in list(self._last_telem_time.items()):
|
||
gap = now - last_t
|
||
if gap > self.HEARTBEAT_TIMEOUT_S:
|
||
self.alert.emit(
|
||
did,
|
||
f"心跳超时 {gap:.1f}s,设备可能失联"
|
||
)
|
||
|
||
def status_report(self) -> str:
|
||
"""生成所有设备状态摘要"""
|
||
lines = [f"{'─'*65}", f" UASManager 状态报告 设备数={self.device_count}",
|
||
f"{'─'*65}"]
|
||
for did, ctrl in self._controllers.items():
|
||
telem = ctrl.get_telemetry()
|
||
conn = "✅已连接" if ctrl.is_connected else "❌未连接"
|
||
bat = f"🔋{telem.battery_percent}%" if telem else "🔋--"
|
||
lines.append(
|
||
f" {did:12s} [{ctrl.device_type:10s}] "
|
||
f"{conn} {bat} "
|
||
f"adapter={ctrl._adapter.__class__.__name__}"
|
||
)
|
||
lines.append(f"{'─'*65}")
|
||
return "\n".join(lines)
|
||
|
||
def __repr__(self) -> str:
|
||
return (f"UASManager("
|
||
f"devices={self.device_count}, "
|
||
f"ids={list(self._controllers.keys())})")
|