MAVROS 完全指南(ROS 2 Humble):在无人机上架起 MAVLink 与 ROS 的桥梁

一、MAVROS 是什么,为什么还需要它

MAVROS 是一个将 MAVLink 协议转化(bridge)为 ROS 话题、服务和动作 的软件包。它运行在机载计算机(如 Raspberry Pi、Jetson Orin、Intel NUC)上,一端通过串口或 UDP 连接飞控(PX4 / ArduPilot),另一端向 ROS 2 节点暴露标准化的接口。

2017 年 PX4 v1.14 引入了 uXRCE-DDS 中间件,允许 ROS 2 节点直接通过 DDS 协议读写 PX4 的 uORB 话题,不需要 MAVROS 作为中间层。既然有了更直接的方案,为什么还要用 MAVROS?

场景 推荐方案
ROS 2 + PX4 v1.14+,所有传感器数据都是自研 uXRCE-DDS 直连(延迟更低)
ROS 2 + PX4,但希望和地面站(QGC)共享同一 MAVLink 链路 MAVROS(自带 GCS proxy)
ROS 2 + ArduPilot(不支持 uXRCE-DDS) MAVROS(唯一选择)
ROS 2 + 老版本 PX4(v1.13 及以下) MAVROS
同时运行 ROS 2 节点和传统地面站,需要双向转发 MAVROS(内置 proxy)
使用 MAVSDK 或 pymavlink 已有大量代码,不想迁移 MAVROS(也是 MAVLink 桥)

简单来说:uXRCE-DDS 是 PX4 官方推荐的未来方向,MAVROS 是当前最通用、最成熟的方案,特别是涉及多品牌飞控、GCS 代理、或已有 MAVLink 基础设施的场景。


二、架构:MAVROS 的三个核心组件

MAVROS(ROS 2 版本,代号 ros2 分支)由三个核心节点组成:

图:MAVROS 三层架构:飞控 (FCU) → MAVROS (Router + UAS 插件容器) → ROS 2 用户节点,同时通过 GCS Proxy 连接地面站。

2.1 Router(路由器)

Router 负责管理所有的网络端点。它可以同时连接:

  • 一个或多个飞控(FCU):通过 fcu_url 参数指定
  • 一个地面站(GCS)代理:通过 gcs_url 参数指定,将飞控的 MAVLink 流转发到地面站
  • 一个 UAS 节点:内部挂载所有协议插件

2.2 UAS(无人机系统)

UAS 是一个插件容器,每个插件负责将一类 MAVLink 消息翻译为 ROS 2 话题/服务。每个插件是一个独立的 ROS 2 组件(subnode),可以在运行时动态加载。

核心插件(均在 mavros 包内):

插件 对应的 ROS 话题 功能
sys_status mavros/state 飞控状态:连接、解锁、模式
imu mavros/imu/data IMU 数据(加速度、角速度、方向)
local_position mavros/local_position/pose 局部位置(ENU 坐标系)
global_position mavros/global_position/global GPS 位置(经纬度)
command 服务: mavros/cmd/arming 解锁/锁定、切换模式
setpoint_position mavros/setpoint_position/local 发送位置目标(Offboard)
setpoint_velocity mavros/setpoint_velocity/cmd_vel 发送速度目标(Offboard)
setpoint_attitude mavros/setpoint_attitude/attitude 发送姿态目标(Offboard)

2.3 坐标变换:NED → ENU

这是 MAVROS 做的最关键的隐式工作。飞控内部使用 NED(北东地)坐标系,ROS 使用 ENU(东北天)坐标系。

MAVROS 自动完成转换:

  • 飞机姿态数据:绕 X(ROLL)轴旋转 180°
  • 局部位置数据:绕 X 轴旋转 180° 再绕 Z(YAW)轴旋转 90°

这意味着:

  • mavros/local_position/pose 中的 position.z 是向上为正(ENU)
  • mavros/imu/data 中的加速度 linear_acceleration.z 也是向上为正

对于 Offboard 控制,当向 PX4 发送 SET_POSITION_TARGET_LOCAL_NED 时,MAVROS 会自动将 ENU 转换回 NED。


