🦾 ROS2 机器人 少年创客营:Day 9
主题:造物主时刻 ------ 用代码"组装"你的机器人 (URDF)
🎉 欢迎回到 Level 3:高级架构师!
昨日回顾:昨天我们进入了 Gazebo 的 3D 物理世界,驾驶了现成的 TurtleBot3。你感受到了重力、碰撞和真实的传感器数据。
今天的痛点 :
别人的机器人虽好,但不够酷!
- 你想做一个六轮火星车?
- 你想做一个带机械臂的配送员?
- 你想做一个长得像瓦力 (WALL-E) 的清洁工?
在 ROS 2 中,我们不画图纸,我们写代码 来定义机器人的样子。这种描述文件叫做 URDF (Unified Robot Description Format)。
今日目标:
- 📜 理解 URDF:掌握 Link (连杆) 和 Joint (关节) 的概念。
- ✍️ 手写模型:从零创建一个简单的"双轮小车"URDF 文件。
- 👁️ 可视化:在 RViz2 中看到你自己设计的机器人。
- 🎮 动起来 :通过
joint_state_publisher让机器人的轮子转起来!
🗺️ 今日探险地图 (Checklist)
- 🧠 概念学习:理解"连杆"与"关节"的乐高积木原理。
- 📝 创建包 :新建一个专门存放机器人描述的包
my_robot_description。 - 🛠️ 编写 URDF:手写 XML,定义底盘、轮子和摄像头。
- 👀 RViz 预览:加载 URDF,在 3D 视图中检查模型。
- 🔄 关节测试:让轮子旋转,验证模型是否正确。
🧠 第一关:URDF 核心概念 (乐高积木理论)
想象你在搭乐高。URDF 就是告诉电脑怎么搭这个乐高的说明书。它主要由两个核心元素组成:
1. 🧱 Link (连杆)
- 定义:机器人的刚性部件。比如:底盘、轮子、机械臂的大臂、摄像头外壳。
- 属性 :
visual:长什么样?(颜色、形状、贴图) -> 给人看的。collision:碰撞体积多大?(简化形状,用于物理计算) -> 给物理引擎算的。inertial:多重?重心在哪?(影响惯性和平衡) -> 给物理引擎算的。
2. 🔗 Joint (关节)
- 定义:连接两个 Link 的活动部件。
- 类型 :
fixed:固定死,动不了 (比如摄像头固定在底盘上)。continuous:可以无限旋转 (比如车轮)。revolute:有限角度旋转 (比如机械臂肘部,只能转 0-180 度)。prismatic:直线滑动 (比如升降台)。
- 关键参数 :
parent:父连杆 (谁连着谁)。child:子连杆。axis:旋转轴 (x, y, z)。origin:关节相对于父连杆的位置偏移。
💡 口诀 :先造零件 (Link),再用关节 (Joint) 把它们串起来,最后形成一个树状结构 (通常底盘是根节点
base_link)。
💻 第二关:动手实战 ------ 创建你的第一个机器人
我们将创建一个名为 "MyBot" 的简易双轮小车:
- 底盘:一个蓝色的长方体。
- 轮子:左右两个黑色的圆柱体。
- 摄像头:头顶一个红色的小方块。
步骤 1:创建功能包
我们需要一个包来存放 URDF 文件。
bash
cd ~/ROS2/src
ros2 pkg create my_robot_description --build-type ament_cmake --dependencies urdf launch rviz2
(注:这里用 ament_cmake 是因为 URDF 解析通常涉及 C++ 库,虽然我们也写 XML,但这是标准做法。如果你更习惯 Python 包也可以,只要依赖加上 urdf)
步骤 2:编写 URDF 文件
在 my_robot_description 包下创建一个文件夹 urdf,并在其中新建文件 mybot.urdf。
xml
<robot name="mybot">
<!-- ================= 材料定义 (方便复用颜色) ================= -->
<material name="blue">
<color rgba="0 0 0.8 1"/>
</material>
<material name="black">
<color rgba="0 0 0 1"/>
</material>
<material name="red">
<color rgba="0.8 0 0 1"/>
</material>
<!-- ================= 1. 底盘 (Base Link) ================= -->
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.2 0.1"/> <!-- 长0.4, 宽0.2, 高0.1 米 -->
</geometry>
<material name="blue"/>
<origin xyz="0 0 0.05" rpy="0 0 0"/> <!-- 视觉原点抬高一半高度,放在地面上 -->
</visual>
<collision>
<geometry>
<box size="0.4 0.2 0.1"/>
</geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.01" ixy="0.0" ixz="0.0" iyy="0.01" iyz="0.0" izz="0.01"/>
</inertial>
</link>
<!-- ================= 2. 左轮 (Left Wheel) ================= -->
<link name="left_wheel">
<visual>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="black"/>
<!-- 圆柱默认竖着放,需要旋转90度让它横过来 -->
<origin xyz="0 0 0" rpy="1.57 0 0"/>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<origin xyz="0 0 0" rpy="1.57 0 0"/>
</collision>
<inertial>
<mass value="0.5"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
</link>
<!-- 左轮关节:连接 底盘 和 左轮 -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<axis xyz="0 1 0"/> <!-- 绕 Y 轴旋转 -->
<origin xyz="-0.1 0.12 0" rpy="0 0 0"/> <!-- 位置:车身后部左侧 -->
</joint>
<!-- ================= 3. 右轮 (Right Wheel) ================= -->
<link name="right_wheel">
<visual>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<material name="black"/>
<origin xyz="0 0 0" rpy="1.57 0 0"/>
</visual>
<collision>
<geometry>
<cylinder radius="0.05" length="0.04"/>
</geometry>
<origin xyz="0 0 0" rpy="1.57 0 0"/>
</collision>
<inertial>
<mass value="0.5"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
</link>
<!-- 右轮关节 -->
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<axis xyz="0 1 0"/>
<origin xyz="-0.1 -0.12 0" rpy="0 0 0"/> <!-- 位置:车身后部右侧 (注意 Y 是负数) -->
</joint>
<!-- ================= 4. 摄像头 (Camera) ================= -->
<link name="camera_link">
<visual>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
<material name="red"/>
</visual>
<collision>
<geometry>
<box size="0.05 0.05 0.05"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.0001" ixy="0.0" ixz="0.0" iyy="0.0001" iyz="0.0" izz="0.0001"/>
</inertial>
</link>
<!-- 摄像头关节:固定在底盘顶部 -->
<joint name="camera_joint" type="fixed">
<parent link="base_link"/>
<child link="camera_link"/>
<origin xyz="0.15 0 0.08" rpy="0 0 0"/> <!-- 位置:车头顶部 -->
</joint>
</robot>
👁️ 第三关:在 RViz2 中见证诞生
光有文件不行,得看见它!我们需要一个 Launch 文件来加载 URDF 并启动 RViz。
1. 创建 Launch 文件
在 my_robot_description/launch/ 下新建 view_mybot.launch.py。
python
import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
# 找到功能包路径
pkg_share = FindPackageShare(package='my_robot_description').find('my_robot_description')
urdf_file = os.path.join(pkg_share, 'urdf', 'mybot.urdf')
# 获取 robot_state_publisher 可执行文件
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': Command([FindExecutable(name='cat'), ' ', urdf_file])}]
)
# 启动 joint_state_publisher_gui (提供一个滑块界面让我们手动转动关节)
joint_state_publisher_gui_node = Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui'
)
# 启动 RViz2
rviz_config_file = os.path.join(pkg_share, 'rviz', 'mybot.rviz') # 稍后创建,或者用默认
# 为了简单,我们先不指定配置文件,直接用默认配置,用户在 RViz 里手动添加 RobotModel
rviz_node = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', os.path.join(FindPackageShare('rviz2'), 'default.rviz')]
# 注意:实际使用中通常会复制 default.rviz 修改后使用
)
return LaunchDescription([
robot_state_publisher_node,
joint_state_publisher_gui_node,
rviz_node
])
(注意:为了让 joint_state_publisher_gui 工作,你需要安装它:sudo apt install ros-jazzy-joint-state-publisher-gui)
2. 编译并运行
bash
cd ~/ROS2
colcon build --packages-select my_robot_description
source install/setup.bash
# 启动!
ros2 launch my_robot_description view_mybot.launch.py
3. 在 RViz2 中配置
- RViz 启动后,你可能看不到机器人,或者看到报错 "No transform"。
- 在左下角 Fixed Frame 下拉框中,输入或选择
base_link。 - 点击底部的 Add 按钮 -> 选择 RobotModel -> OK。
- ✨ 奇迹出现! 你应该能看到一个蓝色的底盘,两个黑轮子,头顶一个红方块!
4. 互动测试
- 查看那个自动弹出的 Joint State Publisher GUI 窗口。
- 你会看到
left_wheel_joint和right_wheel_joint的滑块。 - 拖动滑块!看着 RViz 里的轮子转动起来!
- 这就是 ROS 2 的魔法:代码定义了结构,节点发布了状态,RViz 负责渲染。
🧪 第四关:进阶挑战 (Gazebo 预热)
目前的模型只能在 RViz 里看,还不能在 Gazebo 里跑(因为没有物理属性和插件)。但我们可以做个小实验:
🥉 青铜挑战:修改外观
- 把底盘颜色改成绿色。
- 把轮子变大一圈 (修改
radius)。 - 把摄像头移到车头最前方。
- 操作 :修改
mybot.urdf->colcon build-> 重新 launch。
🥈 白银挑战:添加机械臂
- 在
camera_link上再添加一个joint和一个link,做一个可以上下摆动的"脖子"。 - 类型设为
revolute,限制角度-1.57到1.57(即 -90 度到 90 度)。 - 在 GUI 里测试能否摆动。
🥇 黄金挑战:思考物理
- 如果在 Gazebo 里,这个机器人能走吗?
- 答案 :不能!因为我们还没加 Gazebo 插件 (告诉仿真器如何驱动轮子) 和 摩擦系数。
- 思考题:如果轮子和地面摩擦力为 0,会发生什么?(原地打滑)
🆘 常见问题急救包
| 问题 | 现象 | 解决方案 |
|---|---|---|
| RViz 里全是红色报错 | "No transform from [base_link] to [odom]" | 这是因为没有里程计发布。在纯 URDF 预览模式下,把 Fixed Frame 改为 base_link 即可。 |
| 模型看不见/很小 | 视野太远或坐标不对 | 在 RViz 中调整视角,或者检查 URDF 中 origin 的数值是否过大/过小。 |
| 轮子方向不对 | 轮子是立着的而不是躺着的 | 检查 <cylinder> 的 rpy 属性。圆柱体默认 Z 轴朝上,需要绕 X 轴旋转 90 度 (1.57) 才能变成车轮。 |
| Joint 滑块没反应 | 话题没连通 | 确保 joint_state_publisher_gui 节点正在运行,且 RViz 中的 RobotModel 已正确加载。 |
| XML 解析错误 | parsing error |
检查 XML 标签是否闭合 (<link>...</link>),括号是否匹配。URDF 对语法非常严格! |
📝 Day 9 总结清单
| 概念 | 关键词 | 作用 |
|---|---|---|
| URDF | XML, Robot Description | 描述机器人几何、物理属性的标准格式 |
| Link | Visual, Collision, Inertial | 定义机器人的"骨头"和"肉" |
| Joint | Parent, Child, Axis, Type | 定义机器人的"关节"和运动方式 |
| robot_state_publisher | TF Tree | 根据关节状态计算机器人各部分在空间中的位置 |
| joint_state_publisher_gui | GUI Slider | 手动模拟关节运动,用于调试模型 |
🔮 明日预告 (Day 10 - 赋予感官)
今天我们的机器人有了身体,但它是个"瞎子"和"聋子"。
它不知道前面有墙,也不知道自己倾斜了。
Day 10 主题 :传感器仿真 ------ 给机器人装上眼睛和耳朵。
- 如何在 URDF 中添加 激光雷达 (Lidar)?
- 如何添加 摄像头 (Camera) 并看到图像?
- 如何让 Gazebo 真的把这些传感器数据发出来?
准备好让你的 MyBot 真正感知这个世界了吗?
© 2026 ROS2 机器人 少年创客营 | 从今往后,你不仅是驾驶员,更是创造者! 🛠️🤖