Skip to content

feat(bridge_ros2): align REP-105 TF publishing with Nav2 conventions#132

Closed
robin-zealrobotics wants to merge 1 commit intoMOLAorg:developfrom
Zeal-Robotics:feat/bridge-ros2-rep105-tolerance-and-rebroadcast
Closed

feat(bridge_ros2): align REP-105 TF publishing with Nav2 conventions#132
robin-zealrobotics wants to merge 1 commit intoMOLAorg:developfrom
Zeal-Robotics:feat/bridge-ros2-rep105-tolerance-and-rebroadcast

Conversation

@robin-zealrobotics
Copy link
Copy Markdown
Contributor

@robin-zealrobotics robin-zealrobotics commented Apr 22, 2026

Brings the localization /tf publisher in line with what AMCL / slam_toolbox / RTAB-Map do, so MOLA is a drop-in REP-105 citizen for Nav2 consumers. Three things in one commit:

  1. REP-105 composition fix. (map -> base)(t_scan) * (base -> odom)(t) was using waitForTransform() which only does "latest" — biases the correction by the localizer's processing latency. Now uses tf_buffer_->lookupTransform(..., scan_tp) so both factors share l.timestamp. (Skip-on-failure behavior from fix(bridge_ros2): skip TF publish when REP-105 odom lookup fails #130 preserved.)

  2. transform_tolerance (default 0.1 s). Stamp = node->now() + tolerance so consumers can lookupTransform(map, base, now()) without ExtrapolationException. Same convention as AMCL transform_tolerance / RTAB-Map tf_tolerance. Uses rclcpp::Node::now() so use_sim_time works automatically.

  3. transform_publish_period (default 0.05 s = 20 Hz, 0 disables). Wall timer re-broadcasts the cached map -> odom independent of localizer rate. Same as slam_toolbox transform_publish_period / RTAB-Map tf_delay.

Backwards-compat: set both new params to 0 to recover prior stamping behavior (modulo the t_scan fix). publish_in_sim_time still controls all other publishers; only the localization /tf stamp source moved to Node::now().

Companion PR for mola_lidar_odometry (env-var plumbing) follows.

Summary by CodeRabbit

  • New Features
    • Added transform caching and periodic rebroadcasting of localization updates
    • Introduced configurable parameters: transform_tolerance (timestamp offset in seconds) and transform_publish_period (rebroadcast interval, disabled when set to 0)
    • Enhanced timestamp handling for transform messages

This brings the localization /tf publisher in line with the convention
used by AMCL, slam_toolbox, RTAB-Map and Cartographer, addressing three
issues observed when MOLA is consumed by Nav2-style downstream nodes:

