ROS 2 教程
目标
ROS 2 教程说明 UGV Rover PT Jetson Orin ROS2 Kit 的 ROS 2 产品功能,包括模型显示、TF、底盘驱动、雷达、相机、rosbag、2D 建图、2D 导航、行为控制、AprilTag、RTAB-Map、Gazebo 仿真和 ROS 2 Web App。Node、Topic、Service、Action、Launch 等概念只做必要说明,通用 ROS 2 开发知识不展开。
前置知识
ROS 2 在 UGV 上的运行关系
UGV Rover PT Jetson Orin ROS2 Kit 采用“上位机 + 下位机”的控制结构。ROS 2 运行在 Jetson Orin 的 Docker 容器中,不直接运行在 ESP32 下位机上。ROS 2 节点通过底盘驱动、串口通信或传感器驱动与硬件交互。
浏览器 / SSH / RViz / JupyterLab
↓
Jetson Orin 上位机
├── Web 控制端 / JupyterLab
└── ROS 2 Docker 容器:RViz、TF、雷达、相机、SLAM、Navigation
↓
ESP32 下位机:电机、编码器、IMU、LED、OLED、云台和底盘反馈
开发者补充:仓库与 ROS 2 功能包
ugv_jetson 偏 Web 控制端、JupyterLab 教学、上位机视觉示例、下位机 JSON 控制和媒体功能。入门教程、上位机教程、下位机教程主要围绕这一套流程展开。
ugv_ws 偏 ROS 2 工作空间,包含机器人模型、TF、雷达、里程计、建图、导航、视觉接口、行为控制、仿真和 ROS 2 Web 可视化。
| 功能包 | 作用 |
|---|---|
ugv_base_node | 根据原始里程计和 IMU 发布 /odom,可发布 odom → base_footprint TF |
ugv_bringup | 底盘、雷达、驱动和基础数据启动入口 |
ugv_description | URDF 模型、机器人结构 TF、RViz 模型显示 |
ugv_interface | 产品封装的 Action / 消息接口 |
ugv_nav | 2D 定位、Navigation、Nav2 参数和地图 |
ugv_slam | Gmapping、Cartographer、RTAB-Map 等建图流程 |
ugv_tools | 键盘控制、手柄控制、行为控制 |
ugv_vision | USB 相机、OAK-D Lite、AprilTag、视觉交互入口 |
ugv_gazebo | Gazebo 仿真模型、世界、仿真建图和导航入口 |
ugv_web_app | 基于 Vizanti 的 ROS 2 Web 可视化或控制入口 |
ugv_chat_ai | Web AI 交互入口,依赖额外 AI 服务配置 |
依赖包中常见名称还包括 apriltag_ros、cartographer、emcl2、explore_lite、slam_gmapping、ldlidar、rf2o_laser_odometry、robot_pose_publisher、teb_local_planner 和 vizanti。
RoadMap
本页后续内容面向实体 UGV,操作从“环境准备”开始。
如需在 Windows 的 WSL2 环境中验证 UGV 模型、传感器、控制、建图、定位、导航和自动探索流程,请进入 Gazebo 仿真开发。Gazebo 仿真与实体 UGV 的 Docker ROS 2 环境相互独立,不是本页后续操作的前置步骤。
环境准备
先完成入门教程中的 开机、网络连接 和 Web 页面访问。确认 UGV 电量 和 UGV 预检 已完成,雷达、相机无遮挡,底盘周围有足够空间。
后续 ROS 2 操作需要多个 Docker ROS 2 容器终端同时运行。JupyterLab Terminal 只适合临时检查,Notebook 代码单元格不适合运行持续占用终端的 ROS 2 命令。SSH 工具用于通过网络进入远程命令行环境。 使用 SSH 工具登录 Docker ROS 2 容器,并按需要多开终端标签页。MobaXterm 不是必须工具,但适合 Windows 环境按示例填写地址、端口、用户名和密码,也方便保存会话和复制标签页。
从 JupyterLab 启动 Docker 并配置 MobaXterm
按 入门教程:打开 Web 控制页面 和 上位机教程:JupyterLab 界面速览 打开 JupyterLab,并在 Launcher 中进入 Terminal。这里用 Terminal 启动 Docker 远程 SSH 服务,不要在 Notebook 代码单元格中运行持续占用终端的 ROS 2 命令。
-
在 JupyterLab Terminal 中进入 ROS 2 工作空间目录。
cd ~/ugv_ws -
启动 ROS 2 Docker 远程服务。
sudo chmod +x ros2_humble.sh remotessh.sh./ros2_humble.sh终端输出示例:
jetson@ubuntu:~/ugv_ws$ sudo chmod +x ros2_humble.sh remotessh.sh./ros2_humble.sh[sudo] password for jetson:Entering the container...ugv_jetson_ros_humbleContainer started successfully.Executing docker exec command to open a bash shell in the container...* Starting OpenBSD Secure Shell server sshd [ OK ]Opened bash shell in the container. -
打开 MobaXterm,点击左上角
Session。
-
在弹出的会话类型窗口中选择
SSH。 -
在
Basic SSH settings中填写 Docker ROS 2 容器连接信息。Remote host填写 OLED 或 Web 页面中看到的 UGV IP。Port填写23。端口23用于进入 ROS 2 Docker 镜像,不要改成普通 SSH 端口。- 其余选项保持默认。

-
点击
OK连接。出现login as:时输入root。
-
按提示输入密码。当前镜像密码为
jetson,输入时终端不会显示明文字符。
-
第一次保存密码提示选择
No。
-
第一次选择鼠标右键动作时,选择
Paste,这样 Windows 复制命令后可在 MobaXterm 中右键粘贴。
-
登录后会进入 Docker ROS 2 容器终端。看到
root@ubuntu这类提示符时,表示已经进入容器环境。
从Windows往终端复制粘贴代码的方法
在 Windows 中先选中并按 Ctrl + C 复制代码,再切回 MobaXterm,把光标放到终端窗口内并右键粘贴。后续复制多行命令时,MobaXterm 会提示即将粘贴多行内容。确认内容无误后点击 OK。
-
ROS 2 后续经常需要多个终端同时运行。在 MobaXterm 左侧
User sessions中再次双击刚创建的 UGV IP 会话,每双击一次会新开一个 Docker ROS 2 容器 SSH 标签页。也能点击顶部标签栏旁边的+,再重新打开同一个 SSH 会话。
sudo chmod +x ros2_humble.sh remotessh.sh 一般仅需执行一次。脚本文件未替换、系统未重新刷写时,后续不需要重复执行该命令。
./ros2_humble.sh 用于启动 ROS 2 Docker 远程 SSH 服务。UGV 重启后,先在 Windows PowerShell 中检查 23 端口是否可连接:
Test-NetConnection <UGV_IP> -Port 23
如果显示 TcpTestSucceeded : True,表示 MobaXterm 可登录 ROS 2 Docker。如果显示 TcpTestSucceeded : False,返回 JupyterLab Terminal 执行:
cd ~/ugv_ws
./ros2_humble.sh
更换 Wi-Fi、热点或路由器后,先确认 UGV 的当前 IP 地址。为避免左侧会话名称与实际连接 IP 不一致,新建一个使用当前 IP 的 MobaXterm SSH Session。
新建 Session 时仍然使用相同配置:
Remote host:UGV 当前 IP;Specify username:root;Port:23。
新建 Session 后,可让 Session 名称使用当前 IP,例如 192.168.9.197,方便识别。旧 Session 不会自动更新 IP。如果旧 IP 不再使用,右键旧 Session,选择 Delete session 删除,避免后续误点。
首次调试时先保持 JupyterLab 页面不关闭,便于回到 Terminal 检查或重新启动服务;确认端口 23 仍可连接后,关闭 JupyterLab 页面不影响已启动的 Docker SSH 服务。
开发者补充:修改已有 MobaXterm Session
如果需要修改已有 Session,右键旧 Session,选择 Edit session,在 SSH → Basic SSH settings 中修改 Remote host。Remote host 才是 MobaXterm 实际连接的 UGV IP。Rename session 只修改左侧显示名称,不修改实际连接 IP。


