ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ROS 2 Jazzy与Gazebo Harmonic迷宫机器人仿真全攻略

ROS 2 Jazzy与Gazebo Harmonic迷宫机器人仿真全攻略 简介本资源是一个面向高校机器人方向本科生与研究生的ROS2实践项目聚焦迷宫环境下的自主导航与路径规划算法实现适用于毕业设计、课程设计及AI机器人交叉学习场景。压缩包共30个文件180KB含13个SDF世界模型文件定义不同尺寸迷宫结构、6个Python核心脚本涵盖A*路径搜索、路径跟踪、障碍物生成与TSP优化等关键逻辑、3组配置文件用于参数调优及配套README说明与.gitignore规范。已有73人学习下载项目采用ROS2 Jazzy与Gazebo Harmonic官方仿真环境深度集成提供从世界建模、机器人模型搭建、传感器仿真到算法闭环验证的完整流程所有代码模块化清晰、注释完备可直接运行并支持快速替换自定义算法模块是理解ROS2节点通信、TF坐标变换与实时路径规划落地的理想教学级工程范例。 拿到这个项目标题的时候我第一反应是终于有人把ROS 2的版本迭代和Gazebo的仿真体系捋到一块儿了。Jazzy是ROS 2的长期支持版本Harmonic是Gazebo的同步长期支持版本这两个名字绑在一起本身就意味着开发者在选型上踩过坑、查过兼容表最后才敲定的组合。如果你还在Humble和Foxy之间犹豫或者被Gazebo Classic和Gazebo的差异搞到头大这篇东西应该能帮你省下好几个周末的折腾时间。整个项目的核心目标是做一个能在仿真环境里自主走迷宫的差速机器人。听起来不算复杂但真做起来会发现它把ROS 2的通信机制、Gazebo的物理引擎、SLAM建图、路径规划、甚至传统迷宫求解算法全部串在了一起。我从环境搭建开始一路做到机器人成功走出迷宫中间踩了无数坑这篇文章会把整个过程拆开揉碎包括我为什么选这套技术栈、每个环节的核心原理、以及你照着做的时候最容易翻车的地方。1. 从版本选择说起为什么是Jazzy搭配Harmonic很多初学者容易忽略一个问题ROS 2和Gazebo不是两个独立的软件它们之间有严格的版本对应关系。Jazzy Jalisco是2024年发布的ROS 2长期支持版本官方推荐搭配的仿真器就是Gazebo Harmonic。这个组合有一个天然优势——两者的维护周期几乎对齐不会出现一个还在更新、另一个已经停止维护的尴尬情况。我见过太多人在Humble上装Gazebo 11然后在启动仿真时遇到各种奇怪的段错误。根源在于Gazebo Classic和ROS 2的接口是通过gazebo_ros_pkgs桥接的而新版GazeboHarmonic及以后采用了完全不同的插件机制和传输层。Jazzy Harmonic的组合ros_gz功能包已经非常成熟机器人和传感器的仿真插件直接调用Harmonic的原生API通信延迟和稳定性都明显优于老方案。具体到系统环境我推荐Ubuntu 24.04。Jazzy对24.04的支持是最好的Harmonic在24.04上安装也只需要几行命令。如果你还在用22.04也不是不行但需要自己处理一些依赖冲突尤其是sdformat和gz-transport的版本问题纯属给自己找麻烦。还有一个容易忽略的细节ROS 2的rmw实现会影响仿真数据传输的实时性。默认的rmw_fastrtps_cpp在重负载下偶尔会出现数据丢失我后来换成了rmw_cyclonedds_cpp发布里程计和激光数据时的表现稳定了不少。这一行配置差一点整个迷宫求解过程的稳定性就完全不同。2. 搭建一个能跑迷宫仿真的最小环境环境搭建这一步说简单也简单说坑也坑。我强烈建议你直接从二进制包安装不要从源码编译。源码编译Jazzy Harmonic的组合在部分机器上会触发编译器版本问题纯属浪费时间。2.1 先装ROS 2 Jazzy如果你用的是Ubuntu 24.04安装流程比老版本要简洁很多。核心步骤是设置软件源、添加ROS 2 GPG密钥、然后apt install ros-jazzy-desktop。ros-jazzy-desktop这个包包含了rviz2、demo_nodes_cpp、tf2等常用工具做迷宫仿真完全够用。装完之后别忘了做一件事把source /opt/ros/jazzy/setup.bash写进~/.bashrc。如果你用zsh就写setup.zsh。这个步骤漏掉的话每次新开终端都要手动source一遍极其影响心情。2.2 再装Gazebo HarmonicHarmonic的安装分为gz-harmonic主包和ros_gz桥接包两部分。前者主要是仿真器本体后者是ROS 2和Gazebo通信的桥梁。装完之后用gz sim -v 4启动一个空白世界能正常打开就说明基本环境没问题。注意这里的桥接包必须和你的ROS 2版本匹配。Jazzy对应的是ros-jazzy-ros-gz这个包名在Ubuntu的apt源里可以直接搜到别下错了版本。如果下成了Humble的ros_gz你会发现节点虽然能启动但话题永远收不到数据。2.3 环境变量配置Harmonic装好之后还要在.bashrc里加一行export GZ_SIM_RESOURCE_PATH$GZ_SIM_RESOURCE_PATH:~/your_workspace/src。这个变量告诉Gazebo去哪里找机器人模型和世界文件。我当时忘了配这一步结果机器人模型怎么都加载不出来报错还特别隐蔽只说找不到model://路径排查了好久。另外再装一个gz-tools2它提供了一些命令行工具比如gz model、gz world调试模型坐标和关节状态的时候能救命。3. 迷宫环境建模我不建议手画每一面墙迷宫场景看起来只是几面墙拼在一起但如果你真的用Gazebo的图形界面一块块拖墙体模型后续调整迷宫布局会非常痛苦。正确做法是直接用文本格式定义世界文件把墙体坐标写成一个数组然后用脚本批量生成。这样改迷宫结构只需要改一组坐标重启仿真就能生效。3.1 用SDF格式定义迷宫墙体Harmonic沿用SDFormat作为世界描述格式。每一面墙本质是一个model节点里面包含一段box几何体和对应的collision属性。麻烦的地方在于每个模型都要写重复的mass、inertia、surface friction信息手写十几面墙能写到怀疑人生。我当时写了一个Python脚本把墙体的坐标和尺寸存成CSV文件脚本自动生成对应的SDF片段拼接到世界文件里。这样不仅省事还避免了手写时容易漏掉collision导致的物理穿透问题。3.2 设置合适的物理参数迷宫求解对里程计精度要求不高但对碰撞检测的稳定性要求很高。建议把default_gravity保持默认但把墙体的surface摩擦系数改到0.8左右不然机器人轮子打滑会导致后续定位漂移。还有一个参数值得注意max_step_size。Harmonic默认是0.001秒如果电脑性能一般仿真速度会拖得很慢。我调到了0.002物理精度少量下降但仿真速度明显提升对迷宫这种低速场景完全够用。3.3 生成一个25格乘25格的测试迷宫我的实现里迷宫设计成正方形网格布局每个格子边长1米墙壁高0.2米。这个尺寸既能容纳机器人顺畅转向又不会让雷达扫描范围超出墙体。为了避免机器人一开始就被困在角落入口放在迷宫外沿机器人从入口出发后通过右手法则决定下一步走向。这样的布局对后续算法验证比较友好。4. 机器人本体差速底盘和传感器配置迷宫求解的机器人本体不需要机械臂不需要复杂关节一个差速底盘加一个激光雷达就足够了。我在项目里用的是一台两轮差速小车前侧还有一个万向轮支撑。4.1 URDF模型的关键连接URDF里最核心的就是joint的类型和limit设置。差速轮要设成continuous类型不需要角度限制万向轮设成fixed不然仿真里它会随意摆动导致抖动。两个驱动轮还要通过transmission和gazebo插件绑定到差速控制器上这一步漏了的话机器人就变成了一台雕塑。4.2 激光雷达选型迷宫求解最适合的传感器是2D激光雷达。Harmonic自带的gpu_ray传感器插件效率很高能直接输出模拟的LaserScan消息。扫描范围设成360度解析度1度最大测距10米。如果你用的是ray非GPU版本CPU占用会居高不下建图实时性反而变差。4.3 里程计和IMU的配合差速底盘的里程计通过轮式编码器积分获得。这里有个经典坑纯里程计在仿真中也会有累计误差所以最好加一个IMU作为辅助。Harmonic的IMU插件输出的是Imu消息把它和轮式里程计做EKF融合定位精度会好很多。安装robot_localization包后配置ekf_node的odom0和imu0输入即可。robot_localization的配置是一个低频但关键的工作。频率设置注意一点IMU的频率最好高于100Hz里程计50Hz即可太低的话融合出来的姿态跳变明显。我一开始IMU设成30Hz融合结果惨不忍睹后来改成200Hz后效果才稳定下来。5. 让机器人知道自己在哪里SLAM建图和定位迷宫求解的前提是机器人必须拿到准确的自身位姿和地图信息。我选择的方式是用rtabmap从单线激光雷达数据实时构建二维占据栅格地图然后基于这个地图做路径规划。这套方案的好处是建图过程和迷宫求解可以解耦——先跑一遍建图拿到完整迷宫地图再单独执行迷宫搜索逻辑。5.1 slam_toolbox还是rtabmap这里有个重要的取舍slam_toolbox计算量小续航性好但它推出的地图通常以激光扫描匹配为主在长走廊场景容易飘。rtabmap则维护了完整的位姿图闭环检测做得好迷宫这种大量重复纹理的环境恰好是它的强项。我的项目里用的是rtabmap因为它能直接输出里程计话题/rtabmap/odom省去自己维护TF树的部分麻烦。如果你非要坚持用slam_toolbox至少把minimum_travel_heading设为0.2弧度minimum_travel_distance设为0.1米不然建图时机器人在原地打转也会频繁插入帧拖慢建图速度。5.2 让你的导航栈信任你的地图在建图过程中地图的分辨率和尺寸直接影响后续Nav2路径规划的可行性。分辨率设成0.02米/像素既能捕捉迷宫墙体的细节又不会让地图体积过大。最终生成的地图通过map_server发布同时在Nav2的配置里把map_topic指向对应的地图话题避免默认值找不到地图。一个容易被忽略的细节地图的TF关系必须严格满足map - odom - base_footprint的层级结构。rtabmap默认会输出整个TF树但如果你自己写节点广播位姿很容易把map和odom搞反导致机器人在地图上的位置一直跳变。6. 迷宫求解算法从传统规则到图搜索拿到地图之后就进入重头戏了。迷宫求解有两种主流思路一种是传统的沿墙走算法不需要全局地图另一种是基于已知地图的图搜索算法机器人先把迷宫完整地探索一遍然后规划一条从入口到出口的最优路径。我的项目最终采用的是第二种思路更贴近实际导航场景也有更强的泛用性。6.1 为什么最终摒弃了纯右手法则右手法则或左手法则在真实的物理迷宫里是可行的因为机器人可以靠触碰墙壁来决定转向。但在仿真环境中激光雷达扫描到的墙是离散点它代表的“墙”其实是一条具有一定宽度和噪声的点云边界。如果你仅靠规则去碰壁很容易出现所谓的“贴边抖动”——机器人不断修正自己的朝向导致里程计误差迅速累积。所以我的方案是先做全图探索让机器人沿着迷宫网格逐格扫描利用Nav2的navigate_to_pose动作接口驱动机器人逐个遍历可达格点同时把未知区域逐步填充进地图。等整张图被探索完毕之后再利用A*或Dijkstra在主地图上规划一条最优路径。6.2 栅格地图与A*搜索因为迷宫是基于网格生成的世界所以地图天然可以用二维数组表示。A*算法之所以比BFS更适合这个场景在于它融合了到起点的实际代价和到终点的启发式估计。在迷宫这类格子成本几乎相等的地图里用曼哈顿距离作为启发函数已经足够了不需要引入更复杂的欧氏距离。在写搜索逻辑时要注意相邻节点不仅包括上下左右四个方向还包括斜角方向。但斜角穿墙是很多初学者会犯的错误——如果右上角有墙那么从当前格子移动到右上格子实际上会穿过墙体交接点。一个安全的做法是只有两个相邻的正方向都通行时才允许走斜角。6.3 实现A*的代码骨架下面是我在项目里用的A*核心逻辑包含了完整的路径输出。为了简洁我用的是Python伪代码风格实际项目中你可能需要转成C插件import heapq def heuristic(a, b): # 曼哈顿距离 return abs(a[0] - b[0]) abs(a[1] - b[1]) def astar(grid, start, goal): open_list [] heapq.heappush(open_list, (0, start)) came_from {} g_score {start: 0} f_score {start: heuristic(start, goal)} while open_list: _, current heapq.heappop(open_list) if current goal: path [] while current in came_from: path.append(current) current came_from[current] path.append(start) return path[::-1] for dx, dy in [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]: neighbor (current[0]dx, current[1]dy) if not (0 neighbor[0] len(grid) and 0 neighbor[1] len(grid[0])): continue if grid[neighbor[0]][neighbor[1]] 1: continue # 斜角安全校验 if dx ! 0 and dy ! 0: if grid[current[0]dx][current[1]] 1 or grid[current[0]][current[1]dy] 1: continue tentative_g g_score[current] (1.414 if dx!0 and dy!0 else 1.0) if tentative_g g_score.get(neighbor, float(inf)): came_from[neighbor] current g_score[neighbor] tentative_g f_score[neighbor] tentative_g heuristic(neighbor, goal) heapq.heappush(open_list, (f_score[neighbor], neighbor)) return None这个函数输入是二维占据栅格0表示可通行1表示墙体输出是路径点列表。实际部署时我会把Ros消息里的OccupancyGrid转换成这个二维数组跑完A*之后再把路径点转化成坐标调用nav2_msgs/action/NavigateToPose跟随路径。6.4 探索策略用覆盖路径完成全图扫描覆盖路径规划Coverage Path Planning在自动扫地机器人里很常用思路是让机器人逐行扫描不走重复路线。但在迷宫里格子之间的连通性不是矩形所以不能直接用网格全覆盖。我采用的是当前节点左转优先BFS回溯的策略来逐格遍历。具体操作是机器人当前站在某个格子先检查左边的格子是否未探索且可通行如果可行就走左边如果左边不行则按前方、右边的顺序试探如果周围都已走过就利用已建好的全局路径回到最近一个未探索分支点。这个策略配合A*回溯能保证机器人最终走过所有可达格子。6.5 路径追踪与避障拿到A算出的全局路径后让机器人沿着路径走本质上是把路径点依次发送为Nav2的NavigateToPose目标。这里有一个关键点Navigation2不会沿着你给的路径点一个一个执行它只接收目标点然后自行规划局部路径。因此你需要把A路径拆分成多段每次发给Nav2一个子目标等机器人到达后再发下一个。如果你一次性把每个路径点全发出去Nav2只会保留最后一个目标点中间路径直接被丢弃。所以主控节点通常会维护一个路径点队列在navigate_to_pose的done回调里弹出下一个点。如果机器人中途被未知障碍挡停需要重新调用A*计算当前点到目标点的路径。我在主控里加了一个定时器每200ms检查机器人当前位姿和目标点的距离误差如果误差超过0.2米且持续多秒就触发重规划。7. 导航集成在迷宫里跑通Nav2的关键细节Nav2是ROS 2里最重量级的导航栈但在迷宫这种窄通道场景里直接套默认参数几乎必失败。下面几个是我在测试中总结出最值得调的地方。7.1 Costmap的膨胀层默认的膨胀半径是0.55米这对60cm宽的车身可能都有点保守。但如果你把膨胀半径设得太大机器人会被迷宫墙体堵死——因为所有通道都变成了“不可通行”区域。我的经验是把inflation_radius设为0.08米左右cost_scaling_factor设为5.0让代价地图只在墙体边缘产生高代价而通道中央保持低代价。这样才能保证机器人在1米宽的走廊里走“S型”而不撞墙。7.2 局部规划器DWB还是TEBNav2默认的局部规划器是DWB它的优势是计算量小但在狭窄通道里容易来回甩尾。TEBTimed Elastic Band则在优化轨迹时会考虑时间最优和避障约束在迷宫里表现好了很多。TEB的参数有几个要动dt_ref时间步长从默认的0.3改成0.2max_vel_x设成0.5m/smax_vel_theta设成1.0rad/smin_obstacle_dist设成0.05m。如果你的车轮轴距较小还需要适当降低acc_lim_x否则急转时容易原地打滑。另外TEB依赖代价地图的代价函数进行优化它对costmap的实时更新要求较高。如果你发现机器人总是偏离路径可以打开teb.verbose日志它会打印每个优化轨迹的评分细节帮助你判断是避障权重过高还是路径跟踪得太死板。7.3 机器人半径和Footprint配置URDF模型里的机器人半径不一定等于footprint因为footprint是导航栈认为的“安全轮廓”。我建议把footprint设成比实际车身小一圈比如实际长度为0.45m就设成一个长为0.4m、宽为0.3m的矩形。这样在迷宫窄通道里会让规划器更有余量。7.4 生命周期节点和自动启动Nav2启动时会启动一堆生命周期节点调试时经常会漏掉“激活”这一步导致导航没反应。我建议直接用现成的nav2_bringup里的launch文件或者写一个只包含自己参数文件的launch把autostart设为True避免每天手动点击激活。8. 避坑记录我在这个项目里踩过的六个深坑这部分是我最想写的内容。这些坑每一个都曾经让我卡了好几个小时上网搜也搜不到完整答案。8.1 坑一Harmonic的仿真时间戳和ROS 2的时钟不同步Gazebo Harmonic发布的话题默认带的是仿真时间戳而ROS 2节点默认用系统时钟。如果/clock话题没有正确发布rtabmap和Nav2的TF时间戳会直接对不上表现为地图疯狂抖动。解决办法是启动Gazebo时加上-r参数运行实时仿真同时在ros_gz_bridge的配置里确保把/clock话题桥接到ROS 2。如果还是不生效检查仿真器是否在暂停状态——gz sim默认是暂停的必须按播放键才开始走时间。8.2 坑二URDF里漏配gazebo插件导致车轮不转很多人写完URDF启动仿真后发现机器人一动不动/odom话题也没有数据。这通常是因为差速驱动插件没有正确加载。Harmonic里对应的插件是gz_ros2_control里的GazeboSimSystem你需要把plugin节点放到URDF的gazebo标签里并指定parameters指向你的ros2_controllers.yaml配置文件。一个更隐蔽的问题是插件加载成功后控制器管理器不会自动激活。你还需要通过ros2 control load_controller手动加载diff_drive_controller或者写个launch让它自动加载。直接用ros2 run跑的话很容易漏这一步。8.3 坑三地图大小和实际世界坐标不一致用rtabmap建图时如果地图的origin设成非零值Nav2的代价地图会以map的坐标系原点去解释。这会导致A*规划的路径在Rviz里看起来正确但机器人实际运动轨迹偏移。解决办法是启动rtabmap节点时用frame_id:map选项并且确认没有额外的静态坐标变换把地图原点搬到别的地方。我建议在启动文件里加一个static_transform_publisher把map到odom之间的变换始终发布成零值防止TF树出现毛刺。8.4 坑四TEB在窄通道里的震荡TEB的参数其实非常依赖机器人运动学。如果你发现机器人在墙边来回摇摆通常有两个原因一是max_vel_x太大导致优化器无法在有限的距离内把速度降下来二是weight_obstacle设得太高机器人为了避免碰撞而大幅绕路。我的调试方法是把max_vel_x先降到0.3然后逐步往上加同时观察/cmd_vel话题的速度曲线。如果速度曲线出现剧烈的正负交替就说明TEB在目标函数里没有找到平滑解此时需要加大dt_ref或者增加weight_kinematics_forward_drive。8.5 坑五雷达点云里缺少墙体信息有时候机器人一进迷宫雷达数据看起来很正常但建图时墙体的轮廓特别模糊。原因多半是gpu_ray插件的光线数量太少或者range设得不够远。gpu_ray的samples参数决定了每帧的激光线条数我设成了360对应每度一条效果就非常清晰。如果你发现雷达的视角有盲区检查一下min_angle和max_angle是否设成了90度或270度而不是360度。8.6 坑六自主导航时机器人总往未知区域跑Nav2的代价地图对未知区域默认是“可通行”的。如果你的迷宫在探索阶段还没完全建图机器人很可能会把未知区域当成潜在通路然后一头撞上去。解决办法是把代价地图的track_unknown_space设为true同时把unknown_cost_value设成一个较大的值让规划器把未知区域视为禁行区。探索阶段结束后再切换成完整的迷宫地图这样行进路径就很安全。9. 从仿真到实车的移植思路仿真里有几点可以预见地在实体机器人上会出问题建议趁早考虑。第一仿真的轮地摩擦、电机响应都偏理想化实车差速控制器的PID参数必须重调第二激光雷达的扫描范围、角度分辨率在不同硬件上差异巨大建图参数要重新标定第三robot_localization的EKF协方差矩阵在仿真里会趋于零实车上则需要根据传感器噪声手动调整。在架构层面只要URDF、控制器、SLAM和Nav2的接口全部按ROS 2标准写仿真到实车的迁移基本只需要更换机器人驱动插件和传感器驱动节点导航和规划部分可以直接复用。根据我自己的测试从零搭建这套环境到机器人成功走出第一个迷宫大概需要一周左右的业余时间。如果你是第一次接触ROS 2建议先把教程里的turtlesim和demo_nodes跑一遍再回来啃这个项目会顺很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进