简介:这份资源是一套基于ROS 2与Navigation 2框架搭建的智能巡检机器人仿真系统,面向机器人方向的学生、开发者及工业巡检与仓库管理场景的研发人员,用于在无实体硬件条件下完成运动控制、环境感知与自主导航的软件验证。压缩包共63个文件,约96KB,以20个Python脚本、12个xacro模型文件、5个XML配置、4个YAML参数文件为主,另含sdf、urdf、world、rviz、pgm地图及srv接口定义等,覆盖机器人描述、导航配置与仿真环境搭建。系统集成运动控制、激光雷达与摄像头环境感知、路径规划与避障、语音播报、图像采集以及目标点循环巡检等模块,可支撑周期性设备监控类任务。目前已有110人学习下载。读者可借此获得一套结构完整的仿真工程,用于快速迭代算法、降低研发成本,并作为进一步开发与测试的平台基础。
1. 从零搭一套能跑循环巡检的 ROS 2 仿真系统,到底要打通哪几层
工业巡检和仓库管理这两类场景,最近两年被问得特别多。原因不复杂:真机部署贵、场地协调难、安全风险高,而一套基于 ROS 2 和 Navigation 2 的仿真系统,能在笔记本上把「机器人怎么走、怎么感知、怎么自己找路、怎么报话、怎么拍照、怎么一圈圈巡」这条链路完整跑一遍。标题里这一长串功能——运动控制、环境感知、自主导航、语音播报、图像采集、目标点循环巡检——不是堆砌,它们恰好对应一个巡检机器人从底盘到应用层的六个真实模块。这篇文章面向的是想自己复现一套、或者正在评估这条路值不值得投入的工程师。我会按「先立住概念、再给可抄的配置和代码、最后讲清楚哪里最容易翻车」的顺序讲,新手能跟着搭,熟手能直接看参数边界和踩坑点。
2. 仿真系统分层拆解:六个功能模块各自归谁管
2.1 从底盘到应用,六层职责怎么切
一套能跑的巡检仿真,最怕的就是把所有逻辑塞进一个节点。我一般按职责切成六层,每层只干一件事,层与层之间用 topic、service、action 通信。
| 层级 | 模块 | 主要载体 | 对外接口 |
|---|---|---|---|
| 底盘层 | 运动控制 | 差速驱动插件 + 控制器 | /cmd_vel |
| 感知层 | 环境感知 | 激光雷达 + 深度相机 | /scan、/camera/depth |
| 定位层 | 定位与建图 | AMCL / SLAM | /amcl_pose、/map |
| 导航层 | 自主导航 | Navigation 2 | NavigateToPoseaction |
| 应用层 | 图像采集 | 相机订阅节点 | 图像文件 |
| 交互层 | 语音播报 | TTS 节点 | 播报 topic |
这么切的好处是:导航挂了不影响拍照,语音卡了不影响走位。巡检任务本质是「导航到点 → 停稳 → 采集 → 播报 → 去下一个点」,每一环都能单独替换和调试。
2.2 为什么选 Navigation 2 而不是自己写路径规划
很多人第一反应是「我自己写个 A* 不就行了」。真做过就知道,工业巡检要的不是一条静态路径,而是动态避障、代价地图实时更新、恢复行为、路径平滑这一整套。Navigation 2 把这些都封装成了可配置的 behavior tree 和 plugin,你只需要调参和接目标点。自己写,光是一个局部代价地图的膨胀半径和恢复行为就够调一周。常见做法是直接用 Navigation 2 的nav2_bringup,把精力放在业务逻辑上。
2.3 仿真环境用什么搭:Gazebo 与 URDF 的分工
仿真环境我一般用 Gazebo(新版对应 Gazebo Sim),机器人模型用 URDF/Xacro 描述。URDF 负责几何、关节、惯性矩阵,Gazebo 负责物理引擎、传感器插件、世界文件。两者通过gazebo_ros系列插件桥接。关键点是:URDF 里的惯性参数不能瞎填,填错了机器人会在仿真里抖成筛子,这是新手第一个血泪坑。
<!-- 差速驱动插件,挂在 base_link 上 --> <gazebo> <plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so"> <ros> <namespace>/</namespace> </ros> <!-- 左右轮关节名,必须和 URDF 里一致 --> <left_joint>left_wheel_joint</left_joint> <right_joint>right_wheel_joint</right_joint> <!-- 轮距和轮径,单位米,填错直接导致里程计漂移 --> <wheel_separation>0.35</wheel_separation> <wheel_diameter>0.1</wheel_diameter> <!-- 最大轮速,决定机器人能跑多快 --> <max_wheel_torque>20</max_wheel_torque> <max_wheel_acceleration>1.0</max_wheel_acceleration> <!-- 发布里程计和 tf --> <publish_odom>true</publish_odom> <publish_odom_tf>true</publish_odom_tf> <publish_wheel_tf>true</publish_wheel_tf> <odometry_frame>odom</odometry_frame> <robot_base_frame>base_link</robot_base_frame> </plugin> </gazebo>这段插件的逻辑是:Gazebo 接管左右轮关节,根据收到的/cmd_vel解算轮速,再反推里程计并发布odom到base_link的 tf。参数里wheel_separation和wheel_diameter是最容易填错的,它们直接决定里程计精度。如果发现机器人走直线却画弧,八成是这两个值和你 URDF 里的实际几何对不上。
3. 把运动控制和环境感知在仿真里跑通
3.1 运动控制:从 cmd_vel 到轮速的完整链路
运动控制这条链路是:你的业务节点发/cmd_vel(Twist 消息,线速度 + 角速度)→ 差速插件解算 → 左右轮关节速度 → Gazebo 物理引擎 → 里程计反馈。调试时最直接的办法是手动发一条速度命令,看机器人动不动。
# 让机器人以 0.2 m/s 前进,持续 3 秒 ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \ "{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}" \ --rate 10--rate 10表示每秒发 10 次。差速插件通常有超时保护,如果超过一定时间收不到新命令会自动停车,所以调试时要么持续发,要么把超时参数调大。线速度x是前进方向,角速度z是绕垂直轴旋转,正值左转。这两个值不要一上来就给大,先给 0.1 到 0.2 试,确认方向对了再加速。
3.2 环境感知:激光雷达和深度相机怎么配
巡检机器人感知主要靠两样:2D 激光雷达做导航避障,深度相机做近距离检测和拍照。激光雷达在 URDF 里挂一个ray传感器插件,输出/scan;深度相机挂camera插件,输出图像和点云。
<!-- 2D 激光雷达插件 --> <gazebo reference="laser_link"> <sensor type="ray" name="laser_sensor"> <pose>0 0 0 0 0 0</pose> <visualize>true</visualize> <update_rate>10</update_rate> <ray> <scan> <horizontal> <!-- 采样点数,360 表示 1 度一个点 --> <samples>360</samples> <min_angle>-3.14159</min_angle> <max_angle>3.14159</max_angle> </horizontal> </scan> <range> <!-- 量程,工业场景一般 0.1 到 10 米够用 --> <min>0.1</min> <max>10.0</max> </range> </ray> <plugin name="laser_controller" filename="libgazebo_ros_ray_sensor.so"> <ros> <remapping>~/out:=scan</remapping> </ros> <output_type>sensor_msgs/LaserScan</output_type> <frame_name>laser_link</frame_name> </plugin> </sensor> </gazebo>update_rate是雷达刷新率,10 Hz 对巡检够用,调太高会吃 CPU。samples是每圈采样点数,360 点对应 1 度分辨率,仓库货架密集时可以加到 720。frame_name必须和 URDF 里的 link 名一致,否则 tf 树断了,Navigation 2 直接报错找不到雷达坐标系。
3.3 用 ros2 topic 验证感知数据是否正常
配完插件别急着上导航,先确认数据在发。
# 看雷达数据频率和内容 ros2 topic hz /scan ros2 topic echo /scan --once # 看深度相机图像 ros2 topic hz /camera/image_rawros2 topic hz显示的频率如果远低于update_rate,说明仿真算力不够或者插件配置有问题。echo --once看一帧数据,重点确认frame_id和ranges数组长度对不对。这一步是后面导航能不能跑起来的前提,感知数据不对,Navigation 2 的代价地图就是一片空白。
4. 自主导航与目标点循环巡检的落地实现
4.1 Navigation 2 参数配置:代价地图和规划器怎么调
Navigation 2 的核心是几个 yaml 参数文件:nav2_params.yaml里配代价地图、规划器、控制器、恢复行为。工业巡检场景我一般这么调:
local_costmap: local_costmap: ros__parameters: # 局部代价地图尺寸,巡检机器人 3x3 米够用 width: 3 height: 3 resolution: 0.05 # 膨胀半径,比机器人半径大一点,防止贴墙 inflation_layer: inflation_radius: 0.55 cost_scaling_factor: 3.0 # 障碍物来源 observation_sources: scan scan: topic: /scan max_obstacle_height: 2.0 clearing: true marking: true controller_server: ros__parameters: # 控制器频率,20 Hz 对差速底盘够用 controller_frequency: 20.0 # 最大线速度和角速度,和底盘能力匹配 max_vel_x: 0.3 max_vel_theta: 1.0 # 到点容差,太大停不准,太小来回蹭 xy_goal_tolerance: 0.15 yaw_goal_tolerance: 0.1inflation_radius是最关键的参数之一。设太小,机器人贴着货架走容易刮蹭;设太大,窄通道直接过不去。经验值是机器人半径加 0.15 到 0.2 米。xy_goal_tolerance决定到点精度,巡检要拍照的话建议 0.1 到 0.15 米,太大会导致相机对不准目标。
4.2 目标点循环巡检:一个 action 客户端搞定
循环巡检的本质是:维护一个目标点列表,依次发NavigateToPoseaction,每个点到了之后执行采集和播报,然后去下一个,走完一圈从头再来。
import rclpy from rclpy.node import Node from rclpy.action import ActionClient from nav2_msgs.action import NavigateToPose from geometry_msgs.msg import PoseStamped class PatrolNode(Node): def __init__(self): super().__init__('patrol_node') self.client = ActionClient(self, NavigateToPose, 'navigate_to_pose') # 巡检点列表:x, y, 朝向四元数 z/w self.waypoints = [ (1.0, 0.0, 0.0, 1.0), (1.0, 2.0, 0.707, 0.707), (0.0, 2.0, 1.0, 0.0), ] self.idx = 0 def send_next(self): goal = NavigateToPose.Goal() x, y, qz, qw = self.waypoints[self.idx] goal.pose.header.frame_id = 'map' goal.pose.pose.position.x = x goal.pose.pose.position.y = y goal.pose.pose.orientation.z = qz goal.pose.pose.orientation.w = qw self.client.wait_for_server() # 发送目标点,回调里处理到达后的采集和播报 self.client.send_goal_async(goal).add_done_callback(self.on_goal_accepted) def on_goal_accepted(self, future): goal_handle = future.result() goal_handle.get_result_async().add_done_callback(self.on_arrived) def on_arrived(self, future): # 到点后:触发图像采集 + 语音播报 self.get_logger().info(f'到达巡检点 {self.idx}') # 循环到下一个点 self.idx = (self.idx + 1) % len(self.waypoints) self.send_next()这段代码的关键在on_arrived回调:到达目标点后先做业务动作(拍照、播报),再切到下一个点。waypoints列表里的朝向用四元数表示,qz和qw控制机器人到点后的朝向,拍照场景下这个朝向很重要,决定了相机对着哪。frame_id必须是map,和 Navigation 2 的全局坐标系一致。
4.3 语音播报和图像采集怎么挂进巡检流程
语音播报我一般用一个独立节点订阅一个字符串 topic,收到就调 TTS 播出来。图像采集节点订阅相机 topic,收到触发信号存一帧。
# 图像采集节点核心逻辑 def image_callback(self, msg): if not self.capture_flag: return # 用 cv_bridge 把 ROS 图像转成 OpenCV 格式 cv_image = self.bridge.imgmsg_to_cv2(msg, 'bgr8') filename = f'/tmp/inspect_{self.point_id}_{self.stamp}.jpg' cv2.imwrite(filename, cv_image) self.capture_flag = False # 采一帧就关,避免刷屏capture_flag是个开关,巡检到点时才置 True,采完立刻置 False。这样避免机器人移动过程中疯狂存图把磁盘写满。播报节点同理,收到播报请求才发声,不要一直循环播。
5. 这套仿真系统最容易翻车的几个地方
5.1 现象:机器人原地打转或画圈
原因:里程计参数和 URDF 几何不一致,最常见的是wheel_separation填错,或者左右轮关节名写反。解决:先单独发/cmd_vel直线命令,看odom的 y 值有没有漂移;再发纯旋转命令,看机器人是不是绕自身中心转。对不上就回去核对 URDF 里的轮距和插件参数。
5.2 现象:Navigation 2 启动后报 tf 超时
原因:tf 树断了,通常是某个 link 的frame_name和 URDF 不一致,或者雷达、相机插件没发布 tf。解决:ros2 run tf2_tools view_frames生成 tf 树图,看map → odom → base_link → laser_link这条链有没有断。断在哪一环就查哪个插件的frame_name。
5.3 现象:代价地图一片空白,规划不出路径
原因:observation_sources里的 topic 名写错,或者雷达数据frame_id不在 tf 树里。解决:ros2 topic echo /scan --once确认数据在发,再看frame_id是不是laser_link,然后确认这个 link 在 tf 树里。两个都对,代价地图就会有障碍物。
5.4 现象:到点停不准,反复前后蹭
原因:xy_goal_tolerance和yaw_goal_tolerance设太小,控制器在容差边缘反复修正。解决:适当放大容差,或者调低controller_frequency让控制更平滑。巡检拍照场景,0.15 米和 0.1 弧度是比较好用的值。
5.5 现象:仿真跑一会儿就卡顿
原因:雷达采样点太多、相机分辨率太高、或者 Gazebo 物理步长太小。解决:雷达samples降到 360,相机分辨率降到 640x480,物理引擎max_step_size调到 0.001 到 0.005 之间。仿真不是越精细越好,够用就行。
6. 让循环巡检更稳的两个进阶技巧
第一个技巧是给巡检点加「到达确认」。上面代码里on_arrived是 action 返回成功就切下一个点,但实际中机器人可能因为容差判定「到了」其实偏了。我的做法是在到点后加一个短暂的稳定等待,再读一次amcl_pose,确认位置在容差内才触发采集,否则重发一次目标点。这个后悔药能省掉大量「照片拍歪了」的返工。
def on_arrived(self, future): # 到点后等 1 秒让底盘停稳 self.create_timer(1.0, self.confirm_and_capture) def confirm_and_capture(self): # 读当前位姿,确认在容差内 if self.pose_within_tolerance(): self.trigger_capture() self.trigger_tts() else: self.send_next() # 没停准,重发第二个技巧是把巡检点配置外置成 yaml 文件,而不是硬编码在代码里。仓库布局一变,改配置就行,不用重新编译。
# waypoints.yaml waypoints: - {id: 1, x: 1.0, y: 0.0, qz: 0.0, qw: 1.0, action: capture} - {id: 2, x: 1.0, y: 2.0, qz: 0.707, qw: 0.707, action: capture_tts} - {id: 3, x: 0.0, y: 2.0, qz: 1.0, qw: 0.0, action: tts}每个点带一个action字段,决定到点后是只拍照、只播报还是都做。这样一套代码能适配不同巡检路线,扩展性比硬编码强太多。我自己踩过的最大坑就是早期把点位写死在代码里,后来仓库货架挪了一次,改代码改到怀疑人生。现在一律外置配置,改完重启节点就生效。希望帮到你。
本文还有配套的精品资源,点击获取