Panther /tf and /tf_static topics are not namespaced in multi-robot setup

Hello Husarion team,

I am working with multiple real Husarion Panther UGVs in a ROS 2 multi-robot setup, not simulation.

The TF frames already have their own prefix per robot, which helps avoid frame-name collisions. However, on the real robot, the TF topics themselves are still global:

/tf
/tf_static

They do not become:

/<robot_namespace>/tf
/<robot_namespace>/tf_static

This creates complexity when multiple real robots share the same ROS 2 network. Even if the frames are prefixed, all robots still publish TF data on the same global topics.

My questions are:

  1. What is the recommended approach for handling Panther real-robot multi-robot TF?

  2. Should we rely only on frame prefixes, or is it supported/recommended to namespace /tf and /tf_static?

  3. Is there an easy way to switch between these two modes:

  • prefixed frames on namespaced /tf and /tf_static

  • namespaced /tf and /tf_staticwithout prefixed frames

  1. Can this be configured through Docker Compose/environment variables, or do we need to override the husarion_ugv_ros launch files and add the TF remapping manually?

This is important for us because we are building a real multi-robot system, and we need a clean and scalable TF architecture for multiple Panthers on the same network.

Thanks.

Hello @eldesouky,

Currently, the namespace behavior matches what you described. There is a single topic, /tf, and /tf_static, which contains the frames of all robots, preceded by the appropriate prefix.

This stems from the project’s roots, which evolved from ROS 1, where this approach was standard.

For a long time, ROS 2 didn’t provide any recommendations on how to handle tf with multiple robots, so this concept remained unchanged. Frankly, I haven’t been following this thread for a while, so I don’t know if there are official recommendations for this type of solution. However, I’m aware that many solutions use the /tf topic remapping approach.

Answering your questions:

  1. What is the recommended approach for handling a multi-robot TF structure in Panther?
    Setting the namespace in compose.yaml adds the namespace to all nodes and the appropriate prefix to the frame names. If a frame is a common source for all, such as a map, an appropriate map -> robot_odom transformation should be provided for each robot.

  2. Should we rely solely on frame prefixes, or is this supported/recommended in the /tf and /tf_static namespaces?
    This is not supported and would require rebuilding the husarion_ugv_ros rep with appropriate changes to the launch file, such as adding a function (SetRemap to globally set all absolute /tf topics to relative tf ).

  3. & 4. Is there a simple way to switch between these two modes?
    Currently, there is no such functionality.

Could you share your experience on why changing the approach from namespaced tf topics is better in your use cases? We could then analyzing the benefits and implementing this type of functionality on the development branch.

1 Like

Hello @Rafal,

Thanks for the clarification.

After reviewing other ROS 2 multi-robot approaches, especially the Open Robotics Discourse discussion about multi-robot TF trees and the Nav2 discussion about namespaced TFs, I think the main point is:

Frame prefixes solve frame-name collision, but they do not solve TF topic traffic isolation.

References:

Open Robotics Discourse: TF tree in a multi-robot setup in ROS 2
https://discourse.openrobotics.org/t/tf-tree-in-a-multi-robot-setup-in-ros2/41426

Nav2 issue: How should I interact with namespaced Nav2 TFs in Jazzy?
https://github.com/ros-navigation/navigation2/issues/5449

There is also a very relevant comment from Nav2 project lead Steve Macenski in the Nav2 issue:

“We remap the TF from the global namespace for all nodes into the robots’ individual namespaces so that they do not collide. In a realistic system, you wouldn’t want to have TF maintaining high rate data about every other robot, growing N^2.”

This matches our concern exactly: the issue is not only frame collision, but also high-rate TF traffic scaling and isolation.

In the Open Robotics Discourse discussion, one proposed approach is “multiple TF trees”, where each robot has its own TF topics:

/robot1/tf
/robot1/tf_static

/robot2/tf
/robot2/tf_static

Then, if a global or fleet-level TF view is needed, only selected transforms can be republished or bridged to a shared/global TF topic.

This is also close to how Nav2 multi-robot deployments commonly handle TF remapping: absolute /tf and /tf_static are remapped to relative tf and tf_static, so the namespace controls the final TF topic.

With the current Panther behavior:

/tf
/tf_static

all robots publish and consume TF through the same global topics. Even if the frames are prefixed correctly, every robot-local stack can still receive TF traffic from the other robots. This becomes harder to scale, debug, record with rosbag, filter, and isolate when multiple real robots are running on the same ROS 2 network.

