话题通信机制
话题通信 的实现依赖于三个角色的设立:
- 订阅者(Subscriber)
- 发布者(Publisher)
- 管理者(Master)
简单来说,Master负责保管发布者和订阅者注册的信息,帮助匹配话题相同的发布者和订阅者,实现话题通信。
这样的连接建立之后,发布者可以发布消息,且发布的消息会被订阅者订阅。
2.1 话题通信的概念
2.1.1 发布者的注册
发布者启动之后,会通过RPC在Masterzhub注册自己,同时会注册自己发布的话题(名称)。Master会将节点的注册信息加入注册表。
2.1.2 订阅者的注册
订阅者启动之后,也会通过RPC在Masterzhub注册自己,同时会注册自己订阅的话题(名称)。Master会将节点的注册信息加入注册表。
2.1.3 Master实现匹配
Master会定时扫描注册表,将注册表内的节点信息进行匹配(并通过RPC向订阅者发送发布者的RPC地址),将匹配成功的节点信息加入匹配表。
2.1.4 Subscriber向Pubilsher发送连接请求
订阅者根据接受的RPC地址,通过RPC向发布者发布连接请求(订阅的话题名称、消息类型以及通信协议)。
通信协议有TCP和UDP两种。
2.1.5 Publisher确认请求
Publisher收到请求,通过RPC返回一个信息确认,发送自己的地址信息。
2.1.6 连接建立
Subscriber根据上一步返回的消息,拿到确认了拿到地址了,这样就可以建立连接了。
2.1.7 发送消息
连接建立后,由发布者发送消息,订阅者收到消息。
我感觉这个协议很像TCP的那套握手挥手,事实上前五步都是在使用RPC协议,最后两步使用了TCP协议。
需要注意的是发布者和订阅者之间没有“先有鸡后有蛋”的讲究,也就是不考虑启动顺序;发布者和订阅者都可以有多个;连接建立之后,Master就不再重要(做了个媒),关闭Master也可以照常通信。
2.2 C++实现话题通信
ROS master不需要实现了,而且连接的建立也已经被封装了。所以现在回想ROS的话题通信,还剩下发布方和接收方,以及流通的“货币”————数据。
实现流程一般是如下框架:
- 编写发布方实现
- 编写接收方实现
- 编辑配置文件
- 编译运行
2.2.1 发布方Talker
# include "ros/ros.h"
# include "std_msgs/String.h"
# include <sstream> //读取字符串并且处理
int main(int argc, char **argv[])
{
setlocale(LC_ALL, ""); // 设置中文编码
ros::init(argc, argv, "talker"); //初始化talker节点
ros::NodeHandle nh; //创建节点句柄
ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 1000); //创建发布者
std_msgs::String msg;// 动态组织数据
std::string msg_front = "Hello 你好!";
int count = 0; // 消息计数器
ros::Rate r(10); //设置循环频率
while (ros::ok())
{
std::stringstream ss;
ss << msg_front << count;
msg.data = ss.str(); //将数据组织成字符串
chatter_pub.publish(msg); //发布消息
ROS_INFO("%s", msg.data.c_str()); //打印消息
r.sleep(); //循环频率
count++; //消息计数器
ros::spinOnce(); //处理回调函数
}
}
2.2.2 接收方Listener
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("我听见你说: [%s]", msg->data.c_str()); // 输出接收到的消息
}
int main(int argc, char **argv)
{
setlocale(LC_ALL, ""); // 设置中文编码
ros::init(argc, argv, "listener"); //初始化listener节点(命名唯一)
ros::NodeHandle nh;// 实例化ROS句柄
// 创建订阅者
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
// 处理订阅的消息(回调函数)
ros::spin();
return 0;
}
"
2.2.3 配置CMakeLists.txt
add_executable(talker src/talker.cpp) // 编译talker
add_executable(listener src/listener.cpp) // 编译listener
target_link_libraries(Hello_pub
${catkin_LIBRARIES}
) // 链接库
target_link_libraries(Hello_sub
${catkin_LIBRARIES}
) // 链接库
2.2.4 执行
- 启动roscore
- 启动talker
- 启动listener
2.3 话题通信自定义msg
在ROS通信协议中,还需要注意msg(数据),ROS通过std_msgs封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty...
这些数据一般只包含一个data字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,std_msgs本身提供的描述性就有点弱,这时,需要自定义消息类型。
msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型:
- int8, int16, int32, int64(uint*)
- float32, float64
- string
- time, duration
- other msg files
- variable-length array[] and fixed-length array[C]
ROS中还有一种特殊类型:Header
这个标头包含时间戳和ROS中常用的坐标帧信息.
需求:创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。
流程:
- 按照固定格式创建msg文件
- 编辑配置文件
- 编译运行(中间文件)
2.3.1 创建msg文件
功能包下新建msg目录,添加文件Person.msg
string name
uint16 age
float64 height
2.3.2 编辑配置文件
package.xml中添加编译依赖与执行依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
CMakeLists.txt编辑 msg 相关配置
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
// 配置 msg 源文件
add_message_files(
FILES
Person.msg
)
// 生成消息时依赖于 std_msgs
generate_messages(
DEPENDENCIES
std_msgs
)
// 执行时依赖
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
2.3.4 编译运行
编译后的中间文件查看:
C++需要调用的中间文件(.../工作空间/devel/include/PACKAGE_NAME/MSG_NAME.h)
Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/msg)
后续调用相关msg,从这些中间文件调用。