ROS节点之间基本的通讯方式介绍


这一篇我们来聊一聊ROS的通讯方式。

1 Understanding the ROS computation graph level

这篇我们来理解一下ROS中的Nodes、Master、Parameter server、Messages、Topics、Services,以及Bags。不理解这些ros的基本概念,后面没有办法继续给ROS盖楼。我理解的这个是ROS的通讯方式,但是为什么在书中叫做computation graph(计算图)?Anyway,与ROS通信相关的软件包包括核心客户端库(例如roscpp和rospython)以及概念的实现(例如topics,nodes,parameters和services),都包含在称为ros_comm的堆栈中。该堆栈还包含rostopic,rosparam,rosservice和rosnode之类的工具。ros_comm堆栈包含ROS通信中间件程序包,这些程序包统称为ROS Graph层:
01

接下来对上图中的这些概念予以说明。

  • Nodes:节点是执行计算的过程。每个ROS节点都是使用ROS客户端库编写的。使用客户端库API,我们可以实现不同的ROS功能,例如节点之间的通信方法,当我们的机器人的不同节点必须在它们之间交换信息时,该功能特别有用。使用ROS通信方法,它们可以相互通信并交换数据。 ROS节点的目的之一是构建简单的过程,而不是具有所有功能的大型过程。 ROS节点结构简单,易于调试。
  • Master:ROS主机为其余节点提供名称注册和查找。没有ROS Master,节点将无法找到彼此,交换消息或调用服务。在分布式系统中,我们应该在一台计算机上运行主机,其他远程节点可以通过与该主机进行通信来找到彼此。
  • Parameter server:参数服务器使您可以将数据存储在中央位置。所有节点都可以访问和修改这些值。参数服务器是ROS Master的一部分。
  • Messages:节点之间通过消息进行通信。消息只是包含类型字段的数据结构,该字段可以保存一组数据,并且可以发送到另一个节点。有标准的基本类型(整数,浮点型,布尔型等),ROS消息支持这些类型。我们还可以使用这些标准类型构建自己的消息类型。
  • Topics:主题。ROS中的每个消息都是使用称为主题的命名总线传输的。当节点通过主题发送消息时,可以说该节点正在发布主题。当节点通过主题接收消息时,可以说该节点正在订阅主题。发布节点和订阅节点不知道彼此的存在。我们甚至可以订阅可能没有任何发布者的主题。简而言之,信息的产生和信息的消耗是分离的。每个主题都有一个唯一的名称,只要节点具有正确的消息类型,任何节点都可以访问该主题并通过该主题发送数据。
  • Services:在某些机器人应用程序中,发布/订阅通信模型可能不合适。 例如,在某些情况下,我们需要一种请求/响应交互,其中一个节点可以要求对另一个节点执行快速过程(例如,要求快速计算)。 ROS service交互就像一个远程过程调用。
  • Logging:ROS提供了一个日志记录系统,用于存储数据(例如传感器数据),这些数据可能很难收集,但是对于开发和测试机器人算法(bagfile)是必需的。 当我们使用复杂的机器人机制时,bagfile是非常有用的功能。

OK,上述是直接从《Mastering ROS for Robotics Programming-Second Edition》翻译过来的。咱们还是说点人话吧1

  • Nodes几乎是无处不在,这个东西相当于可执行文件,目前我更愿意把它当做cpp文件,通过catkin_make之后生成可执行文件。
  • Messages就像是血液一样,将Nodes连接起来,没有Messages,就成了哑巴,是无法建立起传递机制的。
  • Topics就像是QQ群一样,提供一个平台。当有人发了一条消息后,QQ群告诉任何一个加群的人,有没有红包啥的。显然这是一种通过第三方传递的方式,也就是间接传递。发红包的人就是publisher,看群消息的人就是subscriber,然后是MESSAGE传递,这就很简单了,就是水群。Topics适用于流式传输连续的数据流,例如传感器数据。 例如,播放游戏手柄数据以遥控机器人,发布机器人测距法,发布来自摄像机的视频流。
  • Services有点像两人私聊的意思,或者是很多人找一个人私聊。跳过了Topics平台,它与Topics区别就在于是消息直接传递还是间接传递。水群就是间接传递,多对多传递。私聊就是直接传递,一对一。Services适用于使得执行程序快速终止。 例如,保存传感器的校准参数,保存机器人在导航过程中生成的地图或加载参数文件。

2 ROS computation graph 实战

了解了通讯的基本构建后,不来点实际的怎么能过瘾呢?

2.1 Topics实战

2.1.1 创建ros package

Topics是两个nodes间最基本的通讯方式。本节我们来聊聊Topics的工作方式。首先,自行创建一个示例包demo_pkg

