2026 ROS 2 Lyrical 工程实例(一):从零搭建 AMR 激光 SLAM——Gazebo、slam_toolbox 与 Nav2 全链路跑通
系列: 2026 ROS 2 Lyrical 工程实例
环境: Windows 11、WSL2、Ubuntu 26.04、ROS 2 Lyrical、Gazebo Sim、slam_toolbox、Nav2、RViz2
项目名称:amr_slam
完整代码: zephastra/robotics:01-amr-slam
关键词: ROS 2、Lyrical、AMR、SLAM、Gazebo、slam_toolbox、Nav2、RViz2
前言
前面几篇文章分别讨论了 ROS 2 Lyrical 的编译、DDS、Gazebo、TF、SLAM 与 Nav2 排错。这一次不再单独讲某个知识点,而是把这些组件连接起来,完成一个可以实际运行的 AMR 激光 SLAM 工程实例。
这个实例实现了以下完整流程:
Gazebo Sim 启动差速 AMR
↓
ros_gz_bridge 桥接时钟、激光、里程计与速度指令
↓
slam_toolbox 在线建立二维栅格地图
↓
保存 map.pgm 与 map.yaml
↓
Nav2 加载地图并使用 AMCL 定位
↓
在 RViz2 中设置目标点,机器人自主规划并到达目标
这不是只有目录和配置文件的演示工程。本文记录的最终版本已经完成实际验收:机器人可以移动、地图可以生成和保存、Nav2 各生命周期节点能够进入 active,导航目标最终返回 SUCCEEDED。
项目名称是
amr_slam。其中 AMR 是 Autonomous Mobile Robot(自主移动机器人),不要误写成arm_slam;后者更容易让人误以为是机械臂项目。
一、最终实现效果
本实例完成了两种工作模式。
1. 在线建图模式
启动 Gazebo、Bridge、TF、slam_toolbox 和 RViz2 后,使用键盘控制机器人在房间中运动。slam_toolbox 根据 /scan、里程计和 TF 持续建立地图。
键盘遥控 → /cmd_vel → Gazebo 差速底盘
Gazebo LiDAR → /scan → slam_toolbox
Gazebo Odometry → /odom → odom_to_tf
slam_toolbox → /map + map→odom
2. 静态地图导航模式
完成建图并保存地图后,停止建图进程,再启动 Nav2:
map_server 加载地图
↓
AMCL 完成定位并发布 map→odom
↓
Planner Server 生成全局路径
↓
Controller Server 输出 /cmd_vel
↓
Gazebo 中的 AMR 执行运动
这里最重要的设计是:建图时由 slam_toolbox 发布 map → odom,静态地图导航时由 AMCL 发布 map → odom,两个定位来源不会同时运行。
二、项目目录结构
最终项目目录如下:
amr_slam/
├── README.md # 建图运行说明
├── README_Nav2.md # Nav2 运行说明
├── amr_env.sh # ROS、DDS、WSLg 环境配置
├── amr_world.sdf # 仿真世界、障碍物、差速底盘和 2D LiDAR
├── bridge_config.yaml # Gazebo 与 ROS 2 话题桥接
├── odom_to_tf.py # 将 /odom 转换为 odom→base_link TF
├── slam_params.yaml # slam_toolbox 参数
├── nav2_params.yaml # ROS 2 Lyrical 对应的 Nav2 参数
├── nav2_params_legacy.yaml # 修复前的参数,仅用于对照
├── rviz_slam.rviz # 建图模式 RViz2 配置
├── rviz_nav.rviz # 导航模式 RViz2 配置
├── install.sh # 一次性安装依赖
├── run_bridges.sh # 启动 ros_gz_bridge
├── run_mapping.sh # 一键启动建图链路
├── save_map.sh # 保存 map.pgm 与 map.yaml
└── run_nav2.sh # 一键启动静态地图导航
这个目录刻意保持扁平。对于教学实例来说,读者打开目录就能看到每条数据链对应的脚本和配置,比一开始就封装成多个 ROS 2 功能包更容易理解。
如果以后需要发布、复用或接入真实机器人,再将世界模型、Bringup、Navigation 和自定义节点拆分成功能包会更加合适。
三、系统数据链设计
整个工程能否跑通,关键不是某一个参数,而是四条数据链必须同时正确。
| 数据 | Gazebo 端 | ROS 2 端 | 方向 |
|---|---|---|---|
| 仿真时钟 | /clock | /clock | Gazebo → ROS 2 |
| 激光雷达 | /scan | /scan | Gazebo → ROS 2 |
| 里程计 | /model/amr/odometry | /odom | Gazebo → ROS 2 |
| 速度指令 | /model/amr/cmd_vel | /cmd_vel | ROS 2 → Gazebo |
对应的数据流可以概括为:
┌──────────── /scan ────────────┐
│ ▼
键盘 / Nav2 ── /cmd_vel ──► ros_gz_bridge ──► Gazebo ──► slam_toolbox
│ │
└── /odom ─────────┘
│
▼
odom_to_tf
Bridge 配置的核心思路如下:
- ros_topic_name: /clock
gz_topic_name: /clock
ros_type_name: rosgraph_msgs/msg/Clock
gz_type_name: gz.msgs.Clock
direction: GZ_TO_ROS
- ros_topic_name: /odom
gz_topic_name: /model/amr/odometry
ros_type_name: nav_msgs/msg/Odometry
gz_type_name: gz.msgs.Odometry
direction: GZ_TO_ROS
- ros_topic_name: /scan
gz_topic_name: /scan
ros_type_name: sensor_msgs/msg/LaserScan
gz_type_name: gz.msgs.LaserScan
direction: GZ_TO_ROS
- ros_topic_name: /cmd_vel
gz_topic_name: /model/amr/cmd_vel
ros_type_name: geometry_msgs/msg/Twist
gz_type_name: gz.msgs.Twist
direction: ROS_TO_GZ
不要看到 Topic 名称相同就默认桥接正确。还要确认消息类型、方向和 Gazebo 内部 Topic 名称完全匹配,尤其是 /cmd_vel 必须从 ROS 2 流向 Gazebo。
四、补齐 TF:为什么需要 odom_to_tf.py?
本工程使用的 TF 链为:
map → odom → base_link → laser_link
各段变换的发布者如下:
| TF | 建图时发布者 | 导航时发布者 |
|---|---|---|
map → odom | slam_toolbox | AMCL |
odom → base_link | odom_to_tf.py | odom_to_tf.py |
base_link → laser_link | static_transform_publisher | static_transform_publisher |
ros_gz_bridge 可以把 Gazebo 的里程计转换成 ROS 2 的 /odom 消息,但它不会自动替我们发布 odom → base_link。因此需要一个很小的 Python 节点读取 /odom,再广播对应 TF:
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from nav_msgs.msg import Odometry
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
class OdomToTF(Node):
def __init__(self):
super().__init__('odom_to_tf')
self.tf_broadcaster = TransformBroadcaster(self)
self.sub = self.create_subscription(Odometry, '/odom', self.cb, 10)
def cb(self, msg):
transform = TransformStamped()
transform.header.stamp = msg.header.stamp
transform.header.frame_id = msg.header.frame_id or 'odom'
transform.child_frame_id = 'base_link'
transform.transform.translation.x = msg.pose.pose.position.x
transform.transform.translation.y = msg.pose.pose.position.y
transform.transform.translation.z = msg.pose.pose.position.z
transform.transform.rotation = msg.pose.pose.orientation
self.tf_broadcaster.sendTransform(transform)
def main():
rclpy.init()
node = OdomToTF()
try:
rclpy.spin(node)
except (KeyboardInterrupt, ExternalShutdownException):
pass
finally:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()
if __name__ == '__main__':
main()
LiDAR 安装在底盘上方 0.20 米,因此还要发布静态变换:
ros2 run tf2_ros static_transform_publisher \
--x 0 --y 0 --z 0.20 \
--roll 0 --pitch 0 --yaw 0 \
--frame-id base_link \
--child-frame-id laser_link
这里的高度必须与 amr_world.sdf 中 LiDAR 的安装位置一致。否则 RViz2 可能看得到激光,slam_toolbox 却无法把扫描数据正确转换到底盘坐标系。
五、slam_toolbox 的核心配置
本实例采用异步在线建图,核心参数如下:
slam_toolbox:
ros__parameters:
use_sim_time: true
odom_frame: odom
map_frame: map
base_frame: base_link
scan_topic: /scan
mode: mapping
resolution: 0.05
map_update_interval: 5.0
transform_publish_period: 0.05
min_laser_range: 0.15
max_laser_range: 12.0
minimum_time_interval: 0.2
angular_update: 0.1
linear_update: 0.1
throttle_scans: 1
几个重点参数:
use_sim_time: true:让 slam_toolbox 使用 Gazebo 发布的仿真时间;scan_topic: /scan:必须与 Bridge 输出一致;base_frame: base_link:必须在 TF 树中真实存在;resolution: 0.05:每个地图像素代表 5 厘米;mode: mapping:当前运行的是在线建图,而不是定位模式。
如果 /clock 没有桥接,或者部分节点使用系统时间、部分节点使用仿真时间,就可能出现 TF 查询超时、消息被丢弃和地图不更新等问题。
六、安装依赖
进入 WSL2:
wsl -d Ubuntu
进入项目目录:
cd ~/projects/amr_slam
首次运行时安装依赖:
bash install.sh
工程需要的主要组件包括:
- Gazebo Sim;
ros_gz_bridge;slam_toolbox;- Nav2;
- RViz2;
teleop_twist_keyboard;- Cyclone DDS。
安装完成后,可以确认几个关键功能包:
ros2 pkg prefix slam_toolbox
ros2 pkg prefix nav2_bringup
ros2 pkg prefix ros_gz_bridge
ros2 pkg prefix teleop_twist_keyboard
如果新开了终端,先加载环境:
source /opt/ros/lyrical/setup.bash
本项目的启动脚本还会读取 amr_env.sh,统一 DDS、ROS 2 和 WSLg 相关环境,避免不同终端使用不同通信设置。
七、一键启动建图
运行:
cd ~/projects/amr_slam
bash run_mapping.sh
脚本会依次启动:
[1/5] Gazebo Sim
[2/5] ros_gz_bridge
[3/5] static TF 与 odom_to_tf
[4/5] slam_toolbox
[5/5] RViz2
启动完成后,当前终端进入键盘遥控:
i 前进
, 后退
j 左转
l 右转
k 停止
推荐的建图方式:
- 先沿房间外围缓慢行驶;
- 转弯时降低角速度,避免快速原地旋转;
- 再进入中间障碍物区域补全地图;
- 最后回到起点附近形成闭环;
- 观察地图经过回环优化后是否更加整齐。
建图时可以在另一个终端检查关键节点:
source ~/projects/amr_slam/amr_env.sh
ros2 node list
ros2 lifecycle get /slam_toolbox
预期能够看到:
/amr_gz_bridge
/odom_to_tf
/slam_toolbox
并且 slam_toolbox 应显示:
active [3]
继续检查话题:
ros2 topic hz /scan
ros2 topic hz /odom
ros2 topic echo /map --once --field info
检查 TF:
ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link laser_link
ros2 run tf2_ros tf2_echo map base_link
如果这三段变换都能够查询,说明建图所需的 TF 链已经连通。
八、保存地图
完成建图后,不要立即关闭建图终端。打开第二个 WSL 终端:
cd ~/projects/amr_slam
bash save_map.sh
脚本会通过 Nav2 Map Saver 保存:
map.pgm
map.yaml
检查文件:
ls -lh map.pgm map.yaml
map.yaml 中记录图像文件、分辨率、原点和占用阈值;map.pgm 是实际的二维栅格图像。两个文件必须配套使用。
地图保存成功后,回到第一个终端按 Ctrl+C。run_mapping.sh 会清理本次启动的 Gazebo、Bridge、SLAM、RViz2 和辅助节点,避免它们与下一阶段的 Nav2 冲突。
九、启动 Nav2 自主导航
确认地图文件存在后运行:
cd ~/projects/amr_slam
bash run_nav2.sh
如果地图不在默认位置,也可以显式传入:
bash run_nav2.sh /完整路径/map.yaml
启动后,在 RViz2 中完成两步操作。
1. 设置初始位姿
选择 2D Pose Estimate,在地图上点击机器人实际位置,并拖动箭头设置朝向。
这是在告诉 AMCL:“机器人当前大约位于这里。”如果初始位置差得太远,粒子可能无法收敛。
2. 设置导航目标
选择 Nav2 Goal,在地图可通行区域点击目标点并设置目标朝向。
Nav2 将依次完成:
AMCL 定位
↓
Global Planner 生成路径
↓
Controller Server 跟踪路径
↓
/cmd_vel 通过 Bridge 进入 Gazebo
↓
机器人到达目标
可以检查关键生命周期节点:
for node in \
/map_server \
/amcl \
/controller_server \
/planner_server \
/bt_navigator
do
ros2 lifecycle get "$node"
done
这些节点都应该处于:
active [3]
十、本实例的实际验收结果
最终版本不是只验证“窗口能打开”,而是完成了从数据到行为的整链路测试。
| 验收项目 | 结果 |
|---|---|
| Gazebo 世界与 AMR 模型启动 | 通过 |
/clock、/scan、/odom 桥接 | 通过 |
odom → base_link → laser_link TF | 通过 |
| slam_toolbox 生命周期 | active |
机器人接收 /cmd_vel 并移动 | 通过 |
| 二维地图生成 | 通过 |
| 地图保存 | 成功,测试地图为 307×280 |
| Map Server、AMCL、Planner、Controller、BT Navigator | 全部 active |
| 实际导航目标 | SUCCEEDED |
| Bash、Python、YAML、SDF 检查 | 通过 |
| Ctrl+C 后的残留进程检查 | 无残留 |
这组结果证明系统不是“Topic 看起来都存在”,而是实际完成了:
传感器输入 → 建图 → 地图保存 → 定位 → 规划 → 控制 → 到达目标

