ExplorRobo

How Nav2 works: planners, controllers, costmaps and behavior trees

By ExplorRobo Editorial · · 11 min read

What Nav2 is

Nav2 is the navigation framework for ROS 2 mobile robots. The documentation describes it as a set of action servers that host maps of algorithm plugins and share an environmental representation, rather than a single monolithic node (navigation servers).

There is no one process that plans, drives and recovers. There is a behavior tree coordinator, planner server, controller server, smoother server, behavior server, waypoint follower and support nodes, each with its own action interface and parameter namespace.

If ROS 2 itself is new to you, start with What is ROS 2?. The action pattern that Nav2 leans on is covered in ROS 2 topics vs services vs actions.

Two loops and one coordinator

Navigation splits into two loops at different time scales.

The global loop answers which way to go. The planner server searches the global costmap in the map frame and returns a path.

The local loop answers what command to send now. The controller server follows that path on the local costmap in the odom frame, typically a rolling window around the robot.

The behavior tree decides when each loop runs and what to do when either fails. Recoveries, retries and replanning cadence are behavior tree logic, not controller internals.

The servers

The core nodes in a standard bringup, as listed by navigation_launch.py:

NodeJob
bt_navigatorHosts the NavigateToPose and NavigateThroughPoses actions and ticks the tree
planner_serverGlobal paths, global costmap
controller_serverVelocity commands, local costmap, goal and progress checkers
smoother_serverPath refinement
behavior_serverRecoveries: spin, backup, drive on heading, wait
waypoint_followerPose sequences with a task executor plugin
velocity_smootherVelocity, acceleration and deadband limits
collision_monitorStops or slows the robot from sensor polygons
route_serverRoutes over a navigation graph instead of free space
lifecycle_managerOrdered activation and bond monitoring

The planner, controller, smoother and behavior servers each hold a map of plugins, selected by alias. In nav2_params.yaml the planner is GridBased, mapped to nav2_navfn_planner::NavfnPlanner, and the controller is FollowPath, mapped to nav2_mppi_controller::MPPIController (params file). The controller server’s built in default, with no plugin list, is DWB instead.

The bringup loads these nodes into one component container with a single threaded executor, because use_composition defaults to true (bringup launch). Executor behavior is covered in ROS 2 executors and callback groups explained.

Costmaps are the shared world model

A costmap is a regular 2D grid of cells holding a cost: unknown, free, occupied or inflated. The global costmap is searched for a plan, the local costmap is sampled for control efforts (environmental representation).

Layers are plugins that write into the grid: static_layer loads the map from map_server, obstacle_layer marks obstacles from scans, voxel_layer marks 3D points from point clouds, and inflation_layer expands obstacles so the planner keeps clearance.

With no plugins override, the costmap loads static_layer, obstacle_layer and inflation_layer. The default local costmap uses voxel_layer and inflation_layer instead, since a rolling window does not need the static map.

The robot shape is a circle of radius robot_radius or a polygon given as footprint. The defaults are worth checking: robot_radius is 0.1 metres and track_unknown_space is false, so unknown cells count as free unless you change them (costmap parameters). An undersized footprint produces paths the robot cannot follow.

Costmap filters add keepout zones, speed limits and preferred lanes from a mask image, loaded through the filters list.

Behavior trees are the application logic

Nav2 uses BehaviorTree.CPP version 4. The bt_navigator loads an XML tree, and its nodes are plugins that call action servers (behavior trees).

The default navigate to pose tree explains observed robot behavior (default tree). It wraps navigation in a RecoveryNode with six retries. Inside, a PipelineSequence selects the checkers and plugins, then runs a RateController at 1 Hz around ComputePathToPose, followed by FollowPath.

If that pipeline fails, a RoundRobin branch clears the local costmap, clears the global costmap, spins 1.57 radians, waits 5 seconds, then backs up 0.30 metres at 0.15 metres per second. That is why a robot that cannot find a path sometimes spins in place before giving up. It is configured logic, not a fault.

The tree can be replaced per request. The NavigateToPose goal carries a PoseStamped and a behavior_tree string (action definition), while default_nav_to_pose_bt_xml sets the fallback and bt_loop_duration defaults to 10 milliseconds.

From goal to wheel command

Trace the command path once: most “the robot does not move” bugs sit somewhere on it.

  1. A client sends a goal to the navigate_to_pose action.
  2. bt_navigator ticks the tree, which calls ComputePathToPose and then FollowPath.
  3. The controller publishes cmd_vel, remapped to cmd_vel_nav; velocity_smoother publishes cmd_vel_smoothed.
  4. collision_monitor takes cmd_vel_smoothed and publishes cmd_vel to the base driver.

The remappings and the collision monitor topic names are visible in navigation_launch.py and the shipped parameters. When debugging, echo both cmd_vel_nav and cmd_vel: if the first has data and the second does not, the smoother or the collision monitor is stopping the robot.

Transforms and localization

Nav2 follows REP 105: map to odom comes from a global positioning source, odom to base_link from odometry, and sensor frames hang off base_link in the URDF (state estimation).

