2、ROS2核心概念详解:节点、话题、服务、动作与参数

好,咱们直接进入正题。ROS2 里最核心的五个概念,说白了就是:节点、话题、服务、动作、参数。这五个东西你搞明白了,整个 ROS2 的骨架你就搭起来了。我刚开始学的时候,也绕了不少弯路,今天我把这些坑都给你指出来。

2.1 节点(Node)的创建与运行

节点是什么?你可以把它想象成一个独立的小程序,专门干一件事。比如一个节点负责读激光雷达数据,另一个节点负责做路径规划。它们之间互相通信,但各自独立运行。

创建一个节点,在 Python 里其实很简单。我习惯用面向对象的方式写,这样代码好维护。

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()

if __name__ == '__main__':
    main()

嗯,这里要注意一点:rclpy.spin() 这个函数会让节点一直运行,直到你按 Ctrl+C 停止。我在项目中遇到过有人忘了写这行,结果节点启动后立刻退出了,查了半天才发现。

小技巧:节点名字在整个 ROS2 系统里必须是唯一的。如果你不小心起了重名,后启动的节点会覆盖前面的。我建议在节点名后面加个后缀,比如 my_node_v1,这样好区分。

2.2 话题(Topic)的发布与订阅

话题是 ROS2 里最常用的通信方式。它就像是一个广播频道:发布者往频道里发消息,订阅者接收消息。一个话题可以有多个发布者和多个订阅者,非常灵活。

举个例子,你要发布一个字符串消息:

class Talker(Node):
    def __init__(self):
        super().__init__('talker')
        self.publisher = self.create_publisher(String, 'chatter', 10)
        timer_period = 0.5  # 每秒发两次
        self.timer = self.create_timer(timer_period, self.timer_callback)

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello, ROS2!'
        self.publisher.publish(msg)
        self.get_logger().info(f'发布了: {msg.data}')

订阅端怎么写呢?

class Listener(Node):
    def __init__(self):
        super().__init__('listener')
        self.subscription = self.create_subscription(
            String, 'chatter', self.listener_callback, 10)

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

你想想看,为什么要有队列大小(上面代码里的 10)?说白了就是防止消息堆积。如果订阅者处理得慢,队列满了,旧消息就会被丢掉。我在项目中遇到过传感器数据延迟的问题,后来发现就是队列设得太小了。

避坑指南:我曾经把话题名字写错了,发布者和订阅者用的不是同一个名字,结果数据死活传不过去。检查了半小时才发现。所以,话题名字必须完全一致,大小写和标点都不能错。

2.3 服务(Service)的客户端与服务器

话题是单向的,服务是双向的。服务就像打电话:客户端发起请求,服务器处理并返回响应。适合那些需要即时反馈的场景,比如让机器人抓取一个物体,你得知道它抓没抓到。

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

int64 a
int64 b
---
int64 sum

服务器端代码:

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

    def add_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info(f'收到请求: {request.a} + {request.b} = {response.sum}')
        return response

客户端代码:

class AddClient(Node):
    def __init__(self):
        super().__init__('add_client')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待服务上线...')
        self.req = AddTwoInts.Request()

    def send_request(self, a, b):
        self.req.a = a
        self.req.b = b
        self.future = self.cli.call_async(self.req)

我个人习惯在客户端加一个等待服务的循环,不然服务还没启动,客户端就发请求,会报错。这个坑我踩过好几次。

2.4 动作(Action)的客户端与服务器

动作是服务的升级版。服务只能返回最终结果,动作可以实时反馈进度。比如让机器人走到某个位置,你可以实时知道它走到哪了。

动作有三个关键部分:目标(goal)、反馈(feedback)、结果(result)。

服务器端示例:

class MoveRobotServer(Node):
    def __init__(self):
        super().__init__('move_robot_server')
        self.action_server = ActionServer(
            self, MoveRobot, 'move_robot', self.execute_callback)

    async def execute_callback(self, goal_handle):
        feedback_msg = MoveRobot.Feedback()
        for i in range(1, 11):
            feedback_msg.progress = i * 10
            goal_handle.publish_feedback(feedback_msg)
            time.sleep(1)
        goal_handle.succeed()
        result = MoveRobot.Result()
        result.success = True
        return result

客户端代码:

class MoveRobotClient(Node):
    def __init__(self):
        super().__init__('move_robot_client')
        self.action_client = ActionClient(self, MoveRobot, 'move_robot')

    def send_goal(self, target_x, target_y):
        goal_msg = MoveRobot.Goal()
        goal_msg.target_x = target_x
        goal_msg.target_y = target_y
        self.action_client.send_goal_async(goal_msg)

动作的反馈机制特别实用。我记得有一次调试机器人导航,就是靠动作反馈发现某个路径点一直达不到,才找到算法里的 bug。

核心要点:动作适合长时间任务,服务适合短时间查询,话题适合持续数据流。选哪个,看你的场景。

2.5 参数(Parameter)的声明与使用

参数就是节点的配置项。比如机器人的最大速度、传感器的采样频率,这些都可以做成参数,方便运行时调整。

声明参数:

class ParamNode(Node):
    def __init__(self):
        super().__init__('param_node')
        self.declare_parameter('max_speed', 1.0)
        self.declare_parameter('robot_name', 'turtlebot')
        self.declare_parameter('enable_debug', False)

读取参数:

max_speed = self.get_parameter('max_speed').value
robot_name = self.get_parameter('robot_name').value
enable_debug = self.get_parameter('enable_debug').value

动态修改参数:

self.set_parameters([Parameter('max_speed', Parameter.Type.DOUBLE, 2.0)])

你想想看,参数的好处是什么?不用改代码就能调配置。我曾经为了调一个 PID 参数,每次改完都要重新编译,后来改成参数后,直接在命令行里改,效率高多了。

建议:所有可能变化的数值,都做成参数。比如阈值、超时时间、路径名称。这样你的节点就灵活多了。

好了,这五个核心概念就讲完了。节点是基础,话题、服务、动作是通信方式,参数是配置手段。把它们组合起来,你就能搭建出复杂的机器人系统。下一章我们讲如何用 launch 文件把这些节点组织起来,到时候见。