Skip to the project
FixTheRobot Explore Robotics / Navigation / M9 / M9-L4

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.sh script.

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).

  1. 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 that ros2 launch nav2_bringup tb3_simulation_launch.py opens the simulator and RViz before you use the files below.
  2. 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).
  3. Terminal 1: python3 scripts/make_params.py sim_overrides --name lab, then ros2 launch launch/bringup_sim.launch.py. In RViz use "2D Pose Estimate" to place the robot, or let send_goal.py publish the pose.
  4. 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.

  1. 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 in WORKSHEET.md.
  2. 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.
  3. 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.
  4. 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.
  5. Experiment D: the clock. Restart with ros2 launch launch/bringup_sim.launch.py use_sim_time:=false and the baseline file. Read the first transform error and compare the timestamps involved (lesson M9-T3).
  6. 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

Troubleshooting guide
SymptomLikely causeWhat to try
package 'nav2_bringup' not foundROS 2 is not sourced in this terminal, or Nav2 is not installedsource /opt/ros/<distro>/setup.bash; ros2 pkg list | grep nav2
The simulator window is blank or crashesGraphics acceleration in a virtual machine or containerLaunch with headless:=True and use RViz only; see the simulator notes in the Nav2 documentation
make_params.py lists skipped keysYour release renamed or removed a parameter, or the file has a different structureOpen 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 activeSet the pose in RViz, or drop --skip-initial-pose; run ros2 lifecycle get on each server
The goal is rejected immediatelyThe navigator is not active, or the action name differsros2 action list; ros2 lifecycle get /bt_navigator; read the lifecycle manager log
The robot does not move although a plan appearsThe 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 argumentThe arguments of tb3_simulation_launch.py differ in your releaseros2 launch nav2_bringup tb3_simulation_launch.py --show-args; edit the wrapper
No module named nav2_simple_commanderThe Simple Commander package is not installed or the environment is not sourcedInstall 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.

Evaluation criteria
CriterionHow to checkPasses when
Every Nav2 server is activeros2 lifecycle get for each serveractive for map_server, amcl, controller, planner, behavior and bt_navigator (check.sh)
Nav2 runs on simulated timeros2 param get /bt_navigator use_sim_timeTrue, and the same on the other Nav2 nodes (check.sh for bt_navigator; self-assessed for the rest)
The costmaps are in the right framesthe global_frame parametersglobal costmap in map, local costmap in odom (check.sh)
The robot is localizedtf2_echo map base_linka transform is printed (check.sh)
The velocity command has a publisherros2 topic list | grep cmd_vel, ros2 topic infoat least one velocity topic has a publisher (check.sh)
A goal succeedssend_goal.pyresult SUCCEEDED with the robot at its start pose (check.sh sends a goal 1 m ahead of the default start pose)
The worksheet is completeWORKSHEET.mdbaseline plus four experiments, each with a prediction written before it ran (self-assessed)
You can trace a goal through the architecturea diagram from memoryscript → 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.

  1. A: inflation. With robot_radius 0.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).
  2. 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.
  3. 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.
  4. D: the clock. The simulator publishes /clock and stamps scans and transforms with simulated time, a few seconds or minutes since the start. Nodes with use_sim_time false 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:=true on 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

Next

M10-L1: The Shift Simulator

Run a full compressed shift with your own planner, controller and recovery logic, scored on hidden shifts.