话题通信机制
话题通信的实现依赖于三个角色的设立:
- 订阅者(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,从这些中间文件调用。
2.4 话题通信自定义msg调用(C++)
需求: 编写发布订阅实现,要求发布方以10Hz的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。
回顾三要素:发布方,接收方,数据 回顾实现流程:
- 编写发布方实现
- 编写接收方实现
- 编辑配置文件
- 编译运行
2.4.1 vscode配置
为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:
{
"configurations": [
{
"browse":{
"databaseFilename": "",
"limitSymbolsToIncludedHeaders": true,
}
"includePath": [
"/opt/ros/noetic/include/**",
"/usr/include/**"
"/xxx/yy工作空间/devel/include/**"//配置head文件
"name":ROS,
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17"
]
}
],
"version": 4
}
2.4.2 编写发布方实现
#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"
int main(int argc, char **argv)
{
setlocale(LC_ALL, ""); // 设置中文编码
ros::init(argc, argv, "talker"); //初始化talker节点(命名唯一)
ros::NodeHandle nh;// 实例化ROS句柄
// 创建发布者对象
// advertise<数据类 型>("话题名称",队列长度)
ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000);
demo02_talker_listener::Person p;
p.name = "八戒";
p.age = 800;
p.height = 5.5;
ros::Rate r(1);
while (ros::ok())
{
pub.publish(p);
p.age += 1;
ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);
r.sleep();
ros::spinOnce();
}
return 0;
}
2.4.3 编写接收方实现
#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"
void doMsg(const demo02_talker_listener::Person::ConstPtr &msg)
{
ROS_INFO("我:%s,今年%d岁,%.2f米", msg->name.c_str(), msg->age, msg->height);
}
int main(int argc, char **argv)
{
setlocale(LC_ALL, "");
// 初始化节点
ros::init(argc, argv, "listener");
// 创建句柄
ros::NodeHandle nh;
// 创建一个Listener的对象
ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person", 1000, doMsg);
ros::spin();
return 0;
}
2.4.4 配置CMakeLists.txt
需要添加add_dependencies用以设置所依赖的消息相关的中间文件。
add_executable(person_talker src/person_talker.cpp)
add_executable(person_listener src/person_listener.cpp)
add_dependencies(person_talker ${catkin_EXPORTED_TARGETS})
add_dependencies(person_listener ${catkin_EXPORTED_TARGETS})
target_link_libraries(person_talker ${catkin_LIBRARIES})
target_link_libraries(person_listener ${catkin_LIBRARIES})
2.4.5 编译运行
- 启动roscore
- 启动发布节点
- 启动订阅节点
2.5 服务通信理论模型
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。
2.5.1 Server注册
Server启动之后,通过RPC在ROS Master中注册自身信息,其中包含提供的服务的名称,ROS Master会将节点的注册信息加入到注册表中。
2.5.2 Client注册
Client启动后,也会通过RPC在ROS Master中注册自身信息,其中包含请求的服务的名称,ROS Master会将节点的注册信息加入到注册表中。
2.5.3 ROS Master匹配
ROS Master根据注册表中的信息匹配Server和Client,并通过RPC向Client发送Server的TCP地址信息。
2.5.4 Client发送请求
Client根据信息匹配使用TCP与Server建立网络连接,并发送请求数据。
2.5.5 Server发送响应
Server接收、解析请求的数据,把响应结果返回给Client。
2.6 服务通信自定义srv
srv文件内的可用数据类型与msg文件一致,且定义srv实现流程与自定义msg实现流程类似,
2.6.1 定义srv文件
srv文件内的可用数据类型与msg文件一致,且定义srv实现流程与自定义msg实现流程类似:
功能包下新建srv目录,添加文件xxx.srv:
int32 a;
int32 b;
int32 sum;# 服务响应数据
2.6.2 编辑配置文件
package.xml中添加编译依赖 与执行依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
CMakeLists.txt中添加编译依赖与执行依赖
find_package(catkin REQUIRED COMPONENTS
message_generation
roscpp
rospy
std_msgs
)
# 必须有std_msgs,否则编译会报错
add_service_files(
FILES
AddInts.srv
)
2.6.3 编译
编译之后的中间文件: c++: xxx.h python: srv
2.7 服务通信自定义srv调用(C++)
2.7.1 vscode配置
{
"configurations": [
{
"browse": {
"databaseFilename": "",
"limitSymbolsToIncludedHeaders": true
},
"includePath": [
"/opt/ros/noetic/include/**",
"/usr/include/**",
"/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径
],
"name": "ROS",
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17"
}
],
"version": 4
}
2.7.2 服务端
#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"