Skip to main content
🗺️ Navigation GuideUpdated June 2026

ROS 2 SLAM & Navigation Guide 2026slam_toolbox · RTAB-Map · Nav2

Complete 2026 guide to autonomous robot navigation in ROS 2 Jazzy/Humble. Covers slam_toolbox online_async setup, RTAB-Map visual SLAM with RealSense, Nav2 costmap tuning, BT Navigator, and Python NavigateToPose client — with working code examples for real robots.

slam_toolboxRTAB-MapNav2NavigateToPoseCostmaps

ROS 2 Nav Stack Architecture

Perception

  • LiDAR → LaserScan
  • RGB-D → PointCloud2
  • IMU → sensor_msgs/Imu

Localization & Mapping

  • slam_toolbox (online_async)
  • RTAB-Map (visual SLAM)
  • AMCL (localization only)

Navigation

  • Nav2 BT Navigator
  • Global planner (NavFn/Smac)
  • Controller (RPP/DWB)

slam_toolbox — Lidar SLAM Setup

slam_toolbox is the official ROS 2 SLAM solution. online_async mode is recommended for real robots — it never blocks your odometry update loop.

Install SLAM Toolbox & Nav2

One-line install on Ubuntu 24.04 with ROS 2 Jazzy.

bash
sudo apt update
sudo apt install -y \
  ros-jazzy-slam-toolbox \
  ros-jazzy-navigation2 \
  ros-jazzy-nav2-bringup \
  ros-jazzy-robot-localization \
  ros-jazzy-twist-mux

# Verify
ros2 pkg list | grep slam_toolbox
ros2 pkg list | grep nav2

Launch SLAM Toolbox (online async)

online_async_slam is the recommended mode for real robots — non-blocking scan processing.

bash
# online_async: processes scans asynchronously (never blocks odometry)
ros2 launch slam_toolbox online_async_launch.py \
  slam_params_file:=./config/mapper_params_online_async.yaml \
  use_sim_time:=false

# online_sync: processes every scan before returning (safer for fast scanners)
# ros2 launch slam_toolbox online_sync_launch.py

online_async is correct for most robots. Use online_sync only with very slow scan rates (<10 Hz).

mapper_params_online_async.yaml

Key parameters for real-world SLAM quality.

yaml
slam_toolbox:
  ros__parameters:
    # Solver — use ceres for best loop closure accuracy
    solver_plugin: solver_plugins::CeresSolver
    ceres_linear_solver: SPARSE_NORMAL_CHOLESKY
    ceres_preconditioner: SCHUR_JACOBI

    # Scan matcher
    odom_frame: odom
    map_frame: map
    base_frame: base_footprint

    # Scan topic — must publish sensor_msgs/LaserScan
    scan_topic: /scan

    # Mapping settings
    resolution: 0.05          # 5 cm cells — increase for larger environments
    max_laser_range: 20.0     # metres
    minimum_travel_distance: 0.5   # trigger scan only after 0.5 m movement
    minimum_travel_heading: 0.5    # or after 0.5 rad rotation

    # Loop closure
    link_match_minimum_response_fine: 0.45
    link_scan_maximum_distance: 1.5
    do_loop_closing: true
    loop_search_maximum_distance: 3.0
    loop_match_minimum_chain_size: 10

    # Save on exit
    map_file_name: /tmp/my_map

RTAB-Map — Visual SLAM

RTAB-Map (Real-Time Appearance-Based Mapping) builds 3D maps from RGB-D cameras and provides 2D occupancy grids for Nav2. Best for indoor visual SLAM without lidar.

Install RTAB-Map

RTAB-Map supports both 2D lidar and RGB-D cameras for visual SLAM.

bash
# Binary install (recommended)
sudo apt install ros-jazzy-rtabmap-ros

# Docker (pre-built with all optional deps including GTSAM, g2o, OpenCV CUDA)
docker pull introlab3it/rtabmap:jazzy

Launch RTAB-Map with 2D Lidar

