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 rclpyfrom rclpy.node import Nodefrom mavros_msgs.msg import Statefrom mavros_msgs.srv import CommandBool, SetModeclass 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 PoseStampedclass OffboardNode (Node ): def __init__ (self ): self .pose_pub = self .create_publisher( PoseStamped, '/mavros/setpoint_position/local' , 10 ) self .timer = self .create_timer(0.05 , self .publish_setpoint) self .setpoint = PoseStamped() self .setpoint.pose.position.z = 5.0 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 安全机制):
切换至 Offboard 模式前,必须已发送 setpoint 至少 1 秒
进入 Offboard 后,如果 0.5 秒内未收到新的 setpoint,自动退出
解锁必须在 Offboard 模式下进行(或先解锁再切 Offboard)
QGC 或 RC 必须有一者在线,否则解锁被禁止
5.5 速度控制与姿态控制 除了位置控制,MAVROS 还支持速度控制和姿态控制:
1 2 3 4 5 6 7 /mavros/setpoint_velocity/cmd_vel /mavros/setpoint_velocity/cmd_vel_unstamped /mavros/setpoint_attitude/attitude /mavros/setpoint_attitude/thrust
速度控制的 type_mask:
1 2 type_mask = 0b0000111111000111
5.6 获取靶机 GPS 信息(多机场景) 在截击/跟踪任务中,截击机需要实时获取靶机的 GPS 位置来做制导计算。MAVROS 本身只连接一台 飞控,要获取另一台无人机的位置,有两种方案。
方案一:多实例 MAVROS(推荐) 每台飞控对应一个 MAVROS 实例,分别用不同的 namespace 和端口:
1 2 3 4 5 6 7 8 9 ros2 run mavros mavros_node --ros-args \ -r __ns:=/interceptor \ -p fcu_url:=udp://:14540@127.0.0.1:14550 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 rclpyfrom rclpy.node import Nodefrom sensor_msgs.msg import NavSatFixfrom geometry_msgs.msg import TwistStampedfrom mavros_msgs.msg import Stateimport mathclass InterceptorNode (Node ): """截击机:订阅自身+靶机 GPS,计算拦截航向""" def __init__ (self ): super ().__init__('interceptor' ) self .own_gps = None self .own_velocity = None self .own_state = State() self .target_gps = None 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 ) 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" ]:.6 f} , {info["target_lon" ]:.6 f} ) ' f'| Range: {info["distance_2d" ]:.1 f} m ' f'| Bearing: {info["bearing" ]:.1 f} ° ' f'| ΔAlt: {info["delta_alt" ]:.1 f} 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" ) { 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; }); 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_); } rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr own_gps_sub_; rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr target_gps_sub_; rclcpp::TimerBase::SharedPtr timer_; double own_lat_ = NAN, own_lon_ = NAN, own_alt_ = NAN; double target_lat_ = NAN, target_lon_ = NAN, target_alt_ = NAN; };
方案二:MAVLink 转发(单实例) 如果靶机 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 mavlinkfrom pymavlink import mavutilconn = 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 : lat = msg.lat / 1e7 lon = msg.lon / 1e7 alt = msg.alt / 1000.0 print (f'Target: ({lat:.6 f} , {lon:.6 f} , {alt:.1 f} 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 插件生命周期
UAS 节点启动时,根据参数加载插件列表
每个插件订阅感兴趣的 MAVLink 消息 ID
当 Router 收到匹配的 MAVLink 包时,调用对应插件的 handle() 方法
插件将 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 import rclpyimport timefrom rclpy.node import Nodefrom geometry_msgs.msg import PoseStampedfrom mavros_msgs.msg import Statefrom mavros_msgs.srv import CommandBool, SetModeclass 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...' ) 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' : 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 节点。
参考文献
MAVROS ROS 2 README. https://github.com/mavlink/mavros/tree/ros2
PX4 ROS 2 User Guide. https://docs.px4.io/main/en/ros2/user_guide.html
PX4 Offboard Control. https://docs.px4.io/main/en/ros/mavros_offboard_cpp.html
MAVROS ROS Wiki. http://wiki.ros.org/mavros
PX4 ROS 2 Offboard Control Example. https://docs.px4.io/main/en/ros2/offboard_control.html
uXRCE-DDS Documentation. https://docs.px4.io/main/en/middleware/uxrce_dds.html