TIKI-MINI로 시작하는 ROS2 실무
목차

2. 시뮬레이션

2.5로봇 모델: URDF와 TF

1.9장에서 URDF는 링크와 조인트로 로봇 구조를 적은 XML이라고 배웠습니다. 이번에는 실물 파일을 읽습니다. 대상은 tiki_description/urdf/tiki.urdf.xacro이고 전체가 164줄입니다. 위에서 아래로 세 덩어리로 나뉩니다.

줄내용
3~18색(material) 4개
20~139링크 8개와 조인트 7개
141~163ros2_control 블록

xacro가 하는 일

파일 첫 줄의 <robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="andino">에서 xmlns:xacro는 이 파일이 xacro 문법을 쓴다고 선언합니다. 이 파일이 쓰는 xacro 기능은 하나뿐입니다. ${ } 안의 식을 계산해서 값으로 바꾸는 일입니다.

<color rgba="${255/255} ${108/255} ${10/255} 1.0"/>
<origin xyz="0 0 ${0.0325 + 0.004}" rpy="0 0 0.0"/>

xacro는 식을 Python으로 계산합니다. 색은 0~255 값을 0~1 범위로 나눈 것이고, yellow는 대략 (1.0, 0.4235, 0.0392)가 됩니다. 두 번째 줄은 base_link의 높이입니다. 바퀴 반지름 0.0325에 0.004를 더하니 0.0365 m입니다. 부동소수점 때문에 xacro 출력에는 0.036500000000000005처럼 긴 숫자가 찍힐 수 있고, 의미는 같습니다.

material식계산한 값(r g b)
yellow255/255, 108/255, 10/2551.0, 0.4235, 0.0392
gray50/2550.1961
dark_gray25/2550.0980
black5/2550.0196

매크로나 xacro:property는 쓰지 않았습니다. 그래서 같은 숫자(바퀴 반지름 0.0325 등)가 파일 안에 여러 번 적혀 있고, 다른 파일(tiki_controllers.yaml)에도 따로 적혀 있습니다. 한 군데를 고치면 나머지도 직접 맞춰야 합니다.

base_footprint와 base_link

URDF의 첫 링크는 모양이 없는 base_footprint입니다.

<link name="base_footprint"/>

<joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link" />
    <origin xyz="0 0 ${0.0325 + 0.004}" rpy="0 0 0.0"/>
</joint>

base_footprint는 바닥에 닿는 기준점입니다. base_link는 로봇 본체 위 0.0365 m에 있습니다. 바퀴 조인트가 base_link에서 z -0.004만큼 내려가므로 바퀴 중심의 높이는 0.0365 − 0.004 = 0.0325 m입니다. 이 값이 바퀴 반지름과 같으니 바퀴 아랫면이 정확히 z = 0에 닿습니다. 바닥을 z = 0으로 잡으려고 이 높이를 둔 것입니다.

이 프레임의 쓰임은 다른 파일에서 확인됩니다. tiki_controllers.yaml의 base_frame_id와 slam.yaml의 base_frame이 모두 base_footprint입니다. 2차원 주행에서는 로봇의 높이가 필요 없어서 바닥 기준점을 로봇 위치로 쓰는 것이 자연스럽습니다. Nav2 설정에는 base_link도 섞여 있는데 2.12장에서 확인합니다.

링크와 조인트 전체 표

링크는 8개입니다. 모양(visual)으로 그려집니다.

링크visual 모양크기(m)색
base_footprint없음
base_linkbox0.1 x 0.16 x 0.048black
back_sidebox0.121 x 0.214 x 0.048yellow
right_wheelcylinder반지름 0.0325, 길이 0.0265gray
left_wheelcylinder반지름 0.0325, 길이 0.0265gray
lidar_mountbox0.07 x 0.07 x 0.05dark_gray
lidar_bottombox0.0556 x 0.0556 x 0.02312black
lasercylinder반지름 0.025592, 길이 0.0182black

box의 크기는 x(앞뒤), y(좌우), z(높이) 순서입니다. 바퀴 원통의 visual에는 rpy="1.570795 0 0"이 붙어 있습니다. 원통은 기본적으로 z축 방향으로 서 있으므로 x축 둘레로 약 90도 눕혀 바퀴 모양을 만듭니다.

