话题通信
理论模型
话题通信的三个角色:
ROS Master
–管理者
-> 负责保管Talker
和Listener
注册的信息并匹配话题相同的Talker
与Listener
Talker
–发布者
-> 发布消息Listener
–订阅者
-> 订阅来自Talker
的消息
话题通信的步骤:
1. Talker向Master注册自身信息,包括发布的话题名称
2. Listener向Master注册自身信息,包括订阅的话题名称
3. Master将Talker和Listener进行匹配
4. Master匹配完成后,将Talker的RPC地址发送给Listener
5. Listener根据RPC地址,通过RPC向Talker发送连接请求
6. Talker确认连接信息,发送自己的TCP地址信息
7. Listener根据Talker的TCP地址信息,通过TCP通讯协议与Talker建立连接
=============================================================
注意点
1. Talker与Listener的连接使用的是RPC协议,但最后两步通讯使用TCP协议
2. Talker与Listener无先后启动顺序要求
3. Talker与Listener可以有多个
4. Talker与Listener连接后,不再需要Master,因此即使关闭Master,二者仍可正常通信
=============================================================
C++
的话题通讯基本操作
实际的话题通信建立过程,我们关注什么?
需要关注的:
1. Talker
2. Listener
3. 通信的数据
=============================================================
无需关注的:
1. ROS Master
2. 连接过程(RPC、TCP等)
这是因为这两部分内容已经被封装
话题通讯模型建立流程
1. 编写Talker实现
2. 编写Listener实现
3. 编辑配置文件CmakeList等
4. 编译并执行(Vscode使用crtl+shift+B编译)
Talker
代码实现
/*
消息发布方:
循环发布信息:HelloWorld 后缀数字编号
实现流程:
1.包含头文件
2.初始化 ROS 节点:命名(唯一)
3.实例化 ROS 句柄
4.实例化 发布者 对象
5.组织被发布的数据,并编写逻辑发布数据
*/
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char *argv[]){
//1. 设置编码
setlocale(LC_ALL, "");
//2.初始化 ROS 节点:命名(唯一)
ros::init(argc, argv, "Talker");
//3.实例化 ROS 句柄
ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能
//4.实例化 发布者 对象
ros::Publisher pub = nh.advertise<std_msgs::String>("Chatter", 10);
//5.组织被发布的数据,并编写逻辑发布数据
std_msgs::String msg;
std::string msg_front = "Hello nihao!";
int count = 0;
//循环(一秒10次)
ros::Rate r(1);
while (ros::ok())
{
std::stringstream ss;
ss << msg_front << count;
msg.data = ss.str();
pub.publish(msg);
ROS_INFO("masage sent:%s",msg.data.c_str());
r.sleep();
count ++;
ros::spinOnce();
}
return 0;
}
代码中的名词:
节点
:ROS系统中程序运行与通信的基本单位,一个程序只能对应一个节点
,节点的命名是唯一的句柄
:可以简单理解为节点的把柄,系统可以根据句柄的使用情况,开启或关闭节点来节省内存。一个节点可以对应多个句柄,一个句柄可以对于多个Talker
与Listener
C++代码
ros::init(argc, argv, "name")
这行代码用于初始化一个ROS节点,
argc
argv
后期传值使用
name
表示节点的命名,节点的命名是唯一的,不可重复ros::NodeHandle nh
实例化一个句柄对象,名称为nh
,该类封装了 ROS 中的一些常用功能ros::Publisher pub
实例化一个Talker
对象,名称为pub
,这行代码同时实现了注册的功能nh.advertise<std_msgs::String>("Chatter", 10)
利用句柄nh
,
nh.advertise("Chatter", 10)
表示当前节点要发布话题
"Chatter"
为话题的名称,该名称为Listener
的订阅对象
10
表示发送信息在大于10个之后舍弃之前发布的消息
<std_msgs::String>
表示发布的话题消息类型std_msgs::String
为ROS系统的标准消息之一String
ros::ok()
可用于检测crtl+c
退出,当检测到crtl+c
时,变为False
循环终止
或所有节点被关闭时,输出False
pub.publish(msg)
实例化的Talker
发布消息msg
ros::Rate r(1)
用作定时器的作用,1
表示1hz
结合r.sleep()
完成定时效果ros::spinOnce()
十分重要的函数,称为消息回调处理函数
没有这行代码,Listener
就不会执行回调函数Callback
,而是只仅仅进行实参传输
Listener
代码实现
#include "ros/ros.h"
#include "std_msgs/String.h"
void doMsg(const std_msgs::String::ConstPtr& msg_p){
ROS_INFO("I Hear:%s",(*msg_p).data.c_str());
}
int main(int argc, char *argv[]){
setlocale(LC_ALL, "");
ros::init(argc, argv, "Listener");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe("Chatter", 10, doMsg);
ros::spin();
return 0;
}
代码的解读
- 可以看到相比于
Talker
,Listener
多了一个doMsg
的回调函数,少了一个publish
- 回调函数的作用在于,
Listener
每次接收到消息之后,都会将接收的消息传入回调函数,而ros::spin()
函数负责控制执行该回调函数,与ros::spinOnce()
作用类似。 - 缺少类似于
publish
的函数是因为,订阅的过程是被动的,所以被封装化了,在这个订阅的过程中,不断有参数传入回调函数,创建了多个函数内容相同但参数不同的回调函数序列。 ros::spin()
和ros::spinOnce()
的区别在于:ros::spin()
是一个内部循环的函数,内部循环不断执行被传入参数的回调函数队列ros::spinOnce()
只执行当前回调函数队列中的第一个
CmakeList与launch文件设置
add_executable(talker_test src/talker_test.cpp)
add_executable(listener_test src/listener_test.cpp)
target_link_libraries(talker_test ${catkin_LIBRARIES})
target_link_libraries(listener_test ${catkin_LIBRARIES})
<launch>
<node pkg="topic_test" type="talker_test" name="talker" output="screen" />
<node pkg="topic_test" type="listener_test" name="listener" output="screen" />
</launch>
示例效果
问题
1. 为什么订阅时第一条消息丢失了?
这是因为发布第一条消息时,Listener
还没有注册完毕
在publisher
注册完毕之后加入ros::Duration(3.0).sleep()
休眠一段时间即可
2. 关于rqt_graph
通过该图,可以很好地看出,我们定义了两个节点talker
和listener
,talker
发布了话题Chatter
,Listener
订阅
3. 总结 -- 在代码中我们究竟需要做什么?
- 编写
Talker
节点,应包括节点初始化、句柄初始化、基于句柄实例注册发布者(包括其话题名称、队列最大保存消息数)、发布消息指令; - 编写
Listener
节点,应包括节点初始化、句柄初始化、基于句柄实例注册订阅者、编写回调函数、适当使用spin
函数执行回调; - 填写
CmakeList
与launch
文件;