4. 실기기 연동
4.8최종 프로젝트와 평가
2부의 에뮬레이터와 4부의 로봇에서 하나씩 쌓은 것을 이 장에서 한 과제로 묶습니다. 새로 배우는 개념은 없습니다. 앞 장의 순서를 혼자 계획하고 실행하고 기록하는 것이 과제입니다.
과제
방의 한 구역(약 3 m x 3 m)을 지도로 만든다. 그 지도 위에서 TIKI-MINI가 출발점에서 시작해 세 지점을 순서대로 자율 주행으로 방문하고 출발점으로 돌아온다.
2.14장의 미니 프로젝트를 했다면 흐름이 익숙할 것입니다. 실기기에서는 지도, 위치 추정, 오차를 직접 다뤄야 합니다.
요구사항
- 지도는 4.7장 순서대로 만들고
~/maps/<팀이름>.yaml과 이미지 파일로 저장한다. - 지도에는 구역의 벽과 고정 장애물이 보이고, 방문 지점 세 곳과 출발점이 자유 공간에 있다.
tiki_navigation의bringup.launch.py로 Nav2를 켜고 RViz2의 2D Pose Estimate로 초기 자세를 준다.- 세 지점과 출발점으로 돌아오는 주행을 목표 전달로 수행한다. 방법은 둘 중 하나를 고른다. 동작 확인용으로는 RViz2의 Nav2 Goal 버튼으로 하나씩 보내고, 점수 시연에는 아래 코드로 한 번에 보낸다.
- 주행 동안
/odom이나 액션 피드백으로 진행 상황을 기록하는 로그를 남긴다. - 시연은 한 번 실패해도 원인을 설명하고 다시 시도할 수 있다.
산출물
| 산출물 | 형식 | 비고 |
|---|---|---|
| 지도 | .yaml과 이미지 파일 | 만든 날짜와 구역 설명을 README에 적음 |
| 방문 지점 목록 | 표 (이름, x, y, 방향) | RViz2에서 읽은 지도 좌표 |
| 실행 스크립트 | waypoint_runner.py 또는 실행 순서 문서 | 명령이 그대로 재현되도록 |
| 주행 기록 | 로그 파일 또는 표 | 지점별 도착 오차, 소요 시간, 실패와 재시도 횟수 |
| 문제 해결 기록 | 표 | 증상, 확인 명령, 원인, 해결, 관련 파라미터 (4.7장 표 형식) |
| 시연 | 현장 | 아래 체크리스트 |
코드로 목표 보내기
navigation2 humble 브랜치에는 nav2_simple_commander 패키지가 있고, navigation2 메타패키지의 실행 의존성에 이 패키지가 들어 있습니다(package.xml에서 확인). ros-humble-navigation2를 설치했다면 이미 들어 있을 것입니다. 로봇이나 VM에서 python3 -c "import nav2_simple_commander"로 확인하세요. 안 되면 아래 코드 대신 RViz2 목표 버튼과 /odom 기록을 씁니다.
BasicNavigator의 followWaypoints, isTaskComplete, getFeedback, getResult는 humble 소스에서 확인했습니다. 아래 노드는 지도 좌표의 지점 네 개(세 곳과 복귀)를 보내고 진행 상황을 출력합니다. 정적 코드이며 이 페이지에서 실행되지 않습니다. 4.5장의 규칙과 같게 종료 처리를 넣었습니다.
import math
import rclpy
from rclpy.duration import Duration
from rclpy.signals import SignalHandlerOptions
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
# 지도 프레임 기준 (x m, y m, 방향 도). RViz2에서 클릭하며 읽은 값으로 바꾼다.
WAYPOINTS = [
(1.0, 0.0, 0.0), # 1번
(1.0, 1.0, 90.0), # 2번
(0.0, 1.0, 180.0), # 3번
(0.0, 0.0, 0.0), # 출발점으로 복귀
]
TIME_LIMIT = Duration(seconds=240) # 이 시간이 지나면 중단한다
def make_pose(nav, x, y, yaw_deg):
p = PoseStamped()
p.header.frame_id = 'map'
p.header.stamp = nav.get_clock().now().to_msg()
p.pose.position.x = x
p.pose.position.y = y
half = math.radians(yaw_deg) / 2.0
p.pose.orientation.z = math.sin(half)
p.pose.orientation.w = math.cos(half)
return p
def main():
rclpy.init(signal_handler_options=SignalHandlerOptions.NO)
nav = BasicNavigator()
try:
# 초기 자세는 RViz2의 2D Pose Estimate로 이미 준 상태여야 한다.
# localizer를 기본값 'amcl'로 두면 이 함수가 초기 자세를 원점으로 발행해 버리므로
# bt_navigator만 기다린다.
nav.waitUntilNav2Active(localizer='bt_navigator')
poses = [make_pose(nav, *w) for w in WAYPOINTS]
start = nav.get_clock().now()
nav.followWaypoints(poses)
last = -1
while not nav.isTaskComplete():
fb = nav.getFeedback()
if fb is not None and fb.current_waypoint != last:
last = fb.current_waypoint
nav.info(f'웨이포인트 {last + 1}/{len(poses)} 로 이동 중')
if nav.get_clock().now() - start > TIME_LIMIT:
nav.warn('시간 초과, 중단')
nav.cancelTask()
break
result = nav.getResult()
if result == TaskResult.SUCCEEDED:
nav.info('모든 웨이포인트 완료')
elif result == TaskResult.CANCELED:
nav.warn('취소됨')
else:
nav.error('실패')
except KeyboardInterrupt:
pass
finally:
try:
nav.cancelTask() # Ctrl+C 시 진행 중인 목표를 취소해 로봇을 세운다
except Exception:
pass
nav.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
읽어 둘 점은 이렇습니다.
WAYPOINTS의 좌표는 RViz2에서 그 지점에 마우스를 올려 읽거나, 2D Pose Estimate의 값을 참고해 정합니다. 지도 파일의origin을 알아야 합니다.waitUntilNav2Active()를 기본값으로 부르면BasicNavigator는 AMCL이amcl_pose를 낼 때까지setInitialPose로 저장된 초기 자세를 계속 발행합니다. 그 값이 비어 있으면 지도 원점으로 AMCL을 덮어씁니다(robot_navigator.py소스에서 확인). 그래서 RViz2에서 이미 초기 자세를 준 경우를 가정하고localizer='bt_navigator'로 불렀습니다.nav_through_poses나NavigateToPose를 하나씩 보내는 방법도 있습니다.followWaypoints는nav2_params.yaml의waypoint_follower설정(stop_on_failure: false, 각 지점에서waypoint_pause_duration: 200ms 정지)으로 동작합니다.stop_on_failure: false이므로 한 지점이 실패해도 다음 지점으로 넘어갑니다. 실패한 지점을 기록으로 남기세요.TIME_LIMIT을 넘으면cancelTask()로 중단합니다. 로봇이 헤매면 직접 개입하기 전에 이 시간 제한이 먼저 작동합니다.
마일스톤
두세 번의 세션으로 나눕니다. 시간은 제안입니다.
| 세션 | 마일스톤 | 시간 | 완료 기준 |
|---|---|---|---|
| 1 | 안전 점검과 기본 구동 | 15분 | 4.1장 점검, 4.4장 확인 명령이 모두 통과 |
| 1 | 지도 만들기와 저장 | 45분 | 벽이 선명한 지도 파일 2개, RViz2에서 재확인 |
| 1 | 지점 좌표 정하기 | 20분 | 표에 이름, x, y, 방향이 적힘 |
| 2 | Nav2 시작과 초기 자세 | 20분 | tf2_echo map odom 출력, 파티클 수렴 |
| 2 | 지점 하나씩 수동 목표 | 30분 | RViz2 목표로 각 지점에 도착, 시간과 오차 기록 |
| 2 | 코드로 순회 | 30분 | waypoint_runner.py가 네 지점 완주 또는 실패 지점 기록 |
| 3 | 반복과 정리 | 40분 | 같은 순회 3회, 평균 오차, 문제 해결 기록 |
| 3 | 시연 준비 | 30분 | 아래 체크리스트 |
한 세션으로 끝낼 수도 있지만 지도를 만들고 나면 로봇 배터리와 집중력이 줄기 쉽습니다. 세션 사이에는 지도 파일을 VM에 백업하세요.
안전 조건
평가 루브릭
가중치 합은 100점입니다. 점수는 0~4점이고, 항목 점수에 가중치를 곱해 4로 나눠서 더합니다. 이 기준은 제안이므로 강의에서 조정하세요.
| 항목 | 가중치 | 4점 기준 | 0점 |
|---|---|---|---|
| 안전과 준비 | 10 | 점검, 저속 시험, 비상 정지 절차까지 시연 | 안전 수칙을 지키지 않음 |
| 지도 품질 | 20 | 벽이 선명하고 지점이 자유 공간, 루프를 닫고 필요하면 다시 만든 기록 | 지도 없음 |
| 위치 추정 | 10 | 초기 자세를 한 번에 맞추고 어긋났을 때 원인을 설명하고 복구 | 초기 자세를 주지 못함 |
| 웨이포인트 주행 | 25 | 세 지점 방문과 복귀, 도착 오차 측정 기록 | 도달 못함 |
| 코드와 구성 | 15 | 종료 처리, 시간 제한, 로그, 값이 상수로 모여 있고 다른 지도에도 값만 고치면 동작 | 실행 안 됨 |
| 문제 해결 기록 | 10 | 증상, 확인 명령, 원인, 해결, 관련 파라미터가 짝지어 있음 | 기록 없음 |
| 발표와 시연 | 10 | 계획한 순서로 시연, 질문에 답하고 실패와 개선 계획을 근거와 함께 설명 | 시연 못함 |
아래에서 자기 평가를 해 보세요. 항목마다 점수를 고르면 해당 기준과 총점이 나옵니다. 기록은 저장되지 않습니다.
항목마다 점수(0~4)를 고르면 가중치를 곱한 총점이 계산됩니다. 기록은 저장되지 않습니다.
- 안전과 준비가중치 10 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
- 지도 품질가중치 20 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
- 위치 추정가중치 10 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
- 웨이포인트 주행가중치 25 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
- 코드와 구성가중치 15 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
- 문제 해결 기록가중치 10 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
- 발표와 시연가중치 10 · 미선택
점수를 고르면 해당 기준이 표시됩니다.
총점 0.0 / 100
채점한 항목 0 / 7
등급 -
자주 하는 실수
- 지도를 만들자마자 SLAM을 끄지 않고 Nav2를 켠다.
map→odom을 두 노드가 발행해 TF가 흔들린다. map:=~/maps/...로~를 쓴다.$HOME이나 절대 경로를 쓴다.- 초기 자세를 대충 찍고 목표를 보낸다. 파티클이 모이기 전에 보낸 목표는 엉뚱한 곳으로 간다.
- 지점을 벽 바로 옆이나 좁은 틈에 둔다. 코스트맵의
inflation_radius와robot_radius때문에 목표가 거부되거나 닿지 못한다. - 지도를 만든 뒤 가구를 옮긴다. 지도와 현실이 어긋나 위치가 튄다.
- 한 번에 여러 파라미터를 바꾼다. 효과가 있었는지 알 수 없다.
- 시연 때 처음 해 보는 지점을 쓴다. 시연 전에 같은 순서를 최소 두 번 돌려 본다.
rplidar_node가 켜지기 전에(기동 후 약 3초) 노드를 켜고/scan이 없다고 당황한다. 10초 기다린다.
발표와 시연 체크리스트
- 로봇 배터리를 충전하고 주행 구역을 비웠다.
- 지도 파일과 지점 표, 스크립트를 한 폴더에 모아 뒀다.
- 두 컴퓨터의
ROS_DOMAIN_ID와 시각이 맞는지 확인했다. -
ros2 topic hz /scan,/odom,tf2_echo map odom을 시연 직전에 확인했다. - 초기 자세를 찍는 위치를 바닥 표시로 정해 뒀다.
- 시연 순서(기본 구동, Nav2, RViz2, 초기 자세, 순회)를 2분 안에 설명할 수 있다.
- 실패했을 때 어느 명령으로 무엇을 확인할지 정리해 뒀다.
- 측정한 도착 오차와 소요 시간을 표로 준비했다.
- 개선하고 싶은 점 하나를 파일의 파라미터 이름과 함께 말할 수 있다.
한눈에 정리
- 과제에서는 한 구역의 지도를 만들고 세 지점을 자율 주행으로 방문한 뒤 출발점으로 복귀한다.
- 산출물은 지도, 지점 표, 실행 스크립트, 주행 기록, 문제 해결 기록, 시연이다.
nav2_simple_commander의BasicNavigator.followWaypoints로 지점을 한 번에 보낸다. 초기 자세는 RViz2에서 이미 줬다고 가정하고localizer='bt_navigator'로 기다린다.- 두세 세션으로 나눈다. 안전 점검과 지도, Nav2와 수동 목표, 코드 순회와 반복 순으로 진행한다.
- 평가는 안전 10, 지도 20, 위치 추정 10, 주행 25, 코드 15, 문제 해결 기록 10, 시연 10이다.
- 시연 전에 같은 순서를 두 번 돌려 보고, 실패 대응 명령을 정리해 둔다.
확인 문제
Q1. followWaypoints로 네 지점을 보냈는데 두 번째 지점이 실패했는데도 로봇이 세 번째 지점으로 갔습니다. 어떤 설정 때문인가요?
nav2_params.yaml의 waypoint_follower에 stop_on_failure: false가 설정되어 있기 때문입니다. 실패한 지점을 건너뛰고 다음 지점으로 진행하므로 로그에서 실패한 지점을 따로 기록해야 합니다.
Q2. waitUntilNav2Active()를 기본값으로 호출했더니 RViz2에서 잡아 둔 위치가 지도 원점으로 튀었습니다. 왜 그랬을까요?
기본 localizer='amcl'에서는 amcl_pose가 올 때까지 저장된 초기 자세를 반복 발행하는데, 코드에서 초기 자세를 설정하지 않았으면 기본값(지도 원점)이 발행되어 AMCL을 덮어씁니다. 초기 자세를 RViz2에서 줬다면 localizer='bt_navigator'로 호출합니다.
Q3. 시연 직전에 모든 것이 되는데 목표를 보내자 곧바로 거부됩니다. 지점이 어떤 곳일 가능성이 높고 어떤 설정과 연결되나요?
글로벌 코스트맵에서 점유되었거나 inflation 범위 안에 있는 곳입니다. robot_radius: 0.22, inflation_radius: 0.2로 로봇 반경과 번짐이 지도에 반영되어, 벽에 가까운 지점은 자유 공간이 아닙니다. 지점을 벽에서 멀리 옮깁니다.
다음 단계
이 매뉴얼의 순서는 여기까지입니다. 이어서 배울 만한 주제를 적어 둡니다.
- Nav2 튜닝: DWB 비평가 가중치, 코스트맵 반경, 속도 한계를 하나씩 바꿔 가며 결과를 비교합니다. 이 매뉴얼에서는 값만 읽었던 항목입니다. 속도 한계는 DWB 0.26, 평활기 0.15, 컨트롤러 0.2로 불일치합니다.
nav2_params.yaml을 복사해 새 파일로 고치고params_file로 지정하는 방식을 그대로 씁니다. - 행동 트리:
bt_navigator가 쓰는 기본 행동 트리 XML을 읽고, 막혔을 때의 복구 순서(spin,backup,wait등)를 바꿔 봅니다. - 여러 로봇:
ROS_DOMAIN_ID를 나누거나 네임스페이스(bringup.launch.py의namespace,use_namespace인자)로 로봇 두 대를 같은 네트워크에서 분리해 운용합니다. - 오도메트리 보정: 4.6장의 실측 값으로
wheel_radius와wheel_separation을 조정하고 같은 실험을 반복해 오차가 줄어드는지 봅니다.