Navigate with Nav2 in hangar_sim
The hangar_sim robot configuration features a Clearpath Ridgeback mobile base carrying a UR5e arm. This guide covers the full mobile navigation stack: how the lidar feeds into Nav2's costmaps, how the robot localizes itself, how velocity commands reach the mecanum wheels, and how to use the navigation Objectives.
For a summary of what MoveIt Pro supports for mobile bases, including sensors and drive types, see Mobile Base Support.
See Whole-Body Motion for configuring whole-body MoveIt planning with a mobile base, and Mobile Navigation Setup for wiring Nav2 into a new robot configuration package.
Launch the Runtime and Desktop App
We assume you have already installed MoveIt Pro to the default install location. Start the MoveIt Pro Runtime using:
moveit_pro run -c hangar_sim
Then launch or connect the separately distributed MoveIt Pro Desktop App. The Runtime does not serve a bundled user interface.
The Robot: Ridgeback Mecanum Base

Lidar Sensors
The robot is equipped with two SICK TIM571 planar lidars: one at the front bumper and one at the rear bumper.
Key characteristics:
- Each lidar sweeps 270° at ⅓° resolution — 811 beams, the TIM571's datasheet geometry — over a 0.05–25 m range, at 10 Hz
- Front lidar publishes scans to
/scan_front; rear lidar publishes to/scan_rear, both with best-effort QoS - Each scan passes through an independent angular bounds filter that clips chassis self-hits, removing the two rear-corner arcs at 0–31° and 239.5–270.5° (−0.01–0.54 rad and 4.18–4.72 rad) and keeping the roughly 208° in between
- Filtered scans are published on
/scan_front_filteredand/scan_rear_filtered - Combined coverage: 360° around the robot, with the two kept arcs overlapping to the sides
Both filtered topics are the obstacle sources for both costmaps.
How the scans are produced
Each scanner is simulated as a MuJoCo camera with DEPTH_TYPE=THREE_D_LIDAR, which picknik_mujoco_ros renders as depth tiles and stitches into an organized PointCloud2 on /lidar_front/points and /lidar_rear/points. The sweep is rendered on the render thread rather than measured inside mj_step, which is what lets it carry the full 811 beams without charging them to the physics step.
Localization and mapping need a LaserScan — slam_toolbox and amcl each accept exactly one scan topic and no point cloud — so lidar_flattener.py subscribes to both clouds and publishes the scans the rest of the stack reads. It takes the horizontal row of each cloud (the rows above and below it would put ceiling and floor returns into the scan plane), reverses it to the counter-clockwise LaserScan convention, and publishes /scan_front and /scan_rear spanning angle_min = 0 to angle_max = 4.7124 rad.
The scans are published in the lidar_front_ROS and lidar_rear_ROS frames, which robot_drivers_to_persist_sim.launch.py broadcasts as static transforms on ridgeback_base_link at ±0.45 m along X and 0.15 m up. Each is z-up with its X axis at the scan's angle_min, so a beam's index maps directly to a bearing on the robot. The clouds themselves are published in lidar_front_optical_frame and lidar_rear_optical_frame, which are there for RViz and tooling; nothing in the navigation stack reads them.
Localization
By default, hangar_sim runs beluga_amcl for map-based localization. AMCL matches both filtered lidar scans — front and rear, relayed onto /scan_interleaved one scan per message rather than merged, so no message combines two scan times — against a pre-built map to estimate the robot's pose, publishing the map→odom transform that anchors the odometry frame to the map. Both lidars are used because the front lidar alone gives the particle filter too little structure near the long hangar fuselage, where it can otherwise lose track. Because the Ridgeback is a mecanum (omnidirectional) base, AMCL uses an omnidirectional motion model so that lateral motion is not mistaken for rotation. Localization is configured in the amcl section of nav2_params.yaml. To tune it for your robot, see Localization Tuning.
Set localization:=False in robot_drivers_to_persist_sim.launch.py to disable AMCL; the launch then publishes a static map→odom transform instead, falling back to passing odometry straight through to Nav2. Also set use_fuse to false, because nothing corrects the Fuse estimate's drift without AMCL, and the launch warns that the combination is unsupported.
With AMCL disabled, the robot relies on odometry alone. On a real robot the position estimate then drifts over time due to wheel slip and IMU bias; over medium to long distances this drift accumulates, causing the costmap and planned paths to diverge from the robot's actual position. Keep map-based localization enabled for real-robot deployments.
Underneath AMCL, hangar_sim runs a Fuse state estimator that fuses three sensor sources for a more realistic odometry estimate:
- Wheel odometry from
/platform_velocity_controller_nav2/odom— provides x/y position and velocity, fused differentially to handle drift - Whole-body wheel odometry from
/platform_velocity_controller/odom— the same measurement from the whole-body controller, with the same settings. Only the active base controller publishes odometry, so this source is what lets the estimate follow the base when a whole-body Objective moves it - IMU from
/imu_sensor_broadcaster/imu— provides absolute orientation (roll, pitch, yaw) and yaw rate, and is the only source of heading, so wheel slip does not reach it
The filtered estimate is published on /odom_filtered, and Nav2 navigates on it. To navigate on the simulator's ground-truth odometry instead, set use_fuse to false in robot_drivers_to_persist_sim.launch.py; the launch then points Nav2's odom_topic back at /odom.
The video below shows Fuse in action during straight, spin, strafe, and straight maneuvers. The green arrow is raw wheel odometry (drifting), the red arrow is ground truth from MuJoCo, and the yellow path is the Fuse-filtered estimate tracking the ground truth closely.
Costmaps
Nav2 maintains two costmaps simultaneously. Both read obstacle data from /scan_front_filtered and /scan_rear_filtered as independent observation sources.
Global Costmap
The global costmap covers the full hangar map and is used for high-level path planning.
| Property | Value |
|---|---|
| Frame | map |
| Resolution | 5 cm/cell |
| Update frequency | 1 Hz |
| Layers | Static (pre-built map) + Obstacle (front + rear lidar, 8.0 m max range) + Inflation (0.7 m radius) |
The static layer loads the pre-built occupancy grid of the hangar. The obstacle layer adds dynamic obstacles from both lidars, up to 8.0 m range (with 10.0 m raytrace for clearing). The inflation layer expands all obstacles by 0.7 m, approximately the robot radius plus padding, to ensure the robot body clears obstacles while traversing.
A few reasons drive this choice:
- Long-range lidar returns tend to be noisier, so capping the obstacle range filters out unreliable readings at the edges of the sensor's range.
- Restricting the range limits how much of the hangar scene is visible to the costmap, keeping costmap updates more focused and easier to observe in tutorials.
- The raytrace range (10.0 m) is intentionally set longer than the obstacle range (8.0 m). This avoids a common edge case where a beam that just misses a real obstacle fails to clear a previously marked cell, leaving a phantom obstacle stuck in the costmap.
Local Costmap
The local costmap is a 5 × 5 m rolling window centered on the robot, used for reactive obstacle avoidance during path following. In hangar_sim, the lidar is primarily a long-range planar sensor suited for localization; the local costmap here demonstrates the concept but does not include the short-range sensors (such as depth cameras) that would typically drive its size in a production system.
| Property | Value |
|---|---|
| Frame | odom |
| Size | 5 × 5 m rolling window |
| Resolution | 5 cm/cell |
| Update frequency | 10 Hz |
| Layers | Obstacle (front + rear lidar, 4.5 m raytrace range, 3.5 m obstacle range) + Inflation (0.75 m radius) |
The local costmap updates at 10 Hz (much faster than the global costmap) and marks obstacles up to 3.5 m away, with a 4.5 m raytrace range for clearing. It does not include a static layer; the robot's immediate surroundings are treated as fully dynamic.
Costmaps in the MoveIt Pro Desktop App
Both costmaps are visible in the Visualization view as colored overlays in the 3D scene. The UI subscribes to the costmap topics published by Nav2 and updates in real time as the robot moves and new obstacles are detected.
To enable the costmap overlays, open the View menu and toggle Global Costmap and/or Local Costmap under the Navigation section.
Footprint overlays
The same Navigation section also exposes Local Footprint (light green) and Global Footprint (cyan) toggles that overlay the robot footprint published by each costmap. Local and global footprints are rendered separately because each costmap can apply different inflation, so the two outlines do not always coincide. The footprint topics default to /local_costmap/published_footprint and /global_costmap/published_footprint; they are configurable through the nav2LocalFootprintTopic and nav2GlobalFootprintTopic keys in the frontend settings.

