Skip to content

ROS2 与 DDS 互操作:wire 层约定与跨实现限制 ​

ROS2 的通信后端是 DDS,但「都用了 DDS」不等于天然互通。一个用原生 DDS API 写的节点(比如自研中间件、跨系统桥接程序)要与 ROS2 节点互相收发数据,必须在 wire 层逐项对齐 ROS2 的约定。本文整理这些约定的原理与对齐清单:序列化发生在哪一层、topic 与类型名的变换规则、跨 DDS 实现互通的根本限制、Service wire 帧的头部注入,以及 QoS 静默失配的排查思路。适合做中间件适配、跨系统集成或排查「端点都在却收不到数据」问题的读者。ROS2 内部的序列化管线细节(rosidl 代码生成、CDR 编码规则)另见本站《ROS2 消息序列化:从 .msg 到 CDR 字节流》,本文聚焦「外部节点如何对上」。

ROS2 的序列化架构:DDS 只是管道 ​

先建立一个关键认知:ROS2 没有使用 DDS 自带的序列化机制。序列化发生在 typesupport 层,由 rosidl 工具链生成,DDS 被降级为透明传输管道,只搬运 CDR 字节流,不参与编解码。

每种 DDS 后端对应一种 typesupport 策略:FastDDS 与 Connext 为每个消息类型生成专用序列化代码;CycloneDDS 走内省(introspection)路线,只生成字段元数据,运行时由通用函数遍历编解码。两种策略的对比展开在序列化一文中,这里只需要结论:

不同 rmw 实现的 ROS2 节点之间能互通,是因为 typesupport 层统一了 wire 约定——type name 映射、CDR 编码细节、消息头格式都一致。绕过 typesupport 直接用 DDS 原生 API 的节点,只能与 wire 约定逐项一致的对端互通。

wire 层命名规则 ​

与 ROS2 互通的节点要遵守 rmw_cyclonedds_cpp 的命名约定,分 topic 前缀和类型名两部分。

Topic 命名前缀 ​

通信模式 前缀 示例
Topic rt/ rt/fibonacci/_action/feedback
Service request rq/ rq/fibonacci/_action/send_goalRequest
Service response rr/ rr/fibonacci/_action/send_goalReply

前缀无尾斜杠,直接拼在话题名前。rmw_cyclonedds 内部用 make_fqtopic() 做这个拼接(源码位于 rmw_cyclonedds_cpp/src/rmw_node.cpp);原生 DDS 节点要么自己拼前缀,要么在封装层统一处理。

类型名变换(rosidl_dds 规则) ​

rosidl_generator_dds_idl 消费解析后的 IDL(不是原始 .msg),输出带尾下划线的 {MessageName}_.idl。三个结构性变换:

  1. module dds_ 包装:struct 嵌套在额外的 module dds_ { } 中
  2. struct 名尾下划线:Test → Test_
  3. 成员名尾下划线:bool_value → bool_value_
idl
module test_msgs {
module msg {
module dds_ {
struct Test_ {
    boolean bool_value_;
    octet   byte_value_;
    long    int32_value_;
};
};
};
};

类型名公式:::'.join(namespaces + ['dds_', name + '_'])。例如 std_msgs/msg/Header 对应 wire 层类型名 std_msgs::msg::dds_::Header_。

基本类型映射 ​

ROS 类型 DDS IDL 备注
int8 / uint8 octet DDS 无 signed byte,符号信息在 IDL 层丢失
int16 / uint16 short / unsigned short
int32 / uint32 long / unsigned long
int64 / uint64 long long / unsigned long long
string<=N string<N> 有界字符串
固定数组 <type> <name>_[<size>]
有界序列 sequence<<type>, <max>> <name>_

接口规模的展开也值得注意:一个 Message 对应一个 struct;一个 Service 拆成 request 与 response 两个;一个 Action 展开为七个接口——Goal、Result、Feedback 三个消息,send_goal、cancel_goal、get_result 三个服务,再加一个内层 FeedbackMessage。

跨 DDS 互通的根本限制 ​

DDS 实现之间理论上可以互通(都遵循 CDR/RTPS 标准),但前提是双方对 wire 层的所有约定完全一致。实际障碍主要有三个:

  1. type name:端点发现时靠它匹配,不同 DDS 实现从同一份 IDL 生成的 type name 可能不同
  2. CDR 封装细节:string、sequence 等类型的对齐和编码方式
  3. TypeInformation 编码:DDS-XTypes 1.3 规定 TypeObject 用 XCDR2(PL_CDR2)编码,较早的实现使用 CDR1——解析方按错误格式读取类型信息,轻则匹配失败,重则直接异常
