第二章:ROS2核心概念——节点、话题、服务、动作、参数

各位嵌入式同仁,欢迎来到第二章。说实话,ROS2 的概念体系,是很多初学者第一道坎。我记得刚接触 ROS1 时,被节点、话题、服务这几个词绕得晕头转向。后来做项目多了,才慢慢摸清它们的脾气。

这一章,我会把 ROS2 的五个核心概念——节点、话题、服务、动作、参数,掰开揉碎了讲。每个概念我都会结合嵌入式场景,说说我踩过的坑。

2.1 节点:ROS2 的最小执行单元

节点是什么?说白了,就是一个独立的可执行进程。每个节点负责一件事。比如,一个节点读传感器,另一个节点控制电机,第三个节点做决策。

我习惯把节点想象成一个「小工人」。每个工人只干自己的活,不操心别人。这样系统解耦,好维护。

关键点:ROS2 是分布式架构。节点可以运行在同一块芯片上,也可以分布在多台机器上。节点之间通过「通信总线」交换数据。

创建节点很简单。用 Python 的话,几行代码搞定:

import rclpy
from rclpy.node import Node

class MyNode(Node):
    def __init__(self):
        super().__init__('my_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() 会让节点一直运行,处理回调。如果你在嵌入式板子上跑,记得控制好 CPU 占用。

2.2 话题:异步通信的「广播」模式

话题是 ROS2 最常用的通信方式。一个节点发布消息,其他节点订阅。发布者和订阅者互不知道对方存在。

我在项目中遇到过一个问题:两个传感器节点同时往同一个话题发数据,结果数据打架了。后来我加了时间戳,才解决。

话题通信是异步的。发布者只管发,不管有没有人收。订阅者只管收,不管谁发的。这种模式适合传感器数据、状态信息等周期性数据。

举个例子,发布话题:

class PublisherNode(Node):
    def __init__(self):
        super().__init__('publisher')
        self.pub = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello, ROS2!'
        self.pub.publish(msg)

订阅话题:

class SubscriberNode(Node):
    def __init__(self):
        super().__init__('subscriber')
        self.sub = self.create_subscription(String, 'chatter', self.callback, 10)

    def callback(self, msg):
        self.get_logger().info(f'收到: {msg.data}')

小技巧:话题的 QoS(服务质量)设置很重要。我建议对传感器数据用「可靠」模式,对控制指令用「尽力而为」模式,减少延迟。

2.3 服务:同步通信的「请求-应答」模式

服务跟话题不一样。服务是同步的。客户端发请求,服务端处理完返回结果。客户端必须等。

你想想看,什么时候用服务?比如,你让机器人「打开摄像头」,它打开后告诉你「已打开」。这种一次性的、需要确认的操作,用服务最合适。

我曾经犯过一个错:用话题去实现「打开摄像头」的功能。结果发布者发了指令,不知道摄像头到底开了没。后来改成服务,问题解决。

定义服务接口(.srv 文件):

int64 a
int64 b
---
int64 sum

服务端代码:

class ServiceNode(Node):
    def __init__(self):
        super().__init__('service_node')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_callback)

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

客户端代码:

class ClientNode(Node):
    def __init__(self):
        super().__init__('client_node')
        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)
        rclpy.spin_until_future_complete(self, future)
        self.get_logger().info(f'结果: {future.result().sum}')

注意:服务调用是阻塞的。在嵌入式系统里,如果服务处理时间太长,会卡住整个节点。我建议用异步调用(call_async),或者把耗时操作放到单独线程。

2.4 动作:带反馈的「长任务」模式

动作是 ROS2 里最复杂的概念,但也是最强大的。它适合执行时间长的任务,比如「导航到某个点」、「抓取物体」。

动作有三个阶段:

  • 目标:客户端发送目标
  • 反馈:服务端不断返回进度
  • 结果:任务完成,返回最终结果

我记得第一次用动作时,觉得它跟服务差不多。后来做机器人底盘控制才发现,动作的「可取消」特性太重要了。用户随时可以喊停。

定义动作接口(.action 文件):

# 目标
int32 target_x
int32 target_y
---
# 结果
bool success
string message
---
# 反馈
int32 current_x
int32 current_y
float32 progress

动作服务端示例:

class MoveActionServer(Node):
    def __init__(self):
        super().__init__('move_action_server')
        self._action_server = ActionServer(
            self,
            Move,
            'move_robot',
            self.execute_callback)

    def execute_callback(self, goal_handle):
        feedback_msg = Move.Feedback()
        # 模拟移动过程
        for i in range(10):
            feedback_msg.current_x = i
            feedback_msg.current_y = i
            feedback_msg.progress = i / 10.0
            goal_handle.publish_feedback(feedback_msg)
            time.sleep(0.5)
        goal_handle.succeed()
        result = Move.Result()
        result.success = True
        result.message = '到达目标'
        return result

核心区别:话题是「广播」,服务是「问答」,动作是「任务」。选哪个,取决于你的场景。

2.5 参数:节点的「配置开关」

参数是节点的配置项。比如,摄像头帧率、PID 系数、串口波特率。参数可以在启动时设置,也可以在运行时动态修改。

我习惯把参数看作「旋钮」。你可以在系统运行时拧一拧,看看效果。不用重新编译代码。

声明参数:

self.declare_parameter('camera_fps', 30)
self.declare_parameter('pid_kp', 1.0)
self.declare_parameter('serial_baud', 115200)

读取参数:

fps = self.get_parameter('camera_fps').value
kp = self.get_parameter('pid_kp').value

动态修改参数(命令行):

ros2 param set /my_node camera_fps 60

建议:把硬件相关的配置(如 GPIO 引脚号、I2C 地址)都做成参数。这样换硬件时,改参数就行,不用改代码。

2.6 五大概念的关系图

说了这么多,它们之间到底是什么关系?我画个简单的图帮你理解:

┌─────────────────────────────────────────────────────┐
│                     ROS2 系统                        │
│                                                     │
│  ┌──────────┐   话题    ┌──────────┐               │
│  │ 传感器节点 │ ────────→ │ 控制节点  │               │
│  └──────────┘           └──────────┘               │
│       │                                              │
│       │ 服务                                         │
│       ↓                                              │
│  ┌──────────┐   动作    ┌──────────┐               │
│  │ 执行器节点 │ ←───────→ │ 决策节点  │               │
│  └──────────┘           └──────────┘               │
│       │                                              │
│       │ 参数                                         │
│       ↓                                              │
│  ┌──────────┐                                       │
│  │ 参数服务器 │                                       │
│  └──────────┘                                       │
└─────────────────────────────────────────────────────┘

简单总结:

  • 节点是工人
  • 话题是广播喇叭
  • 服务是电话问答
  • 动作是带进度条的任务
  • 参数是旋钮开关

在实际项目中,这五种通信方式经常混合使用。比如,传感器数据用话题传,控制指令用服务发,导航任务用动作执行,系统配置用参数调。

好了,这一章就到这里。下一章我们会深入话题的 QoS 设置,那是很多嵌入式工程师容易忽略的地方。到时候我会分享一个因为 QoS 设置不当导致丢数据的真实案例。

记住:理解这些概念,是 ROS2 系统设计的第一步。别急,慢慢来。