TIKI-MINI로 시작하는 ROS2 실무
목차

4. 실기기 연동

4.7실기기 SLAM과 Nav2

2.10장과 2.12장에서 slam.yaml과 nav2_params.yaml을 읽었습니다. 이 장은 같은 파일을 실제 로봇에서 돌립니다. 설정의 의미는 그 장들을 참고하고, 여기서는 실행 순서와 확인, 실패 진단에 집중합니다. 먼저 전체 흐름을 눌러 보며 익히세요.

단계를 눌러 필요한 명령, 떠 있어야 하는 것, 이상 징후를 확인하세요.

실행 위치 로봇 터미널 (미리 설치된 환경)

명령

ros2 launch tiki_bringup tiki.launch.py

떠 있어야 하는 노드

  • robot_state_publisher
  • controller_manager
  • diff_controller
  • joint_state_broadcaster
  • rplidar_node

토픽 또는 액션

  • /cmd_vel
  • /odom
  • /scan
  • /joint_states
  • /tf
  • /tf_static

TF

  • odom → base_footprint
  • base_footprint → … → laser

이상 징후

  • /scan이 안 나온다

    ros2 node list에 rplidar_node가 있는지, LiDAR가 /dev/ttyUSB0로 잡혔는지(ls /dev/ttyUSB*)

  • /odom이 안 나온다

    런치 후 7초가 지났는지, ros2 control list_controllers에 diff_controller가 active인지

  • VM에서 토픽이 안 보인다

    양쪽 ROS_DOMAIN_ID와 네트워크 (4.2장)

터미널 계획

터미널이 여러 개라 번호를 붙여 둡니다. 모든 터미널에서 ROS_DOMAIN_ID는 같아야 합니다(4.2장).

번호위치하는 일단계
T1로봇ros2 launch tiki_bringup tiki.launch.py항상 켜 둠
T2VMros2 launch tiki_slam slam.launch.py지도 작성
T3VMros2 run teleop_twist_keyboard teleop_twist_keyboard지도 작성
T4VMrviz2지도 작성
T5VMros2 launch tiki_navigation bringup.launch.py map:=...내비게이션 (T2를 끈 뒤)
T6VMros2 launch nav2_bringup rviz_launch.py내비게이션

SLAM과 Nav2는 개발 PC(VM)에서 실행합니다. 로봇 보드에서는 slam_toolbox와 Nav2를 돌리지 않습니다. OVA의 VM에는 tiki_slam과 tiki_navigation이 이미 빌드되어 있습니다. /scan, /odom, TF는 무선망을 지나 VM으로 들어오므로, 지연이 크면 slam.yaml의 transform_timeout: 0.2 안에 변환을 못 찾을 수 있습니다.

1단계: 기본 구동 확인

T1을 실행하고 약 10초 기다립니다. tiki.launch.py는 LiDAR를 3초 뒤, 컨트롤러를 7초 뒤에 올립니다. VM의 새 터미널에서 확인합니다.

ros2 node list
ros2 topic hz /scan
ros2 topic hz /odom
ros2 control list_controllers
ros2 run tf2_ros tf2_echo odom base_footprint

/scan이 약 10 Hz, /odom이 publish_rate: 50.0에 가까운 값으로 나오고, joint_state_broadcaster와 diff_controller가 active이며 tf2_echo가 변환을 계속 출력하면 준비된 것입니다. 하나라도 안 되면 4.4장으로 돌아갑니다.

2단계: 지도 만들기

T2에서 SLAM을 켜고 T4에서 RViz2를 엽니다. Fixed Frame을 map으로 하고 Map(토픽 /map), LaserScan(토픽 /scan), TF를 추가합니다. map 프레임이 목록에 없으면 SLAM이 아직 첫 스캔을 처리하지 못한 것입니다. LaserScan 디스플레이는 Reliability를 Best effort로 두면 연결이 안정적입니다. /map은 slam_toolbox가 Transient Local로 발행하므로(소스에서 확인) 지도가 안 보이면 Map 디스플레이의 Durability Policy를 Transient Local로 바꿉니다.

OVA의 VM에는 SLAM과 Nav2용 RViz 설정이 ~/.rviz2/nav2_default_view.rviz로 들어 있습니다. 이 설정으로 열면 위 디스플레이가 이미 갖춰져 있습니다.

