4、ROS2节点编程基础:节点概念、rclcpp库入门、编写第一个发布者节点、编写第一个订阅者节点、使用rclpy(Python版)

好,咱们进入正题。这一章是ROS2编程的起点,说白了就是学会怎么让机器人“说话”和“听话”。

我刚开始学ROS的时候,最困惑的就是“节点”到底是个啥玩意儿。后来在项目里调试一个机械臂,折腾了一整天才明白——节点其实就是一个个独立的小程序,各干各的活,通过话题互相传数据。嗯,理解了这一点,后面就顺了。

4.1 节点概念:ROS2里的“小工人”

ROS2的节点,你可以把它想象成一个工厂里的工人。每个工人只负责一件事:

  • 有的工人专门负责拿传感器数据(比如激光雷达节点)
  • 有的工人专门负责算路径(比如导航节点)
  • 有的工人专门负责控制电机(比如底盘驱动节点)

这些工人之间怎么沟通?通过“话题”(Topic)。工人A把数据发布到话题上,工人B订阅这个话题就能拿到数据。这就是发布-订阅模式。

核心要点:一个ROS2系统就是由多个节点组成的分布式网络。每个节点都是独立的可执行文件,节点之间通过话题、服务、动作等机制通信。

我个人习惯把节点设计得尽量“小”——一个节点只做一件事,做好一件事。这样调试起来特别方便,哪个节点出问题,直接换掉就行,不影响其他部分。

4.2 rclcpp库入门:C++版本的ROS2客户端库

rclcpp,全称是ROS Client Library for C++。说白了,它就是ROS2官方给C++开发者准备的“工具箱”。

你想想看,如果没有这个库,你要自己处理节点创建、话题通信、参数管理这些底层细节,那得多痛苦?rclcpp把这些都封装好了,我们直接调用API就行。

先看一个最简单的rclcpp程序框架:

#include <rclcpp/rclcpp.hpp>

int main(int argc, char **argv)
{
  // 初始化ROS2
  rclcpp::init(argc, argv);
  
  // 创建一个节点,名字叫"my_first_node"
  auto node = std::make_shared<rclcpp::Node>("my_first_node");
  
  // 打印一条日志
  RCLCPP_INFO(node->get_logger(), "节点已启动!");
  
  // 让节点保持运行
  rclcpp::spin(node);
  
  // 关闭ROS2
  rclcpp::shutdown();
  return 0;
}

这段代码里,rclcpp::init是必须的第一步,它负责和ROS2底层通信。然后创建节点,rclcpp::spin让节点一直活着,等待接收消息或执行回调。最后rclcpp::shutdown做清理工作。

小技巧:我在项目中经常用RCLCPP_INFO来打印调试信息。它比std::cout好使,因为会自动带上时间戳和节点名,排查问题一目了然。

4.3 编写第一个发布者节点

发布者节点,就是那个“说话”的节点。它定期往话题上发数据。

我记得第一次写发布者节点时,犯了个低级错误——忘记设置发布频率,结果数据像机关枪一样往外蹦,把订阅者节点直接搞崩了。后来学乖了,一定要用rclcpp::Rate来控制节奏。

来看一个完整的发布者节点示例,它每隔1秒发布一个“Hello ROS2”字符串:

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  
  // 创建节点
  auto node = std::make_shared<rclcpp::Node>("talker");
  
  // 创建发布者,话题名是"chatter",队列大小10
  auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
  
  // 创建消息对象
  std_msgs::msg::String message;
  message.data = "Hello ROS2!";
  
  // 设置循环频率为1Hz(每秒1次)
  rclcpp::Rate rate(1);
  
  while (rclcpp::ok())
  {
    // 发布消息
    publisher->publish(message);
    RCLCPP_INFO(node->get_logger(), "发布了: '%s'", message.data.c_str());
    
    // 休眠,保持频率
    rate.sleep();
  }
  
  rclcpp::shutdown();
  return 0;
}

这里有几个关键点:

  • create_publisher的第一个参数是消息类型,第二个参数是队列大小。队列大小决定了如果订阅者处理不过来,最多缓存多少条消息。
  • rclcpp::ok()检查ROS2是否还在正常运行,按Ctrl+C会返回false,从而退出循环。
  • rate.sleep()控制发布频率,1Hz就是每秒一次。