The two modes we would like to support are:

1. Prefixed frames on namespaced TF topics

Example topics:

/robot1/tf
/robot1/tf_static

Example frames:

robot1/base_link
robot1/odom
robot1/front_laser

This gives both frame-name safety and TF topic isolation.

2. Namespaced TF topics without prefixed frames

Example topics:

/robot1/tf
/robot1/tf_static

Example frames:

base_link
odom
front_laser

This is useful when each robot stack is fully isolated inside its namespace, and any global/fleet TF view is handled separately by a bridge or coordination layer.

From our current investigation, this seems to require two launch-level changes.

A. Add global TF remapping in the bringup launch

As you mentioned, we would need to use SetRemap inside the bringup launch, preferably around the relevant nodes using a GroupAction, to force absolute TF topics to become relative:

SetRemap(src="/tf", dst="tf")
SetRemap(src="/tf_static", dst="tf_static")

B. Make frame_prefix configurable in load_urdf.launch.py

Currently, robot_state_publisher receives:

robot_state_pub_node = Node(
    package="robot_state_publisher",
    executable="robot_state_publisher",
    parameters=[
        {"robot_description": robot_description_content},
        {"frame_prefix": namespace_ext},
    ],
    namespace=namespace,
    emulate_tty=True,
)

To support switching between prefixed and non-prefixed frames, frame_prefix would need to be controlled by a launch argument or environment variable instead of always being tied to namespace_ext.

In conclusion, the current prefix-based design is valid for avoiding frame-name conflicts, but namespaced TF topics provide better traffic isolation, cleaner per-robot debugging, cleaner rosbag recording, and better separation between local robot TF and global/fleet TF.

For us, the important point is to avoid maintaining a fork of husarion_ugv_ros, because this is the robot’s basic driver layer. We want to keep receiving upcoming Husarion updates, support, and Docker image releases without losing compatibility or maintaining separate custom Docker images.

If you are open to supporting this in the official branch, we can also propose or collaborate on the solution.

Add this to the thread:

I also want to add another related real-robot case to this discussion.

We are integrating a Fixposition Vision-RTK 2 onto a Panther running ROS 2 Jazzy using fixposition_driver_ros2, and we have encountered a TF namespacing issue. We would appreciate your guidance before implementing a custom solution.

Setup

Panther publishes its TF tree under a namespace/prefix as shipped, for example:

panther1/base_link

panther1/odom

…

The Fixposition driver, however, publishes TF frames as global, non-namespaced names, including:

vrtk_link

odom

FP_ECEF

FP_POI

FP_VRTK

FP_ENU0

…

We verified from the Fixposition documentation that there is no support for tf_prefix / frame_prefix; namespacing applies only to topics.

We also reviewed the Husarion Fixposition demo repository:

It appears that the setup does not support applying a frame prefix to Fixposition TF frames either.

Problem

This results in two odom frames in /tf:

panther1/odom

odom

These represent different physical/logical frames, but TF consumers such as Nav2, robot_localization, and RViz can interpret or configure them ambiguously.

Additionally, all Fixposition frames exist outside the panther1/ namespace, which is problematic for multi-robot setups.

This connects directly to the previous TF discussion: frame prefixes and TF-topic namespacing need to be configurable independently. Some integrations need prefixed frames, while others need namespaced TF topics and controlled bridging/rewriting of selected transforms.

Question

Have you previously integrated Fixposition Vision-RTK 2 with Panther, or with other Husarion robots, while maintaining proper TF namespacing for frames?

If yes, what is the recommended approach?

Hi @eldesouky,

Personally, I’m for option A. As for the decision on which option we’ll choose and when, I’ll let you know tomorrow (I need to discuss your observations).

1 Like

As promised, I’m back with an answer. Next week, I could prepare a developer version with a namespace. However, I’ve been working on ROSbot for the past few months and don’t currently have Panther for physical testing. So I’d appreciate your feedback once the branch is ready and simulation-tested.

Regarding the fix position, the issue isn’t fully resolved. My colleague’s findings are presented here: https://github.com/husarion/husarion_outdoor_nav_ros/issues/392#issuecomment-4182495189

1 Like

Thank you for the follow-up.

That would be very helpful. If you can prepare a developer branch with TF topic namespacing support, we can test it on real Panther robots and provide feedback from a physical multi-robot setup.

This will help validate the behavior beyond simulation, especially real network traffic and TF isolation in a multi-robot system. We will wait for the developer branch and hold our custom changes for now, so we can test the official approach first.