Nav2 ships AMCL, a particle filter for localizing in a static map, and uses SLAM Toolbox as its default SLAM algorithm. The docs are explicit that a LiDAR is not required: vision, depth or motion capture can provide the global transform, as long as the frame conventions hold.

In the bringup launch file, slam defaults to false and use_localization defaults to true, so the standard path is a map plus AMCL. Passing slam:=True includes slam_launch.py instead. The lifecycle manager waits for that transform chain before the stack activates, which is why the robot sits idle until you set an initial pose in RViz.

Running the reference example

The official quickstart installs the binaries and runs a simulated TurtleBot 3 (quickstart):

source /opt/ros/$ROS_DISTRO/setup.bash
sudo apt update
sudo apt install ros-$ROS_DISTRO-navigation2 ros-$ROS_DISTRO-nav2-bringup
ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False

Then set the approximate starting pose with the “2D Pose Estimate” button in RViz. Until that happens the transform tree is incomplete and Nav2 stays inactive. After that, send a goal with the “Navigation2 Goal” button, or check the stack state:

ros2 lifecycle get /bt_navigator

The goal itself is a PoseStamped plus a behavior tree string. The command line form takes YAML:

ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
  "{pose: {header: {frame_id: map}, pose: {position: {x: 1.0, y: 0.5}, orientation: {w: 1.0}}}, behavior_tree: ''}"

That shape comes from the action definition and the ros2 action send_goal interface, so check your own frames and topic names rather than copying the numbers.

Configuration, plugins and tuning

Parameters live under each node name in a YAML file passed with params_file. The bringup launches a lifecycle_manager with autostart true, which activates the servers in a fixed order. The launch namespace argument namespaces topics and frames, turning one stack into /robot1/navigate_to_pose without editing every parameter.

Tune in this order: TF tree and footprint, local costmap sensors and rates, controller and planner, then behavior tree recoveries once you know which failures are real.

The controller runs at 20 Hz by default and the local costmap updates at 5 Hz in the shipped parameters. That split between control rate and perception rate means obstacle information can be a fraction of a second old when a command is computed.

Pitfalls

No initial pose, no navigation. With AMCL the stack waits for an approximate starting position, so a healthy launch that never activates is often waiting for that pose.

The footprint is a safety parameter. The default radius of 0.1 metres is a placeholder. Set robot_radius or footprint to the real shape, including any manipulator that extends beyond the base.

Unknown space is free by default. With track_unknown_space: false, the planner may route through cells nobody has observed.

The local costmap needs rolling_window: true, otherwise it stays fixed in the odom frame instead of following the robot.

Simulation needs use_sim_time, because the bringup argument defaults to false and a simulated clock otherwise produces TF and sensor timing that will not line up.

Recoveries hide model errors. Spinning and clearing costmaps can make a robot look resilient while the real problem is a bad footprint or a stale map.

When Nav2 is not the right tool

Nav2 is built around a 2D environmental representation and a mobile base. For a manipulator, MoveIt 2 is the matching stack. For 3D flight, full 3D planning or high speed off road driving, the 2D costmap and the planar motion assumptions become limiting, and the docs list gradient maps, 3D costmaps and mesh representations as alternatives outside the current implementation.

Within its scope, the architecture is the point: the tree holds policy, servers hold resources, plugins hold algorithms. A failed path is a planner or costmap question, a robot that will not follow one is a controller or TF question, and a robot that gives up is a behavior tree question.


Sources

Frequently asked questions

What does the Nav2 bt_navigator actually do?
It hosts the NavigateToPose and NavigateThroughPoses action servers, loads a behavior tree XML file, and ticks that tree at a fixed loop duration. The tree nodes are thin plugins that call the planner, controller, smoother and behavior action servers. It holds application logic, not the algorithms.
Do I need a LiDAR to use Nav2?
No. The documentation states that there is no requirement to use a LiDAR for collision avoidance, localization or SLAM. What is required is a TF tree with map to odom to base_link, plus an environmental representation the planner and controller can search. Vision, depth cameras, radar and sonar are supported through costmap layers or alternative positioning.
What is the difference between the global and local costmap in Nav2?
The global costmap lives in the map frame, covers the known environment, and is searched by the planner server. The local costmap lives in the odom frame, normally uses a rolling window around the robot, and is used by the controller server for collision checking and local control. Both are layered costmaps built from plugins.
Why does Nav2 use lifecycle nodes?
The servers are managed lifecycle nodes so they can be configured, activated, deactivated and cleaned up in a defined order. The lifecycle manager takes an ordered list of names, transitions them one by one, and creates bond connections so a crashed server brings the stack down instead of leaving it with stale state.
Can I change the planning or control algorithm without forking Nav2?
Yes. The planner, controller, smoother and behavior servers load algorithms as pluginlib plugins, selected by alias in the parameter file. planner_plugins lists GridBased, and the GridBased namespace names nav2_navfn_planner::NavfnPlanner. Swapping planners means changing the plugin name and its parameters, not editing the server.

Related reading