互通场景 是否可行
原生节点 ↔ 原生节点(同一 DDS 后端) 是
原生节点 ↔ ROS2 rmw_cyclonedds_cpp 是(对齐上述约定后)
原生节点 ↔ ROS2 rmw_fastrtps_cpp 通常不可行(TypeInformation 编码冲突,加上 typesupport 约定的差异)
原生节点 ↔ 其他 DDS 实现的原生节点 取决于 wire 约定是否逐项一致

这也是很多团队在引入 ROS2 互通时统一 DDS 实现的原因:多实现互通意味着自行对齐全部 wire 约定并跟踪各实现的版本差异,通常成本高于收益。

Service 头部注入(header_guid / header_seq) ​

ROS2 Service 的 wire 帧在 CDR payload 前有一段额外头部,由 rmw_cyclonedds 的序列化器直接写入,不做字节对齐:

text
[RTPS header 4B][guid 8B][seq 8B][IDL payload...]
  • guid:DataWriter 的 dds_instance_handle_t(uint64_t)
  • seq:全局递增的 int64_t
  • IDL struct 本身不含这些字段,它们是序列化器额外 prepend 的

原生 DDS 实现要模拟这个 wire 格式,需要把头部当作 IDL 的前两个字段(header_guid + header_seq),并保证 CDR 字节布局与序列化器的无对齐写入一致。注意 idlcxx(CycloneDDS 的 IDL 编译器)对 uint64 会插入 8 字节对齐 padding,若与无对齐写入错位会反序列化失败——特定版本组合下 padding 影响可能恰好被吸收,但这是脆弱点,升级版本时要重新验证。

QoS 与容错机制 ​

ROS2 的三种容错 QoS ​

QoS 作用 触发 适用范围
Deadline 超时检测 期望时间内没收到新数据 Topic
Liveliness 存活检测 活跃性断言未在 lease 期内刷新 Topic
Lifespan 数据过期 数据存活超过设定时长被丢弃 Topic

关键限制:ROS2 的 Service 和 Action API 未暴露 Deadline/Liveliness 这类容错 QoS——它们只在 Topic 上可用。Service server 感知不到 client 的异常退出,Action 对端死亡时也不会触发回调(相关讨论见 ros2/rclcpp#2241,"No callbacks if client/server died",截至目前仍然 open)。该 issue 报告者的做法是补一条心跳 Topic 并配 Deadline QoS 检测对端存活,这也是社区通行的补偿模式。CycloneDDS 提供 WaitSet + StatusCondition + Listener 三种方式监听 QoS 违规,无需轮询。

QoS 兼容性是静默失败陷阱 ​

DDS 端点匹配时,reader 要求的可靠性不能高于 writer 提供的。例如 RELIABLE reader 订阅 BEST_EFFORT writer,QoS 不兼容,DDS 会静默拒绝匹配——不报错,只是 matched 计数为 0。这类问题排查起来很困难,表现为「端点都在但收不到数据」「wait_for_server 超时」。Action 各通道的 QoS 也必须逐项与 ROS2 对齐(feedback 通道是 RELIABLE 而非 BEST_EFFORT,status 通道是 TRANSIENT_LOCAL + depth=1),否则触发同样的静默失配。

要点 ​

  • ROS2 的序列化在 typesupport 层,DDS 只是管道。绕过 typesupport 的原生 DDS 节点,只能与 wire 约定逐项一致的对端互通。
  • wire 命名三件事:topic 前缀(rt/、rq/、rr/)、类型名(dds_::Name_ 形态)、成员名尾下划线。CycloneDDS 的 XTypes 匹配可能检查 member name,命名不一致会静默不匹配。
  • 跨 DDS 互通的前提是 wire 约定逐项一致:type name、CDR 编码、TypeInformation 编码。XCDR2 与 CDR1 的分歧是 CycloneDDS 与老版本 FastDDS 之间最常见的死结。
  • int8/uint8 都映射 octet,符号信息在 IDL 层丢失。
  • Service wire 帧的头部(guid + seq)由序列化器 prepend,非对齐写入;模拟时注意 idlcxx 的对齐 padding 与它错位的风险。
  • QoS 不兼容是静默失败:RELIABLE reader 撞上 BEST_EFFORT writer 会被 DDS 静默拒绝,永远收不到数据。
  • Service/Action 无原生 Deadline/Liveliness,通行做法是心跳 Topic 补偿。

参考资料 ​

最近更新

基于 VitePress 构建