Regarding the Fixposition issue, the link you shared does not open from my side. Could you please confirm if it is public or share the relevant findings here?

Hi @eldesouky,

Thank you for your willingness to test it on a physical robot.
I’ll let you know when I start working on this project. I will most likely close the currently open projects around Wednesday.

Fixposition
The link is actually not public. I’m copying my colleague’s comment:

There are 2 approaches to integrate it with our stack. We can use just fixposition as a standalone sensor for positioning. Or we can use data published by it and provide map → odom transform (with ekf for example). Second approach allows to add more sensors when needed.

Standalone

  • Disable lokal ekf in the driver, tis is the most problematic for dynamic switching. FP provides it’s own odom frame which would cause confilcts with our current setup
  • tf vrtk_link to lynx/base_link (weird to have tf from sensor to base_link, but that how it works)
  • Optional fork of the repo to make it possible to assign different frames than map and odom (without tf_prefix)
  • Adjust nav2 to use config without tf_prefix ORRR add static tf’s map → lynx/map, odom → lynx/odom

Custom map → odom TF with Fixposition data

  1. first approach - EKF + provided transforms
  • Disable FP_ECEF transform publishing
  • tf map → FP_ENU0
  • tf base_link → FP_POI
  • ekf with single fixposition subscription, optionally can add additional info
  1. second approach - EKF (or not) + ignore FP transforms
  • node, that subscribes to fixposition/odometry_enu, and republishes it changing frames to vrtk_link and map,
  • tf base_link (mount_link) → vrtk_link
  • this node could also publish TF map → odom (this can be enabled/disabled by param if needed), if we dont want to use EKF for more inputs.

I did a PoC with second approach. It works and output is identical to our custom setup with GPS. Others approaches would require access to hardware. I’m not continuing until we decide which approach is the best.

1 Like

Hi @RafalGorecki,

Thank you for the clarification and for sharing the findings.

We will wait for the TF namespacing developer branch. This is currently blocking our multi-robot integration, so we will hold our custom solution and test the developer branch once it is ready.

Regarding Fixposition, the standalone approach also aligns with one of the TF modes we requested: namespaced /tf and /tf_static topics without frame prefixes. In that mode, the robot can keep TF traffic isolated under the robot namespace, while the frame names can remain unprefixed, such as map, odom, and base_link.

So the TF changes would help not only with multi-robot TF isolation, but also with integrations like Fixposition where removing the frame prefix may be needed.

Thanks again.

Hi @eldesouky,

I started changing the repository. As expected, on Tuesday-Wednesday I managed to make a few major changes and definitely got the simulation building and running. All changes are stored on launch-refactor Due to further support issues, I didn’t have time to properly test and analyze my own changes to see if I’d forgotten anything.

I’ll be testing this even more in the coming days.

I also plan to:

  • Simulation tests and self-review
  • Release Docker with a branch name so that you don’t have to build everything on the RPi (time-consuming due to the low RAM and processing power).

Once all of this is ready. You will be able to use this version by editing compose.yaml

  • Change the image name to husarion/husarion-ugv:jazzy-launch-refactor
  • Add argument tf_namespace_bridge:=False which disables the default backward compatibility by remapping the entire /<ns>/tf -> /tf with prefix

I’ll keep you updated on any further changes. For now, it’s in progress.

1 Like

Hi @RafalGorecki,

Thank you for the update and for starting the launch refactor.

Great to hear that the simulation is already building and running on the launch-refactor branch. We will wait until you finish your testing, self-review, Docker image release, and compose changes.

Once the branch/image is ready, we can test it on real Panther hardware and give feedback from our multi-robot setup.

The planned tf_namespace_bridge:=False option sounds useful, especially if it allows us to test isolated namespaced TF topics without relying on the backward-compatibility bridge.

Thanks again, and we’ll wait for your next update.

Hi @eldesouky,

Hi, I’ve uploaded the development image to Dockerhub. Feel free to test it. I’ve noticed that due to changes in the external behaviortree_ros2 repository, LEDs may stop working. I’ll try to determine what changes caused this issue. If LEDs aren’t critical for you, you can replace the image. I’d love to hear your feedback, and I ask that you be especially careful during initial runs.

According to previous msg. To use development branch

  • Change the image name to husarion/husarion-ugv:jazzy-launch-refactor
  • Add argument tf_namespace_bridge:=False which disables the default backward compatibility by remapping the entire /<ns>/tf -> /tf with prefix

