ROS2 + C++ 话题(自用笔记)

系列文章目录

留空



前言

自用


一、题目

  • 李四是一个作家,每隔三天更新一章小说(小说名:没空)。
  • 王二订阅李四的小说。

李四创建一个“小说”话题,并在上面发布和更新“没空”小说的章节,王二订阅此话题。在这里插入图片描述

二、创建工作空间和功能包

(1)创建工作空间

在上一篇“节点”时,我们就已经创建了工作空间towm_ws

我们直接进入工作空间

cd towm_ws

(2)创建功能包

进入src文件夹

cd src

创建一个基于C++learning_topic功能包

ros2 pkg create learning_topic --build-type ament_cmake --dependencies rclcpp

(3)创建李四和王二节点

在这里插入图片描述

三、使用RCLCPP编写话题

1.基础代码

(1)作家李四节点

#include "rclcpp/rclcpp.hpp"

class Writers : public rclcpp::Node
{
    public:
        Writers(std::string name) : Node(name)
        {
            RCLCPP_INFO(this->get_logger(),"作家节点%s已启动",name.c_str());

        }

    private:


};

int main(int argc,char** argv)
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared<Writers>("li4");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

(2)读者王二节点

#include "rclcpp/rclcpp.hpp"

class Readers : public rclcpp::Node
{
    public:
        Readers(std::string name) : Node(name)
        {
            RCLCPP_INFO(this->get_logger(),"读者节点%s已启动",name.c_str());

        }

    private:


};

int main(int argc,char** argv)
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared<Readers>("wang2");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

(3)每次编写完代码记得修改CmakeLists.txt,添加以下代码

add_executable(wang2_node src/wang2.cpp)
ament_target_dependencies(wang2_node rclcpp)
add_executable(li4_node src/li4.cpp)
ament_target_dependencies(li4_node rclcpp)

install(TARGETS
    li4_node
    wang2_node
    DESTINATION lib/${PROJECT_NAME}
)

打开一个终端

colcon build --packages-select learning_topic
source install/setup.sh
ros2 run learning_topic wang2_node

打开另一个终端

source install/setup.sh
ros2 run learning_topic li4_node

在这里插入图片描述
若无报错,我们就可以开始编写发布和订阅的代码了。

2.编写“小说”话题发布者

完整代码

li4.cpp

#include "rclcpp/rclcpp.hpp"
//(1)导入发布的话题接口类型
#include "std_msgs/msg/string.hpp"

class Writers : public rclcpp::Node
{
    public:
        Writers(std::string name) : Node(name)
        {
            RCLCPP_INFO(this->get_logger(),"作家节点%s已启动",name.c_str());
            //(3)创建发布者
            pub_writers = this->create_publisher<std_msgs::msg::String>("no_time",10);
            //(7)创建定时器
            NovelsTimer = this->create_wall_timer(std::chrono::milliseconds(3000), std::bind(&Writers::publish_message, this));
        }

    private:
    //(2)声明发布者
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_writers;
    //(6)声明定时器
    rclcpp::TimerBase::SharedPtr NovelsTimer;

    int chapter = 0;//小说的章节数

    //(4)创建发布数据函数
    void publish_message()
    {
        //(5)编写发布数据函数
        std_msgs::msg::String message;
        message.data = "<<我哪天都没空>>第" + std::to_string(chapter) + "章,我第" + std::to_string(chapter) + "天还是没空"; 
        pub_writers->publish(message);
        RCLCPP_INFO(this->get_logger(), "李四更新: '%s'", message.data.c_str());
        chapter ++;
    }

};

int main(int argc,char** argv)
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared<Writers>("li4");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

编写话题发布者的步骤:
(1)导入发布的话题接口类型
(2)声明发布者
(3)创建发布者
(4)创建发布数据函数
(5)编写发布数据函数

接下来,我们按着步骤编写代码

(1)导入发布的话题接口类型

