MoveIt2는 물체를 집는 기능이 아니에요. “여기로 손끝을 보내라”는 목표 좌표를 관절 궤적으로 바꾸는 실행기예요. 어디로 보낼지는 위층이 정해요. 사용자의 클릭이 좌표를 주든 검출 모델이 주든, 좌표 공급원만 교체되고 이 층은 그대로예요.
물체 인식도 목표 결정도 이 프레임워크의 일이 아니라는 경계를 먼저 그어 두면, 커 보이는 스택도 읽을 범위가 확 줄어요.
요청 하나가 지나가는 사슬
중심은 노드 하나예요. 시각화 도구의 플러그인도 C++ 인터페이스도 파이썬 바인딩도 전부 이 노드에 요청을 보내는 클라이언트일 뿐이에요.
목표 pose (위치 + 방향)
→ IK 솔버 그 pose를 만족하는 관절 구성을 찾는다
→ 플래너 현재 구성 → 목표 구성 사이의 충돌 없는 경로를 샘플링으로
→ 시간 파라미터화 속도·가속 한계 안에서 웨이포인트마다 시각을 배정
→ 액션으로 컨트롤러에 전달, 실행 감시
첫 고리부터 봐요. 목표가 좌표로 오면 관절 공간의 어디로 가야 하는지부터 정해야 해요. 솔버가 그 조합을 찾아 주고, 못 찾으면 사슬이 여기서 끊겨요. 그래서 플래닝 실패의 절반은 플래너가 아니라 이 단계 문제예요.
입구에서 갈리는 길이 하나 있어요. 목표를 좌표가 아니라 관절각으로 직접 주면 이 단계가 통째로 생략돼요. “홈 자세로 돌아가라”가 전형이죠. 목표 구성을 이미 아니까 풀 것이 없어요. 진단할 때 유용한 이유이기도 해요. 관절각으로는 가는데 좌표로는 못 가면 부러진 고리가 IK라는 뜻이거든요.
두 번째 고리인 플래너는 샘플링 기반이에요. 관절 구성을 무작위로 뽑아 충돌 없이 이어지는지 확인하는 방식이라 같은 목표라도 실행할 때마다 경로가 조금씩 달라요. 출력은 시간 정보가 없는 웨이포인트의 나열이고 사이는 보간으로 이어져요.
충돌 없음을 판정하는 근거는 로봇 자신의 링크와 환경 물체를 담은 세계예요. 여기서 설정 파일의 충돌 매트릭스가 일해요. 인접한 링크끼리는 원래 늘 닿아 있으니 검사할 필요가 없고 기하적으로 절대 만날 수 없는 쌍도 마찬가지라서, 미리 “검사 생략” 표시를 해 둬요. 이 목록이 없으면 매 샘플마다 헛검사가 쌓여 플래닝이 느려져요.
세 번째가 시간 파라미터화예요. 기하학적 경로에 “언제 어느 웨이포인트를 지나는가”를 붙여 실행 가능한 궤적으로 만들어요. 로봇 모델에 적어 둔 관절 속도 한계가 여기서 천장으로 쓰이고, 속도 스케일 파라미터가 그 천장에 곱해지는 비율이에요.
마지막은 실행이에요. 완성된 궤적을 액션으로 컨트롤러에 보내고 결과를 피드백으로 지켜봐요.
설정 파일이 각 고리를 먹여요
| 파일 | 먹이는 고리 | 내용 |
|---|---|---|
| 로봇 모델 | 전부의 바닥 | 링크·관절·관절 한계 |
| 시맨틱 설명 | IK·플래너 | 플래닝 그룹, 엔드이펙터, 충돌 매트릭스 |
| 기구학 설정 | IK | 솔버 선택 — 교체가 일어나는 자리 |
| 플래너 설정 | 플래너 | 기본값 유지 |
| 컨트롤러 설정 | 실행 | 궤적 컨트롤러 연결 |
플래닝 파이프라인이 하나가 아니라 여러 플래너가 나란히 로드될 수 있는 구조라는 점도 알아 둘 만해요. 참고한 예제가 쓰지도 않는 플래너까지 로드하려다 없는 설정 파일을 찾으며 기동이 죽는 일이 있는데, 파이프라인을 실제 쓰는 하나로 제한하면 해결돼요. 문서보다 에러가 먼저 가르쳐 주는 구조예요.
함정 넷
기본 솔버는 수식으로 닫힌 답이 아니라 수치해라서, 현재 자세에서 출발해 목표에 조금씩 다가가는 반복 계산을 해요. 어떤 구조의 팔에도 일반적으로 적용되는 대신 반복이 관절 한계에 부딪히거나 특이점 근처를 지나면 수렴에 실패해요.
| 증상 | 원인 | 조치 |
|---|---|---|
| 플래닝 실패, 이유 불명 | 기본 솔버가 한계·특이점에서 수렴 실패 | 더 나은 수치 솔버로 교체 |
| 그리퍼 낀 그룹에서 IK 전멸 | 교체한 솔버가 종속 관절 미지원 | 그리퍼를 플래닝 그룹에서 분리 |
| 어떤 솔버로도 안 풀림 | 목표가 작업 공간 밖 | 도달성부터 확인 — 솔버 탓 아님 |
| 플래닝이 몇 초씩 | 충돌 형상이 정밀 메시 | 볼록 껍질로 단순화 |
두 번째가 흥미로워요. 종속 관절은 한 관절이 다른 관절을 따라 움직이도록 걸어 둔 구속인데, 그리퍼 두 손가락이 값 하나로 같이 벌어지는 구조가 전형이에요. 그래서 팔과 그리퍼를 한 그룹에 넣으면 IK가 전멸하고, 조치는 그룹을 분리하는 거예요. 분리가 자연스러운 이유도 있어요. 그리퍼는 IK로 풀 대상이 아니라 열림·닫힘 명령의 대상이거든요.
네 번째는 시각용 형상과 충돌용 형상을 따로 두는 이유를 설명해요. CAD에서 나온 정밀 메시로 충돌 검사를 돌리면 삼각형 수천 개를 매 샘플마다 확인하게 돼요. 보기엔 정밀하게, 검사는 단순하게가 원칙이에요.
자유도가 6 미만인 팔에는 생존 스위치가 하나 필요해요. 손끝의 위치와 방향을 다 만족하려면 관절이 최소 여섯 개 필요한데, 네 개짜리 팔은 수학적으로 불가능한 요구를 받는 셈이라 위치만 풀게 낮춰 주는 설정이 있어요.
팔을 펴는 것과 IK가 죽는 것은 같은 수식의 양면
특이점은 야코비안 행렬식이 0이 되는 자세예요. 야코비안은 각 관절을 조금 돌리면 손끝이 어느 방향으로 얼마나 움직이는가를 모아 둔 표예요.
팔이 완전히 펴지는 순간을 그려 볼게요. 어깨와 팔꿈치가 일직선이 되면 어느 관절을 돌려도 손끝은 팔과 수직인 방향으로만 원을 그려요. 팔이 뻗은 방향으로 손끝을 더 밀어낼 수 있는 관절이 하나도 없어요. 그 방향의 성분이 전부 0이 되어 행렬식이 0이고, 행렬식이 0이면 역행렬이 없어요.
그런데 수치해 IK는 야코비안의 역행렬을 반복해서 쓰는 계산이라, 행렬식이 0에 가까워지면 역행렬 원소가 폭발해요. 관절 속도가 비정상적으로 튀거나 수렴에 실패하는 게 그 폭발의 겉모습이에요.
2링크 평면 팔이면 이 이야기가 수식 두 줄로 보여요.
cos(q2) = (x² + y² − L1² − L2²) / (2·L1·L2)
우변 > 1 → 해가 없다: 목표가 팔 길이 밖 (작업 공간 밖)
우변 = 1 정확히 → q2 = 0: 팔을 완전히 편 단 하나의 해 (특이점 위)
|우변| < 1 → 해가 둘: 팔꿈치를 위로 굽히나 아래로 굽히나
도달성과 특이점과 해의 개수가 한 수식의 세 구간인 거예요. 그리고 이 팔의 야코비안 행렬식은 L1·L2·sin(q2)라서 팔꿈치각이 0이거나 ±π일 때 정확히 0이 돼요.
작업 공간의 가장자리는 정의상 팔을 다 편 자세로만 닿는 곳이라, “경계 근처 목표에서 IK가 간헐적으로 실패한다”는 특이점과 도달성이 겹쳐서 나타나는 무늬예요.
해가 둘일 때 어느 쪽을 고르는가도 실무 질문이에요. 수치 솔버는 반복 계산의 출발점에서 가까운 해로 수렴해요. 그래서 같은 목표라도 팔이 어떤 자세에서 출발했느냐에 따라 다른 구성이 나올 수 있고, “어제는 됐는데 오늘은 안 되는” IK의 정체가 출발점 차이인 경우가 있어요.
진단에는 순서가 있어요
이게 이 주제에서 가장 값어치 있는 부분이에요.
기동이 됐나(설정 층)
→ 목표가 도달 가능한가
→ 관절 한계 안인가
→ 특이점 근처인가
→ 그제야 솔버를 의심한다
앞 단계를 건너뛰고 솔버부터 바꾸면, 작업 공간 밖 목표에 솔버를 갈아 끼우며 시간을 태우게 돼요.
첫 칸을 확인하는 건 명령 셋이면 돼요. 플래닝 액션이 떠 있나(노드 기동), 궤적 액션이 떠 있나(컨트롤러 연결), 관절 상태 토픽에 값이 흐르나(로봇 상태 수신). 셋이 확인된 뒤에야 “요청이 왜 안 풀리나”라는 다음 질문으로 넘어갈 자격이 생겨요.
기록도 같은 틀이에요. 성공 스크린샷은 누구나 있고, “이 목표점에서 기본 솔버가 실패했고 교체하니 풀렸다”가 기록의 본체예요. 남길 최소 형식은 다섯이에요. 목표 좌표 / 솔버 / 결과 / 원인 판정(특이점·한계·도달성 중 무엇) / 조치.
“이거였음”만 적으면 결과 기록이지 진단 기록이 아니에요. 무엇을 의심했고 무엇으로 좁혔는지가 카드의 가치예요.
코드는 짧아요
moveit::planning_interface::MoveGroupInterface arm(node, "arm"); // 그룹 이름
arm.setMaxVelocityScalingFactor(0.5); // 시간 파라미터화에 곱해지는 비율
geometry_msgs::msg::Pose target; // 위층이 준 목표 좌표
target.position.x = 0.20;
target.position.z = 0.15;
arm.setPoseTarget(target); // 사슬의 입구 — 여기부터 IK가 돈다
moveit::planning_interface::MoveGroupInterface::Plan plan;
if (arm.plan(plan) == moveit::core::MoveItErrorCode::SUCCESS) {
arm.execute(plan); // 액션으로 나간다
}계획과 실행이 나뉜 게 설계 의도예요. 결과를 검사하고 마음에 안 들면 버릴 수 있고, 둘을 합친 한 방보다 실패 지점이 분명해져요. 계획에서 죽으면 IK·플래너 문제, 실행에서 죽으면 컨트롤러·하드웨어 문제로 절반이 갈리거든요.
반환값을 버리지 않는 습관이 기록의 재료가 돼요. 성공이 아닐 때의 에러 코드가 IK 실패인지 시간 초과인지 시작 상태가 이미 충돌 중이었는지를 갈라 주니까요.
사슬의 마지막 고리를 직접 다루는 골격은 C++ rclcpp — 콜백 수명과 스레딩이 만드는 사고의 액션 클라이언트 절에 있어요. 이 프레임워크를 쓰면 그 코드를 인터페이스가 대신 짜 주는 셈이에요. 좌표를 팔 기준으로 옮기는 앞 단계는 TF2 좌표변환 — lookup 한 줄 밑의 수학고요.