ROS2 Navigation Stack: Autonomous Mobile Robots
The Nav2 stack gives your robot the ability to navigate autonomously from point A to point B, avoiding obstacles along the way. Here's how to get started.
Telling a robot “go to the kitchen” sounds simple, but underneath that one command is a surprising amount of machinery: the robot has to know where it is, plan a route, follow that route smoothly, and react when a chair suddenly appears in its path. In the ROS2 world, the package that ties all of this together is Nav2 (the Navigation2 stack).
What Nav2 Is
Nav2 is the standard navigation framework for ROS2. It’s not a single algorithm but a collection of cooperating components that together turn a goal pose (“be at this X, Y, and orientation”) into a stream of velocity commands your robot’s motors can follow.
For a robot to navigate autonomously, it needs to answer four questions continuously:
- Where am I? (localization)
- Where do I want to go? (the goal)
- What’s the best route to get there? (global planning)
- How do I follow that route right now without hitting anything? (local control)
Nav2 provides a piece for each of these and orchestrates them so they work as one system.
The Navigation Pipeline
Here’s how the pieces fit together, in roughly the order data flows through them:
- A map: a pre-built representation of the environment, usually an occupancy grid (a 2D image where each cell is free, occupied, or unknown). You typically build this with SLAM ahead of time.
- Localization (AMCL): AMCL stands for Adaptive Monte Carlo Localization. It uses a “particle filter” — a cloud of guesses about where the robot might be — and matches the laser scan against the known map to figure out the robot’s true pose on that map.
- Global planner: given your current pose and a goal, it computes a full route across the map (think of it as the line a GPS draws). It plans on the global costmap.
- Local controller (trajectory follower): takes that global route and produces actual velocity commands, adjusting in real time to follow the path and dodge obstacles that aren’t on the map. It works against the local costmap.
- Costmaps: a costmap is the map annotated with “cost” — how dangerous it is to be in each cell. The global costmap covers the whole environment for route planning; the local costmap is a small rolling window around the robot for immediate reactions. Both apply an inflation layer, which grows obstacles outward by a safety margin so the planner keeps the robot’s body (not just its center point) away from walls.
- Recovery behaviors: when the robot gets stuck — path blocked, lost localization — Nav2 runs recovery actions like spinning in place, backing up, or clearing the costmaps, then tries again.
- Behavior trees: all of the above is coordinated by a behavior tree, a structured decision flow that decides “plan, then follow, and if that fails, recover, then retry.” This is what makes Nav2 configurable without rewriting code.
If that’s a lot to hold in your head, the diagram below shows the main flow: sensor data and the map feed the costmaps, the planner draws a route, the controller turns that route into motion, and the result is a stream of velocity commands (/cmd_vel) your motors follow.
<!-- costmaps -->
<rect class="d-fill-amber d-stroke-amber" stroke-width="2" x="200" y="75" width="130" height="48" rx="8"/>
<text class="d-label-bold" x="265" y="104" text-anchor="middle">Costmaps</text>
<!-- planner -->
<rect class="d-fill-mint-light d-stroke-mint" stroke-width="2" x="380" y="30" width="160" height="48" rx="8"/>
<text class="d-label-bold" x="460" y="54" text-anchor="middle">Global planner</text>
<text class="d-label-sm" x="460" y="70" text-anchor="middle">full route</text>
<!-- controller -->
<rect class="d-fill-mint-light d-stroke-mint" stroke-width="2" x="380" y="120" width="160" height="48" rx="8"/>
<text class="d-label-bold" x="460" y="144" text-anchor="middle">Local controller</text>
<text class="d-label-sm" x="460" y="160" text-anchor="middle">follows the route</text>
<!-- output -->
<rect class="d-fill-coral d-stroke-coral" stroke-width="2" x="380" y="200" width="160" height="42" rx="8"/>
<text class="d-label-bold" x="460" y="226" text-anchor="middle">/cmd_vel → motors</text>
<!-- arrows into costmaps -->
<path class="d-line" d="M150,54 H175 V92 H198" marker-end="url(#arrow-w18a)"/>
<path class="d-line" d="M150,144 H175 V106 H198" marker-end="url(#arrow-w18a)"/>
<!-- costmaps to planner & controller -->
<path class="d-line" d="M330,90 H355 V54 H378" marker-end="url(#arrow-w18a)"/>
<path class="d-line" d="M330,108 H355 V144 H378" marker-end="url(#arrow-w18a)"/>
<!-- planner to controller -->
<path class="d-line" d="M460,78 V120" marker-end="url(#arrow-w18a)"/>
<text class="d-label-sm" x="495" y="102" text-anchor="middle">path</text>
<!-- controller to cmd_vel -->
<path class="d-line" d="M460,168 V200" marker-end="url(#arrow-w18a)"/>
<defs>
<marker id="arrow-w18a" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">
<path d="M0,0 L9,4.5 L0,9 z" fill="#374151"/>
</marker>
</defs>
</svg>
Required Inputs
Nav2 will not work unless you feed it four things. Most “Nav2 won’t start” problems trace back to one of these being missing:
- A map of the environment (or SLAM running live).
- TF transforms: the coordinate-frame tree connecting
maptoodomtobase_linkto your sensor frames. We’ll cover TF2 in depth in Week 23 — for now, know that Nav2 is useless without it. - Odometry (
/odom): the robot’s estimate of its own motion from wheel encoders or similar. - A laser scan (
/scan): a 2D LiDAR feed Nav2 uses for localization and obstacle detection.
Installing Nav2
On ROS2 Humble (the release this series uses), install the stack and its bringup helpers:
sudo apt install ros-humble-navigation2 ros-humble-nav2-bringup
The Easiest Hands-On Path: TurtleBot3 Simulation
You don’t need a physical robot to try Nav2. The TurtleBot3 simulation is the canonical starting point because it comes pre-configured with everything Nav2 expects. Install it:
sudo apt install ros-humble-turtlebot3*
TurtleBot3 ships in several variants (burger, waffle, waffle_pi), so you have to tell the launch files which one you want by setting an environment variable:
export TURTLEBOT3_MODEL=waffle
Add that line to your ~/.bashrc so you don’t have to repeat it every terminal. Now launch the full simulation — Gazebo, the robot, a pre-built map, AMCL, and Nav2 — with a single command:
ros2 launch nav2_bringup tb3_simulation_launch.py
This opens Gazebo (the simulator) and RViz (the visualization tool where you’ll interact with Nav2).
Driving the Robot from RViz
When RViz first opens, the robot doesn’t know where it is — you’ll see the particle cloud scattered or the map misaligned. Two buttons in the RViz toolbar drive everything:
- Set the initial pose: click 2D Pose Estimate, then click on the map at the robot’s real location and drag in the direction it’s facing. This seeds AMCL so it can lock onto the correct position. The laser scan should snap into alignment with the map walls.
- Send a goal: click Nav2 Goal, then click anywhere reachable and drag to set the final orientation. Nav2 plans a path (you’ll see a line) and the robot starts driving, smoothly avoiding obstacles as it goes.
That’s the entire loop: localize once, then send goals as often as you like.
Troubleshooting
- The robot won’t localize / drives randomly: you almost certainly forgot to set the initial pose. Until you give AMCL a starting guess with 2D Pose Estimate, it has no idea where the robot is. Set it and watch the laser scan line up with the map.
- “No path found” or the robot refuses to move: this is usually a costmap issue. The goal may be inside an obstacle, or the inflation layer radius is so large that the planner thinks every route is blocked. Lower the inflation radius, or pick a goal with more clearance.
- TF errors like “Could not transform” or “frame does not exist”: the transform tree is incomplete or stale. Confirm
odom,base_link, and your laser frame are all connected and being published. Runros2 run tf2_tools view_framesto see the tree. (More on this in Week 23.) - AMCL drifts after a while: poor odometry or a map that doesn’t match reality. Try re-localizing, or rebuild the map with SLAM.
Once your robot can find its way across a known map, the natural next question is how to give it an arm to manipulate the world it’s navigating — next week we’ll dig into robot arm kinematics.