조인트는 7개입니다. origin은 부모 링크 기준입니다.

조인트종류부모 → 자식xyzrpy축
base_jointfixedbase_footprint → base_link0, 0, 0.03650, 0, 0
back_side_jointfixedbase_link → back_side-0.1105, 0, 00, 0, 0
right_wheel_jointcontinuousbase_link → right_wheel-0.0085, -0.0962, -0.0040, 0, 00 1 0
left_wheel_jointcontinuousbase_link → left_wheel-0.0085, 0.0962, -0.0040, 0, 00 1 0
lidar_mount_jointfixedbase_link → lidar_mount0, 0, 0.050, 0, 0
lidar_bottom_jointfixedlidar_mount → lidar_bottom0, 0, 0.026550, 0, 0
lidar_top_jointfixedlidar_bottom → laser0, 0, 0.020650, 0, 3.14159

조인트가 7개이고 링크가 8개입니다. 루트가 하나인 트리에서는 항상 링크가 조인트보다 하나 많습니다. 또한 continuous인 바퀴 조인트 두 개만 움직이고 나머지 다섯 개는 고정입니다.

위에서 본 TIKI-MINI

아래 그림은 위 표의 숫자만으로 그린 것입니다. 링크를 눌러 부모, 조인트, origin을 확인하고, 프레임 축을 켜서 각 링크의 x(빨강)와 y(초록)가 어디를 향하는지 보세요. 맨 아래 칸에 LiDAR 위치의 계산식이 나옵니다.

앞 (+x)왼쪽 (+y)base_footprintlaser5 cm

laser

LiDAR 상단 회전부이고 센서 프레임 이름이 laser입니다. yaw가 3.14159(약 180도)라 laser의 x축이 로봇의 뒤쪽을 향합니다.

부모 링크lidar_bottom
조인트lidar_top_joint (fixed)
origin xyz0 0 0.02065
origin rpy0 0 3.14159
visualcylinder 반지름 0.025592, 길이 0.0182
base_footprint 기준(0, 0, 0.1337) m, yaw 3.14159 rad

경로: base_footprint → base_link → lidar_mount → lidar_bottom → laser

laser의 base_footprint 기준 위치 (조인트 origin의 합)

x = 0 + 0 + 0 + 0 = 0

z = 0.0365 + 0.05 + 0.02655 + 0.02065 = 0.1337

yaw = 3.14159 rad (약 180도)

바퀴 간격

|0.0962 − (-0.0962)| = 0.1924 m

그림에서 다음을 확인해 보세요.

  • 앞쪽 본체(base_link)는 작고 뒤쪽 본체(back_side)가 넓습니다. 바퀴는 base_link 옆, x = -0.0085에 달려 있습니다.
  • LiDAR(laser)는 앞쪽 본체(base_link) 위에 있고, 위에서 보면 base_footprint 원점과 같은 자리입니다.
  • laser를 선택하면 축이 뒤집혀 있습니다. x가 로봇의 뒤쪽, y가 오른쪽을 향합니다.

직접 계산 1: 바퀴 간격

두 바퀴 조인트의 y 좌표는 -0.0962와 +0.0962입니다.

바퀴 간격 = 0.0962 - (-0.0962) = 0.1924 m

tiki_controllers.yaml의 wheel_separation: 0.1924와 같은 값입니다. wheel_radius: 0.0325도 바퀴 원통의 반지름과 같습니다. 컨트롤러는 URDF를 읽지 않고 yaml의 숫자만 쓰므로 URDF의 숫자와 yaml의 숫자는 사람이 맞춰야 합니다. 이 값이 틀리면 로봇이 제자리 회전을 명령한 각도만큼 돌지 않습니다. 2.6장에서 이 값이 어떻게 쓰이는지 계산합니다.

간격은 바퀴 중심 사이의 거리입니다. 바퀴의 바깥쪽 끝 사이는 0.1924 + 0.0265 = 0.2189 m입니다. 또 바퀴 축은 base_footprint보다 8.5 mm 뒤(x = -0.0085)에 있어서, 로봇이 제자리에서 돌 때의 회전 중심은 base_footprint의 원점이 아니라 바퀴 축의 가운데입니다.

직접 계산 2: LiDAR 위치