三、安装与配置(ROS 2 Humble)

3.1 二进制安装(推荐)

1
sudo apt install ros-humble-mavros ros-humble-mavros-extras

安装 GeographicLib 地磁数据集(必须,否则 MAVROS 启动时会报错退出):

1
2
3
wget https://raw.githubusercontent.com/mavlink/mavros/ros2/mavros/scripts/install_geographiclib_datasets.sh
chmod +x install_geographiclib_datasets.sh
./install_geographiclib_datasets.sh

3.2 源码安装

如果需要自定义插件或修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws

# 获取 MAVLink 和 MAVROS 源码
git clone https://github.com/mavlink/mavlink.git -b master src/mavlink
git clone https://github.com/mavlink/mavros.git -b ros2 src/mavros

# 安装依赖
sudo apt install python3-vcstool python3-colcon-common-extensions
rosdep install --from-paths src --ignore-src -y

# 安装 GeographicLib 数据集
./src/mavros/mavros/scripts/install_geographiclib_datasets.sh

# 编译
colcon build --symlink-install
source install/setup.bash

3.3 启动 MAVROS

连接到仿真中的 PX4(Gazebo SITL):

1
ros2 launch mavros px4.launch fcu_url:=udp://:14540@

这里的 14540 是 PX4 SITL 默认向机载计算机发送 MAVLink 的 UDP 端口。

连接到 USB 串口的实体飞控:

1
ros2 launch mavros px4.launch fcu_url:=/dev/ttyUSB0:921600

同时连接飞控和地面站(proxy 模式):

1
ros2 launch mavros px4.launch fcu_url:=udp://:14540@ gcs_url:=udp://@14550

这样地面站通过 UDP 14550 连接到 MAVROS,MAVROS 再将数据转发给飞控。地面站不需要直接连接飞控。

3.4 连接 URL 格式

MAVROS 使用统一的 URL 格式指定连接。支持的协议:

协议 URL 格式 典型用途
串口 /dev/ttyUSB0:57600 实体飞控有线连接
串口(硬件流控) serial-hwfc:///dev/ttyS0:921600 高速串口
UDP udp://bind_host:bind_port@remote_host:remote_port 仿真、WiFi 连接
UDP 广播 udp-b://:14550@:14550 自动发现飞控
TCP 客户端 tcp://192.168.1.1:5760 可靠连接
TCP 服务端 tcp-l://0.0.0.0:5760 等待飞控连接

UDP 默认端口:本机 14555,远端 14550


四、常用话题与消息

启动 MAVROS 后,可以通过 ros2 topic list 查看所有可用话题。以下是无人机开发中最常用的一组:

4.1 飞控状态

1
2
3
4
5
6
7
8
9
10
/mavros/state
- connected: bool # MAVROS 与飞控是否建立心跳
- armed: bool # 是否已解锁
- guided: bool # 是否在 GUIDED/OFFBOARD 模式
- mode: string # 当前飞行模式名称

/mavros/battery
- voltage: float32 # 电池电压 (V)
- current: float32 # 电流 (A)
- remaining: float32 # 剩余电量百分比 [0, 1]

4.2 姿态与位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/mavros/imu/data
- orientation # 四元数姿态
- angular_velocity # 角速度 (rad/s, ENU)
- linear_acceleration # 线加速度 (m/s², ENU)

/mavros/local_position/pose # 局部位置 (PoseStamped, ENU)
- pose.position.x/y/z # 位置 (m),z 向上为正
- pose.orientation # 姿态四元数

/mavros/local_position/odom # 局部里程计 (Odometry)
- pose.pose # 位置+姿态
- twist.twist # 线速度+角速度

/mavros/global_position/global # GPS 位置 (NavSatFix)
- latitude, longitude, altitude # WGS84 经纬度、椭球高

/mavros/global_position/local # 全局→局部转换后的 NED 位置

4.3 传感器与状态

1
2
3
/mavros/altitude              # 各类高度(相对/绝对/气压/地形)
/mavros/rc/in # 遥控器通道值(16 通道)
/mavros/vision_pose/pose # 视觉定位输入(可发布到此话题进行外部位置估计)

