One script, sim to real – building OmniBot's autonomy stack
This technical write-up documents the architecture, sensor fusion pipeline, and control arbitration implemented in the open-source OmniBot tree (under omnibot-ros2 and mecanum-kinematics). All quantitative tracking metrics presented below are currently simulation benchmarks evaluated in Gazebo Harmonic. Physical testbed trials on physical ground surfaces are scheduled and pending test fixture execution. No physical performance claims are asserted without verified bag recordings committed directly to the repository.
In mobile robotics, the gap between simulation and real-world deployment is rarely a failure of mathematics. It is almost always a failure of configuration drift. Teams write one ROS 2 launch file for simulation with mock odometry and idealized physics, and maintain an entirely separate set of launch scripts for the physical machine. Over months of development, transform trees diverge, remappings fall out of sync, topic names drift, and the simulation stops being a trustworthy predictor of physical behavior.
When designing the autonomy stack for OmniBot—our open reference mecanum mobile manipulator—we established a foundational constraint: a single parameterised launch file must bring up the complete autonomy system from simulation to physical silicon. Running hybrid_robot.launch.py with use_sim_time:=true connects to Gazebo Harmonic; running it without arguments connects to serial motor controllers and hardware sensors. The transform tree, EKF state estimation node, SLAM pipeline, Nav2 lifecycle managers, and safety arbitration multiplexers remain identical byte-for-byte.
Achieving that equivalence on a 4-wheel mecanum platform required solving three hard engineering challenges:
- Mecanum odometry drift: The 45° angled rollers that grant omnidirectional agility also induce continuous microscopic slippage, causing naive encoder dead reckoning to fail within meters.
- Sensor fusion design: Configuring an Extended Kalman Filter (EKF) to fuse noisy wheel odometry and MEMS inertial measurements without letting accumulating encoder drift corrupt the filter.
- Holonomic trajectory control and safety gating: Tuning Nav2 for true 3-DOF planar motion while arbitrating commands through an active safety multiplexer that prevents runaway behavior.
1. Architecture: The Single-Pipeline Stack
The central entrypoint of the autonomy stack is omnibot_hybrid/launch/hybrid_robot.launch.py. It orchestrates eight core functional subsystems into a cohesive hierarchy:
In this architecture, high-level task instructions enter via /mission/command. The mission planner sequences high-level waypoints or hands control to an AI policy, publishing the active routing mode to /control_mode. The command velocity multiplexer (cmd_vel_mux) gates input channels to prevent overlapping control streams, and forwards sanitized commands to /cmd_vel/out, which drives the physical motor firmware or simulation plugin.
2. The Mecanum Kinematics Problem: Micro-Slip and Odometry Drift
A 4-wheel mecanum drive provides holonomic motion: the vehicle can translate forward/backward (vx), strafe laterally (vy), and rotate in place (ω) simultaneously. Each wheel features passive rollers angled at 45° to the wheel plane.
From the kinematics library in mecanum_drive_ros2/kinematics.py, the forward kinematic relations mapping wheel angular velocities (ωfl, ωfr, ωbl, ωbr) to planar body twist are:
vx = (r / 4) * (omega_fl + omega_fr + omega_bl + omega_br) vy = (r / 4) * (-omega_fl + omega_fr + omega_bl - omega_br) omega = (r / (4 * (L/2 + W/2))) * (-omega_fl + omega_fr - omega_bl + omega_br)
where r is wheel radius (0.04 m), L is front-rear wheelbase (0.165 m), and W is left-right track width (0.215 m).
Under idealized mathematical conditions, integrating body twist over time produces planar position:
x(t + dt) = x(t) + (vx * cos(theta) - vy * sin(theta)) * dt y(t + dt) = y(t) + (vx * sin(theta) + vy * cos(theta)) * dt theta(t + dt) = theta(t) + omega * dt
On a physical robot, however, this naive dead reckoning model collapses rapidly. Because mecanum rollers rely on rolling contact at 45°, any lateral motion or rotational resistance causes continuous microscopic slip between the roller rubber and the floor surface. Furthermore, minute differences in floor levelness cause wheel unloading, where one wheel temporarily loses normal force and spins with near-zero traction.
In our baseline experiments, integrating raw encoder ticks during a 10-meter straight-line run resulted in a 14.2% lateral position error and over 18° of rotational drift. If fed directly to Nav2 as the robot's authoritative odometry frame, this drift corrupts local costmaps, leads to phantom obstacle generation, and causes path planning to abort with recovery failures.
3. Taming Odometry Drift: Velocity-Only EKF Fusion
To eliminate runaway dead reckoning errors, we structure state estimation around an Extended Kalman Filter (robot_localization) with two non-negotiable architectural rules.
Rule 1: Fuse Velocities Only from Wheel Odometry
Most beginner robotics setups configure the EKF to consume (x, y, yaw) positions directly from wheel odometry. On a mecanum robot, this is fatal: encoder position is an integrated signal containing non-stationary accumulated error. Feeding accumulating position error into the filter as an absolute measurement forces the Kalman gain to fight the IMU and global map corrections.
In omnibot_navigation/config/robot_localization.yaml, we configure odom0_config to fuse velocities only:
# robot_localization.yaml (excerpt)
ekf_filter_node:
ros__parameters:
frequency: 30.0
two_d_mode: true
publish_tf: true
map_frame: map
odom_frame: odom
base_link_frame: base_link
world_frame: odom
# Wheel odometry: FUSE VELOCITIES ONLY (vx, vy, vyaw).
# Position fields (x, y, z, roll, pitch, yaw) are set to FALSE.
odom0: /odom
odom0_config: [false, false, false,
false, false, false,
true, true, false,
false, false, true,
false, false, false]
odom0_differential: false
odom0_relative: falseBy treating wheel encoders as a source of body-frame instantaneous linear velocities (vx, vy) and rotational rate (vyaw), the EKF integrates them dynamically inside its own state covariance matrix, bounding drift and allowing other sensors to correct position error.
Rule 2: Strip Noisy Linear Accelerations from the IMU
Low-cost onboard 6-DOF MEMS IMUs suffer from significant acceleration noise, temperature drift, and vibration coupling from the drive motors. In 2D planar mode, integrating raw ax and ay accelerations quickly introduces fictitious linear velocities.
Instead, we fuse only the IMU's angular velocity (ω_z) and differential orientation:
# IMU: differential orientation + raw angular velocity.
# Linear accelerations (ax, ay, az) are STRICTLY DISABLED.
imu0: /imu/data
imu0_config: [false, false, false,
true, true, true,
false, false, false,
true, true, true,
false, false, false]
imu0_differential: true
imu0_relative: trueRule 3: Single-Broadcaster TF Tree Ownership
A common source of simulation-to-real divergence is transform broadcaster conflict. In our hardware driver launch (base_control.launch.py), we explicitly set publish_tf: false. The base controller driver emits raw nav_msgs/Odometry on /odom, but only the EKF node is permitted to broadcast the odom → base_link transform. Running multiple broadcasters on the same transform causes high-frequency TF jitter (30–50 Hz), which immediately destabilizes SLAM Toolbox scan matching and corrupts the local costmap.
4. Tuning Nav2 for Holonomic Bases and Traction Limits
Nav2 is conventionally deployed on differential drive platforms where lateral velocity vy = 0. Deploying it on a mecanum platform without parameter tuning artificially restricts the robot to non-holonomic turning, sacrificing its ability to navigate narrow corridors and strafe around obstacles.
In omnibot_navigation/config/nav2_params.yaml, we tune the DWB local trajectory planner to take full advantage of omnidirectional capability while respecting physical traction constraints:
- Bilateral lateral velocity limits: Setting
max_vel_y: 0.3andmin_vel_y: -0.3allows DWB to generate diagonal and pure-strafe trajectories. - 3-D velocity space sampling: Differential planners only sample forward speed and yaw. For holonomic bases, trajectory generation requires sampling across
vx,vy, andvtheta. We configurevx_samples: 20,vy_samples: 15, andvtheta_samples: 20to provide dense trajectory evaluation without overloading the onboard computer. - Traction-aware acceleration limits: Sudden accelerations on a mecanum base break static friction on the 45° rollers, causing wheel spin before the robot's inertia moves. We set conservative linear accelerations (
acc_lim_x: 0.4 m/s²,acc_lim_y: 0.3 m/s²) and insertnav2_velocity_smootherbetween DWB and the driver to eliminate command step discontinuities.
5. The Safety Gate and Command Arbitration Pipeline
A complex autonomous system integrates multiple command sources: classical global navigation (Nav2), vision-language policies (VLA), reinforcement learning nodes, and human teleoperation. Without a strict hardware safety gate, these sources can publish conflicting velocities simultaneously, creating unpredictable vehicle motions.
We enforce safety through a dual-stage architecture:
Stage 1: cmd_vel_mux
A deterministic ROS 2 node subscribing to /cmd_vel (Nav2), /cmd_vel/vla, /cmd_vel/teleop, and /cmd_vel/rl. Only the channel matching the current /control_mode is forwarded to /cmd_vel/out. If no message arrives on the active channel within 500 ms, zero velocity is published immediately.
Stage 2: Hardware Verifier
Defined in agent_engine/core/interfaces.py as VerifierPort, all low-level commands pass through hardware limit checks before actuation. The verifier validates maximum linear speeds (0.5 m/s), lateral speeds (0.3 m/s), and angular rates (1.5 rad/s), rejecting or clamping out-of-envelope commands.
Crucially, manual teleoperation always takes absolute priority: publishing a teleop command or triggering an emergency stop immediately revokes autonomy modes and halts the motors.
6. Benchmark Results: Simulation vs. Physical Trials
Honesty in robotics reporting requires clearly separating simulated validation from physical hardware trials. Below are our empirical benchmark measurements across five standardized trajectories, comparing the naive kinematic integration against our velocity-only EKF fusion pipeline in Gazebo Harmonic, alongside placeholders for pending real-world trials.
| Test Trajectory / Scenario | Naive Odometry (Sim) | EKF Fusion (Sim - Gazebo) | Physical Trials (Pending) |
|---|---|---|---|
| 10 m Forward Translation (vx = 0.3 m/s) | 14.2% dist drift, 18.4° rot | 1.8% dist drift, 2.1° rot | [Pending real trials] |
| 5 m Pure Lateral Strafe (vy = 0.2 m/s) | 22.8% dist drift, 24.1° rot | 3.4% dist drift, 3.8° rot | [Pending real trials] |
| 4-Corner Square Loop (3m × 3m closed path) | 86.4 cm closure error | 7.2 cm closure error | [Pending real trials] |
| In-Place 360° Spin (5 full revolutions) | 38.2° heading error | 4.1° heading error | [Pending real trials] |
| Nav2 Obstacle Waypoint Mission (10 runs) | 4 / 10 succeeded (6 aborts) | 10 / 10 succeeded (100%) | [Pending real trials] |
* Note on Table 1: Gazebo Harmonic tests were performed with 100 Hz ODE physics and simulated surface friction coefficients of μ = 0.6. Physical validation trials will be conducted on smooth polished industrial concrete and short-pile carpet fixtures. When completed, raw MCAP recording bags and rosbag checksums will be published directly to the repository alongside updated figures.
7. Reproducing the Stack on Your Machine
All code discussed here is open-source and ready to run. You do not need physical robot hardware to exercise the entire stack:
# 1. Clone the OmniBot repository git clone https://github.com/ohho-robotics/OmniBot.git cd OmniBot # 2. Source your ROS 2 Jazzy environment and build source /opt/ros/jazzy/setup.bash colcon build --symlink-install source install/setup.bash # 3. Launch full autonomy in Gazebo Harmonic simulation ros2 launch omnibot_hybrid hybrid_robot.launch.py use_sim_time:=true # 4. In a separate terminal, dispatch a test mission ros2 topic pub --once /mission/command std_msgs/msg/String \ "data: 'navigate:kitchen'"
When running on the physical robot, the exact same command is executed without the simulation flag:
# On the robot's onboard SBC (connected to the base controller via USB) ros2 launch omnibot_hybrid hybrid_robot.launch.py
By keeping the interfaces identical, developers can build behaviors, train navigation policies, and verify recovery routines in simulation with confidence that the physical vehicle will execute the exact same topics, transforms, and safety envelopes.