laser는 base_footprint에서 고정 조인트 4개를 거쳐 달려 있습니다. 조인트의 origin을 위에서부터 더하면 됩니다. 이 경로에는 마지막 조인트에만 회전이 있어서 단순 합이 맞습니다.

조인트xyz
base_joint000.0365
lidar_mount_joint000.05
lidar_bottom_joint000.02655
lidar_top_joint000.02065
합000.1337

base_footprint 기준으로 laser는 x, y가 모두 0이라 base_footprint 바로 위, 바닥 위 0.1337 m에 있습니다. 앞뒤와 좌우로 어긋나지 않으므로 에뮬레이터처럼 거리를 로봇 중심에서 잰다고 봐도 됩니다. 회전은 yaw 3.14159 rad로 π(3.14159265...)와 거의 같고 약 180도입니다. 쿼터니언으로 바꾸면 (0, 0, 1, 약 0.0000013)입니다.

yaw π의 의미는 이렇습니다. LiDAR 드라이버는 laser 프레임의 +x 방향을 각도 0으로 보고 스캔을 만듭니다. 그런데 laser의 +x가 로봇의 뒤쪽을 향하니, 로봇의 정면은 스캔 각도 π(±π 근처)에 해당합니다. /scan의 숫자를 로봇 기준 방향으로 읽을 때 이 180도 차이를 놓치면 앞뒤를 거꾸로 해석합니다. 브라우저 에뮬레이터의 /scan은 다릅니다. 각도 0이 로봇 정면이 되도록 만들어져 있어서 이 180도 차이가 반영되어 있지 않습니다. 그래서 에뮬레이터에서 짠 코드를 실기기로 옮길 때(4.5장)는 스캔 인덱스와 각도도 다시 맞춰야 합니다.

옆에서 본 TIKI-MINI 링크 높이back_side바퀴laser의 +x (yaw π)로봇 정면 (+x)laser z 0.1337lidar_bottom z 0.11305lidar_mount z 0.0865base_link z 0.0365바퀴 중심 z 0.0325base_footprint z 0
URDF 값으로 옆에서 본 TIKI-MINI를 같은 축척으로 그렸습니다. laser는 base_footprint 원점 바로 위 0.1337 m에 있고 +x가 로봇 뒤쪽을 향합니다.

참고로 URDF의 형상을 모두 감싸는 가장 작은 원(base_footprint 중심)을 계산하면 반지름이 약 0.20 m입니다. 가장 먼 점은 뒤쪽 본체의 모서리(-0.171, ±0.107)입니다. Nav2 설정의 robot_radius와 견주어 보는 일은 2.12장에서 합니다.

TF 트리

고정 조인트는 /tf_static, 움직이는 조인트는 /joint_states와 함께 /tf로 발행됩니다(1.9장). 이 URDF만으로 만들어지는 트리는 이렇습니다.

base_footprint
└── base_link
    ├── back_side
    ├── right_wheel   (continuous, joint_states 필요)
    ├── left_wheel    (continuous, joint_states 필요)
    └── lidar_mount
        └── lidar_bottom
            └── laser

로봇이 실제로 구동될 때는 맨 위에 프레임이 하나 더 붙습니다. tiki_controllers.yaml에 odom_frame_id: odom, base_frame_id: base_footprint, enable_odom_tf: true가 있어서 diff_controller가 odom → base_footprint 변환을 발행합니다. SLAM이나 Nav2가 켜지면 그 위에 map → odom이 이어집니다. 그래서 URDF에서 루트였던 base_footprint가 실제 트리에서는 odom의 자식입니다.

구동 중인 TIKI-MINI의 TF 트리mapodombase_footprintbase_linkURDF 밖, 구동 중에 붙는 프레임back_sideright_wheelleft_wheellidar_mountlidar_bottomlaserSLAM 또는 Nav2map → odomdiff_controllerodom → base_footprintrobot_state_publisher고정 조인트, /tf_staticrobot_state_publisher바퀴, /joint_states 필요
구동 중인 TF 트리를 변환마다 발행하는 노드별로 나눠 그렸습니다. URDF의 루트였던 base_footprint 위에 odom과 map이 붙습니다.

ros2_control 블록

파일 맨 아래 블록이 URDF와 컨트롤러를 잇습니다.