五、Offboard 控制实战

Offboard 控制是机载计算机接管无人机飞行的核心功能。以下示例演示如何用 MAVROS 实现解锁→起飞→悬停→降落。

5.1 等待飞控连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import rclpy
from rclpy.node import Node
from mavros_msgs.msg import State
from mavros_msgs.srv import CommandBool, SetMode

class OffboardNode(Node):
def __init__(self):
super().__init__('offboard_node')
self.state_sub = self.create_subscription(
State, '/mavros/state', self.state_cb, 10)
self.arm_service = self.create_client(
CommandBool, '/mavros/cmd/arming')
self.set_mode_service = self.create_client(
SetMode, '/mavros/set_mode')
self.current_state = None

def state_cb(self, msg):
self.current_state = msg
self.get_logger().info(
f'Mode: {msg.mode}, Armed: {msg.armed}, Connected: {msg.connected}')

def wait_for_connection(self):
while rclpy.ok() and (self.current_state is None
or not self.current_state.connected):
rclpy.spin_once(self)
self.get_logger().info('Waiting for FCU connection...')
time.sleep(0.5)

5.2 解锁并切换 Offboard 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
def arm(self):
req = CommandBool.Request()
req.value = True
future = self.arm_service.call_async(req)
rclpy.spin_until_future_complete(self, future)
return future.result()

def set_offboard_mode(self):
req = SetMode.Request()
req.custom_mode = 'OFFBOARD'
future = self.set_mode_service.call_async(req)
rclpy.spin_until_future_complete(self, future)
return future.result()

5.3 发送位置目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from geometry_msgs.msg import PoseStamped

class OffboardNode(Node):
# ... (接上面)
def __init__(self):
# ...
self.pose_pub = self.create_publisher(
PoseStamped, '/mavros/setpoint_position/local', 10)
# 必须先发送一组 setpoint 再切换模式,否则 PX4 拒绝进入 Offboard
self.timer = self.create_timer(0.05, self.publish_setpoint)
self.setpoint = PoseStamped()
self.setpoint.pose.position.z = 5.0 # 目标高度 5 米

def publish_setpoint(self):
self.setpoint.header.stamp = self.get_clock().now().to_msg()
self.setpoint.header.frame_id = 'map'
self.pose_pub.publish(self.setpoint)

5.4 完整启动流程

MAVROS Offboard 有一个重要的时序要求:必须先以 20Hz 以上发送 setpoint,再切换模式。PX4 在收到 Offboard 模式切换请求时,如果最近没有收到 setpoint 数据,会拒绝切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────┐
│ 启动发送 setpoint │ ← 20Hz
│ (目标位置 z=5m) │
└─────────┬───────────┘

┌─────────▼───────────┐
│ 等待心跳连接 │
│ (mavros/state │
│ .connected = true) │
└─────────┬───────────┘

┌─────────▼───────────┐
│ 切换到 OFFBOARD 模式 │ ← SetMode 服务
└─────────┬───────────┘

┌─────────▼───────────┐
│ 解锁 │ ← CommandBool 服务
│ (必须在 Offboard │
│ 模式之后解锁) │
└─────────┬───────────┘

┌─────────▼───────────┐
│ 无人机起飞并悬停 5m │
│ (setpoint 持续发送) │
└─────────────────────┘

关键时序规则(PX4 的 Offboard 安全机制):

  1. 切换至 Offboard 模式前,必须已发送 setpoint 至少 1 秒
  2. 进入 Offboard 后,如果 0.5 秒内未收到新的 setpoint,自动退出
  3. 解锁必须在 Offboard 模式下进行(或先解锁再切 Offboard)
  4. QGC 或 RC 必须有一者在线,否则解锁被禁止

5.5 速度控制与姿态控制

除了位置控制,MAVROS 还支持速度控制和姿态控制:

1
2
3
4
5
6
7
# 速度控制话题
/mavros/setpoint_velocity/cmd_vel # TwistStamped: 线速度+角速度
/mavros/setpoint_velocity/cmd_vel_unstamped # Twist: 无时间戳

