base_agent/uas/uas_control/protocols/mavlink_adapter.py

334 lines
12 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- 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())