Nav2 path planning: request a route and diagnose no path

Run the real Nav2 planner server on a saved map. Request a path between supplied poses, inspect the returned geometry, and diagnose a free goal that the planner cannot reach.

By 11 min read

What you will learn

  • Configure the real Nav2 planner server and its global costmap.
  • Submit an explicit start and goal with ComputePathToPose.
  • Distinguish an accepted action request from a successful planning result.
  • Inspect the returned path's frame, endpoints, and segments.
  • Diagnose a free goal that lies in a disconnected region.
  • State what a global path establishes before a controller follows it.

Before you start

Nav2's planner server returns a global path between poses using a configured planner and costmap. A successful result gives a sequence of poses. A separate controller is responsible for turning that path into motion commands.

This tutorial runs the real planner server with NavFn on a small saved map. One goal has a route from the supplied start. Another goal lies in free space behind a closed barrier. The example checks both results and preserves the inputs and outputs for inspection.

What does a global planner return?

The request supplies a goal pose, a planner ID, and optionally a start pose. The result includes a nav_msgs/Path, which contains a header and an ordered sequence of stamped poses. It also reports planning time and an error code.

A costmap describes the cells available to the planner. It is an input to planning, not a route by itself. The inflation tutorial shows how robot geometry and distance from obstacles affect raw costs. Here the planner server owns its own global costmap and reads the saved occupancy map into that costmap.

The workflow uses supplied poses to isolate the planning request. No localization estimate or moving robot feeds the experiment.

Run the checked Nav2 example

Save these six files together: Dockerfile, planner_workflow.py, verify.sh, planner.yaml, map.yaml, and map.pgm.

Open a host terminal in that directory and run:

docker build --platform linux/arm64 -t dustin-nav2-planner-lesson:jazzy .
mkdir -p planner-output
docker run --rm --network none --platform linux/arm64 \
  --mount "type=bind,source=$(pwd),target=/lesson,readonly" \
  --mount "type=bind,source=$(pwd)/planner-output,target=/output" \
  dustin-nav2-planner-lesson:jazzy \
  bash /lesson/verify.sh --output /output/run

The build needs network access. It pins an official ROS Jazzy base and exact ARM64 versions of the Nav2 1.3.13 map-server, costmap, planner-server, and NavFn packages. Other dependencies resolve during installation. The experiment runs without external networking or a host ROS installation.

Choose a new output directory for each run. The script refuses an existing destination and keeps map data, loaded settings, requests, responses, process logs, and geometry checks under planner-output/run. It starts the nodes, submits both requests, checks the results, and stops the nodes.

Read the README, environment record, and captured output for exact versions and checks. Testing used Docker Desktop on macOS ARM64 with Ubuntu 24.04 inside the container. Other architectures remain unverified.

Put two goals on one saved map

The grid has 40 columns and 30 rows, at 0.1 m per cell, with origin (0, 0) and no rotation. It describes a 4 m × 3 m rectangle. Cell indices start at zero, columns increase right, and rows increase up.

An outer wall encloses the map. A vertical divider occupies column 20, except for a five-cell passage at rows 7 through 11. A second set of walls encloses a small room near the upper-right corner. Sixteen unknown cells sit near the upper-left corner, away from the intended route.

The three supplied poses are:

PosePosition (x, y), in metersRole
Start(0.55, 1.55)Start both requests here
Reachable goal(3.45, 1.35)Cross through the passage
Disconnected goal(3.35, 2.45)Try to enter the closed room

All three positions lie in free source-map cells. Their orientations have zero yaw. A static map → base_link transform places the robot at the supplied start so the global costmap can initialize. Setting use_start: true controls where the planning request begins; the costmap still needs its robot transform.

The map server loads the PGM image using map.yaml. The checker verifies every published occupancy value against the authored cells and captures the planner's raw costmap separately. That keeps a map-loading error distinct from a search failure.