在ROS 2中,每个话题都有特定的消息类型,话题发布者和订阅者的消息类型必须是一致的
如果发布者发布的是字符串类型的数据,而订阅者接收的是整数(int)类型的数据。发布者向话题发布了一个字符串"hello!",那么订阅者将无法成功接收这个消息。
所以编写一个发布者节点时,我们需要导入要发布的消息类型,就像是告诉话题订阅者:“我这个话题要发布的消息是什么样的,记得用一样的消息类型来接收”。比如这儿“小说”话题发布的是字符串std_msgs::msg::String类型的消息,所以我们导入字符串std_msgs/msg/string.hpp接口类型,同样等会订阅者也要导入相同的消息类型。

#include "std_msgs/msg/string.hpp"

(2)声明发布者

声明发布者的模板代码

rclcpp::Publisher<发布的消息类型>::SharedPtr 发布者对象;

“小说”话题,我们需要声明std_msgs::msg::String消息类型的一个发布者对象pub_writers

rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_writers;

这样,我们就声明了一个名为pub_writers的智能指针变量,它指向一个能够发布std_msgs::msg::String类型消息的发布者对象。稍后,我们就可以使用这个发布者对象来发布消息到“小说”话题中。

(3)创建发布者

创建发布者的模板代码

pub_writers = this->create_publisher<发布的消息类型>("话题名称",缓冲区大小);

我们需要创建一个发布者对象,发布std_msgs::msg::String类型的消息到 “no_time” 话题

pub_writers = this->create_publisher<std_msgs::msg::String>("no_time",10);

这样,我们就创建了一个发布者pub_writers,可以向no_time话题发布std_msgs::msg::String类型的消息。该发布者对象的缓冲区大小为10,表示最多可以缓存10条未发送的消息。

什么是缓冲区?

发布者对象的缓冲区大小不是用来缓存未发送的数据的,而是用来缓存已经发布但还未被接收的数据的。

当发布者发布一条消息时,消息会被放入缓冲区中,等待被传输到指定的话题。如果缓冲区已满,新发布的消息将会被丢弃,而不是被缓存起来。缓冲区的作用是在消息发布速度快于消息接收速度时,可以暂时存储一定数量的消息,以确保消息不会丢失。

我们设置发布者对象的缓冲区大小为10,意味着最多可以缓存10条“小说”话题已发布但未被接收的数据。如果缓冲区已满,订阅者只能收到缓冲区中的最后10条消息。

(4)创建发布数据函数

void publish_message()
 {

 }

(5)编写发布数据函数

  • 编写要发送的数据

创建一个std_msgs::msg::String类型的消息对象message

std_msgs::msg::String message;

然后将消息的数据字段设置为想要发布的数据。

message.data = "<<我哪天都没空>>第n章,我第n天还是没空"; 
  • 发布数据

使用发布者对象pub_writers发布这条消息。

 pub_writers->publish(message);

完整的发布数据函数如下:

void publish_message()
 {
    //(5)编写发布数据函数
     std_msgs::msg::String message;
     message.data = "<<我哪天都没空>>第n章,我第n天还是没空"; 
     pub_writers->publish(message);
     RCLCPP_INFO(this->get_logger(), "李四更新: '%s'", message.data.c_str());
 }

