3. ROS2通信机制(一) 话题通信

聊到ROS2,绕不开的就是通信机制。而话题通信,说白了就是ROS2的“血管”——节点之间靠它来输送数据。我刚开始接触ROS2时,第一个实战就是写一个Publisher和Subscriber。那时候觉得,不就是发个消息、收个消息嘛,能有多难?结果一上手,各种坑就来了。今天咱们就把这块彻底捋清楚。

3.1 话题通信的核心逻辑

先看一张图,帮你快速建立整体认知。

ROS2 话题通信核心模型 Publisher 节点 (发布者) 例:传感器节点 Subscriber 节点 (订阅者) 例:控制节点 话题 /chatter 消息类型:std_msgs/String 发布者发布消息到话题,订阅者从话题接收消息 双方通过相同的“话题名”和“消息类型”建立连接 ROS2 话题通信模型示意图

这张图其实就说明白了:发布者往话题里扔数据,订阅者从话题里取数据。双方不需要知道对方是谁,只需要约定好话题名和消息类型就行。这种解耦设计,我在做多传感器融合项目时深有体会——加一个新传感器,完全不影响已有的控制节点。

3.2 准备工作:创建功能包

动手之前,先把环境搭好。我习惯用C++和Python各写一份,方便对比。

# 进入你的工作空间
cd ~/ros2_ws/src

# 创建C++功能包
ros2 pkg create --build-type ament_cmake cpp_topic_demo \
  --dependencies rclcpp std_msgs

# 创建Python功能包
ros2 pkg create --build-type ament_python py_topic_demo \
  --dependencies rclpy std_msgs

这里有个小细节:--dependencies参数会自动帮你把依赖写进package.xml和CMakeLists.txt里。我刚开始学的时候总忘了加,结果编译报错才回头补,浪费时间。

3.3 C++实现:Publisher与Subscriber

3.3.1 编写发布者节点

在cpp_topic_demo/src下新建talker.cpp:

#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");
  auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
  auto message = std_msgs::msg::String();
  rclcpp::WallRate loop_rate(2);  // 2Hz

  while (rclcpp::ok())
  {
    message.data = "Hello ROS2: " + std::to_string(count);
    RCLCPP_INFO(node->get_logger(), "发布: '%s'", message.data.c_str());
    publisher->publish(message);
    rclcpp::spin_some(node);
    loop_rate.sleep();
  }

  rclcpp::shutdown();
  return 0;
}

嗯,这里要注意create_publisher的第二个参数——队列大小。我见过有人设成1,结果消息稍微一多就丢包。设成10或100比较稳妥,具体看你的实时性要求。

3.3.2 编写订阅者节点

在cpp_topic_demo/src下新建listener.cpp:

#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");
  auto subscription = node->create_subscription<std_msgs::msg::String>(
    "chatter", 10, topic_callback);
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

回调函数是订阅者的灵魂。数据一来,它就被触发。我曾经在回调里做复杂计算,结果把整个节点卡死了。记住:回调里别干重活,收到消息后扔到队列里处理才是正解。

3.3.3 修改CMakeLists.txt

add_executable(talker src/talker.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

add_executable(listener src/listener.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

install(TARGETS
  talker
  listener
  DESTINATION lib/${PROJECT_NAME}
)

3.4 Python实现:Publisher与Subscriber

3.4.1 编写发布者节点

在py_topic_demo/py_topic_demo下新建talker.py:

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

class Talker(Node):
    def __init__(self):
        super().__init__('talker')
        self.publisher_ = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(0.5, self.timer_callback)
        self.count = 0

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

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

Python版用了定时器来循环发布,比C++的while循环更优雅。我个人习惯用Python做快速原型验证,C++做最终部署。

3.4.2 编写订阅者节点

在py_topic_demo/py_topic_demo下新建listener.py:

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

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('收到: "%s"' % msg.data)

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

3.4.3 配置setup.py

entry_points={
    'console_scripts': [
        'talker = py_topic_demo.talker:main',
        'listener = py_topic_demo.listener:main',
    ],
},

3.5 编译与运行

回到工作空间根目录:

cd ~/ros2_ws
colcon build
source install/setup.bash

然后开两个终端:

# 终端1:运行发布者
ros2 run cpp_topic_demo talker
# 或者Python版
ros2 run py_topic_demo talker

# 终端2:运行订阅者
ros2 run cpp_topic_demo listener
# 或者Python版
ros2 run py_topic_demo listener

看到消息在来回传递,说明你成功了。你想想看,C++写的发布者可以和Python写的订阅者通信,这就是ROS2的跨语言能力。

3.6 常用调试命令

命令 作用 示例
ros2 topic list 列出所有活跃话题 查看当前有哪些话题
ros2 topic info /chatter 查看话题详情 显示发布者/订阅者数量
ros2 topic echo /chatter 实时打印话题数据 调试时看消息内容
ros2 topic hz /chatter 查看发布频率 检查是否达到预期频率
ros2 topic bw /chatter 查看带宽占用 排查网络瓶颈
小技巧:调试时先用ros2 topic echo确认数据格式对不对,再用ros2 topic hz检查频率。这两个命令能解决80%的通信问题。

3.7 避坑指南

我曾经踩过的坑:
  • 话题名拼写错误:发布者和订阅者的话题名必须完全一致,大小写敏感。我有一回把chatter写成了chater,排查了半小时。
  • 消息类型不匹配:发布者用String,订阅者用Int32,肯定连不上。用ros2 topic info可以快速检查。
  • 忘记source环境:每次新开终端都要source install/setup.bash,否则找不到你的节点。
  • 回调函数阻塞:回调里别做耗时操作,否则会拖垮整个节点。实在需要,就用多线程或异步处理。

3.8 总结

话题通信是ROS2的基石。说白了,就是发布者往话题里写数据,订阅者从话题里读数据。C++性能好,适合底层驱动;Python开发快,适合原型验证。两者可以混用,这是ROS2的跨语言优势。

我个人建议:先跑通Python版,理解流程后再看C++版。遇到问题多用ros2 topic命令调试,别闷头改代码。嗯,今天就到这里,动手试试吧。

可可精品资料库