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。三个结构性变换:
module dds_包装:struct 嵌套在额外module dds_ { }中- struct 名尾下划线:
Test→Test_ - 成员名尾下划线:
bool_value→bool_value_
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 → 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 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 层的所有约定完全一致。实际障碍有三:
- type name:端点发现时用于匹配,不同 DDS 实现从同一份 IDL 生成的 type name 可能不同
- CDR 封装细节:string、sequence 等的对齐和编码
- 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 补偿。