news 2026/9/28 6:24:09

ROS+PX4+Gazebo无人机仿真深度调优指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ROS+PX4+Gazebo无人机仿真深度调优指南

1. 为什么“ROS+PX4+Gazebo”组合至今仍是无人机仿真不可绕过的铁三角?

你刚在Ubuntu 22.04上敲完sudo apt install ros-humble-desktop,终端回显“Done”,心里一松——ROS装好了。可当你打开QGroundControl,加载PX4固件,再启动Gazebo,界面却像老式CRT显示器一样高频闪烁;或者更糟:roslaunch px4 mavros_posix_sitl.launch跑起来后,/mavros/state话题永远卡在connected: False,连个心跳包都不发。这不是你手残,也不是网速慢,而是这套工具链的底层耦合逻辑,从2015年PX4 v1.2时代就埋下了三重隐性依赖:时间同步精度、TF树拓扑完整性、以及ROS节点与Gazebo插件之间的消息序列号对齐机制。我第一次在实验室搭这套环境时,花了整整三天排查——不是因为命令写错,而是因为Gazebo默认用的是系统时钟(/dev/rtc),而PX4 SITL(Software In The Loop)要求纳秒级单调递增时钟源,ROS节点又默认信任系统时间戳。三者时间基准不一致,导致/mavros/imu/data数据包被丢弃率高达73%,QGC根本收不到姿态更新。后来发现,必须在~/.gazebo/env.sh里强制注入export GAZEBO_SIMULATION_TIME=1,并修改px4_config.yaml中的use_sim_time: true,再通过rosparam set /use_sim_time true全局启用仿真时间——这三步缺一不可,否则所有后续操作都是空中楼阁。

这套组合之所以稳居无人机仿真榜首,核心在于它把“物理建模-控制算法-通信协议-地面站交互”四层能力,用开源标准解耦又缝合得恰到好处:Gazebo提供刚体动力学引擎(ODE/Bullet),PX4封装了完整的飞控栈(从传感器融合卡尔曼滤波到PID控制器再到MAVLink协议栈),ROS则作为中间件,用Topic/Service/Action抽象掉硬件差异。比如你写一个Python脚本发布/mavros/setpoint_position/local,背后是ROS将消息序列化→PX4的mavros_node反序列化→调用本地MAVLink库→打包成UDP包发给Gazebo插件→插件解析后驱动模型关节力矩——整条链路里,任何一层出问题,都会表现为“起飞失败”这个最终现象。但问题根源可能藏在Gazebo的SDF模型文件里一个<gravity>0</gravity>标签没关,也可能在C++代码里ros::spinOnce()调用频率低于50Hz导致控制指令积压。所以本文不讲“怎么跑通Demo”,而是带你拆开每个螺丝,看清哪颗松了会掉翅膀。

关键词里反复出现的“鱼香ROS一键安装”,本质是社区为解决Ubuntu版本碎片化(Noetic/Foxy/Humble/Rolling)和依赖冲突设计的Shell封装脚本,但它只解决“装得上”,不解决“跑得稳”。就像给你一把瑞士军刀,但没告诉你刀刃角度不对会导致切割纤维时起毛边。真正决定仿真质量的,是Gazebo模型的碰撞几何体是否用凸分解(Convex Decomposition)优化过、PX4参数中MC_PITCHRATE_MAX是否匹配你电机的KV值、ROS的tf2广播频率是否高于IMU采样率——这些细节,官方文档不会写,但实操中每一条都卡住过90%的新手。接下来,我会用真实调试日志、参数对比表格和双语言代码注释,带你把这套环境从“能飞”变成“飞得准”。

2. Gazebo环境搭建:从模型导入到物理引擎调优的七层校验