<ros2_control name="RealRobot" type="system">
    <hardware>
        <plugin>tiki_base/DiffDriveTiki</plugin>
    </hardware>
    <joint name="left_wheel_joint">
        <command_interface name="velocity">
            <param name="min">-5</param>
            <param name="max">5</param>
        </command_interface>
        <state_interface name="velocity"/>
        <state_interface name="position"/>
    </joint>
    ...
</ros2_control>
  • <plugin>은 tiki_base 패키지의 tiki_hw.xml(로봇에 설치되어 있습니다. 4.3장)에 적힌 클래스 이름입니다. controller_manager가 이 문자열로 tiki_base의 플러그인을 불러옵니다.
  • 조인트 이름 left_wheel_joint, right_wheel_joint는 세 곳에서 같아야 합니다. 위쪽의 <joint> 정의, diffdrive_tiki.h가 기본값으로 들고 있는 left_wheel_name, 그리고 tiki_controllers.yaml의 left_wheel_names입니다.
  • command_interface name="velocity"는 컨트롤러가 바퀴 속도를 명령으로 내려보낸다는 뜻이고, state_interface의 velocity와 position은 하드웨어가 올려보내는 상태(바퀴 속도와 누적 각도)입니다. tiki_base의 read()는 엔코더 값을 2π/1170을 곱해 라디안으로 바꿉니다.

실기기를 연결해서 확인하기

이 절은 로봇이 있어야 하므로 4부에서 로봇을 연결한 뒤에 해도 됩니다. 지금은 건너뛰고 다음 장으로 가도 흐름이 끊기지 않습니다.

URDF(tiki_description 패키지)는 OVA의 VM이 아니라 로봇 컴퓨터에 있습니다. 로봇이 tiki.launch.py로 기동하면 robot_state_publisher가 URDF를 /robot_description 파라미터와 TF로 발행하고, VM은 네트워크로 그것을 받아서 봅니다(3.1장, 3.11장). 그래서 이 절의 명령은 로봇이 켜져 있고 VM과 ROS_DOMAIN_ID가 같은 상태에서 VM의 Tilix에서 실행합니다(4.2장, 4.4장).

먼저 RViz2에서 모델을 봅니다. RViz2의 RobotModel 디스플레이를 추가하고 Description Topic을 /robot_description으로 둡니다. Fixed Frame은 base_footprint입니다. 이 설정을 담은 tiki_description.rviz는 로봇에 있으므로, 쓰려면 로봇에서 VM으로 복사해야 합니다. 디스플레이를 직접 추가해도 됩니다(3.8장).

몸체와 LiDAR 부품은 보여야 합니다. 바퀴 링크는 조인트 위치를 알려 주는 /joint_states가 있어야 TF가 만들어집니다. 실기기에서는 joint_state_broadcaster가 이 토픽을 발행하므로 바퀴도 보이고 움직입니다. 로봇이 바퀴를 못 돌리는 상태(컨트롤러가 뜨지 않은 경우)라면 바퀴 링크에 변환이 없다는 경고가 나올 수 있습니다. 위 화면 설명은 파일 내용에서 추론한 것이고 이 교재에서 실기기로 직접 확인한 것은 아닙니다. 화면이 설명과 다르면 그대로 기록해 두세요.

다음 명령으로 TF와 모델 파일을 확인합니다. VM에는 tf2_tools와 graphviz가 설치되어 있습니다.

ros2 run tf2_tools view_frames
ros2 run tf2_ros tf2_echo base_footprint laser

view_frames는 5초쯤 TF를 듣고 frames_<날짜_시각>.pdf와 .gv 파일을 현재 폴더에 만듭니다. 열어 보면 위의 TF 트리와 같은 구조이고, 로봇이 움직이는 중이면 odom과 map 쪽 프레임이 위에 붙어 있습니다.

tf2_echo base_footprint laser는 base_footprint에서 본 laser의 위치를 계속 출력합니다. 고정 변환이라 값은 변하지 않아야 하고, 앞의 계산이 맞다면 다음과 같이 나옵니다. 정확한 줄 모양은 버전마다 조금 다릅니다.