I’ll preface this question by saying that it’s hard to say when these changes will be incorporated into the main repo branch and the next version will be released.

Hi @RafalGorecki,

Thank you for the development image.

We tested husarion/husarion-ugv:jazzy-launch-refactor with:

tf_namespace_bridge:=False

1. Bringup/config feedback

At first, the bringup failed because the new launch-refactor image expected a newer /config layout. The mounted /home/husarion/config was missing files such as:

/config/husarion_ugv_description/urdf/panther.urdf.xacro
/config/husarion_ugv_description/config/WH05.yaml
/config/husarion_ugv_diagnostics/config/system_monitor.yaml
/config/husarion_ugv_teleop/config/joy2twist_panther.yaml

After syncing the missing files from the image install space into /home/husarion/config, the URDF/xacro issue was fixed.

Then we hit another config mismatch: twist_mux_controller was missing its type field in the mounted controller YAML. Copying the updated WH05_controller.yaml from the image fixed that too.

We also had to update the manager config and behavior tree files because the mounted config was missing/incompatible with the new image, including:

/config/husarion_ugv_manager/config/lights_manager.yaml
/config/husarion_ugv_manager/config/safety_manager.yaml
/config/husarion_ugv_manager/behavior_trees/SafetyBT.btproj

After syncing these files, the bringup reached a working state. The controllers loaded and activated correctly, including twist_mux_controller, and both lights_manager and safety_manager initialized successfully.

So the first feedback is:

  1. The development image works after syncing the mounted /config directory with the new launch-refactor layout.
  2. The issue was mainly a config/version mismatch between the mounted host config and the new image.
  3. It would be helpful if you can provide the expected /config structure or migration steps for this branch.

2. Blocking issue: odom -> base_link translation is not updating

After bringup was fixed, we continued testing the namespaced TF behavior.

The namespaced TF topics appear to be active, but we found a blocking issue with the odom -> base_link transform.

The robot can move with teleoperation, and the laser scan moves in RViz, but the robot model / base_link frame stays fixed. This breaks SLAM and navigation because the robot pose is not translated in TF.

We checked:

ros2 run tf2_ros tf2_echo odom base_link --ros-args --remap tf:=/panther2/tf --remap tf_static:=/panther2/tf_static

The transform exists, and the yaw changes, but the translation remains always zero:

Translation: [0.000, 0.000, 0.000]

So it looks like odometry orientation is being published into TF, but the x/y translation between odom and base_link is not being updated.

ros2 run tf2_ros tf2_echo odom base_link tf:=/panther2/tf tf_static:=/panther2/tf_static

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.947, -0.322]

Rotation: in RPY (radian) [0.000, -0.000, -2.485]

Rotation: in RPY (degree) [0.000, -0.000, -142.402]

Matrix:-0.792  0.610  0.000  0.000-0.610 -0.792  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110261.477649030

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.949, -0.315]

Rotation: in RPY (radian) [0.000, -0.000, -2.502]

Rotation: in RPY (degree) [0.000, -0.000, -143.331]

Matrix:-0.802  0.597  0.000  0.000-0.597 -0.802  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110262.528939413

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.312]

Rotation: in RPY (radian) [0.000, -0.000, -2.507]

Rotation: in RPY (degree) [0.000, -0.000, -143.650]

Matrix:-0.805  0.593  0.000  0.000-0.593 -0.805  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110263.570482620

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.312]

Rotation: in RPY (radian) [0.000, -0.000, -2.507]

Rotation: in RPY (degree) [0.000, -0.000, -143.637]

Matrix:-0.805  0.593  0.000  0.000-0.593 -0.805  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110264.523427268

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.312]

Rotation: in RPY (radian) [0.000, -0.000, -2.507]

Rotation: in RPY (degree) [0.000, -0.000, -143.629]

Matrix:-0.805  0.593  0.000  0.000-0.593 -0.805  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110265.582458153

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.312]

Rotation: in RPY (radian) [0.000, -0.000, -2.507]

Rotation: in RPY (degree) [0.000, -0.000, -143.623]

Matrix:-0.805  0.593  0.000  0.000-0.593 -0.805  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110266.443401456

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.312]

Rotation: in RPY (radian) [0.000, -0.000, -2.507]

Rotation: in RPY (degree) [0.000, -0.000, -143.631]

Matrix:-0.805  0.593  0.000  0.000-0.593 -0.805  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110267.547336797

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.313]

Rotation: in RPY (radian) [0.000, -0.000, -2.505]