# 姿态控制话题
/mavros/setpoint_attitude/attitude # PoseStamped: 目标姿态四元数
/mavros/setpoint_attitude/thrust # 推力 (0~1 归一化)

速度控制的 type_mask:

1
2
# 忽略位置和加速度,只使用速度
type_mask = 0b0000111111000111

5.6 获取靶机 GPS 信息(多机场景)

在截击/跟踪任务中,截击机需要实时获取靶机的 GPS 位置来做制导计算。MAVROS 本身只连接一台飞控,要获取另一台无人机的位置,有两种方案。

方案一:多实例 MAVROS(推荐)

每台飞控对应一个 MAVROS 实例,分别用不同的 namespace 和端口:

1
2
3
4
5
6
7
8
9
# 终端1:截击机 MAVROS(UDP 14540)
ros2 run mavros mavros_node --ros-args \
-r __ns:=/interceptor \
-p fcu_url:=udp://:14540@127.0.0.1:14550

# 终端2:靶机 MAVROS(UDP 14541)
ros2 run mavros mavros_node --ros-args \
-r __ns:=/target \
-p fcu_url:=udp://:14541@127.0.0.1:14560

然后在截击机控制节点中同时订阅两个 namespace:

Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import NavSatFix
from geometry_msgs.msg import TwistStamped
from mavros_msgs.msg import State
import math

class InterceptorNode(Node):
"""截击机:订阅自身+靶机 GPS,计算拦截航向"""

def __init__(self):
super().__init__('interceptor')

# ── 自身状态 ──
self.own_gps = None # (lat, lon, alt)
self.own_velocity = None # (vx, vy, vz) NED
self.own_state = State()

# ── 靶机状态 ──
self.target_gps = None

# 订阅截击机自身 GPS(namespace /interceptor)
self.create_subscription(
NavSatFix, '/interceptor/mavros/global_position/global',
self.own_gps_cb, 10)
self.create_subscription(
TwistStamped, '/interceptor/mavros/setpoint_velocity/velocity',
self.own_vel_cb, 10)
self.create_subscription(
State, '/interceptor/mavros/state',
self.own_state_cb, 10)

# 订阅靶机 GPS(namespace /target)
self.create_subscription(
NavSatFix, '/target/mavros/global_position/global',
self.target_gps_cb, 10)

self.get_logger().info('Interceptor ready, waiting for both drones...')

def own_gps_cb(self, msg: NavSatFix):
self.own_gps = (msg.latitude, msg.longitude, msg.altitude)

def own_vel_cb(self, msg: TwistStamped):
self.own_velocity = (msg.twist.linear.x,
msg.twist.linear.y,
msg.twist.linear.z)

def own_state_cb(self, msg: State):
self.own_state = msg

def target_gps_cb(self, msg: NavSatFix):
self.target_gps = (msg.latitude, msg.longitude, msg.altitude)

def compute_intercept_heading(self):
"""计算从截击机到靶机的方位角和距离"""
if self.own_gps is None or self.target_gps is None:
return None

lat1, lon1, alt1 = self.own_gps
lat2, lon2, alt2 = self.target_gps

# 简化平面近似(短距)
dlat = (lat2 - lat1) * 111320.0 # 纬度差 → 米
dlon = (lon2 - lon1) * 111320.0 * math.cos(math.radians(lat1))
dalt = alt2 - alt1

distance_2d = math.sqrt(dlat**2 + dlon**2)
bearing = math.degrees(math.atan2(dlon, dlat)) % 360

return {
'distance_2d': distance_2d,
'bearing': bearing,
'delta_alt': dalt,
'target_lat': lat2,
'target_lon': lon2
}


def main():
rclpy.init()
node = InterceptorNode()

# 等待两架飞机都连接
while rclpy.ok():
rclpy.spin_once(node, timeout_sec=0.5)
if node.own_state.connected and node.target_gps is not None:
node.get_logger().info('Both drones connected!')
break

