【ROS2】Concept(Advanced )
一、The build system 构建系统
目录
- 构建工具(Build tool)
- 构建辅助工具(Build helpers)
ament_package
包ament_cmake
代码仓库ament_lint
代码仓库
- 元构建工具(Meta-build tool)
构建系统能让开发者根据需要构建 ROS 2(机器人操作系统 2)代码。ROS 2 高度依赖将代码划分为多个包(package)的方式,每个包都包含一个清单文件(package.xml
)。该清单文件包含包的关键元数据,包括其对其他包的依赖关系。元构建工具要正常运行,必须依赖此清单文件。
ROS 2 构建系统包含三大核心概念。
1、构建工具
构建工具是控制单个包编译和测试的软件。在 ROS 2 中,C++ 代码通常使用 CMake 作为构建工具,Python 代码通常使用 setuptools,但也支持其他构建工具。
2、构建辅助工具
构建辅助工具是一些辅助函数,它们可与构建工具结合使用,以提升开发者的使用体验。ROS 2 包通常依赖 ament
系列包来实现这一功能。ament
包含多个重要的代码仓库,这些仓库均位于 GitHub 组织中。
2.1 ament_package
包
该包位于 GitHub 上的 ament/ament_package 仓库,包含一个独立的 ament Python 包,可为 ament 包提供各类实用工具,例如环境钩子(environment hooks)模板。
无论底层使用何种构建系统,所有 ament 包的根目录下都必须包含一个 package.xml
文件。package.xml
这个“清单文件”包含处理和操作某个包所需的信息,这些包信息包括包的全局唯一名称、包的依赖关系等。此外,package.xml
文件还可作为标记文件,标识包在文件系统中的位置。
package.xml
文件的解析由 catkin_pkg
(与 ROS 1 中相同)负责,而通过在文件系统中搜索这些 package.xml
文件来定位包的功能,则由 colcon
等构建工具提供。
package.xml
包清单文件,用于标记包的根目录,并包含包的元信息,包括包名、版本、描述、维护者、许可证、依赖关系等。清单内容采用机器可读的 XML 格式,其规范在 REP 127 和 REP 140 中定义,未来的 REP(ROS 增强提案)可能会对其进行进一步修改。
因此,当某个包被称为“ament 包”时,意味着它是一个独立的软件单元(包含源代码、构建文件、测试文件、文档及其他资源),且通过 package.xml
清单文件进行描述。
ament 包
任何包含 package.xml
文件且遵循 ament
打包规范的包,无论其底层使用何种构建系统,都可称为 ament 包。
由于“ament 包”这一术语与构建系统无关,因此存在多种类型的 ament 包,例如 ament CMake 包、ament Python 包等。
以下是你在该软件栈中可能会遇到的常见包类型:
- CMake 包:任何包含普通 CMake 项目和
package.xml
清单文件的包。 - ament CMake 包:既包含 CMake 项目,又遵循
ament
打包规范的包。 - Python 包:任何包含基于 setuptools 的 Python 项目和
package.xml
清单文件的包。 - ament Python 包:既包含 Python 项目,又遵循
ament
打包规范的包。
2.2ament_cmake
代码仓库
该仓库位于 GitHub 上的 ament/ament_cmake 仓库,包含多个“ament CMake”包和纯 CMake 包,这些包为创建“ament CMake”包提供了 CMake 所需的基础架构。在此语境下,“ament CMake”包指的是:使用 CMake 构建的 ament
包。因此,该仓库中的包提供了必要的 CMake 函数/宏和 CMake 模块,以助力创建更多“ament CMake”(或 ament_cmake
)包。此类包可通过 package.xml
文件中 <export>
标签内的 <build_type>ament_cmake</build_type>
标签进行标识。
该仓库中的包具有高度模块化的特点,但存在一个名为 ament_cmake
的“核心”包。开发者只需依赖 ament_cmake
包,即可获取该仓库中所有包的综合功能。以下是该仓库中的包列表及简要描述:
ament_cmake
:聚合该仓库中的所有其他包,开发者只需依赖此包即可。ament_cmake_auto
:提供便捷的 CMake 函数,可自动处理编写包的CMakeLists.txt
文件时的大量繁琐工作。ament_cmake_core
:为ament
提供所有内置核心功能,例如环境钩子、资源索引、符号链接安装等。ament_cmake_gmock
:提供便捷函数,用于创建基于 gmock 的单元测试。ament_cmake_gtest
:提供便捷函数,用于创建基于 gtest 的自动化测试。ament_cmake_nose
:提供便捷函数,用于创建基于 nosetests 的 Python 自动化测试。ament_cmake_python
:为包含 Python 代码的包提供 CMake 函数(详见 ament_cmake_python 用户文档)。ament_cmake_test
:通过 CTest 将各类测试(如 gtest、nosetests)聚合到单个目标下。
ament_cmake_core
包包含大量 CMake 基础架构,这些架构能让包之间通过通用接口清晰地传递信息。这使得各个包与其他包的构建接口耦合度更低,不仅提高了包的可复用性,还推动了不同包在构建系统方面的规范统一。例如,它提供了一种标准方式,可在包之间传递包含目录、库、定义和依赖关系等信息,以便需要这些信息的包能通过通用方式获取。
ament_cmake_core
包还提供 ament
构建系统的部分功能,如符号链接安装。该功能允许将源空间或构建空间中的文件通过符号链接的方式链接到安装空间,而非直接复制文件。这意味着开发者只需执行一次安装操作,之后修改 Python 代码、配置文件等非生成资源时,无需重新执行安装步骤,修改即可生效。该功能在很大程度上替代了 catkin
(ROS 1 中的构建系统)中的“开发空间(devel space)”,因为它不仅具备“开发空间”的大部分优势,还避免了其诸多复杂性和缺陷。
ament_cmake_core
包提供的另一项功能是“包资源索引”,该功能可让包标识自身包含某种类型的资源。此功能的设计使得回答“某个前缀(如 /usr/local
)下有哪些包”这类简单问题的效率大幅提升——只需列出该前缀下某个特定位置的文件即可。如需了解该功能的更多信息,可查阅资源索引的设计文档。
与 catkin
类似,ament_cmake_core
也提供环境设置文件和包专属的环境钩子。环境设置文件(通常命名为 setup.bash
之类)是包开发者定义使用该包所需环境变量修改的载体。开发者可通过“环境钩子”实现这一目的——“环境钩子”本质上是一段任意的 Shell 代码,可用于设置或修改环境变量、定义 Shell 函数、配置自动补全规则等。例如,ROS 1 就是通过该功能设置 ROS_DISTRO
环境变量的,而无需 catkin
了解任何关于 ROS 发行版的信息。
2.3ament_lint
代码仓库
该仓库位于 GitHub 上的 ament/ament_lint 仓库,包含多个包,这些包以便捷、统一的方式提供代码检查(linting)和测试服务。目前,这些包支持的功能包括:使用 uncrustify
进行 C++ 代码风格检查、使用 cppcheck
进行 C++ 代码静态检查、检查源代码中的版权信息、使用 pep8
进行 Python 代码风格检查等。未来,这类辅助包的数量可能会进一步增加。
3、元构建工具
元构建工具是一款能够对一组包进行拓扑排序,并按照正确的依赖顺序构建或测试这些包的软件。该软件会调用“构建工具”来执行包的编译、测试和安装等实际操作。
在 ROS 2 中,用于实现这一功能的工具是 colcon
。
二、Internal ROS 2 interfaces
目录
- 内部 API 架构概述
- 特定类型接口
- 静态类型支持
- 基于 DDS 的静态类型支持
- 动态类型支持
rcl
代码仓库rmw
代码仓库rosidl
代码仓库rcutils
代码仓库
ROS 2 内部接口是公开的 C 语言 API,主要供开发人员用于创建客户端库或添加新的底层中间件,而非供普通 ROS 用户使用。ROS 客户端库提供了大多数 ROS 用户所熟悉的面向用户的 API,且这些 API 可能支持多种编程语言。
1、内部 API 架构概述
ROS 2 主要包含两类内部接口:
- ROS 中间件接口(
rmw
API) - ROS 客户端库接口(
rcl
API)
rmw
API 是 ROS 2 软件栈与底层中间件实现之间的接口。ROS 2 所使用的底层中间件要么是 DDS(数据分发服务)实现,要么是 RTPS(实时发布-订阅协议)实现,主要负责服务发现、发布-订阅机制、服务的请求-响应机制以及消息类型的序列化。
rcl
API 是层次稍高的 API,用于实现客户端库,它不直接与中间件实现交互,而是通过 ROS 中间件接口(rmw
API)这一抽象层进行交互。
ROS 2 软件栈
如图所示,这些 API 呈分层结构:普通 ROS 用户会使用客户端库 API(如 rclcpp
)来编写代码(可执行文件或库)。客户端库(如 rclcpp
)的实现依赖 rcl
接口,rcl
接口提供对 ROS 计算图及计算图事件的访问能力。rcl
的实现则通过 rmw
API 访问 ROS 计算图。
rcl
实现的目的是为各类客户端库提供通用的复杂 ROS 概念与工具实现,同时保持对底层所用中间件的无关性(即不依赖特定中间件)。rmw
接口的目的是提炼出支持 ROS 客户端库所需的最基础中间件功能。最后,rmw
API 的实现由特定于中间件实现的包(如 rmw_fastrtps_cpp
)提供,该包的库会基于特定厂商的 DDS 接口和类型进行编译。
在上述架构图中,还有一个标为 ros_to_dds
的模块,该模块代表一类特殊包,这类包允许用户通过 ROS 对应的对象和设置来访问 DDS 厂商特定的对象与配置。这种抽象接口的目标之一是让 ROS 用户空间代码与所用中间件完全隔离,从而使切换 DDS 厂商甚至中间件技术时,对用户代码的影响降至最低。
然而,我们也意识到,在某些情况下,即便可能存在不良后果,深入到中间件实现内部手动调整设置仍有其价值。通过要求必须使用这类 ros_to_dds
包才能访问底层 DDS 厂商的对象,可避免在常规接口中暴露厂商特定的符号与头文件。同时,通过检查包的依赖关系,查看是否使用了某类 ros_to_dds
包,就能轻松识别哪些代码可能违背了厂商可移植性原则。
2、特定类型接口
在整个 API 体系中,部分 API 功能必然与所传输的消息类型相关(例如发布消息或订阅话题),因此需要为每种消息类型生成对应的代码。下图展示了从用户定义的 rosidl
文件(如 .msg
文件)到用户及系统用于执行特定类型功能的特定类型代码的生成流程:
图:“静态”类型支持生成流程图(从 rosidl
文件到面向用户的代码)
上图右侧展示了 .msg
文件如何直接传递给特定语言的代码生成器(如 rosidl_generator_cpp
或 rosidl_generator_py
)。这些生成器负责生成用户需包含(或导入)的代码,这些代码是 .msg
文件中定义的消息在内存中的表示形式。例如,对于 std_msgs/String
消息,C++ 用户可通过 #include <std_msgs/msg/string.hpp>
语句使用该消息,Python 用户则可通过 from std_msgs.msg import String
语句使用——这些语句之所以能生效,正是得益于这些特定于语言(但与中间件无关)的生成器包所生成的文件。
此外,.msg
文件还用于为每种类型生成类型支持代码。在此语境下,“类型支持”指的是特定于某一类型的元数据或函数,系统可借助这些元数据或函数为该类型执行特定任务。某一消息的类型支持可能包括消息中每个字段的名称和类型列表,也可能包含指向特定功能代码的引用(如用于发布该消息的代码)。
静态类型支持
当类型支持引用的代码需为特定消息类型执行特定功能时,这些代码有时需要完成中间件特定的操作。例如,对于特定类型的发布函数:使用“厂商 A”的中间件时,该函数需调用“厂商 A”的 API;使用“厂商 B”的中间件时,则需调用“厂商 B”的 API。
为支持中间件厂商特定代码,用户定义的 .msg
文件可能会生成厂商特定的代码。但通过类型支持抽象层(其原理类似“私有实现(Pimpl)”设计模式),这些厂商特定代码仍对用户不可见。
基于 DDS 的静态类型支持
对于基于 DDS 的中间件厂商(尤其是那些基于 OMG IDL 文件(.idl
文件)生成代码的厂商),用户定义的 rosidl
文件(.msg
文件)会先转换为等效的 OMG IDL 文件(.idl
文件)。基于这些 OMG IDL 文件,会生成厂商特定代码,这些代码随后会用于特定类型的函数中——而这些特定类型的函数又由某一类型的类型支持所引用。
如上图左侧所示:rosidl_dds
包处理 .msg
文件以生成 .idl
文件,之后这些 .idl
文件会被传递给特定于语言和 DDS 厂商的类型支持生成包。
以 Fast DDS 实现为例,其对应的包为 rosidl_typesupport_fastrtps_cpp
。该包负责生成相关代码,以完成诸如将 C++ 消息对象转换为用于网络传输的序列化字节缓冲区等操作。尽管这些代码是 Fast DDS 特定的,但由于类型支持代码中的抽象层,用户仍无法直接访问这些代码。
动态类型支持
实现类型支持的另一种方式是为“发布话题”等操作提供通用函数,而非为每种消息类型生成一个专属函数版本。要实现这一点,通用函数需要获取所发布消息类型的元信息(例如消息类型中字段的名称、类型及其排列顺序)。
发布消息时,只需调用通用发布函数,传入待发布的消息以及包含该消息类型必要元信息的结构体即可。这种方式被称为“动态”类型支持,与“静态”类型支持形成对比——静态类型支持需要为每种类型生成专属的函数版本。
图:“动态”类型支持生成流程图(从 rosidl
文件到面向用户的代码)
上图展示了从用户定义的 rosidl
文件到生成面向用户代码的流程。该流程与静态类型支持的流程非常相似,唯一区别在于类型支持的生成方式(如上图左侧所示)。在动态类型支持中,.msg
文件会直接转换为面向用户的代码。
这类代码同样与中间件无关,因为它仅包含消息的元信息。实际执行“发布话题”等操作的函数是消息类型通用的,会根据需要调用中间件特定的 API。
需要注意的是:静态类型支持中,类型支持代码由特定于 DDS 厂商的包提供;而动态类型支持中,每种语言都有一个与中间件无关的包(如 rosidl_typesupport_introspection_c
和 rosidl_typesupport_introspection_cpp
)。包名中的“introspection”(自省)指的是借助为消息类型生成的元信息,对任意消息实例进行自省(即查看消息内部结构与数据)的能力——这是实现“发布话题”等通用函数的核心基础。
动态类型支持的优势在于:所有生成的代码都与中间件无关,只要其他中间件支持动态类型支持,这些代码就能复用于不同的中间件实现;同时,生成的代码量更少,可缩短编译时间并减小代码体积。
然而,动态类型支持要求底层中间件也支持类似的动态类型机制。以 DDS 为例,DDS-XTypes 标准允许通过元信息而非生成的代码发布消息,因此要支持动态类型支持,底层中间件必须具备 DDS-XTypes 或类似功能。此外,动态类型支持的性能通常低于静态类型支持:静态类型支持中,特定于类型的生成代码可被优化得更高效,无需通过遍历消息类型的元信息来完成序列化等操作。
3、rcl
代码仓库
ROS 客户端库接口(rcl
API)可供客户端库(如 rclc
、rclcpp
、rclpy
等)使用,以避免逻辑与功能的重复开发。通过复用 rcl
API,客户端库的体积可更小,且彼此间的一致性更强。
客户端库的部分功能有意未纳入 rcl
API,因为这些功能应采用符合语言习惯的方式实现。例如,执行模型完全未在 rcl
中定义:C 语言客户端库可使用 pthreads
,C++11 客户端库可使用 std::thread
,Python 客户端库可使用 threading.Thread
——这些均为符合各语言习惯的实现方式。
通常而言,rcl
接口提供的函数既不依赖特定语言模式,也不绑定特定消息类型。
rcl
API 位于 GitHub 上的 ros2/rcl 代码仓库中,以 C 语言头文件的形式提供接口定义。rcl
的 C 语言实现由同一代码仓库中的 rcl
包提供,该实现不直接与中间件交互,而是通过 rmw
和 rosidl
API 完成交互。
如需查看 rcl
API 的完整定义,请参阅 rcl 文档。
4、rmw
代码仓库
ROS 中间件接口(rmw
API)是在中间件之上构建 ROS 所需的最基础中间件原语功能集合。不同中间件实现的提供者必须实现该接口,才能支持在其之上运行完整的 ROS 栈。目前,所有中间件实现均对应不同的 DDS 厂商。
rmw
API 位于 GitHub 上的 ros2/rmw 代码仓库中。rmw
包包含定义该接口的 C 语言头文件,接口的实现则由支持不同 DDS 厂商的各类 rmw
实现包提供。
如需查看 rmw
API 的定义,请参阅 rmw 文档。
5、rosidl
代码仓库
rosidl
API 包含若干与消息相关的静态函数和类型,同时定义了消息在不同语言中应生成的代码格式。API 中规定的生成消息代码是特定于语言的,但可能会复用其他语言的生成代码,也可能不会。
API 规定的生成消息代码包括消息数据结构、构造与析构函数等内容。此外,API 还会实现获取消息类型支持结构体的方法——在发布或订阅某一消息类型的话题时,需使用该结构体。
多个代码仓库共同参与 rosidl
API 的定义与实现,其中核心的 ros2/rosidl 代码仓库(位于 GitHub)的功能包括:
- 定义消息 IDL 语法(即
.msg
文件、.srv
文件等的语法); - 提供用于解析这些文件的包;
- 提供用于从消息生成代码的 CMake 基础架构;
- 生成与实现无关的代码(头文件和源文件);
- 确定默认的代码生成器集合。
该代码仓库包含以下包:
rosidl_cmake
:提供用于从rosidl
文件(如.msg
文件、.srv
文件等)生成代码的 CMake 函数与模块。rosidl_default_generators
:定义默认生成器列表(确保这些生成器作为依赖被安装),同时也支持使用其他注入式生成器。rosidl_generator_c
:提供用于为rosidl
文件生成 C 语言头文件(.h
)的工具。rosidl_generator_cpp
:提供用于为rosidl
文件生成 C++ 头文件(.hpp
)的工具。rosidl_generator_py
:提供用于为rosidl
文件生成 Python 模块的工具。rosidl_parser
:提供用于解析rosidl
文件的 Python API。
其他语言的生成器(如 rosidl_generator_java
)托管在外部(位于不同代码仓库中),但它们会采用与上述生成器相同的机制,将自身“注册”为 rosidl
生成器。
除上述用于解析 rosidl
文件并生成头文件的包外,ros2/rosidl 代码仓库还包含用于为文件中定义的消息类型提供“类型支持”的包。“类型支持”指的是解析和操作特定类型 ROS 消息实例所表示信息的能力(例如发布消息)。
类型支持的实现方式有两种:
- 编译时生成代码:在编译阶段生成支持代码;
- 运行时解析:基于
rosidl
文件(如.msg
或.srv
文件)的内容以及接收的数据,通过数据自省(introspection)以编程方式实现。
对于第二种方式(通过消息的运行时解析实现类型支持),ROS 2 生成的消息代码可与 rmw
实现无关。通过数据自省提供类型支持的包包括:
rosidl_typesupport_introspection_c
:提供用于生成支持rosidl
消息数据类型的 C 语言代码的工具。rosidl_typesupport_introspection_cpp
:提供用于生成支持rosidl
消息数据类型的 C++ 代码的工具。
若需通过编译时生成代码(而非编程方式)实现类型支持,则需使用特定于 rmw
实现的包。这是因为通常某一 rmw
实现会要求数据以特定于 DDS 厂商的方式存储和操作,才能被 DDS 实现正常使用。如需了解更多细节,请参阅上文的“特定类型接口”部分。
如需了解 rosidl
API(静态部分与生成部分)的具体内容,请参阅 此页面。
6、rcutils
代码仓库
ROS 2 C 语言工具库(rcutils
)是一套 C 语言 API,由宏、函数和数据结构组成,广泛用于 ROS 2 代码库中。这些工具主要用于错误处理、命令行参数解析和日志记录,它们不特定于客户端层或中间件层,可被这两层共同复用。
rcutils
的 API 与实现位于 GitHub 上的 ros2/rcutils 代码仓库中,以 C 语言头文件的形式提供接口定义。
三、ROS 2 middleware implementations
目录
- DDS 中间件实现的通用包
- DDS 中间件实现的结构
- Zenoh 中间件实现的结构
ROS 2 中间件实现是一组包,为 ROS 2 提供底层通信框架。这些包与 ROS 2 核心接口(如 rmw
、rcl
和 rosidl
API)交互,以集成 Zenoh、DDS 等外部协议。例如,rmw_fastrtps_cpp
将 eProsima 的 Fast DDS 实现适配到 ROS 2 的中间件 API,而 rmw_zenoh_cpp
则为 Zenoh 协议提供类似的集成功能。
1、DDS 中间件实现的通用包
许多 ROS 2 中间件解决方案基于完整或部分 DDS 实现构建,例如使用 RTI 的 Connext DDS、eProsima 的 Fast DDS 以及 GurumNetworks 的 GurumDDS 的中间件实现。这些基于 DDS 的实现共享一些通用包和模式。
在 GitHub 的 ros2/rosidl_dds 代码仓库中,包含以下包:
rosidl_generator_dds_idl
:提供从rosidl
文件(如.msg
文件、.srv
文件等)生成 DDS.idl
文件的工具。
rosidl_generator_dds_idl
包会为 ROS 包中所有 ROS 接口定义文件(.msg
、.srv
、.action
等)生成对应的 DDS .idl
文件。这些接口定义文件指定了 ROS 2 中话题、服务和动作所使用的数据结构。随后,基于 DDS 的 ROS 中间件实现会利用这些生成的 .idl
文件,创建特定于厂商的预编译类型支持。
2、DDS 中间件实现的结构
一个基于 DDS 的 ROS 中间件实现,其单个代码仓库中通常包含(但不限于)以下包:
<implementation_name>_cmake_module
:包含用于发现和暴露所需依赖项的 CMake 模块。rmw_<implementation_name>_<language>
:包含特定语言(通常为 C++)的 RMW API 实现。rosidl_typesupport_<implementation_name>_<language>
:包含为rosidl
文件生成静态类型支持代码的工具,代码会适配特定语言(通常为 C 或 C++)的实现。
1. <implementation_name>_cmake_module
包
该包包含中间件实现所需的所有 CMake 模块和函数,用于查找支持性依赖项。例如:
rti_connext_dds_cmake_module
为 RTI Connext DDS 自带的 CMake 模块提供封装逻辑,确保所有依赖它的包都会选择同一版本的 RTI Connext DDS 安装包。- 类似地,
fastrtps_cmake_module
包含用于查找 eProsima Fast DDS 的 CMake 模块,gurumdds_cmake_module
包含用于查找 GurumNetworks GurumDDS 的 CMake 模块。
并非所有实现都需要此类包:例如,Eclipse 的 Cyclone DDS 已自带 CMake 模块,其 RMW 实现可直接使用该模块,无需额外封装。
2. rmw_<implementation_name>_<language>
包
该包以特定语言实现 rmw
C API。即便实现本身使用 C++ 编写,也必须将头文件中的符号声明为 extern "C"
,以便 C 应用程序能链接该实现。
3. rosidl_typesupport_<implementation_name>_<language>
包
该包提供一个生成器,用于生成特定语言的 DDS 代码。生成过程会用到两部分资源:
rosidl_generator_dds_idl
包生成的.idl
文件;- DDS 厂商提供的 DDS IDL 代码生成器。
此外,该包还会生成用于在 ROS 消息结构与 DDS 消息结构之间进行转换的代码。同时,它还负责为所使用的消息包创建一个共享库,该库既特定于消息包中的消息,也特定于所使用的 DDS 厂商。
补充说明
如前文所述,若某一 RMW 实现支持消息的运行时解析,则可使用 rosidl_typesupport_introspection_<language>
(而非特定于厂商的类型支持包)。这种无需预先生成代码、即可通过编程方式在话题上发送和接收类型的能力,是通过支持 DDS X-Types 动态数据标准实现的。因此,RMW 实现既可以提供对 X-Types 标准的支持,也可以提供一个特定于其 DDS 实现的、在编译时生成类型支持的包。
DDS RMW 实现代码仓库示例
- Eclipse Cyclone DDS 的 ROS 中间件实现:GitHub 地址为 ros2/rmw_cyclonedds。
- Fast DDS 的 RMW 实现:GitHub 地址为 ros2/rmw_fastrtps_cpp。
- Connext DDS 的 RMW 实现:GitHub 地址为 ros2/rmw_connextdds。
- GurumDDS 的 RMW 实现:GitHub 地址为 ros/rmw_gurumdds。
3、Zenoh 中间件实现的结构
要通过 ROS 2 基于 Zenoh 发送和接收数据,中间件包 rmw_zenoh_cpp
会借助 zenoh-c 将 ROS 2 中间件 API 映射到 Zenoh 的 API。与基于 DDS 的实现不同,该中间件依赖 Zenoh 路由器(router)来发现对等节点(peer),并通过 Zenoh 的“ gossip scouting”( gossip 侦察)传递发现信息。因此,rmw_zenoh_cpp
要求 Zenoh 路由器(zenohd
)在本地系统上处于活跃状态,或可通过网络访问。
核心映射逻辑
在 ROS 2 与 Zenoh 的集成中,每个上下文(context)会映射到一个单独的 Zenoh 会话(session)。该会话会在该上下文中的所有发布者、订阅者、服务和客户端之间共享。上下文会维护一个本地计算图缓存,用于跟踪 ROS 2 实体的网络拓扑;每个实体的存在状态由创建时分配、销毁时撤销的唯一活性令牌(liveliness token)管理。
ROS 2 实体与 Zenoh 的映射方式(非详尽列表)
1. 节点(Nodes)
ROS 2 中的节点是计算图中的“计算单元”,每个节点应负责单一、模块化的功能。Zenoh 中没有与节点直接对应的概念,因此 rmw_zenoh_cpp
不会为节点创建 Zenoh 实体。但通过 RMW API 创建节点时,会声明一个类型为 NN
的活性令牌。
2. 发布者(Publishers)
ROS 2 发布者向特定话题发送数据。由于 Zenoh 发布者通过“键(Key)”实现的功能与之非常相似,rmw_zenoh_cpp
会将这两种实体直接映射。通过 RMW API 创建发布者时,会声明一个类型为 MP
的活性令牌。
3. 订阅者(Subscribers)
ROS 2 订阅者监听话题以获取新数据,其概念与 Zenoh 中的订阅者完全一致,因此 rmw_zenoh_cpp
会将这两种实体直接映射。当有新数据到达时,Zenoh 中间件包会调用一个内部回调函数,该函数获取数据所有权并向 rmw_wait
发送可用性信号。通过 RMW API 创建订阅者时,会声明一个类型为 MS
的活性令牌。
4. 服务客户端(Service Clients)
rmw_zenoh_cpp
使用 Zenoh 的“可查询对象(queryables)”实现 ROS 2 服务。在 ROS 2 中,客户端通过 rmw_send_request
发送请求,请求中会携带用于关联响应的元数据(如序列号、发送请求的客户端的 GUID)。随后,Zenoh 中间件包可使用 z_get
向网络中发送查询。通过 RMW API 创建客户端时,会声明一个类型为 SC
的活性令牌。
5. 服务端(Service Server)
rmw_zenoh_cpp
同样使用 Zenoh 的“可查询对象”实现 ROS 2 服务。ROS 2 节点通过 rmw_create_service
向网络宣告服务,同时会调用 Zenoh API 的 z_declare_queryable
来创建 ROS 2 服务的服务端表示。rmw_take_request
会将查询传递给用户回调函数进行处理;计算完成后,rmw_send_response
会将结果返回给请求者。创建服务端时,会声明一个类型为 SS
的活性令牌。
Zenoh RMW 实现代码仓库
Zenoh 的 RMW 实现:GitHub 地址为 ros/rmw_zenoh。