ROS软时间同步机制message_filters总结与测试

本文介绍ROS中的时间同步机制,包括硬同步和软同步,并详细解释了三种同步方式:普通时间同步机制、时间戳完全相同机制及时间戳近似机制。通过实例展示了不同同步方式的效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

时间同步机制对于多机器人(主控)系统或多传感器信息融合非常重要,划分为硬同步和软同步。

  • 硬同步从源头上同步时间,杜绝了传输延迟影响。
  • 软同步时间由话题发布时刻决定,期间存在传感器到主控话题订阅节点的传输延迟在里面,这个延迟有时候会比较严重,此时如果通过软同步无法解决延迟问题,就需要采用硬同步方案。
  • 注意区别采集时刻时间和到达时刻时间:在ROS中,我们假定发布时间即为采集时刻时间,订阅时间即为到达时刻时间,因此,时间戳近似机制(采用只基于时间戳信息的自适应算法来匹配多个话题之间的时间同步性)能够适用于时间传输网络或数据处理导致的延迟场景。

ROS提供了3种同步机制,其实本质上是2种:普通时间同步机制,时间戳完全相同机制,时间戳近似机制。前2种本质是一样的。在message_filters包中实现。

其实,在本篇学习了ROS时间同步机制的同时,也会学习到定时器和C++封装ROS知识。

目录

测试源码

运行结果

测试1:2个定时器频率,timer1=0.1s, timer2=0.5s

测试2:2个定时器频率,timer1=0.5s, timer2=0.5s

测试3:时间同步器订阅相同话题


测试源码

将ROS封装成C++形式了,与C有一些不同地方,尤其需要注意

#ifndef TIME_SYNCHRONIZER_H_
#define TIME_SYNCHRONIZER_H_

#include <ros/ros.h>
#include <ros/timer.h>
#include <geometry_msgs/PointStamped.h>
#include <message_filters/subscriber.h>

/* 以下2个头文件有何区别? */
#include <message_filters/time_synchronizer.h>
// #include <message_filters/synchronizer.h>

#include <message_filters/sync_policies/approximate_time.h>
#include <iostream>

using std::cout;
using std::endl;

// #define TIME_SYNCH
#define EXACT_SYNCH
// #define APPRO_SYNCH

class TimeSynch {
  public:
    TimeSynch(ros::NodeHandle &node) : node_(node) {};

  public:
    ros::Publisher timer1_pub, timer2_pub;
    ros::Timer timer1;
    ros::Timer timer2;
    // message_filters::Subscriber timer1_sub, timer2_sub;
    // 先在类内定义
    message_filters::Subscriber<geometry_msgs::PointStamped> timer1_sub;
    
  public:
    bool Initialization();
    void Timer1Handler(const ros::TimerEvent&);
    void Timer2Handler(const ros::TimerEvent&);
    void TimeSynchHandler(const geometry_msgs::PointStampedConstPtr &p1, const geometry_msgs::PointStampedConstPtr &p2);

  private:  
    ros::NodeHandle node_;
    std::string topic_name1_, topic_name2;
};

bool TimeSynch::Initialization() {
  /* 2个定时器,以不同频率发布数据 */
  timer1 = node_.createTimer(ros::Duration(1), &TimeSynch::Timer1Handler, this);
  timer2 = node_.createTimer(ros::Duration(1), &TimeSynch::Timer2Handler, this);
  /* 2个订阅器,分别订阅上述2个定时器发布的话题 */
  timer1_pub = node_.advertise<geometry_msgs::PointStamped>("p1", 10);
  timer2_pub = node_.advertise<geometry_msgs::PointStamped>("p2", 10);

  // timer1_sub = node_.subscribe("p1", 10); // wrong!
  // 再在构造函数或初始化函数中进行初始化
  timer1_sub.subscribe(node_, "p1", 10);
  // 直接在构造函数或初始化函数中定义并初始化
  message_filters::Subscriber<geometry_msgs::PointStamped> timer2_sub(node_, "p2", 10);

/* 以下分别定义了3种类型时间同步器,测试其效果,使用同一个回调函数 */
/* 1、普通时间同步器 */
#ifdef TIME_SYNCH
  message_filters::TimeSynchronizer<geometry_msgs::PointStamped, geometry_msgs::PointStamped> time_synch(timer1_sub, timer2_sub, 10);
  // time_synch.registerCallback(boost::bind(&TimeSynch::TimeSynchHandler, _1, _2)); // 错误!
  time_synch.registerCallback(&TimeSynch::TimeSynchHandler, this);
#endif
/*2、时间同步器:要求时间戳完全相同 */
#ifdef EXACT_SYNCH
  using ExactSynch = message_filters::sync_policies::ExactTime<geometry_msgs::PointStamped, geometry_msgs::PointStamped>;
  message_filters::Synchronizer<ExactSynch> exact_synch(ExactSynch(10), timer1_sub, timer2_sub);
  exact_synch.registerCallback(&TimeSynch::TimeSynchHandler, this);
#endif
/*3、时间同步器:只要求时间戳近似即可 */
/* 默认近似范围多大?近似范围是否支持自定义?如何定义?
 */
#ifdef APPRO_SYNCH
  typedef message_filters::sync_policies::ApproximateTime<geometry_msgs::PointStamped, geometry_msgs::PointStamped> ApproSynch;
  message_filters::Synchronizer<ApproSynch> appro_synch(ApproSynch(10), timer1_sub, timer2_sub);
  appro_synch.registerCallback(&TimeSynch::TimeSynchHandler, this);
#endif
  // 注意!由于作用域原因,必须放在订阅函数之后(不能放在主函数中,否则无法订阅到话题)!
  ros::spin();

  cout << "Initialized." << endl;
  return true;
}

