ROS 2 基础:节点、坐标系与机器人视觉
你能把机器人组织成一组 ROS 2 节点,在话题、服务和动作之间做出正确选择,并把相机的观测结果放进机器人的坐标系树。
本站的其他一切内容都跑在单片机上:一个程序、一个循环、完全可控。ROS 2 是你应当在机器人超出这个范围时转向的东西——当它同时拥有一台相机、一个规划器和一套驱动栈,需要一并运行、由不同的人编写、并且能够各自独立重启的时候。
代价是一整套新观念,而第一个是:ROS 2 机器人不是一个程序,而是一张彼此通信的节点图。早期的大部分困惑,都源于在这四种通信方式之间选错了,所以本路线就从这里开始:话题用于数据流,服务用于快速请求,动作用于你可能需要取消的长任务。
接下来是 TF2,它是悄悄把人打败的那一部分。机器人就是一棵坐标系树——底盘、轮子、每一个传感器——而几乎每一个真实的 bug 都是某个 transform 缺失、过期,或者挂在了错误的父节点上。一旦坐标系理顺,标定好的相机就只是又一个向那棵树里发布位姿的节点,而这正是视觉接入机器人其余部分的方式。
开始之前
你需要熟悉 Linux 命令行,并且能读懂 Python 或 C++ 中的任意一种。 树莓派是通常的目标平台,但直到硬件节点之前的全部内容都可以在笔记本电脑上运行。安装本身要预留一个晚上——ROS 2 是一个分布式系统,从第一条命令开始它的行为就像一个分布式系统。
你将会做到什么
- 在话题、服务和动作之间正确选择,而不是把所有东西都做成话题、事后才发现它的局限。
- 建立一棵 TF2 坐标系树,把一个点从传感器的坐标系变换到机器人的坐标系,并在 transform 缺失时调试它。
- 标定相机、获取标记物的位姿,并弄清这些数字是相对于什么而言的。
- 把
cmd_vel的 twist 转换成左右轮速度,并让饱和处理保持指令所要求的弧线。
大家卡在哪里
**节点互相看不见。**ROS 2 的自动发现一直有效,直到它失效为止。两台
ROS_DOMAIN_ID 值不同的机器对彼此完全不可见;而会拦截组播的 Wi-Fi 会让发现机制彻底失效,同时每个节点都报告自己状态完全健康。
**QoS 不匹配。**一个提供尽力而为传输的发布者与一个要求可靠传输的订阅者永远连不上,而且更麻烦的是,双方都不会记录任何错误。话题上就是简单地空无一物。 传感器数据是常见的元凶,因为传感器驱动通常默认使用尽力而为。
**忘了 source 工作空间。**新开的终端在被 source 之前对你的包一无所知,
所以 ros2 run 会把一个明明存在的包报告成缺失。
**一个坐标系有两个父节点。**TF2 是一棵树。从两个来源发布同一个子坐标系会让它拥有两个父节点, 于是查询开始随时序间歇性失败——这是最难排查的 ROS bug 之一,因为它大部分时间是正常的。
**时间戳和 frame ID 为空。**头部没有 frame_id 的消息无法被变换,而时间戳为零的消息也无法与任何东西对齐。
两者都能毫无怨言地通过类型系统,只在有东西试图把它们一起使用时才失败。
你需要什么
| 什么 | 这条路线为什么需要它 | 说明 |
|---|---|---|
| 你能自如使用的 Linux 命令行 | ROS 2 是一个以 Linux 为先的系统,工具链都假定如此 | Ubuntu 与官方二进制匹配;其他发行版就得自己编译 |
| Python 或 C++ | 节点代码用这两者之一编写 | Python 上手更快;性能关键的节点则用 C++ |
| 一台有真实显示器的机器 | 通过 SSH 调试分布式系统会痛苦得多 | 笔记本电脑比无显示器的树莓派更适合作为学习目标 |
| 树莓派(仅硬件节点) | 只有当机器人自带计算机时才需要 | Pi 4 或更好;Pi Zero 会让你失望 |
| 相机(仅视觉节点) | 这条路线的最后一个节点 | 树莓派相机模块或任意 UVC 摄像头 |
| 一个晚上用于安装 | 不是玩笑——这是路线的一部分 | 安装确实是第一个难关 |
为什么按这个顺序
**通信模式排在最前,因为选错的代价要在后面才付出。**ROS 2 节点的这四种 通信方式不可互换,而新手最典型的失败就是把一切都做成话题——然后在写到三百行时才发现,你需要的是取消能力和进度反馈,而自己一直在手写动作。十分钟用来在话题、服务和动作之间做决定,能省下一次重写。
**TF2 排在第二,因为它是悄悄把人打败的那一部分。**一旦有多个节点 发布关于物理世界的数据,它们各自所说的“3 米”都是相对于不同的东西——相机、底盘、地图。TF2 就是把这些关联起来的机制,而真实的 ROS bug 大多数都是某个 transform 缺失、过期或挂在错误的父节点上。在需要它之前把它当作一个专题来学,比以一次莫名其妙的查询失败的方式撞上它便宜得多。
**相机标定排在第三,因为它完全依赖第二项。**不知道标记物是相对于什么, 位姿本身就没有意义。一旦坐标系理顺,标定好的相机就只是又一个向树里发布位姿的节点,视觉也就不再是一个特例。
驱动节点为这条路线收尾,因为这里是 ROS 与本站其他一切所讲的硬件相遇的地方。
cmd_vel 的 twist 是 ROS 每个导航组件都使用的标准接口,而把它转换成左右轮速度——并且让饱和处理保持指令的弧线而不把它扭曲——就是 ROS 世界与电机世界之间的桥。
检查点:如何判断一个阶段已经落地
| 阶段 | 检查内容 | 通过是什么样 |
|---|---|---|
| 安装 | 在新终端里执行 ros2 topic list |
source 之后能用,并且你知道 source 之前为什么会失败 |
| 两个节点 | 在两台机器上分别运行 talker 和 listener | 消息到达了——而且你能说出它们共享的 domain ID |
| 话题与服务 | 为同一份数据各写一个 | 你能用一句话说清每种模式为什么适合它的任务 |
| 动作 | 启动一个长动作并在中途取消 | 它干净地停下来了,而且运行期间你收到了反馈 |
| QoS | 故意在某个话题上让可靠性不匹配 | 它悄无声息地什么都没传——现在你认得出这个症状 |
| TF2 树 | 运行 ros2 run tf2_tools view_frames |
一棵树,一个根,没有哪个坐标系带两个父节点 |
| transform 查询 | 把一个点从传感器坐标系变换到底盘坐标系 | 数值正确,而且你能用卷尺核对 |
| 过期的 transform | 在查询进行时停掉某个 transform 的发布者 | 你会得到一个外推错误——这种失败现在有名字了 |
| 相机标定 | 标定并检查重投影误差 | 远低于一个像素;数值偏大意味着采集的数据集质量差 |
| 标记物位姿 | 把标记物放在一个实测距离上并读出其位姿 | 与卷尺一致,而且你能说出它位于哪个坐标系 |
cmd_vel |
发布一个同时含线速度和角速度项的 twist | 两个轮子都正确转动,且其中一个饱和时另一个会按比例缩放以保持弧线 |
这条路线到哪里为止
这条路线构建的是 ROS 2 机器人的底层基础——节点如何通信、坐标系如何关联, 以及传感器和传动系统如何接入这张图。它刻意停在大个子系统之前,因为那些子系统都假定这些基础已经存在。
它不涵盖 Nav2 栈,那是 ROS 2 中自主导航的标准答案,本身就是一个相当可观的
系统——代价地图、行为树、规划器和控制器,全都是配置出来的而非写出来的。Nav2 假定你已经有可用的 TF 树和 cmd_vel 接口,而这正是本路线给你的东西。
它不涵盖 URDF 与机器人描述,也就是告诉 ROS 其余部分你的机器人是什么形状的 XML 格式。本路线中的一切都是手工发布 transform 的,而 URDF 是让你不必再这么做的办法。
它不涵盖 Gazebo 中的仿真(认真的 ROS 开发实际上大多在这里进行), 也不涵盖 ros2_control——面向真实传动系统的标准硬件抽象层。
它同样不涵盖实时行为。ROS 2 构建在 DDS 之上,在这方面比 ROS 1 强得多, 但一台运行 Python 节点的 Linux 机器并不是实时控制系统。那些紧凑的回路——平衡、电流控制,任何千赫兹级的东西——仍然属于单片机,ROS 2 放在它上面。这种分工是正经机器人的常规架构,而不是一种妥协。
学习路线
路线
按顺序完成各个节点——完成一个就会解锁下一个。你的进度会保存在这台设备上。
已完成 0 / 5
常见问题
常见问题解答
机器人什么时候应该从 Arduino 转向 ROS 2?
当一个循环不再够用的时候。对于循迹车、迷宫求解器,或者任何整个行为都能装进一个控制循环的机器人来说,一块运行单个程序的单片机就是正确的架构——而且在这些场合它确实比 ROS 2 更好,因为它是确定性的,你能把它的全部都装在脑子里。当一台机器人有好几样东西必须以不同的频率同时运行——相机流水线、规划器、驱动栈——并且它们写于不同的时间、需要能各自独立重启时,ROS 2 的复杂度才值得付出。代价是一个分布式系统,而且从第一条命令开始,它的表现就像一个分布式系统。
在 ROS 2 里,话题、服务还是动作——该怎么选?
让通信模式与任务相匹配。话题是没有回复的数据流:传感器数据、里程计、速度指令——任何持续发布、只关心最新值的东西。服务是一次快速的请求与响应,用于几毫秒内就能完成的事情,比如查询一个参数或切换一个模式。动作用于你可能想获取进度、或者需要取消的长任务——导航到一个目标,或者让夹爪合拢夹住一个物体。早期最常见的错误,是把一切都做成话题,然后在上面手工搭建请求—响应的语义。
为什么我的 ROS 2 节点互相看不见?
三个原因几乎涵盖了所有情况。ROS_DOMAIN_ID 值不同,会让两台机器对彼此完全不可见,而双方都报告自己一切正常。拦截组播的 Wi-Fi 网络会彻底破坏发现协议,这就是为什么节点常常能通过以太网找到彼此,却不能通过办公室的无线接入点找到。还有 QoS 不匹配——发布者提供尽力而为传输,而订阅者要求可靠传输——意味着两者永远连不上,而且谁都不会记录错误。先检查 domain ID;这一步不花任何代价,也是最常见的答案。
什么是 TF2,为什么坐标变换总是失败?
TF2 维护着机器人上的坐标系树——底盘、轮子和每一个传感器——这样你就能询问相机坐标系中的一个点位于机器人坐标系中的哪里。查询失败有三个反复出现的原因。一个坐标系有两个父节点会破坏这棵树,而由于它是否失败取决于哪个发布者在时序上抢先,它大部分时候都能正常工作。消息头里的 frame_id 为空,意味着这条消息根本无法被变换。而时间戳为零或已过期,意味着这个坐标变换存在,却不存在于你询问的那个时刻。这三种情况都能毫无怨言地通过类型系统。
学 ROS 2 需要树莓派吗?
不需要。除了硬件驱动节点之外,所有东西都能在笔记本电脑上完美运行,而且笔记本电脑是更好的学习场所——构建更快、有真正的屏幕,也不会因为 SD 卡损坏而白白损失一个晚上。只有当你希望机器人自带计算机时,树莓派才变得必要。如果你确实要用它,就要预料到树莓派会成为瓶颈:相机流水线加上规划器,很快就会摸到树莓派 4 的极限。
为什么 ros2 run 说我的包不存在,而它明明就在那里?
你没有在那个终端里加载工作空间的环境(也就是执行 source)。每个新终端在你加载安装目录下的 setup.bash 脚本之前,对你构建好的包都一无所知,而 ROS 报告的结果是包缺失,而不是环境没有加载。这是 ROS 2 最常见的小坑,每个人都会反复踩中,标准的缓解办法是在终端的启动配置文件(如 ~/.bashrc)里加载它——但要注意,当你确实有两个工作空间时,这样做反而会把问题掩盖起来。