Post

[ROS2 입문] Day 5: Nav2와 자율주행 스택 - 목적지까지 스스로 이동하기

[ROS2 입문] Day 5: Nav2와 자율주행 스택 - 목적지까지 스스로 이동하기

서론: 지도 위에서 목적지로

ROS2의 표준 내비게이션 스택 Nav2(Navigation2)는 로봇이 지도 위에서 장애물을 피하며 목적지까지 이동하도록 한다. 센서 → 지도 → 경로 계획 → 제어 명령까지 전체 파이프라인을 제공한다.

1. Nav2 전체 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
LiDAR / 카메라
  ↓
SLAM (지도 생성) 또는 정적 지도 로드
  ↓
Costmap (장애물 표현)
  ├─ Global Costmap (전체 경로 계획용)
  └─ Local Costmap (실시간 장애물 회피용)
  ↓
Planner Server (Global Path 생성)
  ↓
Controller Server (Local Path 추종)
  ↓
cmd_vel (속도 명령) → 로봇 하드웨어

각 서버는 ROS2 액션으로 통신한다. Nav2는 이 서버들을 BehaviorTree로 조합해 복잡한 내비게이션 시나리오를 처리한다.

2. 설치

1
2
3
sudo apt install ros-jazzy-nav2-bringup \
                 ros-jazzy-nav2-msgs \
                 ros-jazzy-slam-toolbox

3. SLAM으로 지도 만들기

slam_toolbox를 사용해 실시간으로 지도를 생성한다.

1
2
3
4
5
6
7
8
9
10
# 1. slam_toolbox 실행
ros2 launch slam_toolbox online_async_launch.py \
  slam_params_file:=./slam_params.yaml

# 2. 로봇을 수동 조종하며 공간 스캔
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# 3. 지도 저장
ros2 run nav2_map_server map_saver_cli -f ~/maps/my_map
# → my_map.yaml + my_map.pgm 생성

4. Nav2 실행

1
2
3
4
# 저장된 지도로 내비게이션 실행
ros2 launch nav2_bringup bringup_launch.py \
  map:=$HOME/maps/my_map.yaml \
  use_sim_time:=false

내부적으로 map_server, amcl(위치 추정), planner, controller, recoveries 서버가 모두 실행된다.

5. 초기 위치 설정 (AMCL)

AMCL은 파티클 필터로 지도 위의 로봇 위치를 추정한다. 처음에는 초기 위치를 수동으로 설정해야 한다.

1
2
3
4
5
6
7
# CLI로 초기 위치 퍼블리시
ros2 topic pub /initialpose geometry_msgs/msg/PoseWithCovarianceStamped \
  "{ header: {frame_id: map},
     pose: { pose: {
       position: {x: 0.0, y: 0.0, z: 0.0},
       orientation: {w: 1.0}
     }}}" --once

RViz2에서 “2D Pose Estimate” 버튼으로 클릭해 설정하는 것이 더 편하다.

6. 이동 목표 전송 (Python 액션 클라이언트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from geometry_msgs.msg import PoseStamped
from nav2_msgs.action import NavigateToPose
from rclpy.action import ActionClient

class Navigator(Node):
    def __init__(self):
        super().__init__('navigator')
        self._client = ActionClient(
            self, NavigateToPose, 'navigate_to_pose')

    def go_to(self, x, y, yaw=0.0):
        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
        goal.pose.pose.orientation.w = 1.0  # yaw=0

        self._client.wait_for_server()
        future = self._client.send_goal_async(
            goal, feedback_callback=self._feedback_cb)
        future.add_done_callback(self._goal_response_cb)

    def _feedback_cb(self, feedback_msg):
        dist = feedback_msg.feedback.distance_remaining
        self.get_logger().info(f'남은 거리: {dist:.2f}m')

    def _goal_response_cb(self, future):
        result = future.result()
        if result.accepted:
            self.get_logger().info('목표 수락됨')
        else:
            self.get_logger().warn('목표 거부됨')

7. Costmap 설정

Costmap은 장애물 주변에 비용을 부여해 경로가 장애물에서 충분히 떨어지게 한다.

1
2
3
4
5
6
7
8
# nav2_params.yaml (일부)
global_costmap:
  global_costmap:
    ros__parameters:
      robot_radius: 0.22        # 로봇 반지름 (m)
      inflation_radius: 0.55    # 장애물 팽창 반경
      obstacle_max_range: 2.5   # 장애물 감지 최대 거리
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]

inflation_radius가 너무 작으면 좁은 공간에서 충돌 위험이 있고, 너무 크면 통로를 못 지나간다.

8. 시리즈 종합 체크리스트

  1. ROS2 설치 후 talker-listener로 DDS 기반 통신을 확인했다.
  2. 토픽/서비스/액션 중 상황에 맞는 통신 패턴을 선택할 수 있다.
  3. colcon build로 Python/C++ 패키지를 빌드하고 노드를 실행했다.
  4. TF 프레임 트리를 설계하고 TransformBroadcaster로 변환을 퍼블리시했다.
  5. Nav2로 SLAM 지도를 생성하고 목적지까지 자율이동 명령을 전송했다.

시리즈 마무리

ROS2는 DDS 기반 미들웨어 위에 표준화된 통신 패턴(토픽/서비스/액션), 빌드 시스템(colcon), 공간 기하학(TF2), 내비게이션(Nav2)을 쌓아올린 생태계다.

각 레이어를 독립적으로 이해하면 문제가 생겼을 때 어느 레이어에서 발생했는지 빠르게 찾을 수 있다. 통신이 안 된다면 토픽 QoS부터, 위치가 이상하다면 TF 트리부터, 경로가 이상하다면 Costmap 설정부터 확인하는 순서로 접근한다.

This post is licensed under CC BY 4.0 by the author.