void TimeSynch::Timer1Handler(const ros::TimerEvent&) {
  cout << "timer1=" << ros::Time::now() << endl;
  geometry_msgs::PointStamped p1;
  p1.header.stamp = ros::Time::now();
  timer1_pub.publish(p1);
}

void TimeSynch::Timer2Handler(const ros::TimerEvent&) {
  cout << "timer2=" << ros::Time::now() << endl;
  geometry_msgs::PointStamped p2;
  p2.header.stamp = ros::Time::now();
  timer2_pub.publish(p2);  
}

void TimeSynch::TimeSynchHandler(const geometry_msgs::PointStampedConstPtr &p1, const geometry_msgs::PointStampedConstPtr &p2) {
  cout << "p1 time = " << p1->header.stamp << "  p2 time = " << p2->header.stamp << endl;
}


int main(int argc, char **argv) {
    ros::init(argc, argv, "time_synch");
    ros::NodeHandle node;
    TimeSynch timeSynch(node);
    timeSynch.Initialization();

    // ros::spin(); // 不能放这儿!

    return true;
}

#endif

 

运行结果

测试1:2个定时器频率,timer1=0.1s, timer2=0.5s

从左到右:时间戳近似同步机制,时间戳完全相同机制,普通时间同步机制。

只有时间戳近似定时器能够订阅到2个时间戳大致相近的话题。其他2个机制只有在2个话题时间戳完全相同时才能订阅到(以下没有出现完全相同的时候,因此没有订阅到)

 

测试2:2个定时器频率,timer1=1s, timer2=1s

我们将2个定时器发布频率设定相同都为1秒,由于两个发布器发布时间不可能完全相同,会存在一点点时间偏差,导致时间戳完全相同机制和普通时间同步机制都无法订阅到2个话题

测试3:时间同步器订阅相同话题

之所以这样做,是为了保证“2”个话题时间戳完全相同,测试时间戳完全相同机制。

此时,时间戳完全相同机制和普通时间同步机制都可以订阅到话题进入回调函数了。话题时间戳完全相同。

 

 

 