Combines wheel odometry and lidar for robust loop-closure SLAM.

bash
ros2 launch rtabmap_launch rtabmap.launch.py \
  rtabmap_args:="--delete_db_on_start" \
  frame_id:=base_footprint \
  subscribe_scan:=true \
  scan_topic:=/scan \
  odom_topic:=/odom \
  qos:=1 \
  rviz:=true

Launch RTAB-Map with RGB-D Camera

Visual SLAM with Intel RealSense D435i — no lidar required.

bash
# Start RealSense node first
ros2 launch realsense2_camera rs_launch.py \
  align_depth.enable:=true \
  pointcloud.enable:=true

# Launch RTAB-Map
ros2 launch rtabmap_launch rtabmap.launch.py \
  rtabmap_args:="--delete_db_on_start" \
  rgb_topic:=/camera/camera/color/image_raw \
  depth_topic:=/camera/camera/aligned_depth_to_color/image_raw \
  camera_info_topic:=/camera/camera/color/camera_info \
  frame_id:=base_footprint \
  approx_sync:=false \
  rviz:=true

RTAB-Map creates a 3D point cloud map AND a 2D occupancy grid for Nav2.

Save & Load Maps

Save and Load Maps

Save your SLAM map to disk for repeated use in localization-only mode.

bash
# Save the current map (while SLAM is running)
ros2 run nav2_map_server map_saver_cli \
  -f /home/user/maps/my_map \
  --ros-args -p save_map_timeout:=5.0

# Output: my_map.yaml + my_map.pgm

# Load saved map for navigation (localization only, SLAM off)
ros2 launch nav2_bringup localization_launch.py \
  map:=/home/user/maps/my_map.yaml \
  use_sim_time:=false

# SLAM Toolbox serialized map (recommended for re-localization)
ros2 service call /slam_toolbox/save_map slam_toolbox/srv/SaveMap \
  "{name: {data: '/home/user/maps/my_map'}}"

Nav2 — Autonomous Navigation

Nav2 is the ROS 2 navigation stack. It uses a Behavior Tree navigator, separate global (path) and local (controller) costmaps, and action servers for goal sending.

Launch Nav2 Navigation Stack

Full Nav2 with BT navigator, costmaps, and path planner.

bash
# Nav2 bringup (assumes map already loaded or SLAM running)
ros2 launch nav2_bringup navigation_launch.py \
  use_sim_time:=false \
  params_file:=./config/nav2_params.yaml

# OR combine SLAM + Nav2 in one launch
ros2 launch nav2_bringup slam_launch.py \
  use_sim_time:=false \
  params_file:=./config/nav2_params.yaml

nav2_params.yaml — Key Settings

Critical Nav2 parameters for real-robot performance.

yaml
bt_navigator:
  ros__parameters:
    global_frame: map
    robot_base_frame: base_link
    odom_topic: /odom
    bt_loop_duration: 10          # ms per BT tick
    default_server_timeout: 20
    # Use the default BT XML for point-to-point nav
    default_nav_to_pose_bt_xml: >
      /opt/ros/jazzy/share/nav2_bt_navigator/behavior_trees/navigate_w_replanning_and_recovery.xml

planner_server:
  ros__parameters:
    expected_planner_frequency: 20.0
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"
      tolerance: 0.5
      use_astar: false          # Dijkstra by default; set true for A*
      allow_unknown: true

controller_server:
  ros__parameters:
    controller_frequency: 20.0
    controller_plugins: ["FollowPath"]
    FollowPath:
      plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"
      desired_linear_vel: 0.5
      lookahead_dist: 0.6
      min_lookahead_dist: 0.3
      max_lookahead_dist: 0.9
      use_velocity_scaled_lookahead_dist: true
      min_approach_linear_velocity: 0.05
      approach_velocity_scaling_dist: 1.0
      use_collision_detection: true
      max_allowed_time_to_collision_up_to_goal: 1.0

