ROS之通信机制
话题通信
话题通信原理
话题通信有三种角色:
- ROS Master (管理者)
- Talker (发布者)
- Listener (订阅者)
Master负责链接发布者和订阅者,通过相同的话题进行发布和订阅链接,链接后发布者和订阅者之间不再需要Master的持续链接,就可以持续发布和订阅通信数据(Master通过roscore启动)
0.Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。
3.Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
4.Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。
5.Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。
6.Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。
注意点:
-
启动listener和tailer前要先启动Master(roscore)
-
上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议
-
Talker 与 Listener 的启动无先后顺序要求
-
Talker 与 Listener 都可以有多个
-
Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。
C++代码实现话题通信
发布者(文件名:Hello_pub)
#include "ros/ros.h"
#include "std_msgs/String.h" //系统内置文本类型的消息
#include <sstream>
int main(int argc, char *argv[])
{
//设置编码,防止中文乱码
setlocale(LC_ALL,"");
//2.初始化 ROS 节点:命名(唯一)
// 参数1和参数2 后期为节点传值会使用
// 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
ros::init(argc,argv,"talker");
//3.实例化 ROS 句柄
ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能
//4.实例化 发布者 对象
//泛型: 发布的消息类型
//参数1: 要发布到的话题
//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
//5.组织被发布的数据,并编写逻辑发布数据
//数据(动态组织)
std_msgs::String msg;
// msg.data = "你好啊!!!";
std::string msg_front = "Hello 你好!"; //消息前缀
int count = 0; //消息计数器
//逻辑(一秒1次)
ros::Rate r(1);
//不通过强制退出就一直为真
while (ros::ok())
{
//使用 stringstream 拼接字符串与编号
std::stringstream ss;
//将数据存放进ss
ss << msg_front << count;
//将文本内容取出,为string类型数据赋值给msg变量,msg变量数据存放在data属性下
msg.data = ss.str();
//发布msg消息
pub.publish(msg);
//加入调试,打印发送的消息,%s为格式化输出char性数组数据,通过c_str()转化出成char类数据
ROS_INFO("发送的消息:%s",msg.data.c_str());
//根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
r.sleep();
count++;//循环结束前,让 count 自增
//暂无应用
ros::spinOnce();
}
return 0;
}
订阅者(文件名:Hello_sub)
#include "ros/ros.h"
#include "std_msgs/String.h"
void doMsg(const std_msgs::String::ConstPtr& msg_p){
ROS_INFO("我听见:%s",msg_p->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.实例化 订阅者 对象,运行后挂起继续向下运行,直到遇见命令主动回调才开始进入doMsg函数中
//回调函数调用时会将接受的订阅数据作为参数传给回调函数,
//"chatter"是要接受的话题名称,Master会通过该名称对于ros运行时的话题进行匹配链接。
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
//5.处理订阅的消息(回调函数)
ros::spin();//循环读取接收的数据,并调用回调函数处理
return 0;
}
配置文件
功能包下的CMakeLists.txt文件,并不是工作空间下的。
# Hello_pub.cpp在配置文件中映射一个名称叫做Hello_pub,可以自定义,但一般选择不带后缀的文件名
add_executable(Hello_pub
src/Hello_pub.cpp
)
add_executable(Hello_sub
src/Hello_sub.cpp
)
# Hello_pub就是填入文件映射名称定位Hello_pub.cpp文件
target_link_libraries(Hello_pub
${catkin_LIBRARIES}
)
target_link_libraries(Hello_sub
${catkin_LIBRARIES}
)
运行
编译文件
catkin_make
启动Master
roscore
刷新环境变量
source ./devel/setup.bash
# 运行Hello_pub文件
rosrun 功能包名称 Hello_pub
# 运行Hello_sub文件
rosrun 功能包名称 Hello_sub
自定义数据的传输
ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,就不能较好的传输。那么我们就要创建自定数据类进行数据打包传输。
在文件数据打包时支持的数据类型有
msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:
-
int8, int16, int32, int64 (或者无符号类型: uint*)
-
float32, float64
-
string
-
time, duration
-
other msg files
-
variable-length array[] and fixed-length array[C]
创建msg文件用于存放自定义数据包
在新建的 msg 目录下添加文件 Person.msg
string name
uint16 age
float64 height
那么Person.msg就是一个自定义数据包,里面有三种属性,是由原生数据组成
自定义文件配置
package.xml中添加:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
CMakeLists.txt文件添加:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
add_message_files(
FILES
Person.msg
)
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
)
关于C++代码
发布者
导包:
#include "功能包名称/msg文件名.h"
例如:
#include "demo02_talker_listener/Person.h"
实例化传输包,赋值和传输:
//包类型为 包名::文件名
demo02_talker_listener::Person p;
p.name = "sunwukong";
p.age = 2000;
p.height = 1.45;
//实例化传输通道,确定数据数据传输类型和话题名称
ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chattern",1000);
//发步消息
pub.publish(p);
订阅者
导包:
#include "功能包名称/msg文件名.h"
例如:
#include "demo02_talker_listener/Person.h"
接收数据
//定接受类型和接受话题名称
ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person",10,doPerson);
回调函数
void doPerson(const demo02_talker_listener::Person::ConstPtr& p){
char[] name=p->name.c_str();
int age=p->age;
double height=p->height;
}
服务通信
服务通信原理
,该模型中涉及到三个角色:
- ROS master(管理者)
- Server(服务端)
- Client(客户端)
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。
0.Server注册
Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Client注册
Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。
3.Client发送请求
Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。
4.Server发送响应
Server 接收、解析请求的数据,并产生响应结果返回给 Client。
C++实现服务通信
自定义传输数据
srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:
- 按照固定格式创建srv文件
- 编辑配置文件
- 编译生成中间文件
srv文件夹内创建test.srv文件
int32 num1
int32 num2
---
int32 sum
int32 num1和int32 num2客户端输入的数据
int32 sum是服务端给客户的返回数据
中间用- - -分割
配置
package.xml:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
CMakeLists.txt:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
add_service_files(
FILES
test.srv # 自定义的文件
)
generate_messages(
DEPENDENCIES
std_msgs
)
这样 test.srv就可以被使用了
服务端代码编写(文件Add_Server.cpp)
#include "ros/ros.h"
#include "first_ros/test.h"//服务通信自定义数据包first_ros是功能包
// bool 返回值由于标志是否处理成功
bool doReq(first_ros::test::Request& req,//接受test文件---上的数据
first_ros::test::Response& resp){//接受test文件---下的数据
//取出客户端输入的数据
int num1 = req.num1;
int num2 = req.num2;
ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
//如果没有异常,那么相加并将结果赋值给 resp下的sum变量
resp.sum = num1 + num2;
//返回值bool告诉服务端是否成功接受数据并提交发送给客户端,自动提交数据给客户端
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点
ros::init(argc,argv,"Add_Server");
// 3.创建 ROS 句柄
ros::NodeHandle nh;
// 4.创建 服务 对象,确定服务话题名称
ros::ServiceServer server = nh.advertiseService("Add",doReq);
ROS_INFO("服务已经启动....");
// 5.回调函数处理请求并产生响应
// 6.由于请求有多个,需要调用 ros::spin(),运行后一直在
// ros::ServiceServer server = nh.advertiseService("Add",doReq);等待运行
ros::spin();
return 0;
}
客户端代码编写(文件:Add_Client.cpp)
#include "ros/ros.h"
//1。导入包
#include "first_ros/test.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点
ros::init(argc,argv,"Add_Client");
// 3.创建 ROS 句柄
ros::NodeHandle nh;
// 4.创建 客户端 对象,接受话题为Add的消息
ros::ServiceClient client = nh.serviceClient<first_ros::test>("Add");
//等待服务启动成功,就可以在启动服务端前运行客户端
//方式1
ros::service::waitForService("Add");
//方式2
// client.waitForExistence();
// 5.组织请求数据
first_ros::test ai;
std::cout<<"请输入两个整数"<<std::endl;
std::cin>>ai.request.num1>>ai.request.num2;
// 6.发送请求,返回 bool 值,标记是否成功,,就是服务端回调函数的返回值
bool flag = client.call(ai);
// 7.处理响应
if (flag)
{
ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
}
else
{
ROS_ERROR("请求处理失败....");
return 1;
}
return 0;
}
配置
add_executable(Add_Server src/Add_Server.cpp)
add_executable(Add_Client src/Add_Client.cpp)
add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp)
add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)
target_link_libraries(Add_Server
${catkin_LIBRARIES}
)
target_link_libraries(Add_Client
${catkin_LIBRARIES}
)
运行
运行
编译文件
catkin_make
启动Master
roscore
刷新环境变量(未找到运行文件就要刷新变量)
source ./devel/setup.bash
运行
# 运行Add_Server.cpp文件
rosrun first_ros Add_Server
# 运行Add_Client,cpp文件
rosrun first_ros Add_Client
参数服务
原理
参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:
- ROS Master (管理者)
- Talker (参数设置者)
- Listener (参数调用者)
ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。
整个流程由以下步骤实现:
1.Talker 设置参数
Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。
2.Listener 获取参数
Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。
3.ROS Master 向 Listener 发送参数值
ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。
C++代码实现参数设置
#include "ros/ros.h"
int main(int argc, char *argv[])
{
ros::init(argc,argv,"set_update_param");
//NodeHandle--------------------------------------------------------
ros::NodeHandle nh;
//设置也可以是修改
nh.setParam("nh_int",10); //整型
nh.setParam("nh_double",3.14); //浮点型
nh.setParam("nh_bool",true); //bool
nh.setParam("nh_string","hello NodeHandle"); //字符串
nh.setParam("nh_vector",stus); // vector
nh.setParam("nh_map",friends); // map
/*
param(键,默认值)
存在,返回对应结果,否则返回默认值
getParam(键,存储结果的变量)
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
getParamCached键,存储结果的变量)--提高变量获取效率
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
getParamNames(std::vector<std::string>)
获取所有的键,并存储在参数 vector 中
hasParam(键)
是否包含某个键,存在返回 true,否则返回 false
searchParam(参数1,参数2)
搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
deleteParam("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
*/
return 0;
}
运行中的常用命令
rosnode
rosnode ping 节点名 //测试到节点的连接状态
rosnode list //列出活动节点
rosnode info 节点名 //打印节点信息
rosnode machine //列出指定设备上节点
rosnode kill 节点名 //杀死某个节点
rosnode cleanup //清除不可连接的节点
rostopic
rostopic bw //显示主题使用的带宽
rostopic delay //显示带有 header 的主题延迟
rostopic echo 话题名 // 打印消息到屏幕
rostopic find 数据类型 //根据类型查找主题
rostopic hz 话题名 //显示主题的发布频率
rostopic info 话题名 //显示主题相关信息
rostopic list //显示所有活动状态下的主题
rostopic pub 话题名 //将数据发布到主题
rostopic type 话题名 //打印主题类型
rosmsg
rosmsg list
rosmsg package 包名 //列出某个包下的所有msg
rosmsg show 消息名称 //显示消息描述
rosservice
rosservice list 列出所有活动的服务
rosservice args 服务名 //打印服务参数
rosservice info 服务名 // 打印有关服务的信息
rosservice call 服务名。。。。tab键提取 //调用服务对象发布消息
rossrv info 消息数据名 // 详细的服务消息数据组成,话题是rosmsg info 话题数据名
rosparam
获取和设置ROS中master参数。
rosparam set 设置参数
rosparam get 获取参数
rosparam load 从外部文件加载参数
rosparam dump 将参数写出到外部文件
rosparam delete 删除参数
rosparam list 列出所有参数