This is a separate authored fixture. It does not consume the captured map from the SLAM Toolbox lesson or the raw JSON from the costmap comparison. Each tutorial checks one stage with explicit inputs; these examples do not claim a tested end-to-end navigation system.

Configure NavFn and the global costmap

The planner section selects one plugin under the ID GridBased:

planner_server:
  ros__parameters:
    use_sim_time: false
    planner_plugins: [GridBased]
    GridBased:
      plugin: nav2_navfn_planner::NavfnPlanner
      tolerance: 0.0
      use_astar: true
      allow_unknown: false
      use_final_approach_orientation: false

The request's planner_id must match the configured ID. GridBased names this configuration entry; nav2_navfn_planner::NavfnPlanner names the plugin class.

The four plugin settings define the experiment:

  • tolerance: 0.0 requires the requested goal location. A positive tolerance can permit a nearby reachable endpoint.
  • use_astar: true selects NavFn's A* propagation. This example does not compare search modes or benchmark their speed.
  • allow_unknown: false blocks unknown cells for this planner. The costmap also retains their unknown state.
  • use_final_approach_orientation: false retains the requested goal orientation rather than choosing it from the approach direction. This affects the output pose; no controller tests the turn.

The same file configures the planner server's global costmap with a static layer followed by an inflation layer. It uses map as the global frame and base_link as the body frame, with robot_radius: 0.12, zero footprint padding, inflation_radius: 0.35, and cost_scaling_factor: 3.0. The window stays fixed. Static-layer footprint clearing is disabled, and both unknown-inflation flags are false.

The runner configures and activates the managed nodes, waits for the saved map and a populated costmap, and reads back the settings. It does not treat the appearance of an action name as proof that the map is ready.

NavFn converts the raw costs before propagation. For known raw costs below 253, this version uses the integer value of 50 + 0.8 × raw cost, clamped below its internal obstacle value. Raw 253 and 254 are blocked. With allow_unknown: false, raw 255 is blocked too. The NavFn implementation defines those rules.

NavFn builds a navigation potential and extracts a path through that field. The A* lesson introduces heuristic search on a small graph; Dijkstra's algorithm introduces cost-ordered expansion. The returned NavFn route should not be described as the shortest Euclidean curve through the room, or scored by simply summing raw cost bytes.

Send an explicit start and goal

The workflow uses nav2_msgs/action/ComputePathToPose on /compute_path_to_pose. Its goal has four fields:

FieldValue hereMeaning
startSupplied stamped posePosition and orientation from which to plan
goalOne of the two target posesRequested destination
planner_idGridBasedConfigured planner to use
use_starttrueUse the supplied start instead of obtaining it from robot TF

Both pose headers name the map frame. Coordinates without a frame do not identify a location the planner can interpret. The timestamped transforms tutorial explains how frames connect positions from different sources.

A successful result must finish with ROS action status SUCCEEDED (4), Nav2 error code NONE (0), and a nonempty path. The failure case must finish with ABORTED (6), NO_VALID_PATH (208), and an empty path. These are separate fields with separate number sets; Nav2 error 208 is not an action status.

An action has several outcomes to inspect. The server can accept a valid request and still fail to find a path. Check the terminal action status, the result's error code, and the path content together. The ROS 2 communication lesson introduces that separation between starting work and receiving its result.

Inspect the returned path

The figure renders the actual captured map, costmap, requests, and returned poses. The reachable request returns 59 poses in map, with a polyline length of approximately 3.297448 m. Its last pose is the supplied goal, (3.45, 1.35) m, with the requested orientation.

The route crosses the divider through its open passage. The checker inspects every returned position and every connecting segment. It also checks that both requests read the same map and costmap, so a changed obstacle field cannot explain the different outcomes.

The first returned point needs a separate check. The requested start is (0.55, 1.55) m, while the captured first pose is approximately (0.5, 1.5) m. NavFn 1.3.13 rounds world coordinates onto its planning lattice and converts them back without a half-cell offset. The checker derives the expected first pose from that convention and the actual reported map resolution.