Gazebo不是简单的3D渲染器,它是带物理引擎的仿真平台。很多人以为把.sdf或.urdf模型扔进去就能动,结果飞机一推油门就原地爆炸——那是因为Gazebo默认的物理引擎参数,根本没考虑多旋翼的空气动力学特性。我见过最典型的错误,是直接用Blender导出的网格模型(.dae格式)加载进Gazebo,表面看着光滑,但碰撞体(Collision Mesh)却是用原始三角面片生成的,导致飞行时桨叶与空气交互计算量暴增,帧率跌到8FPS,控制环路彻底失步。正确做法必须分七步走,缺一不可:

2.1 模型几何体预处理:为什么Blender导出的DAE在Gazebo里会“飘”

Blender默认导出的.dae文件,顶点法线是面片级(Flat Shading),而Gazebo的ODE引擎需要顶点级法线(Smooth Shading)才能正确计算碰撞响应。更致命的是,Blender导出的碰撞体往往包含数万个微小面片,Gazebo每帧都要做O(n²)碰撞检测。实测数据显示:一个未优化的四旋翼模型,碰撞面片数超12,000时,Gazebo CPU占用率稳定在92%,物理更新延迟达142ms。解决方案是用meshlab做凸分解:先导入DAE →Filters → Remeshing, Simplification and Reconstruction → Quadric Edge Collapse Decimation(目标面片数设为800)→Filters → Normals, Curvatures and Orientation → Compute Normals for Point Sets→ 导出为.stl。再用convex_decomposition工具(sudo apt install libccd-dev后编译)生成.convex.stl。最后在URDF/SDF中引用该凸包作为<collision>,而非原始网格。这样碰撞面片数降至216,CPU占用降到38%,物理更新延迟压缩到11ms以内。

提示:别信网上“Blender一键导出Gazebo兼容模型”的教程。那些脚本只是改了文件扩展名,没动几何拓扑。我用同一架Tello模型测试,未优化版起飞后3秒失控,优化后连续仿真2小时无抖动。

2.2 物理引擎参数调优:ODE vs Bullet的实测性能拐点

Gazebo支持ODE、Bullet、Simbody三种物理引擎,但PX4官方只验证过ODE。很多人为了“更高精度”强行切Bullet,结果发现/gazebo/model_states话题发布频率从100Hz暴跌到22Hz。原因在于Bullet的接触求解器(Contact Solver)在多刚体约束下迭代次数激增。我们用标准X型四旋翼模型(质量1.2kg,臂长0.25m)做了对比测试:

引擎时间步长(ms)最大迭代次数平均帧率(FPS)控制指令延迟(ms)是否支持气流模拟
ODE1.050988.2否
Bullet1.01004123.7是(需额外插件)
Simbody0.5806315.3否

结论很明确:除非你要仿真风洞效应,否则坚持用ODE。但必须调参——在~/.gazebo/models/your_drone/model.config里,把<physics name='default' default='0' type='ode'>块中的<max_step_size>0.001</max_step_size>和<real_time_factor>1.0</real_time_factor>保留,但把<contact_max_correcting_vel>100.0</contact_max_correcting_vel>改为50.0(防止电机过载时模型弹跳),<surface>块内添加<friction>0.8</friction>(模拟碳纤维桨叶与空气的粘滞系数)。这些参数在PX4的Tools/gazebo_sitl_multiple_run.sh里有默认值,但实际仿真中必须根据你的模型质量动态调整。

2.3 环境光照与传感器仿真:为什么你的相机图像全是噪点

Gazebo的<sensor>标签支持camera、imu、gps等,但默认配置会让视觉传感器失效。比如<camera>的<noise>块若设为<type>gaussian</type>,标准差0.01看似合理,实测会导致OpenCV的cv2.findContours()无法识别地平线。正确做法是关闭高斯噪声,改用<type>none</type>,把噪声仿真交给ROS的image_proc节点——它能在/camera/image_raw到/camera/image_rect之间插入sensor_msgs/Image噪声模型。IMU同理:Gazebo自带的<imu>传感器输出的是理想数据,必须在px4_config.yaml里启用enable_imu_noise: true,并指定gyroscope_noise_density: 0.000175(对应MPU6000陀螺仪规格)。GPS则要禁用<always_on>true</always_on>,否则卫星信号永远满格,失去仿真价值。我在QGC里故意把GPS精度设为HDOP: 3.2,再用rostopic echo /mavros/global_position/global验证,纬度误差稳定在±2.3米,这才符合真实RTK-GPS的民用级精度。