### ROS2 中 `message_filters` 的时间同步ROS2 中,虽然官方尚未完全支持像 ROS1 那样的 `message_filters` 功能模块[^4],但可以通过自定义实现来完成类似的时间同步功能。以下是基于现有工具和技术的解决方案。 #### 实现方法概述 为了实现在 ROS2 中类似于 `message_filters::sync_policies::ApproximateTime` 的功能,可以采用以下方式: - 使用 Python 或 C++ 编写节点程序。 - 手动管理消息队列并比较时间戳以匹配来自多个话题的消息。 - 借助第三方库(如 `rosbag2` 提供的相关接口)辅助开发。 --- #### 示例代码:Python 版本 下面是一个简单的 Python 节点示例,展示如何通过手动对比时间戳的方式,在 ROS2 中实现近似时间同步的功能。 ```python import rclpy from rclpy.node import Node from sensor_msgs.msg import Image, CameraInfo from std_msgs.msg import Header class TimeSynchronizer(Node): def __init__(self): super().__init__('time_synchronizer') self.image_sub_ = self.create_subscription( Image, '/camera/image_raw', self.image_callback, 10) self.info_sub_ = self.create_subscription( CameraInfo, '/camera/camera_info', self.info_callback, 10) # 存储未配对的消息 self.image_buffer = [] self.info_buffer = [] def image_callback(self, msg: Image): """存储图像消息""" self.image_buffer.append(msg) self.match_and_process() def info_callback(self, msg: CameraInfo): """存储相机参数消息""" self.info_buffer.append(msg) self.match_and_process() def match_and_process(self): """尝试匹配最近的一组消息""" while self.image_buffer and self.info_buffer: img_time = self.image_buffer[0].header.stamp.sec + \ self.image_buffer[0].header.stamp.nanosec * 1e-9 info_time = self.info_buffer[0].header.stamp.sec + \ self.info_buffer[0].header.stamp.nanosec * 1e-9 time_diff = abs(img_time - info_time) if time_diff < 0.1: # 定义可接受的最大时间差阈值 self.process_messages(self.image_buffer.pop(0), self.info_buffer.pop(0)) elif img_time < info_time: break else: self.info_buffer.pop(0) def process_messages(self, image_msg, camera_info_msg): """处理已匹配的消息""" self.get_logger().info(f'Processed synchronized messages with timestamps {image_msg.header.stamp} & {camera_info_msg.header.stamp}') def main(args=None): rclpy.init(args=args) node = TimeSynchronizer() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main() ``` 上述代码实现了基本的时间同步逻辑,并允许用户调整最大时间偏差阈值以适应具体需求[^5]。 --- #### 示例代码:C++ 版本 对于更高效的性能要求场景,推荐使用 C++ 来编写类似的同步机制。下面是对应的 C++ 示例代码片段: ```cpp #include "rclcpp/rclcpp.hpp" #include "sensor_msgs/msg/image.hpp" #include "sensor_msgs/msg/camera_info.hpp" using namespace std::placeholders; class TimeSynchronizerNode : public rclcpp::Node { public: TimeSynchronizerNode() : Node("time_synchronizer") { image_sub_ = this->create_subscription<sensor_msgs::msg::Image>( "/camera/image_raw", 10, bind(&TimeSynchronizerNode::imageCallback, this, _1)); info_sub_ = this->create_subscription<sensor_msgs::msg::CameraInfo>( "/camera/camera_info", 10, bind(&TimeSynchronizerNode::infoCallback, this, _1)); } private: void imageCallback(const sensor_msgs::msg::Image::SharedPtr msg) { image_buffer_.push_back(*msg); matchAndProcess(); } void infoCallback(const sensor_msgs::msg::CameraInfo::SharedPtr msg) { info_buffer_.push_back(*msg); matchAndProcess(); } void matchAndProcess() { while (!image_buffer_.empty() && !info_buffer_.empty()) { auto img_time = image_buffer_[0].header.stamp; auto info_time = info_buffer_[0].header.stamp; double diff = fabs((img_time.sec - info_time.sec) + (img_time.nanosec - info_time.nanosec) / 1e9); if (diff < 0.1) { // 设置时间窗口大小 RCLCPP_INFO(this->get_logger(), "Matched images at %f", img_time.sec + img_time.nanosec * 1e-9); processMessages(image_buffer_.front(), info_buffer_.front()); image_buffer_.pop_front(); info_buffer_.pop_front(); } else if (img_time.sec + img_time.nanosec * 1e-9 < info_time.sec + info_time.nanosec * 1e-9) { break; // 图片过旧,等待新的图片到来 } else { info_buffer_.pop_front(); // 参数信息过旧 } } } void processMessages(sensor_msgs::msg::Image& image_msg, sensor_msgs::msg::CameraInfo& cam_info_msg) const { RCLCPP_INFO(this->get_logger(), "Processing matched data..."); } rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr image_sub_; rclcpp::Subscription<sensor_msgs::msg::CameraInfo>::SharedPtr info_sub_; std::deque<sensor_msgs::msg::Image> image_buffer_; std::deque<sensor_msgs::msg::CameraInfo> info_buffer_; }; int main(int argc, char **argv) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<TimeSynchronizerNode>()); rclcpp::shutdown(); return 0; } ``` 此版本同样遵循了双缓冲区设计模式,能够有效减少内存占用的同时提高运行效率[^6]。 --- #### 注意事项 尽管以上方案可以在一定程度上满足实际应用中的多源数据融合需求,但仍需注意以下几点: - 确保所有参同步的话题均携带标准的时间戳字段; - 合理设置容忍范围内的最大延迟差异以免丢失过多潜在可用的数据组合; - 对于高频率更新的主题可能需要进一步优化算法降低计算复杂度。 ---
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值