1
2
3
4
cd ~/catkin_ws/src
catkin_create_pkg demo_pkg roscpp std_msgs actionlib actionlib_msgs
cd ..
catkin_make

新包创建完成后的目录如下图所示:

02

因为在创建demo_pkg时,添加了依赖项roscpp,因此会有一个src文件,用来存放cpp源文件。我们创建的这个ros package,它的功能是:一个Publisher node通过名为/numbers的topic来传递一个整型值,Subscriber node将这个值打印在屏幕上。

2.1.2 创建源文件

2.1.2.1 Publisher node

src文件夹下新建一个名为demo_topic_publisher.cpp的文件(该文件即为Publisher node),其内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//roscpp Core header
#include "ros/ros.h"
//Header of Int32 standard message
#include "std_msgs/Int32.h"
#include <iostream>

int main(int argc, char **argv)
{
//Initializing ROS node with a name of demo_topic_publisher
ros::init(argc, argv,"demo_topic_publisher");

//Created a node handle object
ros::NodeHandle node_obj;

//Created a publisher objec and named the topic /numbers with a message type std_msgs::Int32. The second argument is the buffer size.

ros::Publisher number_publisher = node_obj.advertise<std_msgs::Int32>("/numbers",10);

//Created a rate object
ros::Rate loop_rate(10);

//Variable of the number initializing as zero
int number_count = 0;

//While loop for incrementing number and publishing to topic /numbers
while (ros::ok())
{
//Created a Int32 message
std_msgs::Int32 msg;

//Inserted data to message header
msg.data = number_count;

//Printing message data
ROS_INFO("%d",msg.data);

//Publishing the msg to topic /numbers
number_publisher.publish(msg);

//Spinning once for doing the all operation once
ros::spinOnce();

//Sleeping for sometime
loop_rate.sleep();

//Incrementing the count
++number_count;
}

return 0;
}

2.1.2.2 Subscriber node

src文件夹下新建一个名为demo_topic_subscriber.cpp的文件(该文件即为Subscriber node),其内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "ros/ros.h"
#include "std_msgs/Int32.h"
#include <iostream>

//Callback of the topic /numbers
void number_callback(const std_msgs::Int32::ConstPtr& msg)
{
ROS_INFO("Recieved [%d]",msg->data);
}

int main(int argc, char **argv)
{
//Initializing ROS node with a name of demo_topic_subscriber
ros::init(argc, argv,"demo_topic_subscriber");
//Created a nodehandle object
ros::NodeHandle node_obj;
//Create a publisher object
ros::Subscriber number_subscriber = node_obj.subscribe("/numbers",10,number_callback);
//Spinning the node
ros::spin();
return 0;
}

Subscriber node通过number_callback将数字打印出来。

2.1.3 编译

首先,将ROS package中的CMakeLists.txt文件内容修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cmake_minimum_required(VERSION 2.8.3)
project(demo_pkg)

find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
actionlib
actionlib_msgs
message_generation
)

## System dependencies are found with CMake's conventions
find_package(Boost REQUIRED COMPONENTS system)

catkin_package(
CATKIN_DEPENDS roscpp std_msgs actionlib actionlib_msgs
)

# This will create executables of the nodes
include_directories( include ${catkin_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} )

# This will generate message header file before building the target
add_executable(test_topic_publisher src/demo_topic_publisher.cpp)
add_executable(test_topic_subscriber src/demo_topic_subscriber.cpp)

# This will link executables to the appropriate libraries
add_dependencies(test_topic_publisher demo_pkg_generate_messages_cpp)
add_dependencies(test_topic_subscriber demo_pkg_generate_messages_cpp)

target_link_libraries(test_topic_publisher ${catkin_LIBRARIES})
target_link_libraries(test_topic_subscriber ${catkin_LIBRARIES})

编写好CMakeLists.txt文件后,回到Terminal,执行:

1
2
cd ~/catkin_ws
catkin_make

2.1.4 运行程序

  1. 打开一个Terminal,执行roscore
  2. 再开一个Terminal,执行:
    1
    2
    3
    cd ~/catkin_ws/
    source devel/setup.bash
    rosrun demo_pkg test_topic_publisher
  3. 打开第三个Terminal,执行:
    1
    2
    3
    cd ~/catkin_ws/
    source devel/setup.bash
    rosrun demo_pkg test_topic_subscriber
  4. 程序运行结果如下:
    03

2.1.4 查看节点信息

  1. rosnode list:列出活动节点
    04
  2. rosnode info demo_topic_publisher:获取publisher node的信息
    05
  3. rostopic echo /numbers:获取通过topic /numbers传递的数值
    06
  4. rostopic type /numbers:获取通过topic /numbers传递的数据类型
    07

