나만의 Gazebo World와 로봇으로 TurtleBot3 SLAM·Nav2 실행하기

1. 프로젝트 소개

이번 실습에서는 학생이 직접 Gazebo World를 설계하고, 자신이 만든 차동구동 모바일 로봇 모델을 해당 World에 배치합니다.

로봇에는 다음 기능을 적용합니다.

  1. 차동구동 Differential Drive
  2. /cmd_vel 기반 속도 제어
  3. /odom 오도메트리 발행
  4. /scan LiDAR 데이터 발행
  5. odom, base_footprint, base_link, laser TF 구성
  6. TurtleBot3 Cartographer를 이용한 SLAM
  7. Nav2를 이용한 자율주행

Gazebo에서 계산된 LiDAR 데이터를 ROS 2의 /scan 토픽으로 사용하려면 Gazebo 센서와 ROS 2 메시지를 연결하는 플러그인이 필요합니다. 로봇 모델은 robot_state_publisher를 통해 TF를 발행하고, spawn_entity.py를 통해 Gazebo World에 생성합니다.

프로젝트의 최종 실행 구조는 다음과 같습니다.

학생이 제작한 World
        ↓
학생이 제작한 모바일 로봇
        ↓
Gazebo Differential Drive Plugin
        ├── /cmd_vel 구독
        ├── /odom 발행
        └── odom → base_footprint TF 발행
        ↓
Gazebo LiDAR Plugin
        └── /scan 발행
        ↓
TurtleBot3 Cartographer
        ├── map 생성
        └── map → odom TF 발행
        ↓
Nav2
        ├── AMCL 위치 추정
        ├── 전역 경로 생성
        ├── 지역 경로 추종
        └── 장애물 회피

TurtleBot3 공식 ROS 2 Humble 실습에서는 Cartographer를 기본 SLAM 방법으로 실행하고, 생성된 지도를 nav2_map_server로 저장한 뒤 turtlebot3_navigation2 패키지에서 불러오는 흐름을 사용합니다.

2. 실습 환경

이번 프로젝트는 다음 환경을 기준으로 합니다.

운영체제    Ubuntu 22.04
ROS 버전   ROS 2 Humble
시뮬레이터 Gazebo Classic 11
SLAM       TurtleBot3 Cartographer
Navigation Nav2
빌드 도구  colcon
패키지 형식 ament_cmake

Gazebo Classic은 2025년 1월에 공식 지원이 종료되었습니다. 다만 ROS 2 Humble 기반 기존 교육 자료와 gazebo_ros 플러그인을 그대로 활용하기 위해 이번 4시간 실습에서는 Gazebo Classic을 사용합니다. 신규 프로젝트는 ROS 2 Jazzy와 Gazebo Harmonic으로 이전하는 것이 좋습니다.

3. 프로젝트 목표

학생은 4시간 동안 다음 결과물을 완성해야 합니다.

  1. 자신만의 Gazebo World 제작
  2. 자신만의 모바일 로봇 모델 제작
  3. 로봇을 Gazebo World에 Spawn
  4. 키보드로 로봇 주행
  5. /scan, /odom, /tf 데이터 확인
  6. Cartographer를 이용한 지도 작성
  7. 작성한 지도 저장
  8. Nav2를 이용한 목표점 자율주행
  9. 장애물 회피 결과 확인

로봇 외형은 학생마다 달라도 됩니다. 그러나 TurtleBot3 SLAM과 Nav2를 사용하려면 ROS 인터페이스를 다음 규칙에 맞춰야 합니다.

속도 명령 토픽       /cmd_vel
오도메트리 토픽     /odom
LiDAR 토픽          /scan
오도메트리 프레임   odom
로봇 기준 프레임    base_footprint
로봇 본체 프레임    base_link
LiDAR 프레임        laser

Nav2 공식 문서에서도 TurtleBot3 이외의 로봇을 사용할 경우 해당 로봇의 URDF, 센서, 컨트롤러와 인터페이스를 준비해야 한다고 설명합니다. SLAM 노드는 /mapmap → odom TF를 제공하고, 로봇은 오도메트리와 센서 데이터를 제공해야 합니다.

4. 프로젝트 패키지 생성

워크스페이스를 만듭니다.

mkdir -p ~/student_nav_ws/src
cd ~/student_nav_ws/src

패키지를 생성합니다.

ros2 pkg create student_world_nav2 \
  --build-type ament_cmake

필요한 폴더를 만듭니다.

cd ~/student_nav_ws/src/student_world_nav2

mkdir -p launch
mkdir -p urdf
mkdir -p worlds
mkdir -p maps

파일을 생성합니다.

touch launch/simulation.launch.py
touch urdf/student_robot.urdf.xacro
touch worlds/student_world.world

최종 폴더 구조는 다음과 같습니다.

student_nav_ws/
└── src/
    └── student_world_nav2/
        ├── CMakeLists.txt
        ├── package.xml
        ├── launch/
        │   └── simulation.launch.py
        ├── urdf/
        │   └── student_robot.urdf.xacro
        ├── worlds/
        │   └── student_world.world
        └── maps/

5. package.xml 작성

package.xml 파일을 다음과 같이 작성합니다.

<?xml version="1.0"?>
<package format="3">
  <name>student_world_nav2</name>
  <version>0.0.1</version>

  <description>
    Student mini project for Gazebo World, SLAM and Nav2
  </description>

  <maintainer email="student@example.com">
    student
  </maintainer>

  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <exec_depend>ament_index_python</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>

  <exec_depend>xacro</exec_depend>
  <exec_depend>robot_state_publisher</exec_depend>
  <exec_depend>joint_state_publisher</exec_depend>

  <exec_depend>gazebo_ros</exec_depend>
  <exec_depend>geometry_msgs</exec_depend>
  <exec_depend>nav_msgs</exec_depend>
  <exec_depend>sensor_msgs</exec_depend>
  <exec_depend>tf2_ros</exec_depend>

  <exec_depend>turtlebot3_cartographer</exec_depend>
  <exec_depend>turtlebot3_navigation2</exec_depend>
  <exec_depend>turtlebot3_teleop</exec_depend>

  <exec_depend>nav2_bringup</exec_depend>
  <exec_depend>nav2_map_server</exec_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

