3. Fast DDS入门:从安装到第一个HelloWorld
各位同学,今天我们来聊聊Fast DDS。说实话,我第一次接触DDS的时候也被它的概念绕得头晕。但后来我发现,把它当成一个「超级快递系统」就好理解了——你要寄快递(发布数据),快递员(DDS中间件)负责送到收件人手里(订阅数据),而QoS就是你对快递服务的各种要求。
好,我们直接进入正题。
3.1 Fast DDS的安装与编译
安装Fast DDS,我建议你直接用源码编译。为什么?因为我在项目中遇到过好几次,用apt安装的版本太老,跟ROS2的某些特性不兼容,折腾半天才发现是版本问题。
3.1.1 安装依赖
sudo apt update
sudo apt install -y \
cmake \
g++ \
git \
libssl-dev \
libasio-dev \
libtinyxml2-dev \
libp11-dev \
softhsm2 \
libsqlite3-dev \
libgtest-dev \
libgmock-dev
嗯,这里要特别提一下libasio-dev。我记得有一次帮同事排查问题,他编译一直报错,最后发现是asio版本太旧。所以建议你装完后再检查一下版本:
dpkg -l | grep libasio
3.1.2 编译Fast DDS
我个人习惯把源码放在~/ros2_ws/src下面,这样跟ROS2的工作空间统一管理。
cd ~/ros2_ws/src
git clone https://github.com/eProsima/Fast-DDS.git -b v2.10.0
cd Fast-DDS
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make -j$(nproc)
sudo make install
编译时间大概5-10分钟,取决于你的机器性能。这时候可以去泡杯咖啡。
make -j4 限制并行编译数,避免把CPU跑满导致电脑卡死。我吃过这个亏,编译到一半电脑直接死机...
3.1.3 验证安装
装完后,跑一下这个命令看看版本号:
fastdds --version
如果看到类似 Fast DDS version 2.10.0 的输出,恭喜你,安装成功了。
3.2 Fast DDS HelloWorld示例
好了,环境搭好了,我们来写第一个程序。说白了就是:一个发布者发「Hello World」,一个订阅者收「Hello World」。
3.2.1 定义数据类型
先用IDL定义我们的消息结构:
// HelloWorld.idl
struct HelloWorld
{
unsigned long index;
string message;
};
然后用Fast DDS提供的工具生成代码:
fastddsgen HelloWorld.idl
这会生成 HelloWorld.h、HelloWorld.cxx 等文件。你想想看,如果没有这个工具,手动写序列化代码得多痛苦。
3.2.2 编写发布者
// HelloWorldPublisher.cpp
#include "HelloWorld.h"
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/publisher/DataWriterListener.hpp>
using namespace eprosima::fastdds::dds;
class HelloWorldPublisher
{
private:
DomainParticipant* participant_;
Publisher* publisher_;
DataWriter* writer_;
Topic* topic_;
HelloWorld hello_;
public:
HelloWorldPublisher() : participant_(nullptr), publisher_(nullptr),
writer_(nullptr), topic_(nullptr)
{
hello_.index(0);
hello_.message("Hello World");
}
bool init()
{
// 创建DomainParticipant
DomainParticipantQos pqos;
pqos.name("Participant_pub");
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, pqos);
if (participant_ == nullptr) return false;
// 注册类型
TypeSupport type(new HelloWorldPubSubType());
type.register_type(participant_);
// 创建Topic
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (topic_ == nullptr) return false;
// 创建Publisher
publisher_ = participant_->create_publisher(PUBLISHER_QOS_DEFAULT, nullptr);
if (publisher_ == nullptr) return false;
// 创建DataWriter
writer_ = publisher_->create_datawriter(topic_, DATAWRITER_QOS_DEFAULT);
if (writer_ == nullptr) return false;
return true;
}
void publish()
{
hello_.index(hello_.index() + 1);
writer_->write(&hello_);
std::cout << "发送: " << hello_.message() << " #" << hello_.index() << std::endl;
}
};
3.2.3 编写订阅者
// HelloWorldSubscriber.cpp
class HelloWorldSubscriber
{
private:
DomainParticipant* participant_;
Subscriber* subscriber_;
DataReader* reader_;
Topic* topic_;
public:
bool init()
{
// 创建DomainParticipant
DomainParticipantQos pqos;
pqos.name("Participant_sub");
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, pqos);
if (participant_ == nullptr) return false;
// 注册类型
TypeSupport type(new HelloWorldPubSubType());
type.register_type(participant_);
// 创建Topic(必须与发布者一致)
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
// 创建Subscriber
subscriber_ = participant_->create_subscriber(SUBSCRIBER_QOS_DEFAULT, nullptr);
// 创建DataReader并设置监听器
reader_ = subscriber_->create_datareader(topic_, DATAREADER_QOS_DEFAULT, &listener_);
return true;
}
};
3.2.4 编译运行
g++ -std=c++11 -I/usr/local/include -L/usr/local/lib \
HelloWorldPublisher.cpp HelloWorld.cxx \
-lfastdds -lfastcdr -lpthread \
-o publisher
g++ -std=c++11 -I/usr/local/include -L/usr/local/lib \
HelloWorldSubscriber.cpp HelloWorld.cxx \
-lfastdds -lfastcdr -lpthread \
-o subscriber
先启动订阅者,再启动发布者:
./subscriber
# 另一个终端
./publisher
如果看到订阅者终端打印出 收到: Hello World #1,说明通信成功了。
3.3 Fast DDS的QoS策略配置
QoS是DDS的灵魂。说白了,它决定了你的数据怎么传、传多快、丢了怎么办。
3.3.1 常用QoS策略
| QoS策略 | 作用 | 我常用的场景 |
|---|---|---|
| RELIABILITY | 可靠性:RELIABLE(可靠)或 BEST_EFFORT(尽力) | 控制指令用RELIABLE,传感器数据用BEST_EFFORT |
| DURABILITY | 持久性:TRANSIENT_LOCAL(保留最新数据给后加入的订阅者) | 机器人状态信息用这个,新节点加入能拿到当前状态 |
| HISTORY | 历史记录:KEEP_LAST(保留最近N个)或 KEEP_ALL(保留所有) | 日志数据用KEEP_ALL,实时控制用KEEP_LAST |
| DEADLINE | 截止时间:数据必须在指定时间内更新 | 电机控制周期必须严格,超时就报警 |
3.3.2 配置示例
我曾经在一个项目中,需要确保机器人状态信息不丢失,同时新加入的节点能立即获取当前状态。配置如下:
// 配置DataWriter的QoS
DataWriterQos wqos;
wqos.reliability().kind = RELIABLE_RELIABILITY_QOS; // 可靠传输
wqos.durability().kind = TRANSIENT_LOCAL_DURABILITY_QOS; // 持久化
wqos.history().kind = KEEP_LAST_HISTORY_QOS; // 保留最近
wqos.history().depth = 5; // 保留5个样本
wqos.deadline().period.seconds = 1; // 每秒至少更新一次
// 配置DataReader的QoS
DataReaderQos rqos;
rqos.reliability().kind = RELIABLE_RELIABILITY_QOS;
rqos.durability().kind = TRANSIENT_LOCAL_DURABILITY_QOS;
rqos.history().kind = KEEP_LAST_HISTORY_QOS;
rqos.history().depth = 5;
rqos.deadline().period.seconds = 1;
3.3.3 QoS不兼容的情况
举个例子,如果发布者要求RELIABLE,但订阅者只支持BEST_EFFORT,DDS会怎么处理?
答案是:它会尝试协商。如果协商失败,通信就建立不起来。你可以在监听器的 on_offered_deadline_missed 或 on_requested_deadline_missed 回调中捕获这些错误。
知识体系总览
下面这张图帮你理清Fast DDS的核心脉络:
这张图把Fast DDS的三大块串起来了:先装好环境,再跑通HelloWorld,最后用QoS调优。每一步都是下一步的基础。
好了,Fast DDS的入门就到这里。记住:安装要稳,示例要通,QoS要懂。这三步走完,你就正式踏入DDS的大门了。