1. REP-105 composition bug (fix). The inner factor of
   map -> odom = (map -> base)(t_scan) * (base -> odom)(t)
   was sampled at "latest" via waitForTransform(), which only supports
   tf2::TimePoint{}. The two factors must be sampled at the same
   instant or the published correction is biased by the odom-frame
   motion accumulated during the localizer's processing latency. Switch
   to tf_buffer_->lookupTransform(..., scan_tp). On lookup failure the
   publish is skipped (an empty default-constructed TransformStamped
   would inject TF_NO_FRAME_ID into every consumer's tf2 buffer).

2. transform_tolerance (new param, default 0.1s). The published stamp
   is now node->now() + transform_tolerance so consumers can do
   lookupTransform(map, base_link, now()) without
   ExtrapolationException. Mirrors AMCL's transform_tolerance and
   RTAB-Map's tf_tolerance. Uses the ROS clock so use_sim_time is
   honored automatically.

3. transform_publish_period (new param, default 0.05s = 20 Hz). A
   wall timer re-broadcasts the cached map -> odom independent of
   localization update rate, keeping the TF buffer continuously fresh
   even when the localizer runs slower than the consumer query rate.
   Set to 0 to disable. Mirrors slam_toolbox's transform_publish_period
   and RTAB-Map's tf_delay.

Backwards compat: to exactly preserve the prior (post-bugfix) stamping
behavior set transform_tolerance: 0 and transform_publish_period: 0.
publish_in_sim_time retains its meaning for all other publishers
(sensor TFs, odom messages, etc.); only the localization /tf stamp
source changed.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

The changes introduce a caching and periodic rebroadcast mechanism for localization transforms in the ROS2 bridge. A configurable wall timer republishes the most recently computed transform with an updated timestamp offset, while the TF lookup for REP-105 correction now uses the localization scan time instead of the latest available time.

Changes

Cohort / File(s) Summary
Transform Caching & Periodic Rebroadcast
mola_bridge_ros2/include/mola_bridge_ros2/BridgeROS2.h, mola_bridge_ros2/src/BridgeROS2.cpp
Added configurable parameters transform_tolerance and transform_publish_period, member state for caching the latest TransformStamped with mutex protection, and new broadcastCachedLocalizationTf() method. Implemented periodic wall timer to rebroadcast cached transforms with updated timestamps when enabled.
Localization TF Publishing Logic
mola_bridge_ros2/src/BridgeROS2.cpp (publishLocalizationTf method)
Modified REP-105 odom→base_link lookup to query at localization scan time instead of latest time, added exception logging with scan stamp context, integrated caching of computed transforms under mutex, and immediate rebroadcast after computation.

Sequence Diagram

sequenceDiagram
    participant Timer as Wall Timer<br/>(periodic)
    participant Node as ROS Node
    participant Cache as Transform Cache<br/>(mutex-protected)
    participant TFBroadcaster as TF Broadcaster
    
    Timer->>Node: Check if broadcastCachedLocalizationTf()<br/>should execute
    Note over Node: Enters broadcastCachedLocalizationTf()
    Node->>Cache: Acquire lock & read<br/>cachedLocalizationTf_
    alt Cache is populated
        Cache-->>Node: Return cached Transform
        Node->>Node: Update stamp to<br/>node->now() +<br/>transform_tolerance
        Node->>TFBroadcaster: sendTransform(updated_tf)
        TFBroadcaster-->>Node: Transform broadcasted
    else Cache is empty
        Cache-->>Node: Return (no-op)
    end
    Node->>Cache: Release lock
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

Poem

🐰 A clever cache, so swift and bright,
Rebounds the transforms, time by time,
With tolerant stamps, the moments align,
While timers tick in rhythm sublime—
Old transforms reborn, forever in flight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly addresses the main objective: aligning REP-105 TF publishing with Nav2 conventions by implementing transform tolerance and periodic rebroadcasting.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mola_bridge_ros2/src/BridgeROS2.cpp (1)

1649-1661: ⚠️ Potential issue | 🟡 Minor

CI blocker: clang-format style check failing on the stamp assignment.

The pipeline (CI clang-format) reports that lines 1650–1651 need reformatting. Collapse the multi-line assignment onto a single line per the project .clang-format so CI passes:

🎨 Proposed formatting fix
-  tf.header.stamp =
-      node->now() + rclcpp::Duration::from_seconds(params_.transform_tolerance);
+  tf.header.stamp = node->now() + rclcpp::Duration::from_seconds(params_.transform_tolerance);

Alternatively, run /usr/bin/clang-format-14 --style=file -i mola_bridge_ros2/src/BridgeROS2.cpp locally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mola_bridge_ros2/src/BridgeROS2.cpp` around lines 1649 - 1661, The assignment
to tf.header.stamp should be collapsed to a single line to satisfy clang-format:
replace the current multi-line assignment in the block that calls rosNode()
(around rosNode(), tf.header.stamp, params_.transform_tolerance and
rclcpp::Duration::from_seconds) with a single-line expression (e.g.
tf.header.stamp = node->now() +
rclcpp::Duration::from_seconds(params_.transform_tolerance);) and then run
clang-format (or rely on CI) to verify formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mola_bridge_ros2/src/BridgeROS2.cpp`:
- Around line 1595-1614: The code must guard against an epoch/zero scan
timestamp so tf_buffer_->lookupTransform isn't called with a zero time (which
tf2 treats as "latest"); before creating scan_tp or calling
tf_buffer_->lookupTransform check l.timestamp (or the result
scan_stamp.nanoseconds()) for zero and if zero either return/skip publishing
(matching existing behavior) or MRPT_LOG_THROTTLE_WARN_STREAM and return; also
include the actual scan timestamp (e.g., scan_stamp.seconds() or nanoseconds())
in the MRPT_LOG_THROTTLE_ERROR_STREAM message that references params_.odom_frame
and l.child_frame so timing issues are actionable (adjust the catch message near
MRPT_LOG_THROTTLE_ERROR_STREAM accordingly).

---

Outside diff comments:
In `@mola_bridge_ros2/src/BridgeROS2.cpp`:
- Around line 1649-1661: The assignment to tf.header.stamp should be collapsed
to a single line to satisfy clang-format: replace the current multi-line
assignment in the block that calls rosNode() (around rosNode(), tf.header.stamp,
params_.transform_tolerance and rclcpp::Duration::from_seconds) with a
single-line expression (e.g. tf.header.stamp = node->now() +
rclcpp::Duration::from_seconds(params_.transform_tolerance);) and then run
clang-format (or rely on CI) to verify formatting.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8489769-bc24-4089-8f7e-586de1b592da

📥 Commits

Reviewing files that changed from the base of the PR and between 678da6f and 484afe4.

📒 Files selected for processing (2)
  • mola_bridge_ros2/include/mola_bridge_ros2/BridgeROS2.h
  • mola_bridge_ros2/src/BridgeROS2.cpp