<buildtool_depend>ament_cmake</buildtool_depend>

현재 패키지가 ament_cmake 방식으로 빌드된다는 의미입니다.

<exec_depend>gazebo_ros</exec_depend>

Gazebo 실행, 모델 Spawn, Gazebo ROS 플러그인을 사용할 때 필요합니다.

<exec_depend>xacro</exec_depend>

Xacro 파일을 URDF XML 문자열로 변환할 때 필요합니다.

<exec_depend>robot_state_publisher</exec_depend>

URDF에 정의된 링크와 조인트 관계를 기반으로 TF를 발행합니다.

6. CMakeLists.txt 작성

CMakeLists.txt 파일을 다음과 같이 작성합니다.

cmake_minimum_required(VERSION 3.8)
project(student_world_nav2)

find_package(ament_cmake REQUIRED)

install(
  DIRECTORY
    launch
    urdf
    worlds
    maps
  DESTINATION
    share/${PROJECT_NAME}
)

ament_package()

ROS 2에서는 소스 폴더의 파일을 직접 실행하는 것이 아니라, 빌드 과정에서 install 폴더로 복사한 파일을 실행합니다.

install(
  DIRECTORY launch urdf worlds maps
  DESTINATION share/${PROJECT_NAME}
)

위 설정은 다음 파일을 설치 공간으로 복사합니다.

launch 파일
URDF/Xacro 파일
Gazebo World 파일
저장된 지도 파일

이 설정이 없으면 다음과 같은 오류가 발생할 수 있습니다.

launch file was not found
world file was not found
package share directory does not contain urdf

7. 학생별 World 설계 조건

학생은 다음 3가지 유형 중 하나를 선택합니다.

1) 장애물 World

다음 요소를 포함합니다.

  1. 외곽 벽 4개
  2. 크기가 다른 장애물 5개 이상
  3. 출발 구역 1개
  4. 목표 구역 2개
  5. 로봇이 회전할 수 있는 공간

2) Corridor World

다음 요소를 포함합니다.

  1. 직선 복도
  2. 폭이 좁아지는 구간
  3. ㄱ자 코너
  4. 막다른 길
  5. 최소 1개의 우회 경로

좁은 복도는 로봇 footprint, inflation 영역, 지역 경로 생성기의 진동 현상을 확인하는 데 유용합니다. 참고 World 제작 글에서는 소형 모바일 로봇용 기본 복도 폭을 약 1.2m에서 1.8m 정도로 제안하고 있습니다.

3) Warehouse World

다음 요소를 포함합니다.

  1. 선반 6개 이상
  2. 선반 사이 통로 3개 이상
  3. 적재 구역
  4. 도킹 구역
  5. 서로 다른 위치의 목표 지점 3개

Warehouse World는 선반 사이 통로 주행, 여러 목표점 방문, SLAM 지도 생성, Nav2 전역·지역 경로 계획 실습에 적합합니다.

8. 기본 ros_world.world 작성

다음 코드는 모든 학생이 사용할 수 있는 기본 장애물·복도 혼합 World입니다.

학생은 이 코드를 그대로 제출하면 안 됩니다. 벽 위치, 장애물 크기, 색상, 통로 구조 중 최소 5개 항목을 변경해야 합니다.

worlds/student_world.world

<?xml version="1.0"?>
<sdf version="1.6">

  <world name="ros_world">

    <gravity>0 0 -9.8</gravity>

    <physics type="ode">
      <real_time_update_rate>1000</real_time_update_rate>
      <max_step_size>0.001</max_step_size>
      <real_time_factor>1.0</real_time_factor>
    </physics>

    <include>
      <uri>model://sun</uri>
    </include>

    <include>
      <uri>model://ground_plane</uri>
    </include>

    <scene>
      <ambient>0.4 0.4 0.4 1</ambient>
      <background>0.7 0.7 0.7 1</background>
      <shadows>true</shadows>
    </scene>

    <!-- 북쪽 외벽 -->
    <model name="wall_north">
      <static>true</static>
      <pose>0 4 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>8.0 0.1 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>8.0 0.1 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.7 0.7 0.7 1</ambient>
            <diffuse>0.7 0.7 0.7 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 남쪽 외벽 -->
    <model name="wall_south">
      <static>true</static>
      <pose>0 -4 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>8.0 0.1 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>8.0 0.1 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.7 0.7 0.7 1</ambient>
            <diffuse>0.7 0.7 0.7 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 동쪽 외벽 -->
    <model name="wall_east">
      <static>true</static>
      <pose>4 0 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>0.1 8.0 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>0.1 8.0 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.7 0.7 0.7 1</ambient>
            <diffuse>0.7 0.7 0.7 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 서쪽 외벽 -->
    <model name="wall_west">
      <static>true</static>
      <pose>-4 0 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>0.1 8.0 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>0.1 8.0 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.7 0.7 0.7 1</ambient>
            <diffuse>0.7 0.7 0.7 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 중앙 장애물 -->
    <model name="center_box">
      <static>true</static>
      <pose>0.8 1.0 0.5 0 0 0.25</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>1.2 0.8 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>1.2 0.8 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.8 0.2 0.2 1</ambient>
            <diffuse>0.8 0.2 0.2 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 왼쪽 장애물 -->
    <model name="left_box">
      <static>true</static>
      <pose>-2.1 1.5 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>0.8 1.8 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>0.8 1.8 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.2 0.4 0.8 1</ambient>
            <diffuse>0.2 0.4 0.8 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 원통 장애물 -->
    <model name="cylinder_obstacle">
      <static>true</static>
      <pose>2.5 -0.3 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <cylinder>
              <radius>0.45</radius>
              <length>1.0</length>
            </cylinder>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <cylinder>
              <radius>0.45</radius>
              <length>1.0</length>
            </cylinder>
          </geometry>

          <material>
            <ambient>0.2 0.8 0.3 1</ambient>
            <diffuse>0.2 0.8 0.3 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 복도 위쪽 벽 -->
    <model name="corridor_wall_1">
      <static>true</static>
      <pose>-0.5 -1.3 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>4.0 0.12 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>4.0 0.12 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.8 0.6 0.2 1</ambient>
            <diffuse>0.8 0.6 0.2 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 복도 아래쪽 벽 -->
    <model name="corridor_wall_2">
      <static>true</static>
      <pose>-0.5 -2.4 0.5 0 0 0</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>4.0 0.12 1.0</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>4.0 0.12 1.0</size>
            </box>
          </geometry>

          <material>
            <ambient>0.8 0.6 0.2 1</ambient>
            <diffuse>0.8 0.6 0.2 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

    <!-- 작은 장애물 -->
    <model name="small_box">
      <static>true</static>
      <pose>2.7 2.6 0.35 0 0 0.5</pose>

      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>0.7 0.7 0.7</size>
            </box>
          </geometry>
        </collision>

        <visual name="visual">
          <geometry>
            <box>
              <size>0.7 0.7 0.7</size>
            </box>
          </geometry>

          <material>
            <ambient>0.6 0.2 0.8 1</ambient>
            <diffuse>0.6 0.2 0.8 1</diffuse>
          </material>
        </visual>
      </link>
    </model>

  </world>