# 主循环:每秒打印拦截信息
rate = node.create_rate(1.0)
while rclpy.ok():
rclpy.spin_once(node, timeout_sec=0.1)
info = node.compute_intercept_heading()
if info:
node.get_logger().info(
f'Target: ({info["target_lat"]:.6f}, {info["target_lon"]:.6f}) '
f'| Range: {info["distance_2d"]:.1f}m '
f'| Bearing: {info["bearing"]:.1f}° '
f'| ΔAlt: {info["delta_alt"]:.1f}m')
rate.sleep()

node.destroy_node()
rclpy.shutdown()

C++(核心回调+计算,不含 main):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/nav_sat_fix.hpp>
#include <geometry_msgs/msg/twist_stamped.hpp>
#include <mavros_msgs/msg/state.hpp>
#include <cmath>

class InterceptorNode : public rclcpp::Node {
public:
InterceptorNode() : Node("interceptor") {
// 自身 GPS
own_gps_sub_ = create_subscription<sensor_msgs::msg::NavSatFix>(
"/interceptor/mavros/global_position/global", 10,
[this](const sensor_msgs::msg::NavSatFix::SharedPtr msg) {
own_lat_ = msg->latitude;
own_lon_ = msg->longitude;
own_alt_ = msg->altitude;
});

// 靶机 GPS
target_gps_sub_ = create_subscription<sensor_msgs::msg::NavSatFix>(
"/target/mavros/global_position/global", 10,
[this](const sensor_msgs::msg::NavSatFix::SharedPtr msg) {
target_lat_ = msg->latitude;
target_lon_ = msg->longitude;
target_alt_ = msg->altitude;
});

timer_ = create_wall_timer(std::chrono::seconds(1),
std::bind(&InterceptorNode::compute_and_log, this));
}

private:
void compute_and_log() {
if (!target_valid()) return;

double dlat = (target_lat_ - own_lat_) * 111320.0;
double dlon = (target_lon_ - own_lon_) * 111320.0
* std::cos(own_lat_ * M_PI / 180.0);
double dist = std::sqrt(dlat * dlat + dlon * dlon);
double bearing = std::atan2(dlon, dlat) * 180.0 / M_PI;
if (bearing < 0) bearing += 360.0;

RCLCPP_INFO(get_logger(),
"Target range: %.1fm | bearing: %.1f° | Δalt: %.1fm",
dist, bearing, target_alt_ - own_alt_);
}

bool target_valid() {
return !std::isnan(target_lat_) && !std::isnan(own_lat_);
}

// Subscribers
rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr own_gps_sub_;
rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr target_gps_sub_;
rclcpp::TimerBase::SharedPtr timer_;

// State
double own_lat_ = NAN, own_lon_ = NAN, own_alt_ = NAN;
double target_lat_ = NAN, target_lon_ = NAN, target_alt_ = NAN;
};

如果靶机 GPS 通过 MAVLink 多机网络(system ID 不同)到达同一 MAVROS 实例,可以用 pymavlink 直接解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pymavlink.dialects.v20.common as mavlink
# MAVROS 默认不转发其他 system ID 的 GLOBAL_POSITION_INT
# 需要在 MAVROS 参数中设置:
# ros2 param set /mavros sysid_mygcs 1
# ros2 param set /mavros target_system_id 0 # 0 = 接收全部

# 然后在 pymavlink 层面监听:
from pymavlink import mavutil

conn = mavutil.mavlink_connection('udpin:127.0.0.1:14550')
while True:
msg = conn.recv_match(type='GLOBAL_POSITION_INT', blocking=True, timeout=5)
if msg and msg.get_srcSystem() == 2: # 靶机 system ID = 2
lat = msg.lat / 1e7 # MAVLink 编码为 1e7 度
lon = msg.lon / 1e7
alt = msg.alt / 1000.0 # 毫米 → 米
print(f'Target: ({lat:.6f}, {lon:.6f}, {alt:.1f}m)')

两种方案对比

多实例 MAVROS MAVLink 转发
代码复杂度 低(ROS topic 原生订阅) 中(需处理 MAVLink 编解码)
延迟 低(无额外转发) 中(多一跳 MAVLink 路由)
多机扩展 每个无人机一个 namespace 单实例,靠 system ID 区分
适用场景 2-3 机小规模 大规模集群,或仅需读取 GPS

