nav2
The nav-vertical core tool skill for robium: bringup, the BT Navigator and
behavior trees, costmap layers, the planner/controller/smoother servers,
localization (AMCL and slam_toolbox), waypoint following, and tuning a
running stack. Nav2 config in this skill targets ROS 2 Jazzy Jalisco
(LTS, supported to May 2029) rather than Lyrical Luth, the current
overall LTS the ros2 skill defaults to — Nav2 has not yet shipped binary
packages for Lyrical (tracked in ros-navigation/navigation2#6123 as of
2026-07). Gazebo Harmonic is Jazzy's paired simulator. Re-check that gap
before starting a new project, and treat picking the distro itself as
architect's call, not this skill's — load this skill once architect has
routed you to the navigation vertical.
When to use this skill
- Any autonomous mobile-robot navigation task: bringing up Nav2, tuning
costmaps or the planner/controller, adding or debugging a behavior tree,
choosing between AMCL and slam_toolbox, following waypoints.
- The trigger phrases in the description: 'navigation', 'nav2', 'costmap',
'path planning', "robot won't move to goal", 'localization', 'SLAM',
'AMCL', 'waypoint'.
- A robot receives a goal and never moves, or moves erratically — start here
(see
references/common-failures.md), not in application logic.
- Cross-references — go to the sibling skill instead when the question is:
- ROS 2 substrate this stack runs on (workspaces, colcon, packages, nodes,
QoS, launch syntax, TF2 concepts/broadcasters) →
ros2. This skill's
only TF responsibility is verifying the map→odom→base_link chain exists
and is current before tuning anything else; teaching TF2 itself stays in
ros2 — see that skill's interfaces-and-qos and debugging references.
- Simulating the robot Nav2 drives → the
gazebo skill.
- Visualizing costmaps, TF, or BT execution (RViz2, Foxglove) → the
visualization skill.
- Arm/manipulation tasks, learned policies →
lerobot. Nav2 is mobile-base
navigation only.
- Environment/Docker setup for the ROS 2 + Nav2 + Gazebo stack →
environments.
- The whole-stack decision this feeds into →
architect (routes here).
Key directives
- Delegation posture: embed + links. The navigation-specific concepts
(BT Navigator, costmap layers, server roles, AMCL vs slam_toolbox) live in
this skill and its references in depth, because no single upstream page
covers them as a coherent whole for a new project — but every parameter
table and default value is a link back to docs.nav2.org or the
navigation2 GitHub repo, not re-typed from memory. See References.
- Start from the official minimal config, change one subsystem at a
time.
examples/nav2-params-diffdrive.yaml is adapted from
nav2_bringup's own nav2_params.yaml (jazzy branch) — begin a new robot
there, verify it navigates, then change exactly one subsystem (footprint,
controller plugin, costmap layer) before touching the next. Changing
costmap, controller, and planner parameters simultaneously makes a
regression impossible to bisect.
use_sim_time must be consistent across every node, every time. In
simulation, every Nav2 node, the map/odom TF broadcasters, and the sim
clock source must all agree on use_sim_time: true (or all agree on
false on real hardware) — one node left on the wrong value produces TF
extrapolation errors and rejected goals that look like a planning bug but
are a clock mismatch. Set it once, globally, in the params file passed to
every node (see examples/nav2-params-diffdrive.yaml and
examples/bringup-launch-snippet.py), not per-node.
- Verify the TF tree — map→odom→base_link — before tuning anything
else. Costmap2D blocks activation until a full TF tree is available, and
AMCL will not publish
map→odom until it has an initial pose (from
RViz's "2D Pose Estimate" or the /initialpose topic). A robot that
"won't move" is, more often than a bad planner or controller parameter, a
broken or incomplete TF chain — check this first with ros2 run tf2_ros tf2_echo map base_link every time, before touching costmap or controller
tuning. See references/common-failures.md.
- Never write Nav2 parameter defaults, plugin names, or version/status
claims from memory. They change release to release (the controller
default alone has moved from DWB to MPPI upstream). Verify against
docs.nav2.org or the
navigation2 GitHub repo before repeating a claim in
a real project — every example in this skill is marked status: unverified for exactly this reason, and each reference states how its
claims were checked on 2026-07-10.
Quick start
1. Confirm the ROS 2 substrate is ready. A sourced Jazzy workspace with
Nav2 installed (sudo apt install ros-jazzy-navigation2 ros-jazzy-nav2-bringup
— re-verify the package name against docs.nav2.org's install page before
running it) — see the ros2 skill if the workspace itself isn't set up yet.
2. Bring up Nav2 with the example config. Copy
examples/nav2-params-diffdrive.yaml and examples/bringup-launch-snippet.py
into your project, keeping the params filename the launch snippet expects
(or updating both together — see Customization), then:
ros2 launch ./bringup-launch-snippet.py map:=/path/to/your_map.yaml use_sim_time:=true
3. Verify before tuning. Confirm every managed node is active
(ros2 lifecycle get /controller_server etc.) and the TF chain is complete
(ros2 run tf2_ros tf2_echo map base_link) — see
references/common-failures.md if either check fails.
4. Send a goal through RViz2's "Nav2 Goal" tool, or programmatically —
see the "send goals programmatically" usage pattern below.
Usage patterns
Bringup with an existing map. Pass a saved map YAML and leave slam
false — Nav2 launches nav2_map_server + nav2_amcl for localization
against that static map. Set the robot's initial pose (RViz "2D Pose
Estimate" or publish to /initialpose) immediately after launch; AMCL does
not publish map→odom until it has one. See
examples/bringup-launch-snippet.py (map:= argument) and
references/nav2-architecture.md's localization section.
SLAM-then-navigate. Pick exactly ONE of two combinations — on Jazzy
(observed 2026-07-10, nav-trial) nav2_bringup's slam:=True ALREADY
launches its own online_sync_launch.py, so also including slam_toolbox's
online_async_launch.py alongside spawns two slam_toolbox nodes whose
lifecycle managers fight and every goal fails (0/9: "Unable to start
transition 1 from current state inactive", "Timed out while waiting for
action server to acknowledge goal request for compute_path_to_pose").
Either (a) bring up with slam:=True and NO extra slam include, or (b)
launch navigation_launch.py plus your own online_async_launch.py — never
both. Either way pass no map:= argument; a SLAM node publishes /map and
the map→odom transform in place of nav2_map_server/nav2_amcl.
slam_toolbox's Jazzy online_async already ships base_frame: base_footprint and scan_topic: /scan — only use_sim_time: true needs
adding, and any guidance to edit base_frame is stale for Jazzy. Drive the
robot to explore, then save the resulting map with nav2_map_server's
map_saver_cli (see
map_saver's params in examples/nav2-params-diffdrive.yaml) once mapping
is done, so the next run can go back to AMCL-on-a-fixed-map. Mind the frame
convention: the SLAM map frame's origin is the robot's mapping start
pose, not the world/sim origin — goals written in world coordinates are
silently offset by the spawn pose, and the saved map inherits the same
origin (convert goal_map = goal_world − start_pose, or pick goals off the
live map in a viewer). Verified 2026-07-11 (nav-trial). See
references/nav2-architecture.md.
Launch Nav2 servers directly (without nav2_bringup's launch). Three
Jazzy-verified reasons a project outgrows bringup_launch.py (all hit in
one real build, 2026-07-11 nav-trial): slam:=True starts its own
synchronous slam_toolbox, so also launching online_async_launch.py
alongside yields two SLAM nodes; navigation_launch.py hard-codes its
lifecycle-manager params, so bond_timeout can't be adjusted; and a params
file containing $(find-pkg-share ...) substitutions reaches the nodes as
literal strings. When launching servers as plain Nodes yourself: wrap the
params file in launch_ros.parameter_descriptions.ParameterFile(path, allow_substs=True), replicate navigation_launch.py's remappings (the
cmd_vel → cmd_vel_nav → smoother chain), and list every server in your
own lifecycle manager — set bond_timeout: 0.0 on it (the only way to
change it, since navigation_launch.py hard-codes the value; see the
Docker-stall gotcha). To override a single param the ParameterFile gets
wrong (e.g. TB3 burger.yaml's relative yaml_filename: "map.yaml", which
makes map_server's LoadMap fail), append a plain dict AFTER the
ParameterFile in the node's parameters= list — later entries win:
parameters=[ParameterFile(params_yaml, allow_substs=True), {'yaml_filename': abs_map_path}]. yaml_filename fix observed 2026-07-10 (nav-trial).
Send goals programmatically. Use nav2_simple_commander's
BasicNavigator Python class rather than hand-rolling NavigateToPose
action clients: goToPose() / goThroughPoses() for single/multi-pose
goals, followWaypoints() for a waypoint list, and non-blocking
isTaskComplete()/getResult() polling for feedback in a single-threaded
script. See references/nav2-architecture.md's commander-API section for a
minimal snippet shape.
Tune for a new robot footprint/speed. Start from
examples/nav2-params-diffdrive.yaml's local_costmap/global_costmap
robot_radius (switch to an explicit footprint polygon for a non-circular
base), then the controller's velocity/acceleration limits and
velocity_smoother's max_velocity/max_accel/max_decel — change these
before touching planner or BT internals, since a wrong footprint or speed
limit makes every downstream navigation attempt look broken. See
references/tuning-guide.md.
Author a custom global planner plugin.
A global-planner plugin subclasses nav2_core::GlobalPlanner and
implements configure(parent, name, tf, costmap_ros) / cleanup() /
activate() / deactivate() / createPlan(...) — the costmap_ros
argument handed to configure() is how the plugin reaches the same
costmap references/nav2-architecture.md's planner-server section
already documents. Registration needs exactly two pieces, not three:
PLUGINLIB_EXPORT_CLASS(<YourClass>, nav2_core::GlobalPlanner) in the
.cpp, and the CMakeLists.txt's
pluginlib_export_plugin_description_file(nav2_core <plugin>.xml)
call, which installs the plugin-description XML and writes the
ament-index marker pluginlib's ClassLoader queries at runtime — a
package.xml <export> tag for the same XML is a ROS 1 carryover
some tutorials still ship and is not load-bearing in ROS 2, per
search-synthesis of ament_cmake/pluginlib docs (re-verify on next docs
pass). Treat the exact configure()/createPlan() parameter list as
a conceptual pattern, not a pinned signature — confirmed 2026-08-02
against the rolling-branch navigation2_tutorials repo (no
Jazzy/Humble/Iron branch exists there to check directly); re-verify
against this skill's target distro (Jazzy Jalisco) before writing a
plugin from it.
Author a custom costmap layer plugin.
A costmap layer plugin subclasses nav2_costmap_2d::Layer and
overrides onInitialize() (parameter declaration + one-time state),
updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y) (grows the costmap's dirty-bounds window), and
updateCosts(master_grid, min_i, min_j, max_i, max_j) (writes cost
values into that window) — plus reset(), onFootprintChanged(), and
isClearable(). Registration mirrors the planner-plugin pattern's
load-bearing half only (see the custom-global-planner-plugin pattern
above): PLUGINLIB_EXPORT_CLASS(<YourClass>, nav2_costmap_2d::Layer)
in the .cpp, paired with CMakeLists.txt's
pluginlib_export_plugin_description_file(nav2_costmap_2d <layer>.xml) — the macro's first argument is nav2_costmap_2d here,
not nav2_core as in the planner case; it is pluginlib's
resource-index key, not a string a params file's plugins: list
names directly. Cross-references references/nav2-architecture.md's
Costmap 2D section (which names the shared layer types — StaticLayer,
ObstacleLayer, VoxelLayer, InflationLayer — but not how to author a
new one). Confirmed 2026-08-02 against the rolling-branch
navigation2_tutorials repo; the Layer virtual-method set has been
stable across recent distros per search-synthesis, but re-verify
before absorbing a parameter list verbatim.
Platform gotchas
- Jazzy, not Lyrical, until Nav2 ships Lyrical binaries. See the intro
paragraph above; this is a binding, repo-wide fact (
architect and
ros2 both reference it) — don't silently "upgrade" a nav2 project to
Lyrical without re-checking ros-navigation/navigation2#6123 first.
- AMCL is silent, not erroring, without an initial pose. A freshly
launched AMCL-based stack with no
/initialpose published will sit idle —
no error, just no map→odom transform and a costmap that never
activates. This looks identical to a hung launch; check for a missing
initial pose before debugging anything else. Between bringup and the first
goal, global_costmap and AMCL spam transform/pose warnings every
~0.5–2 s ("Timed out waiting for transform from base_link to map ...
Invalid frame ID map", "AMCL cannot publish a pose ... Please set the
initial pose") — this is benign, the map frame doesn't exist until AMCL
gets its initial pose, so don't chase it. See
references/common-failures.md.
- Composed (
use_composition:=true) vs standalone nodes change crash
behavior. nav2_bringup defaults to component-container composition; a
crash inside one composed node can take down the whole container process,
whereas standalone nodes (use_composition:=false, with
use_respawn:=true) restart independently. Prefer standalone + respawn
while iterating on a new robot; composition is a later performance
optimization, not a default to fight while still debugging.
/cmd_vel may be TwistStamped, not Twist. Modern gz robot
integrations (TB3 on Jazzy among them) subscribe TwistStamped; a plain
Twist publisher never matches — no error, ros2 topic pub just waits
forever for a matching subscription — and the robot silently ignores
Nav2. Check with ros2 topic info -v /cmd_vel, and set
enable_stamped_cmd_vel: true in every cmd_vel-publishing section
(controller_server, velocity_smoother, behavior_server,
collision_monitor, docking_server). TB3 Jazzy's burger.yaml already
pre-sets enable_stamped_cmd_vel: true in all five of those sections, so
this trap only bites configs started from nav2_bringup's nav2_params.yaml.
Verified 2026-07-11 (nav-trial).
- On TB3 Jazzy, start from
turtlebot3_navigation2's burger.yaml, not
nav2_bringup's nav2_params.yaml. It is TB3-tuned and carries 13 of
nav2_bringup's 14 sections; it ships no use_sim_time keys and relies on
RewrittenYaml to inject them. Observed 2026-07-10 (nav-trial).
- TB3
burger.yaml's collision_monitor source_timeout is too tight
for its own lidar. The scan source ships source_timeout: 0.2, but the
burger lidar publishes at 5 Hz (a 0.2 s period), so ordinary jitter makes
collision_monitor reject the source and zero cmd_vel — the robot won't
move even with a healthy stack ("Latest source and current collision
monitor node timestamps differ on 0.2xx seconds. Ignoring the source.",
"Robot to stop due to invalid source"). Set source_timeout above the
sensor period (1.0 worked). Observed 2026-07-10 (nav-trial).
- On Docker Desktop/macOS the container stalls ~8 s at Nav2 activation and
the first goal, so the default
bond_timeout: 4.0 self-destructs the
stack ("CRITICAL FAILURE: SERVER controller_server IS DOWN after not
receiving a heartbeat for 4000 ms"). Jazzy's navigation_launch.py
hard-codes the lifecycle_manager params, so bond_timeout cannot be set
through params_file/RewrittenYaml; the only fix is to launch the
servers under your own lifecycle_manager node with bond_timeout: 0.0
(see the direct-server-launch usage pattern). Observed 2026-07-10
(nav-trial).
- In live SLAM, an unreachable goal corrupts the map. A goal in unknown
space that fails to plan triggers a spin/backup recovery loop that smears
the live SLAM map until every subsequent plan fails ("Failed to create
plan with tolerance of: 0.500000", "Goal ... was outside bounds"). Check
waypoints against actual world geometry and keep them ≥0.4 m from
obstacles — e.g.
turtlebot3_world has pillars of r=0.15 at {-1.1, 0,
1.1}² (grep the SDF pillar poses); an octagon route of r=1.7 lands inside
them. Observed 2026-07-10 (nav-trial).
- Gazebo Harmonic's
/clock must actually be publishing before any node
with use_sim_time:=true will progress — a paused or not-yet-started
Gazebo world leaves every Nav2 node waiting on TF timestamps that never
arrive, which looks like a Nav2 hang rather than a sim issue.
Customization
- Different robot footprint or drive type: swap
robot_radius for an
explicit footprint polygon in both local_costmap and global_costmap
in examples/nav2-params-diffdrive.yaml, and change FollowPath's
motion_model (e.g. "DiffDrive" → "Omni") if the base isn't
differential-drive — see references/tuning-guide.md.
- Different controller/planner plugin: the params file's
controller_server.FollowPath.plugin and
planner_server.GridBased.plugin fields select the algorithm; swapping
requires the matching plugin name and its own parameter block (e.g.
nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController or
nav2_smac_planner::SmacPlannerHybrid) — verify exact plugin/class names
against docs.nav2.org's configuration guide before writing them, they
are not interchangeable strings. See references/tuning-guide.md.
- Different params filename or launch structure:
examples/bringup-launch-snippet.py's params_file default and
examples/nav2-params-diffdrive.yaml's own filename must be kept in sync
if you rename either — the launch snippet resolves the params path
relative to itself, so a silent rename of one without the other produces a
"params file not found" failure at launch, not a subtle runtime bug.
- Reverting to Lyrical Luth: once Nav2 ships Lyrical binaries (re-check
ros-navigation/navigation2#6123), swap jazzy for lyrical in every
install command and Docker base image in the environments skill's
Dockerfile.ros2 example; nothing in this skill's params/launch content itself is
distro-specific beyond the install step.
References
references/nav2-architecture.md — the BT Navigator and default behavior
trees, the planner/controller/smoother/behavior/waypoint-follower servers,
costmap 2D layers (global vs local), the lifecycle manager, AMCL vs
slam_toolbox, and the nav2_simple_commander API.
references/tuning-guide.md — costmap resolution/update-rate/inflation
tuning, footprint vs radius, controller/planner plugin selection,
velocity/acceleration limits, and the "one subsystem at a time" workflow.
references/common-failures.md — the "robot won't move" diagnostic
checklist: lifecycle state, TF tree, use_sim_time consistency, costmap
obstacle sourcing, goal rejection, and cmd_vel not reaching the base.
examples/nav2-params-diffdrive.yaml — adapted from nav2_bringup's
official minimal diff-drive nav2_params.yaml (status: unverified — file
header states the exact source and the deviations made).
examples/bringup-launch-snippet.py — a project launch file that includes
nav2_bringup's own bringup_launch.py, pointed at this skill's example
params file (status: unverified — file header states the exact source).
- Upstream: Nav2 documentation (primary source for
this skill, reachable via direct fetch on 2026-07-10), navigation2 GitHub
repo, jazzy branch
(source of the params/launch examples, fetched directly via raw GitHub
URLs on 2026-07-10), nav2_simple_commander
docs, slam_toolbox
GitHub. Sibling skills:
ros2 (foundation, load alongside), gazebo (sim),
visualization (debugging), environments (Docker/env
setup), architect (routes here).
Changelog
1.4.0 (2026-08-02): annotate SKILL.md; update custom-costmap-layer-plugin [reasons: obs-nav2-001, obs-nav2-002] (applied by apply_deltas)
1.3.0 (2026-08-02): add Usage patterns; add Usage patterns [reasons: obs-nav2-001, obs-nav2-002] (applied by apply_deltas)
1.2.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.
1.2.0 (2026-07-31): nav-trial absorption (Jazzy/TB3, observed 2026-07-10) — corrected SLAM-then-navigate (bringup slam:=True already runs a slam_toolbox node; a second online_async is double-SLAM), added Docker-stall bond_timeout:0.0 fix, TB3 burger.yaml starting point + pre-set enable_stamped_cmd_vel + collision_monitor source_timeout, benign costmap/AMCL log storm, live-SLAM recovery map corruption + waypoint clearance, and map_server yaml_filename override.
1.1.1 (2026-07-12): skill-refiner run 1 — provenance claims date-stamped ('this session' → 2026-07-10, the authoring session) so the staleness sweep can age them.
1.1.0 (2026-07-11): nav-trial absorption — TwistStamped cmd_vel gotcha,
SLAM map-origin-at-start-pose convention, direct-server launch pattern
(allow_substs / double-SLAM / bond_timeout), bringup-abort recovery added
to common-failures.
1---2name: nav2-73description: Nav2 mobile-robot navigation for ROS 2: bringup, behavior trees, costmaps, planner/controller servers, localization (AMCL, slam_toolbox), waypoint following, and tuning. Use when: 'navigation', 'nav2', 'costmap', 'path planning', 'robot won't move to goal', 'localization', 'SLAM', 'AMCL', 'waypoint', or any autonomous mobile robot task. Load after architect selects the ROS nav stack; pairs with ros2 (foundation), gazebo (sim), and visualization (debugging). Not for: manipulation (lerobot) or generic ROS 2 issues (ros2).4---56# nav278The nav-vertical core tool skill for robium: bringup, the BT Navigator and9behavior trees, costmap layers, the planner/controller/smoother servers,10localization (AMCL and slam_toolbox), waypoint following, and tuning a11running stack. Nav2 config in this skill targets ROS 2 **Jazzy Jalisco**12(LTS, supported to May 2029) rather than **Lyrical Luth**, the current13overall LTS the `ros2` skill defaults to — Nav2 has not yet shipped binary14packages for Lyrical (tracked in `ros-navigation/navigation2#6123` as of152026-07). Gazebo Harmonic is Jazzy's paired simulator. Re-check that gap16before starting a new project, and treat picking the distro itself as17`architect`'s call, not this skill's — load this skill once `architect` has18routed you to the navigation vertical.1920## When to use this skill2122- Any autonomous mobile-robot navigation task: bringing up Nav2, tuning23 costmaps or the planner/controller, adding or debugging a behavior tree,24 choosing between AMCL and slam_toolbox, following waypoints.25- The trigger phrases in the description: 'navigation', 'nav2', 'costmap',26 'path planning', "robot won't move to goal", 'localization', 'SLAM',27 'AMCL', 'waypoint'.28- A robot receives a goal and never moves, or moves erratically — start here29 (see `references/common-failures.md`), not in application logic.30- Cross-references — go to the sibling skill instead when the question is:31 - ROS 2 substrate this stack runs on (workspaces, colcon, packages, nodes,32 QoS, launch syntax, TF2 concepts/broadcasters) → `ros2`. This skill's33 only TF responsibility is verifying the map→odom→base_link chain exists34 and is current before tuning anything else; teaching TF2 itself stays in35 `ros2` — see that skill's interfaces-and-qos and debugging references.36 - Simulating the robot Nav2 drives → the `gazebo` skill.37 - Visualizing costmaps, TF, or BT execution (RViz2, Foxglove) → the38 `visualization` skill.39 - Arm/manipulation tasks, learned policies → `lerobot`. Nav2 is mobile-base40 navigation only.41 - Environment/Docker setup for the ROS 2 + Nav2 + Gazebo stack →42 `environments`.43 - The whole-stack decision this feeds into → `architect` (routes here).4445## Key directives4647- **Delegation posture: embed + links.** The navigation-specific concepts48 (BT Navigator, costmap layers, server roles, AMCL vs slam_toolbox) live in49 this skill and its references in depth, because no single upstream page50 covers them as a coherent whole for a new project — but every parameter51 table and default value is a link back to docs.nav2.org or the52 `navigation2` GitHub repo, not re-typed from memory. See References.53- **Start from the official minimal config, change one subsystem at a54 time.** <!-- id: one-subsystem-at-a-time --> `examples/nav2-params-diffdrive.yaml` is adapted from55 nav2_bringup's own `nav2_params.yaml` (jazzy branch) — begin a new robot56 there, verify it navigates, then change exactly one subsystem (footprint,57 controller plugin, costmap layer) before touching the next. Changing58 costmap, controller, and planner parameters simultaneously makes a59 regression impossible to bisect.60- **`use_sim_time` must be consistent across every node, every time.** <!-- id: use-sim-time-consistency --> In61 simulation, every Nav2 node, the map/odom TF broadcasters, and the sim62 clock source must all agree on `use_sim_time: true` (or all agree on63 `false` on real hardware) — one node left on the wrong value produces TF64 extrapolation errors and rejected goals that look like a planning bug but65 are a clock mismatch. Set it once, globally, in the params file passed to66 every node (see `examples/nav2-params-diffdrive.yaml` and67 `examples/bringup-launch-snippet.py`), not per-node.68- **Verify the TF tree — map→odom→base_link — before tuning anything69 else.** <!-- id: tf-tree-before-tuning --> Costmap2D blocks activation until a full TF tree is available, and70 AMCL will not publish `map`→`odom` until it has an initial pose (from71 RViz's "2D Pose Estimate" or the `/initialpose` topic). A robot that72 "won't move" is, more often than a bad planner or controller parameter, a73 broken or incomplete TF chain — check this first with `ros2 run tf2_ros74 tf2_echo map base_link` every time, before touching costmap or controller75 tuning. See `references/common-failures.md`.76- **Never write Nav2 parameter defaults, plugin names, or version/status77 claims from memory.** <!-- id: no-param-facts-from-memory --> They change release to release (the controller78 default alone has moved from DWB to MPPI upstream). Verify against79 docs.nav2.org or the `navigation2` GitHub repo before repeating a claim in80 a real project — every example in this skill is marked `status:81 unverified` for exactly this reason, and each reference states how its82 claims were checked on 2026-07-10.8384## Quick start8586**1. Confirm the ROS 2 substrate is ready.** <!-- id: confirm-ros2-substrate --> A sourced Jazzy workspace with87Nav2 installed (`sudo apt install ros-jazzy-navigation2 ros-jazzy-nav2-bringup`88— re-verify the package name against `docs.nav2.org`'s install page before89running it) — see the `ros2` skill if the workspace itself isn't set up yet.9091**2. Bring up Nav2 with the example config.** <!-- id: bringup-example-config --> Copy92`examples/nav2-params-diffdrive.yaml` and `examples/bringup-launch-snippet.py`93into your project, keeping the params filename the launch snippet expects94(or updating both together — see Customization), then:9596```bash97ros2 launch ./bringup-launch-snippet.py map:=/path/to/your_map.yaml use_sim_time:=true98```99100**3. Verify before tuning.** <!-- id: verify-before-tuning --> Confirm every managed node is active101(`ros2 lifecycle get /controller_server` etc.) and the TF chain is complete102(`ros2 run tf2_ros tf2_echo map base_link`) — see103`references/common-failures.md` if either check fails.104105**4. Send a goal** through RViz2's "Nav2 Goal" tool, or programmatically —106see the "send goals programmatically" usage pattern below.107108## Usage patterns109110**Bringup with an existing map.** <!-- id: bringup-existing-map --> Pass a saved map YAML and leave `slam`111false — Nav2 launches `nav2_map_server` + `nav2_amcl` for localization112against that static map. Set the robot's initial pose (RViz "2D Pose113Estimate" or publish to `/initialpose`) immediately after launch; AMCL does114not publish `map`→`odom` until it has one. See115`examples/bringup-launch-snippet.py` (`map:=` argument) and116`references/nav2-architecture.md`'s localization section.117118**SLAM-then-navigate.** <!-- id: slam-then-navigate-single-instance --> Pick exactly ONE of two combinations — on Jazzy119(observed 2026-07-10, nav-trial) nav2_bringup's `slam:=True` ALREADY120launches its own `online_sync_launch.py`, so *also* including slam_toolbox's121`online_async_launch.py` alongside spawns two slam_toolbox nodes whose122lifecycle managers fight and every goal fails (0/9: "Unable to start123transition 1 from current state inactive", "Timed out while waiting for124action server to acknowledge goal request for compute_path_to_pose").125Either (a) bring up with `slam:=True` and NO extra slam include, or (b)126launch `navigation_launch.py` plus your own `online_async_launch.py` — never127both. Either way pass no `map:=` argument; a SLAM node publishes `/map` and128the `map`→`odom` transform in place of `nav2_map_server`/`nav2_amcl`.129slam_toolbox's Jazzy `online_async` already ships `base_frame:130base_footprint` and `scan_topic: /scan` — only `use_sim_time: true` needs131adding, and any guidance to edit `base_frame` is stale for Jazzy. Drive the132robot to explore, then save the resulting map with `nav2_map_server`'s133`map_saver_cli` (see134`map_saver`'s params in `examples/nav2-params-diffdrive.yaml`) once mapping135is done, so the next run can go back to AMCL-on-a-fixed-map. Mind the frame136convention: the SLAM `map` frame's origin is the robot's mapping **start137pose**, not the world/sim origin — goals written in world coordinates are138silently offset by the spawn pose, and the saved map inherits the same139origin (convert `goal_map = goal_world − start_pose`, or pick goals off the140live map in a viewer). Verified 2026-07-11 (nav-trial). See141`references/nav2-architecture.md`.142143**Launch Nav2 servers directly (without nav2_bringup's launch).** <!-- id: direct-server-launch --> Three144Jazzy-verified reasons a project outgrows `bringup_launch.py` (all hit in145one real build, 2026-07-11 nav-trial): `slam:=True` starts its own146*synchronous* slam_toolbox, so also launching `online_async_launch.py`147alongside yields two SLAM nodes; `navigation_launch.py` hard-codes its148lifecycle-manager params, so `bond_timeout` can't be adjusted; and a params149file containing `$(find-pkg-share ...)` substitutions reaches the nodes as150literal strings. When launching servers as plain `Node`s yourself: wrap the151params file in `launch_ros.parameter_descriptions.ParameterFile(path,152allow_substs=True)`, replicate `navigation_launch.py`'s remappings (the153`cmd_vel` → `cmd_vel_nav` → smoother chain), and list every server in your154own lifecycle manager — set `bond_timeout: 0.0` on it (the only way to155change it, since `navigation_launch.py` hard-codes the value; see the156Docker-stall gotcha). To override a single param the `ParameterFile` gets157wrong (e.g. TB3 `burger.yaml`'s relative `yaml_filename: "map.yaml"`, which158makes `map_server`'s `LoadMap` fail), append a plain dict AFTER the159`ParameterFile` in the node's `parameters=` list — later entries win:160`parameters=[ParameterFile(params_yaml, allow_substs=True), {'yaml_filename':161abs_map_path}]`. yaml_filename fix observed 2026-07-10 (nav-trial).162163**Send goals programmatically.** <!-- id: basic-navigator-api --> Use `nav2_simple_commander`'s164`BasicNavigator` Python class rather than hand-rolling `NavigateToPose`165action clients: `goToPose()` / `goThroughPoses()` for single/multi-pose166goals, `followWaypoints()` for a waypoint list, and non-blocking167`isTaskComplete()`/`getResult()` polling for feedback in a single-threaded168script. See `references/nav2-architecture.md`'s commander-API section for a169minimal snippet shape.170171**Tune for a new robot footprint/speed.** <!-- id: tune-footprint-speed --> Start from172`examples/nav2-params-diffdrive.yaml`'s `local_costmap`/`global_costmap`173`robot_radius` (switch to an explicit `footprint` polygon for a non-circular174base), then the controller's velocity/acceleration limits and175`velocity_smoother`'s `max_velocity`/`max_accel`/`max_decel` — change these176before touching planner or BT internals, since a wrong footprint or speed177limit makes every downstream navigation attempt look broken. See178`references/tuning-guide.md`.179180**Author a custom global planner plugin.** <!-- id: custom-global-planner-plugin -->181A global-planner plugin subclasses `nav2_core::GlobalPlanner` and182implements `configure(parent, name, tf, costmap_ros)` / `cleanup()` /183`activate()` / `deactivate()` / `createPlan(...)` — the `costmap_ros`184argument handed to `configure()` is how the plugin reaches the same185costmap `references/nav2-architecture.md`'s planner-server section186already documents. Registration needs exactly two pieces, not three:187`PLUGINLIB_EXPORT_CLASS(<YourClass>, nav2_core::GlobalPlanner)` in the188.cpp, and the CMakeLists.txt's189`pluginlib_export_plugin_description_file(nav2_core <plugin>.xml)`190call, which installs the plugin-description XML and writes the191ament-index marker pluginlib's `ClassLoader` queries at runtime — a192`package.xml` `<export>` tag for the same XML is a ROS 1 carryover193some tutorials still ship and is not load-bearing in ROS 2, per194search-synthesis of ament_cmake/pluginlib docs (re-verify on next docs195pass). Treat the exact `configure()`/`createPlan()` parameter list as196a conceptual pattern, not a pinned signature — confirmed 2026-08-02197against the `rolling`-branch `navigation2_tutorials` repo (no198Jazzy/Humble/Iron branch exists there to check directly); re-verify199against this skill's target distro (Jazzy Jalisco) before writing a200plugin from it.201202**Author a custom costmap layer plugin.** <!-- id: custom-costmap-layer-plugin -->203A costmap layer plugin subclasses `nav2_costmap_2d::Layer` and204overrides `onInitialize()` (parameter declaration + one-time state),205`updateBounds(robot_x, robot_y, robot_yaw, min_x, min_y, max_x,206max_y)` (grows the costmap's dirty-bounds window), and207`updateCosts(master_grid, min_i, min_j, max_i, max_j)` (writes cost208values into that window) — plus `reset()`, `onFootprintChanged()`, and209`isClearable()`. Registration mirrors the planner-plugin pattern's210load-bearing half only (see the custom-global-planner-plugin pattern211above): `PLUGINLIB_EXPORT_CLASS(<YourClass>, nav2_costmap_2d::Layer)`212in the .cpp, paired with CMakeLists.txt's213`pluginlib_export_plugin_description_file(nav2_costmap_2d214<layer>.xml)` — the macro's first argument is `nav2_costmap_2d` here,215not `nav2_core` as in the planner case; it is pluginlib's216resource-index key, not a string a params file's `plugins:` list217names directly. Cross-references `references/nav2-architecture.md`'s218Costmap 2D section (which names the shared layer types — StaticLayer,219ObstacleLayer, VoxelLayer, InflationLayer — but not how to author a220new one). Confirmed 2026-08-02 against the `rolling`-branch221`navigation2_tutorials` repo; the `Layer` virtual-method set has been222stable across recent distros per search-synthesis, but re-verify223before absorbing a parameter list verbatim.224225## Platform gotchas226227- **Jazzy, not Lyrical, until Nav2 ships Lyrical binaries.** <!-- id: jazzy-not-lyrical --> See the intro228 paragraph above; this is a binding, repo-wide fact (`architect` and229 `ros2` both reference it) — don't silently "upgrade" a nav2 project to230 Lyrical without re-checking `ros-navigation/navigation2#6123` first.231- **AMCL is silent, not erroring, without an initial pose.** <!-- id: amcl-silent-no-initial-pose --> A freshly232 launched AMCL-based stack with no `/initialpose` published will sit idle —233 no error, just no `map`→`odom` transform and a costmap that never234 activates. This looks identical to a hung launch; check for a missing235 initial pose before debugging anything else. Between bringup and the first236 goal, `global_costmap` and AMCL spam transform/pose warnings every237 ~0.5–2 s ("Timed out waiting for transform from base_link to map ...238 Invalid frame ID map", "AMCL cannot publish a pose ... Please set the239 initial pose") — this is benign, the `map` frame doesn't exist until AMCL240 gets its initial pose, so don't chase it. See241 `references/common-failures.md`.242- **Composed (`use_composition:=true`) vs standalone nodes change crash243 behavior.** <!-- id: composition-vs-standalone --> nav2_bringup defaults to component-container composition; a244 crash inside one composed node can take down the whole container process,245 whereas standalone nodes (`use_composition:=false`, with246 `use_respawn:=true`) restart independently. Prefer standalone + respawn247 while iterating on a new robot; composition is a later performance248 optimization, not a default to fight while still debugging.249- **`/cmd_vel` may be `TwistStamped`, not `Twist`.** <!-- id: cmd-vel-twiststamped --> Modern gz robot250 integrations (TB3 on Jazzy among them) subscribe `TwistStamped`; a plain251 `Twist` publisher never matches — no error, `ros2 topic pub` just waits252 forever for a matching subscription — and the robot silently ignores253 Nav2. Check with `ros2 topic info -v /cmd_vel`, and set254 `enable_stamped_cmd_vel: true` in every cmd_vel-publishing section255 (`controller_server`, `velocity_smoother`, `behavior_server`,256 `collision_monitor`, `docking_server`). TB3 Jazzy's `burger.yaml` already257 pre-sets `enable_stamped_cmd_vel: true` in all five of those sections, so258 this trap only bites configs started from nav2_bringup's `nav2_params.yaml`.259 Verified 2026-07-11 (nav-trial).260- **On TB3 Jazzy, start from `turtlebot3_navigation2`'s `burger.yaml`, not261 nav2_bringup's `nav2_params.yaml`.** <!-- id: tb3-burger-yaml-start --> It is TB3-tuned and carries 13 of262 nav2_bringup's 14 sections; it ships no `use_sim_time` keys and relies on263 `RewrittenYaml` to inject them. Observed 2026-07-10 (nav-trial).264- **TB3 `burger.yaml`'s `collision_monitor` `source_timeout` is too tight265 for its own lidar.** <!-- id: tb3-collision-monitor-timeout --> The scan source ships `source_timeout: 0.2`, but the266 burger lidar publishes at 5 Hz (a 0.2 s period), so ordinary jitter makes267 `collision_monitor` reject the source and zero `cmd_vel` — the robot won't268 move even with a healthy stack ("Latest source and current collision269 monitor node timestamps differ on 0.2xx seconds. Ignoring the source.",270 "Robot to stop due to invalid source"). Set `source_timeout` above the271 sensor period (1.0 worked). Observed 2026-07-10 (nav-trial).272- **On Docker Desktop/macOS the container stalls ~8 s at Nav2 activation and273 the first goal, so the default `bond_timeout: 4.0` self-destructs the274 stack** <!-- id: docker-desktop-bond-timeout --> ("CRITICAL FAILURE: SERVER controller_server IS DOWN after not275 receiving a heartbeat for 4000 ms"). Jazzy's `navigation_launch.py`276 hard-codes the `lifecycle_manager` params, so `bond_timeout` cannot be set277 through `params_file`/`RewrittenYaml`; the only fix is to launch the278 servers under your own `lifecycle_manager` node with `bond_timeout: 0.0`279 (see the direct-server-launch usage pattern). Observed 2026-07-10280 (nav-trial).281- **In live SLAM, an unreachable goal corrupts the map.** <!-- id: slam-goal-corrupts-map --> A goal in unknown282 space that fails to plan triggers a spin/backup recovery loop that smears283 the live SLAM map until every subsequent plan fails ("Failed to create284 plan with tolerance of: 0.500000", "Goal ... was outside bounds"). Check285 waypoints against actual world geometry and keep them ≥0.4 m from286 obstacles — e.g. `turtlebot3_world` has pillars of r=0.15 at {-1.1, 0,287 1.1}² (grep the SDF pillar poses); an octagon route of r=1.7 lands inside288 them. Observed 2026-07-10 (nav-trial).289- **Gazebo Harmonic's `/clock` must actually be publishing** <!-- id: clock-must-be-publishing --> before any node290 with `use_sim_time:=true` will progress — a paused or not-yet-started291 Gazebo world leaves every Nav2 node waiting on TF timestamps that never292 arrive, which looks like a Nav2 hang rather than a sim issue.293294## Customization295296- **Different robot footprint or drive type:** swap `robot_radius` for an297 explicit `footprint` polygon in both `local_costmap` and `global_costmap`298 in `examples/nav2-params-diffdrive.yaml`, and change `FollowPath`'s299 `motion_model` (e.g. `"DiffDrive"` → `"Omni"`) if the base isn't300 differential-drive — see `references/tuning-guide.md`.301- **Different controller/planner plugin:** the params file's302 `controller_server.FollowPath.plugin` and303 `planner_server.GridBased.plugin` fields select the algorithm; swapping304 requires the matching plugin name and its own parameter block (e.g.305 `nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController` or306 `nav2_smac_planner::SmacPlannerHybrid`) — verify exact plugin/class names307 against `docs.nav2.org`'s configuration guide before writing them, they308 are not interchangeable strings. See `references/tuning-guide.md`.309- **Different params filename or launch structure:**310 `examples/bringup-launch-snippet.py`'s `params_file` default and311 `examples/nav2-params-diffdrive.yaml`'s own filename must be kept in sync312 if you rename either — the launch snippet resolves the params path313 relative to itself, so a silent rename of one without the other produces a314 "params file not found" failure at launch, not a subtle runtime bug.315- **Reverting to Lyrical Luth:** once Nav2 ships Lyrical binaries (re-check316 `ros-navigation/navigation2#6123`), swap `jazzy` for `lyrical` in every317 install command and Docker base image in the `environments` skill's318 Dockerfile.ros2 example; nothing in this skill's params/launch content itself is319 distro-specific beyond the install step.320321## References322323- `references/nav2-architecture.md` — the BT Navigator and default behavior324 trees, the planner/controller/smoother/behavior/waypoint-follower servers,325 costmap 2D layers (global vs local), the lifecycle manager, AMCL vs326 slam_toolbox, and the `nav2_simple_commander` API.327- `references/tuning-guide.md` — costmap resolution/update-rate/inflation328 tuning, footprint vs radius, controller/planner plugin selection,329 velocity/acceleration limits, and the "one subsystem at a time" workflow.330- `references/common-failures.md` — the "robot won't move" diagnostic331 checklist: lifecycle state, TF tree, `use_sim_time` consistency, costmap332 obstacle sourcing, goal rejection, and `cmd_vel` not reaching the base.333- `examples/nav2-params-diffdrive.yaml` — adapted from nav2_bringup's334 official minimal diff-drive `nav2_params.yaml` (status: unverified — file335 header states the exact source and the deviations made).336- `examples/bringup-launch-snippet.py` — a project launch file that includes337 nav2_bringup's own `bringup_launch.py`, pointed at this skill's example338 params file (status: unverified — file header states the exact source).339- Upstream: [Nav2 documentation](https://docs.nav2.org/) (primary source for340 this skill, reachable via direct fetch on 2026-07-10), [navigation2 GitHub341 repo, jazzy branch](https://github.com/ros-navigation/navigation2/tree/jazzy)342 (source of the params/launch examples, fetched directly via raw GitHub343 URLs on 2026-07-10), [nav2_simple_commander344 docs](https://docs.nav2.org/commander_api/index.html), [slam_toolbox345 GitHub](https://github.com/SteveMacenski/slam_toolbox). Sibling skills:346 `ros2` (foundation, load alongside), `gazebo` (sim),347 `visualization` (debugging), `environments` (Docker/env348 setup), `architect` (routes here).349350## Changelog351352<!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->353354355356- 1.4.0 (2026-08-02): annotate SKILL.md; update custom-costmap-layer-plugin [reasons: obs-nav2-001, obs-nav2-002] (applied by apply_deltas)357- 1.3.0 (2026-08-02): add Usage patterns; add Usage patterns [reasons: obs-nav2-001, obs-nav2-002] (applied by apply_deltas)358- 1.2.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.359360- 1.2.0 (2026-07-31): nav-trial absorption (Jazzy/TB3, observed 2026-07-10) — corrected SLAM-then-navigate (bringup slam:=True already runs a slam_toolbox node; a second online_async is double-SLAM), added Docker-stall bond_timeout:0.0 fix, TB3 burger.yaml starting point + pre-set enable_stamped_cmd_vel + collision_monitor source_timeout, benign costmap/AMCL log storm, live-SLAM recovery map corruption + waypoint clearance, and map_server yaml_filename override.361362- 1.1.1 (2026-07-12): skill-refiner run 1 — provenance claims date-stamped ('this session' → 2026-07-10, the authoring session) so the staleness sweep can age them.363364- 1.1.0 (2026-07-11): nav-trial absorption — TwistStamped cmd_vel gotcha,365 SLAM map-origin-at-start-pose convention, direct-server launch pattern366 (allow_substs / double-SLAM / bond_timeout), bringup-abort recovery added367 to common-failures.