Navigation · Navigation in the Real World · M9-L4 · ROS 2 project · Advanced
Bring Up Nav2 in Simulation
Everything you have tuned in the browser, running as a real navigation stack: servers, lifecycle, frames, a clock and a goal.
Run Nav2 on a simulated robot on your own machine, send a goal, and diagnose what you see.
The problem
The pilot at Meridian General is approved, and the first thing the integrator asks is a recorded run: Nav2 up in simulation, a goal sent from a script, and a written account of what the system did. Two weeks later the same people will ask you why a robot refused a goal in the pharmacy corridor.
You will bring up the packaged Nav2 and TurtleBot 3 simulation on your own machine, send a goal, record what you see, and then break the system in four controlled ways and diagnose each one from its symptoms and logs.
Expected behaviour. Every Nav2 server is active; the global costmap lives in map and the local one in odom; all Nav2 nodes use simulated time; a goal about a metre ahead is accepted and finishes with SUCCEEDED, with no recoveries in a clean run; and your worksheet holds a written diagnosis for each of the four experiments.
Observable failure. In the experiments: goals that are rejected or that stall, costmaps that never become current, "Timed out waiting for transform" messages, "Failed to make progress" from the controller, or a robot that moves much more slowly than the controller asks. Each has a first log line that names a component.
This is a real ROS 2 project, not a browser lab. You run it on your own machine with ROS 2, Nav2 and a simulator. The website does not run ROS 2 and does not grade it; you grade yourself with the criteria below and the
check.shscript.
Time: about 2 to 3 hours, plus installation · Level: Advanced · Theory: M9-T1: Nav2 Architecture, M9-T3: Frames, Timing and Simulation
What this project teaches
The browser labs of this module worked on pieces: a cost function, a parameter file, a behaviour tree. This project runs the whole stack. You will see that Nav2 is a set of lifecycle nodes (planner, controller, smoother, behaviour server, navigator, waypoint follower, velocity smoother) that cooperate through actions, topics and transforms, and that a goal travels from a script to a wheel through bt_navigator, planner_server, controller_server and the velocity chain. You will also see how much of the diagnosis comes from the same few questions: is everything active, are the frames and the clock consistent, does each component describe the same robot, and does each limit agree with the next.
The project uses the packaged simulation of the Nav2 tutorials, so every step here is a thin layer on top of the Nav2 documentation (the "Getting Started" and "First-Time Robot Setup Guide" pages at docs.nav2.org). Read those pages for your distribution first; they are the authority, and this page is a worksheet around them.
Prerequisites and setup
You need a computer with a ROS 2 distribution that Nav2 supports (for example Humble on Ubuntu 22.04 or Jazzy on Ubuntu 24.04), Nav2, and the TurtleBot 3 simulation of the Nav2 tutorials. ROS 2 distribution specifics differ: package names, the simulator, launch arguments and parameter names change between releases, so follow the Nav2 documentation for your distribution at docs.nav2.org for installation, and check each command here against it.
Get the project files: tools/navigation-nav2-bringup in the site’s repository (clone it, or copy the files listed below from this page).
- Install ROS 2 and Nav2 for your distribution by following the Nav2 documentation: "Getting Started" and "First-Time Robot Setup Guide". You need the Nav2 packages (
ros-<distro>-navigation2,ros-<distro>-nav2-bringup) and the TurtleBot 3 simulation packages that page names for your distribution. Confirm thatros2 launch nav2_bringup tb3_simulation_launch.pyopens the simulator and RViz before you use the files below. - Source your ROS 2 setup file in every terminal:
source /opt/ros/<distro>/setup.bash. Then open the project folder (cd tools/navigation-nav2-bringup). - Terminal 1:
python3 scripts/make_params.py sim_overrides --name lab, thenros2 launch launch/bringup_sim.launch.py. In RViz use "2D Pose Estimate" to place the robot, or letsend_goal.pypublish the pose. - Terminal 2:
python3 scripts/send_goal.py --goal 1.5 -0.5 0(choose a goal in free space in RViz) and./scripts/observe.sh baseline.
Verification status. The project files have not been executed by the site’s automated tests: the tests cannot run ROS 2, a simulator or Nav2, and the files were written against the documented interfaces without being run on a robot or simulator by their author. They follow the Nav2 documentation, but a launch argument, a parameter or a Python API may differ in your release. If something is rejected, the error names it: treat that as part of the exercise.
The project files
The launch file wraps the packaged simulation launch of nav2_bringup and only chooses the parameter file. The parameter file is not shipped as a whole: make_params.py merges small override files into the nav2_params.yaml installed with your release, because the nodes, plugins and parameter names in that file change between releases. The goal script uses nav2_simple_commander. The remaining files (README.md, CHECKLIST.md, WORKSHEET.md, the other fault files and observe.sh) are in the project folder.
launch/bringup_sim.launch.py
"""Bring up the packaged Nav2 + TurtleBot 3 simulation with a parameter file you choose.
This wraps `tb3_simulation_launch.py` from the `nav2_bringup` package, so the simulator, the robot, the map server,
AMCL, RViz and all Nav2 servers start exactly as in the Nav2 "Getting Started" tutorial. The only thing it adds is
a default `params_file` that points at the file generated by `scripts/make_params.py`.
NOT executed by the site's automated tests. The arguments of `tb3_simulation_launch.py` can change between Nav2
releases: run `ros2 launch nav2_bringup tb3_simulation_launch.py --show-args` and compare.
Usage (from this folder, after `python3 scripts/make_params.py sim_overrides --name lab`):
ros2 launch launch/bringup_sim.launch.py
ros2 launch launch/bringup_sim.launch.py params_file:=params/generated/nav2_params_faulty.yaml
ros2 launch launch/bringup_sim.launch.py use_sim_time:=false # experiment: wall time on a simulated clock
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
def generate_launch_description():
bringup_dir = get_package_share_directory('nav2_bringup')
here = os.path.dirname(os.path.abspath(__file__))
default_params = os.path.normpath(os.path.join(here, '..', 'params', 'generated', 'nav2_params_lab.yaml'))
args = [
DeclareLaunchArgument('params_file', default_value=default_params,
description='Nav2 parameter file (generate it with scripts/make_params.py)'),
DeclareLaunchArgument('use_sim_time', default_value='true',
description='Use the simulator clock (/clock). Leave true unless you are running an experiment.'),
DeclareLaunchArgument('headless', default_value='False',
description='False shows the simulator window; True runs it without one'),
DeclareLaunchArgument('use_rviz', default_value='True', description='Start RViz'),
]
nav2_sim = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(bringup_dir, 'launch', 'tb3_simulation_launch.py')),
launch_arguments={
'params_file': LaunchConfiguration('params_file'),
'use_sim_time': LaunchConfiguration('use_sim_time'),
'headless': LaunchConfiguration('headless'),
'use_rviz': LaunchConfiguration('use_rviz'),
}.items(),
)
return LaunchDescription(args + [nav2_sim])
scripts/make_params.py
#!/usr/bin/env python3
"""Build a Nav2 parameter file for this lab by merging small override files into the file that ships with your Nav2.
Why merge instead of shipping a whole file: the set of nodes, plugins and parameter names in nav2_params.yaml changes
between releases (Humble, Jazzy, ...). An override only touches the keys you name, and this script tells you which of
them do not exist in your release's default file, so a rename shows up as a message instead of a silent no-op.
NOT executed by the site's automated tests.
Examples (run from the project folder):
python3 scripts/make_params.py sim_overrides --name lab
python3 scripts/make_params.py sim_overrides inflation_too_large --name faulty
python3 scripts/make_params.py --base /path/to/my_nav2_params.yaml sim_overrides --name mine
Override names are looked up in params/ and params/faults/ (with or without .yaml). Needs PyYAML (python3-yaml).
"""
import argparse
import os
import sys
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
def default_base():
try:
from ament_index_python.packages import get_package_share_directory
return os.path.join(get_package_share_directory('nav2_bringup'), 'params', 'nav2_params.yaml')
except Exception as exc: # ROS 2 not sourced
sys.exit('Cannot find nav2_bringup (%s). Source your ROS 2 setup.bash, or pass --base FILE.' % exc)
def find_override(name):
for folder in ('params', os.path.join('params', 'faults')):
for suffix in ('', '.yaml'):
path = os.path.join(ROOT, folder, name + suffix)
if os.path.isfile(path):
return path
sys.exit('Override %r not found in params/ or params/faults/.' % name)
def merge(base, over, path, applied, skipped, allow_new):
"""Copy values from `over` into `base`. Only keys that already exist are changed unless allow_new is set."""
for key, value in over.items():
here = path + '.' + key if path else key
if isinstance(value, dict):
if key not in base:
if allow_new:
base[key] = value
applied.append((here, '(new section)', '...'))
else:
skipped.append(here + ' (section not in the default file)')
continue
merge(base[key], value, here, applied, skipped, allow_new)
elif key in base or allow_new:
applied.append((here, base.get(key, '(new)'), value))
base[key] = value
else:
skipped.append(here + ' (parameter not in the default file: renamed in your release?)')
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('overrides', nargs='*', help='override files to merge, in order')
ap.add_argument('--base', help='parameter file to start from (default: the one installed with nav2_bringup)')
ap.add_argument('--name', default='lab', help='output is params/generated/nav2_params_<name>.yaml')
ap.add_argument('--allow-new', action='store_true', help='also add keys that are missing from the base file')
args = ap.parse_args()
base_path = args.base or default_base()
with open(base_path) as f:
params = yaml.safe_load(f)
print('base file :', base_path)
applied, skipped = [], []
for name in args.overrides:
path = find_override(name)
print('override :', os.path.relpath(path, ROOT))
with open(path) as f:
merge(params, yaml.safe_load(f) or {}, '', applied, skipped, args.allow_new)
out_dir = os.path.join(ROOT, 'params', 'generated')
os.makedirs(out_dir, exist_ok=True)
out = os.path.join(out_dir, 'nav2_params_%s.yaml' % args.name)
with open(out, 'w') as f:
yaml.safe_dump(params, f, sort_keys=False)
print('\nchanged (%d):' % len(applied))
for key, old, new in applied:
print(' %s: %s -> %s' % (key, old, new))
if skipped:
print('\nSKIPPED (%d), these keys are not in your base file:' % len(skipped))
for s in skipped:
print(' ' + s)
print('\nwrote :', os.path.relpath(out, ROOT))
print('launch : ros2 launch launch/bringup_sim.launch.py params_file:=%s' % os.path.relpath(out, ROOT))
if __name__ == '__main__':
main()
scripts/send_goal.py
#!/usr/bin/env python3
"""Send one NavigateToPose goal with nav2_simple_commander and print what the navigator reports.
Run it in a terminal where ROS 2 is sourced and the bring-up (launch/bringup_sim.launch.py) is running.
NOT executed by the site's automated tests. The Simple Commander API can change between releases; see the Nav2
documentation ("Simple Commander API") for your distribution.
python3 scripts/send_goal.py # set the initial pose, then drive 1 m ahead of it
python3 scripts/send_goal.py --goal 1.5 -0.5 0 # a goal you chose in RViz (map frame: x y yaw in radians)
python3 scripts/send_goal.py --skip-initial-pose # you already set the pose with RViz "2D Pose Estimate"
Exit status: 0 when the goal succeeded, 1 when it failed or was cancelled, 2 on a timeout.
"""
import argparse
import math
import sys
import time
import rclpy
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
def make_pose(navigator, x, y, yaw, frame='map'):
pose = PoseStamped()
pose.header.frame_id = frame
pose.header.stamp = navigator.get_clock().now().to_msg()
pose.pose.position.x = float(x)
pose.pose.position.y = float(y)
pose.pose.orientation.z = math.sin(yaw / 2.0)
pose.pose.orientation.w = math.cos(yaw / 2.0)
return pose
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--initial', nargs=3, type=float, metavar=('X', 'Y', 'YAW'), default=[-2.0, -0.5, 0.0],
help='initial pose in the map frame (default: the usual spawn pose of the packaged TurtleBot 3 simulation; check yours)')
ap.add_argument('--goal', nargs=3, type=float, metavar=('X', 'Y', 'YAW'),
help='goal in the map frame (default: 1 m ahead of the initial pose, along x)')
ap.add_argument('--skip-initial-pose', action='store_true', help='do not publish an initial pose')
ap.add_argument('--timeout', type=float, default=120.0, help='cancel the goal after this many seconds')
args = ap.parse_args()
rclpy.init()
navigator = BasicNavigator()
goal_xyz = args.goal or [args.initial[0] + 1.0, args.initial[1], args.initial[2]]
if not args.skip_initial_pose:
navigator.setInitialPose(make_pose(navigator, *args.initial))
print('waiting for Nav2 to become active (lifecycle managers, AMCL)...')
navigator.waitUntilNav2Active()
goal = make_pose(navigator, *goal_xyz)
print('sending goal x=%.2f y=%.2f yaw=%.2f in the map frame' % tuple(goal_xyz))
navigator.goToPose(goal)
start = time.time()
last_print = 0.0
while not navigator.isTaskComplete():
feedback = navigator.getFeedback()
now = time.time() - start
if feedback and now - last_print >= 1.0:
last_print = now
eta = feedback.estimated_time_remaining.sec
print(' t=%5.1f s distance remaining %.2f m eta %d s recoveries %d' %
(now, feedback.distance_remaining, eta, feedback.number_of_recoveries))
if now > args.timeout:
print('timeout: cancelling the goal')
navigator.cancelTask()
rclpy.shutdown()
return 2
time.sleep(0.1)
result = navigator.getResult()
names = {TaskResult.SUCCEEDED: 'SUCCEEDED', TaskResult.CANCELED: 'CANCELED', TaskResult.FAILED: 'FAILED'}
print('result:', names.get(result, str(result)))
rclpy.shutdown()
return 0 if result == TaskResult.SUCCEEDED else 1
if __name__ == '__main__':
sys.exit(main())
params/sim_overrides.yaml
# Settings that match the simulated TurtleBot 3 Waffle (a differential-drive robot) and keep every speed limit consistent.
# Merged into the nav2_params.yaml of YOUR Nav2 release by scripts/make_params.py. Keys that your file does not have are
# reported and skipped, so a renamed parameter shows up as a message. Check the TurtleBot 3 and Nav2 documentation for
# the numbers; they are quoted here from the manufacturer's specification and may differ for your robot model.
velocity_smoother:
ros__parameters:
max_velocity: [0.26, 0.0, 1.0] # [x m/s, y m/s, theta rad/s]
min_velocity: [-0.26, 0.0, -1.0]
local_costmap:
local_costmap:
ros__parameters:
robot_radius: 0.22
global_costmap:
global_costmap:
ros__parameters:
robot_radius: 0.22
params/faults/inflation_too_large.yaml
# Experiment A. A "safer" inflation: a huge radius with a slow decay, in both costmaps.
# Predict first: what happens to a goal in a narrow gap, and to the cost on the middle of a corridor?
local_costmap:
local_costmap:
ros__parameters:
inflation_layer:
inflation_radius: 1.8
cost_scaling_factor: 1.0
global_costmap:
global_costmap:
ros__parameters:
inflation_layer:
inflation_radius: 1.8
cost_scaling_factor: 1.0
check.sh
#!/usr/bin/env bash
# Self-check for the Nav2 bring-up project. Run from this folder with the bring-up running and ROS 2 sourced.
# NOT executed by the site's automated tests: if a command is rejected by your release, treat that as part of the exercise.
HERE="$(cd "$(dirname "$0")" && pwd)"
PASS=0; FAIL=0
ok() { echo "PASS $1"; PASS=$((PASS+1)); }
bad() { echo "FAIL $1"; FAIL=$((FAIL+1)); [ -n "${2:-}" ] && echo " hint: $2"; }
check() { local d="$1" h="$2"; shift 2; if "$@" >/dev/null 2>&1; then ok "$d"; else bad "$d" "$h"; fi; }
for n in map_server amcl controller_server planner_server behavior_server bt_navigator; do
check "lifecycle: $n is active" "ros2 lifecycle get /$n ; read the lifecycle manager and the node's own log" \
bash -c "ros2 lifecycle get /$n | grep -q active"
done
check "bt_navigator uses simulated time" "ros2 param get /bt_navigator use_sim_time; launch with use_sim_time:=true" \
bash -c "ros2 param get /bt_navigator use_sim_time | grep -qi true"
check "the global costmap is in the map frame" "ros2 param get /global_costmap/global_costmap global_frame" \
bash -c "ros2 param get /global_costmap/global_costmap global_frame | grep -q map"
check "the local costmap is in the odom frame" "ros2 param get /local_costmap/local_costmap global_frame" \
bash -c "ros2 param get /local_costmap/local_costmap global_frame | grep -q odom"
check "map -> base_link transform exists" "is AMCL running and the initial pose set?" \
bash -c "timeout 10 ros2 run tf2_ros tf2_echo map base_link --ros-args -p use_sim_time:=true 2>&1 | grep -q Translation"
check "a velocity command topic has a publisher" "ros2 topic list | grep cmd_vel ; ros2 topic info <topic> -v" \
bash -c "for t in \$(ros2 topic list | grep cmd_vel); do ros2 topic info \$t | grep -q 'Publisher count: [1-9]' && exit 0; done; exit 1"
check "a goal 1 m ahead succeeds (send_goal.py exits 0)" "read the planner, controller and bt_navigator logs" \
python3 "$HERE/scripts/send_goal.py" --skip-initial-pose --timeout 90
echo; echo "$PASS passed, $FAIL failed"; [ "$FAIL" -eq 0 ]
What to do
Follow the loop of this track: Problem → Experiment → Diagnose → Fix → Evaluate → Understand.
- Problem and baseline. Bring the system up with the baseline file and send a goal. Use
CHECKLIST.md: confirm every server is active, draw the frame tree, read the costmap parameters, and trace the goal through the servers and topics. Record it as Run 0 inWORKSHEET.md. - Experiment A: inflation. Build a file with
inflation_too_large(python3 scripts/make_params.py sim_overrides inflation_too_large --name faulty), write your prediction, run the same goal, and diagnose. Compute the cost at the middle of a 0.9 m passage with the baseline and the faulty values (lesson M2-T2), and compare it with what you see in RViz. - Experiment B: footprint. Do the same with
footprint_mismatch. Which stage accepts a plan the next stage refuses? Compare with incident 1 of lab M1-L1. - Experiment C: velocity cap. Do the same with
speed_cap_too_low. Find the topic that carries the command and measure its rate and values, then find the controller setting that turns slow motion into an error and compute when it triggers. - Experiment D: the clock. Restart with
ros2 launch launch/bringup_sim.launch.py use_sim_time:=falseand the baseline file. Read the first transform error and compare the timestamps involved (lesson M9-T3). - Evaluate. With the baseline running, run
./check.sh. Then, for each experiment, name the cheapest check that would have caught it before the robot moved (a linter rule from lab M9-L2, a launch-time check, a line of the checklist) and finish the summary of the worksheet.
Expected output
Typical output when things are right. Exact numbers, timestamps, node lists and wording differ between machines and releases.
Lifecycle states of the servers
ros2 lifecycle get /controller_server active [3] ros2 lifecycle get /planner_server active [3] ros2 lifecycle get /bt_navigator active [3]
A goal that succeeds
python3 scripts/send_goal.py --goal 1.5 -0.5 0 waiting for Nav2 to become active (lifecycle managers, AMCL)... sending goal x=1.50 y=-0.50 yaw=0.00 in the map frame t= 1.0 s distance remaining 3.48 m eta 14 s recoveries 0 ... result: SUCCEEDED
The frame tree
ros2 run tf2_tools view_frames (writes frames.pdf) map -> odom -> base_footprint -> base_link -> (sensor frames) map -> odom is published by amcl
check.sh with a healthy baseline
PASS lifecycle: controller_server is active ... PASS the global costmap is in the map frame PASS the local costmap is in the odom frame PASS map -> base_link transform exists PASS a velocity command topic has a publisher PASS a goal 1 m ahead succeeds (send_goal.py exits 0) 10 passed, 0 failed
Troubleshooting
| Symptom | Likely cause | What to try |
|---|---|---|
package 'nav2_bringup' not found | ROS 2 is not sourced in this terminal, or Nav2 is not installed | source /opt/ros/<distro>/setup.bash; ros2 pkg list | grep nav2 |
| The simulator window is blank or crashes | Graphics acceleration in a virtual machine or container | Launch with headless:=True and use RViz only; see the simulator notes in the Nav2 documentation |
make_params.py lists skipped keys | Your release renamed or removed a parameter, or the file has a different structure | Open the generated file; find the current name in the Nav2 documentation of your distribution; edit the override file |
send_goal.py waits for ever at "waiting for Nav2 to become active" | AMCL has no initial pose, or a server is not active | Set the pose in RViz, or drop --skip-initial-pose; run ros2 lifecycle get on each server |
| The goal is rejected immediately | The navigator is not active, or the action name differs | ros2 action list; ros2 lifecycle get /bt_navigator; read the lifecycle manager log |
| The robot does not move although a plan appears | The velocity chain is broken (a remap, a smoother or a monitor that stops the command) | ros2 topic list | grep cmd_vel; ros2 topic info <topic> -v for each hop |
| Unknown launch argument | The arguments of tb3_simulation_launch.py differ in your release | ros2 launch nav2_bringup tb3_simulation_launch.py --show-args; edit the wrapper |
No module named nav2_simple_commander | The Simple Commander package is not installed or the environment is not sourced | Install the Nav2 Simple Commander package for your distribution and source ROS 2 |
Evaluation criteria
You are done when every criterion passes. check.sh automates most of them (run it from the project folder, with the bring-up running and ROS 2 sourced). The rest you judge yourself.
| Criterion | How to check | Passes when |
|---|---|---|
| Every Nav2 server is active | ros2 lifecycle get for each server | active for map_server, amcl, controller, planner, behavior and bt_navigator (check.sh) |
| Nav2 runs on simulated time | ros2 param get /bt_navigator use_sim_time | True, and the same on the other Nav2 nodes (check.sh for bt_navigator; self-assessed for the rest) |
| The costmaps are in the right frames | the global_frame parameters | global costmap in map, local costmap in odom (check.sh) |
| The robot is localized | tf2_echo map base_link | a transform is printed (check.sh) |
| The velocity command has a publisher | ros2 topic list | grep cmd_vel, ros2 topic info | at least one velocity topic has a publisher (check.sh) |
| A goal succeeds | send_goal.py | result SUCCEEDED with the robot at its start pose (check.sh sends a goal 1 m ahead of the default start pose) |
| The worksheet is complete | WORKSHEET.md | baseline plus four experiments, each with a prediction written before it ran (self-assessed) |
| You can trace a goal through the architecture | a diagram from memory | script → action → bt_navigator → planner_server → controller_server → velocity chain → base, with the names of the actions and topics (self-assessed) |
cd tools/navigation-nav2-bringup ./check.sh
Worked explanation
Try the experiments before opening this.
Show the diagnosis and the fix
These are the mechanisms, not the exact symptoms: your release, planner and controller plugin decide what you see. Compare with your own worksheet.
- A: inflation. With
robot_radius0.22 m, the cost at 0.45 m from a wall is trunc(252 · e−3 · (0.45 − 0.22)) = 126 with the baseline values (radius 0.55, scaling 3.0) and trunc(252 · e−1.0 · 0.23) = 200 with the experiment's (radius 1.8, scaling 1.0). A 0.9 m passage has its middle at 0.45 m from both walls, so it is much more expensive and the band now reaches 1.8 m into every room. Typical effects are routes that avoid narrow places that the robot fits through, slower motion where the controller scales speed by cost, and goals near walls that are hard to reach. Fix: set the radius a little beyond the inscribed radius plus the clearance you want, and choose the scaling so that the cost at the narrowest door's centre is low (lab M9-L1). - B: footprint. The global costmap describes a 0.22 m robot, the local one a 0.45 m robot. The planner accepts passages wider than about 0.44 m; the controller's costmap treats a passage as blocked below about 0.90 m. Where the two disagree the plan is followed up to the passage and then the controller reports that it cannot find a valid trajectory, or progress stalls and recoveries start. Fix: one robot description, shared by both costmaps.
- C: velocity cap. The controller asks for its normal speed, the velocity smoother limits it to 0.03 m/s, and the base moves at that speed. The progress checker's default requires about 0.5 m of movement within 10 s (check your values with
ros2 param get); 0.03 m/s × 10 s = 0.3 m, so the controller reports that it failed to make progress while the robot is moving, and the behaviour tree starts its recoveries. Fix: make the limits agree: the robot's real maximum, the controller's desired speed, the smoother's maximum. - D: the clock. The simulator publishes
/clockand stamps scans and transforms with simulated time, a few seconds or minutes since the start. Nodes withuse_sim_timefalse stamp and compare with the system clock, billions of seconds since 1970. Every lookup of a transform "at the time of this message" falls outside the buffer, so transform lookups fail or time out, costmaps stay stale, AMCL and the navigator cannot proceed, and goals are rejected or abort. Fix:use_sim_time:=trueon every node, from one place (lesson M9-T3).
Why it happens
Navigation is a system of agreements. The nodes agree on lifecycle state (everything active), on frames (REP 105: map, odom, base_link), on time (one clock), on the robot (one footprint), and on limits (a controller does not ask for what the smoother or the base will not give). A fault in any agreement shows up somewhere else: the log line that mentions it is rarely in the component that is wrong. The habit this project builds is to ask the same questions in the same order, to write the prediction before the experiment, and to turn each diagnosis into a check that runs before the robot moves (lessons M9-T1 to M9-T3, and the linters of this module).
What this project does not cover
- The packaged simulation is a small, static, noise-free world. Real robots have drifting odometry, sensor noise, people, and clocks that are not shared between computers.
- The fault files are deliberately simple and the symptoms depend on your release and plugins. If your system behaves differently from the explanation above, that is a result worth writing down, not a mistake.
- The launch wrapper forwards four arguments of
tb3_simulation_launch.py; other releases may need others. The scripts use the Simple Commander API of recent releases (BasicNavigator,goToPose,isTaskComplete). - The project files have not been executed by the site's automated tests, and nothing on this page runs ROS 2 in the browser. ROS 2 distribution specifics differ: check the Nav2 documentation for yours.
Educational simplification: the simulation is a small, static, noise-free world with one robot. Real deployments add sensor drift, people, bad wireless links and clocks that are not shared. The three fault files are deliberately simple, and the symptoms they cause depend on the controller and planner plugins of your release.
What’s next
Other labs in this module
- M9-L1: Tune the Inflation Radius
- M9-L2: Lint a Nav2 Parameter File
- M9-L3: Validate a Behavior Tree
- M9-L4: Bring Up Nav2 in Simulation: this project.