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

4. 실기기 연동

4.8최종 프로젝트와 평가

2부의 에뮬레이터와 4부의 로봇에서 하나씩 쌓은 것을 이 장에서 한 과제로 묶습니다. 새로 배우는 개념은 없습니다. 앞 장의 순서를 혼자 계획하고 실행하고 기록하는 것이 과제입니다.

과제

방의 한 구역(약 3 m x 3 m)을 지도로 만든다. 그 지도 위에서 TIKI-MINI가 출발점에서 시작해 세 지점을 순서대로 자율 주행으로 방문하고 출발점으로 돌아온다.

2.14장의 미니 프로젝트를 했다면 흐름이 익숙할 것입니다. 실기기에서는 지도, 위치 추정, 오차를 직접 다뤄야 합니다.

요구사항

  1. 지도는 4.7장 순서대로 만들고 ~/maps/<팀이름>.yaml과 이미지 파일로 저장한다.
  2. 지도에는 구역의 벽과 고정 장애물이 보이고, 방문 지점 세 곳과 출발점이 자유 공간에 있다.
  3. tiki_navigation의 bringup.launch.py로 Nav2를 켜고 RViz2의 2D Pose Estimate로 초기 자세를 준다.
  4. 세 지점과 출발점으로 돌아오는 주행을 목표 전달로 수행한다. 방법은 둘 중 하나를 고른다. 동작 확인용으로는 RViz2의 Nav2 Goal 버튼으로 하나씩 보내고, 점수 시연에는 아래 코드로 한 번에 보낸다.
  5. 주행 동안 /odom이나 액션 피드백으로 진행 상황을 기록하는 로그를 남긴다.
  6. 시연은 한 번 실패해도 원인을 설명하고 다시 시도할 수 있다.

산출물

산출물형식비고
지도.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: 200 ms 정지)으로 동작합니다. stop_on_failure: false이므로 한 지점이 실패해도 다음 지점으로 넘어갑니다. 실패한 지점을 기록으로 남기세요.
  • TIME_LIMIT을 넘으면 cancelTask()로 중단합니다. 로봇이 헤매면 직접 개입하기 전에 이 시간 제한이 먼저 작동합니다.
웨이포인트 순회 예시주행 순서도착 방향123출발·복귀 (0, 0, 0°)1번 (1.0, 0.0, 0°)2번 (1.0, 1.0, 90°)3번 (0.0, 1.0, 180°)xymap순회 중 동작네 지점을 한 번에 보냄followWaypoints각 지점에서 200 ms 정지waypoint_pause_duration실패해도 다음 지점으로stop_on_failure: false240초가 지나면 중단TIME_LIMIT, cancelTask()
코드의 WAYPOINTS 예시 값을 지도 좌표에 놓은 순회 경로입니다. 오른쪽은 순회 중에 적용되는 설정과 코드의 시간 제한이며, 실제 좌표는 RViz2에서 읽은 값으로 바꿉니다.

마일스톤

두세 번의 세션으로 나눕니다. 시간은 제안입니다.

세션마일스톤시간완료 기준
1안전 점검과 기본 구동15분4.1장 점검, 4.4장 확인 명령이 모두 통과
1지도 만들기와 저장45분벽이 선명한 지도 파일 2개, RViz2에서 재확인
1지점 좌표 정하기20분표에 이름, x, y, 방향이 적힘
2Nav2 시작과 초기 자세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분 안에 설명할 수 있다.
  • 실패했을 때 어느 명령으로 무엇을 확인할지 정리해 뒀다.
  • 측정한 도착 오차와 소요 시간을 표로 준비했다.
  • 개선하고 싶은 점 하나를 파일의 파라미터 이름과 함께 말할 수 있다.

한눈에 정리

  1. 과제에서는 한 구역의 지도를 만들고 세 지점을 자율 주행으로 방문한 뒤 출발점으로 복귀한다.
  2. 산출물은 지도, 지점 표, 실행 스크립트, 주행 기록, 문제 해결 기록, 시연이다.
  3. nav2_simple_commander의 BasicNavigator.followWaypoints로 지점을 한 번에 보낸다. 초기 자세는 RViz2에서 이미 줬다고 가정하고 localizer='bt_navigator'로 기다린다.
  4. 두세 세션으로 나눈다. 안전 점검과 지도, Nav2와 수동 목표, 코드 순회와 반복 순으로 진행한다.
  5. 평가는 안전 10, 지도 20, 위치 추정 10, 주행 25, 코드 15, 문제 해결 기록 10, 시연 10이다.
  6. 시연 전에 같은 순서를 두 번 돌려 보고, 실패 대응 명령을 정리해 둔다.

확인 문제

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을 조정하고 같은 실험을 반복해 오차가 줄어드는지 봅니다.