Rotation: in RPY (degree) [0.000, -0.000, -143.541]

Matrix:-0.804  0.594  0.000  0.000-0.594 -0.804  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110268.497220747

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.950, -0.311]

Rotation: in RPY (radian) [0.000, -0.000, -2.508]

Rotation: in RPY (degree) [0.000, -0.000, -143.701]

Matrix:-0.806  0.592  0.000  0.000-0.592 -0.806  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110269.562212437

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.953, -0.304]

Rotation: in RPY (radian) [0.000, -0.000, -2.523]

Rotation: in RPY (degree) [0.000, -0.000, -144.577]

Matrix:-0.815  0.580  0.000  0.000-0.580 -0.815  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110270.514895106

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.953, -0.304]

Rotation: in RPY (radian) [0.000, -0.000, -2.524]

Rotation: in RPY (degree) [0.000, -0.000, -144.628]

Matrix:-0.815  0.579  0.000  0.000-0.579 -0.815  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110271.563411344

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.952, -0.307]

Rotation: in RPY (radian) [0.000, -0.000, -2.517]

Rotation: in RPY (degree) [0.000, -0.000, -144.199]

Matrix:-0.811  0.585  0.000  0.000-0.585 -0.811  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110272.521954635

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.955, -0.295]

Rotation: in RPY (radian) [0.000, -0.000, -2.542]

Rotation: in RPY (degree) [0.000, -0.000, -145.627]

Matrix:-0.825  0.565  0.000  0.000-0.565 -0.825  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110273.489695517

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.962, -0.274]

Rotation: in RPY (radian) [0.000, -0.000, -2.587]

Rotation: in RPY (degree) [0.000, -0.000, -148.203]

Matrix:-0.850  0.527  0.000  0.000-0.527 -0.850  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110274.570522723

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.964, -0.267]

Rotation: in RPY (radian) [0.000, -0.000, -2.601]

Rotation: in RPY (degree) [0.000, -0.000, -149.038]

Matrix:-0.858  0.514  0.000  0.000-0.514 -0.858  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110275.532202209

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.964, -0.267]

Rotation: in RPY (radian) [0.000, -0.000, -2.602]

Rotation: in RPY (degree) [0.000, -0.000, -149.058]

Matrix:-0.858  0.514  0.000  0.000-0.514 -0.858  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110276.559183400

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.964, -0.267]

Rotation: in RPY (radian) [0.000, -0.000, -2.601]

Rotation: in RPY (degree) [0.000, -0.000, -149.033]

Matrix:-0.857  0.515  0.000  0.000-0.515 -0.857  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110277.511811349

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.964, -0.267]

Rotation: in RPY (radian) [0.000, -0.000, -2.601]

Rotation: in RPY (degree) [0.000, -0.000, -149.012]

Matrix:-0.857  0.515  0.000  0.000-0.515 -0.857  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110278.576582245

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.964, -0.267]

Rotation: in RPY (radian) [0.000, -0.000, -2.601]

Rotation: in RPY (degree) [0.000, -0.000, -149.008]

Matrix:-0.857  0.515  0.000  0.000-0.515 -0.857  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110279.532698070

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.966, -0.260]

Rotation: in RPY (radian) [0.000, -0.000, -2.616]

Rotation: in RPY (degree) [0.000, -0.000, -149.867]

Matrix:-0.865  0.502  0.000  0.000-0.502 -0.865  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110280.488119441

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.967, -0.253]

Rotation: in RPY (radian) [0.000, -0.000, -2.629]

Rotation: in RPY (degree) [0.000, -0.000, -150.654]

Matrix:-0.872  0.490  0.000  0.000-0.490 -0.872  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110281.547412841

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.962, -0.272]

Rotation: in RPY (radian) [0.000, -0.000, -2.590]

Rotation: in RPY (degree) [0.000, -0.000, -148.417]

Matrix:-0.852  0.524  0.000  0.000-0.524 -0.852  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110282.501706395

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.957, -0.290]

Rotation: in RPY (radian) [0.000, -0.000, -2.552]

Rotation: in RPY (degree) [0.000, -0.000, -146.236]

Matrix:-0.831  0.556  0.000  0.000-0.556 -0.831  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110283.551292544

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.955, -0.296]

Rotation: in RPY (radian) [0.000, -0.000, -2.540]

Rotation: in RPY (degree) [0.000, -0.000, -145.547]

Matrix:-0.825  0.566  0.000  0.000-0.566 -0.825  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110284.488525877

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.955, -0.296]

