【ROS2】高级:解锁 Fast DDS 中间件的潜力 [社区贡献]

news2024/12/25 22:37:08

目标:本教程将展示如何在 ROS 2 中使用 Fast DDS 的扩展配置功能

 教程级别:高级

 时间:20 分钟

 目录

  •  背景

  •  先决条件

  • 在同一个节点中混合同步和异步发布

    • 创建具有发布者的节点

    • 创建包含配置文件的 XML 文件

    • 执行发布者节点

    • 创建一个包含订阅者的节点

    • 执行订阅者节点

    • 示例分析

  • 使用其他 FastDDS 功能与 XML

    • 限制匹配订阅者的数量

    • 在主题内使用分区

  • 配置服务和客户端

    • 使用服务和客户端创建节点

    • 为服务和客户端创建 XML 配置文件

    • 执行节点

 背景

ROS 2 堆栈和 Fast DDS 之间的接口由 ROS 2 中间件实现 rmw_fastrtps 提供。此实现可在所有 ROS 2 发行版中使用,无论是从二进制文件还是从源代码。

ROS 2 RMW 仅允许配置某些中间件 QoS(参见 ROS 2 QoS 策略 https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html )。然而, rmw_fastrtps 提供了扩展的配置功能,以充分利用 Fast DDS 中的功能。本教程将通过一系列示例指导您如何使用 XML 文件解锁此扩展配置。

为了获得有关在 ROS 2 上使用 Fast DDS 的更多信息,请查看以下文档。https://fast-dds.docs.eprosima.com/en/latest/fastdds/ros2/ros2.html

5b7a953ff3efb2f4844d24ef9b3eea1f.png

sudo apt install ros-jazzy-rmw-fastrtps-cpp
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_IMPLEMENTATION=rmw_fastrtps_dynamic_cpp
RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run <package> <application>
RMW_IMPLEMENTATION=rmw_fastrtps_dynamic_cpp ros2 run <package> <application>

先决条件

本教程假设您知道如何创建一个包。它还假设您知道如何编写一个简单的发布者和订阅者以及一个简单的服务和客户端。尽管示例是用 C++实现的,但相同的概念也适用于 Python 包。

在同一个节点中混合同步和异步发布

在这个第一个例子中,将创建一个具有两个发布者的节点,其中一个是同步发布模式,另一个是异步发布模式。

rmw_fastrtps 默认使用同步发布模式

在同步发布模式下,数据直接在用户线程的上下文中发送。这意味着在写操作期间发生的任何阻塞调用都会阻塞用户线程,从而阻止应用程序继续运行。然而,由于线程之间没有通知或上下文切换,这种模式通常在较低的延迟下产生更高的吞吐量。

另一方面,在异步发布模式下,每次发布者调用写操作时,数据会被复制到队列中,后台线程(异步线程)会收到有关队列中新增数据的通知,并在数据实际发送之前将线程的控制权返回给user。后台线程负责消费队列并将数据发送给每个匹配的reader。

创建带有发布者的节点

首先,在新的工作区上创建一个名为 sync_async_node_example_cpp 的新包:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 --dependencies rclcpp std_msgs -- sync_async_node_example_cpp

4332bab791460afb18fd668dfa2ae421.png

然后,向包中添加一个名为 src/sync_async_writer.cpp 的文件,内容如下。请注意,同步发布者将发布在主题 sync_topic 上,而异步发布者将发布在主题 async_topic 上。

#include <chrono> // 包含用于时间操作的头文件
#include <functional> // 包含用于函数对象和绑定的头文件
#include <memory> // 包含用于智能指针的头文件
#include <string> // 包含用于字符串操作的头文件


#include "rclcpp/rclcpp.hpp" // 包含ROS 2的C++客户端库
#include "std_msgs/msg/string.hpp" // 包含标准消息类型String


using namespace std::chrono_literals; // 使用chrono命名空间中的字面量


class SyncAsyncPublisher : public rclcpp::Node // 定义一个名为SyncAsyncPublisher的类,继承自rclcpp::Node
{
public:
    SyncAsyncPublisher() // 构造函数
        : Node("sync_async_publisher"), count_(0) // 初始化节点名称为sync_async_publisher,计数器count_初始化为0
    {
        // 创建一个同步发布者,发布到主题'sync_topic'
        sync_publisher_ = this->create_publisher<std_msgs::msg::String>("sync_topic", 10);


        // 创建一个异步发布者,发布到主题'async_topic'
        async_publisher_ = this->create_publisher<std_msgs::msg::String>("async_topic", 10);


        // 定义一个定时器回调函数,每次定时器触发时执行的操作
        auto timer_callback = this{


            // 创建一个新的消息
            auto sync_message = std_msgs::msg::String();
            sync_message.data = "SYNC: Hello, world! " + std::to_string(count_);


            // 将消息记录到控制台以显示进度
            RCLCPP_INFO(this->get_logger(), "Synchronously publishing: '%s'", sync_message.data.c_str());


            // 使用同步发布者发布消息
            sync_publisher_->publish(sync_message);


            // 创建一个新的消息
            auto async_message = std_msgs::msg::String();
            async_message.data = "ASYNC: Hello, world! " + std::to_string(count_);


            // 将消息记录到控制台以显示进度
            RCLCPP_INFO(this->get_logger(), "Asynchronously publishing: '%s'", async_message.data.c_str());


            // 使用异步发布者发布消息
            async_publisher_->publish(async_message);


            // 准备下一条消息的计数
            count_++;
        };


        // 创建一个定时器,每隔半秒触发一次,执行定时器回调函数
        timer_ = this->create_wall_timer(500ms, timer_callback);
    }


private:
    // 定时器,每隔半秒触发一次,发布新的数据
    rclcpp::TimerBase::SharedPtr timer_;


