Skip to main content
Summary

이제 로봇은 지도를 가지고 있고, 지도 위에서 자기 위치를 추정할 줄도 안다. 아직 남은 질문은 하나다. "그래서 로봇이 혼자 목적지까지 갈 수 있는가?"

이번 장에서는 앞에서 만든 정적 지도와 AMCL 위치 추정을 바탕으로 Nav2를 실행하고, RViz에서 목표 지점을 지정해 로봇을 실제로 이동시킨다. 여기서 중요한 것은 단순히 로봇이 움직이는 장면을 보는 것이 아니다. 지도는 어디서 오고, 위치 추정은 언제 안정되며, 경로는 누가 만들고, 속도 명령은 어디로 나가는지까지 한 번에 관찰해야 한다.

자율주행 로봇이 목표 지점까지 이동하는 순간, 지금까지 배운 거의 모든 요소가 동시에 등장한다. map_server는 저장된 지도를 제공하고, AMCL은 map → odom 변환을 만든다. Nav2의 planner는 전역 경로를 계산하고, controller는 로봇 주변 상황을 보며 /cmd_vel을 발행한다. RViz는 이 모든 흐름을 한눈에 보여 주는 조종석이 된다.

목표 지점까지 자율 이동하기 프로젝트

오늘의 목표: "목표를 찍으면 로봇이 간다"를 완성하기

이번 프로젝트의 성공 조건은 단순하다. RViz에서 지도 위의 목표 지점을 클릭했을 때 로봇이 전역 경로를 만들고, 장애물과 벽을 피하며, 최종적으로 목표 위치 근처에 도착해야 한다.

하지만 로봇이 "간다"는 말 안에는 여러 조건이 숨어 있다. 지도는 정상적으로 로드되어야 하고, 로봇의 현재 위치는 지도와 맞아야 한다. LiDAR 스캔은 지도 위 벽과 겹쳐 보여야 하며, TF 트리는 map → odom → base_link로 끊기지 않아야 한다. Nav2 lifecycle 노드들은 활성 상태여야 하고, local costmap은 로봇 주변 장애물을 계속 반영해야 한다.

이 장에서는 다음 흐름으로 실습한다.

  • 저장한 지도 YAML/PGM 파일을 Nav2의 map_server에 로드한다.

  • 로봇 bringup, AMCL, Nav2, RViz를 실행하고 상태를 점검한다.

  • RViz에서 초기 위치를 지정해 AMCL을 수렴시킨다.

  • Nav2 Goal 또는 NavigateToPose 액션으로 목표를 보낸다.

  • 전역 경로, local trajectory, costmap, /cmd_vel, 액션 결과를 함께 관찰한다.

  • 실패했을 때 지도, lifecycle, TF, AMCL, costmap, 액션 로그 순서로 원인을 좁힌다.

이번 장은 프로젝트 장이다. 따라서 명령어를 따라 치는 것도 중요하지만, 각 단계에서 "무엇이 정상인지"를 보는 눈을 만드는 것이 더 중요하다.

프로젝트 준비물

먼저 앞 장에서 저장한 지도가 있어야 한다. 일반적으로 지도는 두 개의 파일로 구성된다.

my_room.yaml
my_room.pgm

또는 저장 방식에 따라 my_room.png가 함께 있을 수도 있다. Nav2에서 직접 지정하는 파일은 보통 YAML 파일이다. YAML 파일 안에는 실제 이미지 파일 경로, 해상도, 원점, 점유 판단 임계값이 들어 있다.

예를 들어 지도 YAML은 다음과 비슷하다.

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

여기서 resolution: 0.05는 지도 이미지의 픽셀 하나가 실제 세계의 5cm를 의미한다는 뜻이다. origin은 지도 이미지의 원점이 map 좌표계에서 어디에 놓이는지를 나타낸다. 이 값이 엉뚱하면 지도는 보이지만 로봇 위치와 벽 위치가 어긋난다.

실습 환경은 시뮬레이션과 실제 로봇 중 하나를 선택할 수 있다. 시뮬레이션이라면 use_sim_time:=True를 사용하고, 실제 로봇이라면 보통 use_sim_time:=False를 사용한다. 이 차이는 작아 보이지만 매우 중요하다. 시뮬레이션에서는 Gazebo나 Ignition/Gazebo Sim이 /clock을 발행하고, Nav2 노드들이 그 시간을 기준으로 움직인다. 실제 로봇에서는 컴퓨터의 시스템 시간이 기준이다.

이 장의 예시는 TurtleBot 계열 구성을 기준으로 설명하되, 원리는 다른 이동 로봇에도 그대로 적용된다. 패키지 이름과 launch 파일 이름은 로봇마다 다를 수 있으므로, 자신의 로봇 bringup 패키지에 맞게 바꾸어 사용하면 된다.

map_server가 지도를 여는 방식

Nav2에서 저장된 지도는 map_server가 읽어 들인다. map_server는 지도 파일을 로드한 뒤 /map 토픽으로 nav_msgs/msg/OccupancyGrid 형태의 점유 격자 지도를 발행한다. 또한 서비스 인터페이스를 통해 현재 지도를 요청하거나, 실행 중 다른 지도로 바꾸는 기능도 제공한다.1