Rotation: in RPY (radian) [0.000, -0.000, -2.541]

Rotation: in RPY (degree) [0.000, -0.000, -145.567]

Matrix:-0.825  0.565  0.000  0.000-0.565 -0.825  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000At time 1779110285.532265293

Translation: [0.000, 0.000, 0.000]

Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.955, -0.296]

Rotation: in RPY (radian) [0.000, -0.000, -2.541]

Rotation: in RPY (degree) [0.000, -0.000, -145.589]

Matrix:-0.825  0.565  0.000  0.000-0.565 -0.825  0.000  0.0000.000  0.000  1.000  0.0000.000  0.000  0.000  1.000

This also appears in diagnostics as:

ekf_filter: odometry/filtered topic status
Actual frequency: 0.000000 Hz

So the issue may be related to the EKF / odometry filtered output, or to the node responsible for publishing odom -> base_link after the TF namespacing changes.

Could you please check whether the odom -> base_link TF publisher is correctly remapped to /panther2/tf, and whether the EKF/odometry pipeline is still connected correctly with tf_namespace_bridge:=False?

Current behavior:

/panther2/tf exists
odom → base_link exists
yaw changes
x/y translation stays zero
robot moves physically
laser scan moves
robot base frame stays fixed
SLAM/navigation fail

Expected behavior:

odom → base_link should update both rotation and x/y translation while the robot moves.

This is currently blocking real-robot validation of the namespaced TF branch.

TF Fixed Issue

This is a short video demonstrating the issue visually: the robot moves with teleoperation, and the laser scan moves in the Web UI, but the robot model / base_link frame stays fixed because the odom -> base_link translation is not updating.

3. Navigation image / TF alignment

On our side, we also updated our custom navigation image/package to remap TF the same way:

/tf → tf
/tf_static → tf_static

So with PushRosNamespace(namespace), Nav2/SLAM/localization use:

/<robot_namespace>/tf
/<robot_namespace>/tf_static

This was applied in our navigation launch files:

bringup_launch.py
navigation_launch.py
localization_launch.py
slam_launch.py

The important point is that the base robot stack and the navigation stack must both use the same TF topic strategy. If the Panther base stack publishes to /panther2/tf, but the navigation stack still listens to global /tf, Nav2 will not receive the required transforms.

So one final question: do you plan to provide a matching husarion_ugv_navigation / autonomy image that is aligned with the new namespaced TF behavior?

We currently use a custom navigation image, so we can continue with our remapped version. But if Husarion plans to provide an official navigation/autonomy image compatible with tf_namespace_bridge:=False, that would be better for avoiding custom patches.

Finally, we will wait for your next edits on the launch-refactor branch, then re-test on the real robot once the updated image/config is available.

Also, if possible, could you provide a matching husarion_ugv_navigation / autonomy image aligned with the new namespaced TF behavior? This would help us test the full stack without maintaining custom navigation patches.

1 Like

Hi @eldesouky,

Thanks for the detailed report and the video. Replying point-by-point.

1. Bringup / config sync

The cleanest way to refresh the mounted /config for the new image layout is to let the image do it:

docker compose up --force-recreate -d
docker exec husarion_ugv_ros bash -c 'source /ros2_ws/install/setup.bash && update_config_directory'
docker compose restart

update_config_directory copies package defaults from the image’s install space into the mounted host config dir — no hand-syncing needed.

The BT/manager issues you hit are already fixed in the freshly released image (behaviortree_cpp 4.9.0 bump + matching lights.xml / safety.xml updates).

2. odom → base_link translation stuck at zero

I reproduced launch-refactor in simulation with the same args and EKF publishes /panther2/odometry/filtered and /panther2/tf correctly — so the namespace remap path itself isn’t broken. Could you run this on the robot while teleoperating and paste the output?

ros2 topic hz /panther2/odometry/wheels
ros2 topic hz /panther2/imu/data
ros2 topic hz /panther2/odometry/filtered
ros2 topic info -v /panther2/tf | grep -E "Node name|Endpoint type"
ros2 node info /panther2/ekf_filter

3. Navigation / autonomy image alignment

Your remap pattern is correct. We do plan to align the autonomy stack with namespaced TF, but it’s not on the near-term roadmap — launch-refactor on the base stack comes first. Until then, I’d suggest forking husarion_ugv_autonomy_ros and applying the same /tf → tf / /tf_static → tf_static remaps there.

Thanks for your feedback.