항목기대하는 값
Translationx 0.000, y 0.000, z 0.134 (0.1337을 소수 셋째 자리로 반올림)
Quaternion (x, y, z, w)0.000, 0.000, 1.000, 0.000
RPY (radian)roll 0, pitch 0, yaw 약 3.142
RPY (degree)yaw 약 180

모델 파일 자체를 검사하는 방법도 있습니다. 실행 중인 robot_state_publisher가 들고 있는 URDF 문자열을 VM으로 받아서 check_urdf로 문법을 점검합니다. tiki_control의 런치가 robot_description을 가져올 때 쓰는 명령과 같습니다.

ros2 param get --hide-type /robot_state_publisher robot_description > /tmp/tiki.urdf
check_urdf /tmp/tiki.urdf

/tmp/tiki.urdf를 열어 보면 ${255/255} 같은 xacro 식이 모두 숫자로 바뀌어 있습니다. check_urdf는 파일을 파싱하고 링크 트리를 출력하는 도구이고, 정상이면 로봇 이름 andino, 루트 링크 base_footprint, 아래로 이어진 자식 링크가 들여쓰기로 나옵니다. OVA에는 check_urdf가 설치되어 있습니다.

원본 xacro 파일을 직접 처리해 보려면 파일이 있는 로봇에서 실행합니다. OVA의 VM에는 xacro 패키지도 tiki_description도 없습니다.

# 로봇에서 실행
ros2 run xacro xacro ~/ros2_ws/src/tiki_description/urdf/tiki.urdf.xacro > /tmp/tiki.urdf

한눈에 정리

  1. tiki.urdf.xacro는 색 4개, 링크 8개, 조인트 7개, ros2_control 블록으로 이루어지고 xacro는 ${ } 식 계산에만 쓰인다.
  2. base_footprint는 바닥 기준점이고 base_link는 그 위 0.0365 m다. 바퀴 중심 높이 0.0325 m가 바퀴 반지름과 같아 바퀴가 z = 0에 닿는다.
  3. 바퀴 간격은 0.0962 × 2 = 0.1924 m이고 tiki_controllers.yaml의 값과 같다. 두 파일의 숫자는 사람이 맞춘다.
  4. laser는 base_footprint 기준 (0, 0, 0.1337)에 yaw π로 달려 있어서 로봇 정면이 스캔 각도 π 부근이다.
  5. 이 URDF는 visual만 있어 RViz 확인에는 충분하지만 물리 시뮬레이터에는 쓸 수 없다.
  6. URDF는 로봇이 발행한다. VM에서의 확인 명령은 view_frames, tf2_echo base_footprint laser, ros2 param get ... robot_description과 check_urdf다.

확인 문제

Q1. right_wheel_joint의 origin이 x -0.0085, y -0.0962, z -0.004입니다. base_footprint 기준으로 오른쪽 바퀴 중심의 위치는 어디인가요?

base_joint의 z 0.0365를 더합니다. x -0.0085, y -0.0962, z 0.0365 - 0.004 = 0.0325이므로 (-0.0085, -0.0962, 0.0325) m입니다. z가 바퀴 반지름과 같아 바퀴가 바닥에 닿습니다.

Q2. laser 프레임에서 각도 0 방향에 장애물이 있다고 /scan이 알려 줍니다. 로봇 기준으로 어느 쪽인가요?

laser가 yaw π로 장착되어 있어서 laser의 +x는 로봇의 -x, 즉 뒤쪽입니다. 따라서 각도 0 방향의 장애물은 로봇 뒤쪽에 있습니다. 로봇 정면은 각도 π 부근입니다.

Q3. 바퀴 반지름을 0.04로 바꾼 부품으로 교체하고 URDF만 고쳤습니다. 무엇을 더 확인해야 하나요?

tiki_controllers.yaml의 wheel_radius를 같은 값으로 고쳐야 합니다. 컨트롤러는 URDF가 아니라 yaml의 값으로 오도메트리를 계산하기 때문입니다. base_joint의 높이 식(0.0325 + 0.004)과 바퀴 조인트의 z도 새 반지름에 맞는지 봐야 합니다.

다음 장

다음 장에서는 /cmd_vel의 속도가 wheel_separation과 wheel_radius를 거쳐 어떻게 두 바퀴의 속도로 바뀌는지 계산하고 컨트롤러의 속도 제한도 다룹니다.