점유 격자 지도는 바닥을 작은 칸으로 나눈 그림이라고 생각하면 된다. 흰 칸은 지나갈 수 있는 공간, 검은 칸은 벽이나 장애물, 회색 칸은 모르는 영역이다. Nav2의 global costmap은 이 /map 정보를 바탕으로 정적인 벽과 구조물을 반영한다.

Nav2의 map_server 설정은 보통 파라미터 파일 안에 들어간다.

map_server:
  ros__parameters:
    yaml_filename: "/home/robot/maps/my_room.yaml"
    topic_name: "map"
    frame_id: "map"

yaml_filename은 불러올 지도 YAML 파일 경로다. topic_name은 지도를 발행할 토픽 이름이고, 기본적으로 map을 사용한다. frame_id는 지도 메시지가 어떤 좌표계에 속하는지를 나타내며 일반적으로 map이다.2

직접 map_server만 실행해 지도 로드 상태를 확인할 수도 있지만, 실제 Nav2 bringup에서는 map_server, AMCL, planner, controller, behavior tree navigator, lifecycle manager가 함께 실행되는 경우가 많다. 따라서 프로젝트에서는 전체 bringup 흐름 속에서 지도가 정상적으로 들어오는지 확인하는 쪽이 더 실용적이다.

지도 파일이 잘못되었는지는 실행 전에 먼저 확인하자.

ls -l ~/maps/my_room.yaml ~/maps/my_room.pgm
cat ~/maps/my_room.yaml

YAML 안의 image 경로가 상대 경로라면 YAML 파일이 있는 위치를 기준으로 이미지 파일을 찾는다. 만약 YAML 파일만 복사하고 PGM 파일을 빼먹었다면 map_server는 지도를 열 수 없다. 이 실수는 생각보다 자주 나온다. 로봇은 고장 나지 않았는데, 로봇에게 보여 줄 지도를 책상 위에 두고 온 셈이다.

실행 순서 잡기

이 프로젝트의 실행 순서는 다음과 같이 잡으면 안정적이다. 먼저 로봇의 기본 인터페이스를 실행하고, 그다음 Nav2를 지도와 함께 실행한 뒤, RViz에서 위치와 목표를 지정한다.

시뮬레이션을 사용하는 경우 예시는 다음과 같다.

# 터미널 1: 환경 설정
source /opt/ros/$ROS_DISTRO/setup.bash
source ~/ros2_ws/install/setup.bash
export TURTLEBOT3_MODEL=waffle

# 터미널 1: 시뮬레이션 실행 예시
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

실제 TurtleBot3를 사용하는 경우에는 로봇 인터페이스를 먼저 실행한다.

source /opt/ros/$ROS_DISTRO/setup.bash
export TURTLEBOT3_MODEL=waffle

ros2 launch turtlebot3_bringup robot.launch.py use_sim_time:=False

이 단계에서 /scan, /odom, /tf, /cmd_vel 관련 연결이 살아 있어야 한다. Nav2는 로봇을 직접 "마법처럼" 움직이는 프로그램이 아니다. Nav2가 최종적으로 내보내는 것은 속도 명령이고, 실제 바퀴를 움직이는 것은 로봇의 하위 구동 노드다.

다음으로 Nav2를 실행한다. 실제 로봇에서는 다음과 비슷한 형태가 된다.

ros2 launch nav2_bringup bringup_launch.py \
  use_sim_time:=False \
  autostart:=False \
  map:=/home/robot/maps/my_room.yaml

시뮬레이션이라면 use_sim_time:=True로 바꾼다.

ros2 launch nav2_bringup bringup_launch.py \
  use_sim_time:=True \
  autostart:=False \
  map:=/home/robot/maps/my_room.yaml

TurtleBot3의 navigation 패키지를 사용하는 환경에서는 다음처럼 로봇 패키지에서 제공하는 launch 파일을 사용할 수도 있다.

ros2 launch turtlebot3_navigation2 navigation2.launch.py \
  use_sim_time:=True \
  map:=/home/robot/maps/my_room.yaml

공식 TurtleBot3 Nav2 튜토리얼에서도 실제 로봇에서는 로봇 bringup을 먼저 실행하고, 저장된 지도 YAML을 map 인자로 넘겨 Nav2를 실행한 뒤, RViz에서 초기 위치와 목표 위치를 지정하는 흐름을 사용한다.3

autostart:=False로 실행했다면 lifecycle 노드들은 처음부터 active 상태가 아닐 수 있다. 이 경우 RViz의 Nav2 패널에서 Startup 버튼을 누르거나 lifecycle manager를 통해 활성화해야 한다. autostart:=True라면 launch 과정에서 자동으로 활성화된다. 처음 실습할 때는 autostart:=False로 두고, 노드가 어떤 상태에서 active로 전환되는지 보는 것도 좋은 공부가 된다.

RViz를 자율주행 조종석으로 만들기

Nav2를 실행했다면 RViz를 열어야 한다.