⚠️ 注意:以上示例中的经纬度→距离转换用了简化的平面近似(111320 m/deg)。实际工程中若两机距离超过几十公里,应使用 Haversine 公式或 GeographicLib 进行精确的大地线计算。


六、MAVROS 的插件系统

MAVROS 的插件体系是其核心设计之一。每个插件是一个独立的类,继承自 mavros::plugin::PluginBase,注册到 UAS 节点中。

6.1 插件生命周期

  1. UAS 节点启动时,根据参数加载插件列表
  2. 每个插件订阅感兴趣的 MAVLink 消息 ID
  3. 当 Router 收到匹配的 MAVLink 包时,调用对应插件的 handle() 方法
  4. 插件将 MAVLink 消息转换为 ROS 话题发布,或将 ROS 订阅转换为 MAVLink 消息发送

6.2 常用插件速查

mavros 包内插件:

插件文件名 功能
sys_status 心跳、模式、解锁状态
imu IMU 数据发布
local_position 局部位置/里程计
global_position GPS 位置、全球位置
command 解锁/模式切换/命令
setpoint_raw 原始 setpoint(位置/速度/加速度/姿态)
setpoint_position 位置设定点(简化版)
setpoint_attitude 姿态设定点
waypoint 航点上传/下载/执行

mavros_extras(额外插件包):

插件文件名 功能
px4flow PX4Flow 光流传感器
distance_sensor 激光雷达/超声波测距
landing_target 着陆目标检测
fake_gps 模拟 GPS(仿真用)
trajectory 轨迹执行

七、MAVROS 与 uXRCE-DDS 的对比

这是 ROS 2 + PX4 开发中最常遇到的问题:到底用 MAVROS 还是直连 DDS?