Comment on lines +1595 to 1614
tf2::Transform odomOnBase_tf;
try
{
const rclcpp::Time scan_stamp = mrpt::ros2bridge::toROS(l.timestamp);
const tf2::TimePoint scan_tp{std::chrono::nanoseconds(scan_stamp.nanoseconds())};
const auto ref_to_trgFrame =
tf_buffer_->lookupTransform(l.child_frame, params_.odom_frame, scan_tp);
tf2::fromMsg(ref_to_trgFrame.transform, odomOnBase_tf);
}
catch (const tf2::TransformException& ex)
{
// Skip publishing entirely: broadcasting a default-constructed
// TransformStamped here would inject empty frame_ids into every
// subscriber's tf2 buffer (TF_NO_FRAME_ID / TF_SELF_TRANSFORM spam).
MRPT_LOG_THROTTLE_ERROR_STREAM(
5.0, "publish_localization_following_rep105=true but could not resolve tf '"
<< params_.odom_frame << "' -> '" << l.child_frame << "'; skipping TF publish.");
<< params_.odom_frame << "' -> '" << l.child_frame << "' at scan stamp ("
<< ex.what() << "); skipping TF publish.");
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

tf2 BufferCore lookupTransform behavior when time argument is zero (TimePointZero) — does it return the latest available transform?

💡 Result:

Yes, in tf2 BufferCore, passing TimePointZero (time == 0) to lookupTransform returns the latest available transform. This is explicitly documented in the official tf2 API reference across multiple ROS 2 versions (e.g., latest, rolling, humble, foxy). For the single-time overload lookupTransform(target_frame, source_frame, time), the documentation states: "time – The time at which the value of the transform is desired. (0 will get the latest)". The same applies to target_time and source_time in the multi-time overload: "(0 will get the latest)". This behavior is consistent from ROS 1 documentation as well, where time=0 or rospy.Time indicates the latest common time.

Citations:


Add guard against zero/epoch timestamp to prevent silent "latest" TF fallback.

scan_stamp = mrpt::ros2bridge::toROS(l.timestamp) produces nanoseconds() == 0 if LocalizationUpdate is published with a default-constructed timestamp. In tf2 BufferCore::lookupTransform, a zero timestamp is explicitly interpreted as "latest available", which silently re-introduces the timing bias you are removing in this PR — without warning.

Guard against a zero/epoch stamp by either skipping the publish (matching current skip-on-failure behavior) or logging a throttled warning:

Proposed guard
     try
     {
       const rclcpp::Time   scan_stamp = mrpt::ros2bridge::toROS(l.timestamp);
+      if (scan_stamp.nanoseconds() == 0)
+      {
+        MRPT_LOG_THROTTLE_WARN_STREAM(
+            5.0,
+            "publish_localization_following_rep105=true but LocalizationUpdate has a "
+            "default/epoch timestamp; skipping REP-105 composition to avoid a silent "
+            "'latest' TF lookup.");
+        return;
+      }
       const tf2::TimePoint scan_tp{std::chrono::nanoseconds(scan_stamp.nanoseconds())};
       const auto           ref_to_trgFrame =
           tf_buffer_->lookupTransform(l.child_frame, params_.odom_frame, scan_tp);
       tf2::fromMsg(ref_to_trgFrame.transform, odomOnBase_tf);
     }

Minor: the error message at lines 1610–1612 prints frame transform direction but omits scan_tp timestamp detail (e.g., scan_stamp.seconds()), making it less actionable when debugging TF buffer timing issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mola_bridge_ros2/src/BridgeROS2.cpp` around lines 1595 - 1614, The code must
guard against an epoch/zero scan timestamp so tf_buffer_->lookupTransform isn't
called with a zero time (which tf2 treats as "latest"); before creating scan_tp
or calling tf_buffer_->lookupTransform check l.timestamp (or the result
scan_stamp.nanoseconds()) for zero and if zero either return/skip publishing
(matching existing behavior) or MRPT_LOG_THROTTLE_WARN_STREAM and return; also
include the actual scan timestamp (e.g., scan_stamp.seconds() or nanoseconds())
in the MRPT_LOG_THROTTLE_ERROR_STREAM message that references params_.odom_frame
and l.child_frame so timing issues are actionable (adjust the catch message near
MRPT_LOG_THROTTLE_ERROR_STREAM accordingly).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

❌ Failed to clone repository into sandbox. Please try again.

@jlblancoc
Copy link
Copy Markdown
Member

@robin-zealrobotics Thanks!

I guess you have already tested it with a real robot?

Let me study this one carefully to make sure it works with both, REP105 and non-REP105 modes...

@robin-zealrobotics
Copy link
Copy Markdown
Contributor Author

Tested it in simulation, not a real robot yet.
Feel free to wait with merging until I've tested it completely on real hardware, which probably is early next week.

@jlblancoc
Copy link
Copy Markdown
Member

Superseded by #135

@jlblancoc jlblancoc closed this Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants