# Nav2 path planning: a route and a disconnected free goal

This finite fixture runs the real ROS 2 Jazzy `map_server` and `planner_server` with NavFn. The planner owns its global costmap. It loads an authored PGM/YAML map, accepts two `ComputePathToPose` requests, returns a route for one, and returns `NO_VALID_PATH` for a free goal enclosed by walls. It runs without a controller, AMCL, a simulator or a robot.

The recorded example is a static grid experiment. Its geometric checks cover the supplied grid and nominal circular footprint; they do not establish physical robot clearance, optimality, executed navigation or localization accuracy.

## Run

Download these files into one directory: `Dockerfile`, `planner_workflow.py`, `verify.sh`, `planner.yaml`, `map.yaml`, `map.pgm`, `README.md`, `environment.txt`, `sample-output.txt`, and `reference-planning.json`.

On Docker with Linux arm64 support:

```sh
docker build --platform linux/arm64 -t dustin-nav2-planner-lesson:jazzy .
mkdir -p results
docker run --rm --platform linux/arm64 --network none \
  -v "$PWD:/example:ro" \
  -v "$PWD/results:/results" \
  dustin-nav2-planner-lesson:jazzy \
  bash /example/verify.sh --output /results/run-1
```

`run-1` must not already exist. Use a new name for each run. Existing output directories are refused before any files are written. The example directory is mounted read-only; logs and captures go under the new result directory. Omitting `--output` checks temporary files and removes them afterward. The container runtime uses no network. Building needs internet access and a few GB for Docker's ROS layers. The first run requires no packages on the host beyond Docker.

The Dockerfile starts from a digest-pinned official ROS image. Its first install layer matches the preceding costmap lesson and can be reused from Docker's cache. The four principal Nav2 Debian packages are version-pinned; transitive apt dependencies resolve at build time. Exact installed versions from the checked image are listed in `environment.txt`. This is a tested environment, not a fully locked Debian repository snapshot. No `LD_PRELOAD` workaround is used here.

## Fixture and configuration

The map has 40 columns and 30 rows at 0.1 m resolution, with origin `(0, 0)` and identity rotation. Array indices increase from the bottom-left; the PGM stores its top row first. Occupied cells are:

- The outer border.
- Column 20 except rows 7 through 11, leaving a five-cell passage.
- Room walls at columns 28 and 38, rows 19 through 29, and rows 19 and 29, columns 28 through 38. The top edge shares the outer border.

Columns 3 through 6 and rows 23 through 26 are unknown. All other cells are free. `map_server` loads pixels 0/254/205 as occupied/free/unknown with `occupied_thresh: 0.65` and `free_thresh: 0.196`.

The static global costmap uses only `StaticLayer` and `InflationLayer`, with unknown tracking enabled and static footprint clearing disabled. A supplied static `map → base_link` transform places the robot at `(0.55, 1.55)` with zero yaw. It is an authored transform, not localization output. The 0.12 m robot radius generates a 16-vertex polygon with zero padding; its inscribed radius is `0.12 cos(pi/16) ≈ 0.117694 m`. Inflation radius is 0.35 m and the cost scaling factor is 3.0. Unknown cells do not seed inflation, and lower gradient costs do not overwrite them.

NavFn is configured with `use_astar: true`, `allow_unknown: false`, `tolerance: 0.0`, and `use_final_approach_orientation: false`. Both requests use `planner_id: GridBased` and `use_start: true`, with supplied start `(0.55, 1.55)`. The reachable goal is `(3.45, 1.35)`; the disconnected goal is `(3.35, 2.45)`. All have identity orientation and frame `map`.

The supplied start and both goals are checked as source-free and raw cost zero using both floor indexing and NavFn's rounded indexing. This strict zero-cost choice makes the failure easy to interpret. NavFn can also traverse gradient cells below 253; nonzero cost is not itself an occupied-cell diagnosis. With zero tolerance, the enclosed free goal returns no valid path rather than selecting a nearby alternative.

## Checks and saved evidence

The script checks all 1,200 source cells against the authored map. It independently computes every raw cost byte using nearest occupied-cell distance and the configured inflation formula, then queries `/global_costmap/get_costmap` to compare them. It saves that raw grid before and after each request. NavFn explicitly clears its rounded start cell during planning, so the start is required to be zero beforehand and all raw cells must remain unchanged afterward.

The script records lifecycle states for `/map_server`, `/planner_server`, and its child `/global_costmap/global_costmap`, reads back configuration parameters, and checks the published footprint. Lifecycle transitions are sent to the planner server; it manages its child costmap.

Action acceptance and completion are separate checks:

- Reachable request: accepted, action `SUCCEEDED` (4), error code 0 and a nonempty `map` path.
- Disconnected request: accepted, action `ABORTED` (6), error code 208 (`NO_VALID_PATH`) and an empty path. An independent eight-connected search also finds the goal disconnected.

NavFn uses a rounded grid lattice and returns lattice coordinates without a half-cell offset. The first returned pose is checked against that exact convention using the captured float32 map resolution. Its distance from the supplied start must be at most half a cell diagonal. The final position and orientation must match the requested goal. Frames, finite values, planar poses and unit quaternions are checked throughout.

Every full segment, including the supplied-start-to-first-pose connector, is checked against occupied and unknown cell rectangles. The nominal 0.12 m disk must stay inside the map and clear both kinds of cell. The route must cross the divider through the declared opening. These are continuous segment checks, not just waypoint samples, but they concern this authored static grid only.

Saved files include loaded parameters, lifecycle states, process logs and exit codes, original inputs, source maps, raw costmaps before/after requests, the independently expected costmap, published footprint, action requests/results, and combined `planning.json`. Both ROS processes are deactivated and cleaned up before termination; successful verification requires exit code 0. `reference-planning.json` is one captured run, not a fabricated illustration. Timestamps and planning duration can differ on another run; the supplied static arrays and route were checked separately across repeated runs.

## Recorded result

The final captured run returned 59 poses and a 3.297448 m path, plus a 0.070711 m connector from the supplied start to NavFn's first pose. Across all segments including that connector, the smallest occupied-cell rectangle clearance was 0.201140 m and unknown-cell clearance was 0.750000 m. The raw grid contains 333 free, 462 gradient, 201 inscribed, 188 lethal and 16 unknown cells. The second accepted request returned status 6 / error 208 / no poses. Both processes exited 0.

Two fresh final runs passed with identical static map/costmap arrays, path coordinates and orientations, action outcomes and geometric checks. Wall-clock stamps and planning durations were allowed to differ. A separate refusal check preserved all 19 files in an existing output directory byte-for-byte.

## Scope of changes

The checker intentionally enforces this fixture's assumptions. If you change its map, robot size, goals, tolerance or plugin, revise the expected geometry and checks too. A changed fixture that fails is not evidence that Nav2 itself is broken. An unreachable action result is expected in the second case; a nonzero script exit means some verification did not pass.
