hello, robot

내비게이션 스택은 하나의 큰 프로그램이 아니라 서버 여러 개의 연합이에요. 시각화 도구에서 목표점을 찍으면 항법 액션이 트리 실행 서버로 들어가고, 그 서버가 XML로 적힌 행동 트리를 돌리면서 나머지 서버들을 액션으로 호출해요.

서버하는 일호출 주기
트리 실행트리를 틱하며 아래 서버들을 지휘항상
플래너전역 지도 위에서 경로 생성재계획 때만 (기본 1Hz)
컨트롤러지역 지도를 보며 속도 명령 생성매 주기 (기본 20Hz)
복구 동작막혔을 때 회전·후진·대기실패 가지에서만

여기서 이 스택 전체를 관통하는 사실이 하나 나와요. 각 서버는 껍데기이고 실제 알고리즘은 전부 런타임에 로드되는 플러그인입니다. 플래너 알고리즘, 컨트롤러, 지도 레이어, 복구 동작까지 모두요. 플러그인이 구현해야 하는 계약은 코어 패키지의 순수 가상 클래스로 정의돼 있어요.

그래서 이 스택을 고치는 일이 곧 C++ 플러그인을 읽고 고치는 일이 됩니다. 그 구조가 어떻게 가능한지는 C++ 인터페이스와 가상 함수 — 플러그인이 로드되는 원리에 있어요.

컨트롤러 셋은 발상이 서로 달라요

컨트롤러는 전역 경로와 현재 위치·속도를 받아 매 주기 속도 명령을 내는 부품이에요. 플래너가 어디로 갈지를 가끔 정한다면, 컨트롤러는 지금 바퀴를 어떻게 돌릴지를 20Hz로 계속 정해요. 로봇이 벽에 붙거나 코너에서 흔들리는 문제는 대부분 여기서 납니다.

실리는 컨트롤러는 크게 셋이에요.

Regulated Pure Pursuit은 기하로 풀어요. 경로 위에서 일정 거리 앞의 점 하나를 겨냥하고, 거기로 향하는 원호의 곡률을 계산해 따라가요. 규제가 붙은 이유는 원조에 감속 두 가지를 얹었기 때문이에요. 곡률이 크면 감속, 장애물에 가까우면 감속. 계산이 싸고 움직임이 예측 가능한 대신 경로 품질에 전적으로 의존해요. 스스로 경로를 벗어나 회피하지 않고 그건 재계획의 몫으로 남겨요.

DWB는 채점으로 풀어요. 현재 속도에서 가속 한계 안에 도달 가능한 속도 후보를 수십 개 뽑고, 각각을 짧은 시간 앞으로 투영한 궤적을 채점기 여러 개로 점수 매겨 합산 최고점을 골라요. 지역적으로 경로를 벗어나 회피할 수 있는 게 장점이고, 채점 가중치가 서로 얽혀 튜닝이 힘든 게 단점이에요.

MPPI는 확률 최적화로 풀어요. 직전 주기의 최적 제어열에 노이즈를 섞은 샘플을 수천 개 만들고, 각각을 모션 모델로 굴려 비용을 매긴 뒤, 비용이 낮을수록 큰 가중치로 평균해 갱신해요. 예측 지평을 갖고 미래를 보며 움직여 부드럽고 동적 장애물에 강한 대신 계산이 가장 비싸요.

Pure PursuitDWBMPPI
발상기하 (앞점 추종)후보 채점샘플링 최적화
계산 비용낮음중간높음
경로 이탈 회피안 함 (재계획 의존)지역적으로 함지평 안에서 함
어울리는 자리경로가 좋은 통로·순찰회피 유연성이 필요한 곳동적 환경

읽는 순서도 이 순서예요. Pure Pursuit은 손으로 계산을 따라갈 수 있어서 컨트롤러가 매 주기 무엇을 하는지의 원형을 배우기에 좋고, 나머지 둘은 그 위에 “후보를 여럿 만들어 고른다”는 층이 얹힌 것으로 읽으면 돼요.

앞점을 얼마나 멀리 잡느냐가 주행 증상을 어떻게 가르는지는 경로 추종의 전방 주시 거리와 이탈 오차의 최적점에서 실험으로 다뤘어요. 곡률이 앞점 거리의 제곱에 반비례한다는 게 그 실험의 바닥입니다.

컨트롤러는 센서를 직접 안 봐요

장애물을 아는 방법은 센서 구독이 아니라 비용 격자 조회예요. 컨트롤러 서버가 로봇 주변 몇 미터를 따라다니는 지역 격자를 소유하고, 플러그인 설정 시점에 넘겨줘요.

격자 한 칸의 값은 0(자유)부터 252까지의 비용, 253(원형 로봇 중심이 여기 오면 확정 충돌), 254(장애물 자체), 255(미지)예요.

조회는 두 단계입니다. 월드 좌표를 셀 좌표로 바꾸고, 그 셀의 비용을 읽어요.

