326 lines
12 KiB
Python
326 lines
12 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
无人系统控制器抽象基类
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
BaseUASController 所有无人系统控制器的统一抽象基类
|
|||
|
|
|
|||
|
|
职责:
|
|||
|
|
- 持有 BaseProtocolAdapter 实例(可运行时热切换)
|
|||
|
|
- 提供统一的高层控制 API(arm/takeoff/goto 等)
|
|||
|
|
- 内部将高层 API 转换为 Command 并通过 Adapter 发送
|
|||
|
|
- 维护设备连接状态和最新遥测缓存
|
|||
|
|
- 支持指令队列与优先级调度
|
|||
|
|
─────────────────────────────────────────────────────────────────
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
from abc import abstractmethod
|
|||
|
|
from typing import Callable, Dict, List, Optional
|
|||
|
|
|
|||
|
|
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
|
|||
|
|
|
|||
|
|
from uas.uas_control import command_registry
|
|||
|
|
from uas.uas_control.base_adapter import BaseProtocolAdapter
|
|||
|
|
from uas.uas_control.command import Command, CommandResult, CommandType, Commands
|
|||
|
|
from uas.uas_control.telemetry import BaseTelemetry, ConnectionState
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
# 控制器抽象基类
|
|||
|
|
# ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class BaseUASController(QObject):
|
|||
|
|
"""
|
|||
|
|
无人系统控制器抽象基类
|
|||
|
|
|
|||
|
|
── 子类必须实现 ──────────────────────────────────────────────
|
|||
|
|
device_type 属性:设备类型字符串
|
|||
|
|
_build_telemetry_type() 返回对应的遥测类型
|
|||
|
|
|
|||
|
|
── 子类可选覆盖 ──────────────────────────────────────────────
|
|||
|
|
_on_telemetry(telem) 遥测数据到达回调
|
|||
|
|
_on_connected() 连接成功回调
|
|||
|
|
_on_disconnected() 断开连接回调
|
|||
|
|
_pre_send(cmd) 指令发送前拦截(返回 False 则取消)
|
|||
|
|
_post_send(cmd, result) 指令发送后回调
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# ── PyQt5 信号 ────────────────────────────────────────────────
|
|||
|
|
telemetry_updated = pyqtSignal(object) # BaseTelemetry
|
|||
|
|
command_sent = pyqtSignal(object) # Command
|
|||
|
|
command_result = pyqtSignal(object) # CommandResult
|
|||
|
|
connection_changed = pyqtSignal(str, str) # (device_id, state)
|
|||
|
|
error_occurred = pyqtSignal(str) # 错误消息
|
|||
|
|
|
|||
|
|
# 遥测轮询间隔(毫秒)
|
|||
|
|
TELEMETRY_INTERVAL_MS: int = 100
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
device_id: str,
|
|||
|
|
adapter: BaseProtocolAdapter,
|
|||
|
|
parent: QObject = None,
|
|||
|
|
):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self.device_id = device_id
|
|||
|
|
self._adapter: BaseProtocolAdapter = adapter
|
|||
|
|
self._telemetry: Optional[BaseTelemetry] = None
|
|||
|
|
self._cmd_history: List[CommandResult] = []
|
|||
|
|
self._callbacks: Dict[CommandType, Callable] = {}
|
|||
|
|
self._lock = threading.Lock()
|
|||
|
|
|
|||
|
|
self.logger = logging.getLogger(
|
|||
|
|
f"{self.__class__.__name__}[{device_id}]"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 连接 Adapter 信号
|
|||
|
|
self._adapter.connected.connect(self._on_adapter_connected)
|
|||
|
|
self._adapter.disconnected.connect(self._on_adapter_disconnected)
|
|||
|
|
self._adapter.telemetry_ready.connect(self._on_telemetry_signal)
|
|||
|
|
self._adapter.command_ack.connect(self._on_command_ack)
|
|||
|
|
self._adapter.error_occurred.connect(self._on_adapter_error)
|
|||
|
|
|
|||
|
|
# 遥测轮询定时器
|
|||
|
|
self._telem_timer = QTimer(self)
|
|||
|
|
self._telem_timer.timeout.connect(self._poll_telemetry)
|
|||
|
|
|
|||
|
|
# ── 连接管理 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def connect(self) -> bool:
|
|||
|
|
"""连接设备"""
|
|||
|
|
ok = self._adapter.connect()
|
|||
|
|
if ok:
|
|||
|
|
self._telem_timer.start(self.TELEMETRY_INTERVAL_MS)
|
|||
|
|
return ok
|
|||
|
|
|
|||
|
|
def disconnect(self) -> bool:
|
|||
|
|
"""断开设备"""
|
|||
|
|
self._telem_timer.stop()
|
|||
|
|
return self._adapter.disconnect()
|
|||
|
|
|
|||
|
|
def reconnect(self) -> bool:
|
|||
|
|
"""重连设备"""
|
|||
|
|
self._telem_timer.stop()
|
|||
|
|
ok = self._adapter.reconnect()
|
|||
|
|
if ok:
|
|||
|
|
self._telem_timer.start(self.TELEMETRY_INTERVAL_MS)
|
|||
|
|
return ok
|
|||
|
|
|
|||
|
|
def switch_adapter(self, new_adapter: BaseProtocolAdapter) -> None:
|
|||
|
|
"""
|
|||
|
|
热切换协议适配器(运行时切换 MAVLink ↔ ROS 等)
|
|||
|
|
切换前自动断开旧适配器,切换后重新连接。
|
|||
|
|
"""
|
|||
|
|
was_connected = self._adapter.is_connected
|
|||
|
|
self._telem_timer.stop()
|
|||
|
|
|
|||
|
|
# 断开旧适配器信号
|
|||
|
|
try:
|
|||
|
|
self._adapter.connected.disconnect()
|
|||
|
|
self._adapter.disconnected.disconnect()
|
|||
|
|
self._adapter.telemetry_ready.disconnect()
|
|||
|
|
self._adapter.command_ack.disconnect()
|
|||
|
|
self._adapter.error_occurred.disconnect()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._adapter.disconnect()
|
|||
|
|
|
|||
|
|
# 切换
|
|||
|
|
self._adapter = new_adapter
|
|||
|
|
self._adapter.connected.connect(self._on_adapter_connected)
|
|||
|
|
self._adapter.disconnected.connect(self._on_adapter_disconnected)
|
|||
|
|
self._adapter.telemetry_ready.connect(self._on_telemetry_signal)
|
|||
|
|
self._adapter.command_ack.connect(self._on_command_ack)
|
|||
|
|
self._adapter.error_occurred.connect(self._on_adapter_error)
|
|||
|
|
|
|||
|
|
self.logger.info(
|
|||
|
|
f"协议适配器已切换: {new_adapter.__class__.__name__}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if was_connected:
|
|||
|
|
self.connect()
|
|||
|
|
|
|||
|
|
# ── 指令发送(核心)──────────────────────────────────────────
|
|||
|
|
def send(self, cmd: Command) -> CommandResult:
|
|||
|
|
"""
|
|||
|
|
发送控制指令(统一入口)
|
|||
|
|
|
|||
|
|
流程:
|
|||
|
|
① _pre_send() 前置拦截
|
|||
|
|
② 检查连接状态
|
|||
|
|
③ 通过 Adapter 发送
|
|||
|
|
④ 记录历史 + 发射信号
|
|||
|
|
⑤ _post_send() 后置回调
|
|||
|
|
"""
|
|||
|
|
# ① 前置拦截
|
|||
|
|
if not self._pre_send(cmd):
|
|||
|
|
result = CommandResult.fail(cmd, "前置检查未通过")
|
|||
|
|
self.command_result.emit(result)
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# ② 连接检查
|
|||
|
|
if not self._adapter.is_connected:
|
|||
|
|
result = CommandResult.fail(cmd, "设备未连接")
|
|||
|
|
self.command_result.emit(result)
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# ③ 发送
|
|||
|
|
t0 = time.time()
|
|||
|
|
result = self._adapter.send_command(cmd)
|
|||
|
|
result.elapsed_ms = (time.time() - t0) * 1000
|
|||
|
|
|
|||
|
|
# ④ 记录 + 信号
|
|||
|
|
with self._lock:
|
|||
|
|
self._cmd_history.append(result)
|
|||
|
|
if len(self._cmd_history) > 500:
|
|||
|
|
self._cmd_history = self._cmd_history[-500:]
|
|||
|
|
|
|||
|
|
self.command_sent.emit(cmd)
|
|||
|
|
self.command_result.emit(result)
|
|||
|
|
self.logger.debug(f"指令结果: {result}")
|
|||
|
|
|
|||
|
|
# ⑤ 后置回调
|
|||
|
|
self._post_send(cmd, result)
|
|||
|
|
|
|||
|
|
# 自定义回调
|
|||
|
|
cb = self._callbacks.get(cmd.command_type)
|
|||
|
|
if cb:
|
|||
|
|
cb(result)
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# ── 通用高层 API ──────────────────────────────────────────────
|
|||
|
|
@command_registry.register
|
|||
|
|
def arm(self) -> CommandResult:
|
|||
|
|
return self.send(Commands.arm(self.device_id))
|
|||
|
|
|
|||
|
|
@command_registry.register
|
|||
|
|
def disarm(self) -> CommandResult:
|
|||
|
|
return self.send(Commands.disarm(self.device_id))
|
|||
|
|
|
|||
|
|
@command_registry.register
|
|||
|
|
def emergency_stop(self) -> CommandResult:
|
|||
|
|
return self.send(Commands.emergency_stop(self.device_id))
|
|||
|
|
|
|||
|
|
@command_registry.register
|
|||
|
|
def set_mode(self, mode: str) -> CommandResult:
|
|||
|
|
return self.send(Commands.set_mode(self.device_id, mode))
|
|||
|
|
|
|||
|
|
@command_registry.register
|
|||
|
|
def goto(self, lat: float, lon: float,
|
|||
|
|
alt: float, speed: float = 5.0) -> CommandResult:
|
|||
|
|
return self.send(Commands.goto(
|
|||
|
|
self.device_id, lat, lon, alt, speed
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
@command_registry.register
|
|||
|
|
def set_velocity(self, vx: float, vy: float,
|
|||
|
|
vz: float = 0.0) -> CommandResult:
|
|||
|
|
return self.send(Commands.set_velocity(
|
|||
|
|
self.device_id, vx, vy, vz
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
def custom_command(self, model: int, **params) -> CommandResult:
|
|||
|
|
return self.send(Commands.custom(self.device_id, model, **params))
|
|||
|
|
|
|||
|
|
# ── 遥测访问 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def get_telemetry(self) -> Optional[BaseTelemetry]:
|
|||
|
|
# if self._telemetry is None:
|
|||
|
|
# self._telemetry = self._adapter.get_telemetry()
|
|||
|
|
# return self._telemetry
|
|||
|
|
self._poll_telemetry()
|
|||
|
|
return self._telemetry
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def is_connected(self) -> bool:
|
|||
|
|
return self._adapter.is_connected
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def command_history(self) -> List[CommandResult]:
|
|||
|
|
with self._lock:
|
|||
|
|
return list(self._cmd_history)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def adapter(self):
|
|||
|
|
return self._adapter
|
|||
|
|
|
|||
|
|
# ── 回调注册 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def register_callback(
|
|||
|
|
self,
|
|||
|
|
cmd_type: CommandType,
|
|||
|
|
callback: Callable[[CommandResult], None],
|
|||
|
|
) -> None:
|
|||
|
|
"""注册指令完成回调"""
|
|||
|
|
self._callbacks[cmd_type] = callback
|
|||
|
|
|
|||
|
|
def unregister_callback(self, cmd_type: CommandType) -> None:
|
|||
|
|
self._callbacks.pop(cmd_type, None)
|
|||
|
|
|
|||
|
|
# ── 抽象属性 ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
@abstractmethod
|
|||
|
|
def device_type(self) -> str:
|
|||
|
|
"""设备类型字符串(如 'controller' / 'robot_dog')"""
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
# ── 可选覆盖钩子 ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _on_telemetry(self, telem: BaseTelemetry) -> None:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _on_connected(self) -> None:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _on_disconnected(self) -> None:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _pre_send(self, cmd: Command) -> bool:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _post_send(self, cmd: Command, result: CommandResult) -> None:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── 内部槽 ───────────────────────────────────────────────────
|
|||
|
|
def _poll_telemetry(self) -> None:
|
|||
|
|
telem = self._adapter.get_telemetry()
|
|||
|
|
if telem:
|
|||
|
|
self._telemetry = telem
|
|||
|
|
self.telemetry_updated.emit(telem)
|
|||
|
|
self._on_telemetry(telem)
|
|||
|
|
|
|||
|
|
def _on_adapter_connected(self, device_id: str) -> None:
|
|||
|
|
self.connection_changed.emit(device_id,
|
|||
|
|
ConnectionState.CONNECTED.value)
|
|||
|
|
self._on_connected()
|
|||
|
|
|
|||
|
|
def _on_adapter_disconnected(self, device_id: str) -> None:
|
|||
|
|
self.connection_changed.emit(device_id,
|
|||
|
|
ConnectionState.DISCONNECTED.value)
|
|||
|
|
self._on_disconnected()
|
|||
|
|
|
|||
|
|
def _on_telemetry_signal(self, telem: BaseTelemetry) -> None:
|
|||
|
|
self._telemetry = telem
|
|||
|
|
self.telemetry_updated.emit(telem)
|
|||
|
|
self._on_telemetry(telem)
|
|||
|
|
|
|||
|
|
def _on_command_ack(self, result: CommandResult) -> None:
|
|||
|
|
self.command_result.emit(result)
|
|||
|
|
|
|||
|
|
def _on_adapter_error(self, msg: str) -> None:
|
|||
|
|
self.error_occurred.emit(msg)
|
|||
|
|
self.logger.error(f"Adapter 错误: {msg}")
|
|||
|
|
|
|||
|
|
def __repr__(self) -> str:
|
|||
|
|
state = "已连接" if self.is_connected else "未连接"
|
|||
|
|
return (f"{self.__class__.__name__}("
|
|||
|
|
f"id={self.device_id}, "
|
|||
|
|
f"type={self.device_type}, "
|
|||
|
|
f"adapter={self._adapter.__class__.__name__}, "
|
|||
|
|
f"{state})")
|