第1章:ROS2基础通信实战——Publisher与Subscriber编程

大家好,我是你们这门课的主讲人。今天咱们来聊聊ROS2里最基础、也最核心的东西——通信。

说实话,我刚开始接触ROS2的时候,觉得通信嘛,不就是发发消息、收收消息?后来在项目里踩了不少坑,才明白这里面的门道有多深。这一章,我会把Publisher和Subscriber的编程、自定义消息、同步异步通信、还有QoS配置,掰开了揉碎了讲给你听。

核心要点:ROS2的通信机制,说白了就是节点之间怎么“说话”。Publisher负责“说”,Subscriber负责“听”。但怎么说得清楚、听得明白,还得靠消息接口和QoS策略。

1.1 Publisher与Subscriber:一对“说”与“听”的搭档

先看一个最简单的例子。我写一个发布节点,每隔1秒发一个“Hello, ROS2”字符串。再写一个订阅节点,收到消息就打印出来。

你想想看,这就像两个人打电话。Publisher是说话的人,Subscriber是接电话的人。但ROS2里有个好处——说话的人不需要知道谁在听,听的人也不需要知道谁在说。这叫“解耦”。

// 发布者代码示例(C++)
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

class SimplePublisher : public rclcpp::Node
{
public:
    SimplePublisher() : Node("simple_publisher")
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("topic_hello", 10);
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&SimplePublisher::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Hello, ROS2!";
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
        publisher_->publish(message);
    }
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
};

嗯,这里要注意:create_publisher的第二个参数“10”,是队列深度。什么意思?就是如果订阅者处理不过来,最多缓存10条消息。这个数字后面讲QoS时会细说。

我的小习惯:写发布者时,我习惯把定时器周期和队列深度都定义成常量,方便后期调参。别问我为什么——有一次项目里要改频率,满代码找数字,找得我头大。

1.2 自定义消息接口:别老用String

实际项目中,你不可能只发个字符串。比如自动驾驶里,你要发“车辆位置”、“障碍物列表”、“控制指令”……这些都得用自定义消息。

ROS2里自定义消息很简单。在包目录下建一个.msg文件,写上字段就行。

# 文件:msg/VehicleState.msg
float32 x
float32 y
float32 yaw
float32 velocity
string vehicle_id
builtin_interfaces/Time timestamp

然后在CMakeLists.txt里加上:

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/VehicleState.msg"
)

编译后,ROS2会自动生成对应的C++和Python代码。你就可以像用标准消息一样用自定义消息了。

我曾经踩过的坑:自定义消息的字段类型,一定要用ROS2内置的类型,比如float32、int32,别用C++的float、int。还有,时间戳最好用builtin_interfaces/Time,别自己拼字符串——否则后面做数据同步时,你会哭的。

1.3 同步与异步通信:什么时候该等?

ROS2的通信,本质上是异步的。发布者发完消息就干别的事去了,订阅者什么时候收到、什么时候处理,都不影响发布者。

但有些场景,你需要“同步”。比如两个传感器数据必须同时到达才能融合。这时候怎么办?

ROS2提供了TimeSynchronizer和ApproximateTime策略。说白了,就是让订阅者“等一等”,直到所有消息都到齐了,再一起处理。

// 同步订阅示例
message_filters::Subscriber<sensor_msgs::msg::Image> sub_image(this, "camera/image");
message_filters::Subscriber<sensor_msgs::msg::PointCloud2> sub_lidar(this, "lidar/points");

typedef message_filters::sync_policies::ApproximateTime<
    sensor_msgs::msg::Image, sensor_msgs::msg::PointCloud2> MySyncPolicy;

message_filters::Synchronizer<MySyncPolicy> sync(MySyncPolicy(10), sub_image, sub_lidar);
sync.registerCallback(&MyNode::callback, this);

我个人建议:能用异步就别用同步。同步会引入延迟,而且如果某个话题一直不来,整个系统就卡住了。只有真正需要时间对齐的场景,才用同步。

1.4 QoS配置与调优:别让消息丢了

QoS(Quality of Service)是ROS2里最容易被忽视、但最重要的东西。它决定了消息的可靠性、持久性、生命周期等。

我见过太多人,直接用默认QoS,结果消息丢了、延迟高了、系统崩了……

ROS2的QoS策略主要有这几个维度:

策略 选项 说明
Reliability RELIABLE / BEST_EFFORT 可靠传输 vs 尽力而为。RELIABLE保证消息必达,但延迟高;BEST_EFFORT反之。
Durability TRANSIENT_LOCAL / VOLATILE 是否保留历史消息给后来的订阅者。TRANSIENT_LOCAL会缓存最后一条。
Depth 整数(如10、100) 队列深度。消息太多时,旧的会被丢弃。
Lifespan 时间(如1s) 消息的有效期。超时未处理的消息会被丢弃。

举个例子。激光雷达数据,我一般用BEST_EFFORT + Depth=1。为什么?因为激光雷达频率高(10Hz以上),丢几帧无所谓,但延迟必须低。而控制指令,我肯定用RELIABLE + Depth=10,一条都不能丢。

调优口诀:高频传感器用BEST_EFFORT,低频控制用RELIABLE。历史数据用TRANSIENT_LOCAL,实时数据用VOLATILE。

1.5 本章知识体系

下面这张图,是我自己画的ROS2通信知识体系。你看一遍,心里就有谱了。

ROS2基础通信知识体系 ROS2通信 Publisher/Subscriber 自定义消息接口 同步/异步通信 QoS配置与调优 创建发布者 创建订阅者 定义.msg文件 编译生成代码 TimeSynchronizer ApproximateTime Reliability策略 Durability策略 Depth/Lifespan 掌握这些,ROS2通信你就入门了

这张图把本章的核心内容串起来了。你看,从中心出发,四个分支:发布订阅、自定义消息、同步异步、QoS。每个分支下面还有具体的知识点。我建议你把这图存下来,后面写代码时对照着看。

我的经验:学ROS2通信,别急着写代码。先把QoS搞明白,把消息接口设计好。我见过太多人,代码写了一大堆,最后发现QoS不匹配,消息收不到,从头改……那叫一个痛苦。

好了,这一章就到这里。记住:通信是ROS2的骨架,骨架搭好了,后面的感知、规划、控制才能跑得稳。下一章,我们会聊更高级的通信模式——服务与动作。但先把今天的内容消化掉,动手写几个Publisher和Subscriber试试。

可可精品资料库