</sdf>

9. World 소스 설명

1) static 설정

<static>true</static>

벽과 장애물을 정적 물체로 설정합니다.

staticfalse이면 중력이나 로봇 충돌로 인해 벽이 움직이거나 쓰러질 수 있습니다.

2) pose 설정

<pose>0 4 0.5 0 0 0</pose>

값의 순서는 다음과 같습니다.

x y z roll pitch yaw

예를 들어 다음 값은 물체를 X축 2m, Y축 1m 위치에 놓고 Z축으로 45도 회전시킵니다.

<pose>2.0 1.0 0.5 0 0 0.785</pose>

3) collision과 visual

<collision name="collision">

Gazebo 물리 엔진이 충돌을 계산할 때 사용하는 형상입니다.

<visual name="visual">

Gazebo 화면에 표시되는 형상입니다.

visual만 있고 collision이 없으면 벽이 화면에는 보이지만 로봇이 벽을 통과합니다. World 제작 과정에서 가장 자주 발생하는 실수 중 하나입니다.

4) box 크기

<size>8.0 0.1 1.0</size>

값의 순서는 다음과 같습니다.

X축 길이 Y축 길이 Z축 높이

북쪽 벽은 X축으로 8m, Y축으로 0.1m, 높이 1m인 직육면체입니다.

10. 학생 로봇 모델 작성

urdf/student_robot.urdf.xacro 파일을 다음과 같이 작성합니다.

이 로봇은 TurtleBot3와 동일한 로봇은 아닙니다. 학생이 제작한 독립적인 모바일 로봇이지만 TurtleBot3 SLAM과 Nav2가 요구하는 토픽 및 TF 규칙을 따르도록 설계합니다.

<?xml version="1.0"?>

