# SLAM Toolbox: check a supplied room map

This exercise runs the real ROS 2 Jazzy `async_slam_toolbox_node` on supplied laser scans and perfect odometry. The Python fixture casts rays against a known room; SLAM Toolbox builds the map. No robot, simulator, recorded dataset, display server, or RViz session is required.

The first run omits `base_link → laser`. The second starts a fresh mapper with that static transform present. Both use the same supplied scans, odometry transforms, and clock values. The checker verifies the failure, then checks the repaired map against known wall, obstacle, and free-space locations.

## Build and run

Download these files into one directory: `Dockerfile`, `mapper.yaml`, `slam_workflow.py`, and `verify.sh`. Read `environment.txt` and `sample-output.txt` for the tested environment and captured result. `reference-map.json` and `reference-report.json` preserve one captured grid and its checks. They are outputs, not inputs to the mapper or checker.

Build on an ARM64 machine with Docker running:

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

The minimal binary installation still includes RViz libraries and development dependencies required by the package. The tested package installation added roughly 1 GB before Docker's build/export overhead. Allow several GB of free disk space. This exercise never starts RViz. Other architectures and emulation have not been tested.

The runtime has no external network access. ROS participants communicate inside the container with Fast DDS; the two cases use separate domain IDs, 75 and 76. Build-time package downloads need network access. The base digest and SLAM Toolbox binary package are pinned; other apt dependencies resolve when the image is built.

`--output` must name a new directory. To repeat, choose `/output/run-2`; existing outputs are refused before the workflow writes to them. Omit `--output` to check temporary outputs and remove them afterward. If an assertion fails with `--output`, inspect the retained mapper logs and input geometry. The checker uses wall-time deadlines so a missing ROS clock or frame cannot make it wait indefinitely.

## What the fixture supplies

Before rotation, the room spans x = −4 to 4 m and y = −3 to 3 m. A rectangular obstacle spans x = 1 to 2 m and y = 1.5 to 2.5 m. The fixture rotates both by 0.1 rad around the origin: `x′ = x cos(0.1) − y sin(0.1)`, `y′ = x sin(0.1) + y cos(0.1)`. The reference probes receive the same rotation. This keeps wall midpoints away from the grid’s exclusive maximum boundary, which clipped whole upper/right walls in an initial axis-aligned trial. It does not rotate the robot trajectory or laser mounting transform. The laser is 0.2 m ahead of `base_link` with no rotation. Twenty scans have 361 rays across −π to π; the first and last rays share a direction. Each idealized scan has one timestamp, with no motion during acquisition or measurement noise. Its declared range limits are 0.05 and 25 m; every ray hits within them.

The robot moves along x from 0 to 1.14 m in 0.06 m increments. Stamps run from 100 to 109.5 seconds, 0.5 seconds apart. `/clock`, each scan header, and `odom → base_link` use those times. The fixture supplies ideal odometry as TF; it does not publish an `Odometry` topic. It checks that `odom ← laser` is available at every scan stamp only in the repaired case.

The script configures and activates each mapper through its lifecycle services, verifies the active state, and reads back the configured parameters. It waits for a scan subscriber before sending the fixture. That wait and a short settling period assist discovery; the output checks determine whether useful data actually arrived.

## Checks and retained files

- `reference-geometry.json` and `scans.json`: supplied geometry, poses, stamps, and ranges.
- `report.json`: both cases and their checks.
- Each case directory: the configuration, loaded parameter values, mapper log, received `map → odom` samples, accepted mapper poses, and report.
- `complete-tf/map.json`: the received `OccupancyGrid`, including dimensions, origin, resolution, and row-major cells.
- `complete-tf/map.pgm` and `map.yaml`: an export of that same grid made by the exercise's small exporter.

The repaired grid must contain occupied (`100`), free (`0`), and unknown (`−1`) cells. Five occupied probes check four room walls and the obstacle's front edge; a sixth checks free interior space. Each probe searches within 0.2 m of a reference point to allow grid discretization and small scan-matching differences. These are fixture checks, not map-quality thresholds for a robot.

The final `map → odom` sample must be finite, have a normalized quaternion, have a timestamp within the experiment window, and stay within 0.2 m translation and 0.05 rad yaw of the ideal odometry reference. The checker also requires a successfully processed pose at the final scan stamp, a grid stamped at that scan time, and a transform stamped 0.2 seconds later under `restamp_tf: false`. The final pose must lie within 0.2 m of the supplied reference. Map timestamp alone does not prove scan acceptance. The number of received maps and transforms may differ between runs. Scans supplied are not the same as scans accepted by the mapper.

The PGM reverses grid row order so image top corresponds to high world y. Black (0) is occupied, near-white (254) is free, and gray (205) is unknown. `map.yaml` records resolution, origin, and trinary thresholds (`free_thresh: 0.196`, `occupied_thresh: 0.65`). With `negate: 0`, pixel 205 has occupancy about 0.196078, so it must stay above the free threshold to reload as unknown.

A separate October 12 reload check used Nav2 map_server 1.3.13 on the prior captured PGM. The corrected YAML reproduced all 23,256 reference cells exactly: 583 occupied, 18,026 free, and 4,647 unknown. The old 0.25 free threshold incorrectly reloaded those 4,647 unknown cells as free. See `reference-map.pgm`, `reference-map.yaml`, and `map-export-reload.json`; this correction did not rerun SLAM or change its captured grid.

The exporter does not call `/slam_toolbox/save_map` or Nav2's map saver. A grid image is not a serialized SLAM pose graph and cannot by itself resume Toolbox mapping.

## Limits

This is a small integration exercise with supplied scans and perfect odometry. It checks configuration, timing, frame connectivity, and map geometry. It does not measure localization accuracy on real sensors. Loop closing is explicitly disabled. Navigation, map serialization, continuing a mapping session, map reuse for localization, sensor noise, dynamic obstacles, and long-run robustness are outside the exercise.

The missing-frame case publishes a scan stream to the active mapper and observes no usable map during the finite fixture. The repaired case then checks the supplied geometry. That symptom alone does not diagnose every real system; check lifecycle state, discovery, QoS, time, frames, and logs together.
