4、编写延迟测试节点:自定义话题发布与订阅,记录时间戳

好,咱们进入实战环节。

前面几章我们聊了延迟的成因和理论模型。说实话,理论再漂亮,不如自己动手测一把来得实在。我当年刚接触ROS2时,就被一个诡异的延迟问题折磨了两天。后来发现,其实就是测试节点的时间戳记录方式不对。

这一节,我们就来亲手写一个延迟测试节点。核心思路很简单:发布端打上时间戳,订阅端收到后算差值。但这里面的坑,可不少。

4.1 为什么需要自定义话题?

你可能会问:ROS2自带的话题类型那么多,直接用不行吗?

嗯,这里要注意。标准话题类型(比如std_msgs/msg/String)虽然能用,但缺少一个关键字段——时间戳。虽然ROS2的消息头里自带stamp字段,但很多开发者并不用它来记录发布时刻,而是记录采集时刻。这两者是有区别的。

我个人习惯的做法是:自定义一个专门用于延迟测试的消息类型。里面只放两个东西:一个序列号,一个时间戳。干净利落,没有干扰。

我的经验: 测试消息越简单越好。我曾经在一个复杂消息里测延迟,结果发现序列化/反序列化本身占了大头。后来拆成简单消息,问题才暴露出来。

4.2 定义自定义消息

首先,在包目录下创建msg文件夹,新建一个DelayTest.msg文件:

# DelayTest.msg
# 用于测试端到端延迟的消息

uint32 seq          # 序列号,用于追踪丢包
builtin_interfaces/Time timestamp  # 发布时刻的时间戳

然后在CMakeLists.txt里注册这个消息类型:

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/DelayTest.msg"
  DEPENDENCIES builtin_interfaces
)

别忘了在package.xml里添加依赖:

<build_depend>builtin_interfaces</build_depend>
<exec_depend>builtin_interfaces</exec_depend>

4.3 编写发布端节点

发布端的工作很简单:每次发布前,记录当前时间。但这里有个细节——用now()还是用get_clock()->now()?

我建议用后者。为什么?因为get_clock()->now()会考虑ROS2的时间系统(比如模拟时间),而now()直接取系统时间。在仿真测试中,这个区别很重要。

// delay_publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/msg/delay_test.hpp"

using namespace std::chrono_literals;

class DelayPublisher : public rclcpp::Node
{
public:
  DelayPublisher() : Node("delay_publisher"), seq_(0)
  {
    publisher_ = this->create_publisher<your_package::msg::DelayTest>("delay_topic", 10);
    timer_ = this->create_wall_timer(100ms, std::bind(&DelayPublisher::timer_callback, this));
  }

private:
  void timer_callback()
  {
    auto msg = your_package::msg::DelayTest();
    msg.seq = seq_++;
    msg.timestamp = this->get_clock()->now();  // 关键:记录发布时刻

    publisher_->publish(msg);
    RCLCPP_INFO(this->get_logger(), "Published seq: %d", msg.seq);
  }

  rclcpp::Publisher<your_package::msg::DelayTest>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
  uint32_t seq_;
};

int main(int argc, char *argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<DelayPublisher>());
  rclcpp::shutdown();
  return 0;
}
关键点: 时间戳必须在publish()之前记录。如果你在发布之后才记录,那时间戳就晚了。别笑,我真见过有人这么写。

4.4 编写订阅端节点

订阅端收到消息后,立刻记录当前时间,然后计算差值。这个差值就是端到端延迟。

// delay_subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/msg/delay_test.hpp"

class DelaySubscriber : public rclcpp::Node
{
public:
  DelaySubscriber() : Node("delay_subscriber")
  {
    subscription_ = this->create_subscription<your_package::msg::DelayTest>(
      "delay_topic", 10, std::bind(&DelaySubscriber::topic_callback, this, std::placeholders::_1));
  }

private:
  void topic_callback(const your_package::msg::DelayTest::SharedPtr msg)
  {
    auto now = this->get_clock()->now();
    rclcpp::Duration latency = now - msg->timestamp;

    RCLCPP_INFO(this->get_logger(), 
      "Received seq: %d, latency: %.3f ms",
      msg->seq, latency.seconds() * 1000.0);
  }

  rclcpp::Subscription<your_package::msg::DelayTest>::SharedPtr subscription_;
};

int main(int argc, char *argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<DelaySubscriber>());
  rclcpp::shutdown();
  return 0;
}

4.5 时间戳对齐:一个容易被忽略的问题

你以为这样就完了?不,还有个坑。

发布端和订阅端如果不在同一台机器上,它们的系统时间可能不一致。你算出来的延迟,其实是时间差 + 真实延迟。如果两台机器的时间差是100ms,那你的测试结果就完全没意义了。

我曾经在分布式系统上做测试,结果延迟数据忽大忽小。排查了半天,才发现是NTP服务没同步好。

避坑指南:
  • 如果发布端和订阅端在同一进程或同一机器,直接用系统时间没问题。
  • 如果跨机器,务必先同步NTP,或者使用硬件时间戳(如PTP)。
  • 最简单的验证方法:把发布端和订阅端放在同一台机器上跑一次,看看基线延迟是多少。

4.6 运行测试

编译后,分别启动两个节点:

# 终端1
ros2 run your_package delay_publisher

# 终端2
ros2 run your_package delay_subscriber

你会看到类似这样的输出:

[INFO] [delay_subscriber]: Received seq: 1, latency: 0.523 ms
[INFO] [delay_subscriber]: Received seq: 2, latency: 0.498 ms
[INFO] [delay_subscriber]: Received seq: 3, latency: 0.511 ms

这个延迟值包含了:序列化时间 + 传输时间 + 反序列化时间 + 回调执行时间。如果你发现延迟突然变大,那就要排查了——是不是回调里做了耗时操作?是不是网络拥堵?

4.7 知识体系总览

下面这张图,帮你理清整个测试节点的逻辑:

延迟测试节点核心流程 发布端 (Publisher) 1. 创建自定义消息 2. 记录时间戳 (now) 3. 填充序列号 4. 发布消息 get_clock()->now() 话题 delay_topic 订阅端 1. 接收消息 2. 记录当前时间 3. 计算差值 4. 输出延迟 延迟 = now - timestamp 关键:时间戳必须在 publish() 之前记录 跨机器测试时,务必先同步 NTP

4.8 小结

这一节我们亲手搭建了一个延迟测试节点。说白了,就是发布端打时间戳,订阅端算差值。但别小看这个简单逻辑——它是我排查ROS2延迟问题的第一把刀。

你想想看,有了这个工具,你就能量化你的系统延迟。是1ms还是10ms?是稳定还是抖动?数据说话,比猜靠谱多了。

嗯,下一节我们会在这个基础上,加入统计功能,看看延迟的分布情况。不过那是后话了。

我的小建议: 把这个测试节点做成一个独立工具包,以后每个项目都能复用。我自己的工具包里,这个节点已经迭代了十几个版本了。

可可精品资料库