<robot
  name="student_robot"
  xmlns:xacro="http://www.ros.org/wiki/xacro">

  <!-- 1. 기본 크기 파라미터 -->

  <xacro:property name="base_length" value="0.34"/>
  <xacro:property name="base_width" value="0.24"/>
  <xacro:property name="base_height" value="0.08"/>

  <xacro:property name="wheel_radius" value="0.05"/>
  <xacro:property name="wheel_width" value="0.035"/>
  <xacro:property name="wheel_separation" value="0.26"/>

  <!-- 2. 재질 -->

  <material name="body_blue">
    <color rgba="0.1 0.25 0.8 1.0"/>
  </material>

  <material name="wheel_black">
    <color rgba="0.03 0.03 0.03 1.0"/>
  </material>

  <material name="sensor_gray">
    <color rgba="0.35 0.35 0.35 1.0"/>
  </material>

  <!-- 3. base_footprint -->

  <link name="base_footprint"/>

  <!-- 4. base_link -->

  <link name="base_link">

    <visual>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>

      <geometry>
        <box size="${base_length} ${base_width} ${base_height}"/>
      </geometry>

      <material name="body_blue"/>
    </visual>

    <collision>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>

      <geometry>
        <box size="${base_length} ${base_width} ${base_height}"/>
      </geometry>
    </collision>

    <inertial>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>

      <mass value="3.0"/>

      <inertia
        ixx="0.020"
        ixy="0.0"
        ixz="0.0"
        iyy="0.035"
        iyz="0.0"
        izz="0.045"/>
    </inertial>

  </link>

  <joint name="base_footprint_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>

    <origin xyz="0 0 0.05" rpy="0 0 0"/>
  </joint>

  <!-- 5. 왼쪽 바퀴 -->

  <link name="left_wheel_link">

    <visual>
      <origin xyz="0 0 0" rpy="1.5708 0 0"/>

      <geometry>
        <cylinder
          radius="${wheel_radius}"
          length="${wheel_width}"/>
      </geometry>

      <material name="wheel_black"/>
    </visual>

    <collision>
      <origin xyz="0 0 0" rpy="1.5708 0 0"/>

      <geometry>
        <cylinder
          radius="${wheel_radius}"
          length="${wheel_width}"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.2"/>

      <inertia
        ixx="0.0003"
        ixy="0.0"
        ixz="0.0"
        iyy="0.0003"
        iyz="0.0"
        izz="0.0003"/>
    </inertial>

  </link>

  <joint name="left_wheel_joint" type="continuous">

    <parent link="base_link"/>
    <child link="left_wheel_link"/>

    <origin
      xyz="0 ${wheel_separation / 2.0} 0"
      rpy="0 0 0"/>

    <axis xyz="0 1 0"/>

    <dynamics
      damping="0.05"
      friction="0.0"/>

  </joint>

  <!-- 6. 오른쪽 바퀴 -->

  <link name="right_wheel_link">

    <visual>
      <origin xyz="0 0 0" rpy="1.5708 0 0"/>

      <geometry>
        <cylinder
          radius="${wheel_radius}"
          length="${wheel_width}"/>
      </geometry>

      <material name="wheel_black"/>
    </visual>

    <collision>
      <origin xyz="0 0 0" rpy="1.5708 0 0"/>

      <geometry>
        <cylinder
          radius="${wheel_radius}"
          length="${wheel_width}"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.2"/>

      <inertia
        ixx="0.0003"
        ixy="0.0"
        ixz="0.0"
        iyy="0.0003"
        iyz="0.0"
        izz="0.0003"/>
    </inertial>

  </link>

  <joint name="right_wheel_joint" type="continuous">

    <parent link="base_link"/>
    <child link="right_wheel_link"/>

    <origin
      xyz="0 ${-wheel_separation / 2.0} 0"
      rpy="0 0 0"/>

    <axis xyz="0 1 0"/>

    <dynamics
      damping="0.05"
      friction="0.0"/>

  </joint>

  <!-- 7. 뒤쪽 캐스터 -->

  <link name="caster_link">

    <visual>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>

      <material name="wheel_black"/>
    </visual>

    <collision>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.05"/>

      <inertia
        ixx="0.00002"
        ixy="0.0"
        ixz="0.0"
        iyy="0.00002"
        iyz="0.0"
        izz="0.00002"/>
    </inertial>

  </link>

  <joint name="caster_joint" type="fixed">

    <parent link="base_link"/>
    <child link="caster_link"/>

    <origin xyz="-0.14 0 -0.02" rpy="0 0 0"/>

  </joint>

  <!-- 8. LiDAR 링크 -->

  <link name="laser">

    <visual>
      <geometry>
        <cylinder radius="0.045" length="0.04"/>
      </geometry>

      <material name="sensor_gray"/>
    </visual>

    <collision>
      <geometry>
        <cylinder radius="0.045" length="0.04"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.15"/>

      <inertia
        ixx="0.0001"
        ixy="0.0"
        ixz="0.0"
        iyy="0.0001"
        iyz="0.0"
        izz="0.0001"/>
    </inertial>

  </link>

  <joint name="laser_joint" type="fixed">

    <parent link="base_link"/>
    <child link="laser"/>

    <origin xyz="0.08 0 0.16" rpy="0 0 0"/>

  </joint>

  <!-- 9. Gazebo 재질 -->

  <gazebo reference="base_link">
    <material>Gazebo/Blue</material>
  </gazebo>

  <gazebo reference="left_wheel_link">
    <material>Gazebo/Black</material>

    <mu1>10.0</mu1>
    <mu2>10.0</mu2>
  </gazebo>

  <gazebo reference="right_wheel_link">
    <material>Gazebo/Black</material>

    <mu1>10.0</mu1>
    <mu2>10.0</mu2>
  </gazebo>

  <gazebo reference="caster_link">
    <material>Gazebo/Black</material>

    <mu1>0.01</mu1>
    <mu2>0.01</mu2>
  </gazebo>

  <gazebo reference="laser">
    <material>Gazebo/Grey</material>
  </gazebo>

  <!-- 10. Differential Drive Plugin -->

  <gazebo>

    <plugin
      name="student_diff_drive"
      filename="libgazebo_ros_diff_drive.so">

      <ros>
        <namespace>/</namespace>
      </ros>

      <update_rate>50</update_rate>

      <left_joint>left_wheel_joint</left_joint>
      <right_joint>right_wheel_joint</right_joint>

      <wheel_separation>${wheel_separation}</wheel_separation>
      <wheel_diameter>${wheel_radius * 2.0}</wheel_diameter>

      <max_wheel_torque>20.0</max_wheel_torque>
      <max_wheel_acceleration>2.0</max_wheel_acceleration>

      <command_topic>cmd_vel</command_topic>
      <odometry_topic>odom</odometry_topic>

      <odometry_frame>odom</odometry_frame>
      <robot_base_frame>base_footprint</robot_base_frame>

      <publish_odom>true</publish_odom>
      <publish_odom_tf>true</publish_odom_tf>
      <publish_wheel_tf>false</publish_wheel_tf>

    </plugin>

  </gazebo>

  <!-- 11. Gazebo LiDAR Plugin -->

  <gazebo reference="laser">

    <sensor name="student_lidar" type="ray">

      <always_on>true</always_on>
      <visualize>true</visualize>
      <update_rate>10</update_rate>

      <pose>0 0 0 0 0 0</pose>

      <ray>

        <scan>
          <horizontal>
            <samples>360</samples>
            <resolution>1</resolution>

            <min_angle>-3.14159</min_angle>
            <max_angle>3.14159</max_angle>
          </horizontal>
        </scan>

        <range>
          <min>0.12</min>
          <max>8.0</max>
          <resolution>0.01</resolution>
        </range>

        <noise>
          <type>gaussian</type>
          <mean>0.0</mean>
          <stddev>0.01</stddev>
        </noise>

      </ray>

      <plugin
        name="student_lidar_plugin"
        filename="libgazebo_ros_ray_sensor.so">

        <ros>
          <namespace>/</namespace>
          <remapping>~/out:=scan</remapping>
        </ros>

        <output_type>sensor_msgs/LaserScan</output_type>
        <frame_name>laser</frame_name>

      </plugin>

    </sensor>

  </gazebo>

</robot>

