• 【ROS2原理7】中间件和接口(interface)


    一、前言

            本文描述了在 ROS 和特定中间件实现之间使用抽象中间件接口的基本原理。它将概述目标用例及其要求和约束。在此基础上解释了开发的中间件接口。

    二、中间件接口

    2.1 为什么ROS 2有中间件接口

            ROS 客户端库定义了一个 API,它向用户公开发布/订阅等通信概念。

            在 ROS 1 中,这些通信概念的实现基于自定义协议(例如 TCPROS)。

            对于 ROS 2,已决定将其构建在现有中间件解决方案(即 DDS)之上。这种方法的主要优点是 ROS 2 可以利用该标准的现有且开发良好的实现。

            ROS 可以建立在 DDS 的一种特定实现之上。但是有许多不同的实现可用,并且每种实现在支持的平台、编程语言、性能特征、内存占用、依赖关系和许可方面都有自己的优缺点。

            因此,ROS 旨在支持多个 DDS 实现,尽管它们每个实现的确切 API 略有不同。为了从这些 API 的细节中抽象出来,引入了一个抽象接口,可以为不同的 DDS 实现实现。这个中间件接口定义了 ROS 客户端库和任何特定实现之间的 API。

            接口的每个实现通常都是一个瘦适配器,它将通用中间件接口映射到中间件实现的特定 API。下面将省略适配器和实际中间件实现的共同分离。

    1. +-----------------------------------------------+
    2. | user land |
    3. +-----------------------------------------------+
    4. | ROS client library |
    5. +-----------------------------------------------+
    6. | middleware interface |
    7. +-----------------------------------------------+
    8. | DDS adapter 1 | DDS adapter 2 | DDS adapter 3 |
    9. +---------------+---------------+---------------+
    10. | DDS impl 1 | DDS impl 2 | DDS impl 3 |
    11. +---------------+---------------+---------------+

    2.2 为什么中间件接口与 DDS 无关

            ROS 客户端库不应向用户公开任何 DDS 实现细节。这主要是为了隐藏 DDS 规范和 API 的内在复杂性。

            虽然 ROS 2 仅旨在支持基于 DDS 的中间件实现,但它可以努力保持中间件接口不受 DDS 特定概念的影响,以便使用不同的中间件实现接口。通过将几个不相关的库捆绑在一起来实现接口也是可行的,这些库提供了发现、序列化和发布/订阅的必要功能。

    1. +-----------------------------------+
    2. | user land | no middleware implementation specific code
    3. +-----------------------------------+
    4. | ROS client library | above the interface
    5. +-----------------------------------+
    6. | middleware interface | ---
    7. +-----------------------------------+
    8. | mw impl 1 | mw impl 2 | mw impl 3 |
    9. +-----------+-----------+-----------+

    2.3 信息如何通过中间件接口流动

            中间件接口的一个目标是不向用户空间代码公开任何 DDS 特定代码。因此,中间件接口“之上”的 ROS 客户端库只需要对 ROS 数据结构进行操作。 ROS 2 将继续使用 ROS 消息文件来定义这些数据对象的结构,并从中派生出每种支持的编程语言的数据结构。

            中间件接口“下方”的中间件实现必须将从客户端库提供的 ROS 数据对象转换为自己的自定义数据格式,然后再将其传递给 DDS 实现。反过来,来自 DDS 实现的自定义数据对象必须在返回到 ROS 客户端库之前转换为 ROS 数据对象。

            中间件特定数据类型的定义可以从 ROS 消息文件中指定的信息导出。 ROS 消息的原始数据类型和中间件特定数据类型之间定义的映射确保了双向转换是可能的。在类型支持中封装了在 ROS 类型和实现特定类型或 API 之间进行转换的功能(请参阅下文了解不同类型的类型支持)。 

    1. +----------------------+
    2. | user land | 1) create a ROS message
    3. +----------------------+ v
    4. | ROS client library | 2) publish the ROS message
    5. +----------------------+ v
    6. | middleware interface | v
    7. +----------------------+ v
    8. | mw impl N | 3) convert the ROS message into a DDS sample and publish the DDS sample
    9. +----------------------+
    1. +----------------------+
    2. | user land | 3) use the ROS message
    3. +----------------------+ ^
    4. | ROS client library | 2) callback passing a ROS message
    5. +----------------------+ ^
    6. | middleware interface | ^
    7. +----------------------+ ^
    8. | mw impl N | 1) convert the DDS sample into a ROS message and invoke subscriber callback
    9. +----------------------+

            根据中间件的实现,可以通过直接从 ROS 消息实现序列化函数以及将反序列化函数实现为 ROS 消息来避免额外的转换。

    三、考虑的用例

    在设计中间件接口时考虑了以下用例:

    3.1 单一中间件实现

            ROS 应用程序不是以单一方式构建的,而是分布在多个包中。即使有了中间件接口,决定使用哪个中间件实现也会影响代码的重要部分。

            例如,定义 ROS 消息的包将需要提供与中间件特定数据类型之间的映射。天真的每个定义 ROS 消息的包都可能包含用于特定中间件实现的自定义(通常是生成的)代码。

            在提供 ROS 的二进制包(例如,Debian 包)的上下文中,这意味着其中很大一部分(至少所有包含消息定义的包)将特定于所选的中间件实现。

    1. +-----------+
    2. | user land |
    3. +-----------+
    4. |||
    5. +--------------+|+-----------------+
    6. | | |
    7. v v v
    8. +-----------+ +-----------+ +-----------------+ All three packages
    9. | msg pkg 1 | | msg pkg 2 | | middleware impl | on this level contain
    10. +-----------+ +-----------+ +-----------------+ middleware implementation specific code

    3.2 使用 DDS 的静态与动态消息类型

            DDS 有两种不同的方式来使用消息并与之交互。

            一方面,可以在 IDL 文件中指定消息,通常 DDS 实现特定程序将从该文件生成源代码。例如,为 C++ 生成的代码包含专门为消息生成的类型。

            另一方面,可以使用 XTypes 规范的 DynamicData API 以编程方式指定消息。在这种情况下,既不需要 IDL 文件也不需要代码生成步骤。

            一些自定义代码仍必须将 ROS .msg 文件中可用的消息定义映射到 DynamicData API 的调用。但是可以编写通用代码来执行任何传入的 ROS .msg 规范的任务。

    1. +-----------+
    2. | user land |
    3. +-----------+
    4. |||
    5. +--------------+|+----------------+
    6. | | |
    7. v v |
    8. +-----------+ +-----------+ | Each message provides its specification
    9. | msg pkg 1 | | msg pkg 2 | | in a way which allows a generic mapping
    10. +-----------+ +-----------+ | to the DynamicData API
    11. | | |
    12. +-------+-------+ |
    13. | |
    14. v v
    15. +-------------------------+ +-----------------+ Only the packages
    16. | msg spec to DynamicData | | middleware impl | on this level contain
    17. +-------------------------+ +-----------------+ middleware implementation specific code

    然而,与静态生成的代码相比,使用 DynamicData API 的性能可能总是较低。

    3.3 在不同的实现之间切换

            当 ROS 支持不同的中间件实现时,用户在它们之间切换应该尽可能简单和省力。

            1)在编译时决定
            一种明显的方法是让用户从源代码中选择特定的中间件实现来构建所有 ROS 包。虽然工作流程不会太困难(可能有六个命令行调用),但它仍然需要相当长的构建时间。

            为了避免从源代码构建的需要,可以提供一组二进制包,在构建时选择中间件实现。虽然这会减少用户的工作量,但 buildfarm 需要构建一组完全独立的二进制包。支持具有 M 个不同中间件实现的 N 个包的工作将需要大量资源 (M * N) 来维护服务以及必要的计算能力。

            2)在运行时决定
            另一种方法是支持在运行时选择特定的中间件实现。这要求要选择的中间件实现在编译时可用,以便为每个消息包生成中间件特定类型支持。

            使用单个中间件实现构建 ROS 时,结果应遵循设计标准:

            您不使用的任何功能都无需付费。

            这意味着由于能够支持不同的中间件实现,构建时间和运行时都不应该有开销。然而,由于中间件接口的附加抽象仍然有效,以便向用户隐藏实现细节。

    四、中间件接口设计

            API 设计为纯函数式接口,以便在 C 中实现。纯 C 接口可用于大多数其他语言(包括 Python、Java 和 C++)的 ROS 客户端库中,无需重新实现核心逻辑。

    4.1 发布者界面

            基于 ROS 节点、发布者和消息的一般结构,在发布消息的情况下,ROS 客户端库需要调用中间件接口上的三个函数:

    • create_node()
    • create_publisher()
    • publish()

    4.2 create_node 的基本签名

            create_publisher 的后续调用需要引用它们应该在其中创建的特定节点。因此 create_node 函数需要返回一个节点句柄,该句柄可用于标识节点。

    NodeHandle create_node();

    4.3 create_publisher 的基本签名

            除了节点句柄之外,create_publisher 函数还需要知道主题名称和主题类型。主题类型参数的类型暂时未指定。

            发布的后续调用需要引用他们应该发送消息的特定发布者。因此 create_publisher 函数需要返回一个发布者句柄,该句柄可以用来识别发布者。

          1)主题类型参数封装的信息高度依赖于中间件实现。

             DynamicData API 的主题类型信息 在实现中使用 DynamicData API 的情况下,没有可以表示类型信息的 C/C++ 类型。相反,主题类型必须包含描述消息格式所需的所有信息。这些信息包括:

    • 定义消息的包的名称
    • 消息的名称
    • 消息的字段列表,其中每个字段包括:
    • 消息字段的名称
    • 消息字段的类型(可以是内置类型或其他消息类型),可选类型可以是无界、有界或固定大小的数组
    • 默认值
    • 消息中定义的常量列表(同样由名称、类型和值组成)

             2)在使用 DDS 的情况下,此信息使您能够 

    • 以编程方式创建代表消息结构的 DDS TypeCode
    • 向 DDS 参与者注册 DDS 类型代码
    • 创建 DDS Publisher、DDS Topic 和 DDS DataWriter
    • 将 ROS 消息中的数据转换为 DDS DynamicData 实例
    • 将 DDS DynamicData 写入 DDS DataWriter

           3) 静态生成代码的主题类型信息
            在使用从 IDL 派生的静态生成代码的情况下,有代表类型信息的 C/C++ 类型。生成的代码包含以下功能:

    • 创建一个代表消息结构的 DDS Type Code

            由于必须在编译时定义特定类型,因此其他功能不能以通用(不是特定于实际消息)方式实现。因此,对于每条消息,必须分别生成执行以下任务的代码:

    • register the DDS TypeCode with the DDS Participant
    • create a DDS PublisherDDS Topic and DDS DataWriter
    • convert data from a ROS message into a DDS DynamicData instance
    • write the DDS DynamicData to the DDS DataWriter

            主题类型封装的信息必须包含调用这些函数的函数指针。

            4)get_type_support_handle
            由于主题类型参数封装的信息对于每个中间件实现都有根本的不同,它实际上是通过中间件接口的一个附加函数来检索的:

    MessageTypeSupportHandle get_type_support_handle();

            目前该函数是专门针对特定 ROS 消息类型的模板函数。为了与 C 兼容,将使用使用宏的方法,将类型名称更改为函数名称。

            5)发布的基本签名
            除了发布者句柄之外,发布函数还需要知道要发送的 ROS 消息。

    publish(PublisherHandle publisher_handle, .. ros_message);

            由于 ROS 消息没有公共基类,因此函数的签名不能使用已知类型来传递 ROS 消息。相反,它作为 void 指针传递,实际实现根据先前注册的类型重新解释它。

            6)返回的句柄类型
            返回的句柄需要为每个中间件实现封装任意内容。因此,从用户的角度来看,这些句柄只是不透明的对象。只有创建它的中间件实现知道如何解释它。

            为了确保将这些信息传递回相同的中间件实现,每个句柄都编码一个唯一标识符,中间件实现可以在解释句柄有效负载之前检查该标识符。

    4.4 订阅者界面

            本文档中(尚未)描述订户侧所需的接口的细节。

    4.5 可选地公开本机句柄

            RMW 接口仅公开与中间件无关的句柄。但是中间件实现可以选择提供额外的 API 来提供本机句柄。例如。对于给定的 ROS 发布者句柄,特定实现可以提供 API 来访问特定于实现的发布者相关句柄。

            虽然使用这样的特性会使用户登陆代码特定于中间件实现,但它提供了一种使用中间件实现特性的方法,这些特性不通过 ROS 接口公开。有关示例,请参阅此演示。

    五、当前实施

            所描述的概念已在以下包中实现:

    • 包 rmw 定义了中间件接口。
    • 这些函数在 rms/rmw.h 中声明。
    • 句柄在 rmw/types.h 中定义。
    • rosidl_typesupport_introspection_cpp 包生成的代码封装了每个 ROS msg 文件中的信息,从而使数据结构可以从 C++ 代码中自省。
    • 包 rmw_fastrtps_cpp 使用基于自省类型支持的 eProsima Fast-RTPS 实现中间件接口。
    • rosidl_generator_dds_idl 包基于 ROS msg 文件生成 DDS IDL 文件,这些文件被所有使用静态/编译类型消息类型的基于 DDS 的 RMW 实现所使用。
    • 包 rmw_connext_cpp 使用基于静态生成代码的 RTI Connext DDS 实现中间件接口。
    • rosidl_typesupport_connext_cpp 包生成:
    •                 基于每个消息的 IDL 文件的 DDS 特定代码

                           1) 附加代码以启用为每种消息类型调用 register/create/convert/write 函数

                            2)包 rmw_connext_dynamic_cpp 使用基于 Connext 的 DynamicData API 和自省类型支持的 RTI Connext DDS 实现中间件接口。

    • 包 rmw_opensplice_cpp 使用 PrismTech OpenSplice DDS 基于静态生成的代码实现中间件接口。
    • rosidl_typesupport_opensplice_cpp 包生成:

                    1)基于每个消息的 IDL 文件的 DDS 特定代码

                    2)附加代码以启用为每种消息类型调用 register/create/convert/write 函数

    • 包 rmw_implementation 提供了在中间件实现的编译时和运行时选择之间切换的机制。

                  1)如果在编译时只有一个实现可用,它会直接链接到它。

                  2)如果在编译时有多个实现可用,它会实现中间件接口本身,并根据策略模式通过在运行时加载由环境变量标识的特定中间件实现的共享库并传递所有调用。

    5.1 一种或多种类型的支持生成器

            参与每个消息的消息生成过程的包称为类型支持生成器。它们的包名称以前缀 rosidl_typesupport_ 开头。

            每个消息包都将包含从所有类型支持生成器生成的代码,这些代码在配置包时可用。这可以只有一个(在针对单个中间件实现构建时)或多个类型的支持生成器。

    5.2 DDS 和 ROS 概念之间的映射

            每个 ROS 节点都是一个 DDS 参与者。如果多个 ROS 节点在单个进程中运行,它们仍然映射到单独的 DDS 参与者。如果包含进程公开了自己的 ROS 接口(例如,在运行时将节点加载到进程中),它本身就充当 ROS 节点,因此也映射到单独的 DDS 参与者。

            ROS 发布者和订阅者映射到 DDS 发布者和订阅者。 DDS DataReader 和 DataWriter 以及 DDS 主题不通过 ROS API 公开。

            ROS API 定义了队列大小和一些服务质量参数,这些参数被映射到它们的 DDS 等效项。其他 DDS QoS 参数不会通过 ROS API 公开。

    ROS 2 middleware interface

  • 相关阅读:
    Linux系统配置及服务管理-07-文件系统及RAID
    【华为OD机试真题 JS】消消乐游戏
    【Axure视频教程】输入框控制滑动评分条
    Prometheus API 使用介绍|收藏
    【Java】注解 之 定义注解
    (二十七)张量表示定理 —— Cauchy 基本表示定理
    第一章-数据库的概述
    【openwrt学习笔记】新patch的制作和旧patch的修改
    【图解源码】Zookeeper3.7源码分析,包含服务启动流程源码、网络通信源码、RequestProcessor处理请求源码
    BERT-of-Theseus
  • 原文地址:https://blog.csdn.net/gongdiwudu/article/details/126209494