Skip to content

ROS2 与 DDS 互操作深度解析

背景

ROS2 的通信后端是 DDS,但"用了 DDS"不等于"DDS 之间天然互通"。要让一个非 ROS2 的 DDS 节点(如 dds-wrapper)与 ROS2 互通,必须在 wire 层逐项对齐 ROS2 的约定。本文记录这些约定的原理、跨 DDS 互通的根本限制,以及 QoS 容错机制。

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

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

每种 DDS 后端对应一个 typesupport 包:

DDS 后端 typesupport 包 序列化方式
FastDDS rosidl_typesupport_fastrtps_cpp 生成直接调 Fast-CDR 的代码
CycloneDDS rosidl_typesupport_introspection_cpp 生成字段元数据,运行时遍历做通用 CDR 编解码
Connext rosidl_typesupport_connext_cpp 生成调 Connext CDR 的代码

ROS2 的不同 rmw_impl 之间能互通,是因为 typesupport 层统一了 wire 约定(type name 映射、CDR 编码细节、消息头格式)。绕过 typesupport 直接用 DDS 原生 API 的节点,只能与同一家 DDS(且 wire 约定一致)互通。

wire 层命名规则

与 ROS2 互通,wire 层必须遵守 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

enableRos2Compat(true) 启用这些前缀。注意前缀无尾斜杠,拼接为 make_fqtopic("rq", "/fibonacci/...send_goal", "Request")

类型名变换(rosidl_dds 规则)

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

  1. module dds_ 包装:struct 嵌套在额外 module dds_ { }
  2. struct 名尾下划线TestTest_
  3. 成员名尾下划线bool_valuebool_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/Headerstd_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 1:1 一个 struct;Service 1:2(request + response);Action 1:7+(goal + result + feedback + feedback_message + send_goal req/resp + get_result req/resp)。

跨 DDS 互通的根本限制

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

  1. type name:端点发现时用于匹配,不同 DDS 实现从同一份 IDL 生成的 type name 可能不同
  2. CDR 封装细节:string、sequence 等的对齐和编码
  3. TypeInformation 编码:CycloneDDS 用 XCDR2(符合 DDS-XTypes 1.3 §7.3.4.5),老版本 FastDDS 用 CDR1,解析方按错误格式读 → 崩溃(详见踩坑篇的 bad_alloc)
互通场景 是否可行
dds-wrapper ↔ dds-wrapper(同一 DDS 后端)
dds-wrapper ↔ ROS2 rmw_cyclonedds_cpp
dds-wrapper ↔ ROS2 rmw_fastrtps_cpp 否(TypeInformation 编码冲突 + typesupport 约定不同)
dds-wrapper ↔ 其他 DDS 原生节点 取决于 wire 约定是否逐项一致

这正是架构上选单供应商(CycloneDDS)的根本原因:多供应商互通需要自建 typesupport 统一层,成本远高于收益。

Service 头部注入(header_guid / header_seq)

ROS2 Service 的 wire 帧在 CDR payload 前有一段 header,由 rmw_cyclonedds 的序列化器 put_bytes 直接写入,不做对齐

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

非 ROS2 的 DDS 实现要模拟这个 wire 格式,需要把 header 当作 IDL 的前两个字段(header_guid + header_seq),并保证 CDR 字节布局与 put_bytes 一致。注意 idlcxx 对 uint64 会插入 8 字节对齐 padding,若与 put_bytes 的无对齐行为错位会反序列化失败——实测在特定版本组合下 padding 影响被吸收,但这是脆弱点。

QoS 与容错机制

ROS2 的三种容错 QoS

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

关键限制:Service 和 Action 在 ROS2 中没有原生的 Deadline/Liveliness 支持(Service deadline 仍是 ROS2 的 Future Work)。ROS2 Service 无法原生检测客户端断连(rclcpp#2241),server 永远不知道 client 死了。

补偿模式:用 心跳 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 节点只能与同 vendor 互通。
  • wire 命名三件事:topic 前缀(rt//rq//rr/)、类型名(dds_::Name_)、成员名尾下划线。ROS2 rosidl 生成的 IDL 成员名带尾下划线(goal_id_),CycloneDDS XTypes 可能检查 member name。
  • 跨 DDS 互通的前提是 wire 约定逐项一致:type name、CDR 编码、TypeInformation 编码。TypeInformation 的 XCDR2 vs CDR1 是 CycloneDDS 与老版 FastDDS 的死结。
  • int8/uint8 都映射 octet,符号信息在 IDL 层丢失。
  • Service wire 帧的 header(guid + seq)由序列化器 prepend,非对齐写入,模拟时要注意 idlcxx 的对齐 padding。
  • QoS 不兼容是静默失败:RELIABLE reader + BEST_EFFORT writer 会被 DDS 静默拒绝,永远收不到数据。
  • Service/Action 无原生 Deadline/Liveliness,靠心跳 Topic 补偿。

基于 VitePress 构建