维度 MAVROS uXRCE-DDS 直连
通信路径 ROS 2 ↔ MAVROS ↔ MAVLink ↔ PX4 ROS 2 ↔ DDS ↔ uXRCE Agent ↔ uORB ↔ PX4
延迟 中等(MAVLink 序列化+桥接) 低(直接 uORB 读写)
飞控兼容性 PX4 + ArduPilot + 其他 MAVLink 飞控 PX4 v1.14+
GCS 代理 内置(通过 gcs_url 需要额外工具(mavlink-router 等)
Offboard 频率上限 ~100Hz(MAVLink 串口瓶颈) ~500Hz(DDS 本地共享内存)
话题数量 30+ 个预设话题 可访问所有 uORB 话题
学习曲线 只需熟悉 MAVROS 话题名 需要理解 uORB、DDS、QoS
代码生成 不需要 需要 px4_msgs 消息定义
ArduPilot 支持 ✅ 完整 ❌ 不支持

什么时候选哪个

选 MAVROS 当:

  • 需要同时支持地面站(QGC)和 ROS 节点
  • 飞控是 ArduPilot 或老版本 PX4
  • 已有基于 MAVLink 的代码基础设施
  • 需要快速上手,不想学 uORB 和 DDS QoS

选 uXRCE-DDS 当:

  • 纯 PX4 v1.14+ 环境
  • 需要高频率 Offboard 控制(>100Hz)
  • 访问自定义 uORB 话题
  • 延迟敏感的应用(敏捷飞行、高速截击)

八、常见问题

8.1 MAVROS 启动后 connected 始终为 false

1
2
3
4
5
6
7
8
# 检查 MAVROS 日志
ros2 run mavros mavros_node --ros-args -p fcu_url:=udp://:14540@

# 常见原因:
# 1. 端口号不匹配(仿真中 PX4 默认 14540,但可能需要手动确认)
# 2. 飞控未启动(SITL 未运行)
# 3. 串口权限问题(需要将用户加入 dialout 组)
sudo usermod -a -G dialout $USER

8.2 解锁失败

1
2
3
4
5
6
7
8
# 检查条件
ros2 topic echo /mavros/state

# 常见原因:
# 1. QGC 未连接(PX4 默认需要 GCS 连接才能解锁)
# 2. 预检失败(EKF 未收敛、传感器未校准)
# 3. 未在 Offboard/Guided 模式
# 4. Offboard 模式下未持续发送 setpoint

8.3 Offboard 模式丢失

如果无人机自动退出了 Offboard 模式,最常见的原因是 setpoint 发送中断。检查:

  • setpoint 发布频率是否 >= 20Hz
  • 网络是否稳定(UDP 丢包会导致 setpoint 不连续)
  • 机载计算机性能是否足够(高负载可能导致 ROS 节点调度延迟)

8.4 GeographicLib 数据集错误

1
ERROR: The geoid dataset is required for altitude conversion.

运行安装脚本:

1
ros2 run mavros install_geographiclib_datasets.sh

九、完整示例:起飞→悬停→降落(Python)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python3
import rclpy
import time
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped
from mavros_msgs.msg import State
from mavros_msgs.srv import CommandBool, SetMode

class OffboardControl(Node):
def __init__(self):
super().__init__('offboard_control')

# 状态订阅
self.state_sub = self.create_subscription(
State, '/mavros/state', self.state_cb, 10)

# 服务客户端
self.arm_client = self.create_client(
CommandBool, '/mavros/cmd/arming')
self.mode_client = self.create_client(
SetMode, '/mavros/set_mode')

# 位置发布
self.pose_pub = self.create_publisher(
PoseStamped, '/mavros/setpoint_position/local', 10)

self.state = None
self.arming_done = False

# 等待服务
while not self.arm_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Waiting for arming service...')

# 立即开始发送 setpoint
self.timer = self.create_timer(0.05, self.send_setpoint)
self.setpoint = PoseStamped()
self.setpoint.pose.position.z = 5.0

# 主循环
self.main_timer = self.create_timer(0.1, self.main_loop)
self.phase = 'init'

def state_cb(self, msg):
self.state = msg

def send_setpoint(self):
self.setpoint.header.stamp = self.get_clock().now().to_msg()
self.setpoint.header.frame_id = 'map'
self.pose_pub.publish(self.setpoint)

def main_loop(self):
if self.state is None:
return

if self.phase == 'init':
if self.state.connected:
self.get_logger().info('FCU connected')
self.phase = 'set_offboard'

elif self.phase == 'set_offboard':
if not self.state.guided:
req = SetMode.Request()
req.custom_mode = 'OFFBOARD'
self.mode_client.call_async(req)
self.get_logger().info('Setting OFFBOARD mode...')
else:
self.get_logger().info('OFFBOARD mode active')
self.phase = 'arm'

elif self.phase == 'arm':
if not self.state.armed:
req = CommandBool.Request()
req.value = True
self.arm_client.call_async(req)
self.get_logger().info('Arming...')
else:
self.get_logger().info('Armed! Taking off to 5m')
self.phase = 'flying'

elif self.phase == 'flying':
# 持续悬停,setpoint 由定时器发送
pass

def main():
rclpy.init()
node = OffboardControl()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

十、总结

MAVROS 是连接 ROS 2 和 MAVLink 飞控的成熟桥梁。尽管 PX4 正在推动 uXRCE-DDS 直连方案,MAVROS 凭借其飞控无关性、内置 GCS 代理、成熟的插件生态,在 ROS 2 Humble 时代仍然是最通用和最常用的无人机 ROS 通信方案。

对于刚入门 ROS 2 + 无人机开发的场景,MAVROS 提供的 mavros_msgs 消息定义比直接处理 uORB 话题直观得多。而从 MAVROS 迁移到 uXRCE-DDS 也不冲突——两者可以同时运行在同一台机载计算机上,分别服务于不同的 ROS 节点。


参考文献

  1. MAVROS ROS 2 README. https://github.com/mavlink/mavros/tree/ros2
  2. PX4 ROS 2 User Guide. https://docs.px4.io/main/en/ros2/user_guide.html
  3. PX4 Offboard Control. https://docs.px4.io/main/en/ros/mavros_offboard_cpp.html
  4. MAVROS ROS Wiki. http://wiki.ros.org/mavros
  5. PX4 ROS 2 Offboard Control Example. https://docs.px4.io/main/en/ros2/offboard_control.html
  6. uXRCE-DDS Documentation. https://docs.px4.io/main/en/middleware/uxrce_dds.html