unsigned int mx, my;
if (!costmap->worldToMap(wx, wy, mx, my)) {
    // 지도 밖 — 조회 실패도 하나의 답이다
}
unsigned char cost = costmap->getCost(mx, my);   // 0~255

변환의 속은 산수예요. 셀 좌표는 원점에서의 거리를 해상도로 나눠 내린 정수이고, 격자 범위를 벗어나면 실패를 돌려줘요. 1차원 배열로 저장하니 인덱스는 행 × 폭 + 열이고요.

세 컨트롤러가 이 격자를 읽는 자리만 달라요. Pure Pursuit은 지금 낼 원호를 앞으로 투영하며 로봇 발자국이 밟을 셀을 검사하고, 치명 셀이 임박하면 정지하고 예외를 던져 트리의 복구 가지로 넘겨요. DWB는 후보 궤적마다 장애물 채점기가 밟는 셀을 채점하고, MPPI는 수천 샘플의 비용 항에 같은 격자가 들어가요. 비용 격자를 읽어 치명 셀을 피한다는 한 문장이 셋 모두의 공통 바닥이에요.

증상에서 손잡이를 역추적해요

튜닝은 파라미터 목록을 외우는 일이 아니라 증상을 보고 어느 층의 어느 손잡이인지 되짚는 일이에요.

증상먼저 볼 곳
직선에서 좌우로 S자 흔들림앞점 거리 올리기컨트롤러
코너 안쪽을 깎고 모서리에 붙음앞점 거리 내리기컨트롤러
코너에서 과속으로 바깥으로 밀림곡률 감속 켜기컨트롤러 규제
벽·가구에 상시 붙어 주행인플레이션 반경·비용 감쇠지도
목표점 앞에서 맴돌며 못 끝냄도달 판정 허용 오차컨트롤러 서버
목표 직전 급정지접근 감속 시작 거리컨트롤러

원칙은 한 번에 하나만 바꾸고 같은 코스를 다시 도는 것이에요.

층을 가르는 요령이 하나 있어요. 증상이 컨트롤러 쪽인지 지도 쪽인지 헷갈리면 경로 자체를 봐요. 시각화에서 전역 경로가 이미 벽에 붙어 있으면 지도나 플래너 문제이고, 경로는 좋은데 로봇이 그걸 못 따라가면 컨트롤러 문제예요. 층을 먼저 가르면 손잡이 후보가 절반 이하로 줄어요.

소스는 계약부터 읽어요

구현체부터 열면 길을 잃어요. 인터페이스를 먼저 읽고 구현체를 그 목차로 읽는 게 순서입니다.

// 라이프사이클 — 서버가 상태 전환 때 부른다
virtual void configure(…, std::shared_ptr<nav2_costmap_2d::Costmap2DROS>) = 0;
virtual void activate() = 0;
virtual void deactivate() = 0;
 
// 데이터 유입 — 새 전역 경로가 올 때마다 (대략 1Hz)
virtual void setPlan(const nav_msgs::msg::Path& path) = 0;
 
// 매 주기(20Hz) 불리는 곳 — 컨트롤러의 심장
virtual geometry_msgs::msg::TwistStamped computeVelocityCommands(
    const geometry_msgs::msg::PoseStamped& pose,
    const geometry_msgs::msg::Twist& velocity,
    nav2_core::GoalChecker* goal_checker) = 0;

매 주기 불리는 함수는 하나뿐이라는 걸 잡으면 지도가 단순해져요. 나머지는 시작·종료거나 경로 갱신이에요.

그다음 구현체를 열면 그 함수 안의 호출 순서가 앞 절의 개념과 1:1로 겹쳐요. 전역 경로를 로봇 좌표계로 옮기고 지나온 구간을 잘라내고 → 속도에 비례한 앞점 거리를 계산하고 → 앞점을 고르고 → 곡률을 구하고 → 곡률·비용·접근 감속을 적용하고 → 원호를 투영해 충돌을 검사하고 → 속도 명령을 반환해요.

컴파일러가 안 잡아 주는 곳

플러그인은 파라미터 파일에 문자열로 지정돼요.

controller_server:
  ros__parameters:
    controller_plugins: ["FollowPath"]
    FollowPath:
      plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"

이 문자열은 컴파일러가 검사하지 않아요. 런타임에 패키지의 등록 XML과 대조해 공유 라이브러리를 열기 때문에, 철자 하나 틀려도 빌드는 멀쩡하고 서버가 설정되는 순간 클래스를 못 찾겠다며 죽어요.

소스 쪽 끝은 구현 파일 하단의 등록 매크로예요. 파라미터 파일의 문자열, 등록 XML, 매크로 세 곳이 정확히 일치해야 합니다. 플러그인이 안 뜨면 코드보다 먼저 이 세 곳을 대조하는 게 순서예요.

트리가 이 서버들을 어떻게 지휘하는지는 Behavior Tree — 막혔을 때를 구조로 적기에, 플래너가 경로를 만드는 원리는 경로 계획 — 다익스트라와 A*를 손으로에 있어요.