    // 异步发布者
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr async_publisher_;


    // 同步发布者
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr sync_publisher_;


    // 已发送的消息数量
    size_t count_;
};


int main(int argc, char * argv[]) // 主函数
{
    rclcpp::init(argc, argv); // 初始化ROS 2
    rclcpp::spin(std::make_shared<SyncAsyncPublisher>()); // 创建SyncAsyncPublisher节点并运行
    rclcpp::shutdown(); // 关闭ROS 2
    return 0; // 返回0表示程序正常结束
}

现在打开 CMakeLists.txt 文件,添加一个新的可执行文件并将其命名为 SyncAsyncWriter ,以便您可以使用 ros2 run 运行您的节点:

add_executable(SyncAsyncWriter src/sync_async_writer.cpp)
ament_target_dependencies(SyncAsyncWriter rclcpp std_msgs)

最后,添加 install(TARGETS…) 部分,以便 ros2 run 可以找到你的可执行文件:

install(TARGETS
    SyncAsyncWriter
    DESTINATION lib/${PROJECT_NAME})

您可以通过删除一些不必要的部分和注释来清理您的 CMakeLists.txt ,使其看起来像这样:

cmake_minimum_required(VERSION 3.8) # 设置CMake的最低版本要求为3.8
project(sync_async_node_example_cpp) # 定义项目名称为sync_async_node_example_cpp


# 默认使用C++14标准
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14) # 如果没有设置C++标准,则设置为C++14
endif()


# 如果使用GNU编译器或Clang编译器,添加编译选项
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic) # 添加编译选项:显示所有警告、额外警告和严格警告
endif()


find_package(ament_cmake REQUIRED) # 查找ament_cmake包,标记为必需
find_package(rclcpp REQUIRED) # 查找rclcpp包,标记为必需
find_package(std_msgs REQUIRED) # 查找std_msgs包,标记为必需


add_executable(SyncAsyncWriter src/sync_async_writer.cpp) # 添加可执行文件SyncAsyncWriter,源文件为src/sync_async_writer.cpp
ament_target_dependencies(SyncAsyncWriter rclcpp std_msgs) # 设置SyncAsyncWriter的依赖项为rclcpp和std_msgs


install(TARGETS # 安装目标
    SyncAsyncWriter # 安装SyncAsyncWriter
    DESTINATION lib/${PROJECT_NAME}) # 安装路径为lib/${PROJECT_NAME}


ament_package() # 声明ament包

如果现在构建并运行此节点,两个发布者将表现相同,两个发布者在主题中都异步发布,因为这是默认的发布模式默认的发布模式配置可以在节点启动期间使用 XML 文件在运行时更改。

创建包含配置文件的 XML 文件

创建一个名为 SyncAsync.xml 的文件,并包含以下内容:

cxy@ubuntu2404-cxy:~/ros2_ws/src/sync_async_node_example_cpp$ gedit SyncAsync.xml
<?xml version="1.0" encoding="UTF-8" ?> <!-- XML声明,定义版本和编码 -->
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles"> <!-- 定义profiles根元素,并指定其命名空间 -->


    <!-- 默认发布者配置文件 -->
    <publisher profile_name="default_publisher" is_default_profile="true"> <!-- 定义一个发布者配置文件,名称为default_publisher,设置为默认配置文件 -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
    </publisher>


    <!-- 默认订阅者配置文件 -->
    <subscriber profile_name="default_subscriber" is_default_profile="true"> <!-- 定义一个订阅者配置文件,名称为default_subscriber,设置为默认配置文件 -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
    </subscriber>


    <!-- sync_topic主题的发布者配置文件 -->
    <publisher profile_name="/sync_topic"> <!-- 定义一个发布者配置文件,名称为/sync_topic -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
        <qos> <!-- 定义QoS(服务质量)设置 -->
            <publishMode> <!-- 定义发布模式 -->
                <kind>SYNCHRONOUS</kind> <!-- 设置发布模式为同步 -->
            </publishMode>
        </qos>
    </publisher>


    <!-- async_topic主题的发布者配置文件 -->
    <publisher profile_name="/async_topic"> <!-- 定义一个发布者配置文件,名称为/async_topic -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
        <qos> <!-- 定义QoS(服务质量)设置 -->
            <publishMode> <!-- 定义发布模式 -->
                <kind>ASYNCHRONOUS</kind> <!-- 设置发布模式为异步 -->
            </publishMode>
        </qos>
    </publisher>


 </profiles>

