前言

我要给大家来介绍一下ROS当中一些核心的概念,帮助大家去在后面的ROS学习当中更快地吸收这些概念,今天讲解的是ROS中的通信机制——话题通信。

ROS系列文章

ROS的通信机制
从前面的文章我们知道,ROS的通信机制是一个 松散耦合的分布式软件框架 设计而来的,而节点间的通讯就会必然是有几种通讯方式,那ROS主要给节点之间提供三种核心的通讯方式,一种叫做话题(topic),一种叫做服务(server),以及基于RPC的参数服务器。

我们接下来讲解ROS的核心概念——ROS话题通信机制。

话题(topic)
话题就是ROS中一个数据传输的有名字的通道。当一个节点想要分享信息时,它就会发布(publish)消息到对应的一个或者多个话题;当一个节点想要接收信息时,它就会订阅(subscribe)它所需要的一个或者多个话题。 ROS节点管理器负责确保发布节点和订阅节点能找到对方;而且消息是直接地从发布节点传递到订阅节点,中间并不经过节点管理器转交。

topic这个单词对我来说是非常熟悉的,因为我在物联网领域老是捣鼓网络协议栈,在MQTT协议里面经常看到topic,巧合的是ROS中的topic跟MQTT协议中的topic是非常像的,基本可以说是同样的模型,但MQTT协议里面需要服务器转发数据。

比如我们看下面这张图:


ros012

话题通讯的模型比较简单,这张图的模型里面会分为发布者(publisher)和订阅者(subscriber),话题的这种通讯的方式是单向数据传输。比如说有一个节点它要发布一个数据,这个摄像头驱动节点它来驱动摄像头,并且获取摄像头的数据,然后传出来给处理数据的节点去处理,所以它会不断的往外发送一些图像数据,那它发到哪里呢?这个节点作为发布者,需要向某个话题(topic,也可以称为主题)发布这些数据,那么当系统中可能有多个节点要去处理图像,比如有一个节点是需要显示图像数据的,有一个节点是做图像处理的,它们实现要获取到这些数据后,才能处理对吧,而这些节点就是订阅者,它们从话题(topic)中接收这些数据,然后去处理它。

发布者
发布者:发布(publish)是指以将数据发布到某个话题中,为了可以正常执行发布操作。发布者(publisher)必须在节点管理器上注册自己的话题等多种信息。

发布者的工作过程如下:

1.向节点管理器注册节点。

2.告诉管理器要向哪个话题发布数据。

3.节点运行时采集数据,然后发布到这个话题。

订阅者
订阅是指订阅者节点想要接收来自某个话题的数据。为了可以正常执行订阅的操作,订阅者节点必须在节点管理器上注册自己想要订阅的话题等多种信息,并从节点管理器接收来自发布者的数据信息,同一个话题可以有多个发布者也可以有多个订阅者,但一般情况下发布者只有一个,而订阅者是一个或者多个。

订阅者的工作过程如下:

1.向节点管理器注册节点。

2.订阅话题,告诉管理器要接收哪个话题的数据。

3.节点运行时就接收数据,然后去处理数据(一般来说会注册回调函数,当有数据的时候就在回调函数中处理)。

总的来说,话题就像是一个数据传输的管道。打个比方,你可以在运输牛奶的管道接到牛奶,在运输煤气的管道上接收到煤气,在水管上接到水。。。

ros013

就拿上面的图来说,Camera Node节点是发布者,它负责采集图像数据并且发布;Image Processing Node节点是订阅者,在接收到图像数据后要去处理数据,Image Display Node节点也是订阅者,在接收到图像数据后要去显示图像的数据。

话题通讯它是一种异步通讯,因为我们没有办法知道发布者跟订阅者之间单向传输的这个时效性啊,我可能发不出去之后,订阅者可能等很长时间啊或者阻塞才会接收,到那很多情况下,我们更希望我的数据发出去之后,对方能给我一个。

消息(Message)
大家有没有考虑过,话题之间的数据叫什么呢?没错,就是叫消息。

节点之间通过消息(message)来发送和接收数据。消息是用来描述我们传输话题数据的,消息可以是各种类型的数据,比如整形、浮点型、字符等不定程度的数据,我们还可以在消息中使用一些数据结构来描述这些消息。

用C++来写话题通信的代码
因为本系列文章还没介绍到工作空间,那么就先简单看看怎么去写代码吧:

头文件

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>

