ROS学习总结——节点之间的通信方式及其实现(C++)


ROS学习总结——节点之间的两种基本的通信方式实现(C++)

  • 节点(Nodes)
    • 节点相关指令
  • 通信方式——话题(Topic)
    • 话题通信机制详解
      • 几个关键字的解释
      • 发布者和订阅者建立连接之前的准备
      • 发布者和订阅者建立连接
    • 使用C++构建订阅者和发布者节点
      • 创建msg文件
      • 创建发布者(Publisher)
      • 创建订阅者(Subscriber)
      • 编译节点
    • 话题相关命令
  • 通信方式——服务(Services)
    • 服务通信机制详解
      • 服务器和客户端建立连接之前的准备
      • 服务器和客户端通信的过程
    • 使用C++构建一个服务器和客户端
      • 创建srv文件
      • 创建服务端(Service)
      • 创建客户端(Client)
      • 编译节点
    • 服务的相关命令
  • 关于CMakeLists.txt以及package文件的详细内容(去注释)
    • CMakeLists.txt
    • package.xml

节点(Nodes) 节点,一个节点即为一个可执行文件,它可以通过ROS与其他节点进行通信。
节点相关指令
$ roscore#运行所有ROS程序之前都要首先执行的指令 $ rosrun [package_name] [node_name]#运行一个节点 $ rosnode list#显示当前运行的节点 $ rosnode ping [node_name]#测试一个正在运行的节点

通信方式——话题(Topic) Topic是ROS节点之间的一种单向的通讯方式,发布者用于发布话题,同理订阅者用来订阅话题;Topic通信通常适合于对实时性有要求,需要频繁通信的场合。
一个话题可以有多个发布者,也可以有多个订阅者;两者之间的关系如下图
ROS学习总结——节点之间的通信方式及其实现(C++)
文章图片

话题通信机制详解 ROS学习总结——节点之间的通信方式及其实现(C++)
文章图片

几个关键字的解释
XML/RPC:全称是XML Remote Procedure Call,即XML远程方法调用。它是一套允许运行在不同操作系统、不同环境的程序实现基于Internet过程调用的规范和一系列的实现。这种远程过程调用使用http作为传输协议,XML作为传送信息的编码格式。
TCPROS:ROS消息和服务的传输层。它使用标准的TCP/IP套接字来传输消息数据。入站连接通过一个包含消息数据类型和路由信息的头的TCP服务器套接字接收。
ROS Master:ROS的节点管理器,ROS名称服务。
发布者和订阅者建立连接之前的准备
  1. 发布者(Publisher)节点运行后会使用XML/RPC向 ROS Master 注册发布者的相关信息,包括节点名称、话题名称、消息类型、话题缓存大小等,Master会将这些信息存入注册列表中。
  2. 订阅者(Subscriber)节点运行后会使用XML/RPC向 ROS Master 注册订阅者的相关信息,包括节点名称、话题名称、消息类型、话题缓存大小等,Master会将这些信息存入注册列表中。
  3. Master会根据订阅者提供的相关信息,在注册列表中寻找匹配的发布者;如果找到合适的发布者,则将发布者的地址发送给订阅者;如果没有找到匹配的发布者则会一直等待,直到有合适的发布者注册。
  4. 在订阅者接收到Master的回应后,会根据接收到的发布者信息,使用XML/RPC请求与发布者之间直接进行连接;
  5. 发布者接收到订阅者的信息后,会使用XML/RPC方式将TCP服务器的URI地址和端口作为连接响应发送给订阅者节点。
发布者和订阅者建立连接
  1. 订阅者在接收到发布者返回的确认消息后,将会使用TCPROS创建一盒与发布者节点对应的客户端,并直接与发布者节点连接。
  2. 成功建立连接后,发布者将会使用TCPROS向订阅者发布消息。
使用C++构建订阅者和发布者节点 创建msg文件
msg是一个简单的文本文件,描述了ROS message的变量(fileds、数据)。ROS会根据msg文件中定义的内容自动生成不同语言的消息源码
msg文件允许的变量类型
int8, int16, int32, int64 (plus uint*)
float32, float64
string
time, duration
other msg files
variable-length array[] and fixed-length array[C]
第一步:在Package内创建msg文件夹,用于存放编写的msg文件
$ roscd beginner_tutorials $ mkdir msg

第二步:编写msg文件
$ cd msg $ echo "string data" >> message.msg $ echo "int64 count" >> message.msg

第三步:在package.xml功能包中添加下面两行依赖
message_generation用于生成与msg文件相关的.h和.py文件。message_runtime则是运行时的依赖。
message_generation message_runtime

第四步:修改CMakeLists.txt文件中的编译选项
①找到find_package添加参数message_generation
②去掉add_message_files的注释,将参数更改为(FILES message.msg)
③去掉generate_messages()函数的注释
④去掉catkin_package的注释更改参数为CATKIN_DEPENDS message_runtime,导入message runtime依赖
第五步:生成源代码文件
$ rosmake beginner_tutorials