请注意,定义了多个发布者和订阅者的配置文件。定义了两个默认配置文件,将 is_default_profile 设置为 true ,以及两个名称与先前定义的主题相符的配置文件: sync_topic 和另一个 async_topic 。这两个配置文件将发布模式分别设置为 SYNCHRONOUS 或 ASYNCHRONOUS 。还请注意,所有配置文件都指定了一个 historyMemoryPolicy 值,这是示例正常运行所需的值,原因将在本教程后面解释。

执行发布者节点

您需要导出以下环境变量以加载 XML:

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_FASTRTPS_USE_QOS_FROM_XML=1
export FASTRTPS_DEFAULT_PROFILES_FILE=~/ros2_ws/src/sync_async_node_example_cpp/SyncAsync.xml

最后,确保您已获取设置文件并运行节点:

source install/setup.bash
ros2 run sync_async_node_example_cpp SyncAsyncWriter

您应该看到发布者从发布节点发送数据,如下所示:

[INFO] [1612972049.994630332] [sync_async_publisher]: Synchronously publishing: 'SYNC: Hello, world! 0'
[INFO] [1612972049.995097767] [sync_async_publisher]: Asynchronously publishing: 'ASYNC: Hello, world! 0'
[INFO] [1612972050.494478706] [sync_async_publisher]: Synchronously publishing: 'SYNC: Hello, world! 1'
[INFO] [1612972050.494664334] [sync_async_publisher]: Asynchronously publishing: 'ASYNC: Hello, world! 1'
[INFO] [1612972050.994368474] [sync_async_publisher]: Synchronously publishing: 'SYNC: Hello, world! 2'
[INFO] [1612972050.994549851] [sync_async_publisher]: Asynchronously publishing: 'ASYNC: Hello, world! 2'

现在你有一个同步发布者和一个异步发布者在同一个节点内运行。

779d7078955599ce9bb7995710049494.png

创建一个带有订阅者的节点

接下来,将创建一个包含订阅者的新节点,这些订阅者将监听 sync_topic 和 async_topic 发布。在名为 src/sync_async_reader.cpp 的新源文件中写入以下内容:

#include <memory> // 包含用于智能指针的头文件


#include "rclcpp/rclcpp.hpp" // 包含ROS 2的C++客户端库
#include "std_msgs/msg/string.hpp" // 包含标准消息类型String


class SyncAsyncSubscriber : public rclcpp::Node // 定义一个名为SyncAsyncSubscriber的类,继承自rclcpp::Node
{
public:


    SyncAsyncSubscriber() // 构造函数
        : Node("sync_async_subscriber") // 初始化节点名称为sync_async_subscriber
    {
        // Lambda函数,每次接收到新消息时运行
        auto topic_callback = this{
            RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str()); // 将接收到的消息记录到控制台
        };


        // 创建一个同步订阅者,订阅主题'sync_topic'
        // 并将其绑定到topic_callback
        sync_subscription_ = this->create_subscription<std_msgs::msg::String>(
            "sync_topic", 10, topic_callback);


        // 创建一个异步订阅者,订阅主题'async_topic'
        // 并将其绑定到topic_callback
        async_subscription_ = this->create_subscription<std_msgs::msg::String>(
            "async_topic", 10, topic_callback);
    }


private:


    // 一个订阅'sync_topic'主题的订阅者
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sync_subscription_;


    // 一个订阅'async_topic'主题的订阅者
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr async_subscription_;
};


int main(int argc, char * argv[]) // 主函数
{
    rclcpp::init(argc, argv); // 初始化ROS 2
    rclcpp::spin(std::make_shared<SyncAsyncSubscriber>()); // 创建SyncAsyncSubscriber节点并运行
    rclcpp::shutdown(); // 关闭ROS 2
    return 0; // 返回0表示程序正常结束
}

打开 CMakeLists.txt 文件,在前一个 SyncAsyncWriter 下添加一个新的可执行文件,并将其命名为 SyncAsyncReader

add_executable(SyncAsyncReader src/sync_async_reader.cpp)
ament_target_dependencies(SyncAsyncReader rclcpp std_msgs)


install(TARGETS
    SyncAsyncReader
    DESTINATION lib/${PROJECT_NAME})

执行订阅者节点

