188 lines
7.6 KiB
Python
188 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
协议适配器抽象基类
|
||
─────────────────────────────────────────────────────────────────
|
||
BaseProtocolAdapter 所有协议适配器的统一抽象基类
|
||
|
||
职责:
|
||
- 屏蔽底层协议差异(MAVLink / ROS / 自定义 SDK)
|
||
- 将上层 Command 翻译为协议原生消息并发送
|
||
- 将协议原生遥测数据解析为统一 BaseTelemetry
|
||
- 管理连接生命周期(connect / disconnect / reconnect)
|
||
|
||
新增协议只需继承 BaseProtocolAdapter,实现抽象方法即可。
|
||
─────────────────────────────────────────────────────────────────
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import time
|
||
from abc import abstractmethod
|
||
from enum import Enum
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from PyQt5.QtCore import QObject, pyqtSignal
|
||
|
||
from uas.uas_control.command import Command, CommandResult, CommandType
|
||
from uas.uas_control.telemetry import BaseTelemetry
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# 协议类型枚举
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class ProtocolType(Enum):
|
||
MAVLINK = "mavlink" # MAVLink v1/v2(ArduPilot / PX4)
|
||
ROS = "ros" # ROS1 / ROS2
|
||
CUSTOM = "custom" # 厂商私有 SDK
|
||
SIMULATION= "simulation" # 仿真协议(内部测试)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# 协议适配器抽象基类
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class BaseProtocolAdapter(QObject):
|
||
"""
|
||
协议适配器抽象基类
|
||
|
||
── 子类必须实现 ──────────────────────────────────────────────
|
||
connect() 建立协议连接
|
||
disconnect() 断开连接
|
||
send_command(cmd) 发送指令(翻译 + 下发)
|
||
get_telemetry() 拉取最新遥测数据
|
||
supported_commands() 返回本协议支持的指令类型列表
|
||
|
||
── 子类可选覆盖 ──────────────────────────────────────────────
|
||
reconnect() 重连策略
|
||
on_raw_message(msg) 处理底层原始消息(用于调试)
|
||
"""
|
||
|
||
# ── PyQt5 信号 ────────────────────────────────────────────────
|
||
connected = pyqtSignal(str) # device_id
|
||
disconnected = pyqtSignal(str) # device_id
|
||
telemetry_ready = pyqtSignal(object) # BaseTelemetry
|
||
command_ack = pyqtSignal(object) # CommandResult
|
||
error_occurred = pyqtSignal(str) # 错误消息
|
||
raw_message = pyqtSignal(object) # 原始协议消息(调试)
|
||
|
||
def __init__(
|
||
self,
|
||
device_id: str,
|
||
protocol: ProtocolType = ProtocolType.SIMULATION,
|
||
config: Dict[str, Any] = None,
|
||
parent: QObject = None,
|
||
):
|
||
super().__init__(parent)
|
||
self.device_id = device_id
|
||
self.protocol = protocol
|
||
self.config: Dict[str, Any] = config or {}
|
||
self._connected: bool = False
|
||
self._telemetry: Optional[BaseTelemetry] = None
|
||
self._connect_time: float = 0.0
|
||
self._msg_count: int = 0
|
||
self.logger = logging.getLogger(
|
||
f"{self.__class__.__name__}[{device_id}]"
|
||
)
|
||
|
||
# ── 连接状态属性 ──────────────────────────────────────────────
|
||
|
||
@property
|
||
def is_connected(self) -> bool:
|
||
return self._connected
|
||
|
||
@property
|
||
def uptime_s(self) -> float:
|
||
return time.time() - self._connect_time if self._connected else 0.0
|
||
|
||
@property
|
||
def message_count(self) -> int:
|
||
return self._msg_count
|
||
|
||
# ── 抽象接口(子类必须实现)──────────────────────────────────
|
||
|
||
@abstractmethod
|
||
def connect(self) -> bool:
|
||
"""
|
||
建立协议连接
|
||
:return: True=连接成功
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def disconnect(self) -> bool:
|
||
"""
|
||
断开协议连接
|
||
:return: True=断开成功
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def send_command(self, cmd: Command) -> CommandResult:
|
||
"""
|
||
发送控制指令
|
||
将上层 Command 翻译为协议原生格式并发送,
|
||
返回执行结果。
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def get_telemetry(self) -> Optional[BaseTelemetry]:
|
||
"""
|
||
拉取最新遥测数据
|
||
将协议原生遥测解析为统一 BaseTelemetry 子类并返回。
|
||
"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def supported_commands(self) -> List[CommandType]:
|
||
"""返回本协议适配器支持的指令类型列表"""
|
||
...
|
||
|
||
@abstractmethod
|
||
def get_connection(self) -> object:
|
||
...
|
||
|
||
# ── 可选覆盖 ──────────────────────────────────────────────────
|
||
|
||
def reconnect(self) -> bool:
|
||
"""重连(默认:先断开再连接)"""
|
||
self.logger.info(f"尝试重连: {self.device_id}")
|
||
self.disconnect()
|
||
time.sleep(0.5)
|
||
return self.connect()
|
||
|
||
def on_raw_message(self, msg: Any) -> None:
|
||
"""处理底层原始消息(调试用,子类可覆盖)"""
|
||
self._msg_count += 1
|
||
self.raw_message.emit(msg)
|
||
|
||
# ── 内部工具 ──────────────────────────────────────────────────
|
||
|
||
def _set_connected(self, state: bool) -> None:
|
||
self._connected = state
|
||
if state:
|
||
self._connect_time = time.time()
|
||
self.connected.emit(self.device_id)
|
||
self.logger.info(f"已连接: {self.device_id} [{self.protocol.value}]")
|
||
else:
|
||
self.disconnected.emit(self.device_id)
|
||
self.logger.info(f"已断开: {self.device_id}")
|
||
|
||
def _check_command_supported(self, cmd: Command) -> Optional[CommandResult]:
|
||
"""检查指令是否被支持,不支持则返回失败结果"""
|
||
if cmd.command_type not in self.supported_commands():
|
||
return CommandResult.fail(
|
||
cmd,
|
||
f"协议 {self.protocol.value} 不支持指令: "
|
||
f"{cmd.command_type.value}"
|
||
)
|
||
return None
|
||
|
||
def __repr__(self) -> str:
|
||
state = "已连接" if self._connected else "未连接"
|
||
return (f"{self.__class__.__name__}("
|
||
f"id={self.device_id}, "
|
||
f"protocol={self.protocol.value}, "
|
||
f"{state})") |