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.