在一个终端中运行发布者节点后,打开另一个终端并导出加载 XML 所需的环境变量:

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_FASTRTPS_USE_QOS_FROM_XML=1
export FASTRTPS_DEFAULT_PROFILES_FILE=~/ros2_ws/src/sync_async_node_example_cpp/SyncAsync.xml

最后,确保您已获取设置文件并运行节点:

source install/setup.bash
ros2 run sync_async_node_example_cpp SyncAsyncReader

您应该看到订阅者从发布节点接收数据,如下所示:

[INFO] [1612972054.495429090] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 10'
[INFO] [1612972054.995410057] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 10'
[INFO] [1612972055.495453494] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 11'
[INFO] [1612972055.995396561] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 11'
[INFO] [1612972056.495534818] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 12'
[INFO] [1612972056.995473953] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 12'

360ce14b2d48f0ad53fef08614cbfae6.png

示例分析

配置文件 XML

XML 文件定义了发布者和订阅者的几种配置。您可以拥有一个默认的发布者配置文件和几个特定主题的发布者配置文件。唯一的要求是所有发布者配置文件必须有不同的名称,并且只能有一个默认配置文件。订阅者也是如此

为了定义特定主题的配置,只需将配置文件命名为 ROS 2 主题名称(如示例中的 /sync_topic 和 /async_topic ), rmw_fastrtps 将此配置文件应用于该主题的所有发布者和订阅者。默认配置文件由属性 is_default_profile 设置为 true 标识,并在没有其他名称与主题名称匹配的配置文件时充当回退配置文件。

环境变量 FASTRTPS_DEFAULT_PROFILES_FILE 用于通知 Fast DDS 配置文件的 XML 文件路径。

RMW_FASTRTPS_USE_QOS_FROM_XML

在所有可配置属性中, rmw_fastrtps 对 publishMode 和 historyMemoryPolicy 的处理方式不同。默认情况下,这些值在 rmw_fastrtps 实现中设置为 ASYNCHRONOUS 和 PREALLOCATED_WITH_REALLOC ,并且 XML 文件中设置的值将被忽略。为了使用 XML 文件中的值,必须将环境变量 RMW_FASTRTPS_USE_QOS_FROM_XML 设置为 1 。

然而,这还涉及另一个警告:如果设置了 RMW_FASTRTPS_USE_QOS_FROM_XML ,但 XML 文件没有定义 publishMode 或 historyMemoryPolicy ,这些属性将采用 Fast DDS 默认值而不是 rmw_fastrtps 默认值。这一点很重要,尤其是对于 historyMemoryPolicy ,因为 Fast DDS 默认值是 PREALLOCATED ,它不适用于 ROS2 主题数据类型。因此,在示例中,已明确设置了该策略的有效值( DYNAMIC )。

rmw_qos_profile_t 的优先级 

ROS 2 QoS 包含在 rmw_qos_profile_t https://docs.ros.org/en/jazzy/p/rmw/generated/structrmw__qos__profile__s.html中的 QoS 始终被遵守,除非设置为 *_SYSTEM_DEFAULT 。在这种情况下,将应用 XML 值(或在没有 XML 值的情况下应用 Fast DDS 默认值)。这意味着,如果 rmw_qos_profile_t 中的任何 QoS 设置为 *_SYSTEM_DEFAULT 以外的值,则 XML 中的相应值将被忽略

d163218813629388980470c8183c57a2.png

使用其他 FastDDS 功能与 XML

虽然我们创建了一个具有不同配置的两个发布者的节点,但很难检查它们的行为是否不同。现在已经介绍了 XML 配置文件的基础知识,让我们使用它们来配置一些对节点有视觉效果的东西。具体来说,将在一个发布者上设置最大匹配订阅者数量,在另一个发布者上设置分区定义。请注意,这些只是通过 XML 文件可以调整的所有配置属性中的一些非常简单的示例。请参阅*Fast DDS*文档https://fast-dds.docs.eprosima.com/en/latest/fastdds/xml_configuration/xml_configuration.html#xml-profiles 以查看可以通过 XML 文件配置的属性的完整列表。

b53a3af64a33798028be51944a2e028e.png

限制匹配订阅者的数量

将最大数量的匹配订阅者添加到 /async_topic 发布者配置文件。它应该看起来像这样:

<!-- async_topic主题的发布者配置文件 -->
<publisher profile_name="/async_topic"> <!-- 定义名为/async_topic的发布者配置文件 -->
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
    <qos> <!-- 定义QoS(服务质量)设置 -->
        <publishMode> <!-- 定义发布模式 -->
            <kind>ASYNCHRONOUS</kind> <!-- 设置发布模式为异步 -->
        </publishMode>
    </qos>
    <matchedSubscribersAllocation> <!-- 定义匹配订阅者的分配策略 -->
        <initial>0</initial> <!-- 初始分配的订阅者数量为0 -->
        <maximum>1</maximum> <!-- 最大分配的订阅者数量为1 -->
        <increment>1</increment> <!-- 每次增加的订阅者数量为1 -->
    </matchedSubscribersAllocation>