加载 ROS 2 环境
每个新开的 Docker ROS 2 终端都先加载 ROS 2 环境。后文功能步骤中出现“加载 ROS 2 环境”时,均指执行以下命令。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
如果当前镜像工作空间路径不同,请以当前容器中的实际路径为准。source 命令没有输出。
ROS 2 最小检查
-
先在当前 Docker ROS 2 终端加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
确认 ROS 2 能找到 UGV 相关包:
ros2 pkg list | grep ugv
ros2 node list
ros2 topic list
刚加载环境但未启动任何 launch 时,ros2 node list 没有输出不代表错误。ros2 topic list 只看到 /parameter_events 和 /rosout 这类基础 topic,不属于异常。后文功能会在相关节点启动后检查相关 topic。
启动机器人基础功能
启动基础功能时,先连接 ROS 2、真实底盘和雷达,再进行第一次低速遥控。bringup_lidar.launch.py 本身不主动让底盘运动,但会占用底盘串口和雷达;键盘控制、手柄控制和行为控制会驱动实体底盘。
执行底盘运动前,先确认电量、场地和停止方式。首次测试可架空底盘,或放在空旷平整地面低速短时移动。
启动底盘与雷达
bringup_lidar.launch.py 会访问实体底盘和雷达。启动前如果不确定底盘串口是否被占用,先检查并释放目标串口。
检查并释放底盘串口
以下命令在 Jetson 主机终端或 JupyterLab Terminal 中执行,不要求进入固定目录。命令访问 /dev/ttyTHS* 串口设备,与当前所在目录无关。
Jetson Orin Nano 底盘串口为 /dev/ttyTHS0,Jetson Orin NX 底盘串口为 /dev/ttyTHS1,以系统实际列出的串口为准。更多说明见 下位机教程:串口占用检查与释放。
ls /dev/ttyTHS*
PORT=/dev/ttyTHS1
sudo fuser -v $PORT
ls /dev/ttyTHS* 会列出当前系统中的串口,例如:
/dev/ttyTHS1 /dev/ttyTHS2
sudo fuser -v $PORT 有输出时,目标串口正在被某个进程占用。输出中的数字是 PID,也就是进程编号。例如:
USER PID ACCESS COMMAND
/dev/ttyTHS1: jetson 1805 F.... python
其中 1805 是占用 /dev/ttyTHS1 的进程编号。后面的 fuser 释放命令会按 $PORT 处理占用该串口的进程,不需要手动输入 PID。
如需确认占用来源,将输出中的 PID 代入下面命令查看进程信息:
ps -fp <PID>
例如输出中显示 1805,就执行:
ps -fp 1805
这一步不是必须执行。
发送 TERM 信号,让占用目标串口的进程正常退出:
sudo fuser -TERM -k $PORT
sleep 2
sudo fuser -v $PORT
如果 sudo fuser -v $PORT 仍然有输出,说明目标串口仍未释放。确认当前 $PORT 是要释放的目标串口后,再只针对该目标串口执行强制释放:
sudo fuser -KILL -k $PORT
sleep 1
sudo fuser -v $PORT
最后一条 sudo fuser -v $PORT 没有输出,目标串口已经释放。sudo fuser -KILL -k $PORT 只结束占用当前 $PORT 设备文件的进程,不是结束所有 Python 进程。不要使用结束所有 Python 进程的方式释放资源;只释放目标串口或目标摄像头的占用进程。
如果 ps -fp <PID> 显示进程路径中包含 ugv_jetson/app.py,说明占用来源为 Web 主程序。
释放该串口后,Web 控制页面的底盘控制会暂时不可用。完成 ROS 2 测试后,重启或运行程序
sudo reboot
恢复Web端页面。
开发者补充:让 Web 主程序开机后不自动恢复
Web 主程序自启由 autorun.sh 写入当前账号 crontab 的 @reboot 项。JupyterLab 使用 ugv_jupyter.service,两者不是同一个启动入口。不同镜像的自启方式以当前设备为准。
如果只是临时切换到 ROS 2 或 Notebook 独占底盘串口,不要禁用 Web 主程序开机自启。停止 Web 主程序后,完成测试并重启设备,默认启动项会恢复 Web 控制页面。
先查看当前 crontab:
crontab -l
如果看到包含 ~/ugv_jetson/app.py 的 @reboot 行,先保存该行,再编辑 crontab:
crontab -e
删除或注释包含 ~/ugv_jetson/app.py 的 @reboot 行后保存。再次查看:
crontab -l
恢复自启时,把记录的 @reboot ... ~/ugv_jetson/app.py ... 行加回 crontab。
如果当前镜像改用 systemd,先查实际服务名:
systemctl list-unit-files | grep -Ei "ugv|jetson|web|app|access"
systemctl list-units --type=service | grep -Ei "ugv|jetson|web|app|access"
确认服务名后查看:
systemctl status <SERVICE_NAME>
禁用并停止该服务:
sudo systemctl disable --now <SERVICE_NAME>
systemctl is-enabled <SERVICE_NAME>
恢复自启时执行:
sudo systemctl enable --now <SERVICE_NAME>
systemctl status <SERVICE_NAME>
禁用 Web 主程序自启后,设备重启后 Web 控制页面、视频流或依赖 Web 主程序的功能不会自动恢复。记录服务名或 crontab 原始行和恢复命令,避免后续无法回到默认状态。
-
在 Docker ROS 2 终端中加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
设置型号并启动底盘与雷达:
export UGV_MODEL=ugv_rover
export LDLIDAR_MODEL=ld19
ros2 launch ugv_bringup bringup_lidar.launch.py use_rviz:=false
Docker ROS 2 终端 1
启动该命令的终端也可称为 bringup 终端。后续检查 topic、手动控制底盘、手柄控制、RViz 查看雷达扫描等需要实体底盘和雷达数据的步骤,都需要保持该终端继续运行,不要关闭。
该命令会启动实体底盘、雷达和相关 ROS 2 节点,不打开 RViz。命令会启动 robot_state_publisher、joint_state_publisher、ugv_bringup、ugv_driver、ldlidar_node、rf2o_laser_odometry_node、base_node 等进程。雷达数据是否进入 ROS 2,后面通过 /scan topic 检查;需要图形化查看模型和雷达扫描时,再进入“RViz 模型与传感器可视化”章节。
启动后查看终端输出,看到以下日志时,bringup 正在运行:
ldlidar communication is normal;Publish topic message:ldlidar scan data;Got first Laser Scan;- 终端停留在 launch 输出中,没有回到命令提示符。
如需停止,回到启动 bringup_lidar.launch.py 的 MobaXterm 终端,按 Ctrl + C。停止后,底盘节点、雷达节点和相关里程计节点会随 launch 一起退出。
检查 ROS 2 节点和 topic 状态
保持上一步启动 bringup_lidar.launch.py 的 Docker ROS 2 终端 1 继续运行,不要关闭。另开一个 Docker ROS 2 终端 2,用于查看当前正在运行的 node 和 topic。
node 和 topic 检查只读取 ROS 2 状态,不主动驱动底盘,也不需要打开 RViz。
如果 Docker ROS 2 终端 1 已经关闭,请先回到 启动底盘与雷达 重新启动。
-
在 Docker ROS 2 终端 2 中加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
查看当前节点和 topic:
ros2 node list
ros2 topic list
Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 运行时,ros2 node list 会列出 /LD19、/base_node、/rf2o_laser_odometry、/ugv_bringup、/ugv_driver、/ugv/robot_state_publisher 等节点。ros2 topic list 会列出 /scan、/tf、/tf_static、/odom、/odom/odom_raw、/odom_rf2o、/imu/data、/imu/data_raw、/imu/mag、/voltage 等 topic。终端中出现同名节点提示时,继续按 topic 列表检查,不影响 topic 读取。
- 按关键词检查常见 topic:
ros2 topic list | grep scan
ros2 topic list | grep tf
ros2 topic list | grep odom
ros2 topic list | grep imu
ros2 topic list | grep voltage
以 ros2 topic list 的实际输出为准。如果某个 topic 没有出现,不要直接对它执行 echo。
- 用
/scan检查雷达扫描数据。
检查雷达是否发布数据,不需要打开 RViz。/scan topic 存在并持续发布数据,表示雷达扫描数据已经进入 ROS 2。
确认 /scan 存在后,查看发布频率:
ros2 topic hz /scan
看到频率输出后,按 Ctrl + C 结束频率查看。再查看一条雷达扫描消息:
ros2 topic echo /scan --once
如果 /scan 不存在,不要继续执行 echo,先检查 Docker ROS 2 终端 1 是否仍在运行。
- 只在 topic 已存在时查看其它数据。
确认 /odom 存在后,查看一条里程计数据:
ros2 topic echo /odom --once
如果终端提示:
WARNING: topic [...] does not appear to be published yet
Could not determine the type for the passed topic
表示当前没有节点正在发布该 topic,或该 topic 不适用于当前启动配置。先确认 Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 仍在运行,再根据 ros2 topic list 的实际输出选择要查看的 topic。
ros2 topic hz /scan 刚启动时会先等待数据,随后持续输出 average rate。频率稳定在约 10 Hz 时,/scan 正在持续发布。
对 /scan 执行 ros2 topic echo /scan --once 后,终端会输出一条 LaserScan 消息,其中包含 frame_id: base_lidar_link、angle_min、angle_max、range_min、range_max、ranges 和 intensities。ranges 中的数字是雷达到障碍物的距离,.nan 表示该角度没有有效距离读数。
对 /odom 执行 ros2 topic echo /odom --once 后,终端会输出一条里程计消息,其中包含 frame_id: odom、child_frame_id: base_footprint、pose 和 twist。这表示里程计 topic 已经发布。
手动控制底盘
键盘控制和手柄控制都是手动输入方式。控制节点会把按键或手柄摇杆转换成 /cmd_vel 速度指令,Docker ROS 2 终端 1 中的底盘驱动接收指令后,让实体底盘移动。测试前保持 Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 继续运行;如果 Docker ROS 2 终端 1 已经关闭,先回到 启动底盘与雷达 重新启动。
键盘控制和手柄控制会主动驱动实体底盘。首次测试前让机器人悬空,或放在空旷地面,并远离人、宠物、桌脚和线缆。测试结束前先发送停止指令,再退出控制节点。
使用键盘低速移动
保持 Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 继续运行,不要关闭。另开一个 Docker ROS 2 终端 2 运行键盘控制命令。
-
在 Docker ROS 2 终端 2 中加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
确认
/cmd_veltopic。
ros2 topic info /cmd_vel
- 启动键盘控制。
ros2 run ugv_tools keyboard_ctrl
常用按键如下:
| 按键 | 动作 |
|---|---|
I | 前进 |
, | 后退 |
J | 左转 |
L | 右转 |
K 或空格 | 停止 |
Ctrl + C | 退出键盘控制 |
键盘控制终端必须保持输入焦点。每次短按方向键或控制键后,立即按 K 或空格停止。不要长时间持续按方向键。测试结束前先按 K 或空格停止,确认机器人停止后,再按 Ctrl + C 退出键盘控制节点。
使用手柄控制
手柄控制会把手柄输入转换为 /cmd_vel,从而控制底盘运动。先完成键盘低速移动测试,并确认停止方式后,再使用手柄控制。
先将手柄的 2.4G USB 接收器插入 Jetson 主板 USB 接口。打开手柄背面的电源开关,手柄指示灯闪红灯后,再启动 ROS 2 手柄控制节点。

保持 Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 继续运行,不要关闭。另开新的 Docker ROS 2 终端运行手柄控制命令。
-
在新的 Docker ROS 2 终端中加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
启动手柄控制:
ros2 launch ugv_tools teleop_twist_joy.launch.py
手柄控制会主动驱动底盘。测试时先小幅移动摇杆,按下面对应关系检查底盘动作:
| 手柄动作 | 底盘动作 |
|---|---|
| 左摇杆向上 | 前进 |
| 左摇杆向下 | 后退 |
| 右摇杆向左 | 左转 |
| 右摇杆向右 | 右转 |
测试结束前先松开摇杆并确认底盘停止,再按 Ctrl + C 退出手柄控制 launch。
检查 TF 坐标关系
TF 用来描述机器人不同坐标系之间的位置和方向关系,可以理解为 ROS 2 中的坐标换算关系。它不是控制命令,也不是传感器数据,而是让 ROS 2 知道车体、雷达、IMU、相机、轮子、里程计坐标之间如何对齐。
在 UGV 上,雷达扫描来自雷达坐标系,底盘运动与车体坐标系和里程计坐标系有关。TF 关系正常时,RViz、SLAM 和 Navigation 才能把机器人模型、雷达扫描、地图和机器人位置放到同一个空间中显示和计算。TF 正常不等于建图或导航完成。
保持前面启动 bringup_lidar.launch.py 的 Docker ROS 2 终端 1 继续运行。另开一个 Docker ROS 2 终端,用于检查 TF。
-
在新的 Docker ROS 2 终端中加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
查看 TF topic。
ros2 topic list | grep tf
输出中会看到 /tf 和 /tf_static。/tf_static 保存固定安装关系,例如车体到雷达、IMU、相机和轮子的关系。/tf 保存运行中会变化的关系,例如 odom 到 base_footprint 或 base_link 的关系。
- 查看固定安装关系。
查看雷达相对车体的位置和方向:
ros2 run tf2_ros tf2_echo base_link base_lidar_link
查看 IMU 相对车体的位置和方向:
ros2 run tf2_ros tf2_echo base_link base_imu_link
如果终端持续输出 Translation、Rotation 和 Matrix,表示 ROS 2 可以查询到对应的坐标关系。执行 tf2_echo 后,前几秒会出现 Waiting for transform 或 frame does not exist。随后开始持续输出 Translation、Rotation 和 Matrix 时,该 TF 关系已经可以查询到。
- 查看动态坐标关系。
ros2 run tf2_ros tf2_echo odom base_footprint
移动或转动 UGV 时,输出中的 Translation 或 Rotation 会变化。如果只是原地转向,主要关注 Rotation 或 yaw 变化。
如果 odom -> base_footprint 没有输出,尝试:
ros2 run tf2_ros tf2_echo odom base_link
不同工作空间或模型配置中,动态 TF 的 child frame 不完全相同,以终端输出为准。
- 生成 TF 树 PDF。
ros2 run tf2_tools view_frames
该命令会监听几秒 TF 数据,并在当前终端目录生成 frames_*.pdf。这个 PDF 用于检查当前系统的完整 TF 树,不作为理解 TF 的唯一材料。
在 MobaXterm 左侧文件栏勾选 Follow terminal folder,可以在当前目录找到并下载生成的 frames_*.pdf。