一切完成后会在catkin_ws/include/beginner_tutorials/目录下生成名为message.h的C++头文件
创建发布者(Publisher)
在功能包src目录下创建一个talker.cpp文件,代码如下
#include "ros/ros.h"//该文件引用了ROS操作系统的一些常用头文件 #include "beginner_tutorials/message.h"//调用由我们定义的消息文件编译后自动生成的头文件int main(int argc,char **argv){ros::init(argc, argv, "talker"); //ROS出使化并制定节点名称,该名称可以在运行时进行重映射 ros::NodeHandle n; //创建一个指向该线程节点的句柄,第一个创建的节点句柄会做一些节点的初始化操作 ros::Publisher chatter_pub = n.advertise("chatter", 1000); //告知Master节点,我们要向chatter话题发布一个beginner_tutorials/message类型的消息 //将publisher队列的大小定义为1000个消息,超过大小之后将会丢弃旧的消息 ros::Rate loop_rate(10); //定义循环频率为10Hz int count = 0; while(ros::ok()){缺省情况下,roscpp会安装一个SIGINT的handler,这个handler会处理Ctrl+C等异常事件,使ros::ok()返回false beginner_tutorials::message msg; msg.data = "https://www.it610.com/article/Hello World"; msg.count = count++; chatter_pub.publish(msg); //将信息广播给订阅该话题的节点 ROS_INFO("The count is: %ld.\t%s", (long int)msg.count, msg.data.c_str()); //ROS中用来代替printf/cout等函数的输出功能 //ros::spinOnce(); //在不接收任何callback的情况下,可以省略 loop_rate.sleep(); //休眠一定时间保证10Hz的发布频率 } return 0; }

创建订阅者(Subscriber)
在功能包src目录下创建一个listener.cpp文件,代码如下
#include "ros/ros.h" #include "beginner_tutorials/message.h"//定义回调函数 void chatterCallback(const beginner_tutorials::message::ConstPtr& msg){ ROS_INFO("I heard:[%s]\tThe count is %ld.", msg->data.c_str(), (long int)msg->count); }int main(int argc, char **argv){ ros::init(argc, argv, "listener"); ros::NodeHandle n; ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback); //创建一个Subscriber,订阅一个名为chatter的话题,注册回调函数chatterallback ros::spin(); //进入自循环,等待进入回调函数 return 0; }

编译节点
在CMakeLists.txt文件末尾内添加如下语句
include_directories(include ${catkin_INCLUDE_DIRS})
#设置头文件的相对路径
add_executable(talker src/talker.cpp)
#将talker.cpp文件生成为一个名为talker的可执行文件
target_link_libraries(talker ${catkin_LIBRARIES})
#将可执行文件同其所依赖的第三方库进行链接
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
进入工作空间目录,使用catkin_make命令编译成功过后就可以使用rosrun指令来运行节点观看结果
话题相关命令
$ rqt_graph#图形化显示ROS系统当前的运行情况 $ rqt_plot#实时显示一个发布到某个话题上的数据变化图形 $ rostopic list#列出当前运行的所有主题 $ rostopic hz [topic]#查看某一话题发布消息的频率 $ rostopic echo [topic]#查看某一话题发布的消息内容 $ rostopic type [topic]#查看某一话题发布的消息类型 $ rostopic pub [topic] [msg_type] [args]#将数据发布到某个正在广播的话题上 $ rosmsg show [package_name] [msg_type] #查看某一类型消息的详细内容,package_name参数可以省略

通信方式——服务(Services) 服务(services)是基于 C/S 模式的双向数据传输模式(有应答的通信机制),话题是无应答的通信机制。该通讯方式无较大的资源占用,适合于不频繁通信的场合。例如某个节点A向节点B提交服务申请(request),节点B执行回调函数,并将结果返回(response)给节点A。
一个服务器可以有多个客户端,但是一个客户端只能有一个服务器;
服务通信机制详解 服务器和客户端建立连接之前的准备
服务器和客户端之间的连接与上述发布者和订阅者之间的TCPROS连接相同,但是与话题不同的是,服务只连接一次,再执行请求和响应之后彼此断开连接。如果有必要,需要重新进行连接。
服务器和客户端通信的过程
  1. 服务器Node B提供了一个服务的接口/Service
  2. Node A通过/Service发送数据给Node B即“请求”,同时等待服务器Node B返回应答数据。
  3. 服务器返回了应答数据后,客户端Node A与服务器Node B断开连接继续执行。
    ROS学习总结——节点之间的通信方式及其实现(C++)
    文章图片
