Message Filters#
Agnocast provides message synchronization filters compatible with the ROS 2 message_filters API. These allow you to synchronize messages from multiple Agnocast topics based on their timestamps.
Feature Support Status#
| Component | Support |
|---|---|
| Synchronizer (2–9 inputs) | Supported |
| ExactTime policy | Supported |
| ApproximateTime policy | Supported |
| ApproximateEpsilonTime policy | Not supported |
| Subscriber filter | Supported |
| PassThrough filter | Supported |
| Cache | Not supported |
| Chain | Not supported |
Synchronizing standard ROS 2 subscriptions (std::shared_ptr<const M>) and Agnocast subscriptions (agnocast::ipc_shared_ptr<const M>) within the same Synchronizer is not supported — all inputs must be Agnocast subscriptions.
Prerequisites#
Both sync policies extract an rclcpp::Time from each message. You must specialize the message_filters::message_traits::TimeStamp trait for every message type used with the Synchronizer. Messages with a header.stamp field (e.g., sensor_msgs::msg::Image, geometry_msgs::msg::PoseStamped) are supported out of the box.
Migrating a Synchronizer#
Before (rclcpp message_filters)#
#include <message_filters/subscriber.h>
#include <message_filters/synchronizer.h>
#include <message_filters/sync_policies/exact_time.h>
using MsgA = sensor_msgs::msg::Image;
using MsgB = sensor_msgs::msg::CameraInfo;
using SyncPolicy = message_filters::sync_policies::ExactTime<MsgA, MsgB>;
class MySyncNode : public rclcpp::Node
{
message_filters::Subscriber<MsgA> sub_a_;
message_filters::Subscriber<MsgB> sub_b_;
message_filters::Synchronizer<SyncPolicy> sync_;
void callback(
const MsgA::ConstSharedPtr & a,
const MsgB::ConstSharedPtr & b)
{
// Process synchronized messages
}
public:
MySyncNode() : Node("sync_node"),
sub_a_(this, "/image"),
sub_b_(this, "/camera_info"),
sync_(SyncPolicy(10), sub_a_, sub_b_)
{
sync_.registerCallback(&MySyncNode::callback, this);
}
};
After (Agnocast Stage 1)#
#include "agnocast/agnocast.hpp" // (1)
using MsgA = sensor_msgs::msg::Image;
using MsgB = sensor_msgs::msg::CameraInfo;
using SyncPolicy =
agnocast::message_filters::sync_policies::ExactTime<MsgA, MsgB>; // (2)
class MySyncNode : public rclcpp::Node
{
agnocast::message_filters::Subscriber<MsgA> sub_a_; // (3)
agnocast::message_filters::Subscriber<MsgB> sub_b_; // (3)
agnocast::message_filters::Synchronizer<SyncPolicy> sync_; // (4)
void callback(
const agnocast::ipc_shared_ptr<const MsgA> & a, // (5)
const agnocast::ipc_shared_ptr<const MsgB> & b)
{
// Process synchronized messages
}
public:
MySyncNode() : Node("sync_node"),
sub_a_(this, "/image"),
sub_b_(this, "/camera_info"),
sync_(SyncPolicy(10), sub_a_, sub_b_)
{
sync_.registerCallback(&MySyncNode::callback, this);
}
};
Key changes:
- Include changes to
agnocast/agnocast.hpp - Sync policy namespace changes to
agnocast::message_filters::sync_policies message_filters::Subscriber→agnocast::message_filters::Subscribermessage_filters::Synchronizer→agnocast::message_filters::Synchronizer- Callback receives
agnocast::ipc_shared_ptr<const T>instead ofT::ConstSharedPtr
After (Agnocast Stage 2)#
#include "agnocast/agnocast.hpp"
using MsgA = sensor_msgs::msg::Image;
using MsgB = sensor_msgs::msg::CameraInfo;
using SyncPolicy =
agnocast::message_filters::sync_policies::ExactTime<MsgA, MsgB>;
class MySyncNode : public agnocast::Node // (1)
{
agnocast::message_filters::Subscriber<MsgA, agnocast::Node> sub_a_; // (2)
agnocast::message_filters::Subscriber<MsgB, agnocast::Node> sub_b_;
agnocast::message_filters::Synchronizer<SyncPolicy> sync_;
void callback(
const agnocast::ipc_shared_ptr<const MsgA> & a,
const agnocast::ipc_shared_ptr<const MsgB> & b)
{
// Process synchronized messages
}
public:
MySyncNode() : Node("sync_node"),
sub_a_(this, "/image"),
sub_b_(this, "/camera_info"),
sync_(SyncPolicy(10), sub_a_, sub_b_)
{
sync_.registerCallback(&MySyncNode::callback, this);
}
};
Additional changes from Stage 1:
- Base class changes to
agnocast::Node Subscribertakes the node type as its second template argument (Subscriber<MsgA, agnocast::Node>); it defaults torclcpp::Node, whichagnocast::Nodedoes not derive from
ExactTime Policy#
For ExactTime, the queue_size constructor argument is the maximum number of incomplete message sets to buffer. When exceeded, the oldest sets are dropped.
You can register a drop callback that is invoked whenever incomplete sets are discarded. It has the same signature as the main callback:
sync.getPolicy()->registerDropCallback(&MySyncNode::dropCallback, this);
ApproximateTime Policy#
The migration steps are the same as ExactTime — replace ExactTime with ApproximateTime in the policy type:
using SyncPolicy =
agnocast::message_filters::sync_policies::ApproximateTime<MsgA, MsgB>;
All parameter APIs from rclcpp's message_filters::sync_policies::ApproximateTime are supported with identical signatures:
SyncPolicy policy(10); // queue size
policy.setMaxIntervalDuration(rclcpp::Duration(0, 100'000'000)); // 100ms max
policy.setAgePenalty(0.5);
policy.setInterMessageLowerBound(0, rclcpp::Duration(0, 30'000'000)); // 30ms for input 0
agnocast::message_filters::Synchronizer<SyncPolicy> sync(policy, sub_a_, sub_b_);
| Method | Description | Default |
|---|---|---|
setMaxIntervalDuration(duration) |
Maximum time difference between matched messages | No limit |
setAgePenalty(double) |
Weight given to message age in matching (must be >= 0) | 0.1 |
setInterMessageLowerBound(i, duration) |
Minimum expected interval between messages for input i |
0 (auto-estimated) |
Unlike ExactTime, the queue_size constructor argument limits the number of buffered messages per topic (not message sets). It must be greater than 0; a value of at least 2 is recommended, since a queue size of 1 tends to drop many messages.
ApproximateTime does not support drop callbacks.
Note
The policy logs a one-time warning if messages arrive out of timestamp order, or if the actual interval between messages is smaller than the bound declared with setInterMessageLowerBound().
Callback Signatures#
The synchronized callback can use any of these parameter types, and they can be mixed:
// By const reference (recommended)
void cb(const agnocast::ipc_shared_ptr<MsgA const> & a,
const agnocast::ipc_shared_ptr<MsgB const> & b);
// By value
void cb(agnocast::ipc_shared_ptr<MsgA const> a,
agnocast::ipc_shared_ptr<MsgB const> b);
// As MessageEvent (provides receipt time metadata)
void cb(const agnocast::message_filters::MessageEvent<MsgA const> & a,
const agnocast::message_filters::MessageEvent<MsgB const> & b);
Callbacks can be registered as:
// Free function
sync.registerCallback(myFreeFunction);
// Bound via std::bind
sync.registerCallback(std::bind(&MyClass::callback, this, std::placeholders::_1, std::placeholders::_2));
// Member function pointer (recommended for class methods)
sync.registerCallback(&MyClass::callback, this);
Migrating a PassThrough Filter#
PassThrough forwards messages without filtering — useful when you already have a subscription and want to feed it into a Synchronizer. The API is identical to rclcpp's message_filters::PassThrough.
Before (rclcpp message_filters)#
message_filters::PassThrough<MsgType> pass_through;
// Feed messages manually
pass_through.add(message); // message is MsgType::ConstSharedPtr
After (Agnocast)#
agnocast::message_filters::PassThrough<MsgType> pass_through; // (1)
// Feed messages manually
pass_through.add(message); // message is agnocast::ipc_shared_ptr<const MsgType> // (2)
Key changes:
message_filters::PassThrough→agnocast::message_filters::PassThroughadd()takesagnocast::ipc_shared_ptr<const T>instead ofT::ConstSharedPtr