11. 로봇 소스 상세 설명

1) Xacro property

<xacro:property name="wheel_radius" value="0.05"/>

반복해서 사용하는 값을 변수로 정의합니다.

바퀴 반지름을 변경할 때 링크 형상과 Differential Drive Plugin 값을 각각 수정하지 않고, property 값 하나만 변경할 수 있습니다.

2) base_footprint와 base_link

base_footprint
      └── base_link

base_footprint는 로봇 중심을 바닥에 투영한 좌표계입니다.

base_link는 실제 로봇 본체 중심 좌표계입니다.

TurtleBot3 Navigation 설정은 일반적으로 base_footprint를 로봇의 기준 프레임으로 사용하므로 다음 이름을 그대로 사용합니다.

<robot_base_frame>base_footprint</robot_base_frame>

3) visual, collision, inertial

모든 동적 링크에는 다음 3가지 요소를 작성하는 것이 좋습니다.

visual    화면에 보이는 형상
collision 충돌 계산에 사용하는 형상
inertial  질량과 관성 정보

inertial 값이 없거나 지나치게 작으면 로봇이 진동하거나 바퀴가 지면에 박히는 현상이 발생할 수 있습니다.

4) 바퀴 조인트

<joint name="left_wheel_joint" type="continuous">

continuous 조인트는 회전 각도 제한 없이 계속 회전할 수 있습니다.

<axis xyz="0 1 0"/>

바퀴가 Y축을 중심으로 회전하도록 설정합니다.

5) 바퀴 마찰력

<mu1>10.0</mu1>
<mu2>10.0</mu2>

바퀴와 지면 사이의 마찰 계수입니다.

마찰력이 너무 작으면 바퀴만 회전하고 로봇은 이동하지 않습니다.

캐스터는 방향 전환을 방해하지 않도록 낮은 마찰력을 사용합니다.

<mu1>0.01</mu1>
<mu2>0.01</mu2>

6) Differential Drive Plugin

<plugin
  name="student_diff_drive"
  filename="libgazebo_ros_diff_drive.so">

Gazebo의 왼쪽·오른쪽 바퀴를 차동구동 방식으로 제어합니다.

<command_topic>cmd_vel</command_topic>

다음 토픽을 구독합니다.

/cmd_vel

메시지 형식은 다음과 같습니다.

geometry_msgs/msg/Twist
<odometry_topic>odom</odometry_topic>

로봇의 이동량을 다음 토픽으로 발행합니다.

/odom
<publish_odom_tf>true</publish_odom_tf>

다음 TF를 발행합니다.

odom → base_footprint

7) LiDAR 센서 범위

<samples>360</samples>

한 번 스캔할 때 360개의 거리값을 생성합니다.

<min_angle>-3.14159</min_angle>
<max_angle>3.14159</max_angle>

라디안 단위로 약 -180도에서 180도까지 측정합니다.

따라서 로봇 주변 360도를 스캔합니다.

<min>0.12</min>
<max>8.0</max>

최소 측정 거리는 0.12m이고 최대 측정 거리는 8m입니다.

<update_rate>10</update_rate>

초당 10번 LiDAR 데이터를 생성합니다.

Gazebo Ray 센서와 libgazebo_ros_ray_sensor.so 플러그인은 Gazebo 내부 거리 데이터를 ROS 2의 LaserScan 메시지로 변환하며, SLAM과 Nav2에서 /scan 데이터를 사용할 수 있게 합니다.

8) LiDAR 노이즈

<noise>
  <type>gaussian</type>
  <mean>0.0</mean>
  <stddev>0.01</stddev>
</noise>

완벽한 거리값만 생성하지 않고 표준편차 0.01m의 가우시안 노이즈를 추가합니다.

노이즈가 포함된 센서 데이터로 SLAM을 실행하면 실제 센서에 가까운 조건을 만들 수 있습니다.

12. Launch 파일 작성

launch/simulation.launch.py 파일을 다음과 같이 작성합니다.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
    DeclareLaunchArgument,
    IncludeLaunchDescription,
    TimerAction,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import (
    Command,
    LaunchConfiguration,
)

from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue


def generate_launch_description():

    package_name = 'student_world_nav2'

    package_share = get_package_share_directory(package_name)
    gazebo_share = get_package_share_directory('gazebo_ros')

    default_world_path = os.path.join(
        package_share,
        'worlds',
        'student_world.world'
    )

    robot_xacro_path = os.path.join(
        package_share,
        'urdf',
        'student_robot.urdf.xacro'
    )

    world_argument = DeclareLaunchArgument(
        'world',
        default_value=default_world_path,
        description='Gazebo World file path'
    )

    spawn_x_argument = DeclareLaunchArgument(
        'x',
        default_value='-3.0',
        description='Robot initial X position'
    )

    spawn_y_argument = DeclareLaunchArgument(
        'y',
        default_value='-3.0',
        description='Robot initial Y position'
    )

    spawn_yaw_argument = DeclareLaunchArgument(
        'yaw',
        default_value='0.0',
        description='Robot initial yaw angle'
    )

    robot_description = ParameterValue(
        Command([
            'xacro ',
            robot_xacro_path
        ]),
        value_type=str
    )

    gazebo = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            os.path.join(
                gazebo_share,
                'launch',
                'gazebo.launch.py'
            )
        ),
        launch_arguments={
            'world': LaunchConfiguration('world'),
            'verbose': 'true'
        }.items()
    )

    robot_state_publisher = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher',
        output='screen',

        parameters=[
            {
                'robot_description': robot_description,
                'use_sim_time': True
            }
        ]
    )

    joint_state_publisher = Node(
        package='joint_state_publisher',
        executable='joint_state_publisher',
        name='joint_state_publisher',

        parameters=[
            {
                'use_sim_time': True
            }
        ]
    )

    spawn_robot = Node(
        package='gazebo_ros',
        executable='spawn_entity.py',
        name='spawn_student_robot',
        output='screen',

        arguments=[
            '-topic', 'robot_description',
            '-entity', 'student_robot',

            '-x', LaunchConfiguration('x'),
            '-y', LaunchConfiguration('y'),
            '-z', '0.01',

            '-Y', LaunchConfiguration('yaw')
        ]
    )

    delayed_spawn = TimerAction(
        period=2.0,
        actions=[
            spawn_robot
        ]
    )

    return LaunchDescription([
        world_argument,
        spawn_x_argument,
        spawn_y_argument,
        spawn_yaw_argument,

        gazebo,
        robot_state_publisher,
        joint_state_publisher,
        delayed_spawn
    ])