ros2 run rviz2 rviz2 -d $(ros2 pkg prefix nav2_bringup)/share/nav2_bringup/rviz/nav2_default_view.rviz

직접 RViz 구성을 만든다면 Fixed Frame을 반드시 map으로 설정한다. 지도 기반 내비게이션에서는 모든 것이 최종적으로 map 좌표계 위에서 해석된다. Fixed Frame이 odom이나 base_link로 되어 있으면 지도, 경로, 로봇 모델이 이상하게 흔들리거나 보이지 않을 수 있다.

RViz에는 최소한 다음 표시 항목을 추가하는 것이 좋다.

  • Map: /map

  • LaserScan: /scan

  • TF: map, odom, base_link 확인

  • RobotModel: 로봇 형상 확인

  • Path: /plan

  • Path: /local_plan

  • Map: /global_costmap/costmap

  • Map: /local_costmap/costmap

  • PoseArray: /particlecloud

  • Polygon: /local_costmap/published_footprint

RViz에서 /map이 보이지 않는다면 QoS 설정 문제일 수 있다. map_server는 늦게 구독한 노드도 지도를 받을 수 있도록 transient local 성격의 발행을 사용한다. RViz의 Map display에서 Durability를 Transient Local, Reliability를 Reliable로 맞추면 "No map received" 문제를 해결할 수 있는 경우가 많다.4

화면이 제대로 구성되면 RViz는 단순한 시각화 도구가 아니라 계기판이 된다. 지도는 로봇이 기억하는 세계이고, LiDAR 스캔은 지금 눈앞에 보이는 세계다. costmap은 "여기는 지나가도 되고, 여기는 위험하다"는 Nav2의 판단이며, /plan은 머릿속 계획, /local_plan은 바로 다음 몇 초의 몸동작이다.

지도와 로봇의 첫 만남: 초기 위치 지정

Nav2를 켰다고 해서 로봇이 곧바로 자신이 지도 위 어디에 있는지 아는 것은 아니다. 저장된 지도는 과거에 만든 것이고, 지금 로봇은 실제 공간 어딘가에 놓여 있다. 이 둘을 연결하는 첫 단서가 RViz의 2D Pose Estimate다.

RViz 상단의 2D Pose Estimate 버튼을 누른 뒤, 지도 위에서 로봇이 실제로 놓인 위치를 클릭하고 드래그한다. 클릭 위치는 로봇의 위치, 드래그 방향은 로봇의 방향이다. 이 메시지는 /initialpose 토픽으로 발행되고, AMCL은 이 값을 초기 추정값으로 사용한다.5

초기 위치를 지정한 직후에는 다음을 확인한다.

첫째, 로봇 모델이 지도 위의 실제 위치 근처로 이동해야 한다. 실제 로봇이 방 입구를 바라보고 있는데 RViz의 로봇이 벽을 바라보고 있다면 방향을 잘못 준 것이다.

둘째, LiDAR 스캔이 지도 벽과 겹쳐야 한다. RViz에서 /scan을 켜면 점들이 보인다. 이 점들이 지도상의 벽과 잘 맞으면 위치 추정이 꽤 안정적이다. 점들이 벽에서 한참 떨어져 있거나 회전되어 보이면 초기 위치가 틀렸거나 TF가 맞지 않는 것이다.

셋째, /particlecloud가 좁게 모여야 한다. AMCL은 여러 개의 입자, 즉 "로봇이 여기 있을지도 모른다"는 후보들을 유지한다. 초기에는 입자들이 퍼져 있을 수 있지만, 로봇이 조금 움직이고 LiDAR 데이터가 들어오면 점점 한곳으로 모여야 한다. 입자들이 계속 넓게 퍼져 있다면 로봇은 아직 자신이 어디 있는지 확신하지 못하는 상태다.

이 상태에서 목표를 보내면 어떻게 될까? 로봇은 자기 위치를 착각한 채 출발할 수 있다. 사람으로 치면 지하철역 출구를 잘못 알고 길 안내 앱을 켠 상태와 같다. 안내는 열심히 해 주지만, 출발점이 틀렸으니 결과도 틀어진다.

생체 신호 점검: 이동 전 확인 명령

목표를 보내기 전에 ROS2 명령어로 시스템의 생체 신호를 확인하자.

ros2 topic list

다음 토픽들이 보이는지 확인한다.

/map
/scan
/odom
/tf
/tf_static
/cmd_vel
/amcl_pose
/plan
/local_plan
/global_costmap/costmap
/local_costmap/costmap

지도 메시지가 실제로 들어오는지 확인한다.

ros2 topic echo /map --once

토픽 출력이 너무 크다면 ros2 topic info로 타입과 발행자만 확인해도 된다.

ros2 topic info /map
ros2 topic info /scan
ros2 topic info /cmd_vel

TF 연결은 다음처럼 확인한다.

ros2 run tf2_ros tf2_echo map base_link

정상이라면 map에서 base_link로의 변환이 계속 출력된다. 만약 "Could not transform" 류의 메시지가 나오면 Nav2는 로봇의 현재 위치를 계산할 수 없다. 이때는 목표를 보내도 planner가 시작점조차 제대로 잡지 못한다.