The resulting 0.070711 m start connector is separate from the returned path. The figure reports its length separately, and the geometry check includes it. Goal tolerance controls a different question: how close a returned endpoint may be to the requested goal.

For this fixed map, the minimum distance from the checked centerline to an occupied cell rectangle is approximately 0.201140 m. Subtracting the nominal robot radius, 0.12 m, leaves 0.081140 m between the disk boundary and those rectangles. The checker evaluates the complete segments analytically; it does not infer that margin from a few plotted points.

The checks also cover path and pose frames, finite coordinates, unit orientations, the exact requested endpoint, and clearance from unknown cells and the map boundary. These checks apply to the stated teaching geometry. They do not measure a physical robot's tracking error.

Diagnose the unreachable free goal

The second request uses the same start, map, planner, and settings. Only the goal changes to (3.35, 2.45) m, inside the closed room.

The server accepts it, then returns:

accepted: true
status: ABORTED (6)
error_code: NO_VALID_PATH (208)
path poses: 0

That destination has source occupancy 0 and raw cost 0. The checker confirms both the normal containing cell and the cell selected by NavFn's coordinate rounding. It also independently explores the cells connected to the start and confirms that the goal lies outside that component, even when diagonal connections are allowed.

The barrier therefore isolates a genuinely free destination. With zero tolerance, NavFn reports GOAL_OCCUPIED (206) when the requested goal's raw cost is 254. A blocked inscribed cell with raw cost 253 does not take that same early error branch. An incorrect planner ID, a pose outside the map, or a missing transform would be different failures to diagnose.

Keeping tolerance: 0.0 prevents NavFn from substituting a reachable pose near the target. Keeping allow_unknown: false makes the unknown-space policy explicit. The closed room contains no unknown opening, so changing that policy alone would not create a route through its walls.

A free goal cell is a local fact about the destination. Reachability also depends on how free regions connect, which cells the planner may use, and where it starts. Inspecting only the goal cell misses those constraints.

Separate a path from its execution

A global path proposes positions and orientations. A controller must still choose velocities that the robot can follow, respond to local observations, and decide whether it has reached the goal. This workflow runs none of those stages.

The independent clearance check covers a 0.12 m circular robot translating along the checked straight segments, against occupied and unknown cell rectangles, plus the boundary of a fixed map. It includes the connector from the requested start to the first returned pose. It uses the supplied map geometry without sensor, localization, tracking, or model error.

That result does not certify a differently shaped robot, a controller's curved motion between poses, or a route through a changing environment. The collision-checking lesson explains why testing whole segments is stronger than checking endpoints alone, and why the motion model still matters.

The captured planning time records one execution. It is not a speed comparison between A* and Dijkstra. The path length describes the returned polyline, excluding the separate start connector; it does not establish a shortest continuous route or an executed travel distance.

Check two planning decisions

A request reports accepted: true, then finishes with action status ABORTED, Nav2 error NO_VALID_PATH, and zero path poses. Did the planner successfully complete the requested route?

Show the result interpretation

No. Acceptance means the action server took the request. Its terminal result says planning failed. Check all three pieces together: action status, Nav2 error code, and path content. A client that stops checking after acceptance can report success before the planner has produced a route.

The closed-room goal lies in free space. A teammate suggests allowing the planner to return a nearby endpoint by increasing goal tolerance. Would that prove a path reaches the requested goal?

Show the goal-tolerance tradeoff

No. A positive tolerance can let NavFn return a reachable nearby pose when it cannot reach the requested goal. Inspect the actual endpoint and its distance from that goal. For this experiment, zero tolerance keeps the destination requirement fixed so the disconnected-goal failure stays explicit. A different tolerance needs its own checked result.

Sources and further study

Continue with collision checking to examine how an entire motion can collide even when selected poses are clear. For the search reasoning behind path choice, compare Dijkstra's algorithm with A*.