2.2 添加message

本节来聊聊如何向现有的ROS package中添加messages(.msg文件)。这种格式的文件可以告知ROS从node传来的数据类型。当一个message被添加时,ROS会将其定义转为等效的C++代码,这样我们可以将它包含在nodes中。

2.2.1 创建源文件

2.2.1.1 定义message

假定我们要通过message传递一个字符串,一个整型数据。首先,在现有的ROS包中创建一个文件夹msg,在其中新建一个文件demo_msg.msg,并在其中写入以下内容:

1
2
string greeting
int32 number

这样,我们就定义好了message。接下来,要把这个message包含在nodes中。需要做的是将它在ROS package中申明。怎么做呢?打开package.xml文件,添加依赖项:

1
2
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

2.2.1.2 创建message publisher

demo_pkg中打开src文件夹,新建一个demo_msg_publisher.cpp文件用来发布我们自定义的message。其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "ros/ros.h"
#include "std_msgs/Int32.h"
#include "demo_pkg/demo_msg.h"
#include <iostream>
#include <sstream>

//Defining namespace using in this code
using namespace std;
//using namespace ros;
//using namespace std_msgs;
//using namespace demo_pkg;

int main(int argc, char **argv)
{
//Initializing ROS node with a name of demo_topic_publisher
ros::init(argc, argv,"demo_msg_publisher");
//Created a nodehandle object
ros::NodeHandle node_obj;
//Create a publisher object
ros::Publisher number_publisher = node_obj.advertise<demo_pkg::demo_msg>("/demo_msg_topic",10);
//Create a rate object
ros::Rate loop_rate(10);
//Variable of the number initializing as zero
int number_count = 0;

//While loop for incrementing number and publishing to topic /numbers
while (ros::ok())
{
//Created a Int32 message
demo_pkg::demo_msg msg;
//Inserted data to message header

std::stringstream ss;
ss << "hello world ";
msg.greeting = ss.str();

msg.number = number_count;
//Printing message data
ROS_INFO("%d",msg.number);
ROS_INFO("%s",msg.greeting.c_str());

//Publishing the message
number_publisher.publish(msg);
//Spining once for doing the all operation once
ros::spinOnce();
//Setting the loop rate
loop_rate.sleep();
//Increamenting the count
++number_count;

}

return 0;
}

2.2.1.3 创建message subscriber

demo_pkg中打开src文件夹,新建一个demo_msg_subscriber.cpp文件用来接收我们自定义的message。其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "ros/ros.h"
#include "demo_pkg/demo_msg.h"
#include <iostream>
#include <sstream>

void number_callback(const demo_pkg::demo_msg::ConstPtr& msg)
{
ROS_INFO("Recieved greeting [%s]",msg->greeting.c_str());
ROS_INFO("Recieved [%d]",msg->number);
}

int main(int argc, char **argv)
{
//Initializing ROS node with a name of demo_topic_subscriber
ros::init(argc, argv,"demo_msg_subscriber");
//Created a nodehandle object
ros::NodeHandle node_obj;
//Create a publisher object
ros::Subscriber number_subscriber = node_obj.subscribe("/demo_msg_topic",10,number_callback);
//Spinning the node
ros::spin();
return 0;
}

这时候的文件目录如下所示:

08

2.2.2 编译

  1. 打开CMakeLists.txt,添加自定义的message,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    add_message_files(
    FILES
    demo_msg.msg
    )
    # Generate added messages and services with any dependencies listed here
    generate_messages(
    DEPENDENCIES
    std_msgs
    actionlib_msgs
    )
  2. 打开CMakeLists.txt,添加依赖项及生成项:

    1
    2
    3
    4
    5
    6
    add_executable(test_msg_publisher src/demo_msg_publisher.cpp)
    add_executable(test_msg_subscriber src/demo_msg_subscriber.cpp)
    add_dependencies(test_msg_publisher demo_pkg_generate_messages_cpp)
    add_dependencies(test_msg_subscriber demo_pkg_generate_messages_cpp)
    target_link_libraries(test_msg_publisher ${catkin_LIBRARIES})
    target_link_libraries(test_msg_subscriber ${catkin_LIBRARIES})
  3. 编译

    1
    2
    cd ~/catkin_ws/
    catkin_make

