Skip to main content
🗺️ SLAM

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

ModePluginUse Case
Online Asyncasync_slam_toolbox_nodeReal-time mapping while driving — most common setup
Online Syncsync_slam_toolbox_nodeGuaranteed processing of every scan (slower, for rosbag replay)
Lifelong Mappinglifelong_slam_toolbox_nodeExtend an existing saved map with new areas
Localization Onlylocalization_slam_toolbox_nodePure localization against a pre-saved .posegraph map
Map Mergingmap_merging_slam_toolbox_nodeMerge 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: false

Saving 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_POSE

Localization-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.yaml

vs 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

Add Map display
Topic: /map, Color Scheme: costmap or raw
Add slam_toolbox panel
Panels → Add → slam_toolbox → SlamToolboxPlugin (shows Save/Serialize buttons)
Set initial pose
Click '2D Pose Estimate' in RViz2 toolbar, or publish to /initialpose
Watch pose graph
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