ros2 launch tiki_slam slam.launch.py
rviz2 -d ~/.rviz2/nav2_default_view.rviz

실제 로봇으로 찍은 화면입니다. 로봇이 움직이면서 /scan이 쌓여 지도가 점점 커집니다. 흰 영역이 지나간 빈 공간, 검은 점이 벽(점유)입니다.

RViz2에서 라이다 데이터로 지도가 만들어지는 모습. 지도 영역에 번호 표시
  • ① 로봇이 돌아다니면서 만들어 가는 지도. 5초에 한 번씩 갱신됩니다(map_update_interval).

SLAM 로그에 minimum laser range setting (0.0 m) exceeds the capabilities of the used Lidar (0.2 m)라는 WARN이 보일 수 있습니다. slam.yaml의 최소 거리 설정이 0.0인데 이 라이다의 실제 최소 거리는 0.2 m라는 안내입니다. 지도 작성에는 영향이 없는 경고로 보였습니다.

T3으로 로봇을 움직입니다. 처음에는 속도를 낮추세요. 텔레옵 키보드의 속도 키로 줄이면 되고, 컨트롤러가 0.2 m/s, 0.85 rad/s에서 어차피 자르지만 SLAM에는 이보다 느린 편이 낫습니다.

좋은 지도를 만드는 주행

slam.yaml의 값으로 이유를 설명합니다.

값의미주행에 주는 영향
minimum_travel_distance: 0.5, minimum_travel_heading: 0.5이만큼 움직이거나 돌아야 새 스캔을 처리제자리에서 흔들기만 하면 지도가 안 자란다. 0.5 m 직진 또는 약 28.6도 회전마다 한 노드
map_update_interval: 5.0지도 그림을 5초마다 갱신RViz2의 지도는 5초 단위로 바뀐다. 보이지 않아도 조급해하지 않는다
minimum_time_interval: 0.5스캔 처리 최소 간격매우 빠르게 움직여도 초당 2번만 처리한다
do_loop_closing: true, loop_search_maximum_distance: 3.0예전에 본 곳에 돌아오면 누적 오차를 한꺼번에 보정한 바퀴를 닫아 돌아오는 경로를 한 번 포함한다
max_laser_range: 12.0지도에 쓰는 최대 거리라이다의 range_max(4.6장에서 로그로 확인)가 더 짧으면 그 값이 실제 한계다

실제 주행 요령은 이렇습니다.

  • 직진은 천천히, 회전은 더 천천히 합니다. 4.6장에서 본 대로 회전 오차가 방향 오차로 크게 번지고, 방향 오차는 지도에서 벽이 휘거나 겹치는 모양이 됩니다.
  • 같은 구간을 여러 번 오가지 않습니다. 같은 벽을 서로 다른 누적 오차로 여러 번 보면 겹쳐 그려지기 쉽습니다.
  • 출발점으로 돌아오는 고리를 만듭니다. 돌아왔을 때 벽이 이중으로 보이다가 한 번에 맞는 순간이 루프 클로저입니다.
  • 유리문, 거울, 반짝이는 금속 면, 검은 천은 피하거나 가려 둡니다. 4.6장에서 본 대로 측정이 비거나 틀릴 수 있습니다. TIKI-MINI에서 어떤 표면이 문제가 되는지는 직접 재 보지 않았으니 의심되면 그 앞에서 inf를 세어 보세요.
  • 사람이 지도 안에서 움직이면 벽 아닌 자국이 남을 수 있습니다. 지도를 만드는 동안은 로봇 주변을 비웁니다.

주행 중에도 RViz2에서 LaserScan 점이 지도의 벽 위에 겹쳐 보이는지 확인합니다. 점이 벽에서 계속 어긋나면 속도를 낮추고 그 자리에서 다시 시도합니다.

3단계: 지도 저장

방 전체가 선명하게 그려지면 저장합니다. SLAM이 켜져 있어야 합니다.

mkdir -p ~/maps
ros2 run nav2_map_server map_saver_cli -f ~/maps/room1
ls ~/maps