</publisher>

匹配订阅者的数量被限制为一个。

现在打开三个终端,不要忘记源化设置文件并设置所需的环境变量。在第一个终端上运行发布者节点,在另外两个终端上运行订阅者节点。您应该看到只有第一个订阅者节点接收到来自两个主题的消息。第二个订阅者节点无法在 /async_topic 中完成匹配过程,因为发布者阻止了它,因为它已经达到了匹配发布者的最大数量。因此,只有来自 /sync_topic 的消息将会在这个第三终端中接收到。

[INFO] [1613127657.088860890] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 18'
[INFO] [1613127657.588896594] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 19'
[INFO] [1613127658.088849401] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 20'

在主题内使用分区

分区功能可用于控制在同一主题内哪些发布者和订阅者交换信息

分区在由域 ID 引起的物理隔离内引入了逻辑实体隔离级别的概念。为了使发布者与订阅者进行通信,他们必须至少属于一个共同的分区。分区代表了在域和主题之外分离发布者和订阅者的另一个级别。与域和主题不同,一个端点可以同时属于多个分区。为了在不同的域或主题上共享某些数据,每个域或主题必须有一个不同的发布者,分享其自己的更改历史。然而,单个发布者可以使用单个主题数据更改在不同的分区上共享相同的数据样本,从而减少网络过载

让我们将 /sync_topic 发布者更改为分区 part1 ,并创建一个使用分区 part2 的新 /sync_topic 订阅者。他们的配置文件现在应如下所示:

<!-- sync_topic主题的发布者配置文件 -->
<publisher profile_name="/sync_topic"> <!-- 定义一个发布者配置文件,名称为/sync_topic -->
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
    <qos> <!-- 定义QoS(服务质量)设置 -->
        <publishMode> <!-- 定义发布模式 -->
            <kind>SYNCHRONOUS</kind> <!-- 设置发布模式为同步 -->
        </publishMode>
        <partition> <!-- 定义分区 -->
            <names> <!-- 分区名称 -->
                <name>part1</name> <!-- 设置分区名称为part1 -->
            </names>
        </partition>
    </qos>
</publisher>


<!-- sync_topic主题的订阅者配置文件 -->
<subscriber profile_name="/sync_topic"> <!-- 定义一个订阅者配置文件,名称为/sync_topic -->
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy> <!-- 设置历史内存策略为动态 -->
    <qos> <!-- 定义QoS(服务质量)设置 -->
        <partition> <!-- 定义分区 -->
            <names> <!-- 分区名称 -->
                <name>part2</name> <!-- 设置分区名称为part2 -->
            </names>
        </partition>
    </qos>
</subscriber>

打开两个终端。不要忘记加载设置文件并设置所需的环境变量。在第一个终端上运行发布者节点,在另一个终端上运行订阅者节点。您应该看到只有 /async_topic 消息到达订阅者。 /sync_topic 订阅者没有接收到数据,因为它与相应的发布者在不同的分区中。

[INFO] [1612972054.995410057] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 10'
[INFO] [1612972055.995396561] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 11'
[INFO] [1612972056.995473953] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 12'

配置服务和客户端

服务和客户端各有一个发布者和一个订阅者,它们通过两个不同的主题进行通信。例如,对于名为 ping 的服务,有:

  • 在 /rq/ping 上监听请求的服务订阅者。

  • 服务发布者在 /rr/ping 上发送响应。

  • 客户端发布者在 /rq/ping 上发送请求。

  • 一个客户端订阅者正在监听 /rr/ping 上的响应。

尽管您可以使用这些主题名称在 XML 上设置配置文件,有时您可能希望将相同的配置文件应用于节点上的所有服务或客户端。与其为所有服务生成的所有主题名称复制相同的配置文件,您可以只创建一个名为 service 的发布者和订阅者配置文件对。对于创建名为 client 的对的客户端,也可以这样做。

使用服务和客户端创建节点

开始使用该服务创建节点。在您的包中添加一个名为 src/ping_service.cpp 的新源文件,并包含以下内容:

#include <memory> // 包含用于智能指针的头文件


#include "rclcpp/rclcpp.hpp" // 包含ROS 2的C++客户端库
#include "example_interfaces/srv/trigger.hpp" // 包含example_interfaces包中的Trigger服务


/**
 * 服务操作:响应success=true并在控制台打印请求
 */
void ping(const std::shared_ptr<example_interfaces::srv::Trigger::Request> request,
        std::shared_ptr<example_interfaces::srv::Trigger::Response> response)
{
    // 请求数据未使用
    (void) request;


    // 构建响应
    response->success = true;


    // 记录到控制台
    RCLCPP_INFO(rclcpp::get_logger("ping_server"), "Incoming request"); // 打印收到请求的日志
    RCLCPP_INFO(rclcpp::get_logger("ping_server"), "Sending back response"); // 打印发送响应的日志
}


