话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。一般用于不断更新的、少逻辑处理的数据传输场景。以相机信息的采集处理为例,在 ROS 中有一个节点需要时时的发布当前相机采集到的数据,另一个节点会订阅并解析相机数据。
话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色::
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。
advertise("bar",foo:1234)
Talker启动后,会通过RPC地址{foo:1234}在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称{bar}。ROS Master 会将节点的注册信息加入到注册表中。
subscribe("bar")
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。
{foo:1234}
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,如果Talker和Listener的话题一致,就会 Listener 发送 Talker 的 RPC 地址信息。
connect("scan",TCP)
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,进行远程访问,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
TCP server:foo:2345
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。
connect(foo:2345)
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。
data messages
连接建立后,Talker 开始向 Listener 发布消息。
0、从流程0-4通信都是使用的RCP,5-6使用的是TCP协议
1、Talker 与 Listener 的启动注册无先后顺序要求
2、Talker 与 Listener 都可以有多个
3、Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。
4、上述实现流程是做好封装的,可以直接调用。(编写Talker 与 Listener、设置双方话题及实现等等)
编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息并打印输出,订阅方订阅消息并将消息内容打印输出。
流程:
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
/*
发布方实现流程:
1.包含头文件
2.初始化 ROS 节点:命名(在整个ros系统中唯一)
3.创建 ROS 句柄
4.创建 发布者 对象
5.组织被发布的数据,并编写逻辑发布数据
要求:
以10HZ的频率发布数据,并且文本后添加编号
*/
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//2.初始化ROS节点
// 参数1和参数2 后期为节点传值会使用
// 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
ros::init(argc,argv,"Talker");
//3.创建节点句柄
ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能
//4.创建 发布者 对象
//泛型<>: 发布的消息类型
//参数1: 要发布到的话题
//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
//5.组织被发布的数据,并编写逻辑发布数据
//创建被发布的消息
std_msgs::String msg;
//发布频率
ros::Rate rate(10);//10HZ
//设置编号
int count = 0;
//编写循环,循环中发布数据
while(ros::ok())//如果节点活着
{
//实现数据拼接
std::stringstream ss;
ss << "hello " << count;
msg.data = ss.str();
pub.publish(msg);
//添加日志输出
ROS_INFO("发布的消息:%s",msg.data.c_str());
rate.sleep();
count++;
//ros::spinOnce();//官方建议,处理回调函数
}
return 0;
}
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
/*
订阅方实现流程:
1.包含头文件
2.初始化 ROS 节点:命名(唯一)
3.创建 ROS 句柄
4.创建 订阅方 对象
5.处理订阅到的数据
6.ros::spin();
要求:
订阅数据
*/
//回调函数
void doMsg(const std_msgs::String::ConstPtr &msg)
{
//通过msg参数,获取并操作订阅到的数据
ROS_INFO("订阅的数据:%s",msg->data.c_str());
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点:命名(唯一)
ros::init(argc,argv,"Listener");
// 3.创建 ROS 句柄
ros::NodeHandle nh;
// 4.创建 订阅方 对象
泛型<>: 发布的消息类型
//参数1: 要订阅到的话题,需要与发布话题一致
//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
//参数3: 回调函数
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
// 5.处理订阅到的数据
//循环读取接收的数据,并调用回调函数处理
ros::spin();
return 0;
}
add_executable(topic_pub src/topic_pub.cpp)
add_executable(topic_sub src/topic_sub.cpp)
target_link_libraries(topic_pub ${catkin_LIBRARIES})
target_link_libraries(topic_sub ${catkin_LIBRARIES})
????????1.启动 roscore;
??????? 2.启动发布节点;
????????3.启动订阅节点。
?注:订阅的时候,即使先开启订阅节点,依旧会出现前几条消息丢失的情况,这是因为在前几条消息发布的时候,publisher还没在ROS Master中注册完毕。
解决方法:加入ros::Duration(3.0).sleep();? 延迟第一条消息的发送。
分别通过C++和Python实现了发布者和订阅者。比如c++的发布者,用python创建订阅者来订阅,即便语言不通,仍然可以进行数据通信。但前提是任须保持话题一致!!