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 | 항상 켜 둠 |
| T2 | VM | ros2 launch tiki_slam slam.launch.py | 지도 작성 |
| T3 | VM | ros2 run teleop_twist_keyboard teleop_twist_keyboard | 지도 작성 |
| T4 | VM | rviz2 | 지도 작성 |
| T5 | VM | ros2 launch tiki_navigation bringup.launch.py map:=... | 내비게이션 (T2를 끈 뒤) |
| T6 | VM | ros2 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이 쌓여 지도가 점점 커집니다. 흰 영역이 지나간 빈 공간, 검은 점이 벽(점유)입니다.
- ① 로봇이 돌아다니면서 만들어 가는 지도. 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
- ① 칸 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 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)을 씁니다. 순서는 이렇습니다.
- 로봇이 지도의 어디에 있는지 눈으로 보고, RViz2의 2D Pose Estimate로 그 위치와 방향을 클릭하고 드래그해서 찍습니다. 아래 화면처럼 지도에서 로봇이 있는 곳을 누른 채 로봇이 바라보는 방향으로 끌면 초록 화살표가 그려집니다. AMCL은
set_initial_pose가 기본 false이고nav2_params.yaml도 켜지 않았으므로 이 입력을 기다립니다. 소스에서 이 기본값을 확인했습니다. - 파티클 구름이 로봇 주변으로 모이는지 봅니다. 퍼진 채로 있으면 제자리에서 천천히 회전시켜 라이다가 더 많은 벽을 보게 합니다.
ros2 run tf2_ros tf2_echo map odom이 값을 출력하는지 확인합니다. 출력되면map→odom이 생긴 것입니다.
- ①
2D Pose Estimate버튼 - ② 지도 위에서 클릭한 채 드래그해 만든 화살표. 시작점이 위치, 방향이 로봇의 바라보는 방향입니다.
- ③
Navigation 2패널. 위치를 찍기 전에는 Navigation과 Localization이unknown입니다. 찍으면active가 됩니다.
목표 보내기
RViz2의 Nav2 Goal 버튼으로 가까운 곳(1 m 안팎)을 찍어 방향까지 지정합니다. 방식은 2D Pose Estimate와 같고 클릭한 곳이 목표 위치, 끌어서 정한 방향이 도착했을 때 로봇이 향할 방향입니다.
- ①
Nav2 Goal버튼 - ② 지도 위에서 목표 위치를 누르고 방향까지 끕니다. 지도 위의 색 영역이 코스트맵(장애물 주변의 위험도)이고 초록 점들이 AMCL 파티클입니다.
목표를 보내면 로봇이 경로를 계획해 움직입니다.
- ①
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.0 | nav2_params.yaml FollowPath |
| 속도 평활기가 막는 상한 | max_velocity: [0.15, 0.0, 1.0] | velocity_smoother |
| 컨트롤러 클램프 | 0.2 m/s, 0.85 rad/s | tiki_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 값을 비교해 실제로 어떻게 나오는지 적어 두세요. 이 값들은 파일에서 읽은 것이고 로봇에서 측정하지 않았습니다.
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가 시작됩니다. 기존 지도 파일 경로를 주세요.
이 모드로 지도를 만들었다면 같은 방식으로 map_saver_cli로 저장하고, 내비게이션 단계로 넘어갈 때는 이 런치를 끄고 slam:=False로 다시 시작합니다. tiki_slam을 따로 켜 둔 채 slam:=True를 함께 쓰면 SLAM이 둘이 되어 TF가 충돌합니다.
실패 진단표
증상, 확인 명령, 연결되는 파라미터를 묶었습니다. 값은 nav2_params.yaml과 slam.yaml에서 읽은 것입니다.
| 증상 | 확인 | 연결되는 설정 | 대응 |
|---|---|---|---|
map 프레임이 없다, 코스트맵이 초기화 안 된다 | ros2 run tf2_ros tf2_echo map odom | AMCL은 초기 자세를 받아야 map → odom을 발행 | 2D Pose Estimate를 찍는다. SLAM 모드면 slam_toolbox와 /scan을 확인 |
| 파티클이 퍼진 채 모이지 않거나 위치가 튄다 | RViz2의 /particle_cloud, /amcl_pose | AMCL 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.0 | 10초 안에 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.5 | 2.12장의 inflation 모형으로 비용을 계산해 비교 |
| 지도가 이중으로 겹치거나 휜다 | 주행 기록과 RViz2의 스캔 정렬 | minimum_travel_distance, minimum_travel_heading, do_loop_closing, loop_match_minimum_response_fine: 0.45 | 속도를 낮추고 회전을 줄인다. 고리를 닫는 경로를 포함해 다시 만든다 |
| TF 경고가 반복된다 | date 비교, ros2 topic delay /scan | slam.yaml의 transform_timeout: 0.2, AMCL transform_tolerance: 1.0 | 두 컴퓨터의 시각을 맞추고 네트워크 상태를 점검 |
한 번에 한 가지만 바꾸고 결과를 기록하세요. 여러 값을 동시에 바꾸면 무엇이 효과였는지 알 수 없습니다. 값을 바꿀 때는 원본 nav2_params.yaml을 복사해 새 파일로 고치고 params_file:=<절대경로>로 지정합니다.
한눈에 정리
- 순서는 기본 구동, SLAM과 텔레옵, 지도 저장, SLAM 종료,
bringup.launch.py map:=..., 초기 자세, 목표 전달이다. - 좋은 지도는 느린 주행, 한 번의 통과, 고리 닫기에서 나온다. 근거는
minimum_travel_distance,map_update_interval,do_loop_closing이다. - 지도는
ros2 run nav2_map_server map_saver_cli -f ~/maps/<이름>으로 저장하고map:=$HOME/maps/<이름>.yaml로 쓴다. - AMCL은 초기 자세를 받아야
map→odom을 발행한다. SLAM과 AMCL을 동시에 켜지 않는다. - DWB 0.26, 평활기 0.15, 컨트롤러 0.2의 순서로 속도가 잘리는 것으로 읽히며 실제 값은
echo로 확인한다. slam:=True는 동기 slam_toolbox를 쓰고slam.yaml이 적용되지 않을 가능성이 있어 값을 읽어 확인한다.- 실패는 증상, 확인 명령, 연결 파라미터를 짝지어 진단하고 한 번에 한 값만 바꾼다.
확인 문제
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장은 이 장의 순서를 하나의 과제로 묶습니다. 방의 한 구역 지도를 만들고 세 지점을 자율 주행으로 방문한 뒤 출발점으로 돌아옵니다.