4、ROS2基础与通信机制:节点、话题、服务、动作的编程模型与QoS配置

好,咱们进入ROS2的核心地带。说实话,ROS1到ROS2的升级,最让我觉得脱胎换骨的就是通信机制这块。你想想看,自动驾驶系统里,传感器数据、控制指令、状态反馈,哪个不是靠通信在跑?如果通信不稳,车就别想开稳。

我个人习惯把ROS2的通信比作一个「消息快递系统」。节点就是收发快递的人,话题是公共信箱,服务是打电话问客服,动作则是下单后跟踪物流。咱们一个一个来拆解。

4.1 节点:系统的最小单元

节点,说白了就是一个可执行程序。每个节点只干一件事,比如激光雷达节点只管发点云,规划节点只管算路径。我刚开始做项目时,总喜欢把一堆功能塞进一个节点里,结果调试起来想死的心都有。后来学乖了——一个节点只做一件事,做好它

创建节点很简单,Python里这样写:

import rclpy
from rclpy.node import Node

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node_name')
        self.get_logger().info('节点已启动!')

def main(args=None):
    rclpy.init(args=args)
    node = MyNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

嗯,这里要注意:rclpy.spin() 会阻塞主线程,让你的节点一直活着。如果你有多个节点要同时跑,就得用多线程或者多进程了。我在做多传感器融合时,就吃过这个亏——一个节点spin住了,其他节点全饿死。

我的小技巧: 节点命名一定要有规律。比如 lidar_front_nodecamera_left_node,别用 node1node2。不然项目大了,你根本分不清谁是谁。

4.2 话题:发布-订阅模型

话题是ROS2里最常用的通信方式。一个节点发布消息,多个节点可以同时订阅。就像电台广播,你只管发,谁爱听谁听。

我记得有一次做数据采集,激光雷达的点云数据量特别大。我一开始用默认的QoS配置,结果发现数据老是丢包。后来才意识到——话题通信的质量完全取决于QoS配置。咱们先看代码,再聊QoS。

发布者示例:

self.publisher_ = self.create_publisher(String, 'topic_name', 10)
msg = String()
msg.data = 'Hello, ROS2!'
self.publisher_.publish(msg)

订阅者示例:

self.subscription = self.create_subscription(
    String, 'topic_name', self.callback, 10)

那个数字 10 就是队列深度。什么意思?就是消息缓存区能存多少条。我踩过的坑是:如果回调函数处理太慢,队列满了,新消息就会丢。所以队列深度要根据你的处理速度来调,不是越大越好。

4.3 服务:请求-响应模型

话题是单向的,服务是双向的。你发一个请求,对方给你一个响应。就像你问「现在几点了?」,对方回答「下午三点」。适合那些需要确认的场景,比如「启动录制」、「切换模式」。

服务端示例:

from example_interfaces.srv import AddTwoInts

def add_callback(request, response):
    response.sum = request.a + request.b
    return response

self.srv = self.create_service(AddTwoInts, 'add_two_ints', add_callback)

客户端示例:

self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
    self.get_logger().info('服务未就绪,等待中...')
req = AddTwoInts.Request()
req.a = 3
req.b = 5
future = self.cli.call_async(req)
我曾经犯过的错: 调用服务前没检查服务是否在线。结果程序直接崩溃。记住:wait_for_service() 是必须的,别偷懒。

4.4 动作:带反馈的长时间任务

动作是服务的升级版。适合那些需要跑一会儿的任务,比如「导航到A点」、「执行泊车」。你可以中途取消,也可以实时收到进度反馈。

动作由三部分组成:目标、反馈、结果。我做个比喻:你叫了个外卖,目标是「送到家」,反馈是「骑手已取餐、正在路上」,结果是「已送达」。

动作服务器端核心代码:

from example_interfaces.action import Fibonacci

class FibonacciActionServer(Node):
    def __init__(self):
        super().__init__('fibonacci_action_server')
        self.action_server = ActionServer(
            self, Fibonacci, 'fibonacci',
            self.execute_callback)

动作客户端:

self.action_client = ActionClient(self, Fibonacci, 'fibonacci')
goal_msg = Fibonacci.Goal()
goal_msg.order = 10
self.action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_cb)
关键点: 动作的反馈机制在自动驾驶里特别有用。比如数据采集时,你可以实时看到「已采集100帧、200帧...」,而不是傻等。

4.5 QoS配置:通信质量的保障

终于到QoS了。这是ROS2相比ROS1最大的改进之一。QoS决定了消息怎么传输、怎么缓存、怎么丢弃。我把它总结成三个核心参数:

参数 含义 我的建议
History 保留最近N条还是全部 传感器数据用KEEP_LAST,日志用KEEP_ALL
Reliability 可靠传输还是尽力而为 控制指令用RELIABLE,点云用BEST_EFFORT
Durability 晚订阅能否收到历史数据 地图数据用TRANSIENT_LOCAL,实时数据用VOLATILE

举个例子。激光雷达点云数据量巨大,如果用RELIABLE模式,网络稍微一卡,整个系统就堵死了。所以我一般用BEST_EFFORT,丢几帧无所谓,实时性更重要。但控制指令就不一样了——丢一条指令,车可能就撞了,必须用RELIABLE。

配置QoS的代码:

from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy

qos_profile = QoSProfile(
    depth=10,
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST
)

self.publisher = self.create_publisher(
    PointCloud2, 'lidar_points', qos_profile)
避坑指南: 发布者和订阅者的QoS必须兼容。比如发布者用BEST_EFFORT,订阅者用RELIABLE,那订阅者就收不到消息。我曾经因为这个排查了一整天,最后发现是QoS不匹配。

4.6 四种通信模型的选择策略

最后,我给大家一个选择指南。在自动驾驶系统里,怎么选通信方式?

  • 话题:传感器数据、状态信息、高频数据流。比如IMU、GPS、图像。
  • 服务:一次性请求-响应。比如「启动采集」、「切换模式」、「查询状态」。
  • 动作:长时间任务,需要反馈和取消。比如「录制一段数据」、「执行一段路径」。
  • QoS:根据数据重要性、实时性、可靠性要求来配。没有万能配置,只有最适合的配置。

嗯,总结一下。ROS2的通信机制其实不复杂,但细节很多。我做了这么多年自动驾驶,最大的体会就是:通信设计决定了系统的上限。节点拆得细、话题分得清、QoS配得对,你的系统才能跑得稳、跑得快。

下一章咱们聊聊数据采集的具体实现,包括怎么同步多传感器、怎么处理时间戳。到时候我会分享一些我在实车测试中踩过的坑,保证让你少走弯路。