By ihsumlee , 9 June 2026
content

Position in the full course

This is the sixth step of the course.

The full learning order is:

1. ROS 2 node/topic/launch review
2. TF2 frame concept
3. Static sensor frames
4. Dynamic odometry frame
5. URDF + robot_state_publisher
6. SLAM mapping
7. AMCL localization
8. Nav2 path planning
9. RViz goal navigation
10. TF2 debugging in Nav2

In Step 5, you used URDF and robot_state_publisher to publish robot structure frames:

base_link
 β”œβ”€β”€ base_laser
 β”œβ”€β”€ camera_link
 β”œβ”€β”€ imu_link
 β”œβ”€β”€ left_wheel_link
 └── right_wheel_link

In Step 6, you will use SLAM to build a map and add:

map β†’ odom

After this step, your TF tree should become:

map
 └── odom
      └── base_link
            β”œβ”€β”€ base_laser
            β”œβ”€β”€ camera_link
            β”œβ”€β”€ imu_link
            β”œβ”€β”€ left_wheel_link
            └── right_wheel_link

1. Learning objectives

After this module, you should be able to:

  1. Explain what SLAM means.
  2. Explain why SLAM is needed before map-based navigation.
  3. Explain what slam_toolbox does.
  4. Understand the required inputs for 2D LiDAR SLAM.
  5. Check whether /scan, /odom, and TF are available.
  6. Launch slam_toolbox.
  7. Visualize /map, /scan, and TF in RViz2.
  8. Drive the robot to build a map.
  9. Save the generated map.
  10. Understand how the saved map will be used in AMCL localization.

2. Suggested teaching duration

Suggested duration: 2 hours

| Time | Activity | | ----------- | ------------------------------------ | | 0–15 min | Review TF tree from Step 5 | | 15–30 min | Explain SLAM and occupancy grid maps | | 30–45 min | Explain SLAM inputs and outputs | | 45–65 min | Check /scan, /odom, and TF | | 65–85 min | Launch slam_toolbox | | 85–105 min | Build and visualize the map | | 105–115 min | Save the map | | 115–120 min | Summary and homework |


3. What is SLAM?

SLAM means:

Simultaneous Localization and Mapping

This means the robot does two things at the same time:

1. It estimates where it is.
2. It builds a map of the environment.

For example, when an AMR enters an unknown classroom, it does not already have a map.

The robot uses:

LiDAR scan
odometry
TF frames

to estimate its motion and build an occupancy grid map.


4. What is an occupancy grid map?

A 2D occupancy grid map divides the environment into small grid cells.

Each cell may represent:

free space
occupied space
unknown space

In RViz2, the map usually looks like:

white area: free space
black area: obstacle or wall
gray area: unknown space

The map is usually published on:

/map

The message type is:

nav_msgs/msg/OccupancyGrid

Later, Nav2 will use this map for localization and path planning.


5. What does slam_toolbox do?

slam_toolbox is a common ROS 2 package for 2D LiDAR SLAM.

In mapping mode, it usually does the following:

1. Subscribes to LiDAR scan data.
2. Uses odometry and TF to estimate robot movement.
3. Performs scan matching.
4. Builds a pose graph.
5. Detects loop closure when the robot revisits an area.
6. Publishes the map.
7. Publishes the transform map β†’ odom.

For this course, you only need the beginner-level idea:

slam_toolbox uses /scan and odometry-related TF to create /map and map β†’ odom.


6. Required inputs for SLAM mapping

Before launching SLAM, you should already have these items.

6.1 LiDAR scan topic

Usually:

/scan

Message type:

sensor_msgs/msg/LaserScan

Check it:

ros2 topic list
ros2 topic type /scan
ros2 topic echo /scan --once

If /scan does not exist, SLAM cannot build a 2D LiDAR map.


6.2 Odometry topic

Usually:

/odom

Message type:

nav_msgs/msg/Odometry

Check it:

ros2 topic list
ros2 topic type /odom
ros2 topic echo /odom --once

6.3 TF tree

You need at least:

odom β†’ base_link
base_link β†’ base_laser

The first transform tells SLAM how the robot moves.

The second transform tells SLAM where the LiDAR is mounted on the robot.

Check:

ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link base_laser

If both transforms exist, the partial TF tree should be:

odom
 └── base_link
      └── base_laser

After SLAM starts, the TF tree should become:

map
 └── odom
      └── base_link
            └── base_laser

7. Important warning about Step 4 fake odometry

In Step 4, you created a fake odometry node.

That node is useful for learning TF2, but it is not enough for real SLAM unless you also have a real or simulated LiDAR scan.

For SLAM, you need:

real or simulated /scan
valid odom β†’ base_link
valid base_link β†’ base_laser
robot movement

