제5부 좌표·모델
5.7 로봇 모델 품질 검증
5.7.1 이 장에서 배우는 것 / 선수 지식
5.3.9에서 check_urdf가 확인하지 못하는 것들을 나열했다 — 관성의 물리적 타당성, 축 방향의 의도 일치, 메시 존재, 충돌 기하 복잡도, 확장 태그 오타. 이것들은 조용히 통과하고 나중에 비용이 큰 문제로 돌아온다.
시뮬레이터가 발산하고, MoveIt 2의 계획이 느리고, 관절이 반대로 회전하고, 실기에서 제어 게인이 안 맞는다. 그리고 이 문제들의 원인이 로봇 모델이라는 것을 알아채기까지 며칠이 걸린다.
이 장은 그 검증을 체계와 자동화로 만든다.
배우는 것:
- 검증 층위 — 문법·구조·물리·의미·성능.
- 관성 검증 — 삼각 부등식과 현실성.
- 기하 검증 — 충돌 복잡도·메시 존재·스케일.
- 의미 검증 — 축·한계·이름 규약.
- tf 트리 검증 — 실행 시점의 확인.
- CI 자동화 — 무엇을 어떻게 게이트로.
- 단일 출처 점검 — 중복·불일치 탐지.
선수 지식: 제5부 전체(5.1~5.6), 4.10(CI 게이트), 4.5(colcon test).
정직: 아래 검증 스크립트는 이 책에서 제시하는 개념적 구현이며, 실행 검증하지 않았다(목차 3). 기존 도구(
check_urdf·urdf_to_graphiz)를 제외한 물리·의미 검증은 표준 도구가 부족한 영역이라 직접 만들게 되는 경우가 많다. 여기서는 무엇을 검사해야 하는지를 제시하고 구현은 참조 예로 둔다.(→ 166차 확인: 핵심 스크립트 넷(5.7.4·5.7.5·5.7.6·5.7.7)을 책의 코드 그대로 옮겨 실제로 실행했다 — 아래 각 절의 실행 확인(166차) 참고. 넷 다 첫 실행에서 버그 없이 책의 서술과 정확히 일치했다. CI 통합(5.7.10)·joint_state_publisher_gui기반 의미 검증은 여전히 실행 검증하지 않았다.)(→ 167차 확인: 5.7.8의 tf 트리 완전성 테스트도 실제launch_testing으로 실행해 양성·음성 둘 다 확인했다 — 실행 확인(167차) 참고.act가 필요한 5.7.10 CI 워크플로는 이 컨테이너에 Docker가 없어 시도하지 못했다.)
5.7.2 검증 층위
| 층위 | 도구 | 자동화 |
|---|---|---|
| ① 문법 | xacro·xmllint | ✅ 쉬움 |
| ② 구조 | check_urdf | ✅ 쉬움 |
| ③ 물리 | 직접 구현 | ✅ 가능 |
| ④ 의미 | 직접 구현 + 사람 | ⚠️ 부분적 |
| ⑤ 자원 | 직접 구현 | ✅ 쉬움 |
| ⑥ 성능 | 직접 구현 | ✅ 가능 |
| ⑦ 실행 시점 | view_frames·RViz2·시뮬 | ⚠️ 부분적 |
①②는 이미 도구가 있고, ③~⑥이 이 장의 초점이다.
5.7.3 ① 문법과 ② 구조
# xacro 전개 (인자 조합별로 — 5.4.8)
xacro robot.urdf.xacro > /tmp/real.urdf
xacro robot.urdf.xacro use_sim:=true > /tmp/sim.urdf
# XML 문법
xmllint --noout /tmp/real.urdf
# URDF 구조
check_urdf /tmp/real.urdf
# 트리 시각화
urdf_to_graphiz /tmp/real.urdf
전개 자체가 검증이다. xacro 오류(속성 미정의·매크로 누락·include 실패)가 여기서 검출된다(5.4.8).
모든 인자 조합을 검증하라:
for sim in true false; do
for variant in standard extended; do
echo "=== use_sim=$sim variant=$variant ==="
xacro robot.urdf.xacro use_sim:=$sim robot_variant:=$variant \
> /tmp/r.urdf && check_urdf /tmp/r.urdf || exit 1
done
done
견해: 인자 조합 검증이 자주 누락된다.
use_sim:=false만 쓰다가 시뮬을 처음 실행할 때 xacro 오류를 발견하는 식이다. 조합이 적으면 전수, 많으면 대표 조합을 CI에 넣는다(4.10.3).
5.7.4 ③ 물리 검증
관성 삼각 부등식(5.3.5)과 현실성을 검사한다.
#!/usr/bin/env python3
"""URDF 물리 타당성 검사 — 개념 구현"""
import sys
import xml.etree.ElementTree as ET
# 임계값 — 로봇에 맞게 조정할 것
MIN_MASS = 1e-4 # kg
MAX_MASS = 1000.0 # kg
MIN_INERTIA = 1e-9 # kg·m²
TRIANGLE_TOL = 1e-6 # 수치 오차 허용
def check_link_inertia(link):
errors, warnings = [], []
name = link.get('name')
inertial = link.find('inertial')
if inertial is None:
warnings.append(f'{name}: <inertial> 없음 — 시뮬에서 기본값 적용')
return errors, warnings
mass = float(inertial.find('mass').get('value'))
if mass <= MIN_MASS:
errors.append(f'{name}: 질량이 0에 가까움 ({mass}) — 시뮬 불안정')
elif mass > MAX_MASS:
warnings.append(f'{name}: 질량이 큼 ({mass} kg) — 단위 확인(g vs kg)')
i = inertial.find('inertia')
ixx, iyy, izz = (float(i.get(k)) for k in ('ixx', 'iyy', 'izz'))
ixy, ixz, iyz = (float(i.get(k)) for k in ('ixy', 'ixz', 'iyz'))
# 대각 성분은 양수여야 한다
for label, v in (('ixx', ixx), ('iyy', iyy), ('izz', izz)):
if v <= MIN_INERTIA:
errors.append(f'{name}: {label}가 0에 가까움 ({v}) — 수치 불안정')
# 삼각 부등식 (5.3.5)
for a, b, c, lbl in ((ixx, iyy, izz, 'ixx+iyy>=izz'),
(iyy, izz, ixx, 'iyy+izz>=ixx'),
(izz, ixx, iyy, 'izz+ixx>=iyy')):
if a + b < c - TRIANGLE_TOL:
errors.append(f'{name}: 삼각 부등식 위반 ({lbl}) — 물리적으로 불가능')
# 비대각 성분이 대각보다 크면 의심
max_diag = max(ixx, iyy, izz)
for label, v in (('ixy', ixy), ('ixz', ixz), ('iyz', iyz)):
if abs(v) > max_diag:
warnings.append(f'{name}: {label}가 대각 성분보다 큼 — 확인 필요')
return errors, warnings
def main(urdf_path):
root = ET.parse(urdf_path).getroot()
all_errors, all_warnings = [], []
total_mass = 0.0
for link in root.findall('link'):
e, w = check_link_inertia(link)
all_errors += e
all_warnings += w
inertial = link.find('inertial')
if inertial is not None:
total_mass += float(inertial.find('mass').get('value'))
print(f'총 질량: {total_mass:.3f} kg')
for w in all_warnings:
print(f'[경고] {w}')
for e in all_errors:
print(f'[오류] {e}')
return 1 if all_errors else 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1]))
검사 항목 정리:
| 검사 | 수준 | 근거 |
|---|---|---|
| 질량 > 0 | 오류 | 시뮬 불안정·링크 무시 |
| 관성 대각 > 0 | 오류 | 수치 불안정 |
| 삼각 부등식 | 오류 | 물리적으로 불가능(5.3.5) |
| 총 질량 출력 | 정보 | 사람이 현실성 판단 |
| 질량 상한 초과 | 경고 | 단위 혼동 탐지 |
<inertial> 누락 | 경고 | 의도적일 수 있음 |
| 비대각 > 대각 | 경고 | 대개 실수 |
견해: 총 질량을 출력해 사람이 보게 하는 것이 의외로 효과적이다. "이 로봇이 3톤이라고?"는 자동 검사보다 사람이 빨리 알아챈다. 자동 검사는 명백한 불가능을 검출하고, 현실성 판단은 사람에게 남기는 분담이 실용적이다.
실행 확인(166차, Jazzy) — 위
check_physics.py를 책의 코드 그대로 옮겨 5개 URDF에 실제로 실행했다. 정상 URDF(오류 0)·삼각 부등식 위반(izz=100vsixx=iyy=0.001, 오류 1건 정확히 검출)·질량 음수(-5.0, 오류 1건)·<inertial>없음(경고 1건)·비대각 성분이 대각보다 큼(ixy=0.5vs 대각 최대 0.1, 경고 1건) — 5개 케이스 전부 위 표(오류/경고 구분)와 정확히 일치했고, 버그가 하나도 없었다. CLI 진입점(if __name__ == '__main__':)은 책 코드에 없어 테스트를 위해 직접 덧붙였고, 함수 본문은 무수정이다. 원본:tools/gap_harness/gap166/rj57_test/(check_physics.py·physics_check_results.log·URDF 5종).
5.7.5 ⑤ 자원 검증
메시 파일이 실제로 존재하는가. check_urdf는 확인하지 않는다.
"""메시 참조 검증 — 개념 구현"""
import os
import re
import xml.etree.ElementTree as ET
from ament_index_python.packages import get_package_share_directory
def resolve_package_uri(uri):
m = re.match(r'package://([^/]+)/(.*)', uri)
if not m:
return None
pkg, rel = m.group(1), m.group(2)
try:
return os.path.join(get_package_share_directory(pkg), rel)
except Exception:
return None
def check_meshes(urdf_path):
root = ET.parse(urdf_path).getroot()
errors = []
for mesh in root.iter('mesh'):
uri = mesh.get('filename')
path = resolve_package_uri(uri)
if path is None:
errors.append(f'해석 불가: {uri}')
elif not os.path.isfile(path):
errors.append(f'파일 없음: {uri} → {path}')
# 스케일 확인 (5.3.6)
scale = mesh.get('scale')
if scale is None:
print(f'[정보] scale 미지정: {uri} — CAD가 mm 단위면 0.001 필요')
return errors
검사 항목:
| 검사 | 수준 |
|---|---|
package:// 해석 가능 | 오류 |
| 파일 존재 | 오류 |
scale 미지정 | 정보 — mm/m 혼동 탐지(5.3.6) |
| 확장자 지원 | 경고 |
함정: 이 검사는 "설치된 상태"에서 해야 의미가 있다. 소스 디렉터리에서는 파일이 있는데 설치 규칙이 없어 배포본에서 없는 경우를 검출하려면, 빌드·설치 후 설치본 기준으로 검사해야 한다(4.1.4).
colcon build --packages-select my_robot_description
source install/setup.bash
python3 check_meshes.py /tmp/robot.urdf # 설치본 기준 해석
실행 확인(166차, Jazzy) — 위
check_meshes를 책의 코드 그대로 옮겨 3개 URDF에 실제로 실행했다. 실재 패키지(robot_state_publisher)의 실재 파일(package.xml)을 가리킨 경우(오류 0)·실재 패키지의 존재하지 않는 파일을 가리킨 경우("파일 없음" 오류 1건, 실제 해석된 경로까지 정확히 출력)·존재하지 않는 패키지를 가리킨 경우("해석 불가" 오류 1건) — 3개 케이스 전부 정확했다.scale미지정 시의 "[정보]" 출력도 의도대로 동작했다.ament_index_python.packages.get_package_share_directory로 실제 설치된 Jazzy 패키지 경로를 정확히 해석함을 확인했다. 원본:tools/gap_harness/gap166/rj57_test/(check_meshes.py·mesh_check_results.log·URDF 3종).
5.7.6 ⑥ 성능 검증
충돌 기하의 복잡도가 계획·시뮬 성능을 지배한다(5.3.4).
"""충돌 기하 복잡도 검사 — 개념 구현"""
PRIMITIVE = {'box', 'sphere', 'cylinder'}
def check_collision_complexity(root):
report = []
for link in root.findall('link'):
name = link.get('name')
for col in link.findall('collision'):
geom = col.find('geometry')
if geom is None:
continue
child = list(geom)[0]
kind = child.tag
if kind == 'mesh':
report.append((name, 'mesh', child.get('filename')))
elif kind in PRIMITIVE:
report.append((name, kind, None))
mesh_count = sum(1 for r in report if r[1] == 'mesh')
print(f'충돌 기하: 총 {len(report)}개, 그중 메시 {mesh_count}개')
if mesh_count > 0:
print('[경고] 충돌 메시는 계획·시뮬 성능을 저하시킨다 (5.3.4)')
for name, _, f in (r for r in report if r[1] == 'mesh'):
print(f' - {name}: {f}')
return report
추가로 확인할 것:
| 항목 | 방법 |
|---|---|
| 메시 삼각형 수 | 메시 파일 파싱 또는 도구 |
| 볼록성 | 볼록 분해 도구 |
| 충돌 기하 누락 | <collision> 없는 링크 목록 |
| 시각 대비 충돌 크기 | 수동 검토(RViz2) |
견해: "충돌 메시가 몇 개인가"를 리포트로 출력하는 것만으로도 값을 한다. 무심코 시각 메시를 충돌에도 복사한 것이 즉시 드러나고, MoveIt 2 계획이 느린 이유를 설명한다(9.3).
실행 확인(166차, Jazzy) — 위
check_collision_complexity를 책의 코드 그대로 옮겨 실행했다. box·cylinder·mesh 충돌 기하가 섞인 링크 3개(+충돌 기하 없는 링크 1개)를 가진 URDF에서 "충돌 기하: 총 3개, 그중 메시 1개"를 정확히 집계했고, 메시를 쓴 링크(gripper_link)와 그 파일명을 정확히 지목했다. 원본:tools/gap_harness/gap166/rj57_test/(check_collision.py·collision_mixed.urdf·collision_check_result.log).
5.7.7 ④ 의미 검증
가장 어려운 층위다. "축 방향이 의도와 맞는가"는 자동으로 알 수 없다.
부분적으로 자동화 가능한 것:
def check_semantics(root):
errors, warnings = [], []
for joint in root.findall('joint'):
name = joint.get('name')
jtype = joint.get('type')
# revolute · prismatic 에 limit 필수 (5.3.3)
if jtype in ('revolute', 'prismatic'):
limit = joint.find('limit')
if limit is None:
errors.append(f'{name}: {jtype}인데 <limit> 없음')
else:
lo = float(limit.get('lower', 0))
hi = float(limit.get('upper', 0))
if lo >= hi:
errors.append(f'{name}: lower({lo}) >= upper({hi})')
eff = float(limit.get('effort', 0))
vel = float(limit.get('velocity', 0))
if eff <= 0:
warnings.append(f'{name}: effort가 0 — 실제 사양 확인(5.3.3)')
if vel <= 0:
warnings.append(f'{name}: velocity가 0')
# axis 정규화 확인
axis = joint.find('axis')
if axis is not None and jtype in ('revolute', 'continuous', 'prismatic'):
xyz = [float(v) for v in axis.get('xyz').split()]
norm = sum(v * v for v in xyz) ** 0.5
if abs(norm - 1.0) > 1e-3:
warnings.append(f'{name}: axis가 단위벡터가 아님 (norm={norm:.4f})')
if norm < 1e-6:
errors.append(f'{name}: axis가 영벡터')
# 프레임 이름 규약 (1.1.11)
for link in root.findall('link'):
n = link.get('name')
if n.startswith('/'):
errors.append(f'{n}: 프레임 이름에 앞 슬래시 금지')
if ' ' in n:
errors.append(f'{n}: 이름에 공백')
return errors, warnings
자동화할 수 없어 사람이 봐야 하는 것:
| 항목 | 확인 방법 |
|---|---|
| 축 방향이 의도대로인가 | joint_state_publisher_gui로 움직여 본다(5.5.7) |
| 관절 한계가 실제 하드웨어와 맞는가 | 사양서 대조 |
| 센서 위치가 실측과 맞는가 | 실물 측정 |
| 광학 프레임 방향 | 카메라 이미지와 대조(5.1.7) |
| 질량·관성의 현실성 | 사양·CAD 대조 |
견해:
joint_state_publisher_gui로 전 관절을 한 번씩 움직여 보는 것이 의미 검증의 핵심이며, 대체 수단이 없다. 새 모델을 만들거나 크게 고쳤을 때 반드시 하는 절차로 두라 — 5분이면 되고, 축이 반대인 관절을 즉시 고정한다.
실행 확인(166차, Jazzy) — 위
check_semantics를 책의 코드 그대로 옮겨 실행했다. 다섯 가지 결함(축이 단위벡터가 아님·effort/velocity가 0·lower>=upper역전·프레임 이름 앞 슬래시·이름에 공백)을 심은 관절·링크와 정상 관절(j_ok) 하나를 섞은 URDF에서 결함 다섯 개가 전부 정확히 검출됐고(오류 3건 — 한계 역전·슬래시·공백, 경고 3건 — 비단위벡터·effort=0·velocity=0), 정상 관절에는 오탐 0건이었다. 원본:tools/gap_harness/gap166/rj57_test/(check_semantics.py·semantics_mixed.urdf·semantics_check_result.log).
5.7.8 ⑦ 실행 시점 검증
모델이 실제로 tf가 되는지 확인한다.
# 1. 시각화 launch로 실행하기 (5.3.9)
ros2 launch my_robot_description display.launch.py
# 2. tf 트리 확인 (5.1.10)
ros2 run tf2_tools view_frames
# 3. 기대하는 프레임이 전부 있는가
ros2 run tf2_ros tf2_echo base_link camera_optical_frame
# 4. 관절 상태 없이도 고정 관절은 나와야 한다 (5.5.10)
ros2 topic echo /tf_static --qos-durability transient_local --once
자동화 가능한 실행 시점 검증(launch_testing — 14.6):(→ 167차 확인: 아래 함수 본문을 무수정으로 실제 launch_testing에 옮겨 실행했다 — 아래 실행 확인(167차) 참고)
"""tf 트리 완전성 테스트 — 개념 구현"""
import pytest
import rclpy
import tf2_ros
from rclpy.duration import Duration
EXPECTED_FRAMES = [
'base_link', 'laser_link', 'camera_link',
'camera_optical_frame', 'front_left_wheel',
]
def test_tf_tree_complete(ros_context_with_rsp):
node = rclpy.create_node('tf_test')
buf = tf2_ros.Buffer()
listener = tf2_ros.TransformListener(buf, node) # 참조 유지(5.1.3)
# 디스커버리·발행 대기 (3.3.3)
end = node.get_clock().now() + Duration(seconds=5)
while node.get_clock().now() < end:
rclpy.spin_once(node, timeout_sec=0.1)
missing = []
for frame in EXPECTED_FRAMES:
if not buf.can_transform('base_link', frame, rclpy.time.Time(),
timeout=Duration(seconds=1)):
missing.append(frame)
node.destroy_node()
assert not missing, f'tf 트리에 없는 프레임: {missing}'
견해: "기대하는 프레임 목록"을 테스트에 명시하는 것이 값을 한다. 누군가 URDF를 리팩터링하며 프레임 이름을 바꾸면 CI가 검출한다 — 그 이름에 의존하는 Nav2 설정·MoveIt SRDF가 조용히 깨지는 것을 막는다.
실행 확인(167차, Jazzy) — 위 함수 본문을 무수정으로 실제
launch_testing에 옮겨 양성·음성 둘 다 실행했다.generate_test_description()·ReadyToTest골격만 이번에 추가했다(책엔 없었음). 5개 프레임(base_link·laser_link·camera_link·camera_optical_frame·front_left_wheel)이 전부 있는 URDF로robot_state_publisher를 실행하니launch_test가 "Ran 1 test in 5.186s / OK"로 통과했다(EXIT 0).front_left_wheel만 뺀 URDF로 다시 실행하니 정확히 그 프레임만 실패 메시지에 지목됐다(AssertionError: tf 트리에 없는 프레임: ['front_left_wheel'], EXIT 1). "누군가 프레임 이름을 바꾸면 CI가 검출한다"는 위 견해가 양성·음성 둘 다로 결정적으로 확인됐다. 원본:tools/gap_harness/gap167/rj57_08_test/(test_tf_tree.py·full_robot.urdf·missing_frame_robot.urdf·run_positive.log·run_negative.log).
5.7.9 단일 출처 점검
제5부에서 반복한 원칙(5.2.5·5.4.5·5.5.4·5.6.5)을 자동으로 점검한다.
중복 정적 변환 탐지:
# URDF의 고정 관절 목록
xacro robot.urdf.xacro | \
grep -B2 'type="fixed"' | grep -oP '(?<=<joint name=")[^"]+'
# launch에서 static_transform_publisher 사용 여부
grep -rn 'static_transform_publisher' launch/
# 둘이 겹치면 중복 (5.2.5)
여러 곳의 xacro 처리 탐지:
# robot_description을 만드는 곳이 여럿인가
grep -rn 'xacro\|robot_description' launch/ config/ | grep -v '^Binary'
시뮬·실기 모델 일치 확인:
# 구조가 같아야 한다 (gazebo 태그만 달라야)
xacro robot.urdf.xacro use_sim:=false > /tmp/real.urdf
xacro robot.urdf.xacro use_sim:=true > /tmp/sim.urdf
# 관절 이름 비교 — 달라지면 컨트롤러가 깨진다 (5.6.6)
diff <(grep -oP '(?<=<joint name=")[^"]+' /tmp/real.urdf | sort) \
<(grep -oP '(?<=<joint name=")[^"]+' /tmp/sim.urdf | sort)
# 링크 이름 비교
diff <(grep -oP '(?<=<link name=")[^"]+' /tmp/real.urdf | sort) \
<(grep -oP '(?<=<link name=")[^"]+' /tmp/sim.urdf | sort)
견해: 관절·링크 이름이 시뮬과 실기에서 동일한지 CI로 검사하라. 이것이 어긋나면 컨트롤러 설정(6.5)·MoveIt 설정(9.1)이 한쪽에서만 동작하고, 원인 파악이 어렵다.
diff가 비어 있어야 정상이라는 단순한 게이트로 큰 부류의 문제를 막는다.
5.7.10 CI 통합
# .github/workflows/model-check.yml
name: Robot Model Check
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-24.04
container: ros:jazzy-ros-base
steps:
- uses: actions/checkout@v4
with: {path: src/my_robot_description}
- name: 의존 설치·빌드
run: |
apt-get update && rosdep update
rosdep install --from-paths src -y --ignore-src
. /opt/ros/jazzy/setup.sh
colcon build --merge-install
- name: xacro 전개 (인자 조합)
run: |
. install/setup.sh
for sim in true false; do
xacro src/my_robot_description/urdf/robot.urdf.xacro \
use_sim:=$sim > /tmp/r_$sim.urdf
check_urdf /tmp/r_$sim.urdf
done
- name: 물리 검증
run: |
. install/setup.sh
python3 src/my_robot_description/scripts/check_physics.py /tmp/r_false.urdf
- name: 자원 검증 (설치본 기준)
run: |
. install/setup.sh
python3 src/my_robot_description/scripts/check_meshes.py /tmp/r_false.urdf
- name: 시뮬·실기 구조 일치
run: |
diff <(grep -oP '(?<=<joint name=")[^"]+' /tmp/r_false.urdf | sort) \
<(grep -oP '(?<=<joint name=")[^"]+' /tmp/r_true.urdf | sort)
- name: tf 트리 테스트
run: |
. install/setup.sh
colcon test --packages-select my_robot_description
colcon test-result --verbose # ★ 필수 (4.10.11)
게이트 수준 판단(4.10.9):
| 검사 | 수준 |
|---|---|
| xacro 전개 실패 | 🔴 차단 |
check_urdf 실패 | 🔴 차단 |
| 관성 삼각 부등식 위반 | 🔴 차단 |
| 질량·관성 0 | 🔴 차단 |
| 메시 파일 없음 | 🔴 차단 |
| 시뮬·실기 구조 불일치 | 🔴 차단 |
| tf 트리 프레임 누락 | 🔴 차단 |
| 충돌 메시 사용 | 🟡 경고 |
<inertial> 누락 | 🟡 경고 |
| 총 질량 리포트 | 🟢 정보 |
5.7.11 검증 체크리스트
새 로봇 모델·큰 수정 시 수행할 목록이다.
자동(CI):
- [ ] 모든 인자 조합에서 xacro 전개 성공
- [ ]
check_urdf통과 - [ ] 관성 삼각 부등식·양수 조건 통과
- [ ] 메시 파일이 설치본 기준으로 존재
- [ ] 시뮬·실기 관절·링크 이름 일치
- [ ] tf 트리에 기대 프레임 전부 존재
- [ ] 프레임 이름 규약(슬래시·공백 없음)
수동(사람):
- [ ]
joint_state_publisher_gui로 전 관절 구동 — 축 방향·한계 확인 - [ ] RViz2에서 시각 기하 확인 — 스케일·위치
- [ ] 충돌 기하를 RViz2에서 확인 — 시각보다 약간 큰가
- [ ] 총 질량이 현실적인가
- [ ] 관절 한계가 하드웨어 사양과 맞는가
- [ ] 센서 위치가 실측과 맞는가
- [ ] 광학 프레임 방향이 이미지와 맞는가
- [ ] 시뮬에서 로봇이 안정적인가(발산·떨림 없음)
견해: "시뮬에서 로봇이 가만히 있는가"가 물리 모델의 좋은 통합 검사다. 관성이 엉터리면 아무 명령을 안 줘도 링크가 떨리거나 로봇이 튀어 나간다. 검증 스크립트가 놓친 문제를 이 한 가지 관찰이 포착하는 경우가 많다.
5.7.12 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | check_urdf 통과를 완전 검증으로 오해 | 물리·의미 문제 잔존 | 층위별 검증(5.7.2) |
| 2 | 일부 인자 조합만 검증 | 시뮬 첫 실행 시 오류 | 조합 전수·대표 |
| 3 | 메시 검증을 소스 기준으로 | 배포본에서 실패 | 설치본 기준(5.7.5) |
| 4 | 관성 자동 검사만 신뢰 | 현실성 문제 통과 | 총 질량 리포트 + 사람 |
| 5 | 축 방향 검증 생략 | 관절이 반대로 | GUI로 구동 확인 |
| 6 | 시뮬·실기 이름 불일치 방치 | 한쪽에서만 동작 | diff 게이트 |
| 7 | 충돌 메시 무의식적 사용 | 계획·시뮬 느림 | 복잡도 리포트 |
| 8 | tf 프레임 이름 변경 후 미검증 | 하류 설정이 조용히 깨짐 | 기대 프레임 테스트 |
| 9 | 중복 정적 변환 방치 | 값 불일치 | 단일 출처 점검(5.7.9) |
| 10 | colcon test-result 생략 | 실패를 통과로 | CI에 포함(4.10.11) |
| 11 | 시뮬 안정성 미확인 | 실기 튜닝 시 발견 | 정지 상태 관찰 |
| 12 | 검증을 모델 완성 후에만 | 수정 비용 증가 | 초기부터 CI |
5.7.13 체크포인트
- 검증 층위 일곱 가지를 말하고, 기존 도구가 있는 것과 직접 만들어야 하는 것을 구분할 수 있는가.
- 인자 조합 검증이 왜 필요하며, 누락 시 언제 문제가 드러나는가.
- 관성 검증에서 오류로 처리할 것과 경고로 처리할 것을 구분할 수 있는가. 삼각 부등식이 오류인 이유는?
- 총 질량을 출력해 사람이 보게 하는 것이 왜 효과적인가. 자동 검사와 사람의 분담 원칙은?
- 메시 검증을 설치본 기준으로 해야 하는 이유는?
- 충돌 기하 복잡도 리포트가 검출하는 전형적 실수는 무엇인가.
- 의미 검증에서 자동화 가능한 것과 사람이 봐야 하는 것을 각각 세 가지 들 수 있는가.
- 축 방향 검증의 유일한 실용적 방법은 무엇인가.
- tf 트리 테스트에 "기대 프레임 목록"을 명시하는 것이 무엇을 막아 주는가.
- 시뮬·실기 관절 이름 일치를 CI로 검사해야 하는 이유는?
- 물리 모델의 좋은 통합 검사 한 가지는 무엇인가.
여기까지가 제5부 좌표·모델이다. tf2 구조(5.1)와 실전 함정(5.2), URDF(5.3)와 xacro(5.4), 그것을 tf로 바꾸는 RSP(5.5), SDF와의 관계(5.6), 그리고 검증(5.7).
제5부가 반복한 원칙은 하나다 — 단일 출처. 센서 위치는 URDF에만(5.2.5), 시뮬·실기는 하나의 xacro에서(5.4.5), robot_description은 한 곳에서(5.5.4), URDF와 SDF를 병행하지 말 것(5.6.5). 로봇 모델이 여러 곳에 흩어지면 반드시 어긋나고, 어긋난 뒤에는 "시뮬에서는 되는데 실기에서 안 된다"가 된다.
이제 로봇의 형상은 기술했다. 다음은 그것을 움직이는 일이다. 관절 각도는 어디서 오고 명령은 어떻게 나가는가, 실시간 제어 루프는 어떻게 구성하는가, 하드웨어를 어떻게 추상하는가 — 제6부 하드웨어 인터페이스(6.1~6.9) 의 주제다. ros2_control 아키텍처부터 시작한다.
5.7.14 제5부를 마치며 — 좌표·모델
이 장은 제5부(5.1~5.7)의 마지막이므로, 부 전체를 하나의 관점으로 묶는다.
제5부가 반복해 말한 세 가지:
- tf는 시간의 문제다. 프레임이 맞아도 시각이 어긋나면 틀린다(5.2). 외삽 오류 메시지의 두 숫자를 빼면 그 어긋남이 그대로 보인다(5.2.11).
- 모델은 소스가 하나여야 한다. URDF·SDF·시뮬·컨트롤러가 각자 다른 값을 들면 재현되지 않는 오차가 생긴다(5.3~5.6).
- 모델 오류는 조용하다. 문법이 맞으면 통과하고, 틀린 관성·잘못된 원점은 동작으로만 드러난다(5.7). 그래서 검증을 절차로 만들어야 한다.
다음 부에서 무엇이 달라지나: 제5부는 로봇을 기술하는 법이었다. 제6부는 그 기술을 실제 하드웨어에 연결하는 층 — ros2_control, 드라이버, 실시간 루프다. 여기서 처음으로 틀리면 물건이 움직인다.
제6부는 하드웨어 인터페이스(6.1~6.9)다.