ROS2 机器人 少年创客营:Day 9

🦾 ROS2 机器人 少年创客营:Day 9

主题:造物主时刻 ------ 用代码"组装"你的机器人 (URDF)

🎉 欢迎回到 Level 3:高级架构师!

昨日回顾:昨天我们进入了 Gazebo 的 3D 物理世界,驾驶了现成的 TurtleBot3。你感受到了重力、碰撞和真实的传感器数据。

今天的痛点

别人的机器人虽好,但不够酷!

  • 你想做一个六轮火星车
  • 你想做一个带机械臂的配送员
  • 你想做一个长得像瓦力 (WALL-E) 的清洁工

在 ROS 2 中,我们不画图纸,我们写代码 来定义机器人的样子。这种描述文件叫做 URDF (Unified Robot Description Format)。

今日目标

  1. 📜 理解 URDF:掌握 Link (连杆) 和 Joint (关节) 的概念。
  2. ✍️ 手写模型:从零创建一个简单的"双轮小车"URDF 文件。
  3. 👁️ 可视化:在 RViz2 中看到你自己设计的机器人。
  4. 🎮 动起来 :通过 joint_state_publisher 让机器人的轮子转起来!

🗺️ 今日探险地图 (Checklist)

  • 🧠 概念学习:理解"连杆"与"关节"的乐高积木原理。
  • 📝 创建包 :新建一个专门存放机器人描述的包 my_robot_description
  • 🛠️ 编写 URDF:手写 XML,定义底盘、轮子和摄像头。
  • 👀 RViz 预览:加载 URDF,在 3D 视图中检查模型。
  • 🔄 关节测试:让轮子旋转,验证模型是否正确。

🧠 第一关:URDF 核心概念 (乐高积木理论)

想象你在搭乐高。URDF 就是告诉电脑怎么搭这个乐高的说明书。它主要由两个核心元素组成:

  • 定义:机器人的刚性部件。比如:底盘、轮子、机械臂的大臂、摄像头外壳。
  • 属性
    • 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 中配置

  1. RViz 启动后,你可能看不到机器人,或者看到报错 "No transform"。
  2. 在左下角 Fixed Frame 下拉框中,输入或选择 base_link
  3. 点击底部的 Add 按钮 -> 选择 RobotModel -> OK。
  4. ✨ 奇迹出现! 你应该能看到一个蓝色的底盘,两个黑轮子,头顶一个红方块!

4. 互动测试

  • 查看那个自动弹出的 Joint State Publisher GUI 窗口。
  • 你会看到 left_wheel_jointright_wheel_joint 的滑块。
  • 拖动滑块!看着 RViz 里的轮子转动起来!
  • 这就是 ROS 2 的魔法:代码定义了结构,节点发布了状态,RViz 负责渲染。

🧪 第四关:进阶挑战 (Gazebo 预热)

目前的模型只能在 RViz 里看,还不能在 Gazebo 里跑(因为没有物理属性和插件)。但我们可以做个小实验:

🥉 青铜挑战:修改外观

  • 把底盘颜色改成绿色。
  • 把轮子变大一圈 (修改 radius)。
  • 把摄像头移到车头最前方。
  • 操作 :修改 mybot.urdf -> colcon build -> 重新 launch。

🥈 白银挑战:添加机械臂

  • camera_link 上再添加一个 joint 和一个 link,做一个可以上下摆动的"脖子"。
  • 类型设为 revolute,限制角度 -1.571.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 机器人 少年创客营 | 从今往后,你不仅是驾驶员,更是创造者! 🛠️🤖

相关推荐
IT WorryFree4 小时前
肺癌机器人专用技能定制OpenClaw-Medical-Skills 适配版
机器人
ZPC82105 小时前
手柄替代键盘
人工智能·算法·性能优化·机器人
chase。6 小时前
【学习笔记】cuRoboV2——为高自由度机器人打造的动力学感知运动生成框架
笔记·学习·机器人
kyle~6 小时前
导航---Small-GICP重定位算法
c++·机器人·ros2·导航
FTS苏州运营部-富港包装检测技术6 小时前
智能物流机器人的包装验证-ISTA 3E的详细测试参数及方案
机器人·ista3e·ista3a·astmd4169·ista2a·模拟运输验证·运输验证
ZPC82108 小时前
ROS 2 手眼标定完整方案
人工智能·算法·性能优化·机器人
Oflycomm8 小时前
高通公司与Neura达成合作,重注人工智能机器人领域
人工智能·机器人·高通·wifi7·wifi模组
笨小古8 小时前
VLA学习笔记——持续更新中
学习·机器人·大模型·具身智能·vla
FTS苏州运营部-富港包装检测技术8 小时前
【ASTM D4169】之穿梭机器人,仓储机器人,托盘四向穿梭机器人的包装运输安全验证守法
机器人·模拟运输测试·包装运输试验·包装和运输·astm d4169·ista 3e·ista 3a