The RViz view below illustrates the 5×5 m rolling window size — the white square is the local costmap centered on the robot, surrounded by the black global costmap.
The side-by-side below shows the MoveIt Pro Desktop App (left) with the purple inflation overlay around detected obstacles, alongside RViz (right) displaying the local costmap updating over the black global costmap with white lidar hit points from both the front and rear sensors.

Sensor overlays
To inspect the sensor data behind the navigation map, open View and enable Sensor Streaming under Display. Select the settings icon on that row to open the 3D Visualizer sidebar, which lists each source beneath Sensor Streaming, and enable the sources you want to compare. The 3D Visualizer discovers live sensor_msgs/PointCloud2 and sensor_msgs/LaserScan topics. In hangar_sim, useful sources include the front and rear lidar point clouds and the filtered laser scans used by the costmaps.
Expand a source in the sidebar to adjust its Point Size in meters. Leave Use Source Colors enabled to preserve colors supplied by the sensor; sources without color data appear orange so they remain visible against the map. Disable it to select a solid color for that source. Visibility and style preferences are saved in your browser, and changing a style updates the live display without restarting the stream. Disabling a source stops its streaming subscription.
Set the 3D Visualizer's Fixed Frame to map to compare the sensor returns with the navigation map. The Runtime transforms scan returns using their acquisition times before streaming them, so a moving sensor does not drag a previous scan through the scene. Invalid and out-of-range returns are omitted. If the required transforms are unavailable, new scan data is withheld rather than placed using an unrelated latest transform.
Plan overlays
Global Plan (red) draws the planner's path to the goal, and Local Plan (yellow) draws the trajectory the controller is currently following. Each has its own toggle in the Navigation section, so you can watch the controller deviate from the global path around an obstacle with only one of the two on screen. The global plan draws an arrow at every pose; the local plan is a line only, because it republishes at controller rate.
The topics default to /plan and /local_plan and are configurable through the nav2PlanTopic and nav2LocalPlanTopic keys in the frontend settings. /local_plan is what DWB and the Graceful controller publish; Regulated Pure Pursuit has no local-plan topic. MPPI instead publishes its chosen trajectory on /optimal_trajectory, and only when visualize is true in its section of nav2_params.yaml — hangar_sim ships both settings so the overlay works out of the box. visualize is a dynamic parameter, so ros2 param set /controller_server FollowPath.visualize false turns the publishing off on a running system. The flag also covers MPPI's candidate-rollout markers on /trajectories, but those are built only while something subscribes to them, so they cost nothing until you enable the Candidate Trajectories overlay described below.
The two overlays come from different frames: /plan is stamped in map and MPPI's trajectory in odom. Each is re-rooted through its own header, so a localization correction shifts them against each other — that is the estimate moving, not a rendering fault.
The shipped setting only applies while ~/.config/moveit_pro/hangar_sim/frontend_settings.yaml does not exist. That file is written the first time you save settings from the app, and it replaces the shipped file rather than merging with it, so if you had saved settings before upgrading, add nav2LocalPlanTopic: /optimal_trajectory to it by hand.
Both overlays are live-only: a toggle appears once a plan arrives on its topic, and both are cleared shortly after the Objective stops. The local plan is drawn from the last message the controller published, so it holds its last shape if an Objective keeps running after navigation finishes.
Commanded velocity overlay
Commanded Velocity (orange) draws the velocity the controller is asking the base for, at the base itself: an arrow along the commanded linear velocity, and an arc about the base's +Z axis for the commanded yaw rate. A positive yaw rate sweeps counter-clockwise. The arrow is drawn at a fixed scale, so a readout in the corner of the 3D Visualization pane gives the speed in m/s and the yaw rate in rad/s, where the numbers stay legible against the scene behind them. Roll and pitch rates are not drawn, because a ground base commands yaw only.
The overlay is drawn over the map and costmap layers and ignores the depth buffer, so neither the robot model nor an inflation layer hides it. Nothing is drawn while the base is commanded to hold still, and the arrow is removed once no command has arrived for half a second, rather than holding the last one on screen. A publisher at two commands per second or slower therefore reads as stopped between its samples. Like the plan overlays, the toggle is live-only: it appears once a command has arrived on its topic, and is cleared shortly after the Objective stops. On a topic nothing publishes to there is no Commanded Velocity row in the View menu at all, rather than a toggle that draws nothing.
The overlay draws at the origin of the frame the command is expressed in, which assumes that frame is attached to the base — true of the base frame a controller steers. A command stamped in map or odom is drawn at that frame's origin instead of at the robot.
The topic defaults to /cmd_vel and is configurable through the nav2CmdVelTopic key in the frontend settings. Both geometry_msgs/TwistStamped and geometry_msgs/Twist are accepted. The message is checked by shape, and geometry_msgs/Accel has the same shape, so a topic pointed at an acceleration is drawn as though it were a velocity and labelled in m/s. An unstamped Twist carries no frame, so the overlay cannot place it: set nav2CmdVelFrame to the frame the command is expressed in — usually the base frame the controller steers, ridgeback_base_link in hangar_sim. Until it is set, the overlay reports the missing setting instead of guessing a frame.
hangar_sim publishes on neither of the defaults: its launch remaps the controller's command onto /cmd_vel_nav and the velocity smoother's output onto /platform_velocity_controller_nav2/cmd_vel_unstamped, the topic that reaches the wheels. Point nav2CmdVelTopic at the smoothed topic to see what the wheels are told, or at /cmd_vel_nav to see what the MPPI controller asked for before smoothing. Both are unstamped there, so set nav2CmdVelFrame as well.
Candidate trajectory overlay
Candidate Trajectories draws every rollout the controller sampled, not just the one it chose — the spread of options behind each steering decision. MPPI publishes them on /trajectories, and the Navigation section lists one toggle per discovered topic, so hangar_sim shows Candidate Trajectories - /trajectories.
Unlike the other overlays, this one is off by default and subscribes only while checked. The controller builds the markers solely when something is listening, so an unchecked source costs nothing in the Desktop App or on the robot. Checking it is what makes the controller start publishing.
Each point is colored by its step along its rollout, so a trajectory reads from its start to its end, and the whole cloud is drawn as a single instanced mesh to keep thousands of points off the render path. With nav2's default sampling hangar_sim sends roughly 2000 points per message at 20 Hz; raising trajectory_step and time_step under FollowPath.TrajectoryVisualizer thins the cloud, and lowering them thickens it. At most 10,000 points per message are drawn, and the browser console warns once when a message exceeds that. The limit is applied while the message is still being decoded off the render thread, so a controller configured for full detail cannot stall the display no matter how many rollouts it samples.
The rollouts are stamped in the odometry frame and begin at the point the controller steers, which in hangar_sim is ridgeback_base_link — behind and below base_link — and they reach only as far as the controller's prediction horizon, under two metres at these speeds. On a robot of that size much of the cloud therefore lies beneath the chassis; look at it from above rather than from the side.
Particle filter overlay
When a localization node publishes a geometry_msgs/msg/PoseArray on a topic whose final segment contains "particle" (e.g. /particlecloud from nav2 AMCL, /particle_cloud from beluga, or namespaced variants like /robot_2/particle_cloud), the View menu lists it under Navigation as Particle Filter - topic. Enable the topic to overlay magenta arrows at every candidate pose. The arrow positions and headings update with the particle cloud, so the distribution spreads when localization uncertainty grows and contracts as the filter converges.
Controller Pipeline
At startup, joint_trajectory_controller (whole-body) is the active controller. When a navigation Objective runs, the SwitchController Behavior deactivates it and activates platform_velocity_controller_nav2. The Nav2 command pipeline then works as follows:
- MPPI controller (
FollowPath) — runs at 20 Hz, samples trajectory candidates and selects the one with the lowest cost against the costmaps - Velocity smoother — smooths the MPPI output to respect acceleration limits (max ±2.5 m/s²) and publishes it directly to
/platform_velocity_controller_nav2/cmd_vel_unstamped platform_velocity_controller_nav2— aclearpath_mecanum_drive_controllercontroller that converts the body-frame velocity command to individual wheel speeds for the four mecanum wheels
Running a whole-body arm Objective (for example, Teleoperate) switches back to joint_trajectory_controller, displacing platform_velocity_controller_nav2. Navigation Objectives re-activate it via SwitchController at the start of each run.
Localizing the Robot
AMCL starts at the robot's spawn pose. Run Localize Robot when the robot is moved without driving, or when the estimate is wrong:
- The marker starts above the robot. Move it to where the robot really is on the map, X axis forward, and confirm.
- The Objective seeds AMCL there with
SetInitialPose, then refines the estimate in place withCallEmptyServiceon/request_nomotion_update. - It keeps the result only if both lidar scans fit the map, as scored by
ScanMatchResidual. Otherwise it restores the previous estimate and fails.
Refine Localization In Place runs the same refinement from the current estimate. The navigation Objectives run it first, and stop if the scans do not fit the map.
The ScanMatchResidual threshold (min_inlier_fraction) depends on how well the map matches the environment. Measure it on your own map at known-good poses.
Navigating the Robot
Navigate to Clicked Point
The simplest navigation Objective. Select it from the Objectives panel to get started.

It then:
- Refines the AMCL estimate in place with
Refine Localization In Place, and stops if the lidar scans do not fit the map - Computes a single global path to the user-clicked goal using
ComputePathToPoseAction - Displays the planned path and waits for user confirmation with
WaitForUserPathApproval - Follows the approved path using
FollowPathAction

If an obstacle appears during execution, the robot may get stuck — there is no replanning, and the Objective will keep running until you click Stop Objective.
Navigate to Clicked Point with Replanning
For dynamic environments where obstacles may appear during navigation, use Navigate to Clicked Point with Replanning. This Objective delegates full navigation control to Nav2, which replans the path continuously as the local costmap updates.
See Navigate to a Goal with Replanning for a detailed walkthrough of how this Objective works, how Nav2's replanning Behavior Tree handles recovery, and when you may need to stop the Objective manually.