UGV Rover 的简化 TF 树如下:
base_footprint
└── base_link
├── base_lidar_link
├── base_imu_link
├── 3d_camera_link
├── left_up_wheel_link
├── left_down_wheel_link
├── right_up_wheel_link
├── right_down_wheel_link
└── pt_base_link
└── pt_link1
└── pt_link2
└── pt_camera_link
| 坐标系 | 含义 |
|---|---|
base_footprint | 底盘在地面上的参考点 |
base_link | 车身主体 |
base_lidar_link | 雷达安装位置 |
base_imu_link | IMU 安装位置 |
3d_camera_link | 3D 相机安装位置 |
| 四个 wheel link | 轮子模型位置,主要用于模型显示和关节状态 |
pt_base_link → pt_link1 → pt_link2 → pt_camera_link | 云台和云台相机链路 |
只启动 Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 时,重点关注 odom → base_footprint 或 odom → base_link。启动 SLAM / Navigation 后,重点关注 map → odom → base_footprint。没有启动 SLAM 或定位时,看不到 map 不属于错误。
开发者补充:查看 /tf 原始消息
/tf 原始消息会输出大量 frame_id、child_frame_id、translation 和 rotation 数据,适合排查具体 transform。普通检查优先使用 view_frames 查看 TF 树,或使用 tf2_echo 查看一对坐标关系。
ros2 topic echo /tf --once
挑战任务:用 TF 观察车体与雷达的坐标关系
这个挑战用临时脚本让 UGV 低速原地转向,并生成 HTML 动画,用于观察车体、雷达、IMU 和 /scan 如何通过 TF 放到同一个 odom 坐标空间。
要求和注意事项:
- 保持 Docker ROS 2 终端 1 中的
bringup_lidar.launch.py继续运行。 - 脚本会发布
/cmd_vel,让 UGV 低速原地转动。 - 必须添加
--confirm-move参数,脚本才会发布运动指令。 - 测试前停止键盘控制、手柄控制等其它会发布
/cmd_vel的程序。 - 动画用于理解 TF 坐标转换,不代表建图或导航完成。
该挑战会驱动实体底盘。请将 UGV 放在地面,周围留出安全空间,不要架空测试。
脚本结束后会自动发布停止命令。如果需要中途停止,可以在运行脚本的终端按 Ctrl + C,脚本会尝试发送停止命令。必要时也可以直接关闭机器人电源。
展开查看操作步骤、脚本和排查
- 检查
/cmd_vel和/scan。
ros2 topic info /cmd_vel
ros2 topic hz /scan
ros2 topic info /cmd_vel 输出中 Subscription count 大于 0,表示有节点正在订阅 /cmd_vel。ros2 topic hz /scan 能看到频率输出,表示雷达扫描数据正在发布。按 Ctrl + C 退出 hz 检查。
- 创建临时脚本。
cd /home/ws/ugv_ws
mkdir -p tools
创建临时脚本:
cat > tools/tf_auto_turn_lidar_demo.py <<'PY'
#!/usr/bin/env python3
import argparse
import csv
import json
import math
import os
import signal
import time
import rclpy
from geometry_msgs.msg import Twist
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from tf2_ros import Buffer, TransformListener
def yaw_from_quaternion(q):
siny = 2.0 * (q.w * q.z + q.x * q.y)
cosy = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
return math.atan2(siny, cosy)
def transform_to_pose(tf_msg):
t = tf_msg.transform.translation
r = tf_msg.transform.rotation
return t.x, t.y, yaw_from_quaternion(r)
def apply_pose(x, y, pose):
px, py, yaw = pose
c = math.cos(yaw)
s = math.sin(yaw)
return px + c * x - s * y, py + s * x + c * y
def clamp_scan_points(scan, base_pose, lidar_pose, max_points):
if scan is None:
return []
step = max(1, len(scan.ranges) // max_points)
points = []
angle = scan.angle_min
for i, distance in enumerate(scan.ranges):
if i % step:
angle += scan.angle_increment
continue
if math.isfinite(distance) and scan.range_min <= distance <= scan.range_max:
lx = lidar_pose[0] + math.cos(lidar_pose[2] + angle) * distance
ly = lidar_pose[1] + math.sin(lidar_pose[2] + angle) * distance
ox, oy = apply_pose(lx, ly, base_pose)
points.append([round(ox, 3), round(oy, 3)])
angle += scan.angle_increment
return points
class Demo(Node):
def __init__(self, args):
super().__init__("tf_auto_turn_lidar_demo")
self.args = args
self.scan = None
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
self.cmd_pub = self.create_publisher(Twist, "/cmd_vel", 10)
self.create_subscription(LaserScan, "/scan", self.on_scan, 10)
def on_scan(self, msg):
self.scan = msg
def lookup_pose(self, parent, child):
tf_msg = self.tf_buffer.lookup_transform(parent, child, rclpy.time.Time())
return transform_to_pose(tf_msg)
def publish_turn(self):
msg = Twist()
msg.angular.z = self.args.angular
self.cmd_pub.publish(msg)
def stop(self):
msg = Twist()
for _ in range(12):
self.cmd_pub.publish(msg)
rclpy.spin_once(self, timeout_sec=0.03)
def write_csv(path, rows):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(
f,
fieldnames=["t", "x", "y", "yaw", "scan_points", "lidar_x", "lidar_y", "imu_x", "imu_y"],
)
writer.writeheader()
writer.writerows(rows)
def write_html(path, frames, parent, base):
html = """<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>TF Auto Turn Lidar Demo</title>
<style>
body { margin: 0; background: #ffffff; font-family: Arial, "Microsoft YaHei", sans-serif; color: #0b1f3a; }
.wrapper { width: 1200px; margin: 24px auto; }
h1 { margin: 0 0 8px 0; font-size: 30px; }
p { margin: 6px 0 16px 0; color: #5a6b80; font-size: 18px; }
canvas { width: 1200px; height: 720px; border: 2px solid #cfe0f5; border-radius: 18px; background: #f8fbff; }
.legend { margin-top: 12px; padding: 14px 18px; border: 1px solid #cfe0f5; border-radius: 12px; background: #fff; font-size: 16px; color: #30445c; line-height: 1.8; }
code { background: #eef5ff; padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<div class="wrapper">
<h1>TF 自动转向与雷达可视化</h1>
<p><code>""" + parent + """</code> 是参考坐标系,<code>""" + base + """</code> 是车体坐标系,蓝色点是 <code>/scan</code> 雷达扫描数据经过 TF 转换后的显示。</p>
<canvas id="canvas" width="1200" height="720"></canvas>
<div class="legend">
<div>蓝色网格:<code>""" + parent + """</code> 坐标系。</div>
<div>橙色车体:<code>""" + base + """</code>,箭头方向表示车头方向。</div>
<div>蓝色点:经过 TF 转换后的 <code>/scan</code> 雷达扫描数据。</div>
<div>绿色点:<code>base_lidar_link</code>。</div>
<div>紫色点:<code>base_imu_link</code>。</div>
<div>该动画用于理解 TF 如何把车体、雷达和 IMU 放到同一个坐标空间,不代表建图或导航完成。</div>
</div>
</div>
<script>
const frames = """ + json.dumps(frames, ensure_ascii=False) + """;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let index = 0;
function px(x, y) { return [600 + x * 180, 360 - y * 180]; }
function drawGrid() {
ctx.strokeStyle = "#d8e7f7";
ctx.lineWidth = 1;
for (let x = 0; x <= 1200; x += 60) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 720); ctx.stroke(); }
for (let y = 0; y <= 720; y += 60) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(1200, y); ctx.stroke(); }
ctx.strokeStyle = "#82aee0";
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, 360); ctx.lineTo(1200, 360); ctx.stroke();
ctx.beginPath(); ctx.moveTo(600, 0); ctx.lineTo(600, 720); ctx.stroke();
}
function drawRobot(f) {
const p = px(f.x, f.y);
ctx.save();
ctx.translate(p[0], p[1]);
ctx.rotate(-f.yaw);
ctx.fillStyle = "#f5a142";
ctx.strokeStyle = "#9a5a00";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(40, 0); ctx.lineTo(-26, -24); ctx.lineTo(-18, 0); ctx.lineTo(-26, 24); ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.restore();
}
function drawPoint(p, color, size) {
const q = px(p[0], p[1]);
ctx.fillStyle = color;
ctx.beginPath(); ctx.arc(q[0], q[1], size, 0, Math.PI * 2); ctx.fill();
}
function drawInfo(f) {
ctx.fillStyle = "rgba(255,255,255,0.92)";
ctx.fillRect(900, 22, 270, 130);
ctx.fillStyle = "#0b1f3a";
ctx.font = "18px Arial";
ctx.fillText("x: " + f.x.toFixed(3), 922, 56);
ctx.fillText("y: " + f.y.toFixed(3), 922, 84);
ctx.fillText("yaw: " + f.yaw.toFixed(3), 922, 112);
ctx.fillText("scan_points: " + f.scan.length, 922, 140);
}
function draw() {
if (!frames.length) return;
const f = frames[index];
ctx.clearRect(0, 0, 1200, 720);
drawGrid();
for (const p of f.scan) drawPoint(p, "#2274d9", 2.6);
drawRobot(f);
drawPoint(f.lidar, "#1ba784", 6);
drawPoint(f.imu, "#8a4fd3", 6);
drawInfo(f);
index = (index + 1) % frames.length;
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
"""
with open(path, "w", encoding="utf-8") as f:
f.write(html)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--confirm-move", action="store_true")
parser.add_argument("--duration", type=float, default=8.0)
parser.add_argument("--angular", type=float, default=0.5)
parser.add_argument("--parent", default="odom")
parser.add_argument("--base", default="base_footprint")
parser.add_argument("--max-scan-points", type=int, default=120)
parser.add_argument("--outdir", default="/home/ws/ugv_ws/tf_auto_turn_lidar_outputs")
args = parser.parse_args()
if not args.confirm_move:
print("未添加 --confirm-move,脚本不会发布 /cmd_vel。")
return
rclpy.init()
node = Demo(args)
stopping = False
def handle_signal(signum, frame):
nonlocal stopping
stopping = True
signal.signal(signal.SIGINT, handle_signal)
os.makedirs(args.outdir, exist_ok=True)
rows = []
frames = []
start = time.time()
try:
while rclpy.ok() and not stopping and time.time() - start <= args.duration:
rclpy.spin_once(node, timeout_sec=0.05)
node.publish_turn()
try:
base_pose = node.lookup_pose(args.parent, args.base)
lidar_pose = node.lookup_pose(args.base, "base_lidar_link")
imu_pose = node.lookup_pose(args.base, "base_imu_link")
except Exception as exc:
print("waiting for tf:", exc)
continue
lidar_in_parent = apply_pose(lidar_pose[0], lidar_pose[1], base_pose)
imu_in_parent = apply_pose(imu_pose[0], imu_pose[1], base_pose)
scan_points = clamp_scan_points(node.scan, base_pose, lidar_pose, args.max_scan_points)
elapsed = time.time() - start
row = {
"t": round(elapsed, 3),
"x": round(base_pose[0], 4),
"y": round(base_pose[1], 4),
"yaw": round(base_pose[2], 5),
"scan_points": len(scan_points),
"lidar_x": round(lidar_in_parent[0], 4),
"lidar_y": round(lidar_in_parent[1], 4),
"imu_x": round(imu_in_parent[0], 4),
"imu_y": round(imu_in_parent[1], 4),
}
rows.append(row)
frames.append({
"t": row["t"],
"x": row["x"],
"y": row["y"],
"yaw": row["yaw"],
"scan": scan_points,
"lidar": [row["lidar_x"], row["lidar_y"]],
"imu": [row["imu_x"], row["imu_y"]],
})
print("x={x:.3f} y={y:.3f} yaw={yaw:.3f} scan_points={scan_points}".format(**row))
finally:
node.stop()
csv_path = os.path.join(args.outdir, "tf_auto_turn_lidar_samples.csv")
html_path = os.path.join(args.outdir, "tf_auto_turn_lidar_animation.html")
write_csv(csv_path, rows)
write_html(html_path, frames, args.parent, args.base)
print("stopped /cmd_vel")
print(csv_path)
print(html_path)
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
PY
python3 -m py_compile tools/tf_auto_turn_lidar_demo.py
- 运行挑战脚本。
python3 tools/tf_auto_turn_lidar_demo.py --confirm-move --duration 8 --angular 0.50 --parent odom --base base_footprint
该命令会让 UGV 低速原地转动约 8 秒,并生成动画文件。
如果转动太快,降低角速度:
python3 tools/tf_auto_turn_lidar_demo.py --confirm-move --duration 8 --angular 0.40 --parent odom --base base_footprint
如果 base_footprint 显示不符合预期,改用 base_link:
python3 tools/tf_auto_turn_lidar_demo.py --confirm-move --duration 8 --angular 0.50 --parent odom --base base_link
输出目录:
/home/ws/ugv_ws/tf_auto_turn_lidar_outputs
输出文件:
tf_auto_turn_lidar_animation.html
tf_auto_turn_lidar_samples.csv
在 MobaXterm 左侧文件栏勾选 Follow terminal folder,进入 /home/ws/ugv_ws/tf_auto_turn_lidar_outputs,下载 tf_auto_turn_lidar_animation.html,然后用 Windows 浏览器打开。
运行脚本后会看到以下内容:
- 终端会持续输出
x、y、yaw和scan_points; - UGV 会低速原地转向;
- 脚本结束后会自动发布停止命令;
- 下载并打开 HTML 后,可以看到车体箭头随 yaw 变化转动;
- 蓝色点表示经过 TF 转换后的雷达扫描数据;
- 绿色点表示
base_lidar_link; - 紫色点表示
base_imu_link; - 该动画用于理解 TF 如何把车体、雷达和 IMU 放到同一个坐标空间,不代表建图或导航完成。
如果 UGV 不动,先检查 /cmd_vel:
ros2 topic info /cmd_vel
如果 Subscription count 为 0,说明当前没有节点订阅 /cmd_vel,先检查 Docker ROS 2 终端 1 中的 bringup_lidar.launch.py 是否仍在运行。如果 Docker ROS 2 终端 1 已经关闭,先回到 启动底盘与雷达 重新启动。
再做短时阈值测试:
timeout 3 ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}, angular: {z: 0.50}}"
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}, angular: {z: 0.0}}"
如果 0.50 可以让 UGV 原地转向,但挑战脚本不能转动,请检查脚本是否在 /home/ws/ugv_ws 下运行,以及是否添加了 --confirm-move。
RViz 模型与传感器可视化
RViz 用于图形化查看模型、TF、雷达扫描和常见显示项,只负责显示,不会主动驱动底盘。
下面的命令会启动底盘与雷达 bringup,并同时打开 RViz,会占用底盘串口和雷达。
如果前面已经运行 bringup_lidar.launch.py use_rviz:=false,请先在 Docker ROS 2 终端 1 按 Ctrl + C 停止,再运行下面命令,避免重复占用底盘串口和雷达。
export UGV_MODEL=ugv_rover
export LDLIDAR_MODEL=ld19
ros2 launch ugv_bringup bringup_lidar.launch.py use_rviz:=true
如果 Docker ROS 2 终端 1 已经启动 bringup_lidar.launch.py use_rviz:=false,也可以另开终端单独打开 RViz。打开后需要在左侧 Displays 面板中检查或添加 RobotModel、TF、LaserScan 等显示项。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 run rviz2 rviz2
查看优先使用前面的 use_rviz:=true 命令,避免空 RViz 配置带来的困惑。
RViz 界面出现后,左侧是 Displays 面板,中间区域是 3D 视图,右侧是 Views 面板。左侧 Displays 面板用于检查当前任务需要的显示项;TF 的命令行检查见前面的“检查 TF 坐标关系”。
| 显示项 | 常见 topic / 设置 | 作用 |
|---|---|---|
RobotModel | /ugv/robot_description | 显示机器人模型 |
TF | /tf、/tf_static | 显示坐标系 |
LaserScan | /scan | 显示 2D 雷达扫描 |
Grid | RViz 网格显示 | 显示参考网格 |
Map | /map | 显示 2D 地图 |
Path | 以当前 Nav2 输出为准 | 显示规划路径 |
先看左侧状态:Global Status 显示 Ok,表示当前 RViz 配置整体正常;RobotModel 显示 Status: Ok,表示机器人模型已经加载。