13. Launch 파일 상세 설명

1) 패키지 경로 검색

package_share = get_package_share_directory(package_name)

ROS 2가 설치 공간에서 student_world_nav2 패키지 경로를 찾습니다.

절대 경로를 코드에 직접 작성하지 않으므로 다른 학생의 PC에서도 같은 Launch 파일을 사용할 수 있습니다.

2) Xacro 변환

robot_description = ParameterValue(
    Command([
        'xacro ',
        robot_xacro_path
    ]),
    value_type=str
)

Xacro 파일을 실행 시점에 URDF XML 문자열로 변환합니다.

변환된 문자열은 robot_description 파라미터로 전달됩니다.

3) Gazebo 실행

gazebo = IncludeLaunchDescription(...)

gazebo_ros 패키지의 gazebo.launch.py를 현재 Launch 파일에 포함합니다.

참고 모델 Spawn 글에서도 Gazebo 실행, robot_state_publisher 실행, spawn_entity.py 실행을 하나의 Launch 파일에 등록하는 구조를 사용합니다.

4) robot_state_publisher

robot_state_publisher = Node(
    package='robot_state_publisher',
    executable='robot_state_publisher'
)

URDF에 정의된 링크와 조인트를 읽고 다음 TF를 발행합니다.

base_footprint → base_link
base_link → left_wheel_link
base_link → right_wheel_link
base_link → caster_link
base_link → laser
'use_sim_time': True

컴퓨터의 실제 시간이 아니라 Gazebo의 /clock 시뮬레이션 시간을 사용합니다.

SLAM, Nav2, Gazebo가 서로 다른 시간을 사용하면 다음과 같은 TF 오류가 발생할 수 있습니다.

Lookup would require extrapolation into the future
Transform data too old
Message Filter dropping message

5) spawn_entity.py

spawn_robot = Node(
    package='gazebo_ros',
    executable='spawn_entity.py'
)

spawn_entity.pyrobot_description 토픽에서 URDF를 받아 Gazebo World에 로봇을 생성합니다.

'-entity', 'student_robot'

Gazebo 안에서 사용할 모델 이름입니다.

동일한 이름의 모델을 두 번 생성하면 중복 모델 오류가 발생할 수 있습니다.

6) TimerAction

delayed_spawn = TimerAction(
    period=2.0,
    actions=[spawn_robot]
)

Gazebo 서버가 실행되기 전에 Spawn 요청이 전달되는 문제를 줄이기 위해 2초 후 로봇을 생성합니다.

14. URDF 문법 검사

워크스페이스를 빌드하기 전에 Xacro를 URDF로 변환합니다.

cd ~/student_nav_ws

xacro \
  src/student_world_nav2/urdf/student_robot.urdf.xacro \
  > /tmp/student_robot.urdf

URDF를 검사합니다.

check_urdf /tmp/student_robot.urdf

정상적인 경우 링크와 조인트 구조가 출력됩니다.

robot name is: student_robot
Successfully Parsed XML
root Link: base_footprint

오류가 발생하면 표시된 줄 번호 주변에서 다음 항목을 확인합니다.

  1. XML 태그가 닫혔는지 확인
  2. 큰따옴표가 빠지지 않았는지 확인
  3. 링크 이름과 조인트 이름이 일치하는지 확인
  4. Xacro 수식에 오타가 없는지 확인
  5. <robot> 태그 내부에 모든 코드가 포함됐는지 확인

15. 패키지 빌드

cd ~/student_nav_ws

source /opt/ros/humble/setup.bash

colcon build \
  --symlink-install \
  --packages-select student_world_nav2

빌드 결과를 적용합니다.

source ~/student_nav_ws/install/setup.bash

새 터미널을 열 때마다 다음 2개의 명령이 필요합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

16. Gazebo World와 로봇 실행

터미널 1에서 실행합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

ros2 launch student_world_nav2 simulation.launch.py

로봇의 시작 위치를 변경할 수도 있습니다.

ros2 launch student_world_nav2 simulation.launch.py \
  x:=-2.5 \
  y:=-3.0 \
  yaw:=1.57

다른 World 파일을 지정하려면 다음과 같이 실행합니다.

ros2 launch student_world_nav2 simulation.launch.py \
  world:=$HOME/student_nav_ws/src/student_world_nav2/worlds/student_world.world

17. 필수 토픽 확인

터미널 2에서 다음 명령을 실행합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

토픽 목록을 확인합니다.

ros2 topic list

최소한 다음 토픽이 있어야 합니다.

/clock
/cmd_vel
/odom
/robot_description
/scan
/tf
/tf_static

1) LiDAR 확인

ros2 topic echo /scan --once

메시지 타입을 확인합니다.

ros2 topic type /scan

정상 출력은 다음과 같습니다.

sensor_msgs/msg/LaserScan

발행 주기를 확인합니다.

ros2 topic hz /scan

약 10Hz가 출력되어야 합니다.

2) 오도메트리 확인

ros2 topic echo /odom --once

메시지 타입을 확인합니다.

ros2 topic type /odom

정상 출력은 다음과 같습니다.

nav_msgs/msg/Odometry

18. TF 구조 확인

다음 명령으로 TF를 확인합니다.

