ROS2 DDS中间件(图文并茂+超详细)

最近项目中使用到了 ROS2 作为底层开发框架,而 ROS2 底层改为使用 DDS 来实现,因此这里对 DDS 相关的内容进行总结。

ROS2 选择 DDS 作为底层框架的原因及相关评估见 ROS on DDS。总结一下就是项目组认为 DDS 是一个定义良好的端到端的网络通信中间件,可以简化自己构建端到端中间件的工作量,包括代码的工作量和文档的工作量。而且这种已经规范化的中间件可以使 ROS 更好的被评审,审计以及更方便和其他系统互操作。

DDS(Data Distribution Service) 是由 Object Management Group (OMG) 最早于2004年发布的一种基于发布-订阅(publish-subscribe)模式的端到端网络通信中间件,它可用于实时系统中实现可靠,高性能,可扩展,可互操作的数据传输。

DDS 只是一个标准以及一套API定义,各厂商可以根据该标准实现自己的 DDS,这有点类似于 OpenGL。比较有名的 DDS 实现包括 eProsima FastDDS,Eclipse Cyclone DDS,RTI Context,ADLINK Opensplice等。ROS2 对它们的支持情况如下表所示:

见: About different ROS 2 DDS/RTPS vendors

可以根据需要配置 ROS2 使用不同的 DDS 实现。

DDS 标准定义见 About the Data Distribution Service Specification Version 1.4

DDS C++ API 定义见 About the ISO/IEC C++ 2003 Language DDS PSM Specification Version 1.0

DDS 是完全分布式的,没有中心节点,任何两个节点之间都是直接通信的,示意图如下:

为了实现这样的系统,首先要解决的是各节点的互相发现问题。DDS 本身并没有定义应该如何发现各节点,而是在 RTPS 中定义该行为。RTPS(Real-time Publish-Subscribe Protocol) 用来支撑 DDS 的实现,是 DDS 底层使用的协议,用来保证各种 DDS 的实现之间可以互操作。在 RTPS 中定义了两个独立的发现协议,分别是 PDP(Participant Discovery Protocol) 和 EDP(Endpoint Discovery Protocol),PDP 用于发现各参与节点,EDP 用于发现每个节点提供的所有端点(Endpoints)。PDP 的实现可以通过单播列表或者组播实现。RTPS 允许实现者实现不同的 PDP 和 EDP 协议,但是至少要实现 SPDP(Simple PDP) 和 SEDP(Simple EDP) 协议。关于协议的具体规定见 RTPS 规范

在 ROS2 的实现中,默认采用的 DDS 实现为 eProsima FastDDS。eProsima FastDDS 是 Apache2 的开源协议,代码托管在 github 上 eProsima/Fast-DDS。文档见 DDS API — Fast DDS 2.0.0 documentation。eProsima 提供的一张图可以很好的来表示 DDS 的实现原理:

可以看到,DDS 基于 Topic 来实现发布-订阅模式,而且没有中心节点。是一种很好的端到端分布式通信中间件。ROS2 使用它提供了面向机器人领域的开发框架。

DDS 仅提供给予发布-订阅模式的通信,有些框架对它进行了包装,在发布-订阅的基础上提供了RPC的能力,比如同样来自 OMG 的 DDS-RPC,以及来自 eProsima 的 eProsima RPC over DDS

将基于发布订阅模式的 DDS 包装成 RPC 的方法如下:

每个 RPC 调用都会是用2个 Topic 来实现,一个用于发送请求,一个用于接收回复。更详细的信息可以参考官方文档。

总结

DDS 的目标场景在端上,致力于使端上的应用可以更容易的进行通信,由于它的完全分布式架构,可以方便的将端上的应用拆分为多进程架构,从而提高端的容错能力。从产品定位角度来讲,它和 Chromium 中的 mojo 都是端上的通信中间件,只不过 mojo 不强调分布式,内部集成了更多的特定于端上的机制,比如句柄的传输,消息的缓存等。但由于 mojo 生于 Chromium,目前官方并没有把它独立出来,所以用起来比较不方便。而 DDS 或许可以在简单的场景下作为 mojo 的替代品。

