ROS教程14:URDF / Xacro / robot_state_publisher / joint_states——从机器人模型到自动生成整机 TF Tree
摘要:承接第 13 章 TF,以 AGV 为例读懂 URDF,并用 Xacro 完成参数化、宏复用与条件展开,再串联 robot_description、joint_states 和 robot_state_publisher 生成整机 TF。
@[toc]
第 13 章已经把 TF 的核心机制跑通:
1 | map |
当时为了把注意力集中在 tf2 本身,base_link -> laser_link、base_link -> imu_link 使用了手写 static_transform_publisher:
1 | base_link -> laser_link |
这种方法适合验证一两条边,但真实机器人通常不止两个 frame。
一台 AGV 很快就会出现:
1 | base_link |
机械臂还会继续出现:
1 | shoulder_link |
如果每一条几何关系都手工启动一个 TF publisher,机器人结构会被拆散到 launch、参数和代码里,难以统一维护。
所以第 14 章解决的问题不是“再学一种 TF 发布 API”,而是:
怎样先建立一份统一的机器人几何模型,再由 ROS 根据模型和实时关节状态自动生成机器人自身的 TF tree?
本章主线是:
1 | 手写 URDF |
这里必须先建立一个关键判断:
URDF 是机器人模型格式;Xacro 是生成 URDF 的 XML 宏预处理层。
robot_state_publisher最终消费的仍然是展开后的 URDF XML,而不是 Xacro 宏本身。
实验继续复用第 12、13 章已经完成的真实上游数据:
1 | ros1_driver_lab |
本章新增:
1 | /workspace/ros_ws/src/ros1_description_lab |
最终得到:
1 | map |
其中:
1 | map -> odom |
这三个 ownership 必须先分清。
1. URDF 全称是什么,它真正描述什么
URDF 全称:
1 | Unified Robot Description Format |
常译为:
1 | 统一机器人描述格式 |
它本质上是一种 XML 格式,用来描述机器人模型中的:
1 | link |
本章只抓住最重要的两类对象:
1 | link |
可以先把它们理解为:
1 | link |
例如:
1 | base_link |
base_link 和 laser_link 是两个 link。
laser_joint 说明:
1 | 谁是 parent |
因此 URDF 的核心价值不是“画机器人外观”,而是建立:
一棵有明确 parent / child、几何位置和运动自由度的机器人运动学树。
2. URDF 不是 TF,但它可以成为机器人自身 TF 的几何来源
第 13 章已经强调:
1 | frame_id != TF |
这里还要再加一条:
1 | URDF != TF |
URDF 文件本身只是模型。
例如:
1 | <joint name="laser_joint" type="fixed"> |
它表达的是一个几何事实:
1 | laser_link |
但仅仅把这段 XML 放在磁盘里,不会自动产生 /tf_static。
真正负责把模型变成 TF 的组件是:
1 | robot_state_publisher |
所以正确关系是:
1 | graph LR |
这里的两个输入承担不同职责:
1 | robot_description |
robot_state_publisher 把两者组合起来,才能得到当前机器人 link 的实际 TF。
3. 为什么第 14 章直接复用第 12 章 /joint_states
第 12 章已经发布:
1 | /joint_states |
其中 joint 名称为:
1 | left_wheel_joint |
并持续提供:
1 | position |
这正好是本章 URDF 中左右轮 joint 使用的名字。
所以本章不再额外造一个“假 JointState publisher”。
真实学习链应该是:
1 | /cmd_vel |
这比单独运行一个 GUI slider 更接近后续真实 AGV 数据链。
joint_state_publisher 工具当然可以在没有硬件反馈时给 URDF 生成测试关节值,但这里不是主路径;本项目已经有 Driver,就直接消费 Driver 的关节状态。
4. 本章先建立哪一棵机器人模型
本章模型只描述机器人自身:
1 | graph TD |
注意没有:
1 | map |
原因不是忘记写,而是它们不属于“机器人机械结构”。
URDF 可以描述:
1 | 机器人自己的 link 怎样连接 |
但:
1 | map -> odom |
来自全局定位 / SLAM / AMCL;
1 | odom -> base_link |
来自机器人运动状态。
这两条边会随机器人在世界中的状态变化,不应该伪装成机械安装关系写进 URDF。
因此本章把 base_link 作为 URDF 的 root link。
5. link 最重要的含义不是“外壳”,而是一个刚体 frame
最小 link 甚至可以只有:
1 | <link name="base_link" /> |
这已经定义了一个名为:
1 | base_link |
的机器人部件。
实际文件中给 base_link 增加了:
1 | <visual> |
只是为了让模型以后在 RViz 等工具中有可见几何体。
非常重要的一点是:
1 | link 的 visual geometry |
和:
1 | link frame 在 TF tree 中的位置 |
不是一回事。
例如:
1 | <link name="base_link"> |
这个 <visual><origin> 只是在说:
用于显示的盒子相对
base_linkframe 偏移多少。
它不会创建:
1 | base_link -> 某个新 frame |
真正决定 link 与 link 之间 TF 关系的是:
1 | joint |
这个区别后面做机器人模型时非常重要。
6. joint 才是 URDF tree 中真正的“边”
以雷达为例:
1 | <joint name="laser_joint" type="fixed"> |
这条 joint 定义了:
1 | parent = base_link |
所以树的方向是:
1 | base_link |
origin 表示 child joint/link 在零位时相对于 parent frame 的固定几何关系。
本例:
1 | x = +0.25 m |
这与第 13 章手写:
1 | base_link -> laser_link |
表达的是同一个几何事实。
区别在于现在 geometry 的唯一来源变成了 URDF。
7. <origin xyz="..." rpy="..."> 到底是什么
xyz:
1 | x y z |
单位是米。
rpy:
1 | roll pitch yaw |
单位是弧度。
例如:
1 | <origin xyz="0.18 0 0.32" rpy="0 0 0" /> |
表示 camera_link 在:
1 | base_link 前方 0.18 m |
如果实际相机有安装角度,也应该在这里体现。
后面阶段 D 真正发布 Image / CameraInfo 时,它们的 frame_id 必须能够和这里的相机 frame 对应起来。
所以 URDF 不是纯“建模文件”;它直接参与驱动输出与上层消费者之间的空间契约。
8. fixed joint 为什么不需要 /joint_states
雷达、IMU、相机安装到车体以后,正常运行时相对 base_link 不会转动。
因此使用:
1 | <joint name="imu_joint" type="fixed"> |
对于 fixed joint:
1 | parent -> child |
完全由 URDF 中的:
1 | origin xyz/rpy |
确定。
不需要运行时再给:
1 | imu_joint.position |
所以 robot_state_publisher 可以在启动后直接生成这些固定关系。
本章:
1 | base_link -> laser_link |
都会进入静态 TF 路径。
9. 左右轮为什么是 continuous joint
轮子不同。
它们相对车体位置固定,但自身会不停旋转。
因此:
1 | <joint name="left_wheel_joint" type="continuous"> |
表示这个 joint 可以持续绕某一根轴旋转,不使用有限角度上下限描述其运动范围。
对应轴:
1 | <axis xyz="0 1 0" /> |
本项目坐标约定是:
1 | +x forward |
轮轴沿车体左右方向,因此使用 y 轴。
joint 的零位安装位置由:
1 | <origin xyz="0 0.25 0" rpy="0 0 0" /> |
确定。
运行时再把:
1 | /joint_states.position[left_wheel_joint] |
作为该 joint 的旋转量。
于是最终变换不是只有静态安装位姿,而是:
1 | 零位安装变换 |
10. 为什么 URDF joint 名称必须和 /joint_states.name 对得上
URDF 写:
1 | left_wheel_joint |
第 12 章 Driver 也发布:
1 | name: |
这个名称不是为了“看起来一致”。
它就是 robot_state_publisher 把运行时状态映射回模型 joint 的 key。
概念上可以理解为:
1 | /joint_states |
随后模型查找:
1 | left_wheel_joint |
如果名字写成:
1 | left_motor |
但 URDF 中只有:
1 | left_wheel_joint |
这个状态就无法驱动期望的 wheel joint TF。
所以 joint 名称本身也是 Driver 与机器人模型之间的接口契约。
11. JointState.position、velocity、effort 哪个会改变 TF
第 12 章已经讲过 sensor_msgs/JointState:
1 | name[] |
对于 robot_state_publisher 的 link pose 计算,核心输入是:
1 | position |
原因很直接。
一个 revolute / continuous joint 当前的空间姿态由:
1 | 关节角度 q |
决定。
所以概念上:
1 | URDF joint geometry |
velocity 和 effort 对控制、诊断、动力学当然有意义,但它们不直接决定这一时刻 child link 应该旋转到什么姿态。
Noetic robot_state_publisher 的运行路径也是先从 JointState 中构造:
1 | joint name -> joint position |
再据此计算 movable segment 的 pose。
因此如果消息有:
1 | name |
却没有匹配的:
1 | position |
不能指望它仅凭速度自动积分出 TF。
Driver 自己应该明确负责 position 数据的来源与累计语义。
12. /robot_description 是什么
URDF 文件在磁盘里:
1 | ros1_description_lab/urdf/agv.urdf |
但 ROS Node 不应该依赖“大家都自己去找这个文件”。
ROS1 中常见做法是把完整 URDF XML 加载到参数服务器:
1 | /robot_description |
本章 launch 使用:
1 | <param name="robot_description" |
启动以后可以查看:
1 | rosparam get /robot_description |
得到的不是文件路径,而是已经加载到参数服务器的 XML 内容。
所以关系是:
1 | agv.urdf 文件 |
这一步非常重要,因为后面很多 ROS 工具都把:
1 | robot_description |
当作机器人模型的标准入口。
13. robot_state_publisher 启动时内部做了什么
Noetic 的 robot_state_publisher_node 主入口可以压缩成:
1 | ros::init() |
这里有两次关键转换。
第一次:
1 | robot_description XML |
也就是把 XML 解析成程序内部的:
1 | links |
第二次:
1 | urdf::Model |
也就是把模型变成一棵可用于运动学计算的树。
官方 Noetic urdf::Model 提供的 initParam() 本身就是“从参数服务器加载模型”的入口。
所以 robot_state_publisher 并不是每次收到 /joint_states 后重新解析一遍 XML。
它启动时先建立模型与运动学树,运行期主要消费关节状态并更新可运动 joint 的姿态。
14. 内部为什么要把 fixed 和 moving joint 分开
建立运动学树以后,robot_state_publisher 会把 segment 按 joint 类型区分。
可以把内部思路简化成:
1 | URDF/KDL tree |
原因是两类数据的生命周期完全不同。
fixed:
1 | 启动后几何关系就确定 |
moving:
1 | 必须等待运行时 joint position |
因此输出也自然分成:
1 | fixed |
这正好与第 13 章讲过的 tf2 静态 / 动态缓存语义对应起来。
15. fixed joint 怎样进入 /tf_static
本章 launch 显式设置:
1 | <param name="use_tf_static" value="true" /> |
于是:
1 | laser_joint |
对应的:
1 | base_link -> laser_link |
会通过 static TF broadcaster 发布。
它们的计算不需要 joint position,可以理解为:
1 | pose(q = 0) |
因为 fixed joint 根本没有运动自由度。
第 13 章里手写的:
1 | static_transform_publisher base_link laser_link |
到了这里就应该退出。
一个几何关系只保留一个 owner:
1 | 机器人自身 fixed geometry |
否则同一 child frame 被多个 publisher 重复发布,会重新制造 TF ownership 冲突。
16. moving joint 怎样从 /joint_states 进入 /tf
对于:
1 | left_wheel_joint |
robot_state_publisher 需要等 /joint_states。
数据链是:
1 | graph LR |
第 12 章 Driver 会持续累计:
1 | left_wheel_position_rad |
并发布到:
1 | JointState.position |
robot_state_publisher 用 joint name 找到模型中的 segment,再把对应 position 代入运动学关系,得到:
1 | base_link -> left_wheel_link |
这就是“模型 + 状态 -> 当前 TF”的完整过程。
17. header.stamp 为什么仍然重要
第 13 章已经建立:
1 | 动态 TF 是带时间的 |
/joint_states 也有:
1 | header.stamp |
本项目 Driver 在同一个采样循环中为:
1 | JointState |
使用同一份:
1 | stamp |
这非常有价值。
因为后面可以形成:
1 | /odom @ T |
同一批设备状态能够落在一致的时间语义上。
robot_state_publisher 还会根据 publish_frequency 限制 movable transform 的最大发布频率;本章显式设成:
1 | 20 Hz |
与 Driver 当前发布频率保持一致,便于观察。
18. robot_state_publisher 不会替代 odom_tf_broadcaster
这是本章最容易混淆的职责边界之一。
URDF root 是:
1 | base_link |
所以 robot_state_publisher 能从这里向下展开:
1 | base_link |
但它不知道机器人此刻在 odom 中走到了哪里。
那个信息来自:
1 | /odom.pose.pose |
所以:
1 | odom -> base_link |
仍由第 13 章:
1 | odom_tf_broadcaster |
负责。
不要把这两类关系混起来:
1 | 机器人在世界 / 里程计坐标系中的运动 |
19. robot_state_publisher 也不会替代 map -> odom
本章 launch 中仍然暂时存在:
1 | map -> odom |
恒等 static TF。
它只是为了让第 13、14 章实验可以形成完整:
1 | map -> odom -> base_link -> ... |
以后进入:
1 | SLAM |
以后,这条边会被真正的全局定位模块接管。
所以完整 ownership 是:
| TF 边 | 本章 owner | 真实系统长期 owner |
|---|---|---|
map -> odom |
临时 static publisher | SLAM / AMCL / localization |
odom -> base_link |
odom_tf_broadcaster |
odom / state estimator |
base_link -> sensor |
robot_state_publisher |
robot_state_publisher |
base_link -> wheel |
robot_state_publisher + /joint_states |
robot_state_publisher + joint feedback |
第 14 章真正接管的是后两行。
20. 创建 ros1_description_lab
本章新增 package:
1 | ros_ws/src/ros1_description_lab/ |
这个 package 不需要自己写 C++ TF publisher。
它主要保存:
1 | 纯 URDF 学习基线 |
其中:
1 | agv.urdf |
这也是机器人项目里常见的 description package 职责。
21. 第一步只看一个 fixed joint:base_link -> laser_link
完整文件虽然已经提供,但理解时先只看这三块:
1 | <link name="base_link"> |
先不要管轮子、IMU、camera。
这已经足够建立:
1 | base_link |
如果以后修改雷达安装位置,应该优先修改:
1 | laser_joint origin |
而不是再去某个 launch 文件里寻找一条神秘的 static transform 参数。
这就是把“几何事实”集中进 URDF 的收益。
22. 第二步加入左右轮运动 joint
左轮:
1 | <joint name="left_wheel_joint" type="continuous"> |
右轮:
1 | <joint name="right_wheel_joint" type="continuous"> |
注意这里的:
1 | y = +0.25 |
和:
1 | y = -0.25 |
遵循第 12、13 章一直使用的:
1 | +y left |
所以:
1 | left wheel -> y 正方向 |
这份 URDF 的轮距因此是:
1 | 0.50 m |
正好与第 12 章 Driver 参数:
1 | wheel_separation = 0.50 |
保持一致。
这不是巧合。
Driver 的运动学参数和 URDF 的机械几何必须描述同一台机器人。
23. 第三步加入 IMU 和 camera fixed geometry
IMU:
1 | <joint name="imu_joint" type="fixed"> |
这里特意继续使用:
1 | imu_link |
因为第 12 章 /imu/data_raw 已经发布:
1 | header.frame_id = imu_link |
这样消息契约和 TF tree 才真正闭合:
1 | /imu/data_raw |
camera 本章只建立几何 frame:
1 | camera_link |
真正的:
1 | sensor_msgs/Image |
仍然留到阶段 D。
也就是说:
先把“传感器装在哪里”建模好,不等于现在就开始学习传感器数据算法。
24. launch 怎样把三个章节串起来
description_lab.launch 做四件事。
第一,启动第 12 章 Driver:
1 | <include file="$(find ros1_driver_lab)/launch/chassis_lab.launch" /> |
得到:
1 | /joint_states |
第二,继续启动第 13 章 odom_tf_broadcaster:
1 | /odom |
第三,用 Xacro 处理器展开:
1 | agv.urdf.xacro |
第四,启动:
1 | robot_state_publisher |
于是整条链变成:
1 | 第 12 章 Driver |
这就是本章真正要建立的系统关系。
25. Docker Image 增加哪些依赖
本章 Dockerfile 显式加入:
1 | ros-noetic-robot-state-publisher |
因此需要在 Host 重新构建 Image:
1 | cd /home/wdfk/share/ros1-docker |
如果 Container 已经开着但没有重新 build,新 package 文件虽然能通过 bind mount 看到,系统却可能根本没有:
1 | robot_state_publisher |
可执行程序。
这一点属于镜像依赖问题,不是 catkin package 代码问题。
26. 构建第 14 章 package
进入 Container:
1 | docker compose exec ros1-dev bash |
构建:
1 | cd /workspace/ros_ws |
然后:
1 | source /workspace/ros_ws/devel/setup.bash |
确认 package 可以被 rospack 找到:
1 | rospack find ros1_description_lab |
预期路径:
1 | /workspace/ros_ws/src/ros1_description_lab |
27. 启动前先看 robot_description 是怎么装进去的
第 14 章的 launch 不再直接使用:
1 | <param name="robot_description" textfile=".../agv.urdf" /> |
而是执行 Xacro:
1 | <param name="robot_description" |
这里 command= 的返回标准输出会成为参数值,所以真实路径是:
1 | agv.urdf.xacro |
运行:
1 | roslaunch ros1_description_lab description_lab.launch |
另开一个 Container 终端:
1 | source /workspace/ros_ws/devel/setup.bash |
如果看到的是普通 URDF:
1 | <robot name="ros1_agv_lab"> |
而不是:
1 | <xacro:macro ...> |
就证明 Xacro 已经在 robot_state_publisher 之前完成了预处理。
这也是理解 Xacro 最重要的运行时边界:
/robot_description保存的是展开结果,不保存 Xacro 源码。
28. 再确认 /joint_states 和 URDF joint 名字完全一致
执行:
1 | rostopic echo -n 1 /joint_states |
重点看:
1 | name: |
再回到 Xacro 源文件及其宏:
1 | ros1_description_lab/urdf/agv.urdf.xacro |
确认展开后 joint 仍然叫:
1 | left_wheel_joint |
如果这一步不一致,就不要继续怀疑 tf2 Buffer、时间插值或 Navigation。
错误已经发生在:
1 | Driver joint contract |
这是比“TF tree 不完整”更靠上游的故障点。
29. 先观察 fixed joint:传感器 TF 不应该随运动变化
执行:
1 | rosrun tf2_ros tf2_echo base_link laser_link |
应该持续看到接近:
1 | Translation: |
机器人移动、轮子旋转,都不应该改变:
1 | base_link -> laser_link |
因为它属于:
1 | fixed joint |
同理:
1 | rosrun tf2_ros tf2_echo base_link imu_link |
应该保持:
1 | x = 0 |
这就是机器人安装几何的稳定性。
30. 再观察 moving joint:轮子 TF 必须由 /joint_states.position 驱动
先执行:
1 | rosrun tf2_ros tf2_echo base_link left_wheel_link |
然后另开终端持续发送:
1 | rostopic pub -r 5 /cmd_vel geometry_msgs/Twist \ |
第 12 章 Driver 会产生:
1 | left_wheel_joint.position |
继续观察:
1 | rostopic echo /joint_states |
应该看到 position 持续累计。
与此同时:
1 | base_link -> left_wheel_link |
的 translation 应保持安装位置:
1 | x = 0 |
而 rotation 会随 joint angle 改变。
这正是:
1 | 固定安装位置 |
组合后的结果。
31. 为什么车体运动和轮子自转会同时出现在完整 TF 链中
此时已经有两类动态关系:
1 | odom -> base_link |
来自 Odometry。
以及:
1 | base_link -> wheel links |
来自 JointState + URDF。
因此 tf2 可以组合:
1 | odom |
甚至再加上临时:
1 | map -> odom |
得到:
1 | map |
这里两种“运动”完全不同:
1 | odom -> base_link |
tf2 只是把两段正确的空间关系组合起来。
32. 用 view_frames.py 看现在的整棵树
执行:
1 | cd /workspace |
此时应该看到类似:
1 | map |
和第 13 章相比,最重要的变化不是“多了三个 frame”。
而是:
1 | 第 13 章 |
这才是结构上的升级。
33. /tf_static 中现在应该由谁发布什么
本章仍然存在两个 static TF owner。
第一个:
1 | map_to_odom_static |
这是教学占位。
第二个:
1 | robot_state_publisher |
因此如果直接执行:
1 | rostopic echo /tf_static |
需要意识到 /tf_static 上可能有多个 latched publisher。
不要因为第一条看到:
1 | map -> odom |
就误认为 robot_state_publisher 没有发 static TF。
更可靠的确认仍然是直接按目标边查询:
1 | rosrun tf2_ros tf2_echo base_link laser_link |
或者查看完整 frame tree。
34. robot_state_publisher 为什么不能凭 URDF 自动知道轮子转了多少
URDF 中已经写了:
1 | left_wheel_joint |
但这只能说明:
1 | 它怎样运动 |
不能说明:
1 | 它现在运动到了哪里 |
这两类信息分别属于:
1 | model |
即:
1 | URDF |
这也是机器人软件里非常通用的分层方式。
模型不会替代传感器反馈,传感器反馈也不会自动包含完整模型。
robot_state_publisher 的作用就是在两者之间做连接。
35. 如果 /joint_states 停了,哪些 TF 会受影响
假设 Driver 停止发布 /joint_states。
URDF 不会消失:
1 | /robot_description |
仍然存在。
fixed joint:
1 | base_link -> laser_link |
仍然是静态几何。
但 moving joint:
1 | base_link -> left_wheel_link |
没有新的 joint position,就不会得到新的动态姿态更新。
这说明调试时必须区分:
1 | 模型不存在 |
和:
1 | 模型存在,但状态输入停止 |
这两个问题的故障层完全不同。
36. 如果 JointState.name 对,但 position 不对,会发生什么
假设:
1 | left_wheel_joint |
名称正确,但 Driver 把角度单位错当成:
1 | degree |
而不是:
1 | radian |
那么:
1 | Topic 正常 |
但 wheel link 的姿态仍然会错。
所以 URDF + robot_state_publisher 并不能替代第 12 章的数据契约检查。
它们假设上游提供的:
1 | joint name |
已经正确。
这也是为什么本系列先讲 Driver 数据契约,再讲 TF / URDF。
顺序不能反过来。
37. 为什么不能同时保留第 13 章的 sensor static publisher
如果继续启动:
1 | base_to_laser_static |
同时 URDF 又定义:
1 | laser_joint |
那么:
1 | laser_link |
就会同时收到多个 owner 的变换。
即使两边数值暂时完全相同,这也是错误的 ownership 设计。
以后只要一边修改安装位置而另一边忘记同步,就会出现非常难排查的 TF 抖动或 authority 冲突。
所以第 14 章的 description_lab.launch 没有 include 整个:
1 | tf_lab.launch |
而是只复用其中真正仍然需要的:
1 | odom_tf_broadcaster |
传感器 static TF 则彻底交给:
1 | robot_state_publisher |
这就是“复用组件”与“复用整个 launch”之间的区别。
38. Xacro 到底是什么,它和 URDF 是什么关系
Xacro 通常展开为:
1 | XML Macros |
它是一种用于 XML 的宏语言。ROS 机器人模型中最常见的用途,就是用更短、更可复用、更参数化的源文件生成最终 URDF XML。
先直接比较两者的职责:
| 对象 | 本质 | 运行时谁直接消费 |
|---|---|---|
| URDF | 标准机器人模型 XML | urdf::Model / robot_state_publisher 等 |
| Xacro | 生成 XML 的宏预处理语言 | xacro 处理器 |
/robot_description |
展开后的机器人 XML 字符串 | robot_state_publisher |
所以:
1 | Xacro != URDF 的替代运行时格式 |
更准确的理解是:
1 | Xacro source |
robot_state_publisher 不负责解释:
1 | xacro:property |
这些语法在它读取模型之前就必须已经展开掉。
这就是为什么 Xacro 不改变第 13、14 章已经建立的 TF 原理;它只改变“URDF XML 是怎样被生成和维护的”。
39. 为什么纯 URDF 一大就会开始重复
本章纯 agv.urdf 中,左右轮结构几乎完全相同:
1 | <link name="left_wheel_link"> ... </link> |
真正不同的只有:
1 | left / right |
如果以后再增加:
1 | front_left_wheel |
复制粘贴会迅速带来两个问题。
第一,机械尺寸会散落在大量 XML 数字里:
1 | wheel radius |
第二,相同结构修改时必须同步多个副本。
这正是 Xacro 要解决的工程问题:
把“重复 XML”提升成“参数 + 模板”,但最终仍生成标准 URDF。
40. xacro:property:把魔法数字提升成模型参数
agv.urdf.xacro 先定义:
1 | <xacro:property name="body_length" value="0.60" /> |
之后普通 XML 属性可以通过:
1 | ${...} |
求值:
1 | <box size="${body_length} ${body_width} ${body_height}" /> |
还可以写表达式:
1 | <origin xyz="0 0 ${body_height / 2.0}" rpy="0 0 0" /> |
这样 0.60 / 0.44 / 0.20 不再只是某段 XML 中无法解释的数字,而变成有语义的模型参数。
这里要和 ROS Parameter Server 区分:
1 | xacro:property |
两者名字都叫“参数”时很容易混淆,但生命周期完全不同。
41. xacro:macro:把重复 link + joint 抽成模板
本章把左右轮抽成:
1 | <xacro:macro name="wheel" params="side y wheel_radius wheel_width"> |
调用两次:
1 | <xacro:wheel side="left" |
展开以后仍然得到两个普通 joint:
1 | left_wheel_joint |
因此第 12 章 /joint_states.name 契约完全没有改变。
Xacro macro 改变的是源码组织方式,不应该偷偷改变上层接口名称。
本章还定义了:
1 | box_sensor(name, xyz, size) |
用同一个模板生成 IMU 和 camera 的:
1 | link |
这就是宏复用的直接价值。
42. xacro:include:为什么大型机器人模型通常会拆文件
如果所有 macro 都继续堆在:
1 | agv.urdf.xacro |
文件最终还是会变得很长。
因此本章把可复用组件放进:
1 | urdf/macros/components.xacro |
主模型通过:
1 | <xacro:include filename="$(find ros1_description_lab)/urdf/macros/components.xacro" /> |
加载这些宏。
当前目录职责于是变成:
1 | urdf/ |
可以把它理解成:
1 | agv.urdf.xacro |
以后真实项目还可以继续拆成:
1 | materials.xacro |
但拆分的前提仍然是职责清楚,而不是为了“文件越多越工程化”。
43. xacro:arg 与条件展开:同一份模型怎样生成不同配置
除了模型内部 property,Xacro 还可以接收外部参数。
本章定义:
1 | <xacro:arg name="use_camera" default="true" /> |
然后:
1 | <xacro:if value="$(arg use_camera)"> |
默认展开结果包含:
1 | camera_link |
如果启动:
1 | roslaunch ros1_description_lab description_lab.launch use_camera:=false |
launch 会把参数继续传给 Xacro:
1 | <param name="robot_description" |
于是展开后的 URDF 中不再存在 camera 分支。
注意这里有三层参数语义:
1 | roslaunch arg |
它不是:
1 | robot_state_publisher 运行后再动态关掉 camera |
因为条件判断发生在模型生成阶段。
44. Xacro 的执行原理:它做的是“展开”,不是运行机器人算法
把本章模型处理过程拆开:
1 | graph LR |
Xacro 处理器主要完成:
1 | 解析 Xacro XML |
处理结束后,宏语言自身已经消失。
后续:
1 | URDF parser |
都不需要知道原始模型是不是由 Xacro 生成的。
所以从系统分层上看:
1 | Xacro |
这三个层次不能混在一起。
45. 先在命令行单独展开 Xacro,再让 launch 使用它
在正式启动完整系统之前,可以只执行模型生成:
1 | rosrun xacro xacro \ |
查看:
1 | head -n 30 /tmp/agv.generated.urdf |
应该看到普通:
1 | <robot> |
而不应该再看到:
1 | <xacro:macro> |
再验证条件参数:
1 | rosrun xacro xacro \ |
然后:
1 | grep -n "camera" /tmp/agv-no-camera.urdf |
如果没有 camera link/joint,就说明:
1 | launch 参数 |
这一步把 Xacro 问题与 robot_state_publisher 问题分开了。
如果 Xacro 本身无法展开,就没有必要继续排查 /tf。
46. 为什么项目同时保留 agv.urdf 和 agv.urdf.xacro
本章故意保留两份文件:
1 | agv.urdf |
它们承担不同教学职责。
agv.urdf 用来直接学习最终模型:
1 | link |
agv.urdf.xacro 用来学习怎样工程化生成相同结构:
1 | property |
真正运行时,description_lab.launch 使用:
1 | agv.urdf.xacro |
但调试模型时,应该随时记住:
Xacro 的正确性最终仍要落到“它生成的 URDF 是否正确”。
因此遇到模型问题时,最有效的分层排查顺序是:
1 | Xacro 能否展开? |
而不是看到 TF 异常就直接修改宏。
47. 从源码角度重新看完整数据路径
把 Noetic robot_state_publisher 的关键路径压缩后,可以得到:
1 | robot_state_publisher_node::main() |
这里最关键的不是函数名,而是可以明确看到三层职责:
1 | 解析模型 |
这三层分开以后,再看源码会非常清楚。
48. 本章和第 13 章的知识怎样拼起来
第 13 章回答:
1 | TF 是什么? |
第 14 章回答:
1 | 机器人自身那么多 TF 从哪里来? |
于是目前已经形成:
1 | Driver data contract |
这已经是后续 Navigation 数据链真正可用的机器人坐标基础。
49. 第 14 章结束后应该保留的核心判断
第一:
1 | URDF 是模型 |
第二:
1 | joint 是 link tree 的边 |
第三:
1 | fixed joint |
第四:
1 | moving joint |
第五:
1 | JointState.name |
第六:
1 | robot_state_publisher |
它不会自动产生:
1 | map -> odom |
第七:
1 | 一个 TF child frame 只应该有一个明确 owner |
第 13 章手写 sensor static TF 在本章应退出,由 URDF 统一接管。
第八:
1 | Xacro 是 URDF 的生成层,不是 robot_state_publisher 的另一种运行时模型格式 |
第九:
1 | Xacro 调试必须先看展开结果,再看 robot_description,最后才看 TF |
到这里,机器人自身几何关系已经从“零散 TF 参数”升级成“一份模型 + 一份状态输入”。
下一章进入 Navigation 传感器接口,开始研究:
即使
laser_link已经存在、TF tree 也连通,sensor_msgs/LaserScan/Range还必须满足哪些消息字段、时间和 frame 契约,costmap / SLAM 才真正能够消费?
这就是第 15 章的入口。
参考资料
- ROS Noetic
robot_state_publisherAPI:https://docs.ros.org/en/noetic/api/robot_state_publisher/html/ - ROS Noetic
urdf::ModelAPI:https://docs.ros.org/en/noetic/api/urdf/html/classurdf_1_1Model.html - ROS
robot_state_publisher:https://github.com/ros/robot_state_publisher - ROS1 URDF repository:https://github.com/ros/urdf
- ROS Noetic Xacro API:https://docs.ros.org/en/noetic/api/xacro/html/
- ROS Xacro repository:https://github.com/ros/xacro