ros2 run tf2_ros tf2_echo odom base_footprint

로봇이 이동하면 위치와 회전값이 계속 변경되어야 합니다.

LiDAR TF를 확인합니다.

ros2 run tf2_ros tf2_echo base_link laser

고정 조인트이므로 일정한 값이 출력되어야 합니다.

전체 TF 구조를 PDF로 생성할 수도 있습니다.

ros2 run tf2_tools view_frames

정상적인 TF 구조는 다음과 같습니다.

odom
 └── base_footprint
      └── base_link
           ├── left_wheel_link
           ├── right_wheel_link
           ├── caster_link
           └── laser

SLAM 실행 후에는 가장 위에 map 프레임이 추가됩니다.

map
 └── odom
      └── base_footprint
           └── base_link
                └── laser

19. 키보드로 로봇 주행

터미널 3에서 TurtleBot3 키보드 제어 노드를 실행합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

export TURTLEBOT3_MODEL=burger

ros2 run turtlebot3_teleop teleop_keyboard

주요 조작 키는 다음과 같습니다.

w 선속도 증가
x 선속도 감소
a 각속도 증가
d 각속도 감소
s 즉시 정지
Space 즉시 정지

TurtleBot3 Teleop 노드는 /cmd_vel 토픽으로 geometry_msgs/msg/Twist 메시지를 발행합니다.

직접 명령을 보낼 수도 있습니다.

전진 명령입니다.

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.15}, angular: {z: 0.0}}" \
--rate 10

회전 명령입니다.

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0}, angular: {z: 0.5}}" \
--rate 10

정지 명령입니다.

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0}, angular: {z: 0.0}}" \
--once

20. TurtleBot3 Cartographer SLAM 실행

Gazebo 시뮬레이션은 계속 실행한 상태에서 새로운 터미널을 엽니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

export TURTLEBOT3_MODEL=burger

Cartographer를 실행합니다.

ros2 launch turtlebot3_cartographer \
  cartographer.launch.py \
  use_sim_time:=True

TurtleBot3 공식 ROS 2 Humble SLAM Simulation에서도 같은 방식으로 Cartographer를 실행합니다.

RViz에서 다음 항목을 확인합니다.

Fixed Frame map
Map 활성화
LaserScan 활성화
RobotModel 활성화
TF 활성화

키보드 Teleop으로 로봇을 천천히 주행합니다.

SLAM 지도 품질을 높이려면 다음 원칙을 지킵니다.

  1. 처음에는 제자리에서 천천히 한 바퀴 회전합니다.
  2. 벽을 따라 일정한 속도로 이동합니다.
  3. 급격한 가속과 회전을 피합니다.
  4. 같은 구간을 반대 방향으로 다시 주행합니다.
  5. 막다른 공간과 코너까지 LiDAR로 확인합니다.
  6. 출발 위치 근처로 돌아와 Loop Closure를 유도합니다.
  7. 미탐색 회색 공간이 남지 않도록 합니다.

21. SLAM 데이터 확인

지도 토픽을 확인합니다.

ros2 topic echo /map --once

지도 메시지 타입을 확인합니다.

ros2 topic type /map

정상 출력은 다음과 같습니다.

nav_msgs/msg/OccupancyGrid

map → odom TF를 확인합니다.

ros2 run tf2_ros tf2_echo map odom

Cartographer가 정상적으로 실행되면 지속적으로 TF가 출력됩니다.

22. 지도 저장

지도가 완성되면 새로운 터미널에서 다음 명령을 실행합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

지도 저장 위치를 지정합니다.

ros2 run nav2_map_server map_saver_cli \
  -f $HOME/student_nav_ws/src/student_world_nav2/maps/student_map

TurtleBot3 공식 SLAM Simulation에서도 nav2_map_servermap_saver_cli를 이용해 지도를 저장합니다.

저장 결과를 확인합니다.

ls -l ~/student_nav_ws/src/student_world_nav2/maps

다음 2개의 파일이 생성됩니다.

student_map.pgm
student_map.yaml

23. student_map.yaml 설명

생성된 YAML 파일을 확인합니다.

cat ~/student_nav_ws/src/student_world_nav2/maps/student_map.yaml

예시는 다음과 같습니다.

image: student_map.pgm
mode: trinary
resolution: 0.05
origin: [-4.25, -4.25, 0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25

1) image

image: student_map.pgm

점유 지도 이미지 파일 이름입니다.

2) resolution

resolution: 0.05

이미지의 픽셀 1개가 실제 공간에서 0.05m, 즉 5cm를 의미합니다.

3) origin

origin: [-4.25, -4.25, 0]

지도 이미지의 왼쪽 아래 기준점이 map 좌표계에서 어디에 위치하는지 나타냅니다.

4) occupied_thresh

occupied_thresh: 0.65

점유 확률이 0.65 이상인 픽셀을 장애물로 처리합니다.

5) free_thresh

free_thresh: 0.25

점유 확률이 0.25 이하인 픽셀을 주행 가능한 공간으로 처리합니다.

24. Nav2 실행 전 정리

Cartographer와 키보드 Teleop을 종료합니다.

Ctrl + C

Gazebo 시뮬레이션은 계속 실행해도 됩니다.

동시에 여러 노드가 /cmd_vel을 발행하면 명령이 충돌할 수 있으므로 Nav2 실행 전 Teleop 노드를 종료합니다.

실행 중인 /cmd_vel 발행자를 확인할 수 있습니다.

ros2 topic info /cmd_vel --verbose

25. TurtleBot3 Nav2 실행

새로운 터미널에서 다음 명령을 실행합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

export TURTLEBOT3_MODEL=burger

저장한 지도를 지정해 Nav2를 실행합니다.

ros2 launch turtlebot3_navigation2 \
  navigation2.launch.py \
  use_sim_time:=True \
  map:=$HOME/student_nav_ws/src/student_world_nav2/maps/student_map.yaml

