Multi-Robot Coordination with ROS 2: Getting Two Rovers to Work Together
Adrian Krzemiński,

TL;DR: A working ROS 2 multi robot setup needs three things done correctly: unique namespaces and TF frame IDs per robot, a shared DDS discovery domain (or explicit peer lists), and QoS profiles matched to your network. This article walks through a minimal reproducible configuration for two Leo Rovers running ROS 2 Humble, including launch files, a QoS reference table, and the commands to verify topics cross between robots. No fleet manager, no Kubernetes, just two rovers and a laptop on the same Wi-Fi.
Why ROS 2 Multi Robot Setup Is Harder Than It Looks
Running one rover is straightforward. Running two introduces problems that are easier to miss in ROS 2: topic name collisions, DDS discovery traffic, transform tree conflicts, and QoS mismatches that silently drop messages. ROS 2 solves the single-master bottleneck of ROS 1 by using DDS for peer-to-peer discovery, but that same mechanism is what bites you when scaling beyond one machine.
The good news: ROS 2 was designed for multi-robot systems from the start. The tooling is there. You just need to configure it correctly. This guide assumes you have two Leo Rovers running ROS 2 Humble on Ubuntu 22.04, both connected to the same Wi-Fi network, and a development laptop also on that network.
What Namespaces Actually Do in a Multi-Robot ROS 2 Setup
A namespace prefixes relative topic, service, action, and node names with a string. Without namespaces, both rovers publish to /cmd_vel, both subscribe to /cmd_vel, and you get crossed wires. With namespaces, rover one publishes to /leo1/cmd_vel and rover two to /leo2/cmd_vel.
You set namespaces at three levels:
- Node level via the
namespaceargument in a launch file. - Process level via the
ROS_NAMESPACEenvironment variable. - TF level via unique frame IDs, usually with the
frame_prefixparameter onrobot_state_publisher, so each robot hasleo1/base_link,leo1/odom, etc. If your bringup publishes absolute/tfand/tf_statictopics, remap them into the robot namespace as well.
Missing the TF prefix is the most common mistake. Topics get namespaced, but two robots still publish a frame called base_link, and your TF tree breaks.
Minimal Launch File for One Namespaced Rover
Below is a Python launch file you can drop on each rover. Set ROBOT_NAME as an environment variable before launch, or hardcode it per robot. Adjust the included bringup launch file if your Leo Rover image uses a different package or launch-file name.
The file launches the rover bringup inside a namespace, remaps TF topics into that namespace, and applies a TF prefix:
- On rover one:
export ROBOT_NAME=leo1 - On rover two:
export ROBOT_NAME=leo2
multi_rover.launch.py:
import os
from launch import LaunchDescription
from launch.actions import GroupAction
from launch_ros.actions import PushRosNamespace, SetRemap
from launch_ros.substitutions import FindPackageShare
from launch.substitutions import PathJoinSubstitution
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
def generate_launch_description():
robot_name = os.environ.get('ROBOT_NAME', 'leo1')
bringup = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([FindPackageShare('leo_bringup'), 'launch', 'system.launch.py'])
]),
launch_arguments={'frame_prefix': f'{robot_name}/'}.items()
)
group = GroupAction([
PushRosNamespace(robot_name),
SetRemap(src='/tf', dst='tf'),
SetRemap(src='/tf_static', dst='tf_static'),
bringup,
])
return LaunchDescription([group])
How DDS Discovery Works Between Two Robots
By default, ROS 2 uses multicast UDP for DDS participant discovery on ROS_DOMAIN_ID. Set both rovers and your laptop to the same domain ID, and they will find each other automatically on a flat network:
export ROS_DOMAIN_ID=42
This works on a lab bench. It breaks in three real-world cases: Wi-Fi access points that block multicast (common with enterprise APs), large fleets where discovery traffic floods the network, and segmented networks where robots sit behind NAT.
For two Leo Rovers on a known Wi-Fi network, multicast discovery on a shared domain ID is the right starting point. If multicast is blocked, switch to Fast DDS with a static peers list. Create fastdds_peers.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<participant profile_name="participant_profile" is_default_profile="true">
<rtps>
<builtin>
<initialPeersList>
<locator><udpv4><address>192.168.1.101</address></udpv4></locator>
<locator><udpv4><address>192.168.1.102</address></udpv4></locator>
</initialPeersList>
</builtin>
</rtps>
</participant>
</profiles>
Then export FASTRTPS_DEFAULT_PROFILES_FILE=/path/to/fastdds_peers.xml on each machine. If your system is not already using Fast DDS, also set RMW_IMPLEMENTATION=rmw_fastrtps_cpp. Adjust IP addresses to your rovers.
What QoS Profiles You Need for Cross-Robot Topics
QoS (Quality of Service) in ROS 2 controls how messages behave under network stress. If publisher and subscriber QoS are incompatible, no data flows, and the error is not always loud. For multi-robot work over Wi-Fi, the defaults are often wrong.
The table below summarizes profiles to use for typical cross-robot topics:
- Sensor data (LiDAR, camera, IMU): Reliability BEST_EFFORT, Durability VOLATILE, History KEEP_LAST, Depth 5. Drops are acceptable; latency matters.
- Velocity commands (/cmd_vel): Reliability RELIABLE, Durability VOLATILE, History KEEP_LAST, Depth 10. Delivery should be dependable on a good link, but stale commands have no value, so keep a command timeout or watchdog on the robot.
- Map / static transforms: Reliability RELIABLE, Durability TRANSIENT_LOCAL, History KEEP_LAST, Depth 1. Late joiners need the latest map or static transform.
- Diagnostics / status: Reliability RELIABLE, Durability VOLATILE, History KEEP_LAST, Depth 10.
- Coordination signals (goal, sync barriers): Reliability RELIABLE, Durability TRANSIENT_LOCAL, History KEEP_LAST, Depth 1.
If you use Nav2 across robots, note that map_server and static transforms use TRANSIENT_LOCAL semantics for latched data. Custom coordination topics, however, default to RELIABLE/VOLATILE, which means a robot joining late will miss the last published goal. Use TRANSIENT_LOCAL for any “latched” semantics you would have used in ROS 1.
How to Verify the Setup Is Actually Working
After launching both rovers, run these checks from your laptop. They will tell you within a minute whether discovery, namespaces, and QoS line up.
- List nodes from both robots:
ros2 node listshould show/leo1/...and/leo2/...entries. - List topics:
ros2 topic listshould show two parallel trees, one per namespace. - Check QoS compatibility:
ros2 topic info /leo1/cmd_vel --verboseshows publishers and subscribers with their full QoS, so mismatches are visible. - Inspect bandwidth:
ros2 topic bw /leo1/scanfrom the laptop confirms data is crossing the network at the expected rate (a 10 Hz 2D LiDAR is typically 30 to 80 KB/s, depending on scan size). - Verify TF isolation:
ros2 run tf2_tools view_frames --ros-args -r /tf:=/leo1/tf -r /tf_static:=/leo1/tf_static, then repeat forleo2. You should see independent trees with no shared frame names.
If ros2 node list only shows one robot, your discovery is broken. Tcpdump on the DDS UDP ports for your domain ID will show whether DDS traffic is even leaving the rover; for example, domain 0 uses ports around 7400, while ROS_DOMAIN_ID=42 uses ports around 17900 rather than 7400-7500.
When Two Rovers Become a Fleet: What Changes
This pattern (namespace + shared domain + tuned QoS) scales to roughly five to ten robots on a single Wi-Fi segment before discovery traffic and TF broadcasts start to hurt. Beyond that, you move toward ROS 2 Discovery Server (a centralized DDS discovery mechanism shipped with Fast DDS) or Zenoh bridges. Discovery Server reduces multicast discovery traffic; Zenoh can also help route data and handle WAN scenarios.
For most research and field robotics work with two to four Leo Rovers, the configuration in this article is enough. Teams running multi-rover experiments in academic research and planetary analog missions can use essentially this stack: per-robot namespaces, a coordinating laptop, and Nav2 instances running per robot with a thin coordination node on top.
How to Add Coordination Logic on Top
Once the two rovers see each other, coordination is just another ROS 2 node. The simplest pattern is a centralized coordinator running on your laptop or one of the rovers:
- Subscribes to
/leo1/odomand/leo2/odom. - Sends Nav2
NavigateToPoseaction goals to/leo1/navigate_to_poseand/leo2/navigate_to_pose. - Runs whatever logic you need: leader-follower, area partitioning, task auctions.
For decentralized coordination, each rover runs its own logic node and they exchange state on a shared topic like /fleet/state (publish from both, subscribe on both, use RELIABLE QoS with KEEP_LAST depth 20). This avoids a single point of failure but requires conflict resolution, typically a deterministic tie-breaker based on robot ID.
FAQ
Do both rovers need identical ROS 2 versions?
For this guide, use the same ROS 2 distribution on both rovers. Mixing Humble and Iron on the same network can work for basic message types but breaks on any package that changes message definitions between distributions. Keep both rovers on the same distribution and compatible package versions where possible.
Can you use ROS_DOMAIN_ID to isolate robot pairs on the same network?
Yes. Different domain IDs use different UDP ports, so two pairs of rovers can coexist on one Wi-Fi network without seeing each other. The commonly recommended safe range on Linux with default ephemeral port settings is 0 to 101.
What happens to Nav2 in a multi-robot setup?
Each robot runs its own full Nav2 stack inside its namespace. Maps can be shared via a /map topic with TRANSIENT_LOCAL QoS, or each robot can build its own. Nav2 supports namespacing natively through the namespace launch argument in the standard bringup files.
Is Wi-Fi reliable enough for multi-robot coordination?
For coordination messages at 1 to 10 Hz, yes, assuming a decent AP and line of sight. For streaming high-bandwidth raw camera feeds or multiple dense sensor streams between robots, often no. Keep heavy sensor data local to each robot and only share processed outputs (pose estimates, detections, maps).
How do you handle robot identifiers in code?
Read the namespace at runtime with this->get_namespace() in C++ or self.get_namespace() in Python. Do not hardcode robot names in your nodes. This keeps the same binary deployable on any robot.
What is the difference between PushRosNamespace and the namespace argument on a Node?
PushRosNamespace applies to every node in a launch group, including included launch files, as long as those nodes do not force absolute names. The namespace argument on a single Node action only namespaces that one node. For multi-robot work, PushRosNamespace inside a GroupAction is the right tool.
Can you run this setup with one Leo Rover and a simulated second robot?
Yes. Run Gazebo with a namespaced Leo Rover model on your laptop and a real Leo Rover on the network. From a ROS 2 graph perspective they are indistinguishable, which makes this a useful development workflow before buying a second physical platform.
Ready to Build Your Two-Rover Setup?
The configuration in this article is designed for Leo Rover systems running ROS 2 Humble, with only package-name or launch-file adjustments if your image differs. If you already have one rover and want to extend your work into multi-robot coordination, adding a second identical platform is the lowest-friction path: same firmware, same SDK, same documentation. Browse available configurations at the Fictionlab shop.