但是上面的发布函数只能发送一次消息,如果我们想要实现定期发送消息,就需要一个定时器实现周期性的数据发布。我们给定时器设置一个时间间隔,每个间隔结束时,就会自动触发定时器回调函数来执行发布消息的任务。(若不需要定期发布,可以跳过步骤(6)和(7)

基于上面的步骤,还要增加两个步骤。

(6)声明定时器
(7)创建定时器


(6)声明定时器

在ROS 2中,TimerBase是用于创建定时器的基类。

rclcpp::TimerBase::SharedPtr NovelsTimer;

(7)创建定时器

使用create_wall_timer函数创建一个定时器NovelsTimer,该定时器每隔3000毫秒(即3秒)就会触发publish_message()函数,定期发布消息。

NovelsTimer = this->create_wall_timer(std::chrono::milliseconds(3000), std::bind(&Writers::publish_message, this));

this->create_wall_timer: WallTimer是一种ROS 2中的定时器,表示在Wall时间(即系统的真实时间)上运行。
std::chrono::milliseconds(3000): 指定定时器的时间间隔。在这里,定时器每隔3000毫秒(即3秒)触发一次。
std::bind(&Writers::publish_message, this): 使用std::bind函数将publish_message()函数绑定到Writers类的实例上。定时器触发时自动调用publish_message()函数。

3.编写“小说”话题订阅者

完整代码
wang2.cpp

#include "rclcpp/rclcpp.hpp"
//(1)导入订阅的话题接口类型
#include "std_msgs/msg/string.hpp"

class Readers : public rclcpp::Node
{
    public:
        Readers(std::string name) : Node(name)
        {
            RCLCPP_INFO(this->get_logger(),"读者节点%s已启动",name.c_str());

            //(4)创建订阅者
            sub_readers = this->create_subscription<std_msgs::msg::String>
            ("no_time",10,std::bind(&Readers::readnovels_callback,this,std::placeholders::_1));

        }


    private:
        //(2)声明订阅者
        rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_readers;
        //(3)创建订阅回调函数
        void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
        {
            //(5)编写订阅回调函数
            RCLCPP_INFO(this->get_logger(),"王二已阅读%s",novels->data.c_str());
        }

};

int main(int argc,char** argv)
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared<Readers>("wang2");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

编写话题发布者的步骤:
(1)导入订阅的话题接口类型
(2)声明订阅者
(3)创建订阅回调函数
(4)创建订阅者
(5)编写订阅回调函数

(1)导入订阅的话题接口类型

王二订阅的“小说”话题发布的是字符串std_msgs::msg::String类型的消息,所以我们要导入字符串std_msgs/msg/string.hpp接口类型。

#include "rclcpp/rclcpp.hpp"

(2)声明订阅者

声明订阅者的模板代码

rclcpp::Subscription<订阅的消息类型>::SharedPtr 订阅者对象;

根据模版

rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_readers;

这样,我们就声明了一个名为sub_readers的智能指针变量,它指向一个能够发布std_msgs::msg::String类型消息的订阅者对象。稍后,我们就可以使用这个订阅者对象来订阅“小说”话题的消息。

(3)创建订阅回调函数

对于发布者来说,它的主要职责是将消息发布到话题,因此通常不需要回调函数。发布者只需将消息发布到主题后,它的任务就完成了,不需要等待其他节点对发布的消息做出响应。订阅者需要回调函数来处理接收到的消息,以便在消息到达时做出响应。 因为创建订阅者需要绑定回调函数,所以先创建,稍后编写。

创建一个readnovels_callback函数作为收到话题消息后的回调函数。

void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
{

}

当接收到 订阅的“小说”话题 的std_msgs::msg::String类型的消息时,就会自动调用这个函数,并将接收到的消息(可能包含的是小说的内容、名称或其他与小说相关的字符串信息)作为参数传递给函数内部。

(4)创建订阅者

创建订阅者的模板代码

一个订阅对象 = this->create_subscription<订阅话题的消息类型>
            ("订阅的话题名称",缓冲区大小,std::bind(&Readers::回调函数,this,占位符));

我们需要创建一个订阅对象,订阅 “no_time” 话题。

sub_readers = this->create_subscription<std_msgs::msg::String>
            ("no_time",10,std::bind(&Readers::readnovels_callback,this,std::placeholders::_1));

这样,就创建好了一个订阅者对象sub_readers,订阅话题 “no_time”,话题的消息类型为std_msgs::msg::String。订阅者的缓冲区为 10,可以存储最多 10 条消息。当有新消息发布到 “no_time” 主题时,将调用readnovels_callback回调函数,并将接收到的消息作为参数传递。

(5)编写订阅回调函数

void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
{
    //(5)编写订阅回调函数
    RCLCPP_INFO(this->get_logger(),"王二已阅读%s",novels->data.c_str());
}

当接收到消息时,会打印发布者发到话题上的消息。


四、完整代码

li4.cpp

#include "rclcpp/rclcpp.hpp"
//(1)导入发布的话题接口类型
#include "std_msgs/msg/string.hpp"

class Writers : public rclcpp::Node
{
    public:
        Writers(std::string name) : Node(name)
        {
            RCLCPP_INFO(this->get_logger(),"作家节点%s已启动",name.c_str());
            //(3)创建发布者
            pub_writers = this->create_publisher<std_msgs::msg::String>("no_time",10);
            //(7)创建定时器
            NovelsTimer = this->create_wall_timer(std::chrono::milliseconds(3000), std::bind(&Writers::publish_message, this));
        }

    private:
    //(2)声明发布者
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_writers;
    //(6)声明定时器
    rclcpp::TimerBase::SharedPtr NovelsTimer;

    int chapter = 0;//小说的章节数

    //(4)创建发布数据函数
    void publish_message()
    {
        //(5)编写发布数据函数
        std_msgs::msg::String message;
        message.data = "<<我哪天都没空>>第" + std::to_string(chapter) + "章,我第" + std::to_string(chapter) + "天还是没空"; 
        pub_writers->publish(message);
        RCLCPP_INFO(this->get_logger(), "李四更新: '%s'", message.data.c_str());
        chapter ++;
    }

};

int main(int argc,char** argv)
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared<Writers>("li4");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

wang2.cpp

#include "rclcpp/rclcpp.hpp"
//(1)导入订阅的话题接口类型
#include "std_msgs/msg/string.hpp"

class Readers : public rclcpp::Node
{
    public:
        Readers(std::string name) : Node(name)
        {
            RCLCPP_INFO(this->get_logger(),"读者节点%s已启动",name.c_str());

            //(4)创建订阅者
            sub_readers = this->create_subscription<std_msgs::msg::String>
            ("no_time",10,std::bind(&Readers::readnovels_callback,this,std::placeholders::_1));

        }


    private:
        //(2)声明订阅者
        rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_readers;
        //(3)创建订阅回调函数
        void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
        {
            //(5)编写订阅回调函数
            RCLCPP_INFO(this->get_logger(),"王二已阅读%s",novels->data.c_str());
        }

};

int main(int argc,char** argv)
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared<Readers>("wang2");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

每次编写完代码记得修改CmakeLists.txt,添加以下代码
它表示 wang2_nodeli4_node 可执行文件依赖于rclcppstd_msgs两个软件包

find_package(std_msgs REQUIRED)

add_executable(wang2_node src/wang2.cpp)
ament_target_dependencies(wang2_node rclcpp std_msgs)
add_executable(li4_node src/li4.cpp)
ament_target_dependencies(li4_node rclcpp std_msgs)

install(TARGETS
    li4_node
    wang2_node
    DESTINATION lib/${PROJECT_NAME}
)

这次还要修改package.xml,添加第二行代码。
在ROS 2中,package.xml文件用于指定ROS软件包的元数据信息,包括依赖关系、作者信息、版本号等。
std_msgs是 ROS 2中定义了一些标准消息类型的软件包,例如std_msgs::msg::String等,我们代码中使用了,就要把它添加到依赖里。

 <depend>rclcpp</depend>
 <depend>std_msgs</depend> 

五、编译

打开一个终端

colcon build --packages-select learning_topic
source install/setup.sh
ros2 run learning_topic wang2_node

打开另一个终端

source install/setup.sh
ros2 run learning_topic li4_node

(1)无定时器
在这里插入图片描述

(2)有定时器

在这里插入图片描述
再打开一个终端就可以看见节点和话题了。

rqt_graph

在这里插入图片描述

总结

自用

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值