lifecycle 상태도 확인한다.

ros2 lifecycle nodes

노드 이름이 확인되면 각 노드 상태를 볼 수 있다.

ros2 lifecycle get /map_server
ros2 lifecycle get /amcl
ros2 lifecycle get /planner_server
ros2 lifecycle get /controller_server
ros2 lifecycle get /bt_navigator

정상 주행 전에는 주요 Nav2 노드들이 active 상태여야 한다. lifecycle 노드는 단순히 프로세스가 떠 있다고 일을 하는 것이 아니다. unconfigured, inactive, active 같은 상태를 가지며, 실제 토픽 발행과 액션 처리는 active 상태에서 제대로 수행된다.

액션 서버도 확인한다.

ros2 action list

다음 액션들이 보이면 Nav2의 주요 서버들이 준비된 것이다.

/navigate_to_pose
/compute_path_to_pose
/follow_path
/spin
/backup
/wait

실제 목표 이동 요청은 보통 /navigate_to_pose 액션으로 들어간다. 내부적으로는 planner가 경로를 만들고, controller가 그 경로를 따라가며, 필요하면 behavior tree가 복구 행동을 선택한다.

RViz에서 목표 지점 보내기

이제 로봇에게 목표를 줄 차례다. RViz 상단의 Nav2 Goal 도구를 선택한다. 지도 위에서 목표 위치를 클릭한 뒤 드래그하여 최종 방향을 정한다. 화살표의 시작점이 목표 위치이고, 화살표 방향이 목표 지점에서 로봇이 바라볼 방향이다.

마우스를 놓는 순간 RViz는 Nav2에 목표를 보낸다. 이때 Nav2 Goal 도구는 단순한 토픽 발행이 아니라 NavigateToPose 액션을 사용해 목표를 전달한다. 반면 일부 goal tool은 토픽 방식으로 목표를 보낼 수 있으므로 사용하는 RViz 설정에 따라 차이가 있을 수 있다.3

목표가 정상적으로 들어가면 먼저 전역 경로가 나타난다. 보통 /plan으로 표시되는 선이다. 이 선은 "현재 위치에서 목표까지 큰 그림으로 어떻게 갈 것인가"를 보여 준다. 복도 끝에서 오른쪽 방으로 가야 한다면, 전역 경로는 복도를 따라가다가 문 앞에서 꺾이는 형태가 된다.

잠시 뒤 로봇 주변에는 local costmap과 local trajectory가 보인다. local trajectory는 controller가 당장 실행하려는 짧은 움직임이다. 전역 경로가 지도 위의 여행 계획이라면, local trajectory는 지금 발을 어디에 디딜지 정하는 동작이다.

터미널에서는 /cmd_vel을 확인할 수 있다.

ros2 topic echo /cmd_vel

로봇이 움직이는 동안 선속도와 각속도가 출력된다.

linear:
  x: 0.12
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.35

차동 구동 로봇이라면 일반적으로 linear.x와 angular.z가 핵심이다. linear.x는 앞으로 가는 속도, angular.z는 제자리 회전 또는 곡선 주행을 위한 회전 속도다.

목표에 가까워지면 로봇은 속도를 줄이고 방향을 맞춘다. 최종적으로 goal tolerance 안에 들어오면 액션은 성공으로 종료된다. goal tolerance는 "목표점과 정확히 0mm 차이로 일치해야 도착인가, 아니면 몇 cm 정도는 허용할 것인가"를 정하는 파라미터다. 실제 로봇은 바닥 마찰, 바퀴 오차, 센서 노이즈가 있으므로 적절한 허용 오차가 필요하다.

CLI로 NavigateToPose 액션 보내기

RViz 없이 터미널에서 직접 목표를 보낼 수도 있다. 이 방식은 디버깅과 자동화에 유용하다.

ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{
  pose: {
    header: {
      frame_id: 'map'
    },
    pose: {
      position: {x: 1.0, y: 0.5, z: 0.0},
      orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}
    }
  },
  behavior_tree: ''
}"

피드백을 함께 보고 싶다면 --feedback 옵션을 붙인다.

ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{
  pose: {
    header: {frame_id: 'map'},
    pose: {
      position: {x: 1.0, y: 0.5, z: 0.0},
      orientation: {w: 1.0}
    }
  },
  behavior_tree: ''
}" --feedback

NavigateToPose 액션을 직접 호출하면 RViz가 없어도 목표 이동을 테스트할 수 있다. 교육용 자료에서도 /navigate_to_pose 액션에 목표 pose를 보내면 planner의 /plan, controller의 /local_plan, 최종 /cmd_vel 흐름을 확인할 수 있다고 설명한다.6

좌표를 정하기 어렵다면 RViz의 Publish Point 도구를 사용할 수 있다. 원하는 지점을 클릭하면 /clicked_point에 좌표가 나온다.

ros2 topic echo /clicked_point

