ROS 2 slam_toolbox Guide 2026
slam_toolbox is the default SLAM implementation for Nav2 — it builds 2D occupancy maps from LiDAR, supports lifelong mapping (add new areas to existing maps), and can switch to localization-only mode using a pre-saved map.
Operating Modes
| Mode | Plugin | Use Case |
|---|---|---|
| Online Async | async_slam_toolbox_node | Real-time mapping while driving — most common setup |
| Online Sync | sync_slam_toolbox_node | Guaranteed processing of every scan (slower, for rosbag replay) |
| Lifelong Mapping | lifelong_slam_toolbox_node | Extend an existing saved map with new areas |
| Localization Only | localization_slam_toolbox_node | Pure localization against a pre-saved .posegraph map |
| Map Merging | map_merging_slam_toolbox_node | Merge maps from multiple robots |
Install
sudo apt install ros-jazzy-slam-toolbox # or Humble: sudo apt install ros-humble-slam-toolbox
Online Async SLAM Launch
The standard launch file — bring up slam_toolbox in online async mode with your params file:
# Using the built-in launch file
ros2 launch slam_toolbox online_async_launch.py \
slam_params_file:=/path/to/mapper_params_online_async.yaml \
use_sim_time:=false
# Or in your own launch.py:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package="slam_toolbox",
executable="async_slam_toolbox_node",
name="slam_toolbox",
output="screen",
parameters=[
"/path/to/mapper_params_online_async.yaml",
{"use_sim_time": False},
],
),
])Full YAML Reference: mapper_params_online_async.yaml
slam_toolbox:
ros__parameters:
# === Solver ===
solver_plugin: solver_plugins::CeresSolver
ceres_linear_solver: SPARSE_NORMAL_CHOLESKY
ceres_preconditioner: SCHUR_JACOBI
ceres_trust_strategy: LEVENBERG_MARQUARDT
ceres_dogleg_type: TRADITIONAL_DOGLEG
ceres_loss_function: None
# === Scan matching ===
scan_topic: /scan # your LaserScan topic
use_scan_matching: true
use_scan_barycenter: true
minimum_travel_distance: 0.3 # meters to move before processing a scan
minimum_travel_heading: 0.3 # radians to rotate before processing
scan_buffer_size: 10
scan_buffer_maximum_scan_distance: 10.0 # meters
# === Scan matching thresholds ===
link_match_minimum_response_fine: 0.45 # 0.0–1.0, raise if map drifts
link_scan_maximum_distance: 1.5 # meters from pose to try matching
loop_match_minimum_response_coarse: 0.35
loop_match_maximum_variance_coarse: 3.0
loop_match_minimum_response_fine: 0.45
loop_match_minimum_chain_size: 10
# === Map parameters ===
resolution: 0.05 # meters/cell — 5cm is standard
max_laser_range: 20.0 # meters — MUST match your LiDAR spec
minimum_time_interval: 0.5 # seconds between processed scans
transform_timeout: 0.2 # seconds to wait for TF
tf_buffer_duration: 30.0 # seconds of TF history to keep
# === Frames ===
odom_frame: odom
map_frame: map
base_frame: base_footprint # or base_link
# === Behavior ===
mode: mapping # mapping | localization
debug_logging: false
throttle_scans: 1 # process 1 out of every N scans
enable_interactive_mode: true # enables RViz2 interactive markers
# === Serialization (save/load) ===
map_file_name: /home/user/my_map
map_start_pose: [0.0, 0.0, 0.0]
map_start_at_dock: falseSaving and Loading Maps
slam_toolbox saves two file types — a standard .pgm/.yaml for nav2_map_server AND a .posegraph for reload/lifelong use:
# --- Save map (two methods) ---
# Method 1: ROS 2 service call (saves .pgm + .yaml + .posegraph)
ros2 service call /slam_toolbox/save_map slam_toolbox/srv/SaveMap \
"{name: {data: '/home/user/maps/my_map'}}"
# Method 2: Serialize posegraph only (for lifelong/localization reload)
ros2 service call /slam_toolbox/serialize_map slam_toolbox/srv/SerializePoseGraph \
"{filename: {data: '/home/user/maps/my_map'}}"
# --- Load map for Nav2 (standard .pgm/.yaml) ---
ros2 run nav2_map_server map_server --ros-args \
-p yaml_filename:=/home/user/maps/my_map.yaml
# --- Deserialize for lifelong or localization mode ---
ros2 service call /slam_toolbox/deserialize_map slam_toolbox/srv/DeserializePoseGraph \
"{filename: {data: '/home/user/maps/my_map'}, \
match_type: {data: 1}}"
# match_type: 1=START_AT_FIRST_NODE, 2=START_AT_GIVEN_POSE, 3=LOCALIZE_AT_POSELocalization-Only Mode (No Map Growth)
Use this when you have a finished map and want pure localization (like AMCL but graph-based):
# In mapper_params_localization.yaml:
slam_toolbox:
ros__parameters:
mode: localization # key change: localization instead of mapping
map_file_name: /home/user/maps/my_map
map_start_at_dock: true # start pose = where map was saved
scan_topic: /scan
odom_frame: odom
map_frame: map
base_frame: base_footprint
resolution: 0.05
max_laser_range: 20.0
# Launch:
ros2 launch slam_toolbox localization_launch.py \
slam_params_file:=mapper_params_localization.yamlvs AMCL: slam_toolbox localization uses graph-based scan matching (much more accurate in featureless corridors) vs AMCL particle filter. slam_toolbox handles dynamic obstacles better but needs more CPU.
Lifelong Mapping (Extend an Existing Map)
# 1. Launch lifelong node with the existing posegraph
ros2 launch slam_toolbox lifelong_launch.py \
slam_params_file:=mapper_params_lifelong.yaml
# mapper_params_lifelong.yaml critical keys:
slam_toolbox:
ros__parameters:
mode: mapping
map_file_name: /home/user/maps/existing_map # load this on start
map_start_at_dock: false
map_start_pose: [0.0, 0.0, 0.0]
# lifelong: keep adding new nodes as robot explores
enable_interactive_mode: true
# 2. Drive the robot into new areas — new nodes extend the graph
# 3. Save the extended map when done:
ros2 service call /slam_toolbox/serialize_map slam_toolbox/srv/SerializePoseGraph \
"{filename: {data: '/home/user/maps/extended_map'}}"RViz2 Integration
Topic: /map, Color Scheme: costmap or raw
Panels → Add → slam_toolbox → SlamToolboxPlugin (shows Save/Serialize buttons)
Click '2D Pose Estimate' in RViz2 toolbar, or publish to /initialpose
Add → MarkerArray, topic /slam_toolbox/graph_visualization
Common Issues
Map drifts / scans don't align
Increase link_match_minimum_response_fine (0.35→0.45). Check max_laser_range doesn't exceed your actual LiDAR range (phantom reflections cause drift). Verify odom TF is being published by your robot.
slam_toolbox crashes with 'Could not get transform'
Ensure base_frame matches your URDF base link name exactly (base_link vs base_footprint). Increase transform_timeout to 0.5s for slow TF publishers. Check tf tree with ros2 run tf2_tools view_frames.
Map not growing in new areas (lifelong mode)
Check minimum_travel_distance — robot must move 0.3m before adding a new scan. In tight spaces, reduce to 0.1m. Verify the deserialized map match_type was START_AT_FIRST_NODE (1) not LOCALIZE_AT_POSE (3).
Next Steps
- → Nav2 Costmap2D guide — feed the slam_toolbox /map into Nav2 global costmap
- → Nav2 SMAC Planner guide — plan paths over the SLAM-generated map
- → ROS 2 PointCloud2 & PCL guide — filter 3D point clouds before projecting to 2D LaserScan for SLAM