AEGLOSLAB ROS 2 Jazzy 대전

제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 검증 층위

로봇 모델 검증 ① 문법 XML · xacro 전개 ② 구조 트리 · 참조 무결성 ③ 물리 관성 · 질량 · 치수 ④ 의미 축 · 한계 · 이름 규약 ⑤ 자원 메시 · 설치 ⑥ 성능 충돌 복잡도 ⑦ 실행 시점 tf 트리 · 시각 확인
층위도구자동화
① 문법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=100 vs ixx=iyy=0.001, 오류 1건 정확히 검출)·질량 음수(-5.0, 오류 1건)·<inertial> 없음(경고 1건)·비대각 성분이 대각보다 큼(ixy=0.5 vs 대각 최대 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 함정 카탈로그

#함정증상대처
1check_urdf 통과를 완전 검증으로 오해물리·의미 문제 잔존층위별 검증(5.7.2)
2일부 인자 조합만 검증시뮬 첫 실행 시 오류조합 전수·대표
3메시 검증을 소스 기준으로배포본에서 실패설치본 기준(5.7.5)
4관성 자동 검사만 신뢰현실성 문제 통과총 질량 리포트 + 사람
5축 방향 검증 생략관절이 반대로GUI로 구동 확인
6시뮬·실기 이름 불일치 방치한쪽에서만 동작diff 게이트
7충돌 메시 무의식적 사용계획·시뮬 느림복잡도 리포트
8tf 프레임 이름 변경 후 미검증하류 설정이 조용히 깨짐기대 프레임 테스트
9중복 정적 변환 방치값 불일치단일 출처 점검(5.7.9)
10colcon 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부가 반복해 말한 세 가지:

  1. tf는 시간의 문제다. 프레임이 맞아도 시각이 어긋나면 틀린다(5.2). 외삽 오류 메시지의 두 숫자를 빼면 그 어긋남이 그대로 보인다(5.2.11).
  2. 모델은 소스가 하나여야 한다. URDF·SDF·시뮬·컨트롤러가 각자 다른 값을 들면 재현되지 않는 오차가 생긴다(5.3~5.6).
  3. 모델 오류는 조용하다. 문법이 맞으면 통과하고, 틀린 관성·잘못된 원점은 동작으로만 드러난다(5.7). 그래서 검증을 절차로 만들어야 한다.

다음 부에서 무엇이 달라지나: 제5부는 로봇을 기술하는 법이었다. 제6부는 그 기술을 실제 하드웨어에 연결하는 층 — ros2_control, 드라이버, 실시간 루프다. 여기서 처음으로 틀리면 물건이 움직인다.

제6부는 하드웨어 인터페이스(6.1~6.9)다.