출력된 x, y 값을 목표 pose에 넣으면 된다. 방향까지 정해야 한다면 RViz의 Nav2 Goal처럼 드래그로 orientation을 지정하는 편이 더 쉽다.

로봇이 이동하는 동안 무엇을 봐야 할까

로봇이 움직이기 시작하면 화면이 갑자기 바빠진다. 지도 위에는 경로가 생기고, 로봇 주변에는 색이 칠해진 costmap이 따라다니며, LiDAR 점들은 벽과 장애물을 계속 찍는다. 이때 아무것도 모르고 보면 "오, 움직인다!"로 끝나지만, 관찰 기준을 가지고 보면 Nav2의 내부 판단이 보인다.

먼저 /plan을 본다. 전역 경로가 벽을 통과하지 않는지, 너무 좁은 틈을 무리하게 지나가려 하지 않는지 확인한다. 전역 경로는 global costmap을 기반으로 만들어진다. 따라서 지도 자체가 잘못되었거나 global costmap의 inflation이 너무 크면 경로가 이상해질 수 있다.

다음으로 local costmap을 본다. 로봇 주변의 장애물이 실시간으로 표시되는지 확인한다. 지도에는 없지만 실제 공간에 놓인 상자나 사람 다리가 LiDAR에 잡힌다면 local costmap에 장애물로 나타나야 한다. 만약 /scan은 보이는데 local costmap에 장애물이 반영되지 않는다면 costmap의 observation source 설정, frame_id, TF 변환을 의심해야 한다.

그리고 /local_plan 또는 controller trajectory를 본다. 전역 경로가 멀리 돌아가더라도 local trajectory는 현재 로봇의 속도 제한, 회전 가능성, 장애물 위치를 고려해 짧은 경로를 선택한다. 좁은 통로에서는 local trajectory가 자주 흔들리거나 멈칫할 수 있다. 이때 로봇이 겁먹은 것처럼 보인다면 실제로는 costmap 비용이 너무 높거나 footprint가 통로보다 크게 평가되고 있을 수 있다.

마지막으로 /cmd_vel을 본다. 경로는 있는데 /cmd_vel이 0이라면 controller가 움직일 수 있는 유효한 속도를 찾지 못했을 가능성이 있다. 반대로 /cmd_vel은 나오는데 로봇이 움직이지 않는다면 Nav2보다 아래쪽, 즉 구동 드라이버, 모터 전원, safety stop, 토픽 remapping 문제를 확인해야 한다.

좁은 통로에서 로봇이 망설이는 이유

넓은 방에서는 로봇이 잘 움직이다가 좁은 통로 앞에서 갑자기 우물쭈물하는 일이 많다. 이 장면은 꽤 사람 같다. 문이 좁으면 사람도 어깨를 움츠리고 조심스럽게 지나간다. Nav2도 비슷하다. 다만 Nav2의 조심성은 감정이 아니라 costmap 파라미터에서 나온다.

가장 먼저 볼 것은 inflation_radius다. 장애물 주변에는 충돌을 피하기 위한 비용 영역이 만들어진다. inflation_radius가 크면 벽 주변의 위험 영역이 넓어진다. 넓은 환경에서는 안전하지만, 좁은 복도에서는 양쪽 벽의 inflation 영역이 겹치면서 통로 전체가 위험해 보일 수 있다.

다음은 footprint 또는 robot_radius다. footprint는 로봇의 실제 바닥 면적을 나타낸다. 둥근 로봇이면 반지름으로 표현할 수 있고, 직사각형 로봇이면 꼭짓점 좌표로 표현한다. 실제보다 크게 설정하면 로봇은 자신을 덩치 큰 트럭처럼 생각한다. 실제로는 지나갈 수 있는 문도 costmap에서는 통과 불가능한 문이 된다.

controller 파라미터도 중요하다. 최대 속도가 너무 빠르거나 회전 속도가 과하면 좁은 곳에서 경로 추종이 불안정해진다. 반대로 최소 속도 조건이 너무 높으면 controller가 섬세한 저속 움직임을 선택하지 못한다. 회전 구간에서 로봇이 좌우로 흔들리거나 목표 앞에서 빙글빙글 돈다면 angular velocity, acceleration limit, goal checker 설정을 함께 봐야 한다.

마지막으로 goal tolerance를 확인한다. 목표 위치가 벽 가까이에 있거나 문턱 바로 앞에 있으면 로봇이 정확한 목표 자세를 맞추기 어려울 수 있다. 사람이 의자 바로 옆 1cm 지점에 정확히 서라고 하면 불편한 것과 같다. 실내 이동 로봇에서는 목표를 벽에서 충분히 떨어진 위치에 두고, 위치와 방향 허용 오차를 현실적으로 조정하는 편이 좋다.

예를 들어 nav2_params.yaml에서 다음 항목을 점검할 수 있다.

local_costmap:
  local_costmap:
    ros__parameters:
      footprint: "[[0.18, 0.14], [0.18, -0.14], [-0.18, -0.14], [-0.18, 0.14]]"
      inflation_layer:
        inflation_radius: 0.35
        cost_scaling_factor: 3.0