查看雷达扫描时,在左侧 Displays 面板中找到或添加 LaserScan,并将 Topic 设置为:
/scan
Topic 为空时,RViz 会显示 Error subscribing: Empty topic name。这表示 LaserScan 显示项还没有订阅雷达 topic,不代表雷达硬件损坏。设置为 /scan 后,LaserScan 变为 Status: Ok,并在 3D 视图中显示彩色点段。


这些彩色点段是 2D 雷达扫描数据,表示雷达检测到的周围障碍物边缘,不是摄像头画面,也不是地图。点段的位置和形状会随周围环境变化。默认显示的网格 Grid 和机器人模型 RobotModel 用于查看机器人在 3D 视图中的位置和朝向。

移动实体 UGV 后,RViz 中的模型位置、朝向和雷达扫描画面会随之变化。如果 TF 关系正常,RobotModel、LaserScan 等显示项才能对齐到同一个空间。这只表示模型显示和雷达可视化正常,不等于建图或导航完成。
开发者补充:只查看 URDF 模型
如果只想查看机器人 URDF 模型和基础 TF,不连接实体底盘和雷达,可以使用:
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
export UGV_MODEL=ugv_rover
ros2 launch ugv_description display.launch.py use_rviz:=true
该命令主要用于查看模型,不主动驱动底盘,也不读取实体雷达,不会发布真实 /scan 数据。查看实体底盘和雷达时,优先使用前面的 bringup_lidar.launch.py use_rviz:=true。
2D 地图与导航总览
2D 地图与导航分成两个阶段:
- 建图:使用 Gmapping 或 Cartographer,让 UGV 在环境中移动,并生成 2D 占据栅格地图。
- 导航:加载已经保存的地图,让 UGV 在地图中定位、规划路径并移动到目标点。
建图阶段不会自动完成导航;Navigation 阶段也不是重新建图。保存后的 map.pgm 和 map.yaml 会在 Navigation 中被加载,用于定位和路径规划。
建图 launch
-> 键盘控制低速移动
-> RViz 中地图逐步出现
-> 保存 map.pgm + map.yaml
-> 打开 map.pgm 快速检查
-> 启动 Navigation
-> 设置 2D Pose Estimate
-> 设置 Nav2 Goal
2D 建图
Gmapping 和 Cartographer 都用于生成 2D 地图。第一轮实车 2D 建图优先使用 Gmapping,先把雷达、里程计、TF、RViz 地图显示和地图保存流程跑完整;熟悉 Gmapping 后,再测试 Cartographer。
建图质量要求与保存前检查
2D 地图会被后续 Navigation、保存导航点和自动探索继续使用。地图文件能保存成功,不代表地图适合导航。如果地图存在重影、错位、断墙、大片未知区域,或与当前测试场地不一致,后续会影响 AMCL、costmap、Nav2 Goal 和导航点任务。
建图时保持测试环境稳定:
- 不要在人、宠物、椅子、箱子频繁移动的环境中建图;
- 建图路线中的门、桌椅、障碍物位置应和后续 Navigation 测试时一致;
- 透明玻璃、镜面、黑色吸光物体、细桌脚和低矮线缆可能影响雷达扫描;
- 雷达前方不要被外壳、手、线缆或桌边遮挡;
- 地面应平整,避免轮子打滑;
- 建图过程中不要手动搬起或拖动 UGV。
建图路线不要只原地转圈,也不要快速移动。建议按下面方式采集:
- 启动 SLAM 后先原地小幅左右转动,让地图开始出现;
- 沿主要通道低速前进;
- 在每个拐角处慢速转向;
- 尽量走一个小闭环,最后回到起点附近;
- 对后续要导航的区域至少扫描一遍;
- 保存地图前让 UGV 停止,观察地图是否稳定。
移动太快、频繁急转或轮子打滑时,地图容易出现重影、错位或墙体弯曲。这样的地图即使能保存,也不适合直接用于 Navigation。
保存地图前,先在 RViz 中检查:
Map显示项不再持续提示No map received;- 地图中主要墙体、桌边、通道边界轮廓连续;
- 没有明显双层墙、重影、弯曲走廊或整体错位;
- 后续要导航的区域不是大片灰色未知区域;
LaserScan彩色点段大致落在地图墙体或障碍物边缘附近;- UGV 停止不动时,地图不会明显抖动或跳变。
如果以上观察点不满足,不要急着保存地图。先继续低速补扫,或停止当前 SLAM 后重新建图。
Gmapping 建图
-
启动前准备。
gmapping.launch.py会启动底盘、雷达、里程计、robot_pose_publisher、slam_gmapping和 RViz。使用use_rviz:=true时,会加载 2D 建图视图。启动前停止其它 ROS 2 终端启动 Gmapping 前,先停止之前用于底盘 bringup、RViz、键盘控制、手柄控制或其它 SLAM 测试的终端。Gmapping launch 会重新启动底盘、雷达、里程计、
slam_gmapping和 RViz。如果前面已经运行
bringup_lidar.launch.py use_rviz:=false,请先在对应终端按Ctrl + C停止,再启动 Gmapping,避免重复占用底盘串口、雷达或产生重复节点。启动 SLAM 前按 检查并释放底盘串口 检查底盘串口,并确认雷达连接和供电正常。
-
启动 Gmapping。
在新的 Docker ROS 2 终端中执行。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashexport UGV_MODEL=ugv_roverexport LDLIDAR_MODEL=ld19ros2 launch ugv_slam gmapping.launch.py use_rviz:=true该命令会启动底盘、雷达、里程计、Gmapping 和 RViz。启动后不要关闭这个 Gmapping 终端。

终端会输出
ldlidar communication is normal、Got first Laser Scan、slam_gmapping的Initialization complete和Registering Scans:Done。这些日志表示雷达数据已经进入 Gmapping,建图节点正在处理扫描数据。提示RViz 刚打开时,
Map可能显示Status: Warn和No map received。这表示 RViz 还没有收到 Gmapping 发布的/map地图数据,不代表雷达或 Gmapping 已经停止。启动后先等待
10 ~ 30秒。Gmapping 完成初始化后,地图有时会自动出现。如果等待后仍然只看到LaserScan彩色点段,没有灰、白、黑的栅格地图,再使用键盘控制让 UGV 低速转向或短距离移动。
-
用键盘控制低速建图。
保持 Gmapping 终端运行。另开一个 Docker ROS 2 终端运行键盘控制。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 run ugv_tools keyboard_ctrl按键如下。
按键 动作 I前进 ,后退 J左转 L右转 K或空格停止 Ctrl + C退出键盘控制 先原地小幅转向,再短距离前进或后退,让雷达扫描覆盖周围环境。每次移动后按
K或空格停止。不要长时间持续按方向键。 -
保存地图。
保存地图前,先按 建图质量要求与保存前检查 检查 RViz 中的地图质量,并确认已经出现灰、白、黑的栅格地图,
Map显示项不再提示No map received。- 保持 Gmapping launch 继续运行。
- 在键盘控制终端按
K或空格停止底盘,再按Ctrl + C退出键盘控制。 - 新开一个保存终端,执行保存脚本。
cd /home/ws/ugv_wschmod +x ./save_2d_gmapping_map.sh./save_2d_gmapping_map.sh如果提示
Permission denied,表示脚本没有执行权限,不表示 Gmapping 建图失败,也不表示地图数据错误。已经执行过chmod +x后仍无法运行时,可以改用下面命令。bash ./save_2d_gmapping_map.sh- 看到
Writing map occupancy data、Writing map metadata、Map saved successfully后,确认map.pgm和map.yaml已生成。 - 回到 Gmapping 终端按
Ctrl + C退出。