发布者

  • 创建节点句柄,并且告诉节点管理器准备向test_topic话题发布消息。
  • 	// ROS节点初始化
    	ros::init(argc, argv, "publisher_topic");
    
    	// 创建节点句柄
    	ros::NodeHandle n;
    
    	// 创建一个Publisher,发布名为test_topic,消息类型为gstd_msgs::String,队列长度100
    	ros::Publisher pub_topic = n.advertise<std_msgs::String>("test_topic", 100);
    
  • 填充消息的内容并且向话题发布消息:
  •     // 初始化std_msgs::String类型的消息
        std_msgs::String str_msg;
    
        str_msg.data = "this is a test_topic message!";
    
        // 发布消息
        pub_topic.publish(str_msg);
    
  • 订阅者创建节点句柄,并且告诉节点管理器订阅一个话题test_topic
  •     // 接收到订阅的消息后,会进入消息回调函数
        void test_topic_cb(const std_msgs::String::ConstPtr& msg)
        {
            // 将接收到的消息打印出来
            ROS_INFO("I heard: [%s]", msg->data.c_str());
        }
    
        // 初始化ROS节点
        ros::init(argc, argv, "subscriber_topic");
    
        // 创建节点句柄
        ros::NodeHandle n;
    
        // 创建一个Subscriber,订阅名为test_topic的消息,注册回调函数test_topic_cb
        ros::Subscriber sub_topic = n.subscribe("test_topic", 100, test_topic_cb);
    
    
  • 循环等待来自话题test_topic的消息,当收到消息后会进入回调函数test_topic_cb()中处理:
  •     // 循环等待回调函数
        ros::spin();
    
  • 此处需要在工作空间进行实验,分别打开新的终端运行节点管理器、发布者节点、订阅者节点,实验效果如下:

  •  

    使用rqt_graph
    publisher_topic节点和subscriber_topic节点之间是通过一个test_topic话题来互相通信的,我们可以使用rqt_graph来显示当前运行的节点和话题。

    rqt_graph是rqt程序包中的一部分,如果你没有安装,请通过以下命令来安装:

    sudo apt-get install ros-<distro>-rqt
    sudo apt-get install ros-<distro>-rqt-common-plugins
    
  •  <distro>替换成你所安装的版本(比如 kinetic、melodic 等)。

    在一个新终端中运行:                                                                                                                                                                                                                                             

    rosrun rqt_graph rqt_graph
    

     你会看到以下界面:

    ros015                                                          

    源码附录

    • subscriber_topic.cpp:                                                                                                                                                                                                                                  
      #include <ros/ros.h>
      #include <std_msgs/String.h>
      
      // 接收到订阅的消息后,会进入消息回调函数
      void test_topic_cb(const std_msgs::String::ConstPtr& msg)
      {
          // 将接收到的消息打印出来
          ROS_INFO("I heard: [%s]", msg->data.c_str());
      }
      
      int main(int argc, char **argv)
      {
          // 初始化ROS节点
          ros::init(argc, argv, "subscriber_topic");
      
          // 创建节点句柄
          ros::NodeHandle n;
      
          // 创建一个Subscriber,订阅名为test_topic的消息,注册回调函数test_topic_cb
          ros::Subscriber sub_topic = n.subscribe("test_topic", 100, test_topic_cb);
      
          // 循环等待回调函数
          ros::spin();
      
          return 0;
      }
      

       

      • publisher_topic.cpp:
      • #include <ros/ros.h>
        #include "std_msgs/String.h"
        
        int main(int argc, char **argv)
        {
        	// ROS节点初始化
        	ros::init(argc, argv, "publisher_topic");
        
        	// 创建节点句柄
        	ros::NodeHandle n;
        
        	// 创建一个Publisher,发布名为test_topic,消息类型为gstd_msgs::String,队列长度100
        	ros::Publisher pub_topic = n.advertise<std_msgs::String>("test_topic", 100);
        	
        	// 设置循环的频率
        	ros::Rate loop_rate(10);
        
        	while (ros::ok())
        	{
        	    // 初始化std_msgs::String类型的消息
        		std_msgs::String str_msg;
        
        		str_msg.data = "this is a test_topic message!";
        
        	    // 发布消息
        		pub_topic.publish(str_msg);
        
        	    // 按照循环频率延时
        	    loop_rate.sleep();
        	}
        
        	return 0;
        }
        
      • 当然你也可以用Python语言来实现话题通信,可以参考我后文中给出的链接~

        引用说明

        本文的部分截图来自《古月居·ROS入门21讲》的课件。

        参考

        ROS官方wiki

        ROS笔记(7) 话题通信