global_costmap:
  global_costmap:
    ros__parameters:
      inflation_layer:
        inflation_radius: 0.45
        cost_scaling_factor: 3.0

값을 줄이면 로봇이 벽에 더 가까이 갈 수 있지만, 충돌 위험도 커진다. 값을 키우면 안전 여유는 커지지만 좁은 통로를 지나가기 어려워진다. 그래서 costmap 튜닝은 "정답 찾기"라기보다 "환경과 로봇 크기에 맞는 타협점 찾기"에 가깝다.

목표 이동의 내부 흐름 다시 보기

목표를 보냈을 때 Nav2 내부에서는 다음과 같은 일이 벌어진다.

RViz 또는 CLI가 /navigate_to_pose 액션 목표를 보낸다. bt_navigator는 Behavior Tree를 따라 전체 이동 절차를 관리한다. planner server는 현재 위치와 목표 위치를 바탕으로 global costmap에서 전역 경로를 계산한다. controller server는 local costmap과 현재 속도, 로봇 운동 모델을 고려해 경로를 따라갈 속도 명령을 만든다. 속도 명령은 /cmd_vel로 발행되고, 로봇의 구동 노드가 이를 받아 바퀴를 움직인다.

이 흐름을 토픽과 액션으로 보면 다음과 같다.

RViz/Nav2 Goal
    ↓
/navigate_to_pose action
    ↓
bt_navigator
    ↓
planner_server → /plan
    ↓
controller_server → /local_plan → /cmd_vel
    ↓
robot base driver

옆에서는 AMCL이 계속 위치를 추정한다.

/map + /scan + /odom
    ↓
AMCL
    ↓
map → odom TF
    ↓
Nav2가 현재 위치 계산

그리고 costmap은 다음 입력을 계속 합친다.

/map             → global costmap의 static layer
/scan            → obstacle layer 또는 voxel layer
footprint        → 충돌 가능 영역
inflation layer  → 장애물 주변 안전 비용

이 구조가 머릿속에 들어오면 디버깅이 쉬워진다. "안 움직인다"는 말은 너무 넓다. 경로가 없는지, 경로는 있는데 controller가 실패했는지, /cmd_vel은 나오는데 바퀴가 안 도는지, 또는 AMCL 위치가 틀려 시작점이 벽 안에 들어갔는지를 나누어 볼 수 있다.

실패 상황 1: 지도가 보이지 않는다

RViz에 지도가 보이지 않으면 가장 먼저 map_server를 의심한다.

ros2 lifecycle get /map_server
ros2 topic info /map
ros2 topic echo /map --once

map_server가 active가 아니면 지도를 발행하지 않을 수 있다. lifecycle manager가 정상 실행 중인지 확인하고, RViz의 Nav2 패널에서 Startup을 눌렀는지도 본다.

지도 파일 경로도 확인한다.

ls -l /home/robot/maps/my_room.yaml
cat /home/robot/maps/my_room.yaml

YAML 안의 image가 my_room.pgm인데 실제 파일 이름이 my_room.png라면 로드에 실패한다. 상대 경로 문제도 흔하다. 안전하게 하려면 처음에는 YAML과 이미지 파일을 같은 폴더에 두고, YAML 안의 image를 같은 디렉터리 기준으로 맞춘다.

RViz QoS도 확인한다. /map 토픽은 있는데 RViz만 지도를 못 받는다면 Map display의 Reliability와 Durability 설정을 바꾼다. 특히 Durability가 Volatile이면 이미 발행된 지도를 늦게 구독한 RViz가 놓칠 수 있다.

실패 상황 2: 로봇 위치가 지도와 맞지 않는다

지도는 보이는데 로봇 모델이 이상한 곳에 있거나 LiDAR 스캔이 벽과 맞지 않는다면 초기 위치와 AMCL을 확인한다.

ros2 topic echo /amcl_pose
ros2 topic echo /particlecloud --once
ros2 run tf2_ros tf2_echo map base_link

RViz에서 2D Pose Estimate를 다시 지정해 본다. 이때 위치뿐 아니라 방향이 중요하다. 복도에서 위치는 맞는데 방향을 180도 반대로 주면 LiDAR 스캔이 지도와 어느 정도 비슷해 보여 헷갈릴 수 있다. 하지만 이동을 시작하면 로봇이 엉뚱한 방향으로 회전하거나 경로가 불안정해진다.

AMCL 입자가 넓게 퍼져 있다면 로봇을 천천히 조금 움직여 주면 도움이 된다. 회전하면서 LiDAR가 주변 벽을 여러 방향에서 관찰하면 AMCL이 수렴하기 쉬워진다. 단, 실제 로봇이라면 수동 조종 시 주변 안전을 반드시 확인해야 한다.

실패 상황 3: 목표를 보내도 경로가 생기지 않는다

목표를 보냈는데 /plan이 생기지 않는다면 planner 단계에서 막힌 것이다.

ros2 action list | grep navigate
ros2 topic echo /plan --once
ros2 lifecycle get /planner_server

