ROS 的三种通信方式 2a82329219bf47c9a8f48a534ab31af7

发布时间 2023-07-12 12:18:47作者: 从未用过的回调函数

ROS 的三种通信方式

注意以下所有代码均基于:

ubuntu ROS Date
18.04 LTS Melodic Morenia May 23rd, 2018 May, 2023

写在最前,ROS1主要有三种通信方式,分别是:

  1. 话题通信
  2. 服务通信
  3. 参数通信

话题通信(Topic)

话题通信主要是指通过发布和订阅服务的方式,来进行匿名的通信,一方是发布者,一方是订阅者。双方彼此之间是单向流通的。该通信方式的英文描述如下:

Topics are named buses over which nodes exchange messages. Topics have anonymous publish/subscribe semantics, which decouples the production of information from its consumption. In general, nodes are not aware of who they are communicating with. Instead, nodes that are interested in data subscribe to the relevant topic; nodes that generate data publish to the relevant topic. There can be multiple publishers and subscribers to a topic.

Topics are intended for unidirectional, streaming communication. Nodes that need to perform remote procedure calls, i.e. receive a response to a request, should use services instead. There is also the Parameter Server for maintaining small amounts of state.

上述的描述很好的介绍了话题通信的适合使用的范围。

该通信所使用的模块主要由3个:

  • ROS Master (管理者)
  • Talker (发布者)
  • Listener (订阅者)

以上就是其模型。

具体实现

接下来本文会就上述所描写的话题通信部分,写一个demo用以加深了解。

其中默认是已经安装了ROS环境的,虚拟机也是可以的,如果有疑惑是否已经安装了环境,可以在Ubuntu中任意打开一个终端,在终端输入 roscore 如果显示以下信息则是安装完备:

如果不是那么就需要考虑安装环境,这一部分本文不提供教程。既然已经实现了ROS环境的安装,就开始实现话题通信。

  1. 建立一个工作空间[workspace]

    mkdir -p ~/catkin_ws/src 
    // 建立目录
    cd ~/catkin_ws/src
    
    catkin_init_workspace
    //初始化
    cd ~/catkin_ws/
    catkin_make
    
  2. 创建一个功能包

    catkin_create_pkg mini_nodes roscpp std_msgs
     //catkin_create_pkg 功能包名称 使用的库....
    
  3. 进入功能包中src目录中,编写一个发布器:

    /**
     * talker.cpp
     * 创建一个Publisher,发布chatter话题,发布字符串"Hello World",类型为string
     */
    
    #include <sstream>
    #include "ros/ros.h"
    #include "std_msgs/String.h"
    
    int main(int argc, char **argv)
    {
      // ROS节点初始化,节点名称为talker
      // 注:节点名称必须唯一 
      ros::init(argc, argv, "talker");
      
      // 创建节点句柄,方便管理节电资源的使用和管理
      ros::NodeHandle n;
      
      // 创建一个Publisher,发布名为chatter的topic,消息类型为std_msgs::String
      // 1000为消息发布队列大小,当发布消息实际速度较慢时,Publisher会将消息存储在一定空间的队列中,当消息数量超过队列大小时,ROS会自动删除队列中最早入队的消息
      ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
    
      // 设置循环的频率,单位为Hz
      // 当调用Rate::sleep()时,ROS节点会根据此处设置的频率休眠响应的时间,以保证维持一致的时间周期.
      ros::Rate loop_rate(10);
    
      int count = 0;
      // 发生异常,ros::ok()返回false,跳出循环
      // 异常包括 1.Ctrl+C/2.被另外同名节点踢掉线/3.节点调用了关闭函数ros::shutdown()/4.所有ros::NodeHandles句柄被销毁
      while (ros::ok())
      {
        // 初始化std_msgs::String类型的消息msg,msg只有一个成员data
        std_msgs::String msg;
        std::stringstream ss;
        ss << "hello world " << count;
        /*这一句一般出现在创建ROS话题的发布者(Publisher)节点程序中,是利用c++自带的头文件sstream,来实现利用输入输出流的方式往string里写东西,并且还可以拼接string和其他类型的变量。
        *该语句实现了string型的数据"hello world"和int型变量count的拼接,形成一个新的string。即如果count是1,那么hello world1会作为string被存放在ss当中。
        *使用ss.str()调用这个string。最后可以用ROS_INFO输出。  
        */
        msg.data = ss.str();
        
        // ROS_INFO类似于printf/cout,用来打印日志信息
        // c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同. 
        ROS_INFO("%s", msg.data.c_str());
        // 发布消息
        chatter_pub.publish(msg);
    
        // 用来处理节点订阅话题的所有回调函数(目前的发布节点没有订阅信息,此函数非必须,但为了保证不出错所以所有节点都默认加入该函数)
        ros::spinOnce();
      
        // 目前为止,Publisher一个周期的工作完成了,让他休息一段时间,100ms后开始下一周期的工作
        loop_rate.sleep();
        ++count;
      }
    
      return 0;
    }
    
  4. 创建一个订阅者的cpp文件

    /**
     * listener.cpp
     * 创建一个Subscriber,订阅chatter话题,消息类型String
     */
     
    #include "ros/ros.h"
    #include "std_msgs/String.h"
    
    // 接收到订阅的消息后,会进入消息回调函数
    void chatterCallback(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, "listener");
    
      // 创建节点句柄
      ros::NodeHandle n;
      
      // 订阅节点需要声明订阅信息,该信息会在ROS Master中注册,Master会关注系统中是否存在发布该话题的节点,若存在则会帮助两个节点建立连接,完成数据传输
      // 创建一个Subscriber,订阅名为chatter的topic,注册回调函数chatterCallback
      ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
    
      // 循环等待回调函数,此函数在ros::ok()返回false时退出
      ros::spin();
    
      return 0;
    }
    
  5. 修改CMakeLists.txt文档

    //在CMakeLists.txt中添加以下的代码:
    add_executable(the_minist_publisher src/talk.cpp)
    target_link_libraries(the_minist_publisher ${catkin_LIBRARIES})
    add_executable(the_minist_sub src/listener.cpp)
    target_link_libraries(the_minist_sub ${catkin_LIBRARIES})
    
  6. 编译整套代码

    catkin_make
    
  7. 启动代码

    source devel/setup.bash
    roscore
    //**************************************
    rosrun mini_nodes talker
    rosrun mini_nodes listener
    

  1. topic通信常使用的命令有

    rostopic list //显示所有的话题列表
    rostopic echo xxxx //打印某个话题的数据
    rostopic pub 话题名称 消息类型 消息内容 //发布话题
    

自定义消息发送

有时候我们除了使用系统规定的数据格式,我们同样也可以自定义数据格式,接下来的过程将会展示一个自定义的数据格式是如何被创建的。

ps:以下的过程使用了catkin_simple的方法来方便过程

  1. 创建一个新功能包

    cs_create_pkg example_ros_msg roscpp std_msgs
    
  2. 进入该包的目录,创建一个新的目录,命名为msg,然后新建一个名为ExampleMessage.msg的文本文件。文件内容仅包含3行:

    Header header
    int32 demo_int
    float64 demo_double
    
  3. 若想要告诉编译器我们需要生成全新的消息头,则编辑package.xml文件。插入下面这一行:

    <build_depend>message_generation</build_depend>
    <run_depend>message_runtime</run_depend>
    
  4. 这种情况下,返回工作空间的目录,执行:

    catkin_make
    
  5. 就可以在devel/include下面发现.h文件

服务通信(Services)

服务(services)是节点之间通讯的另一种方式。服务允许节点发送请求(request) 并获得一个响应(response)。

英文解释:

The publish / subscribe model is a very flexible communication paradigm, but its many-to-many one-way transport is not appropriate for RPC request / reply interactions, which are often required in a distributed system. Request / reply is done via a Service, which is defined by a pair of messages: one for the request and one for the reply. A providing ROS node offers a service under a string name, and a client calls the service by sending the request message and awaiting the reply. Client libraries usually present this interaction to the programmer as if it were a remote procedure call.

