2、ROS2基础:节点、话题、服务、动作,以及DDS通信机制

好,咱们正式开始聊ROS2。说实话,我第一次接触ROS2的时候,心里是有点抵触的。毕竟ROS1用了那么多年,顺手得很。但后来在项目里被ROS1的通信瓶颈坑过几次之后,我才真正理解了ROS2为什么要大改。这一章,我就带你把这些最核心的概念捋一遍。

2.1 节点(Node)—— 系统的“小工兵”

节点是什么?说白了,就是一个独立的可执行程序。每个节点只干一件事,比如控制激光雷达、处理图像、规划路径。我习惯把节点想象成一个“小工兵”,各司其职,互不干扰。

在ROS2里,启动一个节点很简单:

import rclpy
from rclpy.node import Node

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

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

嗯,这里要注意:rclpy.spin() 这个函数会让节点一直运行,直到你按 Ctrl+C。我曾经在调试时忘了加这一行,结果节点启动完就退出了,找了好久才发现。

个人习惯:我一般会在节点初始化时打印一条日志,确认节点确实跑起来了。别小看这个习惯,分布式调试时它能帮你快速定位问题。

2.2 话题(Topic)—— 异步的“广播站”

话题是ROS2里最常用的通信方式。它是异步的,发布者只管发,订阅者只管收,两者不需要知道对方的存在。你想想看,这就像广播电台——主持人只管播,听众只管听,谁也不认识谁。

发布者的代码大概长这样:

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.listener_callback, 10)

那个数字“10”是队列深度。我在项目中遇到过一个问题:如果订阅者处理太慢,队列满了,新消息就会丢。所以,队列深度要根据你的处理速度来调,不是越大越好。

2.3 服务(Service)—— 同步的“一问一答”

话题是单向广播,服务则是双向的。客户端发请求,服务端回响应。这就像你打电话问客服:“我的订单到哪了?”客服查完告诉你结果。整个过程是同步的,你得等着。

服务端定义:

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')
req = AddTwoInts.Request()
req.a = 3
req.b = 5
future = self.cli.call_async(req)
rclpy.spin_until_future_complete(self, future)
避坑指南:我曾经在服务端忘记启动节点,结果客户端一直等,超时了也没反应。记住:服务通信的前提是服务端必须在线。否则客户端会抛出异常。

2.4 动作(Action)—— 带反馈的“长任务”

动作是ROS2里最复杂的通信方式。它适合那些需要长时间执行、并且需要中途反馈的任务。比如让机器人走到某个点——你不仅想知道它最终到了没,还想知道它走到哪了。

动作由三部分组成:目标(Goal)、反馈(Feedback)、结果(Result)。

动作服务端示例:

from example_interfaces.action import Fibonacci

def execute_callback(self, goal_handle):
    self.get_logger().info('开始执行...')
    feedback_msg = Fibonacci.Feedback()
    feedback_msg.sequence = [0, 1]
    
    for i in range(1, goal_handle.request.order):
        feedback_msg.sequence.append(
            feedback_msg.sequence[i] + feedback_msg.sequence[i-1])
        goal_handle.publish_feedback(feedback_msg)
    
    goal_handle.succeed()
    result = Fibonacci.Result()
    result.sequence = feedback_msg.sequence
    return result

self._action_server = ActionServer(
    self, Fibonacci, 'fibonacci', 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_callback)
核心区别:话题是“发完不管”,服务是“一问一答”,动作是“边干边汇报”。选哪个?看你的场景。

2.5 DDS通信机制——ROS2的“隐形引擎”

ROS2和ROS1最大的不同,就是底层通信换成了DDS(Data Distribution Service)。DDS是什么?说白了,它是一个去中心化的实时数据分发标准。

ROS1用的是TCPROS/UDPROS,中心化架构,有一个Master节点。一旦Master挂了,整个系统就瘫痪了。我当年在园区做无人车测试时,就遇到过Master节点因为网络波动宕机,所有节点瞬间失联,车直接停在路中间...那场面,别提多尴尬了。

ROS2用DDS,没有中心节点。每个节点都直接和DDS的“全局数据空间”通信。DDS负责发现节点、管理QoS(服务质量)、保证可靠性。

DDS的几个关键特性:

  • 去中心化:没有单点故障,节点之间点对点通信
  • QoS策略:你可以控制消息的可靠性、持久性、生命周期等
  • 实时性:支持实时系统,延迟低至微秒级
  • 可扩展:从嵌入式设备到云端都能跑

ROS2默认的DDS实现是Fast DDS(以前叫Fast RTPS)。你也可以换成Cyclone DDS或Connext DDS。我个人习惯用Fast DDS,开源、轻量、社区活跃。

下面这张图展示了ROS2的通信架构:

ROS2 通信架构图 应用层(节点、话题、服务、动作) RCL(ROS客户端库) RMW(ROS中间件接口) DDS(数据分发服务) Fast DDS / Cyclone DDS / Connext DDS 抽象层

从这张图你能看到,ROS2的通信是分层的。最上层是应用层,你写节点、发话题、调服务都在这里。中间是RCL和RMW,它们负责把ROS2的API翻译成DDS能理解的语言。最底层就是DDS,它才是真正干活的。

QoS到底有多重要?我举个例子:激光雷达的数据丢了就丢了,下一帧马上来,所以用“尽力而为”模式就行。但控制指令不能丢,丢了车可能就撞了,所以必须用“可靠”模式。DDS的QoS让你能精细控制这些行为。

2.6 小结

这一章我们聊了ROS2的四个核心通信方式:

  • 节点:系统的最小单元,各干各的活
  • 话题:异步广播,适合传感器数据流
  • 服务:同步请求-响应,适合一次性查询
  • 动作:带反馈的长任务,适合机器人控制
  • DDS:底层通信引擎,去中心化、可配置QoS

说实话,这些概念光看文字可能觉得抽象。我建议你动手跑几个示例,把代码敲一遍。只有亲手写过,才能真正理解它们之间的区别。下一章我们会深入聊DDS的QoS策略,那才是ROS2真正强大的地方。

我的建议:刚开始学ROS2,别急着上复杂项目。先搭两个节点,一个发一个收,把话题和服务调通。这个过程能帮你建立对通信机制的直觉。

公众号:蓝海资料掘金营,微信deep3321