터미널 로그에서 "failed to create plan", "start is occupied", "goal is occupied" 같은 메시지가 있는지 본다. 시작점이나 목표점이 costmap에서 장애물 안에 있다고 판단되면 planner는 경로를 만들 수 없다.

이때 RViz에서 global costmap을 켜고 목표 지점을 다시 본다. 목표 화살표를 벽 위에 찍지는 않았는가? 목표가 문 바로 옆 inflation 영역 안에 들어가 있지는 않은가? 로봇의 현재 footprint가 장애물 영역과 겹쳐 있지는 않은가?

처음에는 넓고 확실히 비어 있는 공간에 목표를 찍어라. 성공 기준선을 만든 뒤 점점 좁은 곳으로 이동해야 한다. 처음부터 문틈, 의자 옆, 벽 가까이에 목표를 찍으면 Nav2가 실패한 것인지 목표가 무리한 것인지 구분하기 어렵다.

실패 상황 4: 경로는 있는데 로봇이 움직이지 않는다

/plan은 보이는데 로봇이 움직이지 않는다면 controller와 /cmd_vel을 확인한다.

ros2 topic echo /cmd_vel
ros2 topic echo /local_plan --once
ros2 lifecycle get /controller_server

/cmd_vel이 전혀 나오지 않는다면 controller가 유효한 속도를 찾지 못했거나, controller server가 active 상태가 아닐 수 있다. local costmap에 로봇 주변이 장애물로 가득 차 있으면 controller는 안전한 궤적을 찾지 못한다.

/cmd_vel이 나오는데 로봇이 움직이지 않는다면 Nav2 위쪽보다 로봇 하위 시스템을 봐야 한다.

  • 실제 로봇의 모터 전원이 켜져 있는가?

  • emergency stop 또는 safety stop이 걸려 있지 않은가?

  • 구동 노드가 /cmd_vel이 아니라 다른 토픽을 구독하고 있지 않은가?

  • namespace가 붙어 /robot1/cmd_vel을 기대하는데 Nav2는 /cmd_vel로 내보내고 있지 않은가?

  • 시뮬레이션 시간이 멈춰 있거나 Gazebo가 pause 상태는 아닌가?

이 구분은 매우 중요하다. /cmd_vel이 나오면 Nav2는 최소한 "움직이라는 명령"까지는 내린 것이다. 그다음부터는 로봇 드라이버와 하드웨어의 영역이다.

실패 상황 5: 가다가 멈추고 복구 행동만 반복한다

로봇이 출발은 했지만 중간에 멈추고 회전하거나 후진하고, 다시 시도하다가 실패한다면 costmap과 Behavior Tree 복구 흐름을 봐야 한다.

좁은 통로에서 local costmap을 보면 로봇 앞이 빨갛게 막혀 있을 수 있다. 실제로 장애물이 있는 경우도 있지만, 센서 노이즈나 잘못된 footprint 때문에 로봇 자신 또는 바닥 구조물이 장애물로 들어간 경우도 있다.

다음 서비스를 사용해 costmap을 지우고 다시 시도할 수 있다. 서비스 이름은 Nav2 버전과 namespace에 따라 다를 수 있으므로 ros2 service list | grep costmap으로 먼저 확인한다.

ros2 service list | grep costmap

예를 들어 다음과 같은 clear 서비스가 보일 수 있다.

/local_costmap/clear_entirely_local_costmap
/global_costmap/clear_entirely_global_costmap

서비스 타입을 확인한 뒤 호출한다.

ros2 service type /local_costmap/clear_entirely_local_costmap

복구 행동 자체는 실패가 아니다. 오히려 로봇이 "지금 계획대로는 안 되니 다시 해 보자"고 판단하는 정상적인 행동이다. 문제는 같은 복구 행동이 계속 반복되는 경우다. 이때는 목표 지점, inflation, footprint, sensor source, controller 속도 제한을 함께 조정해야 한다.

액션 결과와 로그 읽기

Nav2를 잘 다루려면 로그를 무서워하지 않아야 한다. 목표 이동은 액션이므로 성공, 실패, 취소 같은 결과가 명확히 나온다. CLI에서 --feedback을 붙여 목표를 보내면 남은 거리와 진행 상황을 볼 수 있고, 최종적으로 성공 또는 실패 결과를 확인할 수 있다.

또 다른 터미널에서 로그를 보려면 launch를 실행한 터미널을 유심히 관찰한다. Nav2는 실패 원인을 꽤 친절하게 말해 준다. "목표가 점유 영역에 있다", "TF 변환을 얻을 수 없다", "controller가 유효한 명령을 찾지 못했다" 같은 메시지는 디버깅의 출발점이다.

액션 피드백은 자동화 프로그램을 만들 때도 중요하다. Python에서 Nav2 Simple Commander를 사용하면 목표 전송, 남은 거리 확인, 취소, 결과 확인을 코드로 처리할 수 있다. 예를 들어 순찰 로봇은 목표 A로 가는 중 일정 시간이 지나도 남은 거리가 줄지 않으면 목표를 취소하고 다른 행동을 선택할 수 있다.