map_saver_cli는 /map을 구독해 이미지와 YAML 두 파일을 만듭니다(옵션 -f, -t, --occ, --free, --fmt, --mode는 navigation2 humble 소스의 도움말에서 확인). 이미지 확장자는 ls로 확인하세요. Failed to spin map subscription이 나오면 /map을 못 받은 것이므로 SLAM과 ros2 topic info /map을 확인합니다. 저장한 뒤에는 YAML을 열어 image, resolution(0.05가 slam.yaml과 맞는지), origin을 한 번 읽어 보세요.

여기서는 홈 폴더에 map01이라는 이름으로 저장했습니다. VM 터미널을 여러 칸으로 나눠 쓰면 편합니다.

ros2 run nav2_map_server map_saver_cli -f /home/ros2/map01
ls /home/ros2
Tilix 네 칸에서 SLAM, RViz2, 텔레옵, map_saver를 각각 실행 중인 화면. 각 칸에 번호 표시
  • ① 칸 1: ros2 launch tiki_slam slam.launch.py (SLAM 노드)
  • ② 칸 2: rviz2 실행 칸. Trying to create a map of size ... 로그가 지도 크기를 알려 줍니다.
  • ③ 칸 3: ros2 run teleop_twist_keyboard teleop_twist_keyboard (이 칸을 클릭해 둔 상태에서 키를 누릅니다)
  • ④ 칸 4: export ROS_DOMAIN_ID를 맞추고 map_saver_cli를 입력한 칸

텔레옵 키는 이렇습니다. 로봇을 움직이려면 텔레옵을 실행한 칸에 포커스를 두어야 합니다.

키동작
i전진
,후진
l시계 방향 회전
j반시계 방향 회전
w / x선속도(전후진 속도)를 10% 올림 / 내림
e / c회전 속도를 10% 올림 / 내림
map_saver 성공 로그와 ls 결과에 map01.pgm과 map01.yaml이 보이는 화면. 번호 표시
  • ① Map saved successfully: 저장 성공 로그
  • ② ls /home/ros2에 map01.pgm(이미지)과 map01.yaml(설명 파일)이 생겼습니다.

저장이 끝나면 T2의 SLAM을 Ctrl+C로 끕니다. SLAM과 AMCL이 둘 다 map → odom을 발행하면 TF가 충돌하기 때문입니다(2.12장).

4단계: 위치 추정과 내비게이션

T5를 실행합니다. map 인자는 필수이고 절대 경로여야 합니다. ~는 map:= 뒤에서 쉘이 풀어 주지 않으니 $HOME을 씁니다.

ros2 launch tiki_navigation bringup.launch.py map:=$HOME/maps/room1.yaml

slam 인자의 기본값은 False라서 이 명령은 localization_launch.py(map_server, amcl)와 navigation_launch.py를 함께 띄웁니다. T6으로 RViz2를 엽니다.

ros2 launch nav2_bringup rviz_launch.py

이 런치는 nav2_bringup에 들어 있는 기본 RViz 설정(nav2_default_view.rviz)을 씁니다. 순서는 이렇습니다.

  1. 로봇이 지도의 어디에 있는지 눈으로 보고, RViz2의 2D Pose Estimate로 그 위치와 방향을 클릭하고 드래그해서 찍습니다. 아래 화면처럼 지도에서 로봇이 있는 곳을 누른 채 로봇이 바라보는 방향으로 끌면 초록 화살표가 그려집니다. AMCL은 set_initial_pose가 기본 false이고 nav2_params.yaml도 켜지 않았으므로 이 입력을 기다립니다. 소스에서 이 기본값을 확인했습니다.
  2. 파티클 구름이 로봇 주변으로 모이는지 봅니다. 퍼진 채로 있으면 제자리에서 천천히 회전시켜 라이다가 더 많은 벽을 보게 합니다.
  3. ros2 run tf2_ros tf2_echo map odom이 값을 출력하는지 확인합니다. 출력되면 map → odom이 생긴 것입니다.
2D Pose Estimate 버튼과 지도 위에 끌어 그린 초록 화살표, Navigation 2 패널에 번호 표시
  • ① 2D Pose Estimate 버튼
  • ② 지도 위에서 클릭한 채 드래그해 만든 화살표. 시작점이 위치, 방향이 로봇의 바라보는 방향입니다.
  • ③ Navigation 2 패널. 위치를 찍기 전에는 Navigation과 Localization이 unknown입니다. 찍으면 active가 됩니다.

