第3章 · ROS核心机制速通

ROS 这东西,说白了就是机器人的「操作系统胶水」。我最早接触 ROS 是在 2016 年,那时候被一堆节点、话题搞得晕头转向。后来在项目里调了几个月,才摸清楚它的脾气。这一章咱们就把 ROS 最核心的四个机制捋一遍:节点与话题通信、服务与参数服务器、Launch 文件、Bag 录制回放。嗯,都是你写 SLAM 系统每天都要打交道的家伙。

ROS 核心机制速通 节点 & 话题 异步发布/订阅 服务 & 参数 同步RPC / 全局字典 Launch 文件 多节点启动器 ROS Bag 录制 / 回放 SLAM 系统仿真联调 每个模块都是 SLAM 仿真中不可跳过的基石

3.1 节点与话题通信 —— 机器人的「神经网」

ROS 里最小的执行单元就是节点。你可以把节点想象成一个独立的小程序,专门干一件事:比如读取激光雷达、处理里程计、发布地图。节点之间通过话题(Topic)互相唠嗑,而且是异步发布/订阅模式——发布者只管发,订阅者只管收,两边不用等。

💡 我个人的习惯: 每个传感器驱动单独写一个节点,不要把所有东西塞进一个节点里。这样调试的时候,你只要重启那个节点就行,不用把整个系统停下来。我在做多雷达融合项目时,就因为节点拆分得细,定位问题快了很多。

举个例子,一个最简单的发布者节点(Python):

#!/usr/bin/env python3
import rospy
from std_msgs.msg import String

def talker():
    rospy.init_node('talker_node', anonymous=True)
    pub = rospy.Publisher('chatter', String, queue_size=10)
    rate = rospy.Rate(10)  # 10 Hz
    while not rospy.is_shutdown():
        msg = "Hello SLAM %s" % rospy.get_time()
        pub.publish(msg)
        rate.sleep()

if __name__ == '__main__':
    try:
        talker()
    except rospy.ROSInterruptException:
        pass

对应的订阅者节点,用 rospy.Subscriber 接收就行。你想想看,这种模式在 SLAM 里多自然——激光雷达节点不断发 sensor_msgs/LaserScan,前端匹配节点订阅它,后端优化节点再订阅前端的结果,各司其职。

⚠️ 我曾经踩过的坑: 话题名字写错一个字母,结果节点怎么都收不到数据。后来我养成了一个习惯:每次运行前先 rostopic list 看一眼实际话题名,再 rostopic echo /topic 确认数据格式。别笑,这招救过我很多次。

3.2 服务与参数服务器

话题是「发完就不管」的异步通信,但有时候你需要一问一答——比如请求地图保存、请求重置里程计。这时候就该服务(Service)上场了。服务是同步的:客户端发请求,服务端处理完返回响应。

另外,参数服务器相当于一个全局字典,所有节点都能读写。我一般把机器人的轮距、激光雷达安装高度、地图分辨率这些「不怎么变但又需要调」的参数放在参数服务器里。

通信方式特点SLAM 典型场景
话题 (Topic)异步、多对多、持续流激光雷达数据、里程计、地图
服务 (Service)同步、一对一、触发式保存地图、重置位姿、加载参数
参数服务器全局键值存储传感器参数、算法阈值、机器人模型路径

服务定义使用 .srv 文件,比如保存地图的服务:

# SaveMap.srv
string map_name      # 请求:地图名字
---
bool success         # 响应:是否成功
string message       # 响应:附加信息

🔧 小技巧: 用 rosparam set /my_param value 可以在命令行临时改参数,不用重启节点。我在调试 SLAM 前端匹配阈值时,经常一边跑一边调参数,效率很高。

3.3 Launch 文件 —— 一键启动全家桶

手动开一堆终端,一个个 rosrun,太原始了。Launch 文件就是用来批量启动节点的,还能设置参数、重映射话题、嵌套包含其他 launch 文件。说白了,它就是 ROS 世界的「docker-compose.yml」。

一个典型的 launch 文件长这样:

<?xml version="1.0"?>
<launch>
    <!-- 启动激光雷达驱动 -->
    <node name="lidar_driver" pkg="my_slam" type="lidar_node.py" output="screen"/>

    <!-- 启动前端匹配节点,并设置参数 -->
    <node name="frontend" pkg="my_slam" type="frontend_node">
        <param name="icp_max_iter" value="30"/>
        <param name="voxel_leaf_size" value="0.1"/>
    </node>

    <!-- 启动可视化 -->
    <node name="rviz" pkg="rviz" type="rviz" args="-d $(find my_slam)/config/slam.rviz"/>
</launch>

我个人习惯把 launch 文件按功能拆分:sensors.launch 放所有传感器驱动,slam_core.launch 放算法节点,viz.launch 放可视化。然后在最外层用一个 startup.launch 把它们 <include> 进来。这样团队协作时,每个人只管自己那部分。

3.4 ROS Bag 录制与回放 —— 时间机器

做 SLAM 仿真,最怕什么?怕机器人跑完了,算法崩了,想复现现场却没法重来。ROS Bag 就是你的「时间机器」——它能把所有话题数据录下来,之后可以原封不动地回放,就好像机器人又跑了一遍。

录制一条 bag 命令极其简单:

# 录制所有话题
rosbag record -a -o my_slam_run.bag

# 只录制关键话题(节省空间)
rosbag record /scan /odom /tf -O lidar_odom.bag

回放时,你可以用 --clock 选项让系统时间跟着 bag 走,还能用 -r 0.5 放慢速度,方便调试:

rosbag play my_slam_run.bag --clock -r 0.5

📌 我自己的经验: 在仿真环境里,我每次跑完一个 SLAM 实验都会自动录一份 bag。如果后面发现地图有重影或者轨迹漂移,直接回放 bag,换参数重新跑算法,不用再启动 Gazebo。这至少节省了我一半的调试时间。

另外,bag 文件还能用 rosbag info 查看信息,用 rosbag filter 裁剪话题。比如你只想要前 30 秒的数据做单元测试:

rosbag filter input.bag output.bag "t.to_sec() <= 30.0"

⚠️ 注意: 录制 bag 时如果硬盘写速度跟不上,可能会丢帧。我建议用 SSD 或者挂载内存盘来录高频话题(比如 100Hz 的 IMU)。曾经我在机械硬盘上录 200Hz 的激光点云,结果 bag 里全是断帧,白忙活了一下午。