int main(int argc, char **argv) // 主函数
{
    rclcpp::init(argc, argv); // 初始化ROS 2


    // 创建节点和服务
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("ping_server"); // 创建名为ping_server的节点
    rclcpp::Service<example_interfaces::srv::Trigger>::SharedPtr service =
        node->create_service<example_interfaces::srv::Trigger>("ping", &ping); // 创建名为ping的服务,并绑定到ping函数


    // 记录服务已准备好的日志
    RCLCPP_INFO(rclcpp::get_logger("ping_server"), "Ready to serve."); // 打印服务已准备好的日志


    // 运行节点
    rclcpp::spin(node); // 运行节点
    rclcpp::shutdown(); // 关闭ROS 2
}

在名为 src/ping_client.cpp 的文件中创建客户端,内容如下:

#include <chrono> // 包含用于时间操作的头文件
#include <memory> // 包含用于智能指针的头文件


#include "rclcpp/rclcpp.hpp" // 包含ROS 2的C++客户端库
#include "example_interfaces/srv/trigger.hpp" // 包含example_interfaces包中的Trigger服务


using namespace std::chrono_literals; // 使用chrono命名空间中的字面量


int main(int argc, char **argv) // 主函数
{
    rclcpp::init(argc, argv); // 初始化ROS 2


    // 创建节点和客户端
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("ping_client"); // 创建名为ping_client的节点
    rclcpp::Client<example_interfaces::srv::Trigger>::SharedPtr client =
        node->create_client<example_interfaces::srv::Trigger>("ping"); // 创建名为ping的客户端


    // 创建请求
    auto request = std::make_shared<example_interfaces::srv::Trigger::Request>(); // 创建Trigger服务的请求


    // 等待服务可用
    while (!client->wait_for_service(1s)) { // 每隔1秒检查一次服务是否可用
        if (!rclcpp::ok()) { // 如果ROS 2被中断
            RCLCPP_ERROR(rclcpp::get_logger("ping_client"), "Interrupted while waiting for the service. Exiting."); // 打印错误日志
            return 0; // 返回0表示程序正常结束
        }
        RCLCPP_INFO(rclcpp::get_logger("ping_client"), "Service not available, waiting again..."); // 打印服务不可用的日志
    }


    // 现在服务可用了,发送请求
    RCLCPP_INFO(rclcpp::get_logger("ping_client"), "Sending request"); // 打印发送请求的日志
    auto result = client->async_send_request(request); // 异步发送请求


    // 等待结果并将其记录到控制台
    if (rclcpp::spin_until_future_complete(node, result) ==
        rclcpp::FutureReturnCode::SUCCESS) // 如果成功接收到响应
    {
        RCLCPP_INFO(rclcpp::get_logger("ping_client"), "Response received"); // 打印接收到响应的日志
    } else {
        RCLCPP_ERROR(rclcpp::get_logger("ping_client"), "Failed to call service ping"); // 打印调用服务失败的日志
    }


    rclcpp::shutdown(); // 关闭ROS 2
    return 0; // 返回0表示程序正常结束
}

打开 CMakeLists.txt 文件并添加两个新的可执行文件 ping_service 和 ping_client :

find_package(example_interfaces REQUIRED) # 查找example_interfaces包,标记为必需


add_executable(ping_service src/ping_service.cpp) # 添加可执行文件ping_service,源文件为src/ping_service.cpp
ament_target_dependencies(ping_service example_interfaces rclcpp) # 设置ping_service的依赖项为example_interfaces和rclcpp


add_executable(ping_client src/ping_client.cpp) # 添加可执行文件ping_client,源文件为src/ping_client.cpp
ament_target_dependencies(ping_client example_interfaces rclcpp) # 设置ping_client的依赖项为example_interfaces和rclcpp


install(TARGETS # 安装目标
    ping_service # 安装ping_service
    DESTINATION lib/${PROJECT_NAME}) # 安装路径为lib/${PROJECT_NAME}


install(TARGETS # 安装目标
    ping_client # 安装ping_client
    DESTINATION lib/${PROJECT_NAME}) # 安装路径为lib/${PROJECT_NAME}

最后,构建包。

为服务和客户端创建 XML 配置文件

创建一个名为 ping.xml 的文件,并包含以下内容:

<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">


    <!-- 默认发布者配置文件 -->
    <publisher profile_name="default_publisher" is_default_profile="true">
        <!-- 历史内存策略设置为动态 -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    </publisher>


    <!-- 默认订阅者配置文件 -->
    <subscriber profile_name="default_subscriber" is_default_profile="true">
        <!-- 历史内存策略设置为动态 -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    </subscriber>


    <!-- 服务发布者配置为同步模式 -->
    <publisher profile_name="service">
        <!-- 历史内存策略设置为动态 -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
        <qos>
            <publishMode>
                <!-- 发布模式设置为同步 -->
                <kind>SYNCHRONOUS</kind>
            </publishMode>
        </qos>
    </publisher>


    <!-- 客户端发布者配置为异步模式 -->
    <publisher profile_name="client">
        <!-- 历史内存策略设置为动态 -->
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
        <qos>
            <publishMode>
                <!-- 发布模式设置为异步 -->
                <kind>ASYNCHRONOUS</kind>
            </publishMode>
        </qos>
    </publisher>