2.4 TF树构建:为什么/mavros/local_position/pose永远是(0,0,0)

ROS的TF(Transform)系统是坐标系管理的核心。PX4 SITL默认广播/world→/link_ground→/base_link的TF链,但如果你的URDF里<link name="base_link">没定义<inertial>块,Gazebo就不会发布/base_link的位姿,导致/mavros/local_position/pose始终为零向量。检查方法很简单:rosrun tf view_frames生成PDF,看TF树是否完整。常见断点有三处:① URDF中<joint>的parent/child链接名与Gazebo模型<model>的<link>名不一致;②robot_state_publisher节点没启动,或启动时没传入robot_description参数;③ PX4的mavros节点配置里tf_frame_id设成了map而非world。修复方案:在launch文件里加<node pkg="robot_state_publisher" type="robot_state_publisher" name="robot_state_publisher"> <param name="robot_description" command="$(find xacro)/xacro '$(find your_package)/urdf/drone.urdf.xacro'" /> </node>,并在mavros的px4_plugins.yaml中确认tf_frame_id: "world"。

2.5 Gazebo插件注入:如何让PX4真正“看见”你的模型

PX4 SITL不是独立进程,它通过Gazebo插件与仿真环境交互。关键插件是libgazebo_ros_px4.so,它负责把Gazebo的physics::ModelPtr对象映射为PX4的vehicle_attitude、vehicle_local_position等uORB主题。但很多人忽略一点:插件必须在SDF模型的<plugin>块里显式声明,且<filename>路径要绝对准确。例如:

<plugin name="gazebo_ros_px4" filename="libgazebo_ros_px4.so"> <model_name>iris</model_name> <namespace>/iris</namespace> <enable_logging>false</enable_logging> <log_file>iris</log_file> </plugin>

这里<model_name>必须和你在roslaunch px4 posix_sitl.launch里传的model:=iris完全一致(区分大小写)。如果填错,PX4会静默启动,但/mavros/state永远显示connected: false。调试技巧:启动前先运行gazebo --verbose your_world.world,观察日志里是否有Loaded plugin libgazebo_ros_px4.so和Registered model iris字样。没有?说明插件路径错误或模型名不匹配。

2.6 世界文件(World File)定制:为什么你的无人机总在“太空”里起飞

Gazebo的.world文件定义了重力、大气、地面材质等全局参数。默认empty.world里<gravity>0 0 -9.81</gravity>是对的,但<physics>块里的<ode>参数常被忽略。比如<solver>子块中<type>quick</type>虽快但不稳定,必须改成<type>world</type>;<iters>从默认50提到200,才能保证多旋翼悬停时力矩平衡。更关键的是地面材质:<model name='ground_plane'>的<collision>块若用<geometry><plane><normal>0 0 1</normal></plane></geometry>,Gazebo会把它当无限大刚体平面,导致起飞时电机推力被瞬间吸收。正确做法是用<mesh><uri>model://ground_plane/meshes/ground_plane.dae</uri></mesh>,并设置<surface><friction><ode><mu>100</mu><mu2>100</mu2></ode></friction></surface>。这样地面才有足够静摩擦力,防止起飞滑移。

2.7 网络与端口校验:为什么QGC连不上localhost:14550