목표 보내기

RViz2의 Nav2 Goal 버튼으로 가까운 곳(1 m 안팎)을 찍어 방향까지 지정합니다. 방식은 2D Pose Estimate와 같고 클릭한 곳이 목표 위치, 끌어서 정한 방향이 도착했을 때 로봇이 향할 방향입니다.

Nav2 Goal 버튼과 지도에 목표를 끌어 그린 초록 화살표에 번호 표시
  • ① Nav2 Goal 버튼
  • ② 지도 위에서 목표 위치를 누르고 방향까지 끕니다. 지도 위의 색 영역이 코스트맵(장애물 주변의 위험도)이고 초록 점들이 AMCL 파티클입니다.

목표를 보내면 로봇이 경로를 계획해 움직입니다.

로봇이 목표로 이동하는 중의 RViz2. Navigation 2 패널의 상태와 경로에 번호 표시
  • ① Navigation 2 패널: Navigation, Localization, Feedback이 모두 active이고 남은 거리(Distance remaining)와 걸린 시간이 갱신됩니다. 중간에 Cancel 버튼으로 멈출 수 있습니다.
  • ② 지도 위에서 로봇 주변의 경로와 코스트맵. 막히면 Recoveries 값이 올라가는 것도 이 패널에서 볼 수 있습니다.

터미널에서는 이렇게도 보냅니다.

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

글로벌 경로는 /plan, 로컬 경로와 코스트맵은 RViz2의 디스플레이로 봅니다. 로봇이 움직이는 동안 속도 명령의 흐름을 비교하세요.

ros2 topic echo /cmd_vel_nav --field linear.x
ros2 topic echo /cmd_vel --field linear.x

속도는 어떻게 잘리나

navigation_launch.py(nav2_bringup humble)에서 컨트롤러 서버의 cmd_vel은 cmd_vel_nav로, 속도 평활기의 입력은 cmd_vel_nav, 출력(cmd_vel_smoothed)은 cmd_vel로 리매핑됩니다. 결과 흐름은 다음 단계입니다.

단계파일의 값위치
DWB가 고르는 후보 속도의 상한max_vel_x: 0.26, max_vel_theta: 1.0nav2_params.yaml FollowPath
속도 평활기가 막는 상한max_velocity: [0.15, 0.0, 1.0]velocity_smoother
컨트롤러 클램프0.2 m/s, 0.85 rad/stiki_controllers.yaml

직선 속도는 평활기의 0.15 m/s가 가장 낮아서 이 값이 먼저 작동하고, 회전 속도는 평활기의 1.0 rad/s보다 컨트롤러의 0.85 rad/s가 낮아서 /cmd_vel이 0.85 rad/s로 잘릴 것으로 읽힙니다. 즉 DWB의 0.26 m/s는 실제로 쓰이지 못하고, 회전은 DWB가 1.0 rad/s를 골라도 로봇은 0.85로 돕니다. 계획한 회전보다 느리게 돌아서 경로 추종이 흔들릴 수 있습니다. 위 두 echo 값을 비교해 실제로 어떻게 나오는지 적어 두세요. 이 값들은 파일에서 읽은 것이고 로봇에서 측정하지 않았습니다.

속도 명령이 잘리는 순서controller_serverDWB (FollowPath)직선 0.26 m/s회전 1.0 rad/svelocity_smoothermax_velocity직선 0.15 m/s회전 1.0 rad/sdiff_controllertiki_controllers.yaml직선 0.2 m/s회전 0.85 rad/s/cmd_vel_nav/cmd_velDWB와 평활기 값은 nav2_params.yamlDWB 직선 0.26실제로 쓰이지 못함직선 상한0.15 m/s가 가장 낮음회전 상한0.85 rad/s가 가장 낮음파일에서 읽은 값 (로봇에서 측정하지 않음)
DWB가 낸 명령은 /cmd_vel_nav, 속도 평활기, /cmd_vel, diff_controller 순서로 흐릅니다. 아래 칸은 직선과 회전 각각에서 가장 낮아 실제로 작동하는 상한입니다.