使用C++构建一个服务器和客户端 创建srv文件
srv 文件与 msgmsg 文件类似,只是 srv 文件描述了serivces的数据;该数据两个部分:request和 response。两个部分用‘---’分割。
①首先beginner_tutorials包目录下建立一个叫做srv的子目录,并创建一个AddTwoInts.srv的文件。文件的内容如下
ROS学习总结——节点之间的通信方式及其实现(C++)
文章图片

上图中a和b使request,sum是response;
②为了使其能够生成C++,python源码,我们需要对package.xml以及CMakeLists.txt文件进行如下修改
对于package.xml文件同样需要添加下面两行代码,关于这两行代码的解释在上面创建msg文件的时候已经说明
message_generation message_runtime

找到CMakeLists.xml文件中的add_service_files()函数,修改其参数为FILES AddTwoInts.msg
③最后使用catkin_make指令对功能包进行编译,生成srv文件的源代码文件。
创建服务端(Service)
在功能包的src目录下创建文件add_two_ints_server.cpp并添加如下代码
#include "ros/ros.h" #include "beginner_tutorials/AddTwoInts.h" //导入由srv文件生成的头文件 /* *函数提供了两个ints相加的serivers。他接收request中的数据并将结果放入response中。 * */ bool add(beginner_tutorials::AddTwoInts::Request &req, \ beginner_tutorials::AddTwoInts::Response &res){ res.sum = req.a + req.b; ROS_INFO("request: x = %ld,y = %ld", (long int)req.a, (long int)req.b); ROS_INFO("sending back response: [%ld]", (long int)res.sum); return true; } int main(int argc, char **argv){ros::init(argc, argv, "add_two_ints_server"); //初始化ROS ros::NodeHandle n; //创建节点句柄 ros::ServiceServer service = n.advertiseService("add_two_ints",add); //创建service并广播给ROS。告知有"add_tow_ints"这么一个service ROS_INFO("Ready to add two ints."); ros::spin(); //进入自循环,等待进入回调函数return(0); }

创建客户端(Client)
在功能包的src目录下创建文件add_two_ints_client.cpp并添加如下代码
#include "ros/ros.h" #include "beginner_tutorials/AddTwoInts.h" #include //调用该头文件用来使用atoll()进行字符串转换 int main(int argc, char **argv){ros::init(argc, argv, "add_two_ints_client"); //初始化ROS if(argc != 3){ //判断输入参数数目是否正确 ROS_INFO("usage:add_two_ints_cliect X Y"); return 1; } ros::NodeHandle n; //创建节点句柄 ros::ServiceClient client = n.serviceClient("add_two_ints"); //创建一个应用add_two_ints服务的client。该对象用于稍后调用服务 beginner_tutorials::AddTwoInts srv; srv.request.a = atoll(argv[1]); srv.request.b = atoll(argv[2]); if(client.call(srv)){//发送service请求,等待返回运算结果 ROS_INFO("Sum: %ld", (long int)srv.response.sum); //将输入的参数取出 }else{ ROS_ERROR("Failed to call service add_two_ints"); return 1; } return 0; }

编译节点
在CMakeLists.txt文件末尾内添加如下语句
add_executable(add_two_ints_server src/add_two_ints_server.cpp)
target_link_libraries(add_two_ints_server ${catkin_LIBRARIES})
add_executable(add_two_ints_client src/add_two_ints_client.cpp)
target_link_libraries(add_two_ints_client ${catkin_LIBRARIES})
服务的相关命令
$ rosservice list#查找可用服务的信息 $ rosservice type [service]#产看指定服务类型 $ rosservice call [service] [args] #调用某一服务 $ rossrv show [service_name]#查看srv文件的内容

关于CMakeLists.txt以及package文件的详细内容(去注释) CMakeLists.txt
cmake_minimum_required(VERSION 2.8.3) project(beginner_tutorials) find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs genmsg message_generation) add_message_files(FILES message.msg) add_service_files(FILES AddTwoInts.srv) generate_messages() catkin_package(CATKIN_DEPENDS message_runtime) include_directories(include ${catkin_INCLUDE_DIRS}) add_executable(talker src/talker.cpp) target_link_libraries(talker ${catkin_LIBRARIES}) add_executable(listener src/listener.cpp) target_link_libraries(listener ${catkin_LIBRARIES}) add_executable(add_two_ints_server src/add_two_ints_server.cpp) target_link_libraries(add_two_ints_server ${catkin_LIBRARIES}) add_executable(add_two_ints_client src/add_two_ints_client.cpp) target_link_libraries(add_two_ints_client ${catkin_LIBRARIES})

package.xml
beginner_tutorials 0.0.0 The beginner_tutorials packagelongTODOcatkin roscpp rospy std_msgs message_generation roscpp rospy std_msgs roscpp rospy std_msgs message_runtime

【ROS学习总结——节点之间的通信方式及其实现(C++)】文章内容包含个人理解,如有错误欢迎指出,我会及时纠正。

    推荐阅读