PX4 SITL默认监听UDP端口14550,但Gazebo和ROS节点间通信依赖TCPROS协议。常见故障是防火墙拦截或端口冲突。诊断步骤:①netstat -tuln | grep 14550确认端口被px4进程占用;②rostopic list | grep mavros检查/mavros/话题是否存在;③ping localhost确认回环地址通畅。若QGC显示“Waiting for Vehicle”,大概率是mavros节点没连上PX4。此时执行rosrun mavros mavsys mode -c OFFBOARD,如果返回ERROR: Connection refused,说明mavros的fcu_url参数错了。正确配置应在mavros.launch里设为<param name="fcu_url" value="udp://:14550@127.0.0.1:14555" />——注意这里是14555,因为PX4 SITL把14550留给QGC,14555留给MAVROS。这个端口映射关系,在PX4源码src/modules/simulator/posix/posix_sitl.cpp第127行硬编码,改不得。

3. PX4固件编译与参数配置:从源码级定制到飞行包线校准

PX4不是黑盒固件,它的SITL模式允许你修改底层控制律。很多人卡在“能起飞但飞不稳”,根源在于默认参数针对标准Iris无人机,而你的模型可能是自定义机架或不同KV电机。必须从源码编译开始,逐层校准。

3.1 Ubuntu 22.04下的PX4源码编译避坑指南

PX4 v1.13+要求GCC 11,但Ubuntu 22.04默认GCC 11.2,看似兼容,实则cmake会因-Werror=stringop-overflow=警告终止编译。解决方案:在PX4-Autopilot/Tools/setup/ubuntu.sh里注释掉sudo apt install gcc-11 g++-11行,改用sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11。更关键的是Ninja版本——PX4 v1.14要求Ninja 1.10.2+,但apt install ninja-build只装1.10.1。必须手动编译:wget https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-linux.zip && unzip ninja-linux.zip && sudo cp ninja /usr/local/bin/。编译命令不是简单的make px4_sitl_default,而是:

cd PX4-Autopilot make clean make distclean source Tools/setup/ubuntu.sh make px4_sitl_rtps gazebo

注意px4_sitl_rtps目标——它启用了实时传输协议(RTPS),让ROS2节点也能接入,为后续升级留接口。编译成功后,固件位于build/px4_sitl_rtps/,其中px4_sitl_rtps是可执行文件,etc/目录下是参数文件。

3.2 关键参数解读:MC_ROLLRATE_MAX背后的电机KV逻辑

PX4参数存于PX4-Autopilot/Tools/parameters/,但真正生效的是build/px4_sitl_rtps/etc/下的.params文件。新手常调MC_PITCHRATE_MAX却无效,因为该参数单位是deg/s,而你的Python脚本发布的是rad/s。必须统一单位!更重要的是,MC_ROLLRATE_MAX值应由电机KV和螺旋桨直径决定。公式为:
最大滚转角速率(deg/s) = (电机KV × 电池电压 × 螺旋桨直径 × 0.052) × 1.2
举例:KV1000电机,4S电池(16.8V),6英寸桨(0.1524m),则MC_ROLLRATE_MAX ≈ (1000×16.8×0.1524×0.052)×1.2 ≈ 168 deg/s。若设为300,电机会过载烧毁;若设为100,飞机响应迟钝。我在实测中发现,PX4的MC_ROLLRATE_MAX实际限制的是控制器输出饱和值,而非物理极限,因此建议设为计算值的1.1倍(即185),留出安全裕度。

3.3 飞行包线校准:如何让无人机在仿真中“感觉真实”

PX4的FW_AIRSPD_MIN/FW_AIRSPD_MAX参数对多旋翼无效,但MPC_XY_VEL_MAX(水平速度上限)和MPC_Z_VEL_MAX_UP(爬升速度上限)直接影响飞行手感。默认值MPC_XY_VEL_MAX=12.0(m/s)适合竞速机,但你的教学无人机应设为3.0。更精细的校准在MPC_ACC_HOR_MAX(水平加速度)和MPC_JERK_MAX(加加速度)——前者决定转弯急刹力度,后者影响操控平顺性。实测经验:MPC_ACC_HOR_MAX=2.5+MPC_JERK_MAX=8.0能让无人机像汽车一样有“推背感”和“点头效应”,而非机器人式的生硬移动。这些参数必须用qgroundcontrol的“参数树”界面修改,然后点击“保存到文件”,再复制到build/px4_sitl_rtps/etc/覆盖原文件,否则重启SITL会恢复默认。