SLAM과 내비게이션을 한 번에 (slam:=True)

bringup.launch.py의 slam:=True는 지도를 만들면서 바로 내비게이션도 쓰는 방식입니다.

ros2 launch tiki_navigation bringup.launch.py slam:=True map:=$HOME/maps/room1.yaml

런치 파일에서 읽은 내용입니다. 이 모드에서는 localization_launch.py(map_server, amcl) 대신 nav2_bringup의 slam_launch.py가 실행되고, 그 안에서 map_saver_server와 slam_toolbox의 online_sync_launch.py(동기 노드 sync_slam_toolbox_node)가 올라옵니다. map 인자는 기본값이 없어서 이 모드에서도 줘야 launch가 시작됩니다. 기존 지도 파일 경로를 주세요.

slam 인자에 따른 런치 구성bringup.launch.pytiki_navigation, map 인자 필수localization_launch.pyslam:=False (기본)map_serveramclslam_launch.pyslam:=Truemap_saver_serversync_slam_toolbox_nodenavigation_launch.py두 모드에서 항상 실행map → odom: amclmap → odom: slam_toolboxTF 충돌 주의SLAM과 AMCL, 또는SLAM 둘을 함께 켜지 않음
bringup.launch.py는 slam 인자에 따라 localization_launch.py와 slam_launch.py 중 하나를 띄우고, navigation_launch.py는 두 모드 모두 띄웁니다. 어느 쪽이든 map → odom을 발행하는 노드는 하나여야 합니다.

이 모드로 지도를 만들었다면 같은 방식으로 map_saver_cli로 저장하고, 내비게이션 단계로 넘어갈 때는 이 런치를 끄고 slam:=False로 다시 시작합니다. tiki_slam을 따로 켜 둔 채 slam:=True를 함께 쓰면 SLAM이 둘이 되어 TF가 충돌합니다.

실패 진단표

증상, 확인 명령, 연결되는 파라미터를 묶었습니다. 값은 nav2_params.yaml과 slam.yaml에서 읽은 것입니다.

증상확인연결되는 설정대응
map 프레임이 없다, 코스트맵이 초기화 안 된다ros2 run tf2_ros tf2_echo map odomAMCL은 초기 자세를 받아야 map → odom을 발행2D Pose Estimate를 찍는다. SLAM 모드면 slam_toolbox와 /scan을 확인
파티클이 퍼진 채 모이지 않거나 위치가 튄다RViz2의 /particle_cloud, /amcl_poseAMCL min_particles: 500, max_particles: 3000, update_min_d: 0.25, update_min_a: 0.2, laser_likelihood_max_dist: 2.0초기 자세를 다시 찍고 천천히 회전. 4.6장의 오도메트리 오차가 크면 alpha1~alpha5(모두 0.2)의 의미를 2.12장에서 다시 읽음
목표가 거부되거나 바로 실패한다시작 로그, 글로벌 코스트맵글로벌 robot_radius: 0.22, inflation_radius: 0.2, track_unknown_space: true, 플래너 tolerance: 0.5목표를 코스트맵의 자유 공간으로 옮긴다. 좁은 통로면 반경 값을 재검토
경로는 보이는데 로봇이 안 움직인다/cmd_vel_nav와 /cmd_vel 비교velocity_smoother의 velocity_timeout: 1.0, 컨트롤러 cmd_vel_timeout한쪽만 나오면 평활기, 둘 다 안 나오면 컨트롤러 서버 상태(ros2 lifecycle get /controller_server)
움직이다가 10초쯤 뒤 멈추고 실패한다컨트롤러 서버 로그progress_checker: required_movement_radius: 0.5, movement_time_allowance: 10.010초 안에 0.5 m 못 나가면 실패로 봄. 평활기 0.15 m/s 아래에서 천천히 돌아 나올 때 걸린다
제자리에서 좌우로 떨거나 목표 앞에서 맴돈다로컬 경로와 로컬 코스트맵controller_frequency: 10.0, critics의 PathAlign.scale: 32.0 등, BaseObstacle.scale: 0.02, 로컬 robot_radius: 0.12, 도착 허용 xy_goal_tolerance: 0.25회전 한계 0.85가 DWB 1.0과 어긋난 영향, 지연(4.6장)을 의심. 한 번에 한 값만 바꿔 비교
벽 옆에서 자꾸 멈추거나 우회가 이상하다두 코스트맵의 inflation 모양로컬 inflation_radius: 0.2, cost_scaling_factor: 2.0, 장애물 입력 obstacle_max_range: 2.52.12장의 inflation 모형으로 비용을 계산해 비교
지도가 이중으로 겹치거나 휜다주행 기록과 RViz2의 스캔 정렬minimum_travel_distance, minimum_travel_heading, do_loop_closing, loop_match_minimum_response_fine: 0.45속도를 낮추고 회전을 줄인다. 고리를 닫는 경로를 포함해 다시 만든다
TF 경고가 반복된다date 비교, ros2 topic delay /scanslam.yaml의 transform_timeout: 0.2, AMCL transform_tolerance: 1.0두 컴퓨터의 시각을 맞추고 네트워크 상태를 점검