2.2.3 运行

  1. 打开一个Terminal,执行

    1
    roscore
  2. 再开一个Terminal,执行:

    1
    2
    3
    cd ~/catkin_ws/
    source devel/setup.bash
    rosrun demo_pkg test_msg_publisher
  3. 打开第三个Terminal,执行:

    1
    2
    3
    cd ~/catkin_ws/
    source devel/setup.bash
    rosrun demo_pkg test_msg_subscriber
  4. 程序运行结果如下:
    09

2.3 Service实战

2.3.1 创建源文件

2.3.1.1 定义service

类似于第2.2.1.1节定义message,这里也在现有的ROS package中创建一个文件夹srv,在其中新建一个文件demo_srv.srv,并在其中写入以下内容:

1
2
3
string in
---
string out

这样定义了通过service传递的是两个字符串。同时,也需要将它包含在ROS package中。做法同message一样。打开package.xml文件,添加依赖项:

1
2
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

2.3.1.2 创建service server

demo_pkg中打开src文件夹,新建一个demo_service_server.cpp文件用来发布我们自定义的service。其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "ros/ros.h"
#include "demo_pkg/demo_srv.h"
#include <iostream>
#include <sstream>


//Defining namespace using in this code
using namespace std;
//using namespace ros;
//using namespace std_msgs;
//using namespace demo_pkg;


bool demo_service_callback(demo_pkg::demo_srv::Request &req,
demo_pkg::demo_srv::Response &res)
{
// ROS_INFO("From Client [%s], Server says [%s]",req.in.c_str(),ss.c_str());

std::stringstream ss;
ss << "Received Here";
res.out = ss.str();

ROS_INFO("From Client [%s], Server says [%s]",req.in.c_str(),res.out.c_str());

return true;
}

int main(int argc, char **argv)
{
ros::init(argc, argv, "demo_service_server");
ros::NodeHandle n;

ros::ServiceServer service = n.advertiseService("demo_service", demo_service_callback);
ROS_INFO("Ready to receive from client.");
ros::spin();

return 0;
}

2.3.1.3 创建service client

demo_pkg中打开src文件夹,新建一个demo_service_client.cpp文件用来接收我们自定义的service。其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "ros/ros.h"
#include "std_msgs/Int32.h"
#include <iostream>

#include "demo_pkg/demo_srv.h"
#include <iostream>
#include <sstream>

//Defining namespace using in this code
using namespace std;

int main(int argc, char **argv)
{
ros::init(argc, argv, "demo_service_client");
ros::NodeHandle n;
ros::Rate loop_rate(10);

ros::ServiceClient client = n.serviceClient<demo_pkg::demo_srv>("demo_service");

while (ros::ok())
{
demo_pkg::demo_srv srv;
std::stringstream ss;
ss << "Sending from Here";
srv.request.in = ss.str();

if (client.call(srv))
{
ROS_INFO("From Client [%s], Server says [%s]",srv.request.in.c_str(),srv.response.out.c_str());
}
else
{
ROS_ERROR("Failed to call service");
return 1;
}

ros::spinOnce();
//Setting the loop rate
loop_rate.sleep();

}
return 0;
}

此时的ROS package目录如下图所示:

10

2.3.2 编译

  1. 打开CMakeLists.txt,添加自定义的message,如下:
    1
    2
    3
    4
    5
    6
    7
    ## Generate services in the 'srv' folder
    add_service_files(
    FILES
    demo_srv.srv
    )

    catkin_package(CATKIN_DEPENDS roscpp std_msgs actionlib actionlib_msgs message_runtime)
  2. 打开CMakeLists.txt,添加依赖项及生成项:

    1
    2
    3
    4
    5
    6
    add_executable(test_service_server src/demo_service_server.cpp)
    add_executable(test_service_client src/demo_service_client.cpp)
    add_dependencies(test_service_server demo_pkg_generate_messages_cpp)
    add_dependencies(test_service_client demo_pkg_generate_messages_cpp)
    target_link_libraries(test_service_server ${catkin_LIBRARIES})
    target_link_libraries(test_service_client ${catkin_LIBRARIES})
  3. 编译

    1
    2
    cd ~/catkin_ws/
    catkin_make

2.3.3 运行

  1. 打开一个Terminal,执行:

    1
    roscore
  2. 再开一个Terminal,执行:

    1
    2
    3
    cd ~/catkin_ws/
    source devel/setup.bash
    rosrun demo_pkg test_service_server
  3. 打开第三个Terminal,执行:

    1
    2
    3
    cd ~/catkin_ws/
    source devel/setup.bash
    rosrun demo_pkg test_service_client
  4. 程序运行结果如下:
    11

  5. 通过rosservice list可以查看当前ROS services。

参考链接

  1. ROS节点,消息,话题,服务的介绍
------ 本文结束感谢您的阅读------
Donate a cup of cola?