334 lines
12 KiB
Python
334 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
协议适配器实现 — 完整版
|
||
MAVLinkAdapter / ROSAdapter / SimulationAdapter
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
import random
|
||
import time
|
||
from typing import Dict, List, Optional
|
||
|
||
from pymavlink import mavutil
|
||
|
||
from uas.uas_control.base_adapter import BaseProtocolAdapter, ProtocolType
|
||
from uas.uas_control.command import Command, CommandResult, CommandType
|
||
from uas.uas_control.telemetry import (
|
||
UAVTelemetry, ConnectionState
|
||
)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# MAVLink 适配器
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class MAVLinkAdapter(BaseProtocolAdapter):
|
||
"""
|
||
MAVLink v1/v2 协议适配器(ArduPilot / PX4)
|
||
|
||
config 字段:
|
||
connection_string : "udp:127.0.0.1:14550" | "tcp:host:port" | "/dev/ttyUSB0:57600"
|
||
system_id : 地面站 sysid(默认 255)
|
||
component_id : 地面站 compid(默认 1)
|
||
source_system : 目标飞控 sysid(默认 1)
|
||
dialect : "ardupilotmega" | "common"(默认 ardupilotmega)
|
||
|
||
实际部署需安装:pip install pymavlink
|
||
本实现在无 pymavlink 时自动降级为模拟模式。
|
||
"""
|
||
|
||
# CommandType → MAVLink CMD ID
|
||
_CMD_MAP: Dict[CommandType, int] = {
|
||
CommandType.ARM: 400, # MAV_CMD_COMPONENT_ARM_DISARM
|
||
CommandType.DISARM: 400,
|
||
CommandType.TAKEOFF: 22, # MAV_CMD_NAV_TAKEOFF
|
||
CommandType.LAND: 21, # MAV_CMD_NAV_LAND
|
||
CommandType.RTL: 20, # MAV_CMD_NAV_RETURN_TO_LAUNCH
|
||
CommandType.GOTO: 16, # MAV_CMD_NAV_WAYPOINT
|
||
CommandType.SET_MODE: mavutil.mavlink.MAV_CMD_DO_SET_MODE, # MAV_CMD_DO_SET_MODE
|
||
CommandType.LOITER: 17, # MAV_CMD_NAV_LOITER_UNLIM
|
||
CommandType.EMERGENCY_STOP: 400,
|
||
CommandType.CAMERA_SHOOT: 203, # MAV_CMD_DO_DIGICAM_CONTROL
|
||
CommandType.GIMBAL_CONTROL: 205, # MAV_CMD_DO_MOUNT_CONTROL
|
||
CommandType.PAYLOAD_ON: 2800,
|
||
CommandType.PAYLOAD_OFF: 2800,
|
||
CommandType.SET_VELOCITY: -1, # 使用 SET_POSITION_TARGET
|
||
}
|
||
|
||
_MODE_MAP: Dict[str, float] = {
|
||
"MANUAL": 0.0,
|
||
"GUIDE": 4.0,
|
||
"OFFBOARD": 6.0,
|
||
"LOITER": 0.0,
|
||
"AUTO": 0.0
|
||
}
|
||
|
||
def __init__(
|
||
self,
|
||
device_id: str,
|
||
connection_string: str = "udp:127.0.0.1:14550",
|
||
system_id: int = 255,
|
||
component_id: int = 1,
|
||
source_system: int = 1,
|
||
dialect: str = "ardupilotmega",
|
||
parent = None,
|
||
):
|
||
config = dict(
|
||
connection_string = connection_string,
|
||
system_id = system_id,
|
||
component_id = component_id,
|
||
source_system = source_system,
|
||
dialect = dialect,
|
||
)
|
||
super().__init__(device_id, ProtocolType.MAVLINK, config, parent)
|
||
|
||
self._mav = None # pymavlink MAVLink 实例
|
||
self._vehicle = None # 连接句柄
|
||
self._last_hb_time = 0.0
|
||
self._sim_mode = False # pymavlink 不可用时降级仿真
|
||
|
||
# 尝试导入 pymavlink
|
||
try:
|
||
from pymavlink import mavutil
|
||
self._mavutil = mavutil
|
||
self.logger.info("pymavlink 已加载")
|
||
except ImportError:
|
||
self._mavutil = None
|
||
self._sim_mode = True
|
||
self.logger.warning(
|
||
"pymavlink 未安装,MAVLinkAdapter 以模拟模式运行"
|
||
)
|
||
|
||
# ── 连接管理 ──────────────────────────────────────────────────
|
||
|
||
def connect(self) -> bool:
|
||
cs = self.config["connection_string"]
|
||
if self._sim_mode or self._mavutil is None:
|
||
self.logger.info(f"[模拟] MAVLink 连接: {cs}")
|
||
self._set_connected(True)
|
||
return True
|
||
try:
|
||
self._vehicle = self._mavutil.mavlink_connection(
|
||
cs,
|
||
source_system = self.config["system_id"],
|
||
source_component = self.config["component_id"],
|
||
dialect = self.config["dialect"],
|
||
)
|
||
# 等待心跳(超时 5s)
|
||
msg = self._vehicle.wait_heartbeat(timeout=5)
|
||
if msg is None:
|
||
raise
|
||
self._set_connected(True)
|
||
self.logger.info(
|
||
f"MAVLink 心跳收到: sysid={self._vehicle.target_system} heartbeat={msg} "
|
||
f"compid={self._vehicle.target_component}"
|
||
)
|
||
return True
|
||
except Exception as e:
|
||
self.logger.error(f"MAVLink 连接失败: {e}")
|
||
self.error_occurred.emit(str(e))
|
||
return False
|
||
|
||
def disconnect(self) -> bool:
|
||
if self._vehicle and not self._sim_mode:
|
||
try:
|
||
self._vehicle.close()
|
||
except Exception:
|
||
pass
|
||
self._vehicle = None
|
||
self._set_connected(False)
|
||
return True
|
||
|
||
def get_connection(self) -> object:
|
||
return self._vehicle
|
||
|
||
# ── 指令发送 ──────────────────────────────────────────────────
|
||
|
||
def send_command(self, cmd: Command) -> CommandResult:
|
||
unsupported = self._check_command_supported(cmd)
|
||
if unsupported:
|
||
return unsupported
|
||
|
||
if self._sim_mode:
|
||
return self._sim_send(cmd)
|
||
|
||
try:
|
||
return self._mav_send(cmd)
|
||
except Exception as e:
|
||
return CommandResult.fail(cmd, f"MAVLink 发送异常: {e}")
|
||
|
||
def _mav_send(self, cmd: Command) -> CommandResult:
|
||
"""真实 MAVLink 发送"""
|
||
ct = cmd.command_type
|
||
p = cmd.params
|
||
|
||
# SET_VELOCITY 使用 SET_POSITION_TARGET_LOCAL_NED
|
||
if ct == CommandType.SET_VELOCITY:
|
||
self._vehicle.mav.set_position_target_local_ned_send(
|
||
0, # time_boot_ms
|
||
self._vehicle.target_system,
|
||
self._vehicle.target_component,
|
||
mavutil_frame := 1, # MAV_FRAME_LOCAL_NED
|
||
0b0000111111000111, # type_mask: 仅速度有效
|
||
0, 0, 0, # x y z(忽略)
|
||
p.get("vx", 0), p.get("vy", 0), p.get("vz", 0),
|
||
0, 0, 0, # ax ay az(忽略)
|
||
0, 0, # yaw yaw_rate(忽略)
|
||
)
|
||
return CommandResult.ok(cmd, "SET_VELOCITY 已发送")
|
||
|
||
# 通用 command_long
|
||
cmd_id = self._CMD_MAP.get(ct, -1)
|
||
if cmd_id < 0:
|
||
return CommandResult.fail(cmd, f"无 MAVLink 映射: {ct.value}")
|
||
|
||
param1 = param2 = param3 = param4 = 0.0
|
||
param5 = param6 = param7 = 0.0
|
||
|
||
if ct == CommandType.ARM:
|
||
param1 = 1.0
|
||
elif ct == CommandType.DISARM:
|
||
param1 = 0.0
|
||
elif ct == CommandType.EMERGENCY_STOP:
|
||
param1 = 0.0; param2 = 21196.0 # force disarm magic
|
||
elif ct == CommandType.TAKEOFF:
|
||
param7 = p.get("altitude", 10.0)
|
||
elif ct == CommandType.GOTO:
|
||
param5 = p.get("lat", 0.0)
|
||
param6 = p.get("lon", 0.0)
|
||
param7 = p.get("alt", 0.0)
|
||
elif ct == CommandType.SET_MODE:
|
||
param1 = 1.0 # MAV_MODE_FLAG_CUSTOM_MODE_ENABLED
|
||
param2 = self._MODE_MAP.get(p['mode'], 0)
|
||
|
||
self._vehicle.mav.command_long_send(
|
||
self._vehicle.target_system,
|
||
self._vehicle.target_component,
|
||
cmd_id, 0,
|
||
param1, param2, param3, param4,
|
||
param5, param6, param7,
|
||
)
|
||
|
||
# 等待 ACK(超时 cmd.timeout_s)
|
||
ack = self._vehicle.recv_match(
|
||
type="COMMAND_ACK", blocking=True,
|
||
timeout=cmd.timeout_s
|
||
)
|
||
if ack is None:
|
||
return CommandResult.timeout(cmd)
|
||
if ack.result == 0: # MAV_RESULT_ACCEPTED
|
||
return CommandResult.ok(cmd, f"MAVLink ACK: ACCEPTED")
|
||
return CommandResult.fail(
|
||
cmd, f"MAVLink ACK: result={ack.result}"
|
||
)
|
||
|
||
def _sim_send(self, cmd: Command) -> CommandResult:
|
||
"""模拟发送(无真实硬件时)"""
|
||
time.sleep(random.uniform(0.01, 0.05))
|
||
self.logger.debug(f"[模拟] MAVLink 发送: {cmd.command_type.value}")
|
||
return CommandResult.ok(
|
||
cmd, f"[模拟] MAVLink {cmd.command_type.value} 已执行"
|
||
)
|
||
|
||
# ── 遥测拉取 ──────────────────────────────────────────────────
|
||
|
||
def get_telemetry(self) -> Optional[UAVTelemetry]:
|
||
if self._sim_mode:
|
||
return self._sim_telemetry()
|
||
if not self._vehicle:
|
||
return None
|
||
try:
|
||
return self._parse_mavlink_telemetry()
|
||
except Exception as e:
|
||
self.logger.error(f"遥测解析异常: {e}")
|
||
return None
|
||
|
||
def _parse_mavlink_telemetry(self) -> UAVTelemetry:
|
||
"""解析 MAVLink 消息为 UAVTelemetry"""
|
||
t = UAVTelemetry(
|
||
device_id = self.device_id,
|
||
timestamp = time.time(),
|
||
connection_state = ConnectionState.CONNECTED.value,
|
||
)
|
||
# GLOBAL_POSITION_INT
|
||
msg = self._vehicle.recv_match(
|
||
type="GLOBAL_POSITION_INT", blocking=False
|
||
)
|
||
if msg:
|
||
t.latitude = msg.lat / 1e7
|
||
t.longitude = msg.lon / 1e7
|
||
t.altitude = msg.alt / 1000.0
|
||
t.relative_altitude = msg.relative_alt / 1000.0
|
||
t.vx = msg.vx / 100.0
|
||
t.vy = msg.vy / 100.0
|
||
t.vz = msg.vz / 100.0
|
||
t.heading = msg.hdg / 100.0
|
||
t.ground_speed = math.hypot(t.vx, t.vy)
|
||
|
||
# ATTITUDE
|
||
msg = self._vehicle.recv_match(
|
||
type="ATTITUDE", blocking=False
|
||
)
|
||
if msg:
|
||
t.roll = math.degrees(msg.roll)
|
||
t.pitch = math.degrees(msg.pitch)
|
||
|
||
# SYS_STATUS
|
||
msg = self._vehicle.recv_match(
|
||
type="SYS_STATUS", blocking=False
|
||
)
|
||
if msg:
|
||
t.battery_voltage = msg.voltage_battery / 1000.0
|
||
t.battery_current = msg.current_battery / 100.0
|
||
t.battery_percent = msg.battery_remaining
|
||
|
||
# GPS_RAW_INT
|
||
msg = self._vehicle.recv_match(
|
||
type="GPS_RAW_INT", blocking=False
|
||
)
|
||
if msg:
|
||
t.gps_fix_type = msg.fix_type
|
||
t.satellites_visible = msg.satellites_visible
|
||
t.hdop = msg.eph / 100.0
|
||
t.vdop = msg.epv / 100.0
|
||
|
||
# HEARTBEAT
|
||
msg = self._vehicle.recv_match(
|
||
type="HEARTBEAT", blocking=False
|
||
)
|
||
if msg:
|
||
t.armed = bool(
|
||
msg.base_mode & 0x80 # MAV_MODE_FLAG_SAFETY_ARMED
|
||
)
|
||
|
||
return t
|
||
|
||
def _sim_telemetry(self) -> UAVTelemetry:
|
||
"""生成模拟遥测数据"""
|
||
t = time.time()
|
||
return UAVTelemetry(
|
||
device_id = self.device_id,
|
||
timestamp = t,
|
||
connection_state = ConnectionState.CONNECTED.value,
|
||
latitude = 39.9 + math.sin(t * 0.01) * 0.01,
|
||
longitude = 116.4 + math.cos(t * 0.01) * 0.01,
|
||
altitude = 100.0 + math.sin(t * 0.05) * 5,
|
||
relative_altitude = 100.0,
|
||
heading = (t * 2) % 360,
|
||
ground_speed = 15.0 + random.uniform(-1, 1),
|
||
battery_voltage = 22.4 - t * 0.0001,
|
||
battery_percent = max(0, int(100 - t * 0.01)),
|
||
gps_fix_type = 3,
|
||
satellites_visible = 14,
|
||
armed = True,
|
||
flight_mode = "AUTO",
|
||
signal_strength = 85,
|
||
)
|
||
|
||
def supported_commands(self) -> List[CommandType]:
|
||
return list(self._CMD_MAP.keys())
|
||
|
||
|