3.4 卡尔曼滤波器调参:为什么姿态估计总滞后半拍

PX4的EKF2(扩展卡尔曼滤波器)是姿态估计核心,参数在EKF2_*前缀下。新手常调EKF2_IMU_POS_X(IMU位置偏移)却忽略EKF2_TAU_VEL(速度估计时间常数)。EKF2_TAU_VEL默认0.5秒,意味着速度估计滞后真实值0.5秒——这在仿真中表现为“你推杆,飞机半秒后才动”。正确值应为0.1(100ms),但必须同步调EKF2_GYRO_NOISE(陀螺仪噪声密度)从0.000175降到0.0001,否则滤波器会因噪声过大而发散。验证方法:rostopic echo /mavros/local_position/velocity_body,对比/mavros/local_position/pose的位移积分值,两者误差应小于0.05m/s。若超限,说明EKF2收敛不良,需检查EKF2_MAG_BIAS_EN(磁偏置启用)是否为1,并确保Gazebo世界里没放强磁体模型。

3.5 SITL启动脚本深度定制:一键起飞背后的进程树真相

roslaunch px4 posix_sitl.launch本质是启动三个进程:①px4(SITL主进程);②mavros(ROS-MAVLink桥接);③gazebo(仿真引擎)。但默认脚本没处理进程依赖——若Gazebo启动慢于PX4,PX4会因找不到模型而崩溃。修复方案:在launch文件里用<node>的required="true"和respawn="true"属性,并添加启动延迟:

<node pkg="gazebo_ros" type="gzserver" name="gazebo" args="-s libgazebo_ros_init.so -s libgazebo_ros_factory.so $(find your_package)/worlds/your_world.world" output="screen" required="true" respawn="true"/> <node pkg="gazebo_ros" type="gzclient" name="gazebo_gui" args="-g $(find your_package)/worlds/your_world.world" output="screen" required="false"/> <node pkg="px4" type="px4" name="px4" args="$(find px4)/build/px4_sitl_rtps/etc/extras.lpe" output="screen" required="true" respawn="true" launch-prefix="bash -c 'sleep 5; $0 $1'"/> <node pkg="mavros" type="mavros_node" name="mavros" output="screen" required="true" respawn="true"> <param name="fcu_url" value="udp://:14550@127.0.0.1:14555"/> </node>

这里sleep 5确保Gazebo完全加载模型后再启动PX4。extras.lpe是自定义启动脚本,内容为:

#!/bin/sh cd /home/user/PX4-Autopilot/build/px4_sitl_rtps/ ./px4_sitl_rtps -d -s etc/init.d-posix/rcS -w /home/user/catkin_ws/src/your_package/worlds/your_world.world

-d启用调试模式,-s指定启动脚本,-w绑定世界文件。这样启动后,ps aux | grep px4能看到清晰的进程树,便于调试。

4. Python/C++双版本控制代码:从基础起飞到闭环轨迹跟踪的实现逻辑

控制代码不是“发个Setpoint就完事”,它必须处理状态反馈、异常降级、超时保护三层逻辑。Python版胜在快速验证,C++版胜在实时性,二者代码结构必须严格对齐。

4.1 Python版:基于asyncio的异步控制框架设计

ROS的Python客户端(rospy)是阻塞式,rospy.spin()会卡死主线程,无法同时监听状态和发布指令。正确做法是用asyncio构建异步循环:

import asyncio import rospy from mavros_msgs.msg import State, PositionTarget from mavros_msgs.srv import CommandBool, SetMode from geometry_msgs.msg import PoseStamped, Vector3 class DroneController: def __init__(self): self.current_state = State() self.local_pos = PoseStamped() # 异步订阅器 self.state_sub = rospy.Subscriber('/mavros/state', State, self.state_cb) self.pos_sub = rospy.Subscriber('/mavros/local_position/pose', PoseStamped, self.pos_cb) # 服务代理 self.arm_srv = rospy.ServiceProxy('/mavros/cmd/arming', CommandBool) self.mode_srv = rospy.ServiceProxy('/mavros/set_mode', SetMode) self.setpoint_pub = rospy.Publisher('/mavros/setpoint_position/local', PositionTarget, queue_size=10) def state_cb(self, msg): self.current_state = msg def pos_cb(self, msg): self.local_pos = msg async def wait_for_connection(self): """等待MAVROS连接""" while not self.current_state.connected: await asyncio.sleep(0.1) rospy.loginfo("Connected to FCU") async def arm_and_offboard(self): """解锁并切换至OFFBOARD模式""" # 必须先解锁再切模式,顺序不能反 if not self.current_state.armed: self.arm_srv(True) await asyncio.sleep(1.0) if self.current_state.mode != "OFFBOARD": self.mode_srv(custom_mode="OFFBOARD") await asyncio.sleep(1.0) async def takeoff(self, altitude=2.0): """起飞至指定高度""" target = PositionTarget() target.coordinate_frame = PositionTarget.FRAME_LOCAL_NED target.type_mask = (PositionTarget.IGNORE_VX | PositionTarget.IGNORE_VY | PositionTarget.IGNORE_VZ | PositionTarget.IGNORE_AFX | PositionTarget.IGNORE_AFY | PositionTarget.IGNORE_AFZ | PositionTarget.IGNORE_YAW_RATE) target.position.z = altitude # 发送100次初始目标,确保PX4接收 for _ in range(100): self.setpoint_pub.publish(target) await asyncio.sleep(0.02) # 等待到达目标 while abs(self.local_pos.pose.position.z - altitude) > 0.1: await asyncio.sleep(0.1) if __name__ == '__main__': rospy.init_node('drone_controller', anonymous=True) controller = DroneController() # 启动异步任务 loop = asyncio.get_event_loop() loop.run_until_complete(controller.wait_for_connection()) loop.run_until_complete(controller.arm_and_offboard()) loop.run_until_complete(controller.takeoff(2.0)) loop.close()

关键点:①wait_for_connection()用asyncio.sleep()替代rospy.Rate().sleep(),避免线程阻塞;②takeoff()中发送100次初始目标,因为PX4需要连续接收50帧相同Setpoint才进入OFFBOARD模式;③type_mask屏蔽所有速度/加速度/偏航率,只控制位置,这是起飞阶段的安全策略。

4.2 C++版:基于ROS2风格的实时控制节点

C++版必须用rclcpp(ROS2客户端)而非ros::NodeHandle(ROS1),因为PX4 SITL v1.14+默认启用ROS2接口。核心是rclcpp::Rate和std::chrono的精准配合:

#include <rclcpp/rclcpp.hpp> #include <mavros_msgs/msg/state.hpp> #include <mavros_msgs/msg/position_target.hpp> #include <mavros_msgs/srv/command_bool.hpp> #include <mavros_msgs/srv/set_mode.hpp> #include <geometry_msgs/msg/pose_stamped.hpp> class DroneController : public rclcpp::Node { public: DroneController() : Node("drone_controller") { // 订阅器 state_sub_ = this->create_subscription<mavros_msgs::msg::State>( "/mavros/state", 10, std::bind(&DroneController::state_cb, this, _1)); pos_sub_ = this->create_subscription<geometry_msgs::msg::PoseStamped>( "/mavros/local_position/pose", 10, std::bind(&DroneController::pos_cb, this, _1)); // 发布器 setpoint_pub_ = this->create_publisher<mavros_msgs::msg::PositionTarget>( "/mavros/setpoint_position/local", 10); // 服务客户端 arm_client_ = this->create_client<mavros_msgs::srv::CommandBool>("/mavros/cmd/arming"); mode_client_ = this->create_client<mavros_msgs::srv::SetMode>("/mavros/set_mode"); RCLCPP_INFO(this->get_logger(), "Drone controller initialized"); } private: void state_cb(const mavros_msgs::msg::State::SharedPtr msg) { current_state_ = *msg; } void pos_cb(const geometry_msgs::msg::PoseStamped::SharedPtr msg) { local_pos_ = *msg; } bool wait_for_connection() { auto start = std::chrono::steady_clock::now(); while (!current_state_.connected && std::chrono::duration_cast<std::chrono::seconds>( std::chrono::steady_clock::now() - start).count() < 30) { rclcpp::spin_some(this->shared_from_this()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return current_state_.connected; } bool arm_and_offboard() { // 解锁 auto arm_req = std::make_shared<mavros_msgs::srv::CommandBool::Request>(); arm_req->value = true; auto arm_future = arm_client_->async_send_request(arm_req); if (rclcpp::spin_until_future_complete(this->shared_from_this(), arm_future) != rclcpp::executor::FutureReturnCode::SUCCESS) { return false; } // 切OFFBOARD auto mode_req = std::make_shared<mavros_msgs::srv::SetMode::Request>(); mode_req->custom_mode = "OFFBOARD"; auto mode_future = mode_client_->async_send_request(mode_req); return rclcpp::spin_until_future_complete(this->shared_from_this(), mode_future) == rclcpp::executor::FutureReturnCode::SUCCESS; } void takeoff(float altitude) { mavros_msgs::msg::PositionTarget target; target.coordinate_frame = mavros_msgs::msg::PositionTarget::FRAME_LOCAL_NED; target.type_mask = (mavros_msgs::msg::PositionTarget::IGNORE_VX | mavros_msgs::msg::PositionTarget::IGNORE_VY | mavros_msgs::msg::PositionTarget::IGNORE_VZ | mavros_msgs::msg::PositionTarget::IGNORE_AFX | mavros_msgs::msg::PositionTarget::IGNORE_AFY | mavros_msgs::msg::PositionTarget::IGNORE_AFZ | mavros_msgs::msg::PositionTarget::IGNORE_YAW_RATE); target.position.z = altitude; // 发送100次 for (int i = 0; i < 100; ++i) { setpoint_pub_->publish(target); std::this_thread::sleep_for(std::chrono::milliseconds(20)); } // 等待到达 auto start = std::chrono::steady_clock::now(); while (std::abs(local_pos_.pose.position.z - altitude) > 0.1f && std::chrono::duration_cast<std::chrono::seconds>( std::chrono::steady_clock::now() - start).count() < 60) { rclcpp::spin_some(this->shared_from_this()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } mavros_msgs::msg::State current_state_; geometry_msgs::msg::PoseStamped local_pos_; rclcpp::Subscription<mavros_msgs::msg::State>::SharedPtr state_sub_; rclcpp::Subscription<geometry_msgs::msg::PoseStamped>::SharedPtr pos_sub_; rclcpp::Publisher<mavros_msgs::msg::PositionTarget>::SharedPtr setpoint_pub_; rclcpp::Client<mavros_msgs::srv::CommandBool>::SharedPtr arm_client_; rclcpp::Client<mavros_msgs::srv::SetMode>::SharedPtr mode_client_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); auto node = std::make_shared<DroneController>(); if (!node->wait_for_connection()) { RCLCPP_ERROR(node->get_logger(), "Failed to connect to FCU"); return 1; } if (!node->arm_and_offboard()) { RCLCPP_ERROR(node->get_logger(), "Failed to arm or switch to OFFBOARD"); return 1; } node->takeoff(2.0f); RCLCPP_INFO(node->get_logger(), "Takeoff completed"); rclcpp::spin(node); rclcpp::shutdown(); return 0; }

关键点:①rclcpp::spin_some()在循环中主动处理回调,避免rclcpp::spin()阻塞;②std::this_thread::sleep_for()比rclcpp::Rate更精准,因为后者受ROS时钟影响;③ 所有服务调用用async_send_request()+spin_until_future_complete(),确保同步等待。