</profiles>

此配置文件将服务上的发布模式设置为 SYNCHRONOUS ,将客户端上的发布模式设置为 ASYNCHRONOUS 。请注意,我们仅定义了服务和客户端的发布者配置文件,但也可以提供订阅者配置文件。

执行节点

打开两个终端,并在每个终端上加载设置文件。然后设置加载 XML 所需的环境变量:

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_FASTRTPS_USE_QOS_FROM_XML=1
export FASTRTPS_DEFAULT_PROFILES_FILE=~/ros2_ws/src/sync_async_node_example_cpp/ping.xml

在第一个终端上运行服务节点。

ros2 run sync_async_node_example_cpp ping_service

您应该看到服务正在等待请求:

[INFO] [1612977403.805799037] [ping_server]: Ready to serve.

在第二个终端上运行客户端节点。

ros2 run sync_async_node_example_cpp ping_client

您应该看到客户端发送请求并接收响应:

[INFO] [1612977404.805799037] [ping_client]: Sending request
[INFO] [1612977404.825473835] [ping_client]: Response received

同时,服务器控制台中的输出已更新:

[INFO] [1612977403.805799037] [ping_server]: Ready to serve.
[INFO] [1612977404.807314904] [ping_server]: Incoming request
[INFO] [1612977404.836405125] [ping_server]: Sending back response

0913e1ab977a79cb45f4e722dd25d82c.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1933435.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI伦理挑战:构建未来信任的桥梁

在人工智能(AI)技术蓬勃发展的今天&#xff0c;其伦理挑战如同双刃剑的另一面&#xff0c;日益成为全球关注的焦点。面对隐私侵犯、算法偏见、信息真实性危机等伦理困境&#xff0c;我们需要构建全面而精细的应对策略&#xff0c;确保技术进步的同时&#xff0c;守护人类社会的…

MimicMotion-腾讯开源视频生成框架

腾讯宣布开源可控视频生成框架 MimicMotion&#xff0c;该框架可以通过提供参考人像及由骨骼序列表示的动作&#xff0c;来产生平滑的高质量人体动作视频 MimicMotion 具有以下几个亮点&#xff1a; 首先&#xff0c;通过引入了置信度感知的姿态引导信号&#xff0c;大幅提升了…

读书笔记:改善既有代码的设计

差不多两年都没写过博客了&#xff0c;好学的习惯差不多都落下了&#xff0c;两年里几乎也把学到的很多东西都应用了&#xff0c;但不学习好像就有点停步不前的感觉了&#xff0c;以后给自己定个目标每周写一遍博客。 写博客好处&#xff1a; 一是加深自己的印象&#xff08;能…

幽微之处见真章:数据类型与内存存储的内在联系

嘿嘿,家人们,今天咱们来深度剖析数据类型在内存中的存储,好啦,废话多不讲,开干! 1.:数据类型介绍 在前面呢,博主已经介绍了基本的数据类型: char //字符数据类型 ---->占据1个字节 short //短整型 …

51单片机STC89C52RC——18.1 HC-SR04超声波测距

目的/效果 独立按键K1按下后开始测距&#xff0c;LCD显示距离&#xff08;mm&#xff09; 一&#xff0c;STC单片机模块 二&#xff0c;HC-SR04 超声波测距 2.1 HC-SR04 简介 HC-SR04超声波测距模块提供2cm~400cm的测距功能&#xff0c;精度达3mm。 2.2 时序 以上时序图表明…

前端面试题(JS篇五)

一、同步与异步的区别 同步指的是当一个进程在执行某一个请求的时候&#xff0c;如果这个请求需要等待一段时间才能返回&#xff0c;那么这个进程会一直等待下去&#xff0c;直到这个消息返回之后才会继续执行。 指的是当一个进程在执行某一个请求的时候&#xff0c;如果这个请…

Leetcode 1302.层数最深子叶结点的和

大家好&#xff0c;今天我给大家分享一下我关于这个题的想法&#xff0c;我这个题过程比较复杂&#xff0c;但大家如果觉得好的话&#xff0c;就请给个免费的赞吧&#xff0c;谢谢了^ _ ^ 1.题目要求: 给你一棵二叉树的根节点 root &#xff0c;请你返回 层数最深的叶子节点的…

初学者如何通过建立个人博客盈利