local_costmap:
  local_costmap:
    ros__parameters:
      update_frequency: 5.0
      publish_frequency: 2.0
      global_frame: odom
      robot_base_frame: base_link
      rolling_window: true
      width: 3
      height: 3
      resolution: 0.05
      footprint: "[[0.25,0.25],[0.25,-0.25],[-0.25,-0.25],[-0.25,0.25]]"

global_costmap:
  global_costmap:
    ros__parameters:
      update_frequency: 1.0
      publish_frequency: 1.0
      global_frame: map
      robot_base_frame: base_link
      resolution: 0.05
      track_unknown_space: true

Send Navigation Goals via Python

Use the NavigateToPose action client to command autonomous navigation.

python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.action import ActionClient
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped
from builtin_interfaces.msg import Duration


class Nav2Client(Node):
    def __init__(self):
        super().__init__("nav2_client")
        self._client = ActionClient(self, NavigateToPose, "navigate_to_pose")

    def send_goal(self, x: float, y: float, yaw: float = 0.0):
        self._client.wait_for_server()

        goal = NavigateToPose.Goal()
        goal.pose = PoseStamped()
        goal.pose.header.frame_id = "map"
        goal.pose.header.stamp = self.get_clock().now().to_msg()
        goal.pose.pose.position.x = x
        goal.pose.pose.position.y = y
        # Simplified: set orientation via quaternion (yaw=0 → facing +X)
        import math
        goal.pose.pose.orientation.z = math.sin(yaw / 2)
        goal.pose.pose.orientation.w = math.cos(yaw / 2)

        self.get_logger().info(f"Navigating to ({x}, {y})")
        future = self._client.send_goal_async(
            goal,
            feedback_callback=self._feedback_callback
        )
        future.add_done_callback(self._goal_response_callback)

    def _goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().error("Goal rejected!")
            return
        self.get_logger().info("Goal accepted")
        result_future = goal_handle.get_result_async()
        result_future.add_done_callback(self._result_callback)

    def _result_callback(self, future):
        result = future.result().result
        self.get_logger().info("Navigation complete!")

    def _feedback_callback(self, feedback_msg):
        f = feedback_msg.feedback
        self.get_logger().info(
            f"Distance remaining: {f.distance_remaining:.2f} m"
        )


def main():
    rclpy.init()
    node = Nav2Client()
    node.send_goal(x=2.0, y=1.5, yaw=0.0)
    rclpy.spin(node)
    rclpy.shutdown()


if __name__ == "__main__":
    main()

💡 NavigateToPose is the primary Nav2 action. Use NavigateThroughPoses for waypoint sequences.

Nav2 Planner Comparison

PlannerAlgorithmBest ForSpeed
NavFnDijkstra / A*Simple open spaces⚡⚡⚡
Smac Hybrid-A*Hybrid A* + SE2 latticeCar-like / Ackermann robots⚡⚡
Smac 2DA* on 2D costmapOmnidirectional robots⚡⚡⚡
ThetaStarAny-angle A*Smooth paths in tight spaces⚡⚡

Common Issues & Fixes

ProblemTF tree broken: base_link → odom
FixStart robot_state_publisher + joint_state_publisher. Check `ros2 run tf2_tools view_frames`.
ProblemSLAM map drifts badly
FixTune minimum_travel_distance / heading to prevent over-mapping. Ensure lidar scans are dense enough for loop closure.
ProblemNav2 controller oscillates near goal
FixReduce max_lookahead_dist or tune approach_velocity_scaling_dist. Check goal_checker tolerance.
ProblemCostmap shows no obstacles
FixVerify LaserScan frame_id matches your TF tree. Use `ros2 topic echo /scan` and confirm header.frame_id.
ProblemAMCL localization jumps
FixIncrease particle count (max_particles: 5000). Provide better initial pose via RViz '2D Pose Estimate'.
ProblemNav2 lifecycle nodes won't activate
FixRun `ros2 service call /lifecycle_manager_navigation nav2_msgs/srv/ManageLifecycleNodes '{command: 0}'` to reset.

Related Guides