이번 장에서는 RViz 중심으로 실습하지만, 실제 제품이나 연구 프로젝트에서는 RViz 클릭 대신 상위 의사결정 노드가 /navigate_to_pose 액션을 호출한다. 사람의 마우스 클릭 자리에 "배송 요청", "순찰 지점", "충전 스테이션 복귀" 같은 로직이 들어가는 것이다.

작은 실험: 파라미터가 움직임을 바꾸는 모습 보기

자율 이동이 한 번 성공했다면, 이제 로봇을 조금 괴롭혀 보자. 단, 실제 로봇에서는 안전한 낮은 속도와 넓은 공간에서 실험해야 한다.

첫 번째 실험은 목표를 벽 가까이에 찍는 것이다. 처음에는 벽에서 1m 떨어진 지점, 다음에는 50cm, 그다음에는 20cm 지점에 목표를 찍어 본다. 어느 순간 planner가 경로를 만들지 못하거나 controller가 목표 근처에서 머뭇거릴 것이다. 이 경계가 현재 footprint와 inflation 설정이 만들어 내는 안전 한계다.

두 번째 실험은 좁은 문을 통과하는 목표를 찍는 것이다. 전역 경로가 문을 통과하는지, 아니면 크게 우회하는지 본다. 문이 실제로는 충분히 넓은데 경로가 지나가지 않는다면 global costmap의 inflation이나 footprint가 너무 보수적일 수 있다.

세 번째 실험은 이동 중 로봇 앞에 작은 장애물을 놓는 것이다. 시뮬레이션이라면 장애물 모델을 추가하거나, 실제 로봇이라면 안전하게 가벼운 물체를 멀리서 배치한다. global path는 그대로일 수 있지만 local costmap이 장애물을 반영하고 controller가 속도를 줄이거나 우회하는지 확인한다.

이 실험들을 하다 보면 Nav2가 단순히 "최단 경로"만 찾는 시스템이 아니라는 것을 알게 된다. 좋은 내비게이션은 빠른 길보다 안전하게 실행 가능한 길을 고른다. 때로는 돌아가고, 때로는 멈추며, 때로는 다시 계획한다. 로봇이 사람처럼 조심스러워지는 순간이 바로 costmap과 controller가 일하고 있는 순간이다.

프로젝트 체크리스트

실습을 마친 뒤 다음 항목을 스스로 확인해 보자.

  • /map이 정상 발행되고 RViz에 저장 지도가 표시된다.

  • map_server, amcl, planner_server, controller_server, bt_navigator가 active 상태다.

  • map → odom → base_link TF 체인이 끊기지 않는다.

  • RViz에서 초기 위치를 지정하면 LiDAR 스캔이 지도 벽과 겹친다.

  • /particlecloud가 좁게 모여 AMCL 위치 추정이 안정된다.

  • Nav2 Goal을 보내면 /plan 전역 경로가 생성된다.

  • 로봇 이동 중 /local_plan, local costmap, /cmd_vel이 갱신된다.

  • 목표 근처에서 속도가 줄고 최종적으로 액션이 성공한다.

  • 실패 시 지도, lifecycle, TF, AMCL, costmap, 액션 로그 순서로 원인을 좁힐 수 있다.

이 체크리스트를 모두 통과했다면 이제 로봇은 저장된 지도를 이용해 목표 지점까지 자율 이동할 수 있다. 이것은 ROS2 이동 로봇 시스템에서 매우 큰 이정표다. 지금까지 따로 배웠던 노드, 토픽, TF, URDF, 센서, SLAM, AMCL, Nav2가 하나의 흐름으로 이어졌기 때문이다.

마무리: 목표 이동은 끝이 아니라 시작이다

이번 장에서 로봇은 드디어 지도 위 목표 지점까지 스스로 이동했다. 하지만 실전 환경은 항상 친절하지 않다. 복도에는 사람이 지나가고, 의자는 어제와 다른 위치에 놓이며, 문 앞에는 상자가 생긴다. 지도에는 없던 장애물이 갑자기 등장하고, 좁은 공간에서는 로봇이 회전할 여유를 잃기도 한다.

그래서 다음 장에서는 동적 장애물과 장애물 회피를 더 깊게 다룬다. local costmap이 어떻게 움직이는 장애물을 반영하는지, controller가 속도를 어떻게 조정하는지, 회피 실패는 어떤 패턴으로 나타나는지 살펴볼 것이다.

오늘의 로봇은 목표까지 갔다. 다음 장의 로봇은 목표까지 가는 길에서 누군가를 만나게 된다. 그때 로봇이 멈출지, 돌아갈지, 기다릴지, 다시 계획할지는 이제 우리가 설계해야 할 차례다.

참고

1navigation2 nav2_map_server README

2Nav2 Map Server / Saver configuration

3Navigating with a Physical Turtlebot 3 — Nav2 documentation

4nav2-rviz-tools RViz display and QoS notes

5TurtleBot 4 Navigation tutorial

6Sending Goals to the Robot Navigation Stack

책 목록

ROS2로 완성하는 자율주행 로봇: 노드·TF·SLAM·Nav2 실전 설계

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기