Services are defined using srv files, which are compiled into source code by a ROS client library.

A client can make a persistent connection to a service, which enables higher performance at the cost of less robustness to service provider changes.

主要流程是:

  • server通过rpc 向master注册
  • client 通过rpc 向 master注册
  • master进行匹配,通过rpc 向client发送server的tcp地址
  • client通过tcp地址与server进行连接,发送请求数据
  • server进行解析,返回响应结果给client

具体实现

  1. 创建一个全新的功能包

    cs_create_pkg example_ros_srv roscpp std_msgs
    
  2. 创建一个新目录,名字是srv,并在该目录中新建一个文件命名为ExampleServiceMsg.srv

    int64 a
    int64 b
    ---
    int64 sum
    
  3. 修改package.xml文件

    <build_depend>message_generation</build_depend>
    <exec_depend>message_runtime</exec_depend>
    
  4. 编译catkin_make就可以生成相应的消息头

  5. 编写服务端代码:

    /*
        需求: 
            编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
            服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
            客户端再解析
    
        服务器实现:
            1.包含头文件
            2.初始化 ROS 节点
            3.创建 ROS 句柄
            4.创建 服务 对象
            5.回调函数处理请求并产生响应
            6.由于请求有多个,需要调用 ros::spin()
    
    */
    #include "ros/ros.h"
    #include "demo03_server_client/AddInts.h"
    
    // bool 返回值由于标志是否处理成功
    bool doReq(demo03_server_client::AddInts::Request& req,
              demo03_server_client::AddInts::Response& resp){
        int num1 = req.num1;
        int num2 = req.num2;
    
        ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
    
        //逻辑处理
        if (num1 < 0 || num2 < 0)
        {
            ROS_ERROR("提交的数据异常:数据不可以为负数");
            return false;
        }
    
        //如果没有异常,那么相加并将结果赋值给 resp
        resp.sum = num1 + num2;
        return true;
    
    }
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
        // 2.初始化 ROS 节点
        ros::init(argc,argv,"AddInts_Server");
        // 3.创建 ROS 句柄
        ros::NodeHandle nh;
        // 4.创建 服务 对象
        ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
        ROS_INFO("服务已经启动....");
        //     5.回调函数处理请求并产生响应
        //     6.由于请求有多个,需要调用 ros::spin()
        ros::spin();
        return 0;
    }
    
  6. 编写客户端代码

    /*
        需求:
            编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
            服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
            客户端再解析
    
        服务器实现:
            1.包含头文件
            2.初始化 ROS 节点
            3.创建 ROS 句柄
            4.创建 客户端 对象
            5.请求服务,接收响应
    
    */// 1.包含头文件#include "ros/ros.h"
    #include "demo03_server_client/AddInts.h"intmain(int argc,char*argv[])
    {
        setlocale(LC_ALL,"");
    
    // 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3if (argc!= 3)
    // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径){
            ROS_ERROR("请提交两个整数");
    return 1;
        }
    
    // 2.初始化 ROS 节点ros::init(argc,argv,"AddInts_Client");
    // 3.创建 ROS 句柄ros::NodeHandle nh;
    // 4.创建 客户端 对象ros::ServiceClient client= nh.serviceClient<demo03_server_client::AddInts>("AddInts");
    //等待服务启动成功//方式1ros::service::waitForService("AddInts");
    //方式2// client.waitForExistence();// 5.组织请求数据demo03_server_client::AddInts ai;
        ai.request.num1= atoi(argv[1]);
        ai.request.num2= atoi(argv[2]);
    // 6.发送请求,返回 bool 值,标记是否成功bool flag= client.call(ai);
    // 7.处理响应if (flag)
        {
            ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
        }
    else{
            ROS_ERROR("请求处理失败....");
    return 1;
        }
    
    return 0;
    }
    
    
  7. 修改CMakeLists.txt

  8. 编译

参数通信

有待添加