한 번에 한 가지만 바꾸고 결과를 기록하세요. 여러 값을 동시에 바꾸면 무엇이 효과였는지 알 수 없습니다. 값을 바꿀 때는 원본 nav2_params.yaml을 복사해 새 파일로 고치고 params_file:=<절대경로>로 지정합니다.

한눈에 정리

  1. 순서는 기본 구동, SLAM과 텔레옵, 지도 저장, SLAM 종료, bringup.launch.py map:=..., 초기 자세, 목표 전달이다.
  2. 좋은 지도는 느린 주행, 한 번의 통과, 고리 닫기에서 나온다. 근거는 minimum_travel_distance, map_update_interval, do_loop_closing이다.
  3. 지도는 ros2 run nav2_map_server map_saver_cli -f ~/maps/<이름>으로 저장하고 map:=$HOME/maps/<이름>.yaml로 쓴다.
  4. AMCL은 초기 자세를 받아야 map → odom을 발행한다. SLAM과 AMCL을 동시에 켜지 않는다.
  5. DWB 0.26, 평활기 0.15, 컨트롤러 0.2의 순서로 속도가 잘리는 것으로 읽히며 실제 값은 echo로 확인한다.
  6. slam:=True는 동기 slam_toolbox를 쓰고 slam.yaml이 적용되지 않을 가능성이 있어 값을 읽어 확인한다.
  7. 실패는 증상, 확인 명령, 연결 파라미터를 짝지어 진단하고 한 번에 한 값만 바꾼다.

확인 문제

Q1. 내비게이션을 켰는데 RViz2에서 로봇이 지도 밖에 있고 목표를 보내도 반응이 없습니다. 먼저 할 일은?

2D Pose Estimate로 초기 자세를 줬는지 확인합니다. AMCL은 이 입력을 받아야 map → odom TF를 발행하며, nav2_params.yaml은 set_initial_pose를 켜지 않았습니다. tf2_echo map odom으로 변환이 생겼는지 봅니다.

Q2. 로봇이 목표로 가다가 약 10초 뒤에 멈추고 실패했습니다. 어떤 설정이 의심되나요?

controller_server의 progress_checker입니다. movement_time_allowance: 10.0초 동안 required_movement_radius: 0.5 m 이상 움직이지 못하면 실패로 처리합니다. 장애물 앞에서 천천히 돌아 나올 때 걸릴 수 있습니다.

Q3. DWB의 max_vel_x가 0.26인데 로봇이 그 속도로 달리지 않는 이유는? 파일 값으로 설명하세요.

컨트롤러 서버의 명령이 속도 평활기를 거쳐 max_velocity의 0.15 m/s로 먼저 제한되고, 마지막에 diff_controller가 0.2 m/s로 자릅니다. 직선 속도는 평활기의 0.15가 가장 낮습니다. 실제 값은 /cmd_vel_nav와 /cmd_vel을 echo해서 확인합니다.

다음 장

4.8장은 이 장의 순서를 하나의 과제로 묶습니다. 방의 한 구역 지도를 만들고 세 지점을 자율 주행으로 방문한 뒤 출발점으로 돌아옵니다.