로봇에 센서를 부착하고, 외형을 정의하기 위해 가장 기본이 되는 것이 바로 URDF입니다. 이번 시간, 실습을 통해 URDF에 대한 개념과 robot desciption을 실습해 보겠습니다.
Description | |
---|---|
Link | 단단하게 고정된 강체(rigid-body)이며, 사람의 골격에 해당합니다. |
Joint | link 사이를 결합해주고 이들 사이 운동을 결정짓습니다. 사람의 관절에 해당합니다. |
다양한 종류의 joint들이 존재하지만, 이론적으로 이들은 결국 prismatic + revolute joint의 결합으로 설명될 수 있습니다.
Description | |
---|---|
revolute joint | 회전 운동을 갖는 joint |
prismatic joint | 수평 병진 운동을 갖는 joint |
⇒ ROS 2에서는 개발 상 편의를 위해 크게 6가지의 joint를 사용하고 있습니다만 그림으로만 보고 넘어가겠습니다.
image from : Martin Androvich
⇒ URDF는 XML 문법을 사용하고 있으며 다양한 tag를 통해 로봇을 표현하게 됩니다. 예시를 통해 URDF에 대한 이해도를 가져봅시다.
<link name="base_link">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0" />
<mass value="8.3" />
<inertia ixx="5.249466E+13" ixy="-1.398065E+12" ixz="-3.158592E+12" iyy="5.786727E+13" iyz="-5.159120E+11" izz="3.114993E+13" />
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 3.1415 3.1415" />
<geometry>
<mesh filename="package://neuronbot2_description/meshes/neuronbot2/base_link.stl" scale="0.001 0.001 0.001" />
</geometry>
<material name="black" />
</visual
<collision>
<origin xyz="0 0 0.125" rpy="0 0 0" />
<geometry>
<box size="0.25 0.25 0.25" />
</geometry>
<material name="black" />
</collision>
</link>
Description | |
---|---|
inertial | 해당 link의 질량, 관성 모멘트와 같은 물성치를 포함합니다. |
visual | 로봇이 겉으로 보여지는 시각적인 요소를 설정합니다. STL과 같은 3D 모델링 파일을 사용할 수 있습니다. |
collision | visual은 겉으로 보여지는 모습일 뿐, 실제 해당 link가 자치하는 부피는 collision에서 지정됩니다. Visual과 collision을 일치시킬수록 좋기 때문에 3D 모델링 파일을 사용하기도 합니다. (종종 계산 단순화를 위해 간소화된 모델을 사용하기도 합니다.) |
<joint name="r_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="wheel_right_link"/>
<origin xyz="0.0 -0.09 0.0415" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<joint name="l_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="wheel_left_link"/>
<origin xyz="0.0 0.109 0.0415" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
Description | |
---|---|
name | joint의 이름은 후에 tf publish 시 그대로 사용되기 때문에 혼란을 야기하지 않도록 설정해야 합니다. |
type | 예시에서 사용중인 joint type은 fixed와 continuous로. fixed는 단단히 결합된 joint를, continuous는 무한히 돌아갈 수 있는 joint를 뜻합니다. |
origin | parent link의 원점을 기준으로 한 joint의 위치를 지정하게 되며, 이러한 수치는 모델링 파일을 통해 미리 조사된 이후 URDF로 변환됩니다. |
axis | 회전하는 joint의 경우 어떠한 축을 기준으로 회전되는지 설정이 필요합니다. |
parent, child | 해당 joint의 전, 후 link를 설정합니다. |
기타 속성들 | limit, dynamics, calibration, mimic, safety_controller등이 있으며, 각종 역학적인 속성을 표현합니다. |
urdf의 joint는 절대 좌표를 기준으로 하는 extrinsic 체계를 갖습니다.
Description | |
---|---|
origin | 해당 요소의 원점을 기준으로, 위치와 방향을 결정합니다. 3축 직교 좌표계를 기준으로 x,y,z 축과 roll pitch yaw 회전각을 사용하고 있습니다. |
geometry | visual과 collision의 기하학적 요소를 결정하는 태그입니다. urdf에서는 box, cylinder, sphere와 같이 단순한 도형을 제공하고 있습니다. 별도 stl 파일을 사용해도 되지만 로봇을 단순화하고 싶은 경우 이를 통해 간소화가 가능합니다. |
material | color, texture등을 지정할 수 있으며, 외향적인 디자인을 위한 요소입니다. |
URDF를 설명하긴 했는데… 실제로 와닿지 않지요? 이러한 이유로 2가지 예시를 통해 URDF에 대한 개요를 확실히 짚고 넘어가보겠습니다.
cbp basic_stick && source install/local_setup.bash
ros2 launch basic_stick description.launch.py
전에 배웠던 tf2 기억하시죠?? 까먹으면 안됩니다…
⇒ launch file은 항상 최하단부터 분석합니다. 지금 2개의 node가 실행되고 있습니다.
return LaunchDescription([
robot_state_publisher,
rviz,
])
# Get URDF via xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
PathJoinSubstitution(
[FindPackageShare("basic_stick"), "urdf", "basic_stick_desc.xacro"]
),
]
)
robot_description = {"robot_description": robot_description_content}
# Robot State Publisher
robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
output='screen',
parameters=[robot_description],
)
pkg_path = os.path.join(get_package_share_directory('basic_stick'))
rviz_config_file = os.path.join(pkg_path, 'rviz', 'desc.rviz')
# Launch RViz
rviz = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
arguments=["-d", rviz_config_file],
)
tf2 예시에서 static/dynamic tf2에 대해서 살펴보았지요? 지금 basic stick은 움직이는 부분이 없기 때문에 모든 파트가 static이라는 점을 안내드립니다.
$ ros2 topic info /robot_description
Type: std_msgs/msg/String
Publisher count: 1
Subscription count: 0
data: <?xml version="1.0" ?> <!-- =================================================================================== --> <!-- | Th...
---
URDF 스크립트를 사람이 모두 작성하기는 매우 비효율적입니다. 더불어, Gazebo에서만 사용하는 속성을 따로 분리하고 싶은 경우, 파일을 나누어 관리하고 싶을 것입니다. 이러한 욕구를 충족시키기 위해서 ROS 2는 URDF의 작성을 보다 편하게 해주는 XML Macro, Xacro를 지원하고 있습니다.
특히 xacro는 수식, 조건을 사용 가능하기 때문에 로봇 파일을 다루기 매우 용이하며, 특정 요소를 모듈화 후 재사용하는 등 효율적인 URDF 작성이 가능하도록 도와줍니다.
<?xml version="1.0"?>
<robot name="basic_stick" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:property name="PI" value="3.14159"/>
<xacro:property name="mass1" value="10" />
<xacro:property name="mass2" value="1" />
<xacro:property name="width1" value="0.1" /> <!--link_1 radius-->
<xacro:property name="width2" value="0.1" /> <!--link_2 radius-->
<xacro:property name="length0" value="0.1" /> <!--link_1 length-->
<xacro:property name="length1" value="1.5" /> <!--link_2 length-->
<xacro:property name="length2" value="0.1" /> <!--link_3 length-->
<xacro:property name="mass_camera" value="0.2" />
<!--Links-->
<link name="world"/>
<link name="link_1">
<link name="link_2">
<link name="link_1">
<visual>
<collision>
<inertial>
</link>
<!--Joints-->
<joint name="fixed_base_joint" type="fixed">
<parent link="world"/>
<child link="link_1"/>
<origin xyz="0 0 ${length1/2}" rpy="0 0 0"/>
</joint>
<joint name="pan_joint" type="fixed">
<parent link="link_1"/>
<child link="link_2"/>
<!-- <axis xyz="0 0 1"/> -->
<origin xyz="0 0 ${length1/2+length2/2}" rpy="0 0 -${pi/2}"/>
</joint>
ros2 launch basic_stick gz.launch.py
⇒ 색도 없고, 밋밋한 basic stick이 동장할 것입니다.
return LaunchDescription(
[
start_gazebo_server_cmd,
start_gazebo_client_cmd,
robot_state_publisher,
spawn_entity,
]
)
def generate_launch_description():
pkg_path = os.path.join(get_package_share_directory('basic_stick'))
pkg_gazebo_ros = FindPackageShare(package='gazebo_ros').find('gazebo_ros')
world_path = os.path.join(pkg_path, 'worlds', 'empty_world.world')
# Start Gazebo server
start_gazebo_server_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzserver.launch.py')),
launch_arguments={'world': world_path}.items()
)
# Start Gazebo client
start_gazebo_client_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzclient.launch.py'))
)
⇒ 등장시킬 시의 이름, 위치와 방향을 지정할 수 있습니다.
# Spawn Robot
spawn_entity = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=[
'-topic', 'robot_description',
'-entity', 'sensor_stick',
'-x', str(0),
'-y', str(0.0),
'-Y', str(0.0),
],
output='screen'
)
⇒ basic_stick.xacro에서 basic_stick.gazebo.xacro를 include 합니다.
<?xml version="1.0"?>
<robot>
<!--base_link-->
<gazebo reference="base_link">
<material>Gazebo/White</material>
</gazebo>
...
</robot>
<!--Import gazebo elements-->
<xacro:include filename="$(find basic_stick)/urdf/basic_stick.gazebo.xacro" />
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
PathJoinSubstitution(
[FindPackageShare("basic_stick"), "urdf", "basic_stick.xacro"]
),
]
)
Basic Stick을 통해 배운 내용들을 복습하자면
일전 sensor stick은 움직이는 파츠가 없이 정적인 모델이었습니다. 그래서 이번에는 움직이는 joint를 갖는 모바일 로봇의 예시를 살펴보겠습니다.
cbp fusionbot_description && source install/local_setup.bash
ros2 launch fusionbot_description description.launch.py
$ ros2 run tf2_tools view_frames.py
<?xml version="1.0"?>
<robot name="fusionbot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:include filename="$(find fusionbot_description)/urdf/materials.xacro" />
<!-- <xacro:include filename="$(find fusionbot_description)/urdf/fusionbot.gazebo" /> -->
<link name='base_footprint' />
<link name="base_link">
<link name="right_wheel">
...
<joint name='base_link_joint' type='fixed'>
<joint name="right_wheel_joint" type="continuous">
<joint name="left_wheel_joint" type="continuous">
...
</robot>
joint state publisher는 로봇 내 존재하는 다양한 joint 값들을 실시간으로 갱신하여 /joint_states라는 topic으로 publish 하고 tf2 broadcast도 담당합니다.
$ ros2 interface show sensor_msgs/msg/JointState
# This is a message that holds data to describe the state of a set of torque controlled joints.
#
#
# The state of each joint (revolute or prismatic) is defined by:
# * the position of the joint (rad or m),
# * the velocity of the joint (rad/s or m/s) and
# * the effort that is applied in the joint (Nm or N).
#
...
std_msgs/Header header
string[] name
float64[] position
float64[] velocity
float64[] effort
$ ros2 topic echo /joint_states
header:
stamp:
frame_id: ''
name:
- right_wheel_joint
- left_wheel_joint
position:
- 1.918884792812646
- -1.409318464400381
velocity: []
effort: []
---
⇒ 이렇게 joint state publisher는 현재 로봇이 가진 움직일 수 있는 모든 joint들을 예의주시하고 있습니다.
ros2 launch fusionbot_description gz.launch.py
현재 로봇이 움직일 수는 없습니다. 이는 다음 시간 plugin을 사용하여 구현해보겠습니다.
return LaunchDescription(
[
start_gazebo_server_cmd,
start_gazebo_client_cmd,
robot_state_publisher,
joint_state_publisher,
spawn_entity,
]
)
지금까지 배운 내용을 총정리해봅시다.
Gazebo의 특징과 기본적인 UI, 그리고 사용법을 짚고 넘어가고자 합니다.
Gazebo는 로봇공학을 위해 제작된 전용 물리 엔진 기반의 높은 3D 시뮬레이터로 ROS를 관리하는 Open Robotics에서 비롯된 시뮬레이터인 만큼 ROS와 높은 호환성을 자랑합니다. 이후 Gazebo 사용 및 오류 발생 시 디버깅을 위해, Gazebo를 구성하는 요소들을 간단하게 짚고 넘어가겠습니다.
Gazebo는 Socket-Based Communication을 갖습니다. 따라서 서버와 Client를 분리하여 실행 가능하며 다른 기기에서의 실행 후 연동도 가능합니다.
$ gzserver
$ gzserver
⇒ client만 실행하면, 연결되고 명령을 수신할 서버가 없기 때문에 (컴퓨팅 리소스를 소비하는 것을 제외하고) 아무 것도 하지 않습니다.
# Terminal1 - gazebo 실행
$ gazebo
# Terminal2 - gazebo process check
$ ps faux | grep gz
⇒ 이러한 이유로 Gazebo를 종료했다고 생각하지만 gzserver가 깔끔하게 종료되지 않아 정상 동작하던 예시에서 오류를 얻는 상황이 발생합니다.
⇒ Gazebo world file에는 로봇 모델, 환경, 조명, 센서, 다른 기타 물체들까지 시뮬레이션 환경의 모든 요소가 포함되어 있으며, 이는 일반적으로 확장자명 .world를 사용합니다. 아래 예시와 같이 Gazebo 실행 시 world 파일을 옵션으로 하여 단독 실행이 가능합니다.
$ gazebo <yourworld>.world
⇒ Gazebo의 World는 다양한 Model들로 구성됩니다. 하지만 Model 파일만으로 gazebo를 실행시킬 수는 없습니다.
⇒ 모델을 별도의 파일로 보관하는 이유는 다른 프로젝트에 재사용하기 위해서이며, 로봇의 모델 파일 또는 다른 모델을 월드 파일 내에 포함하려면 다음과 같이 태그를 통해 import 할 수 있습니다.
<include><uri>model://model_file_name</uri></include>
Gazebo의 World, Model에 장착되는 각종 센서와 제어를 위한 다양한 플러그인이 준비되어 있으며, 이러한 플러그인은 커멘드 라인에서 로드하거나 SDF 파일 내부에 추가할 수 있습니다. (자체 Plugin을 개발할 수도 있습니다.)
이번에는 Gazebo의 기본 조작 방법과 내장 Tool들을 살펴보겠습니다.
⇒ 왼쪽부터 오른쪽 순서대로 각 아이콘 별 기능을 설명해보겠습니다.
Select mode는 가장 일반적으로 사용되는 커서 모드입니다. 장면을 탐색할 수 있습니다.
커서 모드를 선택한 다음 이동시키길 원하는 객체를 클릭합니다. 이후 등장하는 3축 중 적절한 축을 사용하여 개체를 원하는 위치로 끌기만 하면 됩니다.
translate mode와 마찬가지로 이 커서 모드를 사용하면 주어진 모델의 방향을 변경할 수 있습니다.
Scale mode를 사용하면 객체의 전체 크기를 변경할 수 있습니다.
앞,뒤로 되돌리는 기능입니다.
큐브, 구체 또는 실린더와 같은 기본 3D 모델을 환경에 삽입할 수 있습니다.
스포트라이트, 포인트 라이트 또는 방향이 정해진 조명과 같은 다양한 광원을 환경에 추가합니다.
모델을 복사/붙여넣을 수 있습니다. (Ctrl+C/V를 통해서도 가능합니다.)
이 도구를 사용하면 x y z 축 중 하나를 따라 한 모형을 다른 모델과 정렬할 수 있습니다.
⇒ 또는 두 모델을 특정한 면 기반으로 서로 붙이는 것도 가능합니다.
상단 뷰, 측면 뷰, 전면 뷰, 하단 뷰와 같은 다양한 관점에서 장면을 볼 수 있습니다.
다음으로 Side Panel을 살펴보겠습니다.
현재 사용중인 조명 및 모델들이 표시됩니다. 개별 모델을 클릭하여 위치 및 방향과 같은 모델의 기본 파라미터를 보거나 편집할 수 있습니다. 또한 물리 옵션을 통해 중력 및 자기장과 같은 물성치도 변경할 수 있습니다. GUI 옵션을 사용하면 기본 카메라 뷰 각도 및 포즈에 액세스할 수 있습니다.
추가할 모델을 찾을 수 있습니다. 환경 변수에 지정된 폴더에서 모델들을 검색한 뒤 배치하는 것이 가능하며, 환경 변수 설정 없이도 Add Path 옵션을 통해 정해진 포맷을 갖는 모델을 가져올 수 있습니다.
Gazebo는 ROS 2와 호환이 가장 좋은 로봇 시뮬레이션 프로그램입니다. gazebo_ros에 대해서 살펴봅시다.
$ ros2 launch gazebo_ros gazebo.launch.py
겉보기에는 일반 gazebo 실행과 차이가 없는 것 같지만, 내부적으로 이는 큰 차이를 갖습니다. => 바로 ROS 2와 호환할 수 있는 다양한 API를 포함한 Gazebo가 실행된다는 점입니다.
$ ros2 topic list
/clock
/parameter_events
/performance_metrics
/rosout
$ ros2 service list
/apply_joint_effort
/apply_link_wrench
/clear_joint_efforts
/clear_link_wrenches
/delete_entity
/gazebo/describe_parameters
/gazebo/get_parameter_types
/gazebo/get_parameters
/gazebo/list_parameters
/gazebo/set_parameters
/gazebo/set_parameters_atomically
/get_model_list
/pause_physics
/reset_simulation
/reset_world
/spawn_entity
/unpause_physics
# Start Gazebo server
start_gazebo_server_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzserver.launch.py')),
launch_arguments={'world': world_path}.items()
)
# Start Gazebo client
start_gazebo_client_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzclient.launch.py'))
)
⇒ server와 client를 나눈 이유는 server단에서 world 파일이 전달되기 때문입니다.
Topic Name | Description |
---|---|
/clock | Gazebo 자체의 시간으로 시뮬레이션이 시작되는 시점이 0초가 됩니다. |
Service Name | Description |
---|---|
/delete_entity | 모델을 등장시킵니다. |
/spawn_entity | 존재하는 모델을 제거합니다. |
/get_model_list | 전체 모델 리스트를 조회합니다. |
/pause_physics | 중력, 바람 등 물리량들을 모두 정지시킵니다. |
/unpause_physics | 물리량들을 다시 시작합니다. |
/reset_simulation | World 전체를 Reset |
/reset_world | World내 Model Pose를 Reset |
├─ wall
├── model.config
└── model.sdf
Description | |
---|---|
model.config | 해당 model의 이름, 작성자 등 기본적인 정보들이 기입됩니다. |
model.sdf | 실직적인 sdf 형식의 모델이 위치합니다. |
이렇게 생성한 물체를 Gazebo에서 두고두고 사용하는 몇가지 방법들이 있습니다.
$ gedit ~/.gazebo/gui.ini
[geometry]
x=0
y=0
[model_paths]
filenames=/home/kimsooyoung/<your-folder-location>
대신 이 방법을 사용하면, 해당 launch file을 사용해야 GAZEBO_MODEL_PATH가 반영됩니다.
if 'GAZEBO_MODEL_PATH' in os.environ:
os.environ['GAZEBO_MODEL_PATH'] += ":" + gazebo_model_path
else:
os.environ['GAZEBO_MODEL_PATH'] = gazebo_model_path
이번 시간에는 오픈소스 데이터셋 3DGEMS를 사용하여 나만의 World를 꾸미는 예시를 진행해보겠습니다. => 📁 3DGEMS.zip
WSL2를 사용하시는 분들께서는 터미널에서 explorer.exe . 를 입력하면 윈도우 파일 탐색기를 실행 가능합니다.
잠시 시간을 갖고 나만의 building과 model을 추가하여 나만의 world를 만들어 봅시다!
<sdf version='1.7'>
<world name='default'>
<include>
<uri>model://ground_plane</uri>
</include>
<include>
<uri>model://sun</uri>
</include>
</world>
</sdf>
⇒ world 파일은 SDF라는 포멧을 사용하여 작성되었으며, 이는 Gazebo에서 쓰이는 독특한 문법입니다.
pkg_path = os.path.join(get_package_share_directory('my_world'))
# world_path = os.path.join(pkg_path, 'worlds', <your-world-file>)
# Start Gazebo server
start_gazebo_server_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzserver.launch.py')),
launch_arguments={'world': world_path}.items()
)
# Start Gazebo client
start_gazebo_client_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzclient.launch.py'))
)
colcon build --packages-select src_gazebo
source install/local_setup.bash
ros2 launch my_world template.py
world 제작 시 사용된 SDF 포멧은 Blender라는 프로그램에서 export 할 수 있어 많은 사람들이 사용하고 있습니다. 우리는 이제 world 파일을 볼 수 있으니 Github에 있는 수많은 예시들을 가져다 쓸 수 있게 된 것입니다!