公司动态

迷宫求解机器人实战:ROS2 Jazzy与Gazebo Harmonic仿真

📅 2026/8/31 21:44:19
迷宫求解机器人实战:ROS2 Jazzy与Gazebo Harmonic仿真
简介本资源是一个面向高校机器人方向本科生与研究生的毕业设计/课程设计实践项目聚焦ROS2环境下自主迷宫求解机器人的仿真开发与算法验证。项目基于ROS2 Jazzy发行版与Gazebo Harmonic仿真平台构建整合路径规划A*、障碍物建模、机器人控制与可视化等功能解决未知迷宫中自主导航与最优路径搜索的核心问题。压缩包共30个文件180KB含13个SDF世界模型文件定义不同尺寸迷宫结构、6个Python核心脚本涵盖路径生成、运动控制、障碍生成等、3个配置文件参数调优关键、3张迷宫示意图5×5/9×9/15×15及README.md使用指南、.gitignore和setup_bridges.sh等工程支撑文件。已有73人学习下载提供开箱即用的完整仿真工作流从世界加载、机器人启动、路径计算到动态避障执行所有模块均按ROS2标准组织于src目录下便于理解ROS2节点通信机制与Gazebo插件集成逻辑是深入掌握机器人感知-决策-执行闭环的优质教学实践素材。 最近在整理一个基于ROS2 Jazzy与Gazebo Harmonic仿真环境的迷宫求解机器人项目趁着刚跑通全流程把搭建过程、算法思路和踩过的坑一次性记录下来。这个项目解决的核心问题很直接在仿真环境里让一台差速驱动小车从迷宫入口出发沿规划好的最短路径走到出口坐标整个任务涉及机器人建模、仿真场景搭建、路径规划算法和底层运动控制四块内容。不管你是刚学完ROS2基础想找个完整实战练手还是已经在调Nav2但想看看迷宫求解这种强结构化任务怎么落地这篇内容都可以拿来直接参考。整个项目跑下来最深的感觉是仿真环境里的“地图已知”和“地图未知”会带来完全不同的实现策略。迷宫这类结构化场景墙的位置天然适合抽象成栅格矩阵求解器只需要在这个矩阵上做BFS或DFS就能拿到最短路径根本不需要跑视觉SLAM。所以在技术选型和实现方案上我选择了最简单、最不容易出错的路径URDF建模 Gazebo Harmonic仿真 BFS求解 坐标点追踪。接下来我把每个环节的细节都拆开讲。1. 项目整体设计与技术选型1.1 需求拆解与功能边界先把需求说清楚。迷宫求解机器人输入是一个固定布局的迷宫仿真环境输出是小车从起点到达终点的运动过程。进一步拆解这个项目其实包含三个核心功能模块。第一是仿真环境模块。需要一个可以在Gazebo Harmonic中加载的迷宫世界包括地面、墙体、起点和终点标记以及一台搭载差速驱动和激光雷达的机器人模型。第二是求解模块。给定迷宫布局通过算法计算出一条从起点到终点的可行路径这一步完全不依赖仿真器只是纯逻辑计算。第三是运动执行模块。机器人根据求得的路径点序列发布底盘速度指令逐步移动到目标点。这三块各自独立又通过坐标数据和速度指令串起来。我特别建议把求解和执行拆成两个节点而不是揉在一起因为这样调试时可以单独验证算法正确性也能单独测试底盘响应排错成本会低很多。项目边界上我没有加入在线建图和动态避障因为迷宫是静态环境用已知地图加路径追踪就够了加SLAM反而会把问题复杂化后面可以扩展。1.2 技术栈选型为什么是Jazzy和Harmonic这个项目选用的ROS2发行版是Jazzy JaliscoGazebo版本是Harmonic两个组合是针对Ubuntu 24.04的长期支持搭配。Jazzy是ROS2目前较新的LTS版本官方支持到2029年API比Humble时代更规范Python依赖也更干净。Harmonic是Gazebo的长期支持版本官方称为gz-sim 8相比Gazebo Classic即gazebo 11在渲染、物理引擎和插件机制上都有很大变化。选这对组合的一大原因是它们对应同一个Ubuntu发行版。Ubuntu 24.04上直接通过apt安装ros-jazzy-desktop和gz-harmonic不需要牵扯第三方源去手动编译一堆依赖安装过程比较干净。另外一个原因是Harmonic自带一个专门面向ROS2的桥接包ros_gz能把仿真时钟、里程计、激光数据和速度指令都转成ROS2话题省去了以前Gazebo Classic那种写复杂shared library插件的过程。这里要特别提醒一下网上大量教程还是基于ROS2 Humble和Gazebo Classic直接照搬通常会翻车因为经典Gazebo的插件和Harmonic的插件体系完全不兼容。所以如果你决定用这套新组合务必用Harmonic自己的方式去写插件和桥接。1.3 系统架构与数据流整个系统的运行流程可以描述成一个单向流水线。求解节点读取迷宫矩阵和起点终点索引通过BFS算法计算出路径格点序列再把格点换算成世界坐标并以路径点列表的形式发布到话题。导航节点订阅这些路径点结合机器人当前的里程计位姿计算与目标点的角度差和距离差换算成线速度和角速度指令。底盘模型收到指令后在Gazebo中运动同时里程计话题持续反馈位置变化导航节点据此判断是否到达目标点并切换到下一个点。这种架构的好处是每个环节都可以独立观察。求解节点可以用ros2 topic echo直接查看路径点导航节点的日志会打印当前目标点和剩余距离底层就可以用rviz2或Gazebo视角观察底盘是否在正确移动。整个数据链路一点都不复杂却把模块化开发的核心思想体现得很清楚。2. 从零搭建仿真环境与机器人模型2.1 Ubuntu 24.04上的ROS2 Jazzy安装我是在虚拟机里完成整个环境搭建的宿主机只需要能跑VMware或VirtualBox分配4核CPU和8GB内存即可。先把Ubuntu 24.04桌面版装好建议直接用中文或英文系统都行但终端环境变量和软件源必须保证正常。装ROS2 Jazzy有两种方式我推荐直接使用官方apt源。先确保系统locale为UTF-8然后按顺序添加ROS2软件源sudo apt update sudo apt install software-properties-common curl sudo add-apt-repository universe sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release echo $UBUNTU_CODENAME) main | sudo tee /etc/apt/sources.list.d/ros2.list /dev/null sudo apt update sudo apt install ros-jazzy-desktop python3-rosdep python3-colcon-common-extensions装完记得在~/.bashrc里加上source。这一步很多人会漏导致后面每个终端都要手动source非常崩溃。echo source /opt/ros/jazzy/setup.bash ~/.bashrc echo source /usr/share/colcon_cd/function/colcon_cd.sh ~/.bashrc source ~/.bashrc验证安装是否成功开两个终端分别运行ros2 run demo_nodes_cpp talker和ros2 run demo_nodes_py listener能收到消息就是装好了。如果在虚拟机上运行很慢可以顺手把3D加速打开对Gazebo渲染有帮助。2.2 Gazebo Harmonic安装与验证在Ubuntu 24.04上安装Harmonic很简单官方已经放进了默认软件源不需要走ROS2的源sudo apt install gz-harmonic装完后运行gz sim -h看一下帮助信息确认版本是Gazebo Harmonic。想测试图形界面是否正常可以运行gz sim shapes.sdf会打开一个包含方体、球体等形状的空世界。如果黑屏或闪退通常是显卡驱动或虚拟机3D加速问题后面避坑部分会讲。Harmonic和ROS2的桥接需要单独装ros_gz相关包。Jazzy对应的包名是ros-jazzy-ros-gz和ros-jazzy-ros-gz-bridge直接apt安装即可。2.3 机器人模型URDF建模与Gazebo适配我给机器人用的是典型的差速两轮驱动结构车体是一个长方体前后来两个万向支撑轮左右两个驱动轮。URDF文件里需要同时定义视觉、碰撞和惯性参数Gazebo里才能稳定仿真。这里给出核心的xacro模型片段robot namemaze_robot xmlns:xacrohttp://www.ros.org/wiki/xacro link namebase_link visual geometry box size0.30 0.20 0.08/ /geometry material nameblue/ /visual collision geometry box size0.30 0.20 0.08/ /geometry /collision inertial mass value2.0/ inertia ixx0.01 iyy0.02 izz0.01 ixy0 ixz0 iyz0/ /inertial /link link nameleft_wheel visual geometry cylinder radius0.05 length0.03/ /geometry /visual collision geometry cylinder radius0.05 length0.03/ /geometry /collision inertial mass value0.2/ inertia ixx0.0001 iyy0.0001 izz0.0001/ /inertial /link joint nameleft_wheel_joint typecontinuous parent linkbase_link/ child linkleft_wheel/ origin xyz0 0.12 -0.04/ axis xyz0 0 1/ /joint /robot这里的核心点是轮子旋转轴必须是y轴即xyz0 1 0我上面的写法留了个错误作为提醒实际操作一定要用xyz0 1 0否则轮子会横着转。Gazebo里差速驱动插件识别的是轮子joint的旋转方向。每个link必须给出惯性矩阵数值可以粗略但缺失会导致模型抖动或直接掉入地面。另外我加了一个二维激光雷达用一个圆柱体表示并挂载在车体上部。仿真里激光数据直接从Harmonic传感器系统生成经过桥接变成/scan话题给后续定位和避障留了接口。当模型文件写好之后在Gazebo里加载模型的命令是export GZ_SIM_RESOURCE_PATH$PWD gz sim -r empty.sdf使用ros_gz_bridge把/cmd_vel和/odom桥接出来。具体bridge配置后面讲。2.4 构建迷宫场景用墙体拼出10x10网格迷宫迷宫场景我选择用SDF世界文件直接定义墙体。这样最可控因为每堵墙的坐标就是已知的后面求解时可以直接转化成栅格地图不需要再来一次在线建图。一个10x10的网格迷宫每个格子边长0.5米墙体高度0.2米厚度0.05米。在SDF里定义一堵墙是这样的model namewall_1_2 pose0.25 1.25 0.1 0/pose link namelink collision geometry box size0.5 0.05 0.2/ /geometry /collision visual geometry box size0.5 0.05 0.2/ /geometry /visual /link /model手动写10x10的墙体很容易让人崩溃更聪明的办法是写一个Python脚本根据栅格矩阵自动生成SDF文件。比如矩阵中某两个相邻格子之间有一堵墙脚本就输出一个对应坐标的box模型。推荐的做法是先用一个二维数组定义迷宫结构0表示空地1表示墙然后脚本遍历数组并输出所有墙体到world.sdf。这样算法和场景完全一致BFS求解和Gazebo场景之间不会出现错位。3. 迷宫求解算法与导航控制实现3.1 迷宫转化成栅格地图迷宫不管在仿真里是实体墙还是抽象网格第一步都是建立一个统一的地图模型。我这里的做法很简单迷宫本身是20x20的规整网格但墙和通道都占格子。更常见的做法是给每个通道格子一个编号墙格子设为不可通行然后以格子为节点做图搜索。我定义的地图是一个二维int数组1表示墙0表示通道。给定起点栅格如左下角和终点栅格如右上角BFS可以快速找到最短路径。要注意的是BFS的邻居不能只考虑上下左右四个方向还需要检查当前节点是否越界或撞墙这一步很多初写BFS的人容易漏。3.2 BFS与DFS为什么选BFS迷宫求解最直接的算法是BFS和DFS。DFS走迷宫非常像人类“摸着墙走”实现简单但找到的路径不一定最短在深度很大的迷宫里还可能绕远路。BFS把所有相邻可行点同时向外扩展天然保证第一次扩展到终点时路径最短所以我最终选择了BFS。实际实现里我用了双端队列deque做BFS的标准写法from collections import deque def bfs(maze, start, end): rows, cols len(maze), len(maze[0]) visited [[False] * cols for _ in range(rows)] parent {} queue deque([start]) visited[start[0]][start[1]] True while queue: r, c queue.popleft() if (r, c) end: path [] cur (r, c) while cur ! start: path.append(cur) cur parent[cur] path.append(start) return path[::-1] for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]: nr, nc r dr, c dc if 0 nr rows and 0 nc cols and not visited[nr][nc] and maze[nr][nc] 0: visited[nr][nc] True parent[(nr, nc)] (r, c) queue.append((nr, nc)) return None这段代码很短但它把BFS的核心逻辑都体现出来了。visited数组防止重复访问parent字典记录路径回溯信息deque保证先进先出。实际在ROS2节点里我封装成求解节点输入是地图数组和起终点索引输出是路径点序列。3.3 从栅格路径到世界坐标迷宫格子和世界坐标之间的换算一定要统一好。我定义每个格子边长0.5米迷宫原点在世界坐标(0, 0)那么格子(r, c)的中心世界坐标就是world_x c * 0.5 0.25 world_y r * 0.5 0.25这里的0.25是格子边长的一半是为了取格子中心。如果你的迷宫原点不在(0,0)还要加一个偏移量。我在调试时发现如果直接用格子索引乘以边长小车会偏向格子边缘路径追踪时容易撞到墙所以中心点偏移必须加。路径生成后我把它发布为std_msgs/Float32MultiArray或者自定义的PoseArray导航节点订阅后按顺序追踪。3.4 运动控制简单的点追踪PID有了目标点坐标小车怎么走过去我没有上Nav2因为在这个场景里开销太大自己写一个点追踪控制器就够了。核心思路是每次取路径列表里离当前机器人最近或路径中第一个未到达的点作为目标计算机器人当前位置到目标点的航向角再与当前机器人朝向作差用角速度PID把机器人转向目标方向线速度则根据距离调整距离远就全速距离近就减速。关键代码段如下def control_loop(self): rate self.create_rate(20) while rclpy.ok(): # 获取当前位姿从odom话题或tf x, y, yaw self.current_pose if not self.path_poses: break target self.path_poses[0] dx target[0] - x dy target[1] - y dist math.hypot(dx, dy) if dist 0.1: self.path_poses.pop(0) continue target_yaw math.atan2(dy, dx) yaw_error atan2(sin(target_yaw - yaw), cos(target_yaw - yaw)) angular 1.2 * yaw_error linear min(0.4, dist) self.publish_cmd_vel(linear, angular)这个控制器的核心是角度差的计算一定要用atan2处理否则角度跨过正负π时会出大问题小车会原地打转。线速度用dist做了限幅保证接近目标点时不会冲过头。实际项目里也可以换用Nav2但我要说的是对于迷宫这种完全结构化的场景自写路径追踪比Nav2少很多调参成本尤其是在仿真里不需要考虑real-time约束时20Hz的控制频率已经足够稳定。3.5 里程计与TF坐标系要让控制器知道机器人当前在哪必须处理里程计话题。Harmonic里可以通过ros_gz_bridge把/odom桥接出来我直接用nav_msgs/Odometry消息订阅解析位置和姿态。姿态的四元数要转成yaw角可以用tf_transformations里的euler_from_quaternion或者自己写公式yaw math.atan2(2 * (w * z x * y), 1 - 2 * (y * y z * z))这里的四元数顺序是(x, y, z, w)很多人容易搞错成(w, x, y, z)。如果算出来的yaw一直在跳先检查四元数顺序。坐标系方面我让base_link作为机器人根部odom作为世界系。Gazebo仿真里可以通过ros_gz_bridge自动发布odom到base_link的TF但有时候需要手动检查是否发布正常。可以在rviz2中添加TF显示来确认。4. 完整运行流程与关键代码4.1 启动仿真环境整个系统启动分三部分先启动Gazebo世界再启动机器人模型和桥接最后启动求解导航节点。我写了一个launch文件来统一管理from launch import LaunchDescription from launch_ros.actions import Node from launch.actions import ExecuteProcess def generate_launch_description(): return LaunchDescription([ ExecuteProcess( cmd[gz, sim, maze.world], outputscreen ), Node( packagerobot_state_publisher, executablerobot_state_publisher, arguments[maze_robot.urdf] ), Node( packageros_gz_bridge, executableparameter_bridge, parameters[{ config_file: bridge.yaml }] ), Node( packagemaze_solver, executablesolver_node, outputscreen ), Node( packagemaze_solver, executablecontroller_node, outputscreen ), ])这个启动顺序是有讲究的Gazebo先启动保证世界加载完成后再启动桥接和控制节点否则可能出现话题连接不上或模型还没加载就收到速度指令的情况。bridge.yaml里主要配置以下几个话题/clockgz - ROS2仿真时钟/odomgz - ROS2里程计/scangz - ROS2激光雷达/cmd_velROS2 - gz底盘速度指令4.2 迷宫求解节点核心实现求解节点本质上是个参数化节点输入地图矩阵和起终点输出路径点。为了项目复用性我直接把迷宫矩阵放在config.yaml里这样换一个迷宫只需要改配置不用改代码。class MazeSolverNode(Node): def __init__(self): super().__init__(maze_solver_node) self.maze self.load_maze_from_config() self.start (0, 0) self.end (19, 19) self.path_pub self.create_publisher(PoseArray, /maze_path, 10) self.solve_btn self.create_service(Trigger, /solve_maze, self.solve_callback) def solve_callback(self, request, response): path bfs(self.maze, self.start, self.end) poses [] for (r, c) in path: p Pose() p.position.x c * 0.5 0.25 p.position.y r * 0.5 0.25 poses.append(p) msg PoseArray(posesposes) self.path_pub.publish(msg) response.success True response.message fpath length: {len(path)} return response用Trigger服务触发求解的好处是你可以重复调用改了起点终点后不用重启节点非常适合参数调试。PoseArray的发布也能直接在rviz2里可视化显示路径。4.3 导航控制器节点实现导航控制器订阅PoseArray和/odom然后按路径点顺序追踪。这里还需要注意一个细节如果Gazebo里模型加载晚控制器可能先收到路径点但收不到/odom需要做超时保护避免速度指令对零位姿发送导致机器人原地打转。我在节点里加了个简单的等待逻辑收到/odom前不发送速度指令只有收到后才进入控制循环。这一步在实际调试中帮我省了很多时间因为launch启动后Gazebo加载模型可能要几秒。同时控制器里还要保存一个当前目标点索引到达一个点后自动指向下一个点。循环结束条件是所有路径点都走完此时发布零速度指令让车停下。4.4 调参经验整个项目最需要调的是PID参数。我实测的参考值是转向比例系数1.2线速度上限0.4 m/s角速度上限2.0 rad/s到达判定距离0.1米。如果小车在转弯时来回震荡先把转向系数调低到0.6左右如果转弯太慢适当提高线速度上限但不要超过0.5否则惯性太大会把路线走偏。还有一个容易被忽略的参数是轮胎摩擦系数。Gazebo Harmonic默认的轮子摩擦可能导致小车在急转弯时轻微打滑我在URDF的gazebo reference块里把左右驱动轮的mu值调到了1.0转弯明显更稳。5. 高频问题与避坑实录5.1 常见问题速查表现象可能原因解决办法Gazebo启动黑屏或闪退虚拟机未开启3D加速或显卡驱动不兼容宿主机VMware设置里开启加速3D图形更新虚拟机内显卡驱动模型掉入地面URDF缺惯性参数或碰撞几何和视觉几何不一致给每个link补inertial碰撞体要贴合几何体并居中轮子横着转joint轴方向错误驱动轮joint axis设置为xyz0 1 0发出/cmd_vel但车不动桥接话题名不匹配检查bridge.yaml里cmd_vel的左右方向配置确认ros2 topic list里话题正常小车定位持续偏移里程计桥接未启用或TF发布异常确认/odom有数据rviz2里能看到base_link必要时手动发布map-odom静态TFBFS路径与实际墙面重叠栅格索引到世界坐标的换算关系不一致检查原点偏移和格子边长用路径可视化排查转弯时来回震荡角度PID比例系数过大把转向系数调小到0.6或加一个小的微分项虚拟机里Gazebo卡成PPT内存不足或CPU分配不够虚拟机分配至少8GB内存和4核CPU关闭其他高占用程序5.2 独家避坑经验第一条经验是关于Harmonic和旧教程的差异。我在网上查资料时大量文章还是围绕Humble加Gazebo Classic插件的写法完全不通用。Harmonic环境里千万别试图去装libgazebo_ros_diff_drive.so那是classic时代的插件装了也用不了。正确思路是用ros_gz_bridge或者gz_ros2_control一切模型控制都通过/clock和/odom的桥接来打通。第二条是关于迷宫栅格和场景一致性。我最初的做法是手动搭迷宫墙体然后单独写一份迷宫矩阵给BFS用结果跑了两次就发现路径和墙面总是有一两个格子对不上后来改成用Python脚本从同一份矩阵自动生成SDF墙体地图和迷宫表达完全统一错位问题彻底消失。这个思路很多项目里都值得借鉴地图的权威来源只能有一个其他表达全部从这个来源派生。第三条是关于控制频率。我测试了10Hz、20Hz和50Hz三种频率10Hz时小车在急转弯处明显卡顿路径容易被拉偏50Hz虽然平滑但CPU占用很高在虚拟机上Gazebo渲染会掉帧。最终20Hz是最优解既保证了控制的实时性又不会拖垮仿真性能。5.3 扩展方向这个项目现在跑通的是静态已知地图迷宫求解后续扩展空间还是挺大的。比如把求解算法换成A*利用启发式搜索进一步提高求解效率或者把ROS2节点里的点追踪控制器升级成Nav2 costmap让小车具备避障能力就能在未知迷宫里边建图边规划。再比如把Gazebo里的激光雷达数据接出来用SLAM Toolbox在线建图配合AMCL定位整套方案就非常接近商业机器人产品的导航栈了。如果手头有真实的差速机器人底盘这套求解和路径追踪节点几乎可以原封不动地移植过去只要把Gazebo的odom换成底盘驱动器的Odometry话题把cmd_vel直接接到真实下位机就行。这也是我选择写轻量控制器而不是堆大型框架的原因逻辑清晰每一步都能解释清楚后续迁移和改造都不费力。最后再分享一个调试小技巧在Gazebo里运行时先用rviz2把/maze_path和/odom一起显示出来观察路径点和实际轨迹之间的偏差。如果系统运行正常这两个轨迹应该是基本重合的。一旦出现偏差就能立刻判断是控制器问题还是坐标变换问题不用盲猜。这个项目做完之后我自己对ROS2话题通信、URDF模型定义以及路径规划算法的理解都比单纯看文档深刻得多强烈建议找个完整任务动手做一遍。本文还有配套的精品资源点击获取