> Introduction
Autonomous navigation is one of the most fundamental capabilities any mobile robot must possess. The ROS2 Navigation Stack — commonly known as Nav2 — provides a production-grade, highly configurable framework for achieving exactly this. Whether you are building a warehouse AMR, a delivery robot, or a research platform, Nav2 gives you the building blocks to get from point A to point B reliably.
In this guide we walk through the entire Nav2 stack from the ground up. We will cover the costmap architecture, global and local planners, AMCL-based localization, lifecycle node management, and behavior trees that orchestrate the high-level navigation logic. By the end you will have a working understanding of every moving part and how to tune them for your specific robot.
// Table of Contents
> Costmaps: Global and Local
A costmap is a 2D grid where each cell holds a cost value representing how dangerous it is to place the robot's footprint there. Nav2 maintains two separate costmaps: a global costmap used by the planner and a local costmap used by the controller.
Costmap Layers
- • StaticLayer: Inflates obstacles from a static map
- • ObstacleLayer: Marks/clears dynamic obstacles from sensor data
- • InflationLayer: Spreads cost outward from lethal obstacles
- • VoxelLayer: 3D-aware obstacle marking using depth cameras
- • SpeedLimitLayer: Annotates regions with maximum speed constraints
global_costmap params (nav2_params.yaml)
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
robot_base_frame: base_link
use_sim_time: True
robot_radius: 0.22
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
observation_sources: scan
scan:
topic: /scan
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.55
The local costmap is typically smaller (3–5 m rolling window) and updates much faster (5–10 Hz) so the controller can react to dynamic obstacles in real time. Increase inflation_radius for wider robots and reduce cost_scaling_factor when corridors are narrow.
> Path Planners and Controllers
Nav2 separates path planning from path following. The planner computes a feasible global route; the controller drives the robot along that route while avoiding local obstacles.
Global Planners
- • NavFn (Dijkstra/A*): Classic grid-based planner
- • Smac Hybrid-A*: Kinematically feasible paths
- • Smac Lattice: For Ackermann-steered robots
- • Theta*: Any-angle path planning
Local Controllers
- • DWB (Dynamic Window): Samples velocity space
- • TEB (Timed Elastic Band): Trajectory optimization
- • MPPI: Model Predictive Path Integral
- • RPP (Regulated Pure Pursuit): Simple and robust
Configuring Smac Hybrid-A* Planner
planner_server:
ros__parameters:
expected_planner_frequency: 20.0
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_smac_planner/SmacPlannerHybrid"
downsample_costmap: false
downsampling_factor: 1
tolerance: 0.25
allow_unknown: true
max_iterations: 1000000
max_on_approach_iterations: 1000
max_planning_time: 5.0
motion_model_for_search: "REEDS_SHEPP"
angle_quantization_bins: 72
analytic_expansion_ratio: 3.5
minimum_turning_radius: 0.40
reverse_penalty: 2.0
change_penalty: 0.05
non_straight_penalty: 1.20
cost_penalty: 2.0
lookup_table_size: 20.0
cache_obstacle_heuristic: false
smooth_path: True
> AMCL Localization
Adaptive Monte Carlo Localization (AMCL) estimates the robot's pose within a known map by maintaining a particle filter. Each particle is a hypothesis about where the robot might be; particles that match sensor observations survive while inconsistent ones are pruned.
Key AMCL Parameters
amcl:
ros__parameters:
use_sim_time: True
alpha1: 0.2 # rotation noise from rotation
alpha2: 0.2 # rotation noise from translation
alpha3: 0.2 # translation noise from translation
alpha4: 0.2 # translation noise from rotation
alpha5: 0.2 # strafe noise (holonomic only)
base_frame_id: "base_footprint"
beam_skip_distance: 0.5
beam_skip_error_threshold: 0.9
beam_skip_threshold: 0.3
do_beamskip: false
global_frame_id: "map"
lambda_short: 0.1
laser_likelihood_max_dist: 2.0
laser_max_range: 100.0
laser_min_range: -1.0
laser_model_type: "likelihood_field"
max_beams: 60
max_particles: 2000
min_particles: 500
odom_frame_id: "odom"
pf_err: 0.05
pf_z: 0.99
recovery_alpha_fast: 0.0
recovery_alpha_slow: 0.0
resample_interval: 1
robot_model_type: "nav2_amcl::DifferentialMotionModel"
save_pose_rate: 0.5
sigma_hit: 0.2
tf_broadcast: true
transform_tolerance: 1.0
update_min_a: 0.2
update_min_d: 0.25
z_hit: 0.5
z_max: 0.05
z_rand: 0.5
z_short: 0.05
scan_topic: scan
Initial Pose Estimation
AMCL requires a rough initial pose. Publish it programmatically or via RViz2:
import rclpy
from geometry_msgs.msg import PoseWithCovarianceStamped
rclpy.init()
node = rclpy.create_node('pose_publisher')
pub = node.create_publisher(
PoseWithCovarianceStamped, '/initialpose', 10)
msg = PoseWithCovarianceStamped()
msg.header.frame_id = 'map'
msg.pose.pose.position.x = 0.0
msg.pose.pose.position.y = 0.0
msg.pose.pose.orientation.w = 1.0
# Covariance: [xx, xy, xθ, yx, yy, yθ, θx, θy, θθ ...]
msg.pose.covariance[0] = 0.25 # x variance
msg.pose.covariance[7] = 0.25 # y variance
msg.pose.covariance[35] = 0.0685 # yaw variance
pub.publish(msg)
node.destroy_node()
rclpy.shutdown()
> Lifecycle Nodes and the Nav2 Bringup
ROS2 lifecycle nodes introduce a managed node state machine with well-defined transitions: Unconfigured → Inactive → Active → Finalized. Nav2 uses lifecycle nodes throughout so the system can be brought up and torn down deterministically, which is critical for safety-critical applications.
Lifecycle State Machine
- • configure(): Allocate resources, load parameters, set up publishers/subscribers
- • activate(): Start processing data, publish transforms
- • deactivate(): Stop processing but keep resources allocated
- • cleanup(): Release all resources, return to Unconfigured
- • shutdown(): Terminate the node entirely
Nav2 Lifecycle Manager Configuration
lifecycle_manager:
ros__parameters:
use_sim_time: True
autostart: True
bond_timeout: 4.0
attempt_respawn_reconnection: true
bond_respawn_max_duration: 10.0
node_names:
- map_server
- amcl
- planner_server
- smoother_server
- controller_server
- behavior_server
- bt_navigator
- waypoint_follower
- velocity_smoother
> Behavior Trees for Navigation
Nav2's bt_navigator executes behavior trees (BTs) defined as XML files. BTs give you a modular, readable way to encode navigation logic including retries, recovery behaviors, and goal checking — all without tangled state-machine code.
Default Navigate-to-Pose BT (simplified)
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathRecovery">
<ComputePathToPose goal="{goal}"
path="{path}"
planner_id="GridBased"/>
<ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPathRecovery">
<FollowPath path="{path}"
controller_id="FollowPath"/>
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</PipelineSequence>
<Sequence name="RecoverySequence">
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap service_name="global_costmap/clear_entirely_global_costmap"/>
<Spin spin_dist="1.57"/>
<Wait wait_duration="5"/>
<BackUp backup_dist="0.30" backup_speed="0.05"/>
</Sequence>
</RecoveryNode>
</BehaviorTree>
</root>
The tree above first tries to compute a path and follow it. If following fails it clears costmaps. If planning fails it spins in place or backs up. This entire behavior is hot-swappable — just point bt_navigator at a different XML file.
> Real-World Examples and Tuning
Configuration is half the battle. Here are concrete tuning tips gathered from deploying Nav2 on actual hardware.
Sending Goals Programmatically
from nav2_simple_commander.robot_navigator import BasicNavigator
from geometry_msgs.msg import PoseStamped
import rclpy
rclpy.init()
navigator = BasicNavigator()
navigator.waitUntilNav2Active()
goal = PoseStamped()
goal.header.frame_id = 'map'
goal.header.stamp = navigator.get_clock().now().to_msg()
goal.pose.position.x = 3.0
goal.pose.position.y = 1.5
goal.pose.orientation.w = 1.0
navigator.goToPose(goal)
while not navigator.isTaskComplete():
feedback = navigator.getFeedback()
print(f'Distance remaining: {feedback.distance_remaining:.2f} m')
result = navigator.getResult()
print(f'Navigation result: {result}')
Common Tuning Issues
- • Robot oscillates: Reduce DWB
max_vel_thetaor increasegoal_dist_tol - • Planner times out: Increase
max_planning_timeor reduce inflation radius - • Ghost obstacles: Tune
obstacle_layer.track_unknown_spaceand sensor clearing params - • AMCL drifts: Increase
max_particlesand verify odometry covariance - • Costmap lag: Synchronize sensor timestamps with
transform_tolerance
Waypoint Following with Intermediate Tasks
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
from geometry_msgs.msg import PoseStamped
waypoints = []
for (x, y, yaw) in [(1.0, 0.0, 0.0), (2.0, 1.0, 1.57), (0.0, 2.0, 3.14)]:
p = PoseStamped()
p.header.frame_id = 'map'
p.header.stamp = navigator.get_clock().now().to_msg()
p.pose.position.x = x
p.pose.position.y = y
import math
p.pose.orientation.z = math.sin(yaw / 2)
p.pose.orientation.w = math.cos(yaw / 2)
waypoints.append(p)
navigator.followWaypoints(waypoints)
while not navigator.isTaskComplete():
pass
if navigator.getResult() == TaskResult.SUCCEEDED:
print('All waypoints reached!')
> Conclusion
Nav2 is a mature, extensible stack that handles the hard problems of autonomous navigation — costmapping, path planning, trajectory control, localization, and recovery — so you can focus on your application. The key to success is understanding which component to tune when things go wrong.
Start with the default parameters, profile your robot in simulation, and iterate. The behavior tree abstraction makes it easy to add custom logic without touching the core navigation code.
Ready to Navigate?
Clone the nav2_bringup repository, launch the Turtlebot3 simulation, and start sending goals via the simple commander API. The community Discord and GitHub Discussions are great resources when you get stuck.