파싱(Parsing)이란?
**파싱(Parsing)**이란 텍스트 데이터를 읽어서 구조화된 형태로 분해하는 과정입니다. MoveIt2에서는 로봇의 설정 파일인 SRDF(XML 형식)를 파싱하여 관절 정보, 그룹 정의 등을 추출합니다.
flowchart LR
A["XML 텍스트<br/>(긴 문자열)"] -->|ET.fromstring| B["트리 구조"]
B --> C["robot"]
C --> D["group_state"]
D --> E["joint1: 0.0"]
D --> F["joint2: -1.57"]
D --> G["joint3: 1.57"]파싱 전에는 그냥 텍스트 덩어리지만, 파싱 후에는 프로그램이 원하는 값을 바로 꺼낼 수 있습니다.
<!-- SRDF 파일 원본 (그냥 텍스트) -->
<robot name="my_robot">
<group_state name="home" group="manipulator">
<joint name="joint1" value="0.0"/>
<joint name="joint2" value="-1.57"/>
<joint name="joint3" value="1.57"/>
</group_state>
</robot>import xml.etree.ElementTree as ET
# 파싱: 텍스트 → 트리 구조로 변환
root = ET.fromstring(srdf_text)
# 파싱 후: 원하는 값을 바로 접근 가능
group_state = root.find('.//group_state[@name="home"]')
for joint in group_state.findall('joint'):
name = joint.get('name') # "joint2"
value = joint.get('value') # "-1.57"
print(f"{name}: {value}")핵심: 파싱 = 텍스트를 프로그램이 다루기 쉬운 구조로 쪼개는 것
SRDF란 무엇인가?
**SRDF (Semantic Robot Description Format)**는 URDF에 의미(semantic) 정보를 추가하는 파일입니다. URDF가 로봇의 물리적 설계도라면, SRDF는 그 로봇을 어떻게 사용할지에 대한 설정 파일입니다.
| 항목 | URDF | SRDF |
|---|---|---|
| 내용 | 물리적 구조 (링크, 관절, 질량) | 의미/사용 정보 (그룹, 자세, 충돌) |
| 비유 | 자동차 설계도 | 운전 매뉴얼 |
| 예시 | joint1 범위: ~ | joint1~6을 "manipulator"로 묶음 |
| 없으면? | 로봇 자체를 못 만듦 | MoveIt이 못 돌아감 |
flowchart TB
subgraph URDF ["URDF (물리적 구조)"]
U1["링크 크기/질량"]
U2["관절 범위/타입"]
U3["메시 모델"]
end
subgraph SRDF ["SRDF (사용 설정)"]
S1["그룹 정의"]
S2["이름 포즈"]
S3["충돌 무시"]
S4["엔드이펙터"]
end
URDF -->|"+의미 정보"| SRDF
SRDF --> MoveIt["MoveIt2 동작"]1. 그룹 정의 - 어떤 관절들을 묶어서 제어할지
<group name="manipulator">
<chain base_link="base_link" tip_link="gripper_base"/>
</group>
<group name="gripper">
<joint name="left_finger_joint"/>
</group>2. 이름 포즈 - 자주 쓰는 자세를 이름으로 저장
<group_state name="home" group="manipulator">
<joint name="joint1" value="0.0"/>
<!-- ... -->
</group_state>3. 충돌 무시 설정 - 인접한 링크 간 불필요한 충돌 검사 비활성화
<disable_collisions link1="base_link" link2="link1" reason="Adjacent"/>4. 엔드 이펙터 정의
<end_effector name="gripper_ee" parent_link="gripper_base"
group="gripper" parent_group="manipulator"/>팁: SRDF는 MoveIt 전용 파일입니다. 로봇 제조사는 URDF만 제공하고, MoveIt을 사용하려는 개발자가
moveit_setup_assistant를 통해 SRDF를 직접 생성합니다.
utils.py로 코드 통합하기 (ex02 vs ex03)
# SRDF XML 직접 파싱
tree = ET.fromstring(srdf_text)
for joint in group_state.findall('joint'):
name = joint.get('name')
value = joint.get('value')
# MoveIt 메시지 직접 구성
request = MotionPlanRequest()
constraint = JointConstraint()
constraint.joint_name = 'joint1'
constraint.position = 0.0
# ... 하나하나 다 만듦
# Action 직접 호출
goal = MoveGroup.Goal()
goal.request = request
self._action_client.send_goal_async(goal)helper = MoveGroupHelper(node)
helper.go_to_named_target('home') # 한 줄로 끝
helper.go_to_joint_goal([0, 0.5, 0, ...]) # 한 줄로 끝같은 일을 하는데 ex02는 ~230줄, ex03는 ~115줄.
flowchart LR
subgraph before ["ex02: 매번 직접 작성"]
A1["SRDF 파싱"] --> B1["메시지 구성"] --> C1["Action 호출"]
end
subgraph after ["ex03~: utils.py 활용"]
U["utils.py<br/>MoveGroupHelper"] --> D1["go_to_named_target()"]
U --> D2["go_to_joint_goal()"]
U --> D3["go_to_pose_goal()"]
U --> D4["execute_trajectory()"]
end| 항목 | ex02 (직접) | ex03+ (utils) |
|---|---|---|
| 코드량 | ~230줄 | ~115줄 |
| SRDF 파싱 | 매번 직접 | go_to_named_target() |
| 메시지 구성 | 매번 직접 | go_to_joint_goal() |
| Action 호출 | 매번 직접 | execute_trajectory() |
| 목적 | 내부 동작 교육 | 실제 작업에 집중 |
요약: ex02는 "MoveIt 통신이 내부적으로 이렇게 동작한다"를 보여주는 교육용이고, ex03부터는 반복 코드를 utils로 빼서 실제로 하고 싶은 일에만 집중하는 구조입니다.
쿼터니언(Quaternion)과 3D 회전
3D 공간에서 회전을 표현하는 방법은 크게 두 가지가 있습니다.
| 항목 | 오일러 각 (Euler) | 쿼터니언 (Quaternion) |
|---|---|---|
| 표현 | roll, pitch, yaw (3개 값) | x, y, z, w (4개 값) |
| 가독성 | 사람이 읽기 쉬움 | 직관적이지 않음 |
| 짐벌락 | 발생함 | 없음 |
| 계산 효율 | 보통 | 높음 |
| 보간(Slerp) | 부자연스러움 | 부드럽고 자연스러움 |
쿼터니언 에서:
- = 회전축의 방향 (단위 벡터)
- = 회전 각도
# 자주 쓰는 쿼터니언 값
no_rotation = (0, 0, 0, 1) # 회전 없음
x_180 = (1, 0, 0, 0) # X축 180도 회전
z_90 = (0, 0, 0.707, 0.707) # Z축 90도 회전짐벌락은 실제 물체에서 생기는 문제가 아니라, 오일러 각이라는 표현 방식에서 생기는 수학적 문제입니다.
stateDiagram-v2
state "오일러 각 회전 순서" as euler {
[*] --> Roll: 1단계: X축 회전
Roll --> Pitch: 2단계: Y축 회전
Pitch --> Yaw: 3단계: Z축 회전
}
state "Pitch = 90도일 때" as lock {
[*] --> Problem: Roll과 Yaw가 같은 축!
Problem --> Lost: 자유도 3 → 2로 감소
}
euler --> lock: 짐벌락 발생 조건오일러 각은 축 3개를 순서대로 회전합니다. Pitch가 90도가 되면 Roll축과 Yaw축이 겹쳐서 자유도가 하나 사라집니다.
반면 쿼터니언은 "이 방향의 축을 중심으로 이 각도만큼 돌려"라고 한 번에 표현하기 때문에 축이 겹칠 일이 없습니다.
쿼터니언이 유효한 회전을 나타내려면 반드시 단위 쿼터니언이어야 합니다:
# 유효한 쿼터니언 (길이 = 1)
(0, 0, 0.707, 0.707) # Z축 90도 회전 ✅
# 유효하지 않은 쿼터니언 (길이 ≠ 1)
(0, 0, 0, 2) # 의미 없는 숫자 ❌팁:
euler_to_quaternion()함수를 사용하면 항상 정규화된 값이 나오므로 직접 정규화할 필요가 없습니다.
관절 공간 vs 카테시안 공간 (ex02/03 vs ex04)
MoveIt2 예제에서 가장 중요한 개념 전환이 ex04에서 일어납니다.
| 항목 | ex02/03 (Joint Space) | ex04 (Cartesian Space) |
|---|---|---|
| 입력 | 관절 각도 직접 지정 | 목표 위치/자세 지정 |
| IK 필요 | 불필요 (답을 직접 줌) | 필요 (역기구학 계산) |
| 실패 가능성 | 거의 없음 | 있음 (도달 불가, 특이점 등) |
| 사용 예 | 홈 포지션 이동 | "이 좌표로 그리퍼 이동" |
flowchart TB
subgraph joint ["Joint Space (ex02/03)"]
JI["joint1=0, joint2=45, ..."] --> JM["바로 모터 구동"]
end
subgraph cart ["Cartesian Space (ex04)"]
CI["x=0.3, y=0.0, z=0.3 + 자세"] --> IK["IK 계산<br/>(역기구학)"]
IK -->|성공| CM["모터 구동"]
IK -->|실패| CF["NO_IK_SOLUTION"]
end| 에러 | 원인 | 해결 |
|---|---|---|
| NO_IK_SOLUTION | 목표가 팔이 못 닿는 곳이거나 특이점 | 위치를 조금 조정, 자세 변경 |
| Planning failed | IK는 풀렸는데 경로에 충돌이 있음 | planning_time 늘리기, 다른 자세 시도 |
| Quaternion 정규화 오류 | make_pose() 사용하면 자동 처리 |
비동기(Async)가 ROS2에서 중요한 이유
ROS2에서 MoveIt Action을 호출할 때 비동기 패턴은 필수입니다.
sequenceDiagram
participant Node as ex04 노드
participant MG as move_group
participant CB as 콜백 (joint_states, tf)
Note over Node,CB: 비동기 방식 (async)
Node->>MG: send_goal_async(goal)
activate MG
Note over Node: spin_until_future_complete()
loop 대기 중에도 이벤트 처리
CB-->>Node: /joint_states 콜백 ✅
CB-->>Node: /tf 업데이트 ✅
end
MG-->>Node: 결과 반환
deactivate MG# 비동기 호출: 보내고 바로 리턴
future = action_client.send_goal_async(goal)
# 기다리면서도 콜백은 계속 처리됨
rclpy.spin_until_future_complete(node, future)| 방식 | 경로 계획 대기 중 | 콜백 처리 | 결과 |
|---|---|---|---|
| 동기 | 블로킹 | /joint_states, /tf 멈춤 | 상태 정보 유실 |
| 비동기 | 논블로킹 | 정상 처리 | 안정적 동작 |
핵심: 비동기를 사용하지 않으면 경로 계획 5초 대기 동안
/joint_states콜백과/tf업데이트가 전부 멈춥니다. 비동기를 사용하면 대기하는 동안에도 ROS 이벤트 루프가 계속 동작합니다.
Cartesian Path로 직선 경로 만들기 (ex05)
| 항목 | ex04 | ex05 |
|---|---|---|
| 제어 대상 | 목표 지점 | 목표 경로 |
| 경로 형태 | MoveIt이 알아서 결정 | 개발자가 직선으로 지정 |
| 활용 | 위치 이동 | 용접, 도색, 글씨 쓰기 |
ex05는 YZ 평면에 8cm 사각형을 그립니다. **직선 보간(linear interpolation)**이 제대로 동작하는지 시각적으로 확인하기 위한 예제입니다.
flowchart LR
subgraph square ["YZ 평면 8cm 사각형"]
P1["① 시작점"] -->|"Y+8cm"| P2["②"]
P2 -->|"Z+8cm"| P3["③"]
P3 -->|"Y-8cm"| P4["④"]
P4 -->|"Z-8cm"| P1
endtrajectory, fraction = helper.compute_cartesian_path(waypoints, max_step=0.02)max_step = 0.02 (2cm): 웨이포인트 사이를 2cm 간격으로 쪼개서 각 점마다 IK를 계산합니다.
① ──────────────── ② (8cm 구간)
max_step=0.02 적용 후:
① · · · · · · · · ② (4개 중간점 생성, 각 점마다 IK 계산)
fraction = 경로 달성률 (0.0 ~ 1.0)
| fraction 값 | 의미 | 조치 |
|---|---|---|
1.0 | 100% 경로 계획 성공 | 바로 실행 |
0.75 | 75% 지점에서 IK 실패 또는 충돌 | max_step 조정 또는 경로 수정 |
0.0 | 시작부터 실패 | 시작 자세/위치 확인 |
sequenceDiagram
participant EX as ex05 노드
participant MG as move_group 노드
participant RC as ros2_control
participant RSP as robot_state_publisher
EX->>MG: /compute_cartesian_path (서비스 요청)
Note over MG: 웨이포인트 쪼개기 → IK → 궤적 생성
MG-->>EX: trajectory + fraction (서비스 응답)
EX->>MG: /execute_trajectory (액션 요청)
MG->>RC: /arm_controller/joint_trajectory (토픽)
RC->>RC: 모터 구동
RC-->>RSP: /joint_states
RSP-->>MG: /tf한 줄 요약: ex05는 "어디로 가냐"가 아니라 **"어떤 경로로 가냐"**를 지정하는 예제입니다. 직선 이동이 필요한 작업(용접, 도색, 글씨 쓰기 등)에 필수적인 기능입니다.
정리
| 개념 | 핵심 요약 |
|---|---|
| 파싱 | 텍스트를 프로그램이 다룰 수 있는 구조로 변환 |
| SRDF | URDF에 의미 정보를 추가하는 MoveIt 전용 설정 파일 |
| utils.py | 반복되는 MoveIt 통신 코드를 추상화하여 재사용 |
| 쿼터니언 | 짐벌락 없이 3D 회전을 표현하는 4차원 수 |
| 비동기 | ROS 이벤트 루프를 막지 않고 Action을 호출하는 패턴 |
| Cartesian Path | 직선 보간으로 경로의 형태를 직접 지정하는 방식 |
- ex02: MoveIt 내부 동작을 직접 구현하며 학습
- ex03: utils.py로 반복 코드를 제거하고 핵심 로직에 집중
- ex04: Joint Space에서 Cartesian Space로 전환, IK와 쿼터니언 활용
- ex05: Cartesian Path로 직선 경로를 지정하여 정밀 작업 수행