So for classroom practice, the easiest option is usually:

Use a simulated robot that already publishes /scan and /odom.

For a real AMR, you should use:

real LiDAR driver
real wheel odometry
URDF or static TF for sensor frames

8. Install required packages

Open a terminal:

source /opt/ros/jazzy/setup.bash

Install SLAM Toolbox and Nav2 map server tools:

sudo apt update
sudo apt install ros-jazzy-slam-toolbox \
                 ros-jazzy-navigation2 \
                 ros-jazzy-nav2-bringup \
                 ros-jazzy-nav2-map-server \
                 ros-jazzy-rviz2 \
                 ros-jazzy-tf2-tools

If you use TurtleBot simulation for classroom practice, also install the minimal TurtleBot simulation packages:

sudo apt install ros-jazzy-nav2-minimal-tb*

9. Classroom practice option A: Use TurtleBot simulation

This is the recommended first classroom practice because it gives you:

simulated robot
simulated LiDAR
/scan topic
/odom topic
TF tree
RViz2
Gazebo

Terminal 1: Launch TurtleBot simulation with SLAM enabled

source /opt/ros/jazzy/setup.bash
ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False slam:=True

This starts a simulation environment with SLAM.

In RViz2, you should see the robot, LiDAR scan, and map.


Terminal 2: Drive the robot

You can use teleoperation if available:

source /opt/ros/jazzy/setup.bash
ros2 run teleop_twist_keyboard teleop_twist_keyboard

If the package is missing, install it:

sudo apt install ros-jazzy-teleop-twist-keyboard

Drive the robot slowly around the environment.

You should see the map grow in RViz2.


10. Classroom practice option B: Launch slam_toolbox manually

If you already have your robot interface running, launch SLAM Toolbox manually.

You must already have:

LiDAR publishing /scan
Odometry publishing /odom
TF publishing odom β†’ base_link
TF publishing base_link β†’ base_laser

Launch SLAM Toolbox:

source /opt/ros/jazzy/setup.bash
ros2 launch slam_toolbox online_async_launch.py

If your installation does not provide online_async_launch.py, use:

ros2 launch slam_toolbox online_sync_launch.py

11. Verify SLAM output

Open a new terminal:

source /opt/ros/jazzy/setup.bash

Check the map topic:

ros2 topic list | grep map
ros2 topic type /map
ros2 topic echo /map --once

Expected type:

nav_msgs/msg/OccupancyGrid

Check the TF tree:

ros2 run tf2_tools view_frames
xdg-open frames.pdf

Expected TF tree:

map
 └── odom
      └── base_link
            └── base_laser

If you also use the URDF from Step 5, you may see:

map
 └── odom
      └── base_link
            β”œβ”€β”€ base_laser
            β”œβ”€β”€ camera_link
            β”œβ”€β”€ imu_link
            β”œβ”€β”€ left_wheel_link
            └── right_wheel_link

12. Configure RViz2 for SLAM mapping

Open RViz2:

rviz2

Set:

Fixed Frame = map

Add these displays:

Map
LaserScan
TF
RobotModel
Odometry

Recommended settings:

| Display | Setting | | ---------- | -------------------------------- | | Map | Topic: /map | | LaserScan | Topic: /scan | | TF | Show Names: enabled | | RobotModel | Description: robot_description | | Odometry | Topic: /odom |

You should observe:

The robot moves.
The LiDAR scan updates.
The map grows.
TF frames stay connected.

13. Mapping procedure

A good mapping procedure is more important than only launching SLAM.

Use this process:

Step 1: Start near a clear area

Start the robot in an open, recognizable area.

Avoid starting too close to walls or obstacles.


Step 2: Move slowly

Move slowly during mapping.

Good mapping movement:

slow forward motion
slow turns
smooth paths
return to previous areas

Bad mapping movement:

fast rotation
wheel slipping
sudden acceleration
hitting objects
moving people near the robot

Step 3: Cover the whole environment

Drive the robot along walls and corridors.

Try to observe all important areas.

For a classroom:

front area
back area
left wall
right wall
door area
teacher desk area
student desk area

Step 4: Create loop closure

A loop closure means the robot returns to an area it has already seen.

This helps SLAM correct accumulated error.

Example:

Start near the door.
Drive around the room.
Return near the door.

If loop closure works, the map usually becomes more consistent.


Step 5: Stop and inspect the map

Before saving the map, check:

Are the walls straight?
Are there duplicate walls?
Are important obstacles visible?
Is the map complete enough?
Is the robot still localized correctly?

14. Save the map

Create a folder for maps:

mkdir -p ~/ros2_ws/maps

Save the map:

ros2 run nav2_map_server map_saver_cli -f ~/ros2_ws/maps/classroom_map