十一、WSL2 下最容易忽略的几个问题
1. Topic 存在,但收不到数据
在 WSL2 中,多个终端如果使用了不同的 DDS 实现、发现范围或 ROS_DOMAIN_ID,可能出现节点列表正常,但 Topic 没有实际数据的情况。
本工程统一使用 amr_env.sh 加载通信环境:
source ~/projects/amr_slam/amr_env.sh
不要只执行一次后假设所有新终端都会继承。每个新终端都需要加载一致的环境。
2. /cmd_vel 有输出,但机器人不动
检查发布者、订阅者和类型:
ros2 topic info --verbose /cmd_vel
ros2 topic echo /cmd_vel --once
本实例最终统一使用:
geometry_msgs/msg/Twist
如果 Nav2 输出 TwistStamped,而 Gazebo Bridge 等待 Twist,即使 Topic 名称相同也无法正确驱动底盘。
3. 建图与导航同时运行
建图模式的 slam_toolbox 与 AMCL 都可能发布 map → odom。两者同时运行会争夺同一段 TF,造成地图跳动、定位漂移或规划失败。
正确顺序是:
运行建图 → 保存地图 → Ctrl+C 完整退出 → 启动 Nav2
4. Gazebo 或 RViz2 在 WSLg 中打不开
本工程通过 amr_env.sh 统一 Qt/WSLg 环境。没有图形界面时也可以使用无头模式:
AMR_HEADLESS=1 bash run_mapping.sh
无头模式适合自动验收和服务器环境,但没有键盘遥控界面。自动测试时需要单独向 /cmd_vel 发布速度指令。
5. 退出后残留进程
如果脚本只结束父进程,Gazebo、ROS Launch 或 Nav2 的子进程可能继续运行。下一次启动时,就会出现端口、Topic、TF 或生命周期节点冲突。
因此一键脚本不仅要负责启动,也必须设置退出陷阱,按进程组清理全部子进程。调试后可以检查:
pgrep -af "gz sim|ros_gz_bridge|slam_toolbox|nav2_|amcl|odom_to_tf.py"
没有输出才表示环境真正恢复干净。
十二、这个实例还可以怎样扩展?
当前项目已经完成二维仿真建图和导航,但它仍是一个适合学习数据链的基础实例。后续可以沿着下面几个方向继续扩展:
- 使用 URDF/Xacro 与
robot_state_publisher代替部分手工静态 TF; - 使用
ros2_control管理真实或仿真的差速底盘; - 加入 IMU,并使用
robot_localization融合轮速里程计和惯性数据; - 在 Local Costmap 中加入动态障碍物测试;
- 增加自动巡航航点和任务状态机;
- 把启动脚本整理为标准 ROS 2 Bringup 功能包;
- 接入真实 2D LiDAR 和底盘,将仿真参数迁移到实体 AMR。
真正迁移到实体机器人时,最需要重新标定的不是 Nav2 的“神奇参数”,而是:
轮径与轮距
里程计方向和尺度
LiDAR 安装位姿
机器人 Footprint
速度、加速度和制动能力
基础数据可信以后,SLAM 和导航参数才有调优价值。
十三、总结
本实例使用 ROS 2 Lyrical、Gazebo Sim、ros_gz_bridge、slam_toolbox、Nav2 和 RViz2,完成了一套能够实际运行的 AMR 二维 SLAM 与自主导航链路。
整个工程最值得记住的不是某一个 YAML 数值,而是组件之间清晰的职责:
Gazebo:生成机器人运动和传感器数据
ros_gz_bridge:连接 Gazebo 与 ROS 2
odom_to_tf:补齐 odom→base_link
slam_toolbox:在线建图并发布 map→odom
Map Saver:保存二维地图
AMCL:在静态地图中定位
Nav2:完成规划、控制与任务执行
RViz2:显示状态并提供交互入口
最终验收已经证明:机器人能够建图、保存地图、重新定位、规划路径并到达目标。到这里,我们不再只是分别运行几个 ROS 2 节点,而是第一次把感知、定位、规划和控制连接成了一个完整的移动机器人系统。
下一篇工程实例可以继续在这个项目上增加 IMU 与 robot_localization,或者将 AMR 仿真模型整理成标准 ROS 2 功能包。
项目完整代码
本文使用的全部代码已经上传到 GitHub:
https://github.com/zephastra/robotics/tree/main/projects/01-amr-slam
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)