建立个人博客不仅能让你在网上表达自己&#xff0c;还能与他人建立联系。通过博客&#xff0c;可以创建自己的空间&#xff0c;分享想法和故事&#xff0c;并与有相似兴趣和经历的人交流。 本文将向你展示如何通过建立个人博客来实现盈利。你将学习如何选择博客主题、挑选合适…

【华为OD笔试】2024D卷命题规律解读【分析300+场OD笔试考点总结】

可上 欧弟OJ系统 练习华子OD、大厂真题 绿色聊天软件戳 od1441了解算法冲刺训练&#xff08;备注【CSDN】否则不通过&#xff09; 文章目录 相关推荐阅读华为OD笔试2024D卷命题规律解读华为OD算法/大厂面试高频题算法练习冲刺训练 相关推荐阅读 【华为OD笔试】2024D卷机考套题…

Android Framework学习笔记(4)----Zygote进程

Zygote的启动流程 Init进程启动后&#xff0c;会加载并执行init.rc文件。该.rc文件中&#xff0c;就包含启动Zygote进程的Action。详见“RC文件解析”章节。 根据Zygote对应的RC文件&#xff0c;可知Zygote进程是由/system/bin/app_process程序来创建的。 app_process大致处…

LLM 的储备知识

GPT一代 模型堆叠了12个解码器层。由于在这种设置中没有编码器&#xff0c;这些解码器层将不会有普通transformer解码器层所具有的编码器-解码器注意力子层。但是&#xff0c;它仍具有自注意力层。 训练过程 Transformer Decoder 结构 编码器&#xff08;6 layers&#xff09…

Template_C++

C模板 C提供了function template. function template&#xff1a;实际上是建立一个通用函数&#xff0c;其函数类型和形参类型不具体制定&#xff0c;用一个虚拟的类型来代表。这个通用的函数就称为函数模版。 是不是可以这样理解&#xff0c;函数模版就是给了一种功能&…

Linux 下 ElasticSearch 集群部署

目录 1. ElasticSearch下载 2. 环境准备 3. ElasticSearch部署 3.1 修改系统配置 3.2 开放端口 3.3 安装 ElasticSearch 4. 验证 本文将以三台服务器为例&#xff0c;介绍在 linux 系统下ElasticSearch的部署方式。 1. ElasticSearch下载 下载地址&#xff1a;Past Rel…

vue 如何做一个动态的 BreadCrumb 组件,el-breadcrumb ElementUI

vue 如何做一个动态的 BreadCrumb 组件 el-breadcrumb ElementUI 一、ElementUI 中的 BreadCrumb 定义 elementUI 中的 Breadcrumb 组件是这样定义的 <template><el-breadcrumb separator"/"><el-breadcrumb-item :to"{ path: / }">主…

算法 —— LRU算法

算法 —— LRU算法 LRULRU算法的工作原理&#xff1a;实现方法&#xff1a;性能考虑&#xff1a; 模拟过程splice函数对于std::list和std::forward_list基本语法&#xff1a;功能描述&#xff1a; 示例&#xff1a;注意事项&#xff1a; 如果大家已经学习过了Cache的替换算法和…

《人性的弱点》

This book is called ‘How to Win Friends & Influence People’. [COPY] 卡耐基《人性的弱点》有什么干货么&#xff1f;

自学鸿蒙HarmonyOS的ArkTS语言<十>@BuilderParam装饰器

作用&#xff1a;当子组件多处使用时&#xff0c;给某处的子组件添加特定功能 一、初始化 1、只能被Builder装饰的方法初始化 2、使用所属自定义组件的builder方法初始化 3、使用父组件的builder方法初始化 - 把父组件的builder传过去&#xff0c;参数名和子组件的builderPar…

【信号频率估计】MVDR算法及MATLAB仿真

目录 一、MVDR算法1.1 简介1.2 原理1.3 特点1.3.1 优点1.3.2 缺点 二、算法应用实例2.1 信号的频率估计2.2 MATLAB仿真代码 三、参考文献 一、MVDR算法 1.1 简介 最小方差无失真响应&#xff08;Mininum Variance Distortionless Response&#xff0c;MVDR&#xff09;算法最…

AI初学者的利器——香橙派AIpro

目录 引言香橙派介绍公司简介&#xff08;来自官网&#xff09;香橙派AIpro介绍香橙派AIPro硬件规格参数开发板接口详情系统登陆与使用指示灯 AI运行实例AI CPU和control CPU的设置方法香橙派AIpro cpu知识查询AIcpu占用率与cpu类别设置 Juypter lab使用JuypterLab介绍JuypterL…

8款可以替代Axure的设计软件推荐

一个好的原型设计工具对于产品经理或者UI/UX设计师来说非常重要。一个好的原型设计软件可以帮助你快速构建一个还原度高、信息结构清晰的原型图&#xff0c;也可以大大降低工作中与同事的沟通成本&#xff0c;更高效地推进工作。 那么&#xff0c;什么是易于使用和免费的原型设…