很多人第一次听说“用ROS2控制PX4飞个圆”这个需求时,心里想的是“写个坐标,发进去就完事了”。等真正把环境装完、消息发出去、无人机在Gazebo里冲出去炸机之后,才知道这条路的水有多深。这篇文章不是帮你把代码抄一遍,而是把我自己踩过的坑、验证过的流程、以及每个关键决策背后的原因,完整地讲给你听。
这篇文章适合两类人:一类是刚接触ROS2和PX4,想通过一个具体任务把整个开发链路跑通的新手;另一类是已经在用MAVLink或MAVSDK,想切到ROS2生态,但不太清楚Offboard模式在ROS2消息层应该怎么发的人。文中的所有流程都在Ubuntu 22.04 + ROS2 Humble + PX4 v1.14.3 + Gazebo下实际跑过,你可以直接照着复现。
1. 项目全景:ROS2、PX4、Gazebo三兄弟的协作关系
1.1 为什么偏要用这个组合
你可能想问:控制无人机画圆的方案很多,用MAVSDK写个Python脚本也行,用QGC手动规划航线也行,为什么非要用ROS2 + C++ + PX4 + Gazebo这一套?
因为这几样东西组合起来,恰好覆盖了从“算法设计”到“真机部署”的完整链路。ROS2提供分布式通信和丰富的感知、规划库,C++保证实时性,PX4提供经过验证的飞行控制栈,Gazebo则让你在不用炸真机的前提下验证算法。尤其是落地到实际项目里,视觉避障、多机编队、室内定位这些需求,基本都跑在ROS2生态上。用MAVSDK脚本控制一次飞行不难,但要把视觉、路径规划、集群协同这些模块接到一起,就成了“把非ROS系统硬塞进ROS架构”的别扭事。
另外,PX4的Offboard模式本身就是为外部计算机设计的高级控制接口。配合uXRCE-DDS协议,ROS2可以直接以话题的形式向飞控发送期望位置、速度、姿态,精度和实时性都比传统MAVLink串口转发好不少。可以说,这个组合就是当前开源无人机二次开发的事实标准。
1.2 通信链路拆解:从C++节点到飞控的“数据管道”
先看整体数据流向,我用文字画一条链路:
ROS2 C++节点 │ ├─ 发布 /fmu/in/offboard_control_mode (OffboardControlMode 消息) └─ 发布 /fmu/in/trajectory_setpoint (TrajectorySetpoint 消息) │ ▼ Micro XRCE-DDS Agent(运行在Ubuntu宿主机上) │ ▼ uXRCE-DDS Client(运行在PX4固件内部) │ ▼ PX4 Offboard模式状态机 → 位置/姿态控制器 → 混控器 → 电机输出如果我们默认PX4是跑在Gazebo仿真里的SITL(Software In The Loop)模式,那么PX4固件本身就是一个Linux进程,它内部会启动一个uXRCE-DDS Client,默认通过UDP连接宿主机的8888端口。宿主机这边运行一个Micro XRCE-DDS Agent,负责把PX4发出的uORB消息转成标准DDS/ROS2话题,也把ROS2发来的话题转回uORB消息。
这里有个容易懵的地方:ROS2节点到底是在跟谁说话?实际上ROS2节点从来没有直接“接触”PX4,它只是往/fmu/in/前缀的话题上发消息。Micro XRCE-DDS Agent收到后,会把消息序列化,通过网络传给PX4内部的Client,Client再把它翻译成PX4能读懂的uORB消息。整个流程对ROS2节点来说是透明的,你只要按照PX4定义的消息格式发就行。
还有一个关键点:PX4的Offboard是一个“模式”,不是一条指令。你发出的所有期望值,都只是给Offboard状态机参考的输入。PX4要真正进入Offboard模式,必须主动发送MAV_CMD_DO_SET_MODE(或通过QGC、RC切换)执行模式切换动作。这一点很多人会忽略,结果就是消息发了一堆,但无人机纹丝不动,原因就是正反馈——模式根本没切进去。
下面这张表可以帮你快速梳理各层的职责:
| 层级 | 组件 | 协议/机制 | 主要职责 |
|---|---|---|---|
| 决策层 | 我们的C++节点 | ROS2/DDS话题 | 生成圆轨迹期望值,发布控制指令 |
| 桥接层 | Micro XRCE-DDS Agent | XRCE-DDS over UDP/TCP | 在Linux宿主机和PX4之间转发消息 |
| 飞控内部 | uXRCE-DDS Client | uORB | 将外部消息映射为PX4内部消息 |
| 执行层 | PX4控制器 | 位置控制、姿态控制 | 计算电机指令,驱动仿真模型 |
这张表也对排错很有用——消息发不出去,先判断节点有没有发布;话题能看到但飞控不响应,那大概率是Agent和Client之间的桥接问题;如果飞控接收了但飞行动作怪异,那是坐标系或控制参数的问题。
2. 环境搭建:一套能少折腾一个月的基础配置
2.1 版本搭配建议
环境搭建是整个流程里耗时最长、最容易放弃的一步。版本不匹配导致编译失败,或者启动仿真时界面疯狂闪退,这些问题十有八九是版本搭配不当。我自己实测下来比较稳的组合是这样的:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| Ubuntu | 22.04 LTS | 环境最成熟,坑最少 |
| ROS2 | Humble Hawksbill | 和Ubuntu 22.04全兼容 |
| PX4源码 | v1.14.3 tag | 对ROS2/XRCE-DDS支持完善 |
| Gazebo Sim | gz-garden (新Gazebo) | PX4 v1.14默认使用 |
| Micro XRCE-DDS Agent | 最新release | 用于宿主机桥接 |
| px4_msgs | 与PX4 v1.14.3配套 | ROS2侧消息定义 |
如果你看到网上很多教程用Gazebo Classic 11,那是因为PX4 v1.13之前的老教程。v1.14.3依然保留了gazebo-classic的启动方式,但新版世界模型和仿真插件做得更好,直接用gz_x500模型就行。这里我强烈建议使用v1.14.3而不是main分支。PX4的main分支代码变动非常快,今天能编译的接口过两周可能就不兼容了,学习阶段千万别追新。
2.2 操作步骤:从零到能起飞
我按自己能稳定复现的顺序给你梳理一遍,每一步都写清楚为什么要这么做。
第一步:安装ROS2 Humble。
按照官方文档装desktop版本就行,不用纠结选哪个版本。有个老生常谈但总有人踩的坑:装完后一定要执行source /opt/ros/humble/setup.bash,并且把这句话写进~/.bashrc,否则新终端里ros2命令永远找不到。测试一下:
ros2 --version能输出版本号就说明ROS2本体OK。
第二步:安装Gazebo仿真工具链。
对于PX4 v1.14.3,我推荐直接用它的自动脚本把依赖一次装齐,而不是手动一个个装gazebo。在源码目录下执行:
bash ./Tools/setup/ubuntu.sh这个脚本会检测并安装CMake、Python、Gazebo、OpenCV等一堆依赖。执行完以后最好重启一次shell,让新添加的环境变量生效。
第三步:拉取PX4源码并切到稳定版本。
git clone --recursive https://github.com/PX4/PX4-Autopilot.git cd PX4-Autopilot git checkout v1.14.3 git submodule update --init --recursive--recursive非常重要。PX4有不少子模块,比如mavlink、uavcan,不更新子模块,后面编译一定报错。
第四步:编译PX4 SITL。
make px4_sitl gz_x500第一次编译会很久,建议挂机去喝杯茶。如果编译过程中提示缺少某个Python包,用pip install 包名补上就行,基本不用慌。
第五步:创建ROS2工作空间,放置px4_msgs。
mkdir -p ~/ws/src cd ~/ws/src git clone https://github.com/PX4/px4_msgs.git cd ~/ws colcon build --packages-select px4_msgs source install/setup.bash这里有个细节:px4_msgs的版本最好和PX4固件版本对齐。如果你用v1.14.3固件,但px4_msgs是main分支,消息字段可能有差异。我建议直接克隆后也切到对应release tag。
第六步:安装Micro XRCE-DDS Agent。
这步通常有两种方式,源码编译或者ros2包安装。我推荐用ROS2方式:
sudo apt install ros-humble-micro-ros-agent这样装完以后可以直接用ros2 run micro_ros_agent micro_ros_agent启动,省去手动配置的麻烦。
到这里,环境基础就算打好了。启动完这些之后,建议先在终端里验证一下PX4能否正常启动:
cd ~/PX4-Autopilot make px4_sitl gz_x500如果弹出Gazebo 3D窗口,并且终端提示Ready for takeoff,那就可以进入下一步了。
2.3 常见的版本坑
- 子模块缺失:很多人的
gazebo模型加载不出来,原因是PX4的Tools/simulation-gazebo子模块没拉全。重新执行git submodule update --init --recursive就能解决。 - Gazebo界面狂闪:如果GPU驱动不太好,特别是虚拟机环境里,Gazebo渲染会一直闪。临时解决方案是设置环境变量走软件渲染:
export LIBGL_ALWAYS_SOFTWARE=1,但性能会下降。最好装一下GPU驱动,或换一台有独立显卡的机器跑仿真。 - ROS2话题从零开始没内容:这通常是Agent没有启动,或者PX4内部的uXRCE-DDS客户端没连上Agent。启动顺序上,先启动Agent,再启动PX4 SITL,连接更稳定。
3. C++节点设计:把一个“圆”翻译成飞控指令
3.1 先理解Offboard模式下的控制接口
在写代码前,必须搞清楚PX4的Offboard模式到底接收什么格式的期望值。在ROS2接口层,我们需要往两个话题上发消息:
/fmu/in/offboard_control_mode:告知飞控本次期望值的类型(位置/速度/加速度/姿态),当前阶段我们需要的就是位置。/fmu/in/trajectory_setpoint:具体的期望数值,比如期望位置坐标、期望速度、期望偏航角。
关键一点:PX4在Offboard模式下会屏蔽掉所有不属于当前控制类型的其他字段。也就是说,如果你的OffboardControlMode里只把position置为true,那么TrajectorySetpoint里的velocity字段即使填了,也不会参与控制。这点在调试时很重要,不要同时开位置和速度控制,容易让内环和外环打架。
另外,PX4内部使用的坐标系是NED(北东地),即X轴朝北、Y轴朝东、Z轴朝下。ROS2默认坐标系在很多场景下是ENU(东北天),X轴朝东、Y轴朝北、Z轴朝上。如果你在ROS2里拿到的局部位置是ENU,直接塞进PX4的topic里,无人机会往完全错误的方向飞。所以拿到数据后,要么在节点内部统一做NED转换,要么明确知道自己手里的坐标就是NED。后面我给代码时直接按NED生成圆轨迹,反而少一层转换负担。
3.2 圆的数学:参数方程与控制频率
要“画圆”,本质上就是在NED坐标系下周期地给出圆的期望位置。假设圆心在起飞点上方的H高度,半径R,飞行角速度为ω,那么NED位置满足:
x(t) = x0 + R * cos(θ(t)) y(t) = y0 + R * sin(θ(t)) z(t) = -H // NED的Z轴向下,期望高度取负 θ(t) = θ0 + ω * t这里θ0是初始相位,决定无人机起点在圆上的哪个位置。ω的单位是rad/s,想要多飞完一圈,由目标线速度决定:目标线速度v = R * ω。
更要紧的是,PX4的Offboard模式允许你同时发位置和速度设定值。只发位置也能画圆,但控制效果会比较“拖沓”:飞机总是一点点追着目标走,轨迹更像是跟随而非精确飞行。所以在TrajectorySetpoint里,我建议把位置和速度都填上。速度期望可以直接用位置参数方程对时间求导得到:
vx(t) = -R * ω * sin(θ(t)) vy(t) = R * ω * cos(θ(t)) vz(t) = 0飞控拿到位置和速度期望后,位置环输出前馈速度,速度环再跟踪,跟踪效果会平滑很多。
再说发布频率。PX4文档里有个硬性要求:Offboard模式下设定值最低要高于某频率,通常建议至少10Hz,推荐20Hz~50Hz。我实测20Hz已经完全够用,而且CPU占用不会太高。我个人习惯用50Hz作为默认值,因为Gazebo仿真模型动力学本身比较简单,50Hz下轨迹更平滑,视觉上看不出锯齿。
最后说一下给定参数的具体数值。如果你希望无人机在10秒内飞完一整圈,半径R=5m,那根据圆周运动公式,这个组合也可以这样拆:可以先定角速度ω=2π/10≈0.628 rad/s,线速度就是5×0.628≈3.14m/s。这个线速度对四旋翼来说比较适中,太慢会让位置控制环难以收敛,太快则可能触发限幅导致轨迹打折。参数设定这块,我的建议永远是“低参数起步,好稳定后再加大”。
3.3 写C++代码:核心循环
下面是核心类和一个10Hz~50Hz的发布循环。这个代码我实际跑通过,直接放在你创建的ROS2包里就能编译运行。为了不占太多篇幅,我贴核心部分,构造函数和话题创建的模板代码略。
#include <rclcpp/rclcpp.hpp> #include <px4_msgs/msg/offboard_control_mode.hpp> #include <px4_msgs/msg/trajectory_setpoint.hpp> #include <px4_msgs/msg/vehicle_local_position.hpp> #include <cmath> class OffboardCircleNode : public rclcpp::Node { public: OffboardCircleNode() : Node("offboard_circle_node") { // 创建发布器和订阅器 offboard_mode_pub_ = create_publisher<px4_msgs::msg::OffboardControlMode>( "/fmu/in/offboard_control_mode", 10); trajectory_pub_ = create_publisher<px4_msgs::msg::TrajectorySetpoint>( "/fmu/in/trajectory_setpoint", 10); local_pos_sub_ = create_subscription<px4_msgs::msg::VehicleLocalPosition>( "/fmu/out/vehicle_local_position", 10, [this](const px4_msgs::msg::VehicleLocalPosition::SharedPtr msg) { current_position_ = *msg; }); // 定时器:50Hz发布 timer_ = create_wall_timer(std::chrono::milliseconds(20), [this]() { publishCircleSetpoint(); }); } private: void publishCircleSetpoint() { // 1. 先发布OffboardControlMode,标记我们使用的控制类型 px4_msgs::msg::OffboardControlMode offboard_mode{}; offboard_mode.timestamp = stamp(); offboard_mode.position = true; // 我们使用位置控制 offboard_mode.velocity = false; offboard_mode.acceleration = false; offboard_mode.attitude = false; offboard_mode.body_rate = false; offboard_mode.thrust_and_torque = false; offboard_mode.direct_actuator = false; offboard_mode_pub_->publish(offboard_mode); // 2. 计算圆轨迹设定值 double time_now = 0.0; // 实际项目中应从节点启动时刻或收到本地位置后开始计时 // 我这里用节点启动后的系统时间做演示 auto elapsed = (this->now() - start_time_).seconds(); if (elapsed < 0.0) elapsed = 0.0; time_now = elapsed; constexpr double radius = 5.0; // 5米半径 constexpr double omega = 2.0 * M_PI / 10.0; // 10秒一圈 constexpr double height = 3.0; // 3米高度 constexpr double theta0 = 0.0; double theta = theta0 + omega * time_now; float x = radius * std::cos(theta); float y = radius * std::sin(theta); float z = -height; // NED,Z轴向下 px4_msgs::msg::TrajectorySetpoint setpoint{}; setpoint.timestamp = stamp(); setpoint.position[0] = x; setpoint.position[1] = y; setpoint.position[2] = z; // 速度前馈,让轨迹更平滑 setpoint.velocity[0] = -radius * omega * std::sin(theta); setpoint.velocity[1] = radius * omega * std::cos(theta); setpoint.velocity[2] = 0.0f; setpoint.yaw = 0.0f; // 让机头保持正北 trajectory_pub_->publish(setpoint); } uint64_t stamp() const { auto time_now = this->now(); return static_cast<uint64_t>(time_now.seconds() * 1e6) * static_cast<uint64_t>(1000); // 返回纳秒级时间戳 } rclcpp::Publisher<px4_msgs::msg::OffboardControlMode>::SharedPtr offboard_mode_pub_; rclcpp::Publisher<px4_msgs::msg::TrajectorySetpoint>::SharedPtr trajectory_pub_; rclcpp::Subscription<px4_msgs::msg::VehicleLocalPosition>::SharedPtr local_pos_sub_; rclcpp::TimerBase::SharedPtr timer_; px4_msgs::msg::VehicleLocalPosition current_position_; rclcpp::Time start_time_{rclcpp::Time(0)}; };代码里我故意保留了几个演示性质的简化点,这里说一下哪些地方需要你自己替换成实际逻辑:
第一,时间戳必须认真对待。OffboardControlMode和TrajectorySetpoint都要求传入PX4固件时间戳(微秒或纳秒单位)。我上面写的stamp()函数只演示了如何从ROS2时间转换,实际运行中你会发现,如果PX4主机和ROS2主机时间不同步(比如通过多机通信跑),时间戳会比较别扭。SITL模式下两个进程在同一台机器上,时间基本一致,所以问题不大。如果你跑真机,建议用同步后的系统时间或PX4输出的timestamp统一管理。
第二,起飞前的“预置点”逻辑。代码直接从0点开始画圆,但无人机在地面时不能直接从地面跳到3米高,那样飞控会给出很大的期望加速度,导致起飞瞬间剧烈晃动甚至翻转。稳妥做法是:先发送一个当前位置的setpoint,等无人机切换到Offboard模式并且高度到期望值附近后,再开始画圆。我在完整工程里是把“进入Offboard模式”也封装成一个节点行为的,这里篇幅有限就只展示核心循环。
3.4 编译配置:CMakeLists和package.xml
一个干净的package.xml和CMakeLists.txt,主要就是把px4_msgs依赖加进来。
package.xml里需要声明:
<depend>rclcpp</depend> <depend>px4_msgs</depend>CMakeLists.txt里则要加:
find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(px4_msgs REQUIRED) add_executable(offboard_circle src/offboard_circle_node.cpp) ament_target_dependencies(offboard_circle rclcpp px4_msgs) install(TARGETS offboard_circle DESTINATION lib/${PROJECT_NAME})编译:
cd ~/ws colcon build --packages-select your_package_name source install/setup.bash4. Gazebo仿真:看着无人机把圆画出来
4.1 启动PX4 SITL与DDS Agent
一切就绪后,启动顺序很关键。我的习惯是:
# 终端1:启动ROS2环境 source /opt/ros/humble/setup.bash source ~/ws/install/setup.bash ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888 -v注意-v参数会输出详细日志,第一次调试时务必打开,能直接看到Agent和Client是否成功握手。
# 终端2:启动PX4 SITL cd ~/PX4-Autopilot make px4_sitl gz_x500当Gazebo窗口弹出、终端打印[SITL] Ready for takeoff后,说明PX4固件已经在仿真里跑起来了。此时去终端1看一眼日志,应该能看到Agent收到了来自PX4的DDS消息:
[info] New client [client id: 7] connected如果没看到这条,多半是端口不对。默认PX4的SITL模式会往本机8888端口发UDP包,Agent也要监听8888端口,两边改成一致即可。
4.2 运行ROS2节点,验证画圆效果
启动我们的控制节点:
# 另一个终端,同样先source ros2 run your_package offboard_circle理论上无人机会垂直上升到3米高,然后稳定地飞一个半径5米的圆。我建议你在验证阶段做几件事:
第一,打开QGroundControl(QGC)连接到仿真。用QGC的好处是可以目视角速度和位置误差,也能直接远程切换模式。注意,SITL模式下QGC连接的是UDP端口14550,不是USB串口。
第二,确认飞行模式。要让节点真正进入Offboard,你可以通过QGC把飞行模式切到“Offboard”,或者如果不想再额外写模式切换代码,也可以用ros2 topic pub发一条MAVLink指令,但这比较绕。我更推荐先在C++节点里直接发MAVLink指令做自动模式切换,或者跑起来以后在QGC手动切。手动切最省事,适合第一次验证。切换后看页面上的飞控状态,确认当前模式是Offboard而不是Position。
第三,用ros2 topic echo观察实际位置:
ros2 topic echo /fmu/out/vehicle_local_position这里面有几个字段值得关注:x、y、z是当地NED坐标系下的位置估计,vx、vy、vz是速度估计。如果它输出的数值和我们的圆参数一致,说明整个链路已经通了。更直观的方法是装rqt_plot,把x和y画到一个二维坐标里,就能看到圆心位置和轨迹形状。
第四,看Gazebo画面。如果无人机在画面里不断画圈,并且圆半径看着稳定,这事就算大功告成。轨迹是椭圆的话,多半是半径参数或速度前馈没对,下面一节会细说。
4.3 如何让圆“飞得更圆”
你可能觉得,能飞起来就算圆了。但其实第一版代码跑出来的轨迹大概率不圆,甚至有点“方”。我的经验是从三个方向调:
一是提高发布频率到50Hz以上。Offboard模式对丢包和延迟非常敏感,如果发布频率只有5Hz,PX4会频繁触发Offboard超时,甚至自动退出Offboard。频率高一点之后,控制环的跟踪误差会明显下降。
二是用好速度前馈。前面代码里已经填了velocity字段,前提是OffboardControlMode里position=true的同时别把velocity也置true,否则PX4会尝试做速度控制,反而丢掉了位置环。实际上PX4在位置控制下,会把TrajectorySetpoint.velocity当作前馈速度。如果你发现“画出来的圆有滞后”,可以先检查这个字段是否有值。
三是调PX4位置控制器的增益。所有PX4的Offboard外部控制,最终都会落到内部位置环。位置环PID增益在QGC的MC_POSCTRL参数组里。给一个实用经验:如果无人机飞圆时有明显“追赶”感,可适当增大MC_POSCTRL_POS_P,比如从默认的1.0提到1.2。但别一次调太多,否则会有高频抖动。修改后直接改参数,无人机会在线响应,不用重启仿真。
5. 常见问题与排查实录
5.1 高频故障速查表
我把自己和身边人实测中踩过的坑整理成了表格,遇到问题先对号入座:
| 症状 | 可能原因 | 排查思路 |
|---|---|---|
| 消息发了但无人机不动 | 没有进入Offboard模式 | 用QGC或MAVLink确认当前模式 |
| 进入Offboard后立即退飞/降落 | 设定值不连续或频率不足 | 保证循环发布50Hz,预置点设在当前位置附近 |
| 无人机起飞就翻转 | 坐标系反了/IMU标定错误 | 检查NED vs ENU,重新校准加速度计 |
| Gazebo画面狂闪 | 显卡渲染驱动问题 | 尝试LIBGL_ALWAYS_SOFTWARE=1,或升级显卡驱动 |
| ros2 topic list里没有/fmu开头的消息 | Agent未启动或端口不一致 | 运行Agent,确认8888端口,看verbose日志 |
| 圆飞成椭圆 | 速度前馈没填/增益不合适 | 检查速度字段,提升发布频率,微调MC_POSCTRL_POS_P |
| 高度一直往下掉 | NED的Z方向填反了 | NED的期望高度应为负数(如-3) |
| 无人机频繁退Offboard | 控制指令超时 | 查看PX4终端是否打印“Offboard control lost” |
| px4_msgs编译不过 | 版本与PX4不匹配 | 检查px4_msgs的tag,最好和固件版本对齐 |
5.2 几个被忽略的细节
除了表格里的问题,还有几个细节严重影响成功率,但常规文档很少提。
第一个是预置点策略。如果无人机在地面上,代码运行后第一帧就发送3米高的圆心位置,PX4会认为你期望它瞬间到3米。这时候电机会猛加速,飞控可能直接进入保护模式。不要一上来就发目标点,应该先发“当前位置保持”的设定值,等无人机起飞并稳定在目标高度后,再开始圆的轨迹。我的做法是节点启动后先订阅vehicle_local_position,拿当前位置,把它作为前1秒的期望输出,等时间超过1秒后再切换到圆轨迹。
第二个是模式切换的“参数豁免”。如果你没有RC遥控器,SITL在启动时会默认检测到RC信号丢失,一旦无人机进入Offboard后有哪怕几百毫秒的异常,就可能导致飞行终止或落回Position模式。建议在QGC里把COM_RCL_EXCEPT设置为1(表示Offboard模式可以工作于RC丢失状态)。这个参数我在多次实物测试里也被坑过,仿真里不设置也能飞,但真机上不设置的话很容易一上天就触发保护。
第三个是不要在主循环里做耗时操作。比如把打印日志、打开文件操作放在50Hz的publish循环里,偶尔卡顿一帧可能问题不大,但当频率达到20Hz以上时,几百毫秒的卡顿可能会让PX4认为Offboard超时。我一般是发布逻辑只做数学计算和消息发送,必须做的日志用单独的线程或降频输出。
第四个值得单独说的,是**“时间戳差”的问题**。PX4 v1.14开始,OffboardControlMode消息里的timestamp必须填写,否则飞控无法正确判断消息的新鲜度。有的代码里随便填一个固定值,无人机能跑但会有偶发抖动。正确做法是每次消息都取当前时刻,并且单位要统一。PX4内部的timestamp通常是微秒,但有些消息是纳秒,比如vehicle_local_position输出是微秒时间戳。我习惯统一用微秒,避免出现消息被当作过期数据丢掉。
最后的经验分享
这个demo做完之后,最大的收获不是“我会发圆轨迹了”,而是终于把ROS2到PX4之间的这层通信彻底搞明白了。后面你再去做航线规划、视觉避障、编队飞行,核心思路都是一样的:ROS2负责算期望值,PX4负责执行,Agent负责翻译。区别只是期望值从“圆”变成“贝塞尔曲线”还是“多机协同轨迹”而已。
再分享一个小技巧:调通圆轨迹之后,可以把代码里的半径、角速度、高度改成从ROS2参数读取,这样就不用每次改代码重新编译。如果你后续打算做路径规划,把“生成轨迹”的部分抽象成接口,圆只是其中一种轨迹实现,后面接Bezier曲线或B样条就顺理成章了。ROS2的可复用性真正发挥出来,是从“让无人机飞起来”变成“让无人机按我的算法去飞”的时候。