This should create files similar to:

classroom_map.pgm
classroom_map.yaml

The .pgm file stores the map image.

The .yaml file stores map metadata, such as:

image file path
resolution
origin
occupied threshold
free threshold

15. Check the saved map files

Run:

ls ~/ros2_ws/maps

Expected result:

classroom_map.pgm
classroom_map.yaml

Open the image:

xdg-open ~/ros2_ws/maps/classroom_map.pgm

Open the YAML file:

cat ~/ros2_ws/maps/classroom_map.yaml

You should see something similar to:

image: classroom_map.pgm
mode: trinary
resolution: 0.05
origin: [0.0, 0.0, 0.0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25

16. Create your own mapping launch package

This part is useful if you want a clean classroom package.

Create a package:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src

ros2 pkg create --build-type ament_python --license Apache-2.0 amr_mapping

Create folders:

cd ~/ros2_ws/src/amr_mapping
mkdir launch config maps

17. Create a simple SLAM launch file

Create:

nano launch/online_mapping.launch.py

Paste:

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():

    package_name = 'amr_mapping'

    slam_config = os.path.join(
        get_package_share_directory(package_name),
        'config',
        'mapper_params_online_async.yaml'
    )

    slam_toolbox_node = Node(
        package='slam_toolbox',
        executable='async_slam_toolbox_node',
        name='slam_toolbox',
        output='screen',
        parameters=[slam_config]
    )

    rviz_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen'
    )

    return LaunchDescription([
        slam_toolbox_node,
        rviz_node
    ])

This launch file starts:

slam_toolbox
rviz2

It assumes your robot interface is already running.


18. Create a SLAM parameter file

Create:

nano config/mapper_params_online_async.yaml

Paste this beginner configuration:

slam_toolbox:
  ros__parameters:

    use_sim_time: false

    odom_frame: odom
    map_frame: map
    base_frame: base_link
    scan_topic: /scan

    mode: mapping

    debug_logging: false
    throttle_scans: 1
    transform_publish_period: 0.02
    map_update_interval: 1.0
    resolution: 0.05
    max_laser_range: 12.0
    minimum_time_interval: 0.5
    transform_timeout: 0.2
    tf_buffer_duration: 30.0
    stack_size_to_use: 40000000

    use_scan_matching: true
    use_scan_barycenter: true

    minimum_travel_distance: 0.1
    minimum_travel_heading: 0.1

    do_loop_closing: true

Important settings:

| Parameter | Meaning | | ----------------- | ---------------------------------- | | odom_frame | Your odometry frame | | map_frame | Your map frame | | base_frame | Your robot body frame | | scan_topic | Your LiDAR topic | | resolution | Map resolution in meters per pixel | | max_laser_range | Maximum LiDAR range used | | do_loop_closing | Enables loop closure |


19. Modify setup.py

Open:

nano setup.py

Add imports at the top:

import os
from glob import glob

Modify data_files:

data_files=[
    ('share/ament_index/resource_index/packages',
        ['resource/' + package_name]),
    ('share/' + package_name, ['package.xml']),
    (os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
    (os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
    (os.path.join('share', package_name, 'maps'), glob('maps/*')),
],

20. Modify package.xml

Open:

nano package.xml

Add dependencies before the closing </package> tag:

<exec_depend>slam_toolbox</exec_depend>
<exec_depend>rviz2</exec_depend>
<exec_depend>nav2_map_server</exec_depend>
<exec_depend>tf2_tools</exec_depend>

21. Build the mapping package

Run:

cd ~/ros2_ws
colcon build --packages-select amr_mapping
source install/setup.bash

22. Run your mapping launch file

Before running this launch file, make sure your robot interface is already running.

You need:

ros2 topic echo /scan --once
ros2 topic echo /odom --once
ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link base_laser

Then run:

ros2 launch amr_mapping online_mapping.launch.py

In RViz2:

Set Fixed Frame to map.
Add Map, LaserScan, TF, RobotModel, and Odometry.

Drive the robot slowly and build the map.


23. Save the map after using your package

Save the map:

ros2 run nav2_map_server map_saver_cli -f ~/ros2_ws/maps/my_amr_map

Expected output files:

my_amr_map.pgm
my_amr_map.yaml

24. How Step 6 connects to Step 7

In Step 6, SLAM creates a map.

The map files are:

classroom_map.pgm
classroom_map.yaml

In Step 7, you will use the saved map for localization.

That means:

Step 6: Build and save the map.
Step 7: Load the saved map and localize the robot using AMCL.

During SLAM mapping, slam_toolbox provides:

map β†’ odom
/map

During AMCL localization, AMCL will provide:

map β†’ odom

So after Step 7 starts, you should not also run SLAM mapping at the same time unless you intentionally want navigation while mapping.


25. Common problems and solutions

Problem 1: /scan does not exist

Check:

ros2 topic list | grep scan

Possible reasons:

LiDAR driver is not running.
Simulation LiDAR plugin is not running.
LiDAR topic has a different name.

Solution:

Start the LiDAR driver or simulation.
Check the real scan topic name.
Change scan_topic in the SLAM parameter file if needed.

Problem 2: /map does not appear

Possible reasons:

slam_toolbox is not running.
No /scan data is received.
TF from odom to base_link is missing.
TF from base_link to base_laser is missing.
scan_topic parameter is wrong.

Check:

ros2 topic list
ros2 topic echo /scan --once
ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link base_laser

Problem 3: RViz2 shows β€œNo transform”

Possible reasons:

Fixed Frame is wrong.
map β†’ odom is not published.
odom β†’ base_link is missing.
base_link β†’ base_laser is missing.
Frame names do not match the parameter file.

Solution:

Set Fixed Frame to map.
Check view_frames.
Check parameter names: map, odom, base_link, base_laser.

Problem 4: Map is distorted

Possible reasons:

Odometry is poor.
Robot moves too fast.
Wheel slipping occurs.
LiDAR frame is wrong.
LiDAR range setting is wrong.
Environment has many moving objects.

Solution:

Move slower.
Avoid moving people.
Check base_link β†’ base_laser.
Improve odometry.
Try loop closure by returning to a known area.

Problem 5: Map is rotated or flipped

Possible reasons:

LiDAR frame orientation is wrong.
URDF sensor frame is wrong.
The LiDAR driver uses a different frame convention.

Check:

ros2 run tf2_ros tf2_echo base_link base_laser

Also inspect the LaserScan display in RViz2.


Problem 6: Saving map fails

Possible reasons:

nav2_map_server is not installed.
There is no /map topic.
The target folder does not exist.
Permission problem.

Solution:

sudo apt install ros-jazzy-nav2-map-server
mkdir -p ~/ros2_ws/maps
ros2 topic echo /map --once
ros2 run nav2_map_server map_saver_cli -f ~/ros2_ws/maps/test_map

26. Practice assignment

Assignment title

Build and Save a 2D Map Using slam_toolbox

Task 1: Check required topics and TF

Submit screenshots of:

ros2 topic list
ros2 topic echo /scan --once
ros2 topic echo /odom --once
ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link base_laser

Task 2: Launch SLAM

Run either:

ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False slam:=True

or:

ros2 launch slam_toolbox online_async_launch.py

Submit a screenshot showing SLAM running.


Task 3: Build the map

Drive the robot slowly.

Submit a screenshot of RViz2 showing:

Map
LaserScan
TF
RobotModel

Task 4: Save the map

Run:

mkdir -p ~/ros2_ws/maps
ros2 run nav2_map_server map_saver_cli -f ~/ros2_ws/maps/classroom_map

Submit screenshots of:

ls ~/ros2_ws/maps

Expected files:

classroom_map.pgm
classroom_map.yaml

Task 5: Explain your map

Write a short explanation:

The scan topic is:
The odometry topic is:
The map topic is:
The map frame is:
The odom frame is:
The robot base frame is:
The LiDAR frame is:
The saved map files are:
One problem I observed during mapping was:
How I fixed or would fix it:

27. Mini quiz

  1. What does SLAM mean?
  2. What topic usually provides LiDAR data?
  3. What message type is used by /scan?
  4. What topic publishes the occupancy grid map?
  5. What message type is used by /map?
  6. What transform is added by SLAM?
  7. Why does SLAM need odom β†’ base_link?
  8. Why does SLAM need base_link β†’ base_laser?
  9. Why should the robot move slowly during mapping?
  10. Why is loop closure useful?
  11. What command saves the generated map?
  12. What two files are usually created when saving a map?

28. Key summary

In this module, you learned how to build a map using SLAM.

Before SLAM, you need:

LiDAR scan: /scan
Odometry: /odom
TF: odom β†’ base_link
TF: base_link β†’ base_laser

After SLAM starts, you should get:

Map topic: /map
TF: map β†’ odom

Your TF tree becomes:

map
 └── odom
      └── base_link
            └── base_laser

If you also use the URDF model from Step 5, the tree becomes:

map
 └── odom
      └── base_link
            β”œβ”€β”€ base_laser
            β”œβ”€β”€ camera_link
            β”œβ”€β”€ imu_link
            β”œβ”€β”€ left_wheel_link
            └── right_wheel_link

You saved the map as:

classroom_map.pgm
classroom_map.yaml

In the next module, you will learn:

AMCL localization

The saved map from this module will be loaded in Step 7, and AMCL will localize the robot on that map.

Tags