避坑指南:我曾经在队列大小这里吃过亏。设成1的话,如果订阅者处理慢,消息就会丢。设成1000又太占内存。一般10-100是比较合理的范围,具体看你的消息大小和处理速度。

4.4 编写第一个订阅者节点

有说的,就得有听的。订阅者节点就是那个“听话”的节点。

订阅者的工作模式是“回调驱动”——它注册一个回调函数,一旦话题上有新消息,ROS2就会自动调用这个函数。你不用写循环,不用轮询,省心得很。

来看订阅者节点的代码:

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

// 回调函数:当收到消息时被调用
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
  RCLCPP_INFO(rclcpp::get_logger("listener"), "收到了: '%s'", msg->data.c_str());
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  
  // 创建节点
  auto node = std::make_shared<rclcpp::Node>("listener");
  
  // 创建订阅者,订阅"chatter"话题
  auto subscription = node->create_subscription<std_msgs::msg::String>(
    "chatter", 10, topic_callback);
  
  // 进入事件循环,等待回调
  rclcpp::spin(node);
  
  rclcpp::shutdown();
  return 0;
}

看到没?订阅者比发布者还简单。核心就三步:

  1. 定义回调函数
  2. 创建订阅者,绑定话题和回调
  3. 调用rclcpp::spin进入事件循环

你可能会问:为什么发布者用while循环,订阅者用spin

因为发布者是主动的,它要控制什么时候发、发多少。订阅者是被动的,它只需要等着,有消息来了就处理。所以订阅者用spin最合适,它会一直阻塞,直到收到消息才执行回调。

4.5 使用rclpy(Python版)

如果你更喜欢Python,ROS2也提供了rclpy库。说实话,我平时做原型验证时更喜欢用Python,写起来快,调试也方便。但做正式产品时,我会用C++,性能更好。

先看Python版的发布者:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class TalkerNode(Node):
    def __init__(self):
        super().__init__('talker')
        self.publisher_ = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)  # 每秒触发一次
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello ROS2: %d' % self.i
        self.publisher_.publish(msg)
        self.get_logger().info('发布了: "%s"' % msg.data)
        self.i += 1

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

if __name__ == '__main__':
    main()

Python版和C++版的核心逻辑一模一样,只是语法不同。注意Python版用了create_timer来替代rclcpp::Rate,这是rclpy的特色——用定时器来触发周期性任务。

再看Python版的订阅者:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class ListenerNode(Node):
    def __init__(self):
        super().__init__('listener')
        self.subscription = self.create_subscription(
            String,
            'chatter',
            self.listener_callback,
            10)
        self.subscription  # 防止被垃圾回收

    def listener_callback(self, msg):
        self.get_logger().info('收到了: "%s"' % msg.data)

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

if __name__ == '__main__':
    main()

Python版的订阅者同样简洁。有个小细节:self.subscription这一行看起来多余,但如果不赋值给成员变量,Python的垃圾回收可能会把订阅者对象给回收掉,导致收不到消息。嗯,这个坑我踩过。

4.6 编译与运行

代码写好了,怎么跑起来?

对于C++版本,你需要用colcon build编译。在ROS2工作空间的src目录下创建包,把代码放进去,然后:

cd ~/ros2_ws
colcon build --packages-select 你的包名
source install/setup.bash
ros2 run 你的包名 talker
ros2 run 你的包名 listener

对于Python版本,不需要编译,直接运行:

ros2 run 你的包名 talker
ros2 run 你的包名 listener

打开两个终端,一个跑发布者,一个跑订阅者,你会看到订阅者终端里不断打印出收到的消息。这就是ROS2节点通信的基本流程。

调试小工具:ros2 topic list可以查看当前所有活跃的话题。用ros2 topic echo /chatter可以直接在终端里监听话题数据,不用写代码就能验证发布者是否正常工作。

好了,这一章的内容就到这里。节点编程是ROS2的基石,后面的所有高级功能——服务、动作、参数、生命周期——都建立在这个基础之上。把发布者和订阅者练熟了,后面的路就好走了。