欢迎光临
我们一直在努力

深入理解DDS通信:基于Unitree SDK2的实践与解析

一、引言:DDS通信的核心价值与应用场景

在分布式系统中,数据的高效、实时、可靠传输是核心需求之一。尤其在机器人、工业控制、自动驾驶等领域,设备间的协同依赖低延迟、高一致性的数据交互,而数据分发服务(Data Distribution Service, DDS) 正是为满足这一需求而生的中间件标准。

DDS由对象管理组(OMG)制定,采用“发布-订阅”架构,支持去中心化的分布式通信,具备实时性强、可靠性高、可扩展性好等特点。它通过标准化的接口和协议,屏蔽了底层网络细节,让开发者无需关注数据传输的底层实现,只需专注于业务逻辑。

Unitree(宇树科技)作为机器人领域的知名企业,在其新一代SDK(unitree_sdk2)中对DDS进行了深度封装,提供了简洁易用的通信接口,降低了机器人开发中分布式通信的门槛。本文将结合Unitree SDK2的官方DDS通信例程,从核心概念、代码解析、实践操作到注意事项,全面拆解DDS通信的实现逻辑与应用方法。

二、DDS核心概念铺垫

在解析例程前,需先明确几个DDS核心概念,这是理解代码逻辑的基础:

  • 发布-订阅模式(Publish-Subscribe):DDS的核心通信模式。发布者(Publisher)负责生产数据并发送到指定“主题”,订阅者(Subscriber)通过订阅该主题接收数据,两者无需直接建立连接,实现解耦。
  • 主题(Topic):数据的逻辑标识,是发布者和订阅者的“约定”。只有订阅了相同主题的订阅者,才能接收对应发布者发送的数据(例程中主题为TopicHelloWorld)。
  • 数据类型(Data Type):发布/订阅的数据结构定义,发布者和订阅者必须使用完全一致的数据类型,否则无法正常解析数据(例程中数据类型为HelloWorldData::Msg)。
  • 域(Domain):DDS将通信环境划分为不同域,只有处于同一域的发布者和订阅者才能通信,域ID用于隔离不同的通信场景(例程中域ID默认为0)。
  • 通道(Channel):Unitree SDK2对DDS通信链路的封装,包含发布/订阅的底层资源管理(如Socket、缓存队列等),通过ChannelFactory统一创建和管理。
  • 三、Unitree SDK2的DDS封装设计

    Unitree SDK2并未让开发者直接操作底层DDS API,而是通过一层简洁的封装,提供了更高层次的通信接口,核心封装类包括:

  • ChannelFactory(通道工厂):单例模式,负责初始化DDS通信环境(指定域ID、网卡名称)、管理通道资源,是所有通信操作的入口。
  • ChannelPublisher(发布者类):封装DDS发布者逻辑,提供InitChannel()(初始化通道)、Write()(发送消息)等方法。
  • ChannelSubscriber(订阅者类):封装DDS订阅者逻辑,提供InitChannel()(绑定消息处理函数、初始化通道)、CloseChannel()(关闭通道)等方法。
  • 消息类型模板:支持自定义消息结构(如例程中的HelloWorldData::Msg),需通过头文件统一定义,确保发布者和订阅者数据结构一致。
  • 这种封装的优势在于:开发者无需关注DDS底层的域配置、QoS(服务质量)参数、序列化/反序列化等复杂细节,只需调用SDK提供的接口即可快速实现分布式通信。

    四、消息发布例程深度解析(publisher.cpp)

    发布例程的核心功能是:初始化DDS环境,创建发布者,循环向TopicHelloWorld主题发送“HelloWorld”消息。以下是代码逐段解析:

    1. 头文件依赖

    #include <unitree/robot/channel/channel_publisher.hpp> // 发布者类头文件
    #include <unitree/common/time/time_tool.hpp> // 时间工具(获取时间戳)
    #include "HelloWorldData.hpp" // 自定义消息类型头文件

    • 必须包含SDK提供的发布者封装头文件和时间工具头文件;
    • HelloWorldData.hpp是自定义消息类型的定义文件,需与订阅者共用,确保数据结构一致。

    2. 主题定义

    #define TOPIC "TopicHelloWorld"

    • 定义通信主题名称,需与订阅者的主题完全一致(大小写敏感),否则订阅者无法接收消息。

    3. 主函数核心逻辑

    (1)参数检查与通信环境初始化

    if (argc < 2)
    {
    std::cout << "Usage: " << argv[0] << " networkInterface" << std::endl;
    exit(1);
    }
    unitree::robot::ChannelFactory::Instance()->Init(0, argv[1]);

    • 发布例程要求终端传入网卡名称(如eth0、wlan0),用于指定机器人通信使用的网络接口;
    • ChannelFactory::Instance()获取单例工厂对象,Init(0, argv[1])的两个参数分别为:域ID(0)和网卡名称(argv[1]),初始化DDS通信环境。
    (2)创建发布者对象

    ChannelPublisherPtr<HelloWorldData::Msg> publisher = ChannelPublisherPtr<HelloWorldData::Msg>(new ChannelPublisher<HelloWorldData::Msg>(TOPIC));

    • 使用智能指针ChannelPublisherPtr管理发布者对象,避免内存泄漏;
    • 模板参数HelloWorldData::Msg指定发布的消息类型,构造函数传入主题名称,绑定发布者与主题。
    (3)初始化发布通道

    publisher->InitChannel();

    • 初始化发布通道,底层会创建DDS发布者实例、数据写入器(DataWriter),并绑定主题,完成通信前的资源准备。
    (4)循环发送消息

    while (true)
    {
    HelloWorldData::Msg msg(unitree::common::GetCurrentTimeMillisecond(), "HelloWorld.");
    publisher->Write(msg);
    sleep(1);
    }

    • 构造消息对象msg:第一个参数是通过GetCurrentTimeMillisecond()获取的当前时间戳(作为userID字段),第二个参数是消息内容“HelloWorld.”;
    • publisher->Write(msg)将消息发送到指定主题,底层会自动完成消息序列化和网络传输;
    • sleep(1)表示每秒发送一条消息,控制发布频率。

    五、消息订阅例程深度解析(subscriber.cpp)

    订阅例程的核心功能是:初始化DDS环境,订阅TopicHelloWorld主题,接收发布者消息并处理,支持通道关闭与重新初始化。以下是代码逐段解析:

    1. 头文件依赖与主题定义

    #include <unitree/robot/channel/channel_subscriber.hpp> // 订阅者类头文件
    #include "HelloWorldData.hpp" // 与发布者共用的消息类型头文件

    #define TOPIC "TopicHelloWorld" // 与发布者主题完全一致

    • 订阅者需包含channel_subscriber.hpp头文件,消息类型头文件与发布者保持一致。

    2. 消息处理函数

    void Handler(const void* msg)
    {
    const HelloWorldData::Msg* pm = (const HelloWorldData::Msg*)msg;
    std::cout << "userID:" << pm->userID() << ", message:" << pm->message() << std::endl;
    }

    • 自定义消息处理函数,当订阅者接收到消息时,DDS会自动回调该函数;
    • 函数参数const void* msg是接收到的原始消息指针,需强制转换为HelloWorldData::Msg*类型,才能解析userID(时间戳)和message(消息内容)字段;
    • 此处仅打印消息,实际应用中可扩展为业务逻辑处理(如机器人运动控制、数据存储等)。

    3. 主函数核心逻辑

    (1)初始化通信环境

    ChannelFactory::Instance()->Init(0);

    • 订阅例程无需传入网卡名称(默认使用系统默认网卡),仅需指定域ID(0),与发布者保持同一域。
    (2)创建订阅者对象

    ChannelSubscriberPtr<HelloWorldData::Msg> subscriber = ChannelSubscriberPtr<HelloWorldData::Msg>(new ChannelSubscriber<HelloWorldData::Msg>(TOPIC));

    • 智能指针ChannelSubscriberPtr管理订阅者对象,模板参数指定消息类型,构造函数传入主题名称,绑定订阅者与主题。
    (3)初始化订阅通道(绑定处理函数)

    subscriber->InitChannel(std::bind(Handler, std::placeholders::_1), 1);

    • InitChannel()的两个参数:
    • 消息处理函数绑定:通过std::bind(Handler, std::placeholders::_1)将Handler函数绑定为消息回调函数,std::placeholders::_1表示回调时传入的消息指针参数;
    • 消息队列大小(1):指定接收消息的缓存队列大小,若消息接收速度超过处理速度,队列满后会丢弃旧消息(可根据需求调整)。
    (4)通道测试:关闭与重新初始化

    sleep(5);
    subscriber->CloseChannel();
    std::cout << "reseted. sleep 3" << std::endl;
    sleep(3);
    subscriber->InitChannel();

    • 先睡眠5秒,接收并处理发布者消息;
    • CloseChannel()关闭订阅通道,释放底层DDS订阅者、数据读取器(DataReader)等资源;
    • 睡眠3秒后,调用无参数的InitChannel(),使用之前绑定的处理函数和队列大小重新初始化通道,支持通道的动态重启。
    (5)循环等待消息

    while (true)
    {
    sleep(10);
    }

    • 订阅者需保持运行状态以接收消息,此处通过无限循环阻塞程序,避免进程退出。

    六、消息类型定义文件(HelloWorldData.hpp)解析

    例程中未给出HelloWorldData.hpp的源码,但根据发布者和订阅者的使用逻辑,可推导出其核心结构(实际开发中需手动创建该文件):

    #ifndef HELLOWORLDDATA_HPP
    #define HELLOWORLDDATA_HPP

    namespace HelloWorldData
    {
    class Msg
    {
    private:
    uint64_t userID_; // 时间戳字段(发布者传入的当前时间毫秒数)
    std::string message_;// 消息内容字段

    public:
    // 构造函数
    Msg(uint64_t userID, const std::string& message) : userID_(userID), message_(message) {}

    // 访问器方法(供订阅者解析)
    uint64_t userID() const { return userID_; }
    std::string message() const { return message_; }
    };
    }

    #endif // HELLOWORLDDATA_HPP

    • 消息类Msg需包含与发布者构造函数对应的字段(userID_和message_);
    • 提供公开的访问器方法(userID()、message()),供订阅者的处理函数解析消息内容;
    • 需使用头文件保护宏(HELLOWORLDDATA_HPP),避免重复包含。

    七、例程编译与运行步骤

    1. 编译准备

    (1)环境依赖
    • 安装Unitree SDK2(需从官网下载对应版本,支持Ubuntu等Linux系统);
    • 配置SDK环境变量:指定头文件路径(UNITREE_SDK2_INCLUDE)和库文件路径(UNITREE_SDK2_LIB)。
    (2)编写Makefile

    创建Makefile文件,指定编译规则:

    CC = g++
    CFLAGS = -std=c++11 -I$(UNITREE_SDK2_INCLUDE) # 包含SDK头文件
    LDFLAGS = -L$(UNITREE_SDK2_LIB) -lunitree_robot -lunitree_common # 链接SDK库

    # 目标文件
    TARGET_PUB = publisher
    TARGET_SUB = subscriber

    # 源文件
    SRC_PUB = publisher.cpp
    SRC_SUB = subscriber.cpp

    all: $(TARGET_PUB) $(TARGET_SUB)

    # 编译发布者
    $(TARGET_PUB): $(SRC_PUB)
    $(CC) $(SRC_PUB) -o $(TARGET_PUB) $(CFLAGS) $(LDFLAGS)

    # 编译订阅者
    $(TARGET_SUB): $(SRC_SUB)
    $(CC) $(SRC_SUB) -o $(TARGET_SUB) $(CFLAGS) $(LDFLAGS)

    # 清理目标文件
    clean:
    rm -f $(TARGET_PUB) $(TARGET_SUB)

    2. 编译与运行

    (1)编译代码

    在例程目录下执行:

    make

    • 生成publisher(发布者可执行文件)和subscriber(订阅者可执行文件)。
    (2)运行订阅者

    打开第一个终端,执行:

    ./subscriber

    • 订阅者初始化后进入等待状态,等待接收发布者消息。
    (3)运行发布者

    打开第二个终端,执行(需替换eth0为实际网卡名称):

    ./publisher eth0

    • 发布者开始每秒发送一条消息,订阅者终端会打印接收的消息:userID:1695700000000, message:HelloWorld.
      userID:1695700001000, message:HelloWorld.

    八、常见问题与注意事项

    1. 通信失败的核心排查点

    • 主题不一致:发布者和订阅者的TOPIC定义必须完全一致(大小写、字符完全匹配);
    • 域ID不匹配:ChannelFactory::Init()的第一个参数(域ID)必须相同,否则无法跨域通信;
    • 网卡名称错误:发布者传入的网卡名称不存在或未连接网络,需通过ifconfig命令查看实际网卡名称;
    • 消息类型不一致:HelloWorldData::Msg的字段、构造函数、访问器方法必须在发布者和订阅者中完全一致,否则会出现解析错误或崩溃;
    • SDK版本不兼容:例程基于unitree_sdk2,若使用旧版本SDK(如unitree_sdk1),接口可能不兼容,需升级SDK。

    2. 性能与可靠性优化建议

    • 调整消息队列大小:订阅者InitChannel()的第二个参数(队列大小)可根据消息频率调整,高频消息建议增大队列(如10、20),避免消息丢失;
    • 设置QoS参数:Unitree SDK2支持自定义DDS的QoS(服务质量)参数(如可靠性、时效性),需通过ChannelPublisher/ChannelSubscriber的扩展接口配置(适用于对通信质量有特殊要求的场景);
    • 资源释放:长期运行的程序需在退出时调用CloseChannel()释放资源,避免内存泄漏;
    • 网络环境:机器人通信建议使用有线网络(如eth0),减少无线网络(wlan0)的延迟和丢包。

    3. 扩展应用场景

    • 自定义消息类型:实际开发中可扩展Msg类的字段(如机器人关节角度、传感器数据),只需确保发布者和订阅者的头文件一致;
    • 多主题通信:支持创建多个发布者/订阅者对象,绑定不同主题,实现多维度数据的并行传输;
    • 跨设备通信:只要设备处于同一网络、同一域,即可通过该例程实现机器人与PC、机器人与机器人之间的分布式通信。

    九、总结

    DDS作为分布式实时通信的标准中间件,凭借其解耦、实时、可靠的特性,成为机器人等复杂系统的理想选择。Unitree SDK2通过对DDS的封装,将复杂的底层逻辑简化为“初始化-创建发布/订阅者-发送/接收消息”的简洁流程,极大降低了开发者的使用门槛。

    本文通过解析Unitree SDK2的官方DDS例程,从核心概念、代码逻辑、编译运行到问题排查,全面覆盖了DDS通信的实践要点。开发者可基于该例程快速搭建机器人分布式通信框架,并根据实际需求扩展消息类型、调整通信参数,实现多设备间的高效数据交互。

    随着机器人技术向多模块、分布式方向发展,DDS通信的应用将更加广泛。掌握Unitree SDK2的DDS封装使用,不仅能提升机器人开发效率,更能为复杂场景下的协同控制提供可靠的通信支撑。

    赞(0)
    未经允许不得转载:171主机测评 » 深入理解DDS通信:基于Unitree SDK2的实践与解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址