一些代码实现

以下是使用 ROS2 标准方式(基于 .msg 文件 + rclcpp)重构的代码示例,保留了核心功能但完全遵循 ROS2 抽象层的最佳实践:


1. 创建 ROS2 包和消息定义

1.1 创建工作空间
mkdir -p ros2_ws/src && cd ros2_ws/src
ros2 pkg create helloworld_demo --build-type ament_cmake
1.2 定义消息类型

在 ros2_ws/src/helloworld_demo/msg/HelloWorld.msg 中添加:

.msg格式

uint32 id
string message
1.3 修改 package.xml
<!-- 在 package.xml 中添加以下依赖 -->
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
1.4 修改 CMakeLists.txt
# 在 CMakeLists.txt 中添加以下内容
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/HelloWorld.msg"
)

# 发布者节点
add_executable(publisher src/publisher.cpp)
target_include_directories(publisher PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_cpp)
ament_target_dependencies(publisher
  rclcpp
  std_msgs
  rosidl_default_runtime
)

# 订阅者节点
add_executable(subscriber src/subscriber.cpp)
target_include_directories(subscriber PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_cpp)
ament_target_dependencies(subscriber
  rclcpp
  std_msgs
  rosidl_default_runtime
)

# 安装和导出
install(TARGETS publisher subscriber
  DESTINATION lib/${PROJECT_NAME}
)
rosidl_export_interfaces(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)

ament_package()

2. 实现发布者节点

src/publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "helloworld_demo/msg/hello_world.hpp"
#include <chrono>

using namespace std::chrono_literals;

class PublisherNode : public rclcpp::Node {
public:
  PublisherNode() : Node("hello_publisher"), count_(0) {
    publisher_ = this->create_publisher<helloworld_demo::msg::HelloWorld>(
      "hello_topic",  // 话题名称
      10  // QoS历史深度
    );

    timer_ = this->create_wall_timer(
      1000ms,  // 1秒周期
      [this]() {
        auto message = helloworld_demo::msg::HelloWorld();
        message.id = ++count_;
        message.message = "Hello from ROS2 publisher";
        
        RCLCPP_INFO(this->get_logger(), "Publishing: [%d] '%s'", 
          message.id, message.message.c_str());
        publisher_->publish(message);
      });
  }

private:
  rclcpp::Publisher<helloworld_demo::msg::HelloWorld>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
  uint32_t count_;
};

int main(int argc, char* argv[]) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<PublisherNode>());
  rclcpp::shutdown();
  return 0;
}

3. 实现订阅者节点

src/subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "helloworld_demo/msg/hello_world.hpp"

class SubscriberNode : public rclcpp::Node {
public:
  SubscriberNode() : Node("hello_subscriber") {
    subscriber_ = this->create_subscription<helloworld_demo::msg::HelloWorld>(
      "hello_topic",  // 与发布者相同的话题名称
      10,  // QoS历史深度
      [this](const helloworld_demo::msg::HelloWorld::SharedPtr msg) {
        RCLCPP_INFO(this->get_logger(), "Received: [%d] '%s'", 
          msg->id, msg->message.c_str());
      });
  }

private:
  rclcpp::Subscription<helloworld_demo::msg::HelloWorld>::SharedPtr subscriber_;
};

int main(int argc, char* argv[]) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<SubscriberNode>());
  rclcpp::shutdown();
  return 0;
}

4. 编译与运行

4.1 编译
cd ros2_ws
colcon build --packages-select helloworld_demo
source install/setup.bash
4.2 运行(两个终端)
# 终端1:订阅者
ros2 run helloworld_demo subscriber

# 终端2:发布者
ros2 run helloworld_demo publisher

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流浪_彩虹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值