9515ecb79ae04f44a42e412c7ac.../doc.md

1146 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Introduction to Gazebo Sim with ROS2
卡片名称Gazebo中使用URDF创建机器人
描述:学习如何使用 URDF 语言在 Gazebo 仿真中创建机器人,最终完成一个可在 Gazebo 中正确加载和验证的移动机器人模型。
## 目录
- 1 简介
- 2 Gazebo Sim、Ignition Gazebo 与 Gazebo Classic 对比
- 3 为什么使用 URDF 而不是 SDF
- 4 创建移动机器人
- 5 连杆与关节
- 6 网格模型
- 7 使用 Gazebo Sim 服务生成机器人
- 8 使用 ROS2 Launch 文件生成机器人
- 9 总结
<div style="page-break-after: always;"></div>
## 1 简介
预计完成时间:**30 秒**
在本单元中,你将先梳理 **Gazebo Sim**、**Ignition Gazebo** 与 **Gazebo Classic** 的关系,再学习如何使用 **URDF** 语言为 Gazebo 仿真创建机器人。后续内容会逐步覆盖连杆、关节、网格模型以及机器人生成方式,最终完成一个可在 Gazebo 中正确加载和验证的移动机器人模型。
<div style="page-break-after: always;"></div>
## 2 Gazebo Sim、Ignition Gazebo 与 Gazebo Classic 对比
预计完成时间:**5 分钟**
### 什么是 Gazebo Sim
**Gazebo Sim** 是一个 3D 动态模拟器,能够在复杂的室内和室外环境中准确高效地模拟机器人群体。虽然与游戏引擎类似,但 **Gazebo Sim** 提供了更高保真度的物理模拟、一套完整的传感器系统,以及面向用户和程序的接口。
**开源机器人基金会OSRF** 开发并维护 **Gazebo Sim**
### 什么是 Ignition Gazebo
**Gazebo Sim** 前身被称为 **Ignition Gazebo**。2022 年 4 月Open Robotics 团队宣布弃用 **Ignition Gazebo** 这一名称,改用 **Gazebo Sim**。从 **Garden** 版本开始,**Ignition Gazebo** 已经完成重新品牌化,本课程也基于 **Garden** 版本构建,因此统一使用新的 **Gazebo Sim** 品牌和以 `gz` 开头的命令行工具。
例如,本课程里会使用 `gz model --list``gz topic -t /chatter "Hello!"` 这样的命令。如果你计划使用较早的 **Fortress** 版本,则需要把 `gz` 命令替换成 `ign` 命令。
**参考资料:** [https://www.openrobotics.org/blog/2022/4/6/a-new-era-for-gazebo](https://www.openrobotics.org/blog/2022/4/6/a-new-era-for-gazebo)
### Gazebo Sim 对机器人开发者有什么帮助?
**Gazebo Sim** 允许你在真实环境的动态模拟中测试机器人和算法。你可以选择多种物理引擎、高级 3D 图形、传感器及噪声模型、机器人插件以及现成的机器人模型库。此外,它运行在 TCP/IP 传输层之上,这使得网络集成变得简单,就像 ROS2 一样。
虽然 **Gazebo Classic**(版本 11曾是 ROS 的默认模拟器,但它已经进入生命周期末期,且不适用于 `Humble` 之后的新版本 ROS2。新的 **Gazebo Sim** 会逐步取代 **Gazebo Classic**
#### 可视化和调试
运行模拟可以提供你所需的关于环境和机器人的所有信息。你甚至可以调试软件和模拟本身,定位问题发生的位置。如果你想测试某种特定情况,也可以强制设定某个场景,反复运行软件直到获得足够的信息。
#### "快速失败,更快学习"
你不需要在真实机器人上测试软件来发现问题和调整参数。在模拟机器人上失败更快、成本更低,也能比在真实机器人上试错更快地改进软件。
#### 构建机器人太昂贵了!
每个人都想在真实机器人上测试算法,但现实是拥有一台真实机器人太昂贵了。不仅购买构建机器人所需的全部组件很贵,把所有部件组装并协调工作也非常耗时,同时你还需要处理机械、电子和软件等多个方面的问题。
#### 社区支持
**Gazebo Sim** 是开源软件,由 **OSRF** 维护。它拥有一个充满活力的社区,鼓励用户使用、改进并彼此提供支持。
### Gazebo Sim 与 Gazebo Classic 的比较
Gazebo Sim 为仿真带来了新的工作方式,提供了一整套开发库和云服务工具来简化模拟流程。
它可以帮助你利用高保真的传感器数据流,在接近真实的环境中快速迭代物理设计;你也可以在安全环境下测试控制策略,并把模拟充分用在持续集成测试中。
虽然 **Gazebo Classic** 的大多数功能已经在 **Gazebo Sim** 中实现但仍有部分功能尚未完全补齐。例如Gazebo Sim 中长期被提及的一个问题是没有完整的“缩放模式”,模型暂时无法像 Classic 那样直接缩放。
**参考资料:** [https://gazebosim.org/docs/fortress/comparison](https://gazebosim.org/docs/fortress/comparison)
在场景渲染方面,两者差异比较明显。**Gazebo Classic** 的画面更暗,在相同光照条件下阴影不够清晰;相比之下,**Gazebo Sim** 能更好地渲染亮度、光照和阴影细节。
| Gazebo Classic | Gazebo Sim |
| --- | --- |
| ![Gazebo Classic Scene](assets/unit1/01-gazebo-classic-scene.png) | ![Gazebo Sim Scene](assets/unit1/02-gazebo-sim-scene.png) |
<div style="page-break-after: always;"></div>
## 3 为什么使用 URDF 而不是 SDF
预计完成时间:**30 秒**
Gazebo Sim 使用 **SDF** 格式来描述**世界**、**模型**、**机器人**以及构建完整仿真所需的其他组件。那么,为什么这里要用 **URDF** 来构建机器人呢?
**ROS2** 的许多工具都是围绕 **URDF** 构建的,例如 **RVIZ2**。当你在 Gazebo 仿真中生成一个用 URDF 定义的机器人时Gazebo Sim 会在底层自动把 URDF 文件转换为 SDF。
在下一单元中,当你把机器人生成到 Gazebo 仿真里并让它与 ROS2 配合工作时,会进一步看到这一点。
<div style="page-break-after: always;"></div>
## 4 创建移动机器人
预计完成时间:**7 分钟**
要创建一个新的机器人模型,你需要按照特定的目录结构来组织机器人的定义文件和文件夹。
除了机器人的定义文件,也就是 **URDF** 文件之外,你还需要使用 launch 文件来调试机器人的创建过程。
创建一个功能包,并在其中新建以下文件夹:
**在 Web Shell 1 中执行**
```
cd /workspace/src
```
```
ros2 pkg create robot_description --build-type ament_python --dependencies rclpy urdf xacro
```
```
mkdir -p robot_description/urdf
```
```
touch robot_description/urdf/robot.urdf
```
**URDF** 文件夹将用于存放机器人的描述文件。
将下面的内容粘贴到 **robot.urdf** 文件中:
**robot.urdf**
```
<?xml version="1.0"?>
<robot name="robot">
<!-- Link - chassis -->
<link name="link_chassis">
<inertial>
<mass value="10" />
<origin xyz="0 0 0.3" rpy="0 0 0" />
<inertia ixx="1.5417" ixy="0" ixz="0" iyy="3.467" iyz="0" izz="4.742" />
</inertial>
<collision>
<geometry>
<box size="2 1.3 0.4" />
</geometry>
</collision>
<visual>
<geometry>
<box size="2 1.3 0.4" />
</geometry>
<material name="Red">
<color rgba="1 0 0 1" />
</material>
</visual>
</link>
</robot>
```
使用下面的命令在该功能包中创建一个新的 launch 文件:
**在 Web Shell 1 中执行**
```
cd /workspace/src
```
```
mkdir -p robot_description/launch
```
```
touch robot_description/launch/rviz.launch.py
```
**launch** 文件夹将用于存放使用 **RVIZ2** 调试该机器人模型所需的 launch 文件。
复制并粘贴以下内容:
**rviz.launch.py**
```
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.substitutions import Command
from launch_ros.actions import Node
# ROS2 Launch System will look for this function definition #
def generate_launch_description():
# Get Package Description and Directory #
package_description = "robot_description"
package_directory = get_package_share_directory(package_description)
# Load URDF File #
urdf_file = 'robot.urdf'
robot_desc_path = os.path.join(package_directory, "urdf", urdf_file)
print("URDF Loaded !")
# Robot State Publisher (RSP) #
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher_node',
output="screen",
emulate_tty=True,
parameters=[{'use_sim_time': True,
'robot_description': Command(['xacro ', robot_desc_path])}]
)
# RViz2 Launch Configuration (RViz) #
rviz_node = Node(
package='rviz2',
executable='rviz2',
name='rviz2_node',
output='screen',
emulate_tty=True,
parameters=[{'use_sim_time': True}],
)
# Create and Return the Launch Description Object #
return LaunchDescription(
[
robot_state_publisher_node,
rviz_node,
]
)
```
修改 `setup.py` 文件,确保 ROS2 Launch 系统能够检测并执行你在上面步骤中创建的 launch 文件。
将下面的内容复制到 `setup.py` 文件中,直接替换原有内容。
**setup.py**
```
import os
from glob import glob
from setuptools import setup
package_name = 'robot_description'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='user',
maintainer_email='user@todo.todo',
description='TODO: Package description',
license='TODO: License declaration',
entry_points={
'console_scripts': [
],
},
)
```
新旧文件之间真正不同的只有下面这几行:
```
# imports
import os
from glob import glob
...
# paths to urdf and launch files that must be copied to 'share' folder
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
```
在平台右上角点击“分割右侧”和“远程桌面”,并等待连接。
![RViz](assets/unit1/03-1.png)
按照下面的命令来启动你刚刚创建的文件:
**在 Web Shell 1 中执行**
```
cd /workspace
```
```
colcon build --packages-select robot_description && source install/setup.bash
```
```
ros2 launch robot_description rviz.launch.py
```
启动该文件后,等待**图形化工具**打开。你应该会看到下图所示的结果。然后,在显示面板中添加 **RobotModel**,并将 `/robot_description` 设为**描述主题**。接着,把 RVIZ2 中的**固定坐标系**改为 **link_chassis**。这样就会把机器人的底盘连杆设为 **RVIZ2** 模型可视化的参考坐标系。
![RViz URDF First Launch](assets/unit1/03-rviz-urdf-first-launch.gif)
把这个配置保存在你的工作区中,这样下次启动 RVIZ2 时,机器人模型就会被正确显示。
按照下面的命令来创建rviz目录
**新建终端,在 Web Shell 2 中执行**
```
cd /workspace/src
```
```
mkdir -p robot_description/rviz
```
在rviz2界面使用“ctrl+shift+s“保存当前的 RVIZ2 配置到正确的工作目录下。
![RViz](assets/unit1/03-2.png)
将当前的 RVIZ2 配置保存为 `config.rviz`,并放到你的 ROS2 `robot_description` 功能包中新建的 `rviz` 目录里。
`rviz` 文件夹添加到 `setup.py` 中,这样下次编译时就能检测到 `config.rviz`
此时你的 `setup.py` 文件应该包含下面这三行:
```
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'rviz'), glob('rviz/*.rviz')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
```
最后,把 `config.rviz` 文件作为参数传给 launch 文件中的 RVIZ 节点。
```
# Load RViz Configuration File #
rviz_config_file = "config.rviz"
rviz_config_path = os.path.join(package_directory, "rviz", rviz_config_file)
print("RViz Config Loaded !")
# Rviz2 Launch Configuration (RViz)#
rviz_node = Node(
package='rviz2',
executable='rviz2',
name='rviz2_node',
output='screen',
emulate_tty=True,
parameters=[{'use_sim_time': True}],
arguments=['-d', rviz_config_path],
)
```
现在你的 launch 文件应该如下所示:
**rviz.launch.py**
```
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.substitutions import Command
from launch_ros.actions import Node
# ROS2 Launch System will look for this function definition #
def generate_launch_description():
# Get Package Description and Directory #
package_description = "robot_description"
package_directory = get_package_share_directory(package_description)
# Load URDF File #
urdf_file = 'robot.urdf'
robot_desc_path = os.path.join(package_directory, "urdf", urdf_file)
print("URDF Loaded !")
# Robot State Publisher (RSP) #
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher_node',
output="screen",
emulate_tty=True,
parameters=[{'use_sim_time': True,
'robot_description': Command(['xacro ', robot_desc_path])}]
)
# Load RViz Configuration File #
rviz_config_file = "config.rviz"
rviz_config_path = os.path.join(package_directory, "rviz", rviz_config_file)
print("RViz Config Loaded !")
# Rviz2 Launch Configuration (RViz)#
rviz_node = Node(
package='rviz2',
executable='rviz2',
name='rviz2_node',
output='screen',
emulate_tty=True,
parameters=[{'use_sim_time': True}],
arguments=['-d', rviz_config_path],
)
# Create and Return the Launch Description Object #
return LaunchDescription(
[
robot_state_publisher_node,
rviz_node,
]
)
```
<div style="page-break-after: always;"></div>
## 5 连杆与关节
预计完成时间:**15 分钟**
### 连杆
你已经创建了第一个只有单一元素的机器人模型,也就是一个连杆。那么,一个连杆由哪些部分组成呢?
每个连杆都必须包含以下属性:
##### 惯性属性
它包含 **质量kg**、**原点m** 和 **惯性矩阵kg·m²**
```
<inertial>
<mass value="10" />
<origin
xyz="0 0 0.3"
rpy="0 0 0" />
<inertia
ixx="1.5417"
ixy="0"
ixz="0"
iyy="3.467"
iyz="0"
izz="4.742" />
</inertial>
```
##### 碰撞与可视属性
在这个例子中,**collision** 和 **visual** 看起来是相同的,但它们的用途不同。模拟器使用 **collision** 来计算连杆之间的物理交互;**visual** 仅用于渲染显示,不会影响连杆与其他对象的交互方式。
如果你的可视网格非常复杂,但又不希望在物理交互上做不必要的高强度计算,或者想节省算力,就可以让 **collision****visual** 使用不同的几何定义。
```
<collision>
<geometry>
<box size="2 1.3 0.4" />
</geometry>
</collision>
<visual>
<geometry>
<box size="2 1.3 0.4" />
</geometry>
</visual>
```
### 关节
关节用于连接各个连杆,并在它们之间建立运动关系。在本单元中,你要构建的是移动机器人,因此接下来要给你创建的底盘加上一只车轮。
继续在 **robot.urdf** 文件中,在 'link' 结束标签之后添加以下代码:
**复制并粘贴**
```
<!-- Joint - chassis / left wheel -->
<joint name="joint_chassis_left_wheel" type="continuous">
<origin rpy="0 0 0" xyz="-0.5 0.65 0" />
<child link="link_left_wheel" />
<parent link="link_chassis" />
<axis rpy="0 0 0" xyz="0 1 0" />
<limit effort="10000" velocity="1000" />
<joint_properties damping="1.0" friction="1.0" />
</joint>
<!-- Link - left wheel -->
<link name="link_left_wheel">
<inertial>
<mass value="1" />
<origin xyz="0 0 0" rpy="0 0 0" />
<inertia ixx="0.002526666666667" ixy="0" ixz="0" iyy="0.002526666666667" iyz="0" izz="0.005"/>
</inertial>
<collision>
<origin rpy="0 0 1.5707" xyz="0 0.18 0" />
<geometry>
<cylinder length="0.12" radius="0.4"/>
</geometry>
</collision>
<visual>
<origin rpy="0 0 1.5707" xyz="0 0.18 0" />
<geometry>
<cylinder length="0.12" radius="0.4"/>
</geometry>
</visual>
<collision>
<origin rpy="0 0 1.5707" xyz="0 0.06 0" />
<geometry>
<cylinder length="0.12" radius="0.08"/>
</geometry>
</collision>
<visual>
<origin rpy="0 0 1.5707" xyz="0 0.06 0" />
<geometry>
<cylinder length="0.12" radius="0.08"/>
</geometry>
</visual>
</link>
```
### 车轮连杆定义
这里你有两组 **collision****visual** 标签,它们各自对应不同的部分。一个连杆中可以包含多组这样的标签,数量并不受限。它们都会按照关节的位置关系显示出来。
在这个例子里,你分别为车轮本体和它的轮轴创建了一组可视与碰撞定义。
重新启动 **rviz.launch.py** 文件。
**在 Web Shell 1 中执行**
```
cd /workspace
```
```
colcon build --packages-select robot_description && source install/setup.bash
```
```
ros2 launch robot_description rviz.launch.py
```
你应该得到如下结果:
![RViz URDF Links Launch](assets/unit1/04-no-wheel1.png)
但是等等……车轮去哪了?这里发生了什么?
这是因为你当前只运行了 **Robot State Publisher** 和 RViz。这个节点只会发布机器人**固定关节**的状态(这里是底盘),不会发布**可动关节**的状态(例如车轮)。如果想把这些可动关节显示出来,你还需要一个能够提供关节状态的节点,这就是 **Joint State Publisher GUI** 的作用。
要启动 **Joint State Publisher GUI**,请在一个新的 web shell 中运行以下命令:
**在 Web Shell 2 中执行**
```
ros2 run joint_state_publisher_gui joint_state_publisher_gui
```
**提示:** 如果无法运行请先执行:
```
sudo apt update
```
```
sudo apt install ros-humble-joint-state-publisher-gui
```
```
unset PYTHONPATH
```
```
source /opt/ros/humble/setup.bash
```
现在你会看到类似下面的界面:
![RViz URDF Links Launch](assets/unit1/05-rotate-wheel1.gif)
注意,现在你已经可以通过滑块来控制车轮旋转了。
当然,这还不是车轮期望的安装位置或旋转方式。那么问题出在哪里呢?
### 关节定义
来看一下关节定义:
```
<joint name="joint_chassis_left_wheel" type="continuous">
<origin rpy="0 0 0" xyz="-0.5 0.88 0" />
<child link="link_left_wheel" />
<parent link="link_chassis" />
<axis rpy="0 0 0" xyz="0 1 0" />
<limit effort="10000" velocity="1000" />
<joint_properties damping="1.0" friction="1.0" />
</joint>
```
第一个属性 **type** 用来定义该连杆能够进行哪一种运动。常见类型包括 **continuous**、**revolute**、**prismatic** 和 **fixed**。除此之外还有其他类型,你可以在这里查看完整参考:[http://wiki.ros.org/urdf/XML/joint](http://wiki.ros.org/urdf/XML/joint)。
**origin** 属性用于定义关节相对于底盘的位置。底盘连杆起始于 **0 0 0**,长度为 2 米,宽度为 1.3 米。底盘会向两侧延伸,因此它的侧边界位于 **1.3/2**,也就是 **0.65 米** 的位置。
接着,需要定义 **child****parent** 连杆。再比如 **axis**,它用来指定转动轴。你还可以通过 **limit** 设置最大力矩和速度,不过现在这部分并不是重点。当前阶段你主要是在设计机器人的几何结构,而与运动动力学相关的 **joint properties** 会在后续再讨论。
最后来看 **axis** 定义。这里决定该连杆绕哪一个坐标轴旋转。在这个例子中,你希望它绕 Y 轴旋转,这个设置是正确的。
### 修正连杆定义以匹配关节旋转
问题在于,你的这个 **link** 圆柱几何体(长度和半径)是按照 Z 轴方向定义的,因此需要绕 X 轴旋转一下才能修正。
回到左轮的连杆定义,把 **collision****visual** 标签中的 **origin** 属性改成绕 X 轴旋转 90 度:
```
<link name="link_left_wheel">
...
<collision>
<origin rpy="1.5707 0 0" xyz="0 0.18 0" />
<geometry>
<cylinder length="0.12" radius="0.4"/>
</geometry>
</collision>
<visual>
<origin rpy="1.5707 0 0" xyz="0 0.18 0" />
<geometry>
<cylinder length="0.12" radius="0.4"/>
</geometry>
</visual>
<collision>
<origin rpy="1.5707 0 0" xyz="0 0.06 0" />
<geometry>
<cylinder length="0.12" radius="0.08"/>
</geometry>
</collision>
<visual>
<origin rpy="1.5707 0 0" xyz="0 0.06 0" />
<geometry>
<cylinder length="0.12" radius="0.08"/>
</geometry>
</visual>
...
```
重新启动 **rviz.launch**,并同时运行 **Joint State Publisher GUI**
**在 Web Shell 1 中执行**
```
cd /workspace
```
```
colcon build --packages-select robot_description && source install/setup.bash
```
```
ros2 launch robot_description rviz.launch.py
```
**在 Web Shell 2 中执行**
```
unset PYTHONPATH
source /opt/ros/humble/setup.bash
ros2 run joint_state_publisher_gui joint_state_publisher_gui
```
![RViz URDF Links Joints Launch](assets/unit1/06-rviz-urdf-links-joints-launch.gif)
查看下图中的机器人尺寸,你会更容易理解为什么 XML 定义里使用的是这些数值:
![Robot Dimensions](assets/unit1/07-robot-dimensions.png)
**练习 2.5.1**
完善当前模型并把它补全!
在这个练习中,再补充以下几个部分,让机器人更完整:
- 添加**右侧车轮**
- 给两个车轮都添加新的 **visual** 元素,让旋转效果更明显(例如轮毂盖)
- 在机器人前方添加一个通过 **fixed** 关节连接的**脚轮**
- 修改并补充底盘和车轮的**材质颜色**,让它看起来尽量接近下图
![RViz URDF Ex22 Launch](assets/unit1/08-rviz-urdf-ex22-launch.gif)
请参考下图来创建脚轮的固定关节以及可视部件。
![Robot Dimensions Extended](assets/unit1/09-robot-dimensions-extended.png)
**注意:** 当前 ROS2 RVIZ2 中存在一个 BugURDF 中父连杆的颜色会传递到子连杆上,因此 RVIZ 中显示的颜色可能与 URDF 文件中的定义并不完全一致。更多信息请参考 RVIZ2 的[颜色 Bug](https://github.com/ros-visualization/rviz/issues/843)。
**练习 2.5.1 结束**
<div style="page-break-after: always;"></div>
**练习 2.5.1 参考答案**
在你自己尝试完成练习之前,不要先看答案。完成之后再回头对照参考答案,会更有助于比较和检查自己的实现。
如果你在完成练习时遇到问题,可以到论坛寻求支持。
[robot.urdf](assets/files/robot1.urdf)
**练习 2.5.1 参考答案结束**
#### 参考资料
你可以在下面查看连杆和关节的完整规范(链接指向 ROS1 文档):
- [http://wiki.ros.org/urdf/XML/link](http://wiki.ros.org/urdf/XML/link)
- [http://wiki.ros.org/urdf/XML/joint](http://wiki.ros.org/urdf/XML/joint)
在使用 XML 为 ROS2 定义机器人模型时,这些说明依然适用。
<div style="page-break-after: always;"></div>
## 6 网格模型
预计完成时间:**10 分钟**
网格模型可以用来创建更复杂的三维几何形状。设想一下,你手上有一台真实机器人,整个项目是用 CAD 软件完成的现在你想对它进行仿真那么你通常会直接拥有模型文件。URDF 支持 **.stl** 和 **.dae** 文件,其中后者通常能提供更好的显示效果。
下面来看如何在你当前制作的机器人中使用网格模型。
进入底盘的 **visual** 部分,并将下面这段代码替换掉:
```
<geometry>
<box size="2 1.3 0.4" />
</geometry>
```
替换为以下内容:
**复制并粘贴**
```
<geometry>
<mesh filename="package://robot_description/meshes/chassis.stl" />
</geometry>
```
URDF 支持 `package://` 这种写法,可以通过相对路径更方便地找到文件。这也意味着你的功能包中必须有一个 `meshes` 文件夹。先创建这个文件夹,然后上传下面的文件:
**新建终端,在 Web Shell 3 中执行**
```
mkdir -p /workspace/src/robot_description/meshes/
```
将这个 [chassis.stl](assets/files/chassis.stl) 文件放进你在 `robot_description` 功能包里创建的 `meshes` 文件夹。
`meshes` 文件夹添加到 `setup.py` 中,这样下次编译时就能检测到 `*.stl` 文件。
此时你的 `setup.py` 文件应该包含下面这四行:
```
(os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')),
(os.path.join('share', package_name, 'rviz'), glob('rviz/*.rviz')),
(os.path.join('share', package_name, 'meshes'), glob('meshes/*.stl')),
(os.path.join('share', package_name, 'launch'), glob('launch/*.launch.*')),
```
重新启动 **rviz.launch****Joint State Publisher GUI**,你应该看到如下结果:
![Robot Chassis Mesh](assets/unit1/11-robot-chassis-mesh.png)
注意,这个 **.stl** 文件会沿用基础几何体所设置的材质颜色。
**练习 2.6.1**
为车轮也添加一个网格模型文件。
使用下面这个文件:
- [wheel.stl](assets/files/wheel.stl)
调整轮毂盖的可视效果,让机器人尽量接近下图所示的样子:
![Robot Wheel Mesh](assets/unit1/12-robot-wheel-mesh.png)
**练习 2.6.1 结束**
<div style="page-break-after: always;"></div>
**练习 2.6.1 参考答案**
在自己尝试完成练习之前,不要先看答案。完成后再查看参考答案,会更方便你对比自己的实现。
如果你在完成练习时遇到问题,可以到论坛寻求支持。
[robot.urdf](assets/files/robot2.urdf)
**练习 2.6.1 参考答案结束**
<div style="page-break-after: always;"></div>
## 7 使用 Gazebo Sim 服务生成机器人
预计完成时间:**5 分钟**
你已经把机器人搭建好了,它已经有了底盘和车轮,因此已经可以用于仿真了。下面来看看如何把它放进 Gazebo 仿真环境中。
你可以通过两种方式把机器人模型生成到 **Gazebo Sim** 中:
1. 使用 Gazebo Sim 生成服务
2. 使用 ROS 2 Launch 文件
#### 使用 Gazebo Sim 生成服务
虽然这不是把模型生成到 Gazebo Sim 中的首选方法,但你仍然可以借此观察模型在 Gazebo Sim 中生成后的行为。
这种方式会使用 Gazebo Sim 的 `create` 服务,它采用的是 **EntityFactory** 消息类型。
**注意:**
开始之前,请关闭并终止所有 web shell 中正在运行的任务。对所有活跃的 web shell 按下 `Ctrl + C` 来停止当前进程。
修改“远程桌面”面板,切换到“仿真器”。
![gazebo](assets/unit1/12-1.png)
首先需要设置环境变量,为 Gazebo Sim 指定资源路径。
把模型的 URDF 文件路径以及相关网格文件路径都配置进去。
**在 Web Shell 1 中执行**
```
export GZ_SIM_RESOURCE_PATH=/workspace/src/robot_description/urdf:/workspace/src
```
现在以无头模式启动 Gazebo Sim 世界,这里我们使用空世界。
**在 Web Shell 1 中执行**
```
gz sim -s -r empty.sdf
```
**在 Web Shell 2 中执行**
```
bash /workspace/src/run_websocket.sh
```
在另一个终端中,使用下面的命令列出所有可用的服务:
**在 Web Shell 3 中执行**
```
gz service --list
```
执行完上面的命令后,你会在仿真器里看到加载的空世界,并在输出中看到一个名为 `/world/empty/create` 的服务。通用的服务名称格式为 `/world/<name_of_world>/create`。例如,如果你的 world 文件中的世界名称是 `factory_floor`,那么服务名就会是 `/world/factory_floor/create`
继续在同一个终端中,查看该服务的信息,以确认请求和响应所使用的消息类型。
**注意:** 和 ROS2 命令不同,这个命令不支持 Tab 自动补全,所以需要手动输入完整命令。
**在 Web Shell 3 中执行**
```
gz service --info --service /world/empty/create
```
执行完该命令后,你应该会看到与下图类似的输出:
```
Service providers [Address, Request Message Type, Response Message Type]:
tcp://192.168.32.7:37923, gz.msgs.EntityFactory, gz.msgs.Boolean
```
然后在同一个终端中输入下面的命令,将你的 URDF 模型生成到 **Gazebo Sim** 中:
**注意:** 和 ROS2 命令不同,这个命令不支持 Tab 自动补全,所以需要手动输入完整命令。
**在 Web Shell 3 中执行**
```
cd /workspace/src/robot_description/urdf
```
```
gz service --service /world/empty/create \
--reqtype gz.msgs.EntityFactory \
--reptype gz.msgs.Boolean \
--timeout 1000 \
--req 'sdf_filename: "robot.urdf" name: "my_robot"'
```
**注意:**`--req` 参数中的内容**不能**包含空格。如果你写入了空格,就会收到下面这样的报错:
`The following arguments were not expected: "my_robot" name: "robot.urdf",`
如果你正确完成了前面的步骤,那么执行生成服务命令后应该会返回 `data: true`
查看 Gazebo Sim 界面时,你会发现机器人模型已经生成在 `x = 0.0, y = 0.0, and z = 0.0` 的位置。
<div style="page-break-after: always;"></div>
## 8 使用 ROS2 Launch 文件生成机器人
预计完成时间:**10 分钟**
这是把模型生成到 Gazebo Sim 中最推荐的方法。通过这种可控的生成方式,你可以在需要时自动把模型插入 Gazebo Sim。
**launch** 文件夹中创建一个新文件 **empty_world.launch.py**,并写入相应内容。
**在 Web Shell 3 中执行**
```
touch /workspace/src/robot_description/launch/empty_world.launch.py
```
现在,把下面的代码复制到你刚刚创建的 launch 文件中。
**empty_world.launch.py**
```
import os
from ament_index_python.packages import (get_package_prefix, get_package_share_directory)
from launch import LaunchDescription
from launch.actions import (DeclareLaunchArgument, IncludeLaunchDescription)
from launch.substitutions import (PathJoinSubstitution, LaunchConfiguration)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import SetParameter
# ROS2 Launch System will look for this function definition #
def generate_launch_description():
# Get Package Description and Directory #
package_description = "robot_description"
package_directory = get_package_share_directory(package_description)
# Set the path to robot resources before launching Gazebo Sim #
install_dir_path = (get_package_prefix(package_description) + "/share")
robot_meshes_path = os.path.join(package_directory, "meshes")
gazebo_resource_paths = [install_dir_path, robot_meshes_path]
if "GZ_SIM_RESOURCE_PATH" in os.environ:
for resource_path in gazebo_resource_paths:
if resource_path not in os.environ["GZ_SIM_RESOURCE_PATH"]:
os.environ["GZ_SIM_RESOURCE_PATH"] += (':' + resource_path)
else:
os.environ["GZ_SIM_RESOURCE_PATH"] = (':'.join(gazebo_resource_paths))
# Load the empty world in headless server mode #
world_file = "empty.sdf"
world_config = LaunchConfiguration("world")
declare_world_arg = DeclareLaunchArgument(
"world",
default_value=["-s -r ", world_file],
description="Gazebo Sim world and startup flags",
)
# Declare GazeboSim Launch #
gzsim_pkg = get_package_share_directory("ros_gz_sim")
gz_sim = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([gzsim_pkg, "launch", "gz_sim.launch.py"])),
launch_arguments={"gz_args": world_config}.items(),
)
# Create and Return the Launch Description Object #
return LaunchDescription(
[
declare_world_arg,
# Sets use_sim_time for all nodes started below (doesn't work for nodes started from ignition gazebo) #
SetParameter(name="use_sim_time", value=True),
gz_sim,
]
)
```
启动仿真:
**在 Web Shell 1 中执行**
```
cd /workspace
```
```
colcon build --packages-select robot_description && source install/setup.bash
```
```
ros2 launch robot_description empty_world.launch.py
```
**在 Web Shell 2 中执行**
```
bash /workspace/src/run_websocket.sh
```
等待仿真器界面刷新,你现在启动的是一个空世界。建议把生成机器人的逻辑放到单独的文件中,这样在开发过程中就可以不重启整个仿真,而是直接删除并重新生成机器人。
在launch目录下新建一个 **spawn.launch.py** 文件,并填入下面的内容:
**在 Web Shell 3 中执行**
```
touch src/robot_description/launch/spawn.launch.py
```
**spawn.launch.py**
```
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import (Command, LaunchConfiguration)
from launch_ros.actions import (Node, SetParameter)
# ROS2 Launch System will look for this function definition #
def generate_launch_description():
# Get Package Description and Directory #
package_description = "robot_description"
package_directory = get_package_share_directory(package_description)
# Load URDF File #
urdf_file = 'robot.urdf'
robot_desc_path = os.path.join(package_directory, "urdf", urdf_file)
print("URDF Loaded !")
# Robot State Publisher (RSP) #
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher_node',
output="screen",
emulate_tty=True,
parameters=[{'use_sim_time': True,
'robot_description': Command(['xacro ', robot_desc_path])}]
)
# Spawn the Robot #
declare_spawn_x = DeclareLaunchArgument("x", default_value="0.0",
description="Model Spawn X Axis Value")
declare_spawn_y = DeclareLaunchArgument("y", default_value="0.0",
description="Model Spawn Y Axis Value")
declare_spawn_z = DeclareLaunchArgument("z", default_value="0.5",
description="Model Spawn Z Axis Value")
gz_spawn_entity = Node(
package="ros_gz_sim",
executable="create",
name="my_robot_spawn",
arguments=[
"-name", "my_robot",
"-allow_renaming", "true",
"-topic", "robot_description",
"-x", LaunchConfiguration("x"),
"-y", LaunchConfiguration("y"),
"-z", LaunchConfiguration("z"),
],
output="screen",
)
# Create and Return the Launch Description Object #
return LaunchDescription(
[
# Sets use_sim_time for all nodes started below (doesn't work for nodes started from ignition gazebo) #
SetParameter(name="use_sim_time", value=True),
robot_state_publisher_node,
declare_spawn_x,
declare_spawn_y,
declare_spawn_z,
gz_spawn_entity,
]
)
```
这个 CLI 允许你传入三个参数X、Y 和 Z。运行这个 **ros_gz_sim/create** 节点后,它会把 URDF 模型转换并插入当前正在运行的 Gazebo 仿真中。
生成机器人:
**在 Web Shell 3 中执行**
```
cd /workspace
```
```
colcon build --packages-select robot_description && source install/setup.bash
```
```
ros2 launch robot_description spawn.launch.py
```
结果应当类似下图:
![Robot Spawn View](assets/unit1/14-robot-spawn-view.png)
![Robot Spawn View CloseUp](assets/unit1/15-robot-spawn-view-closeup.png)
**练习 2.8.1**
为 launch 文件新增一个参数,用来设置插入模型的名称。它需要把固定写死的 **my_robot** 名称替换为 CLI 中传入的名称。
预期结果是:执行下面的命令后,得到如下效果:
```
ros2 launch robot_description spawn.launch.py
# 生成完后退出
```
```
ros2 launch robot_description spawn.launch.py x:=5 y:=5 model_name:=my_robot_2
```
![Expected Result Ex24](assets/unit1/17-expected-result-ex24.png)
**练习 2.8.1 结束**
<div style="page-break-after: always;"></div>
**练习 2.8.1 参考答案**
在自己动手尝试之前,不要先看答案。完成练习之后再查看参考答案,会更方便你对比自己的实现。
如果你在完成练习时遇到问题,可以到论坛寻求支持。
[spawn.launch.py](assets/files/spawn.launch.py)
**练习 2.8.1 参考答案结束**
<div style="page-break-after: always;"></div>
## 9 总结
预计完成时间:**30 秒**
在本单元中,你学习了使用 **URDF** 构建机器人模型所需的基础元素,包括连杆、关节以及其他关键组件。你还动手创建了自己的机器人,并把它生成到 Gazebo 中,确保模型能够被正确加载,并准备好在仿真环境中进行交互。