TurtleBot3 공식 Navigation Simulation에서도 저장된 지도 YAML 파일을 map 인자로 전달해 Navigation2를 실행합니다.

26. 초기 위치 지정

Nav2를 실행한 직후에는 로봇이 지도상의 어느 위치에 있는지 알지 못합니다.

RViz 상단의 2D Pose Estimate 버튼을 사용합니다.

  1. Gazebo에서 로봇의 실제 위치를 확인합니다.
  2. RViz 지도에서 같은 위치를 클릭합니다.
  3. 마우스를 드래그해 로봇이 바라보는 방향을 지정합니다.
  4. /scan 데이터와 지도 벽이 겹치는지 확인합니다.
  5. 맞지 않으면 다시 2D Pose Estimate를 실행합니다.

AMCL이 정상적으로 위치를 추정하면 RViz에 작은 녹색 화살표 형태의 Particle이 표시됩니다.

TurtleBot3 공식 Navigation 절차에서도 Nav2 Goal을 보내기 전에 2D Pose Estimate로 AMCL의 초기 위치를 지정하도록 안내합니다.

27. Navigation2 Goal 지정

RViz 상단의 Navigation2 Goal 버튼을 선택합니다.

  1. 지도에서 도착 위치를 클릭합니다.
  2. 마우스를 드래그해 도착 방향을 지정합니다.
  3. 전역 경로가 생성되는지 확인합니다.
  4. 로봇이 경로를 따라 이동하는지 확인합니다.
  5. 장애물 앞에서 감속하거나 우회하는지 확인합니다.

Nav2는 지도와 Costmap을 이용해 전역 경로를 만들고, 지역 제어기를 이용해 해당 경로를 따라 로봇을 이동시킵니다. 경로 위에 장애물이 감지되면 지역 Costmap을 기반으로 충돌을 피하도록 속도 명령을 계산합니다.

28. Nav2 실행 상태 확인

Nav2 관련 노드를 확인합니다.

ros2 node list

다음과 비슷한 노드가 표시됩니다.

/amcl
/behavior_server
/bt_navigator
/controller_server
/global_costmap/global_costmap
/lifecycle_manager_localization
/lifecycle_manager_navigation
/local_costmap/local_costmap
/map_server
/planner_server
/waypoint_follower

Nav2 Action을 확인합니다.

ros2 action list

다음 Action이 있어야 합니다.

/navigate_to_pose

Nav2 Lifecycle 상태를 확인합니다.

ros2 lifecycle get /planner_server
ros2 lifecycle get /controller_server
ros2 lifecycle get /bt_navigator

정상적인 경우 다음 상태가 출력됩니다.

active

29. 필수 변경 항목

학생은 기본 코드를 실행하는 것에서 끝내지 않고 다음 항목을 변경해야 합니다.

1) World 변경

다음 항목 중 5개 이상을 변경합니다.

  1. 외곽 공간 크기
  2. 벽 위치
  3. 장애물 크기
  4. 장애물 회전각
  5. 장애물 색상
  6. 복도 폭
  7. 막다른 길
  8. 우회 경로
  9. 선반 배치
  10. 출발 위치
  11. 목표 위치

2) 로봇 변경

다음 항목 중 4개 이상을 변경합니다.

  1. 로봇 길이
  2. 로봇 폭
  3. 로봇 높이
  4. 바퀴 크기
  5. 바퀴 간격
  6. 본체 색상
  7. LiDAR 위치
  8. 캐스터 위치
  9. 로봇 질량
  10. 최대 LiDAR 거리

3) 알고리즘 시험

다음 시험을 모두 수행합니다.

  1. 직선 주행
  2. 제자리 회전
  3. 좁은 통로 통과
  4. SLAM 지도 저장
  5. Nav2 목표점 2개 이상 이동
  6. 장애물 앞에서 정지 또는 우회
  7. 출발 지점 근처로 복귀

30. 실행 명령 전체 정리

1) 터미널 1: Gazebo

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

ros2 launch student_world_nav2 simulation.launch.py

2) 터미널 2: SLAM

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

export TURTLEBOT3_MODEL=burger

ros2 launch turtlebot3_cartographer \
  cartographer.launch.py \
  use_sim_time:=True

3) 터미널 3: Teleop

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

export TURTLEBOT3_MODEL=burger

ros2 run turtlebot3_teleop teleop_keyboard

4) 터미널 4: 지도 저장

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

ros2 run nav2_map_server map_saver_cli \
  -f $HOME/student_nav_ws/src/student_world_nav2/maps/student_map

5) 터미널 2: Nav2

Cartographer를 종료한 후 실행합니다.

source /opt/ros/humble/setup.bash
source ~/student_nav_ws/install/setup.bash

export TURTLEBOT3_MODEL=burger

ros2 launch turtlebot3_navigation2 \
  navigation2.launch.py \
  use_sim_time:=True \
  map:=$HOME/student_nav_ws/src/student_world_nav2/maps/student_map.yaml

31. 최종 성공 조건

다음 조건을 모두 만족하면 프로젝트가 완성된 것입니다.

  1. Gazebo에 학생 World가 정상적으로 표시됩니다.
  2. 학생 로봇이 지정한 위치에 생성됩니다.
  3. /cmd_vel 명령으로 로봇이 움직입니다.
  4. /odom 토픽이 발행됩니다.
  5. /scan 토픽이 약 10Hz로 발행됩니다.
  6. odom → base_footprint → base_link → laser TF가 연결됩니다.
  7. Cartographer에서 지도가 생성됩니다.
  8. student_map.pgm 파일이 저장됩니다.
  9. student_map.yaml 파일이 저장됩니다.
  10. Nav2에서 초기 위치를 지정할 수 있습니다.
  11. Navigation2 Goal을 보낼 수 있습니다.
  12. 로봇이 장애물을 피하며 목표점에 도착합니다.

Leave a Comment