日志中出现
Received a 384 X 384 map @ 0.05 m/pix时,表示保存到的地图尺寸为384 x 384像素,分辨率为0.05 m/pix,也就是每个像素约5 cm。地图像素尺寸不等于实际场地尺寸。
Cartographer 建图
-
Cartographer 和 Gmapping 的区别。
Gmapping 和 Cartographer 都是 2D 建图方法,目的都是让 UGV 利用雷达、里程计和 TF 生成环境地图。对常规使用流程来说,最终重点是得到可用于 Navigation 的
map.pgm和map.yaml。先使用 Gmapping 完成第一轮实车建图。Gmapping 流程更直接,适合先确认雷达、里程计、TF、地图保存和 Navigation 是否能跑通。Cartographer 可作为进阶建图方案;它除了导出
map.pgm和map.yaml,保存脚本还会生成 Cartographer 内部状态文件,例如map.pbstream。对比项 Gmapping Cartographer 主要用途 快速完成第一张 2D 地图,验证雷达、里程计、TF 和地图保存流程 作为另一种 2D 建图方案,适合在熟悉基础流程后进一步测试 建图结果 主要得到 map.pgm和map.yaml通常也会得到 map.pgm和map.yaml,并生成map.pbstream文件怎么理解 map.pgm是地图图片,map.yaml是地图参数;后续 Navigation 主要加载这两个文件map.pgm和map.yaml同样可用于 Navigation;map.pbstream是 Cartographer 的内部状态文件,用于 Cartographer 相关流程学习顺序 第一轮实车建图优先使用 跑通 Gmapping 和 Navigation 后再测试 使用建议 适合写成主线流程 适合作为进阶建图方案,不需要一开始就使用 -
启动 Cartographer。 Cartographer launch 也会占用底盘、雷达和相关 SLAM 资源。启动前先停止其它 bringup、Gmapping、Navigation 或键盘控制终端。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashexport UGV_MODEL=ugv_roverexport LDLIDAR_MODEL=ld19ros2 launch ugv_slam cartographer.launch.py use_rviz:=true启动后不要关闭当前 Cartographer 终端。
-
用键盘控制低速建图。 保持 Cartographer 终端运行。另开 Docker ROS 2 终端运行键盘控制。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 run ugv_tools keyboard_ctrl按
J/L小幅转向,再用I/,短距离移动。每次动作后按K或空格停止。 -
保存地图。 保存前先按 建图质量要求与保存前检查 检查 RViz 中的地图质量,再停止键盘控制底盘运动,但保持 Cartographer launch 运行。
cd /home/ws/ugv_wschmod +x ./save_2d_cartographer_map.sh./save_2d_cartographer_map.sh如果提示
Permission denied,可以用bash执行。cd /home/ws/ugv_wsbash ./save_2d_cartographer_map.sh地图会保存到以下目录。
/home/ws/ugv_ws/src/ugv_main/ugv_nav/maps/保存完成后再回到 Cartographer 终端按
Ctrl + C退出。RViz 中可以看到地图逐步更新。使用键盘控制移动时,地图范围会逐步扩展。保存后会生成或更新
map.pgm、map.yaml和map.pbstream。Cartographer 建图完成后仍需进入 Navigation 才能使用地图导航。
查看保存的地图文件
Gmapping 和 Cartographer 保存后的地图默认位于以下路径:
/home/ws/ugv_ws/src/ugv_main/ugv_nav/maps/map.pgm
/home/ws/ugv_ws/src/ugv_main/ugv_nav/maps/map.yaml
在 Docker ROS 2 终端中执行下面命令检查文件。
cd /home/ws/ugv_ws
ls -lh src/ugv_main/ugv_nav/maps/map.pgm \
src/ugv_main/ugv_nav/maps/map.yaml
cat src/ugv_main/ugv_nav/maps/map.yaml
map.pgm 是地图图片,map.yaml 是地图参数文件。后续 Navigation 会根据 map.yaml 找到 map.pgm,并按照其中的分辨率、原点和阈值解释地图。如果 ls 能看到 map.pgm 和 map.yaml,且文件大小不是 0,说明地图文件已经生成。cat map.yaml 用于确认地图图片路径、分辨率和原点等信息是否存在。
保存地图后,不只检查 map.pgm 和 map.yaml 是否存在,还要打开 map.pgm 快速查看地图质量。文件能保存成功,不等于地图可以用于 Navigation。
在 MobaXterm 左侧文件栏中进入以下目录。
/home/ws/ugv_ws/src/ugv_main/ugv_nav/maps/
下载 map.pgm 和 map.yaml。如果左侧文件栏没有立即显示新文件,点击刷新按钮,或重新勾选 Follow terminal folder 后进入上述目录。
打开 map.pgm 快速检查。 map.pgm 可以作为图片直接打开,用于快速检查地图轮廓。
PGM 地图不是照片,也不是 RViz 截图,而是 2D 占据栅格地图。它通常由灰、白、黑三类颜色组成。
| 颜色 | 含义 |
|---|---|
| 灰色 | 未知区域,机器人还没有可靠扫描到 |
| 白色 | 可通行区域,雷达认为这里没有障碍 |
| 黑色 | 障碍物边缘,例如墙、桌脚、箱子或其它物体轮廓 |
判断地图是否适合进入 Navigation 时,重点看以下内容:
| 检查项 | 可用于 Navigation 的表现 | 不建议用于 Navigation 的表现 |
|---|---|---|
| 墙体轮廓 | 连续、清楚 | 双层墙、断裂、明显重影 |
| 通道区域 | 白色区域连贯 | 通道中间大片黑色或灰色 |
| 未知区域 | 不影响计划导航路线 | 目标点附近大片灰色未知区域 |
| 地图比例 | 与实际场地大致一致 | 房间、走廊或障碍轮廓被拉伸、旋转或错位 |
| 起点附近 | 清楚可辨认 | 起点附近混乱或被障碍覆盖 |
如果更换了测试房间、桌椅位置变化较大、门的开闭状态不同,或地图与当前环境明显不一致,应重新建图。
2D Navigation:使用地图导航
Navigation 使用已经保存的地图进行定位、路径规划和底盘控制。它不会重新建图。启动 Navigation 后,系统会加载 map.pgm / map.yaml,启动定位模块、路径规划模块、控制器、代价地图和 RViz。Gmapping / Cartographer 负责生成地图;Navigation 负责使用地图。
Navigation / Localization 显示 active 只表示相关 Nav2 lifecycle 节点已经激活,不表示地图一定正确、AMCL 一定收敛、LaserScan 一定对齐,也不表示当前目标一定可达。发出目标前必须先确认地图、定位、雷达扫描和 costmap 状态。
Navigation 使用前面保存的 map.pgm 和 map.yaml 进行定位和路径规划。启动 Navigation 前,请先打开 map.pgm 检查地图轮廓是否清楚,并确认当前测试环境与建图时基本一致。
如果地图有明显重影、断墙、错位、大片未知区域,或当前场地已经发生较大变化,不要继续测试 Navigation。应先重新建图或重新保存地图。
前置条件
启动 Navigation 前,先完成以下检查:
- 已按 查看保存的地图文件 确认
map.pgm和map.yaml存在,并检查map.pgm轮廓质量; - 已按 检查并释放底盘串口 确认底盘串口没有被其它程序占用;
- 已停止 Gmapping、Cartographer、基础 bringup、键盘控制或其它会占用底盘、雷达的终端;
- 测试场地空旷,UGV 旁边没有人、宠物、线缆、桌脚或其它障碍;
- 已确认 停止 Navigation 的流程。
nav.launch.py 会包含底盘与雷达 bringup,不要同时单独启动另一个 bringup_lidar.launch.py。
Navigation 会发布速度指令并驱动实体底盘。关闭终端不应作为唯一停止手段,测试时始终保留物理停止方式。
启动 Navigation
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
export UGV_MODEL=ugv_rover
ros2 launch ugv_nav nav.launch.py use_rviz:=true use_localization:=amcl use_localplan:=teb
该命令会启动 Navigation、定位、局部规划、底盘与雷达相关节点,并打开 RViz。启动后不要关闭该 Navigation 终端。
本节使用推荐组合 amcl + teb。其中 amcl 用来判断机器人在地图中的位置,teb 用来根据目标点和障碍物规划局部移动路线。首次导航时先不要修改 use_localization 和 use_localplan 参数,先完成地图加载、初始位姿设置、目标点发送和底盘移动测试。
开发者补充:Navigation 参数说明
nav.launch.py 中的两个参数分别决定:
use_localization:机器人使用哪种方式在地图中定位,也就是判断“我现在在地图哪里”;use_localplan:机器人使用哪种局部规划方式移动到目标点,也就是判断“我怎么绕开附近障碍物走过去”。
| 参数 | 可选值 | 作用 | 建议 |
|---|---|---|---|
use_localization | amcl | 常用 2D 定位方式。启动后需要在 RViz 中用 2D Pose Estimate 设置初始位姿 | 首次实车导航优先使用 |
use_localization | emcl | 另一种 2D 定位方式,也需要设置初始位姿 | 跑通 AMCL 后再测试 |
use_localization | cartographer | 使用 Cartographer 相关定位流程 | 前面使用 Cartographer 建图流程时再测试 |
use_localplan | teb | 局部规划方式之一,用于根据目标点、障碍物和机器人运动约束生成局部运动路径 | 当前主线使用 |
use_localplan | dwa | 另一种局部规划方式 | 跑通 TEB 后再对比测试 |
简单理解:
amcl/emcl/cartographer解决“机器人在哪里”;teb/dwa解决“机器人怎么走过去”;- 首次导航不要改参数,先使用
amcl + teb跑通地图加载、初始位姿、目标点和底盘移动流程。
确认 Navigation 实际加载的地图
启动 Navigation 后,另开 Docker ROS 2 终端确认 /map_server 实际加载的地图路径。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 param get /map_server yaml_filename
再检查源码目录和安装目录中的地图文件。
ls -lh src/ugv_main/ugv_nav/maps/map.pgm \
src/ugv_main/ugv_nav/maps/map.yaml
ls -lh install/ugv_nav/share/ugv_nav/maps/map.pgm \
install/ugv_nav/share/ugv_nav/maps/map.yaml
查看两个 map.yaml 的内容。
echo "===== src map.yaml ====="
cat src/ugv_main/ugv_nav/maps/map.yaml
echo "===== install map.yaml ====="
cat install/ugv_nav/share/ugv_nav/maps/map.yaml
Navigation 实际使用哪张地图,以 /map_server 的 yaml_filename 为准。如果 src 和 install 中的地图文件时间、大小或内容不一致,可能出现保存的是新地图、Navigation 加载的却是旧地图的情况。
地图不匹配时,LaserScan 与地图轮廓会错开,AMCL 定位和 costmap 会异常,近距离目标也可能无法到达。如果发现 Navigation 加载的不是预期地图,请先停止 Navigation,确认当前工作空间的地图保存脚本和 launch 加载路径,再重新启动 Navigation。
认识 Navigation RViz 界面
Navigation RViz 不是只看地图,而是把静态地图、实时雷达扫描、定位粒子、全局路径、局部控制和 costmap 叠加显示。颜色较多属于正常界面状态。
Navigation RViz 中常见显示项如下。
| 显示项 | 含义 |
|---|---|
Map | 保存后加载的 2D 占据栅格地图 |
RobotModel | 当前机器人模型和朝向 |
LaserScan | 实时雷达扫描,用来和地图边缘对齐 |
Amcl Particle Swarm | AMCL 定位粒子,用来估计机器人在地图中的位置 |
Global Planner | 全局规划路径 |
Controller | 局部控制器或局部代价相关显示 |
| costmap 红色 / 紫色 / 粉色 / 青色区域 | 导航认为靠近障碍物、有碰撞风险或需要避开的区域 |
Navigation / Localization | 显示 active 时,表示 Nav2 导航和定位模块已激活 |
需要区分:Map 是原始 2D 地图,LaserScan 是实时雷达扫描,costmap 是导航代价图。costmap 会把障碍物、安全距离和风险区域叠加显示,不等于原始地图。

查看地图时,先选择顶部 Move Camera。鼠标滚轮用于缩放,按住鼠标中键拖动可以平移地图视图。如果视图被左侧 Displays 面板挡住,可以拖动面板边界缩小左侧区域,或折叠暂时不需要查看的显示项。设置初始位姿或目标点前,可以先用 Move Camera 调整视角,再点击 2D Pose Estimate 或 Nav2 Goal。
如果路径、机器人模型或地图被过多显示项遮挡,可在左侧 Displays 面板中临时取消勾选部分 costmap、粒子或调试显示项,只保留查看路径需要的 Map、RobotModel、LaserScan、Global Planner 等显示项。该操作只改变 RViz 显示,不会修改导航参数,也不会停止 Navigation。

设置初始位姿
- 确认地图已经加载。
- 确认实时
LaserScan大致落在地图墙体或障碍物边缘附近。 - 点击顶部
2D Pose Estimate。 - 在地图上点击 UGV 实际位置,按住鼠标左键拖出车头方向箭头。
- 松开鼠标完成设置。
松开鼠标后,RViz 工具栏回到 Move Camera 时属于正常显示状态。判断初始位姿是否生效,不看工具栏当前按钮,而是看 Amcl Particle Swarm 是否收敛到机器人附近,以及实时 LaserScan 是否大致贴合地图中的墙体或障碍物边缘。
设置初始位姿后,先等待 10 ~ 20 秒,不要立即发送目标。观察 LaserScan 是否与地图轮廓对齐,观察 Amcl Particle Swarm 是否集中。如果粒子分散、机器人模型跳动、LaserScan 与墙体轮廓明显错开,需要重新设置 2D Pose Estimate。
判断 LaserScan、AMCL 和 costmap 是否正常
设置目标点前,必须完成四项检查:
- 地图是当前场地的地图;
LaserScan与地图墙体或障碍物轮廓大致对齐;Amcl Particle Swarm集中在机器人真实位置附近;- 机器人自身和目标点附近没有被 costmap 高风险区域覆盖。
如果以上任意一项不满足,不要点击 Nav2 Goal。
costmap 中出现红色、紫色、粉色或青色区域不一定是错误,它表示 Navigation 根据静态地图、实时雷达、障碍层和膨胀层计算出的风险区域。需要重点看机器人中心附近和目标点附近是否被高风险区域覆盖。
如果机器人中心附近被大面积高风险区域覆盖,或者目标点位于高风险区域内,或者 RViz 中无法生成路径、Feedback 显示 canceled、Recoveries 持续增加,不要继续发送目标。先回到地图、LaserScan、AMCL 和传感器状态检查。
清理 costmap
需要排查代价地图缓存时,先查看当前是否存在清理服务。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 service list | grep clear
如果存在以下服务,再执行清理命令。
ros2 service call /global_costmap/clear_entirely_global_costmap nav2_msgs/srv/ClearEntireCostmap {}
ros2 service call /local_costmap/clear_entirely_local_costmap nav2_msgs/srv/ClearEntireCostmap {}
清理 costmap 只清除当前代价地图缓存,不会修复错误地图、错误初始位姿或传感器异常。如果清理后机器人周围很快再次被大面积高风险区域覆盖,应回到地图、LaserScan、AMCL 和传感器状态检查。
设置极近距离 Nav2 Goal
第一次只设置 20 ~ 30 cm 的极近距离目标。目标点必须在白色可通行区域,且不靠墙、不靠障碍、不在线缆附近。极近目标可达后,再测试 0.5 ~ 1 m 目标。不要第一次就跨房间、穿过狭窄区域或贴近障碍物。
- 完成 判断 LaserScan、AMCL 和 costmap 是否正常 中的四项检查。
- 点击顶部
Nav2 Goal。 - 在地图上选择机器人附近
20 ~ 30 cm的白色可通行区域。 - 按住鼠标左键拖出目标点到达后的车头方向箭头。
- 松开鼠标后,观察 RViz 中是否生成路径,以及 Navigation 面板状态是否变化。
Nav2 Goal 会让导航系统发布速度指令并驱动底盘。拖出的箭头表示“到达目标点后的车头朝向”,不表示机器人一定会沿箭头方向直线行驶。实际路径由 Nav2 根据地图、定位、实时雷达扫描和 costmap 自动规划。
目标太近时,Navigation 面板可能很快显示 reached,机器人不会有明显移动;这不代表远距离目标已经可用。先用极近目标确认基础导航状态,再逐步增加距离。
执行目标后,可以在 RViz 左侧 Navigation 2 面板中查看状态:
Navigation: active:导航模块已激活;Localization: active:定位模块已激活;Feedback: active:Nav2 正在执行目标;Feedback: reached:Nav2 认为目标已经到达;ETA:预计剩余时间;Distance remaining:距离目标的剩余距离;Recoveries:恢复行为触发次数。
Recoveries 不是普通计时器。它表示导航过程中触发恢复行为的次数。如果该数值持续增加,通常说明目标点附近代价过高、路线被阻挡、定位不够准,或目标选择不合适。首次测试时可以取消当前目标,重新选择更近、更空旷的白色区域。

