系列文章目录
留空
前言
自用
一、题目
- 李四是一个作家,每隔三天更新一章小说(小说名:没空)。
- 王二订阅李四的小说。
李四创建一个“小说”话题,并在上面发布和更新“没空”小说的章节,王二订阅此话题。
二、创建工作空间和功能包
(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_node
和 li4_node
可执行文件依赖于rclcpp
和std_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
总结
自用