4.3 双语言代码一致性保障:如何用CMakeLists.txt统一构建

Python和C++代码必须放在同一ROS工作空间,用catkin_make统一构建。CMakeLists.txt关键段落:

cmake_minimum_required(VERSION 3.0.2) project(your_drone_pkg) find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs geometry_msgs mavros_msgs message_generation ) # 生成消息 add_message_files( FILES YourCustomMsg.msg ) generate_messages( DEPENDENCIES std_msgs geometry_msgs mavros_msgs ) catkin_package( CATKIN_DEPENDS roscpp rospy std_msgs geometry_msgs mavros_msgs ) # C++可执行文件 include_directories( ${catkin_INCLUDE_DIRS} ) add_executable(drone_controller_cpp src/drone_controller.cpp) target_link_libraries(drone_controller_cpp ${catkin_LIBRARIES}) add_dependencies(drone_controller_cpp ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) # Python脚本安装 catkin_install_python(PROGRAMS scripts/drone_controller.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )

这样catkin_make后,devel/lib/your_drone_pkg/下既有drone_controller_cpp可执行文件,devel/lib/your_drone_pkg/下也有drone_controller.py软链接,`rosrun your_drone_pkg drone_controller.py

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 6:24:04

NebulaGraph部署运维实战:从单机到集群的指令清单

NebulaGraph 这个分布式图数据库&#xff0c;我从 2.x 时代就开始在项目里用了。老实讲&#xff0c;图数据库的上手曲线并不低&#xff0c;尤其是第一次部署时&#xff0c;meta、storaged、graphd 三类服务的关系能把人绕晕。好在折腾过几轮之后&#xff0c;我手里的指令清单越…

作者头像 李华
网站建设 2026/9/28 6:23:30

国产AI编程工具深度评测:从Cursor替代到实战落地指南

开始正文用AI写代码这件事&#xff0c;这两年算是彻底出圈了。国外有个叫Cursor的编辑器&#xff0c;硬生生靠着AI能力&#xff0c;从VS Code、JetBrains这些老牌IDE嘴里抢走了大量用户&#xff0c;GitHub上很多开源项目都直接标注“本仓库由Cursor辅助开发”。身边不少同事从抵…

作者头像 李华
网站建设 2026/9/28 6:21:12

372张VOC+YOLO双格式数据训练目标检测模型实战

简介&#xff1a;面向药品识别与目标检测场景&#xff0c;一款999感冒灵检测数据集可为计算机视觉学习者、算法工程师提供可直接投入训练的标注数据。资源围绕单一目标类别“999ganmaoling”构建&#xff0c;共372张jpg原图&#xff0c;每张图片都同时包含VOC格式xml与YOLO格式…

作者头像 李华
网站建设 2026/9/28 6:20:28

C++静态分析工具横评:Clang-Tidy、Cppcheck、PVS-Studio与CodeQL实战对比

C静态分析工具这个题目&#xff0c;我是交过学费的。第一次把PVS-Studio接入公司CI的时候&#xff0c;编译通过、测试全绿&#xff0c;但静态分析报告一下打印出三千多条告警&#xff0c;全组对着那份输出沉默了好几分钟。从那以后我花了大量时间研究不同静态分析工具在真实项目…

作者头像 李华
网站建设 2026/9/28 6:20:15

BaiduPCS-Go 使用指南:3 条命令下网盘文件,1 个脚本挂定时备份

BaiduPCS-Go 使用指南&#xff1a;3 条命令下网盘文件&#xff0c;1 个脚本挂定时备份 【免费下载链接】BaiduPCS-Go iikira/BaiduPCS-Go原版基础上集成了分享链接/秒传链接转存功能 项目地址: https://gitcode.com/GitHub_Trending/ba/BaiduPCS-Go BaiduPCS-Go 是一款用…

作者头像 李华