图中 Navigation 和 Localization 均为 active,Feedback 为 active,表示 Nav2 已接收目标并正在执行。Distance remaining 表示距离目标的剩余距离,Recoveries 表示恢复行为触发次数。若 Recoveries 持续增加,可取消当前目标,重新选择更近、更空旷的目标点。到达目标后,Feedback 会显示 reached。这些状态表示 Navigation 正在使用地图,不代表正在重新建图。
停止 Navigation
需要停止 Navigation 时,按下面顺序处理:
-
在 RViz 左侧
Navigation 2面板中点击Cancel或Pause。 -
另开 Docker ROS 2 终端发送一次
/cmd_vel零速度。cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}, angular: {z: 0.0}}" -
回到 Navigation launch 终端,按
Ctrl + C停止nav.launch.py。 -
如果 UGV 仍在移动,立即断电。
发送一次零速度只能作为辅助停止动作。Navigation 控制器仍在运行时,可能继续发布新的 /cmd_vel,因此不要把该命令作为唯一停止方式。
常见异常排查
如果近距离 Nav2 Goal 也无法移动、Feedback 显示 canceled、costmap 大面积异常或 UGV 有碰撞风险,先停止 Navigation,再按下面顺序排查:
- Navigation 实际加载的
map.yaml是否是当前场地地图; src与install中地图文件是否不一致;LaserScan是否与地图轮廓对齐;- AMCL 粒子是否收敛;
- 机器人中心或目标点是否被 costmap 高风险区域覆盖;
/scan是否有稳定频率;/odom和/robot_pose是否持续输出;ugv_bringup、ugv_driver、base_node是否仍在运行;- Navigation launch 终端是否出现
process has died、multiple access、KeyError、TF extrapolation、Message Filter dropping message等日志; - 目标点是否太远、靠墙、靠障碍或在未知区域。
检查关键节点:
ros2 node list | grep -E "ugv_bringup|ugv_driver|base_node|amcl|map_server|planner_server|controller_server|bt_navigator"
检查关键数据:
timeout 8 ros2 topic hz /scan
timeout 8 ros2 topic echo /odom
timeout 8 ros2 topic echo /robot_pose
如果 /scan、/odom 或 /robot_pose 没有输出,先不要继续发目标。回到 bringup、定位和地图加载检查。
行为控制与导航点任务
行为控制是产品封装的任务接口,用来把常见底盘动作和导航点操作封装成 Action 任务。它不是 Nav2 标准功能,也不是建图功能。
前置知识
行为控制与Jupyter/Web JSON指令的区别
行为控制不是 Jupyter / Web JSON 指令的替代品。Jupyter / Web JSON 指令更接近下位机协议,用于验证硬件响应;ROS 2 行为控制运行在 ROS 2 层,用 /behavior Action 执行动作任务,例如后退一小段、原地旋转、保存导航点或发布已保存导航点。保存导航点不等于保存地图,前往导航点仍然依赖 Navigation。
| 对比项 | Jupyter / Web JSON 指令 | ROS 2 行为控制 /behavior |
|---|---|---|
| 所在层级 | 下位机 / 产品协议层 | ROS 2 应用层 |
| 控制入口 | Jupyter、Web 命令框或下位机 JSON 指令 | ROS 2 Action:/behavior |
| 控制方式 | 直接发送底盘、灯光、云台、OLED 等硬件指令 | 发送动作任务,由行为节点结合 /odom、/cmd_vel、/robot_pose、/goal_pose 执行 |
| 是否需要 ROS 2 | 不一定 | 需要 ROS 2 环境和相关节点 |
| 典型用途 | 验证下位机通信,测试底盘、LED、OLED、云台等硬件是否响应 | 封装短动作、查看 Action 状态、保存导航点、发布导航目标 |
开发者补充:behavior_ctrl 的 topic 和 Action
behavior_ctrl 创建 /behavior Action Server,订阅 /odom 和 /robot_pose,发布 /cmd_vel 和 /goal_pose。短距离动作通过 /cmd_vel 驱动底盘;导航点任务通过 /robot_pose 获取当前位置,并通过 /goal_pose 发布目标点。
短距离动作与停止
短距离动作适合先验证行为控制接口,例如让 UGV 前进一小段、后退一小段或原地旋转一定角度。它依赖 /odom 判断动作进度,并通过 /cmd_vel 驱动底盘,不依赖地图。
短距离动作、前往导航点和 Navigation 目标都会驱动实体底盘。测试前确认场地空旷,远离人、宠物、线缆和桌脚。测试时先使用低速、短距离任务。
- 确认 Docker ROS 2 终端 1 中的
bringup_lidar.launch.py正在运行。若该终端已经关闭,先回到 启动底盘与雷达 重新启动。
启动 behavior Action Server
-
另开新的 Docker ROS 2 终端启动行为控制。启动后保持该终端运行,不要关闭。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 run ugv_tools behavior_ctrl -
另开 Docker ROS 2 终端发送短距离动作。新终端也先加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash
前进一小段:
ros2 action send_goal /behavior ugv_interface/action/Behavior "{command: '[{\"T\": 1, \"type\": \"drive_on_heading\", \"data\": 0.1}]'}"
后退一小段:
ros2 action send_goal /behavior ugv_interface/action/Behavior "{command: '[{\"T\": 1, \"type\": \"back_up\", \"data\": 0.1}]'}"
原地旋转:
ros2 action send_goal /behavior ugv_interface/action/Behavior "{command: '[{\"T\": 1, \"type\": \"spin\", \"data\": -1}]'}"
停止当前行为:
ros2 action send_goal /behavior ugv_interface/action/Behavior "{command: '[{\"T\": 1, \"type\": \"stop\", \"data\": 0}]'}"
如果行为动作仍在执行,优先发送停止动作;如果仍无法停止,回到行为控制终端或底盘 bringup 终端按 Ctrl + C,必要时直接关闭机器人电源。
对比任务:同样后退一小段,对比 JSON 指令和 ROS 2 行为任务
这个任务用于理解“直接控制下位机”和“ROS 2 Action 任务”的区别。两种方式都会让 UGV 后退一小段,但观察重点不同:JSON 指令关注底层硬件是否响应;ROS 2 行为控制关注 Action 任务是否被接收、执行并返回结果。
目标: 分别使用 Web JSON 指令和 ROS 2 /behavior Action 让 UGV 后退一小段,对比两种控制层级的输出和状态。
完成标准: UGV 能完成短时间后退并停止;JSON 方式用于确认底盘响应,ROS 2 行为控制方式能看到 Action 任务接收和完成状态。 Web 命令输入框使用 在 Web 命令输入框发送后退短动作: 识别结果中会看到: 切换到 ROS 2 行为控制前,先按 从 JupyterLab 启动 Docker 并配置 MobaXterm 连接 Docker ROS 2 终端,再按 检查并释放底盘串口 释放底盘串口占用。随后启动 Docker ROS 2 终端 1,并按 启动 behavior Action Server 启动行为控制节点,保持两个终端运行。 再另开 Docker ROS 2 终端发送后退任务: ROS 2 行为控制方式运行后会看到: 同样是“后退一小段”,JSON 指令更像“直接告诉下位机怎么动”;ROS 2 行为控制更像“告诉 ROS 2 执行一个后退任务,并等待任务完成结果”。展开操作步骤和命令
base -c ... 转发 JSON 指令。Notebook 中可通过下位机教程的 send_cmd({...}) 发送同类 JSON。更多 JSON 控制入口见 下位机教程。base -c ... 只发送一次 JSON 指令。默认配置下,底盘约 1 ~ 2 秒没有收到新的运动指令后会自行停止,因此这里不需要再发送停止指令。
Goal accepted、SUCCEEDED 这类任务状态。
Waiting for an action server to become available...;Goal accepted;Goal finished with status: SUCCEEDED;Result: true 表示行为任务返回完成;
可选验证:保存并发布导航点
保存并发布导航点用于验证 /behavior、/robot_pose 和 /goal_pose 的接口链路。它不是完整的航点管理系统,也不替代基础 Navigation 检查。
save_map_point 会把当前 /robot_pose 记录为命名点。pub_nav_point 会把已保存点发布到 /goal_pose。这相当于把“手动点 Nav2 Goal”替换为“程序发布目标点”。真正路径规划和底盘运动仍由 Navigation / Nav2 完成。
能否实际到达,仍取决于 Navigation 的地图、AMCL、TF、costmap、底盘反馈和目标容差。如果 RViz 手动 Nav2 Goal 的极近距离目标都不能稳定到达,不要继续测试保存导航点后的实际移动。
| 方式 | 目标点来源 |
|---|---|
RViz Nav2 Goal | 在地图上手动点击目标 |
pub_nav_point | 程序把之前保存的导航点发布成目标 |
测试 A:接口链路验证。
-
先按 2D Navigation:使用地图导航 启动 Navigation,再按 启动 behavior Action Server 启动行为控制节点,并保持两个终端运行。
-
另开 Docker ROS 2 终端发送导航点任务命令。新终端也先加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
先确认
/behaviorAction Server 已经可用。ros2 action list | grep behaviorros2 action info /behavior如果发送行为命令时一直停在
Waiting for an action server to become available...,说明当前没有找到/behaviorAction Server。先回到第 1 步的行为控制终端,确认ros2 run ugv_tools behavior_ctrl仍在运行;如果该终端已经退出,重新启动行为控制节点后再发送命令。 -
检查
/robot_pose。ros2 topic echo /robot_pose --once如果
/robot_pose有输出,表示系统已经能得到机器人在地图中的位姿。若没有输出,先检查 Navigation 是否启动、初始位姿是否设置、定位是否正常。 -
保存当前位置为 A 点。
ros2 action send_goal /behavior ugv_interface/action/Behavior "{command: '[{\"T\": 1, \"type\": \"save_map_point\", \"data\": \"a\"}]'}"该命令会把当前
/robot_pose记录为一个导航点。保存时 UGV 不会因为“保存点”这个动作而主动移动。 -
在另一个 Docker ROS 2 终端监听
/goal_pose。cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 topic echo /goal_pose -
回到发送导航点任务命令的终端,发布 A 点为 Navigation 目标。
ros2 action send_goal /behavior ugv_interface/action/Behavior "{command: '[{\"T\": 1, \"type\": \"pub_nav_point\", \"data\": \"a\"}]'}"该命令会向
/goal_pose发布已保存的 A 点。后续是否能规划路径、是否移动、是否到达,取决于 Navigation 的定位、地图、costmap 和路径规划状态。
接口链路验证的观察点:
/behaviorAction Server 可用;/robot_pose有输出;save_map_point返回;pub_nav_point后/goal_pose有输出;/goal_pose的frame_id为map;- Navigation 面板是否收到目标。
测试 B:实际移动验证。
只有在基础 Navigation 已经通过 设置极近距离 Nav2 Goal 后,才继续测试实际移动。不要把精确回到物理 A 点作为通过标准。
- 保存当前位置为 A 点。
- 可以按 设置极近距离 Nav2 Goal 使用 RViz
Nav2 Goal设置一个近距离目标,也可以按 使用键盘低速移动 用键盘控制短距离移动。 - 移动后确认 UGV 已经离开 A 点,并保持 Navigation 正常运行。使用键盘控制时,不要同时保留其它会持续发布速度指令的程序。
- 发布 A 点为 Navigation 目标。
发布后只观察 Navigation 状态,不把到达某个物理点作为唯一判断:
- 是否生成路径;
- 是否开始执行;
- 是否
canceled; Recoveries是否增加;- 是否被 costmap 阻挡;
- 是否接近目标容差范围。
如果手动 Nav2 Goal 的近距离目标都不能稳定到达,先回到 常见异常排查,不要继续测试保存导航点的实际移动。
自动探索
自动探索是进阶功能,不作为首次导航。先完成 Gmapping 建图、查看保存的地图文件 和近距离 2D Navigation:使用地图导航,再测试自动探索。
-
先在当前 Docker ROS 2 终端加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
启动建图导航。
ros2 launch ugv_nav slam_nav.launch.py use_rviz:=true
继续启动探索入口时,不要结束当前建图导航终端。
-
另开终端启动探索入口。新终端也先加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash
ros2 launch explore_lite explore.launch.py
自动探索会让机器人自主选择目标点并移动。使用前确认场地封闭、规则、空旷,并全程准备 停止 Navigation 的方式。保存探索地图时,可按 Gmapping 建图 中的保存方式处理。停止时先停止探索节点,再按 停止 Navigation 中的方式停止底盘运动,最后停止 SLAM / Navigation 相关 launch。
rosbag 数据记录、复现与离线分析
rosbag 是调试和复现工具,不是基础启动流程的一部分。建议先完成底盘、雷达、TF、RViz、2D 建图和 Navigation 的基本流程后,再使用 rosbag 记录问题现场或保存可复现数据。
rosbag 用于把 ROS 2 中已经发布的 topic 数据保存成文件,例如雷达扫描、TF、里程计和 IMU 数据。记录后的数据可以回放,用于复现问题、离线查看和对比调试。rosbag 不是启动功能的命令,只负责保存和回放已经发布出来的数据。
记录底盘、雷达、TF 和里程计数据
该任务用于记录 UGV 低速转向时的雷达、TF 和里程计数据,便于后续检查雷达扫描、车体姿态和坐标关系是否同步变化。
保持 Docker ROS 2 终端 1 继续运行。记录任务本身不驱动底盘;为了让记录内容有观察价值,需要在记录过程中让 UGV 低速原地转向。
另开 Docker ROS 2 终端,加载 ROS 2 环境。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
检查当前 topic。
ros2 topic list | grep -E "scan|tf|odom|imu|voltage|cmd"
本任务默认只记录传感器和状态 topic,不记录 /cmd_vel。/cmd_vel 是底盘速度指令;如果把它录进 rosbag,回放时也会重新发布该控制指令。只有在明确需要复现控制输入时,才记录 /cmd_vel,并在回放前确认真实底盘不会响应该 topic。
开发者补充:记录控制 topic 的风险
如果需要分析控制输入,可以把 /cmd_vel 加入记录命令。但回放包含 /cmd_vel 的 bag 前,先确认真实底盘驱动不会接收该 topic,或在安全环境中测试。
创建保存目录并记录数据。
mkdir -p /home/ws/ugv_ws/rosbag_records
ros2 bag record -o /home/ws/ugv_ws/rosbag_records/ugv_lidar_tf_odom \
/scan \
/tf \
/tf_static \
/odom \
/imu/data_raw \
/voltage
这里使用完整路径 /home/ws/ugv_ws/rosbag_records,便于在 MobaXterm 左侧文件栏中直接找到。
如果某个 topic 在当前 topic 列表中不存在,请从记录命令中删除该 topic 后再执行。
记录期间,另开一个 Docker ROS 2 终端,让记录数据产生变化。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
timeout 5 ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}, angular: {z: 0.50}}"
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}, angular: {z: 0.0}}"
该命令会让 UGV 低速原地转向约 5 秒。请将 UGV 放在地面,周围留出安全空间。动作结束后会发送一次 0 速度命令。
也可以在记录期间使用 键盘控制 短按 J / L 让 UGV 原地转向,随后按 K 或空格停止。
回到 ros2 bag record 终端,按 Ctrl + C 停止记录,并等待数据写入完成。
记录完成后会看到以下内容:
ros2 bag record终端会显示正在记录的 topic;- UGV 低速转向时,
/tf、/odom、/scan等数据会随时间变化; - 停止记录后,
/home/ws/ugv_ws/rosbag_records/ugv_lidar_tf_odom目录中会生成metadata.yaml和.db3数据文件; - 使用
ros2 bag info可以查看记录时长、topic 列表和消息数量。
在 MobaXterm 左侧文件栏勾选 Follow terminal folder,进入 /home/ws/ugv_ws/rosbag_records/ugv_lidar_tf_odom,可以看到本次记录生成的文件。
metadata.yaml 是 rosbag 的索引说明文件,可以用文本编辑器打开。里面会记录 bag 版本、存储格式、记录时长、起始时间、总消息数量、每个 topic 的名称、消息类型和消息数量。例如 /scan 对应 sensor_msgs/msg/LaserScan,/odom 对应 nav_msgs/msg/Odometry。
.db3 文件是 SQLite 数据库文件,保存实际消息数据。它不适合直接用文本编辑器阅读。日常查看优先使用 ros2 bag info 和 ros2 bag play;需要查看数据库表时,可用 SQLite 工具打开 .db3 文件,里面主要包含 topic 信息和按时间保存的 ROS 2 消息数据。
查看与回放 rosbag
先加载 ROS 2 环境。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
查看 rosbag 信息。
ros2 bag info /home/ws/ugv_ws/rosbag_records/ugv_lidar_tf_odom
回放 rosbag。
ros2 bag play /home/ws/ugv_ws/rosbag_records/ugv_lidar_tf_odom
终端显示 Opened database ... for READ_ONLY、Set rate to 1 和暂停 / 调速快捷键提示时,表示 rosbag 已经打开 .db3 数据库并开始回放。回放时终端不会逐条打印 /scan、/tf、/odom 等消息,而是在后台重新发布 bag 中保存的 topic。需要停止回放时,在该终端按 Ctrl + C。
如需确认回放数据正在发布,保持 ros2 bag play 终端继续运行,不要停止。另开 Docker ROS 2 终端执行下面命令。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 topic list | grep -E "scan|tf|odom|imu|voltage"
ros2 topic echo /scan --once
ros2 topic list 会输出本次回放中的 topic,例如 /scan、/tf、/tf_static、/odom、/imu/data_raw 和 /voltage。ros2 topic echo /scan --once 会输出一条 LaserScan 消息,其中包含 frame_id: base_lidar_link、角度范围、距离范围和 ranges 数组。ranges 中的数字是雷达到周围物体的距离,.nan 表示该角度没有有效距离读数。
如果 ros2 bag play 已经结束,或在回放完成后才执行 ros2 topic echo /scan --once,终端会提示 WARNING: topic [/scan] does not appear to be published yet 和 Could not determine the type for the passed topic。这表示当前没有节点正在发布 /scan,不是 rosbag 文件损坏。重新启动 ros2 bag play 后,再另开终端检查。
AprilTag 视觉标签识别
AprilTag 是一种视觉基准标签,由已知编码的黑白图案组成。摄像头拍到标签后,程序可以识别标签 ID,并根据图案角点计算标签在图像中的位置。它常用于视觉识别测试、目标标记、简单交互控制、定位辅助和自动对接等场景。
基础测试只使用 tag36h11 ID 0。先用手机或平板显示标签,确认摄像头画面中能看到完整黑白图案和外侧白边。
手机或平板显示标签时,调高亮度,避免反光,不要裁掉外侧白边。从 30 cm ~ 80 cm 距离开始测试。进行姿态估计、跟踪或控制测试时,使用打印版,按 100% 比例打印并固定在平整纸板上。
有效黑白图案区域才是 AprilTag 姿态估计使用的 tag size;外侧白边和 A4 纸张空白不计入 tag size。如果参数使用 tag size 0.08,打印时让标签有效黑白图案区域接近 80 mm。
- 启动摄像头。在 Docker ROS 2 终端中执行:
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 launch ugv_vision camera.launch.py
保持摄像头终端运行,不要关闭。
- 确认图像。另开 Docker ROS 2 终端检查图像 topic:
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 topic list | grep -E "image|camera|rect"
timeout 5 ros2 topic hz /image_raw
timeout 5 ros2 topic hz /image_raw 会等待约 5 秒。如果看到 average rate,说明 /image_raw 正在发布图像。若一直没有频率输出,说明摄像头节点没有发布图像或摄像头 launch 已退出;先回到摄像头终端检查日志,必要时按 Ctrl + C 停止后重新运行 ros2 launch ugv_vision camera.launch.py。
确认 /image_raw 有频率输出后,继续打开图像查看器:
ros2 run rqt_image_view rqt_image_view
在 rqt_image_view 中选择 /image_raw。能看到 USB 摄像头画面后,再继续下一步启动识别节点。
- 添加安全识别节点。
apriltag_detect_only是本教程用于基础识别的安全节点。该节点只订阅图像、识别 ID、发布结果图像,不调用behavior,也不控制底盘。
另开一个 Docker ROS 2 终端,在设备端工作区执行下面命令。该命令会写入安全识别节点,并把 apriltag_detect_only 加入 ugv_vision 的可执行入口。
cd /home/ws/ugv_ws
mkdir -p src/ugv_main/ugv_vision/ugv_vision
cat > src/ugv_main/ugv_vision/ugv_vision/apriltag_detect_only.py <<'PYFILE'
import cv2
import rclpy
from apriltag import apriltag
from cv_bridge import CvBridge
from rclpy.node import Node
from sensor_msgs.msg import Image
class ApriltagDetectOnly(Node):
def __init__(self):
super().__init__('apriltag_detect_only')
self.declare_parameter('image_topic', '/image_raw')
self.declare_parameter('result_topic', '/apriltag_detect_only/result')
self.declare_parameter('tag_family', 'tag36h11')
self.declare_parameter('draw_result', True)
self.declare_parameter('print_detection', True)
self.declare_parameter('print_interval_sec', 0.5)
image_topic = self.get_parameter('image_topic').value
result_topic = self.get_parameter('result_topic').value
tag_family = self.get_parameter('tag_family').value
self.draw_result = self.get_parameter('draw_result').value
self.print_detection = self.get_parameter('print_detection').value
self.print_interval_sec = float(self.get_parameter('print_interval_sec').value)
self.last_print_time = 0.0
self.bridge = CvBridge()
self.detector = apriltag(tag_family)
self.image_sub = self.create_subscription(Image, image_topic, self.image_callback, 10)
self.result_pub = self.create_publisher(Image, result_topic, 10)
self.get_logger().info(f'Input image topic: {image_topic}')
self.get_logger().info(f'Result image topic: {result_topic}')
self.get_logger().info(f'AprilTag family: {tag_family}')
def image_callback(self, msg):
frame = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
results = self.detector.detect(gray)
now_sec = self.get_clock().now().nanoseconds / 1e9
for result in results:
tag_id = int(result['id'])
center_x = int(result['center'][0])
center_y = int(result['center'][1])
if self.draw_result:
corners = result['lb-rb-rt-lt'].astype(int)
cv2.polylines(frame, [corners], isClosed=True, color=(0, 255, 0), thickness=2)
cv2.circle(frame, (center_x, center_y), 5, (0, 0, 255), -1)
cv2.putText(
frame,
f'ID {tag_id} ({center_x}, {center_y})',
(center_x + 8, center_y - 8),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 255, 0),
2,
)
if self.print_detection and now_sec - self.last_print_time >= self.print_interval_sec:
print(f'Tag ID: {tag_id}, Center: ({center_x}, {center_y})')
self.last_print_time = now_sec
result_msg = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
self.result_pub.publish(result_msg)
def main(args=None):
rclpy.init(args=args)
node = ApriltagDetectOnly()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
PYFILE
python3 - <<'PYSETUP'
from pathlib import Path
p = Path("src/ugv_main/ugv_vision/setup.py")
text = p.read_text()
entry = "apriltag_detect_only = ugv_vision.apriltag_detect_only:main"
if entry not in text:
old = " 'apriltag_track_2 = ugv_vision.apriltag_track_2:main'\n"
new = (
" 'apriltag_track_2 = ugv_vision.apriltag_track_2:main',\n"
" 'apriltag_detect_only = ugv_vision.apriltag_detect_only:main'\n"
)
if old not in text:
raise SystemExit("未找到 apriltag_track_2 入口,请手动检查 setup.py")
p.write_text(text.replace(old, new))
print("apriltag_detect_only entry is ready")
PYSETUP
- 重新构建
ugv_vision并检查入口:
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
colcon build --packages-select ugv_vision --symlink-install
source install/setup.bash
ros2 pkg executables ugv_vision | grep apriltag
重新检查时应能看到 ugv_vision apriltag_detect_only。
启动安全识别节点
-
启动安全识别节点。保持摄像头终端继续运行,另开一个 Docker ROS 2 终端执行下面命令:
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 run ugv_vision apriltag_detect_only
检查 AprilTag ID 0 识别结果
-
查看识别结果。将
tag36h11ID 0 标签完整对准摄像头,保持黑白图案和外侧白边都在画面中。先从30 cm ~ 80 cm距离开始,避免反光、遮挡和过度倾斜。识别运行后,终端会先输出输入图像、结果图像和标签家族信息,随后持续打印识别到的标签 ID 和中心坐标:
[INFO] [apriltag_detect_only]: Input image topic: /image_raw[INFO] [apriltag_detect_only]: Result image topic: /apriltag_detect_only/result[INFO] [apriltag_detect_only]: AprilTag family: tag36h11Tag ID: 0, Center: (369, 291)Tag ID: 0, Center: (318, 289)Tag ID: 0, Center: (322, 278)Tag ID: 0, Center: (335, 280)Tag ID: 0, Center: (369, 296)Tag ID: 0, Center: (371, 295)Tag ID: 0, Center: (382, 294)Tag ID: 0, Center: (388, 294)Tag ID: 0, Center: (380, 270)Tag ID: 0, Center: (448, 190)识别结果中会看到:
- 终端输出
Tag ID: 0; - 移动标签时,
Center坐标变化; - 节点会发布
/apriltag_detect_only/result结果图像 topic; - 遮挡、反光、距离过远或图案过小时,识别可能中断;
- 不出现
Action server not available!; - UGV 不运动。
- 终端输出
需要更多 ID 或不同尺寸时,使用标准 AprilTag 生成器或 AprilTag 官方软件页面 提供的资源生成 tag36h11 家族标签。
OAK-D Lite 基础体验
OAK-D Lite 是一款 RGB-D 深度相机,可以同时提供彩色图像和深度图像。RTAB-Map 3D 建图会使用 OAK-D Lite 的 RGB 图像、深度图像和相机参数,再结合雷达、里程计和 TF 生成地图数据库。
本节只用于确认 OAK-D Lite 数据是否正常。后续 RTAB-Map 会自动启动 OAK-D Lite,不需要再单独启动相机。
-
在 Docker ROS 2 终端中启动 OAK-D Lite。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 launch ugv_vision oak_d_lite.launch.py -
保持 OAK-D Lite 终端运行,另开 Docker ROS 2 终端检查 topic。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 topic list | grep -Ei "oak|rgb|depth|image|camera_info" -
按实际 topic 检查频率。
timeout 8 ros2 topic hz /oak/rgb/image_recttimeout 8 ros2 topic hz /oak/stereo/image_rawtimeout 8 ros2 topic hz /oak/rgb/camera_info如果 topic 名称不同,以
ros2 topic list的实际输出为准。 -
需要查看 RGB 图像时,可以打开
rqt_image_view。ros2 run rqt_image_view rqt_image_view在窗口中选择
/oak/rgb/image_rect,或选择当前实际存在的 RGB 图像 topic。
可以看到 OAK-D Lite 相关 topic;RGB 图像 topic 和 Depth 图像 topic 有频率输出;camera_info topic 存在;rqt_image_view 中可以选择 RGB 图像 topic;终端没有持续出现设备找不到、DepthAI 启动失败或 USB 错误。
OAK-D Lite 单独体验结束后,在 launch 终端按 Ctrl + C 停止。后续启动 RTAB-Map 时,会由 rtabmap_rgbd.launch.py 自动启动 OAK-D Lite,避免重复启动。
RTAB-Map 三维建图与定位
RTAB-Map 在这里用于 RGB-D 建图和定位。它会结合 OAK-D Lite 的 RGB-D 图像、雷达 /scan、底盘 /odom 和 TF,生成 RTAB-Map 数据库。建图结果默认保存在 ~/.ros/rtabmap.db。
RTAB-Map 建图不是自动导航。建图阶段负责采集环境并生成数据库;导航目标点和路径规划属于 Nav2 等导航栈的功能。
RTAB-Map RGB-D 建图
启动 RTAB-Map 后,可能同时看到 RViz 和 RTAB-Map 自带窗口。RViz 适合观察机器人模型、LaserScan、二维地图、点云地图和图结构;RTAB-Map 自带窗口适合观察 RGB 图像、Odometry 图像、3D Map、关键帧 ID 和回环检测状态。
rtabmap_rgbd.launch.py 会启动底盘与雷达 bringup、OAK-D Lite、robot_pose_publisher、rtabmap_slam/rtabmap 和 rtabmap_viz。RTAB-Map 输入 topic 会映射到 OAK-D Lite 的 RGB 图像、深度图像和相机参数。
| RTAB-Map 输入 | OAK-D Lite topic |
|---|---|
rgb/image | oak/rgb/image_rect |
rgb/camera_info | oak/rgb/camera_info |
depth/image | oak/stereo/image_raw |
-
在 Docker ROS 2 终端加载 ROS 2 环境。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bash -
建图前备份旧数据库。
ls -lh ~/.ros/rtabmap.dbcp ~/.ros/rtabmap.db ~/.ros/rtabmap_backup_$(date +%Y%m%d_%H%M%S).db 2>/dev/null || true当前建图模式会重新生成默认数据库。需要保留旧地图时,先备份
~/.ros/rtabmap.db。 -
设置型号并启动 RTAB-Map RGB-D 建图。
export UGV_MODEL=ugv_roverexport LDLIDAR_MODEL=ld19ros2 launch ugv_slam rtabmap_rgbd.launch.py use_rviz:=true本教程统一显式使用
use_rviz:=true。实际运行时可能同时打开 RViz 和 RTAB-Map 自带窗口,可以保留两个窗口进行对比观察。 -
保持 RTAB-Map 终端运行,另开 Docker ROS 2 终端检查输入数据。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 node list | grep -Ei "rtab|oak|depth|rgb|camera|ugv|driver|base|lidar|pose"ros2 topic list | grep -Ei "rtab|oak|rgb|depth|image|camera_info|odom|scan|tf|cloud|map"检查 RTAB-Map 使用的输入 topic。
timeout 8 ros2 topic hz /oak/rgb/image_recttimeout 8 ros2 topic hz /oak/stereo/image_rawtimeout 8 ros2 topic hz /oak/rgb/camera_infotimeout 8 ros2 topic hz /scan如果 topic 名称不同,以当前
ros2 topic list的实际输出为准。 -
另开 Docker ROS 2 终端,用键盘低速移动采集数据。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 run ugv_tools keyboard_ctrl短按
J/L小幅转向;短按I/,小幅前后移动;每次动作后按K或空格停止。不要快速原地旋转,不要长时间连续运动。建议先采集1到2分钟的小范围地图。
RViz 中 Fixed Frame 为 map,Global Status 为 Ok;RobotModel 正常显示;LaserScan 随环境变化;Map 或 MapCloud 随机器人移动逐渐增长;MapGraph 或轨迹随移动增加;终端没有持续 TF、No data、RGB-D sync 报错。
RTAB-Map 自带窗口中 RGB 图像刷新;Odometry 图像刷新;右侧 3D Map 出现点云;New ID 数字随着采集增加;下方日志持续出现地图更新信息;回到旧区域附近时,可以观察 Loop closure 相关变化。
结束建图时,先在 keyboard_ctrl 终端按 K 或空格停止底盘,再按 Ctrl + C 停止键盘控制。回到 RTAB-Map launch 终端按 Ctrl + C,等待数据库写入完成。
ls -lh ~/.ros/rtabmap.db
~/.ros/rtabmap.db 存在且文件大小不是 0 时,表示数据库已经写入。重新定位前不要随意删除该文件。
RTAB-Map 定位
RTAB-Map 定位用于加载已有数据库,不是重新建图。启动前确认已经完成 RTAB-Map 建图,~/.ros/rtabmap.db 存在,并尽量在与建图时相近的环境中测试。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
export UGV_MODEL=ugv_rover
export LDLIDAR_MODEL=ld19
ls -lh ~/.ros/rtabmap.db
ros2 launch ugv_slam rtabmap_rgbd.launch.py localization:=true use_rviz:=true
定位模式同样显式使用 use_rviz:=true。启动日志不应持续提示找不到数据库、No data received、TF 报错或 RGB-D synchronization failed。机器人小幅移动后应仍在已有地图范围内保持定位,地图不应完全从零重新生成。
需要小幅移动验证时,另开 Docker ROS 2 终端运行键盘控制。
cd /home/ws/ugv_ws
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 run ugv_tools keyboard_ctrl
只做小幅转向或短距离移动,测试 30 秒到 1 分钟即可。测试结束前先按 K 或空格停止底盘,再退出键盘控制和 RTAB-Map 定位终端。
ROS 2 Web App / Vizanti 可视化面板
ROS 2 Web App / Vizanti 是浏览器端 ROS 2 可视化面板,不是上位机 Web 主页面,也不是 JupyterLab。首次学习 SLAM 和 Navigation 时,优先使用 RViz;Vizanti 适合远程浏览器查看、教学演示和轻量调试。
| 工具 | 默认入口 | 作用 |
|---|---|---|
| 上位机 Web 主页面 | http://<UGV_IP>:5000 | 整机 Web 控制、视频、按钮、基础功能 |
| JupyterLab | http://<UGV_IP>:8888 | Notebook、文件管理、短脚本和教学示例 |
| ROS 2 Web App / Vizanti | http://<UGV_IP>:5100 | 浏览器端 ROS 2 可视化面板,查看模型、TF、雷达、地图和导航状态 |
ROS 2 Web App 启动后显示空网格不代表错误。它只是浏览器端可视化面板。只有 ROS 2 中已经存在 /scan、/map、/odom、/tf 等数据,并且在页面中添加对应组件后,才会显示雷达、地图、里程计或导航信息。
-
启动 ROS 2 Web App。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 launch ugv_web_app bringup.launch.py host:=0.0.0.0浏览器访问:
http://<UGV_IP>:5100不要把占位 IP 当作参数值直接输入。如果使用
host:=<UGV_IP>,需要把<UGV_IP>替换成 OLED 或当前网络中显示的机器人实际 IP。测试时也可以使用host:=0.0.0.0,浏览器仍访问http://<UGV_IP>:5100。 -
启动一个要显示的数据源,例如底盘和雷达。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashexport UGV_MODEL=ugv_roverexport LDLIDAR_MODEL=ld19ros2 launch ugv_bringup bringup_lidar.launch.py use_rviz:=false -
另开 Docker ROS 2 终端检查 topic。
cd /home/ws/ugv_wssource /opt/ros/humble/setup.bashsource install/setup.bashros2 topic list | grep -E "scan|map|tf|odom|cmd_vel"timeout 8 ros2 topic hz /scan -
在 Vizanti 页面中添加显示组件。
点击页面左上角
+添加显示组件。常见选择如下:要查看的内容 添加组件 topic 机器人模型 RobotModelrobot_descriptionTF TF/tf、/tf_static雷达 LaserScan/scan地图 Map/map里程计 Odometry/odom路径 PathNavigation 发布的路径 topic
如果没有启动 bringup、SLAM 或 Navigation,页面不会自动显示雷达、地图或路径。
Web AI 交互属于可选二次开发功能,依赖额外 AI 服务配置。相关说明见 二次开发。