精华内容
下载资源
问答
  • 今天小编就为大家分享一篇基于ROS 服务通信模式详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • ROS服务通信:节点间直接通信并获得应答.服务需要用户自己开发,可在功能包下创建srv文件夹并编写源代码,服务类型是功能包名称和.srv文件名称的组合,比如python中引入srv类型为,from gqcnn.srv import IMAGEprocess,...

    ROS服务通信:节点间直接通信并获得应答.服务需要用户自己开发,可在功能包下创建srv文件夹并编写源代码,服务类型是功能包名称和.srv文件名称的组合,比如python中引入srv类型为,from gqcnn.srv import IMAGEprocess,其中gqcnn为功能包,srv为gqcnn下的文件夹.

     

    1. 在srv文件夹下创建 IMAGEprocess.srv文件,在里面写上请求参数和应答参数,其中 --- 上面是request的参数,下方是response参数

    2. 编写IMAGEprocess.srv文件后,需要编译生成c++/python等语言的库,以便后面调用,此时需配置package.xml以及CmakeLists.txt文件,在package.xml文件中加上如下图两行代码

    在CmakeLists.txt文件中加上如下几个模块,若有模块,则直接加上没有的代码,若无,则加上模块并编写代码,由于我这边使用的是bool和float32数据类型,所以代码中使用的是std_msgs,若你使用了自定义数据类型或其它数据类型需进行更换.

    find_package(catkin REQUIRED COMPONENTS
      rospy
      message_generation
      std_msgs
    )
    
    add_service_files(
       FILES
       IMAGEprocess.srv
    )
    
    generate_messages(
       DEPENDENCIES
       std_msgs
    )
    
    catkin_package(
      CATKIN_DEPENDS message_runtime
    )

    3. 此时对工作空间进行编译,则在下图中路径则会生成相应的编程语言库,我使用的是python,所以是在python文件夹下

    同时也可执行rossrv show IMAGEprocess查看服务类型是否定义成功,如下图

    4. 服务类型定义成功,则可在程序中进行引用,由于我使用的是python,所以在Python命令行中测试,测试结果如下图,由于中间有些提示信息,故省略了中间部分截图,import时没有提示或报错则成功

     

    Tip: 编写完 .srv文件以及修改package.xml和CmakeLists.txt文件后需要重新编译工作空间,直接catkin_make可能不会马上起效,需要rebuild或者把原先工作空间下的devel和build文件夹删除再catkin_make.

    展开全文
  • ROS 服务通信模式

    千次阅读 2018-07-05 20:59:54
    ROS 服务通信模式 摘自《ROS机器人开发实践》 AddTwoInts.h文件是根据AddTwoInts.srv文件生成的 还会自动生成 AddTwoIntsRequest.h AddTwoIntsResponse.h AddTwoInts.h所在的目录是 \catkin_ws\devel ...

    ROS 服务通信模式

    摘自《ROS机器人开发实践》

    服务(services)是节点之间通讯的另一种方式。服务允许节点发送请求(request) 并获得一个响应(response)
    AddTwoInts.h文件是根据AddTwoInts.srv文件生成的
    还会自动生成
    AddTwoIntsRequest.h
    AddTwoIntsResponse.h
    AddTwoInts.h所在的目录是
    \catkin_ws\devel

    AddTwoInts.srv

    int64 a
    int64 b
    ---
    int64 sum

    server.cpp

    /**
     * AddTwoInts Server
     */
    
    #include "ros/ros.h"
    #include "learning_communication/AddTwoInts.h"
    
    // service回调函数,输入参数req,输出参数res
    bool add(learning_communication::AddTwoInts::Request  &req,
             learning_communication::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节点初始化
        ros::init(argc, argv, "add_two_ints_server");
    
        // 创建节点句柄
        ros::NodeHandle n;
    
        // 创建一个名为add_two_ints的server,注册回调函数add()
        ros::ServiceServer service = n.advertiseService("add_two_ints", add);
    
        // 循环等待回调函数
        ROS_INFO("Ready to add two ints.");
        ros::spin();
    
        return 0;
    }

    client.cpp

    /**
     * AddTwoInts Client
     */
    
    #include <cstdlib>
    #include "ros/ros.h"
    #include "learning_communication/AddTwoInts.h"
    
    int main(int argc, char **argv)
    {
        // ROS节点初始化
        ros::init(argc, argv, "add_two_ints_client");
    
        // 从终端命令行获取两个加数
        if (argc != 3)
        {
            ROS_INFO("usage: add_two_ints_client X Y");
            return 1;
        }
    
        // 创建节点句柄
        ros::NodeHandle n;
    
        // 创建一个client,请求add_two_int service
        // service消息类型是learning_communication::AddTwoInts
        ros::ServiceClient client = n.serviceClient<learning_communication::AddTwoInts>("add_two_ints");
    
        // 创建learning_communication::AddTwoInts类型的service消息
        learning_communication::AddTwoInts srv;
        srv.request.a = atoll(argv[1]);
        srv.request.b = atoll(argv[2]);
    
        // 发布service请求,等待加法运算的应答结果
        if (client.call(srv))
        {
            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(server src/server.cpp)
    target_link_libraries(server ${catkin_LIBRARIES})
    add_dependencies(server ${PROJECT_NAME}_gencpp)
    
    add_executable(client src/client.cpp)
    target_link_libraries(client ${catkin_LIBRARIES})
    add_dependencies(client ${PROJECT_NAME}_gencpp)

    package.xml

    <?xml version="1.0"?>
    <package>
      <name>learning_communication</name>
      <version>0.0.0</version>
      <description>The learning_communication package</description>
      <maintainer email="hcx@todo.todo">hcx</maintainer>
    
      <license>TODO</license>
    
      <buildtool_depend>catkin</buildtool_depend>
      <build_depend>geometry_msgs</build_depend>
      <build_depend>roscpp</build_depend>
      <build_depend>rospy</build_depend>
      <build_depend>std_msgs</build_depend>
      <run_depend>geometry_msgs</run_depend>
      <run_depend>roscpp</run_depend>
      <run_depend>rospy</run_depend>
      <run_depend>std_msgs</run_depend>
    
      <build_depend>message_generation</build_depend>
      <run_depend>message_runtime</run_depend>
    
    
      <!-- The export tag contains other, unspecified, tags -->
      <export>
        <!-- Other tools can request additional information be placed here -->
    
      </export>
    </package>

    文件夹的主要功能

    文件夹的主要功能
    1)config:放置功能包中的配置文件,由用户创建,文件名可以不同。
    2)include:放置功能包中需要用到的头文件。
    3)scripts:放置可以直接运行的Python脚本。
    4)src:放置需要编译的C++代码。
    5)launch:放置功能包中的所有启动文件。
    6)msg:放置功能包自定义的消息类型。
    7)srv:放置功能包自定义的服务类型。
    8)action:放置功能包自定义的动作指令。
    9)CMakeLists.txt:编译器编译功能包的规则。

    如何自定义服务数据
    与话题消息类似,ROS中的服务数据可以通过srv文件进行语言无关的接口定义,一般放置在功能包根目录下的srv文件夹中。该文件包含请求与应答两个数据域,数据域中的内容与话题消息的数据类型相同,只是在请求与应答的描述之间,需要使用“—”三个横线进行分割。
    针对加法运算例程中的服务需求,创建一个定义服务数据类型的srv文件learning_communication/srv/AddTwoInts.sint64 a

    int64 b

    int64 sumv文件的内容较为简单,在服务请求的数据域中定义了两个int64类型的变量a和b,用来存储两个加数;
    又在服务应答的数据域中定义了一个int64类型的变量sum,用来存储“a+b”的结果。
    完成服务数据类型的描述后,与话题消息一样,还需要在功能包的package.xml和CMakeLists.txt文件中配置依赖与编译规则,在编译过程中将该描述文件转换成编程语言所能识别的代码。

    打开package.xml文件,添加以下依赖配置
    message_generation
    message_runtime

    打开CMakeLists.txt文件,添加如下配置
    find_package(catkin REQUIRED COMPONENTS
    geometry_msgs
    roscpp
    rospy
    std_msgs
    message_generation
    )

    add_service_files(
    FILES
    AddTwoInts.srv
    )
    message_generation包不仅可以针对话题消息产生相应的代码,还可以根据服务消息的类型描述文件产生相关的代码。
    功能包编译成功后,在服务的Server节点和Client节点的代码实现中就可以直接调用这些定义好的服务消息了。
    接下来我们就编写Server和Client节点的代码,完成两数相加求和的服务过程。

    代码解释

    Server节点实现过程
    1. 头文件

    #include "learning-communication/AddTwoInts.h"

    使用ROS中的服务,必须包含服务数据类型的头文件,这里使用的头文件是learning_communication/AddTwoInts.h,该头文件根据我们之前创建的服务数据类型的描述文件AddTwoInts.srv自动生成。

    2.主函ros::ServiceServer service = n.advertiseService(“add_two_ints”, add);部分相对简单,先初始化节点,创建节点句柄,重点是要创建一个服务的Server,指定服务的名称以及接收到服务数据后的回调函数。然后开始
    循环等待服务请求;一旦有服务请求,Server就跳入回调函数进行处理。

    3.回调函数部分

    bool add(learning_communication::AddTwoInts::Request &req,learning_communication::AddTwoInts::Response &res)

    回调函数是真正实现服务功能的部分,也是设计的重点。add()函数用于完成两个变量相加的功能,其传入参数便是我们在服务数据类型描述文件中声明的请求与应答的数据结构。

    { 
    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);
    ROS_INFO("sending back response: [%ld]", (long int)res.sum);
    return true;
    }

    在完成加法运算后,求和结果会放到应答数据中,反馈到Client,回调函数返回true。

    服务中的Server实现流程如下:
    ·初始化ROS节点。
    ·创建Server实例。
    ·循环等待服务请求,进入回调函数。
    ·在回调函数中完成服务功能的处理并反馈应答数据。

    Client节点的实现过程。
    1.创建Client

     ros::ServiceClient client = n.serviceClient<learning_communication::AddTwoInts> ("add_two_ints");

    首先需要创建一个add_two_ints的Client实例,指定服务类型为learning_communication:AddTwoInts。
    2.发布服务请求

    learning_communication::AddTwoInts srv;
    srv.request.a = atoll(argv[1]);
    srv.request.b = atoll(argv[2]);

    然后实例化一个服务数据类型的变量,该变量包含两个成员:request和response。将节点运行时输入的两个参数作为需要相加的两个整型数存储到变量中。
    if (client.call(srv))
    接着进行服务调用。该调用过程会发生阻塞,调用成功后返回true,访问srv.reponse即可获取服务请求的结果。如果调用失败会返回false,srv.reponse则不可使用。
    服务中的Client实现流程如下:

    ·初始化ROS节点。
    ·创建一个Client实例。
    ·发布服务请求数据。
    ·等待Server处理之后的应答结果。

    编译功能包
    编辑CMakeLists.txt文件,加入如下编译规则:

    add_executable(server src/server.cpp)
    target_link_libraries(server ${catkin_LIBRARIES})
    add_dependencies(server ${PROJECT_NAME}_gencpp)
    add_executable(client src/client.cpp)
    target_link_libraries(client ${catkin_LIBRARIES})
    add_dependencies(client ${PROJECT_NAME}_gencpp)

    现在就可以使用catkin_make命令编译功能包了。

    运行Server和Client
    运行编译生成的Server和Client节点。
    1.启动roscore
    在运行节点之前,首先需要确保ROS Master已经成功启动:
    roscore2Server使Server r o s c o r e 2 . 运 行 S e r v e r 节 点 打 开 终 端 , 使 用 如 下 命 令 运 行 S e r v e r 节 点 : rosrun learning_communication server

    3.运行Client节点
    打开一个新的终端,运行Client节点,同时需要输入加法运算的两个加数值:
    $ rosrun learning_communication client 3 5

    Client启动后发布服务请求,并成功接收到反馈结果

    Server接收到服务调用后完成加法求解,并将结果反馈给Client
    这里写图片描述

    这里写图片描述

    wiki

    rosservice list         输出可用服务的信息
    rosservice call         调用带参数的服务
    rosservice type         输出服务类型
    rosservice find         依据类型寻找服务find services by service type
    rosservice uri          输出服务的ROSRPC uri
    展开全文
  • 这种通信规则不同于ROS的话题通信服务通信通信规则没有时间规律,因此可以在任意时刻创建服务通信并按照预先设定好的通信规则进行。 服务通信的基本原理 服务是基于 C/S 模式的双向数据传输模式(有应答的通信...

    ROS中“逻辑处理需求”是什么?

    逻辑一词本身的含义就是“规则”的意思,逻辑处理需求就相当于一种基于规则的通信机制,即你呼我应的通信关系。这种通信规则不同于ROS的话题通信,服务通信的通信规则没有时间规律,因此可以在任意时刻创建服务通信并按照预先设定好的通信规则进行。

    服务通信的基本原理

    服务是基于 C/S 模式的双向数据传输模式(有应答的通信机制),话题是无应答的通信机制。

    服务通信和话题通信的对比

    由于服务通信较强的实时性也就代表了talker和listener之间的通信链路的存在是暂时的是有条件的,也就是说每当listener接受到talker的请求并与之建立连接后,listener仅仅触发一次服务,即一次请求对应一次响应。这也就解释了:为什么第五步中通过TCP/IP协议回传给listener的是一个数据,而非是为了建立稳定TCP/IP。

    相比较之下服务通信与话题通信的不同点在于:

    ① 在master中注册的信息不一样:

    注意:无论是话题通信还是服务通信都涉及topic,通信双方必须都有相同的同topic才可以!

    ② master对listener的回复不同:

    ③ listener/talker的连接方式不同于server/client:

    ④ 服务通信对启动顺序有要求:

    服务通信先启动服务器server然后再启动客户端client,但是话题通信的通信双方并没有启动的先后顺序。

    ⑤ 通信逻辑不同:

    话题通信这种无应答的通信方式虽实时性不高,但是传输效率明显高于服务通信;服务通信由于多了一个回应的机制所以在效率上要低于话题通信,但是通信可以根据需求随时快速响应大大提高了实时性。

    服务通信的实现

    .srv文件的配置

    一定要在功能包下建立名称为“srv”的文件夹,再在文件夹中创建.srv文件,否则编译失败,文件内容格式如下:

    int32 num1  
    int32 num1  
    ---  
    int32 sum 

    注:由于服务通信有clientàserver的响应以及serveràclient的回复,因此我们需要分别对两种消息进行数据类型设置,两种数据类型设置以”---”三个小杠为分界线。

    CMakelist.txt编译器配置文件的配置

    包含编译自定义功能包所需的符合ROS标准的功能包

    find_package(catkin REQUIRED COMPONENTS  
      roscpp  
      rospy  
      std_msgs  
      message_generation  
    ) 

    将符合ROS标准功能包编译成符合CMake标准的功能包

    catkin_package(  
    #  INCLUDE_DIRS include  
    #  LIBRARIES hello_ros  
     CATKIN_DEPENDS roscpp rospy std_msgs message_runtime  
    #  DEPENDS system_lib  
    )  

    添加要被编译的.srv文件

    ​## Generate services in the 'srv' folder  
    add_service_files(  
      FILES  
      Addlints.srv  
    ) 

    添加处理.srv文件的功能包

    generate_messages(  
      DEPENDENCIES  
      std_msgs  
    ) 

    声明C++可执行文件

    generate_messages(  
      DEPENDENCIES  
      std_msgs  
    )  

    指定要链接库或可执行目标的库

    target_link_libraries(srv_num  
      ${catkin_LIBRARIES}  
    )  
    target_link_libraries(cli_num  
      ${catkin_LIBRARIES}  
    )  

    该段代码的功能是:链接程序是将有关的目标文件批次链接,也即将一个文件中引用的符号同该符号所在的另外一个文件中的定义链接起来,使得所有的这些目标文件成为一个能够操作系统装入执行的统一整体。

    添加源文件运行前需要提前编译的文件

    代码格式

    add_dependencies(cli_num ${PROJECT_NAME}_gencpp)  
    add_dependencies(srv_num ${PROJECT_NAME}_gencpp)  

    由于我们的.cpp源文件包含了.srv/.msg编译生成的.h头文件,因此我们需要在编译.cpp源文件之前首先编译.srv/.msg文件,使得由.srv/.msg文件生成的对应.h头文件先于.cpp源文件而编译。

    代码位置

    确保在正确的地方使用了正确的add dependencies()调用。如果您仔细阅读了CMakeList.txt,您会发现两个add dependencies()调用发生在两个不同的地方。一个在add executable()之前,另一个在之后。第一个是“Add cmake target dependencies of The library”,第二个是“Add cmake target dependencies of The executable”。很明显,如果我要在.cpp源文件执行之前先执行对.srv/.msg的编译就要将add dependencies()的调用放在add executable()之后。要是放在add executable()之前,那就报出如下错误:

    Cannot add target-level dependencies to non-existent target "srv_num".

    目标文件是什么?

    编译器编译源代码后生成的文件叫做目标文件。从文件结构上来讲,目标文件已经是二进制文件。编译是针对单个源文件的,有几个源文件就会生成几个目标文件,并且在生成过程中不受其他源文件的影响。也就是说,不管当前工程中有多少个源文件,编译器每次只编译一个源文件、生成一个目标文件。

    链接是什么?静态链接、动态链接是什么,它们具体做了什么?

    静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件),再次方式下,只要一个文件包含库,就将库拷贝至内存中不管是否重复拷贝,这一步仅仅将多个二进制文件物理的组装起来,实际上是在为代码做加法;

    动态链接方式:在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝,也就是说动态连接方式的作用是将程序中的冗余库删除,当多个文件引用一个库时,在内存中只拷贝一份库而不是有几个引用拷贝几个,实际上是在为代码做减法。

    package.xml中添加编译依赖与执行依赖

    其实package.xml文件的作用就相当于告诉系统“在编译我自定义的.cpp源文件和消息文件时,我要用到那些包,即依赖包”。比如我们需要roscpp,rospy,std_msgs,message_generation功能包来将我们使用C++/Python编写的非ROS标准功能包转化为标准ROS功能包,那我们就必须向编译器声明我需要roscpp,rospy,std_msgs,message_generation功能包。

    <build_depend>roscpp</build_depend>  // 在基础ROS项目上创建功能包时所设置的依赖项
    <build_depend>rospy</build_depend>  // 在基础ROS项目上创建功能包时所设置的依赖项
    <build_depend>std_msgs</build_depend>  // 在基础ROS项目上创建功能包时所设置的依赖项
    <build_depend>message_generation</build_depend> // 处理自定义消息文件时需人为添加

    然后,我们还要设置运行ROS标准功能包时所需的功能包,即运行功能包的依赖包:

    <exec_depend>roscpp</exec_depend>  
    <exec_depend>rospy</exec_depend>  
    <exec_depend>std_msgs</exec_depend>  
    <exec_depend>message_runtime</exec_depend>  

    上述roscpp,rospy,std_msgs,message_runtime这四个功能包使得ROS标准功能包最终被编译成CMake标准下的可执行文件。

    Package.xml和CMakelist.txt文件的区别

    Package.xml文件向编译器说明了“在使用ROS标准/CMake标准进行编译时分别所需的功能包”,但是有是一回事用又是另一回事,package.xml只告诉编译器我需要这些东西,但又没指明“我需要如何操作这些功能包,这些功能包的执行顺序又是什么样的“,CMakelist.txt文件给我们指明了这一点,在CMakelist.txt文件中find_package告知系统我编译功能包时真正需要哪些功能包,当然了这些功能包必须必须先在package.xml文件被声明才行,二者关系如下:

    在CMakelist.txt中主要指明了以下几点:

    ① 在使用ROS标准编译该.cpp源文件之前需要先准备好什么功能包;

    ② 在使用CMake标准编译ROS标准文件时需要先准备好什么功能包;

    ③ 自定义消息文件是什么,将这些自定义ROS标准消息文件转化成可以嵌入.cpp源文件的.h文件需要什么功能包;

    ④ 我需要编译执行的操作对象有哪些,即我需要处理那些.cpp源文件,以及我需要将那些文件链接哪些CMake标准下的目标文件,最终一个项目会被编译链接生成一个可执行目标文件。

    而package.xml文件主要指明了以下几点:

    ① 你别管我如何用这些功能包,我将.cpp源文件编译成ROS标准文件需要这些功能包,你给我准备好就行,具体怎么做我也不知道,但是我就需要这些功能包;

    ② 你别管我如何用这些功能包,我将ROS标准文件编译成CMake标准下的二进制可执行目标文件需要这些功能包,你给我准备好就行,具体怎么做我也不知道,但是我就需要这些功能包。

    功能包文件编译程序流程

    注意:第一个是将ROS中.srv/.msg文件编译生成可以嵌入C++源文件的.h头文件,第二个才是将ROS项目中src文件夹下的所有.cpp源文件进行链接编译最终生成可执行的二进制文件。

    代码预期实现功能

    服务端代码编写——srv_num.cpp

    #include "ros/ros.h"  
    #include "hello_ros/Addlints.h"  
      
    bool DoSum(hello_ros::Addlints::Request& request,  
                hello_ros::Addlints::Response& response)  
    {  
        int num1 = request.num1,num2 = request.num2;  
        ROS_INFO("request:%d,%d\n\t",num1,num2);  
        response.sum = num1 + num2;  
        ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
      
        return true;  
    }  
      
    int main(int argc,char* argv[])  
    {  
        // locate language  
        setlocale(LC_ALL,"");  
        // initial ROS node  
        ros::init(argc,argv,"srv_object1");  
        // set NodeHandler  
        ros::NodeHandle nh;  
        // create server object  
        ros::ServiceServer service1 = nh.advertiseService<hello_ros::Addlints::Request,  
            hello_ros::Addlints::Response>("Addint",DoSum);  
        // ROS_INFO  
        ROS_INFO("service has been started......");  
        // Callback mechanism  
        ros::spin();  
      
        return 0;  
    } 

    Server/client的数据通信流程

    Client与server信息之间的交互逻辑

    有些人不明白为什么执行如下的赋值代码就可以将client的请求发送给server:

    hello_ros::Addlints ai;  
    ai.request.num1 = atoi(argv[1]);  
    ai.request.num2 = atoi(argv[2]); 

    其实我们编译完我们的自定义.srv服务消息文件之后,devel/include文件夹下会出现三个.h头文件:

    其中Addlints.h是给我们封装好了的,我们直接用这个就可以了,但是其他两个.h文件怎么用呢?这就涉及到数据的交互问题,下面是简单的原理(说明了代码编写的逻辑):

    这是封装之后的简单原理图,我们知道server和client之间是通过topic相互联系的,因此server和client之间可以以topic为为中间媒介建立一下模型(说明了server和client如何实时同步信息的):

    在整个通信过程中我们所需要操作的部分如下:

    我们需要做的就是:

    ① server端:编写回调函数从函数入口参数中的AddlintsRequest对象中提取request并将其在函数体中处理成response,并将其再次传给函数入口参数AddlintsResponse对象;

    ② client端:自定义Addlints对象,将request所需的所有信息赋值给Addlints对象中的子类对象request完成对自定义请求消息类型的封装,然后从Addlints对象中的子类对象response中提取server返回给client的response。

    client向master上报的数据类型

    当我们看了advertiseService函数的重载时,我们就会发现,advertiseService函数需要实例化模板参数,分别是类类型:请求数据类型和响应数据类型。代码如下:

    ros::ServiceServer service1 = nh.advertiseService<hello_ros::Addlints::Request,hello_ros::Addlints::Response>("Addint",DoSum);  

    上述代码的含义:将request的数据类型和response的数据类型以及topic服务通信主题名称上报给master主节点,从而在master中注册信息。值得注意的是,节点句柄是操作节点和读取关于节点的属性的,例如操作节点与主节点之间通信(即将节点的信息在master主节点中进行注册),读取通过命令行了解节点从命令行中获取的参数等。

    回调函数的编写

    bool DoSum(hello_ros::Addlints::Request& request,  
                hello_ros::Addlints::Response& response)  
    {  
        int num1 = request.num1,num2 = request.num2;  
        ROS_INFO("request:%d,%d\n\t",num1,num2);  
        response.sum = num1 + num2;  
        ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
      
        return true;  
    } 

    首先,函数返回值需要时bool类型的,其中true代表server接收client客户端请求成功并且成功调用回调函数处理client传来的请求信息,false则代表server没有接收到client客户端请求或者处理从client发来的信息时出现异常导致失败。

    其次,函数的入口参数要和server服务端所能接收的request和response信息数据类型一致,其中server服务端在master中注册的request和response信息数据类型就是我们下面程序中的实例化模板参数:

    ros::ServiceServer service1 = nh.advertiseService<hello_ros::Addlints::Request,hello_ros::Addlints::Response>("Addint",DoSum);  

    其实,我们可以把回调函数看作一个“发酵罐”,进去的是酿酒的原料(request)出来的就是成品酒(response),关于“将来自client的request处理成server返回给client的response“的形象化比喻如下所示:

    可以将回调函数与client直接的关系也进行如下形象化比喻:

    当从client读取到“server处理请求的相关情况(失败/成功)“,我们可以下一步操作。

    回调函数参数为何为类对象的引用,仅仅传入函数对象不行吗?

    我们前面提到过:

    我们要将函数值处理的结果(即response)在装进Addlints自定义类类型对象之中的话,我们必须要将函数参数设置为引用,这样当函数体内部改变了传入参数的数值,传入参数的数值也会发生改变,这就涉及到C++引用传参的具体知识了。

    但是传入参数的引用,也导致了一些问题:我们可以在server端的回调函数中修改client传给server的request。具体代码如下:

    bool DoSum(hello_ros::Addlints::Request& request,  
                hello_ros::Addlints::Response& response)  
    {  
        request.num1 = 0; request.num2 = 0;  
        ROS_INFO("request have been changed:%d,%d\n\t",request.num1,request.num2 );  
        int num1 = request.num1,num2 = request.num2;  
        ROS_INFO("request:%d,%d\n\t",num1,num2);  
        response.sum = num1 + num2;  
        ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
      
        return true;  
    }  

    客户端响应相应如下:

    服务器端相应如下:

    上面的响应结果告诉我们:由于相互连接的server和client有相同的topic,这样就会导致一端改变全都改变的局面,即我们居然从server端修改了从client发送过来的request,这一下子就会导致“现在的request已不同于client最初发送的request了“,就相当于“我原来给机械臂发送了向左转的信号,但是由于server端将request进行了误修改导致机械臂的响应是向右转”。

    那我们该如何防止在函数体内部的误修改呢?

    我们函数体内部将函数入口参数赋值给我们函数体的内部变量,我们禁止改变的变量就赋值给const类型的类类型的引用,我们想改变的可以不进行任何多于操作,具体代码实现如下:

    bool DoSum(hello_ros::Addlints::Request& request,  
                hello_ros::Addlints::Response& response)  
    {  
        const hello_ros::Addlints::Request& rqu = request;  
        int num1 = rqu.num1,num2 = rqu.num2;  
        ROS_INFO("request:%d,%d\n\t",num1,num2);  
        response.sum = num1 + num2;  
        ROS_INFO("response:%d+%d=%d",num1,num2,response.sum);  
      
        return true;  
    }

    将传入的request参数在函数体内部赋值给常值引用就可以保证我们不对request进行误操作误修改从而保证了处理request所得的response的正确性。我们这里使用const引用类类型而不用const类类型就是为了提高运行效率,因为你要是用的是const类类型的话,这就涉及到了类对象的拷贝,导致程序执行效率下降。

    回调函数的调用

    // Callback mechanism  
     ros::spin();  

    这句代码表示着:如果server接收到来自client的request,那我就执行被挂起的回到函数。注意:每当有回调函数时,我们都必须调用这段代码。

    客户端代码编写——cli_num.cpp

    主要代码如下:

    #include "ros/ros.h"  
    #include "hello_ros/Addlints.h"  
      
    int main(int argc, char* argv[])  
    {  
        // locate language  
        setlocale(LC_ALL,"");  
      
        if(argc != 3) {  
            ROS_INFO("input arg error......");  
            return 1;  
        }  
      
        // initial ROS node  
        ros::init(argc,argv,"client_object1");  
        // create Nodehandler  
        ros::NodeHandle nh;  
        // create client object under service mode  
        ros::ServiceClient cli_obj1 = nh.serviceClient<hello_ros::Addlints::Request,  
            hello_ros::Addlints::Response>("Addint");  
        // send request to server and receive response from server  
        hello_ros::Addlints ai;  
        ai.request.num1 = atoi(argv[1]);  
        ai.request.num2 = atoi(argv[2]);  
      
        // hang up client request to wait for the appearance of server  
        //cli_obj1.waitForExistence(); // from the view of client  
        ros::service::waitForService("Addint"); // from the view of master  
        bool flag = cli_obj1.call(ai);  
      
        if(flag) {  
            ROS_INFO("sum:%d",ai.response.sum);  
        } else {  
            ROS_INFO("request failed......");  
            return 2;  
        }  
      
        return 0;  
    }

    Client对外信息输出接口——自定义消息类类对象

    在上述程序中,hello_ros命名空间下有一个最最重要的Addlints对象,hello_ros是我们自定义功能包的名字在这里充当变量的命名空间,Addlints类类型则是我们自定义的.srv服务消息类型文件编译而来的类类型。

    为什么说Addlints对象最重要呢?我们思考一下,我们在server服务器端程序编写中提及过:server处理请求产生的response信息和client发送给server的request请求信息均会同步存在于server服务器和client客户端中(这是由于他们两个有相同的topic,这使得双方拥有的数据可以同步)。如果在服务器中要找一个既能向server发送request又能从server接收response的变量,那我们只能找我们自定义的Addlints类类型对象了,因为我前面提到过:Addlints.h文件下包含两个子文件,分别为AddlintsRequest.h和AddlintsResponse.h文件,这两个子文件分别负责存储client向server的request和server返回给client的response。

    类对象在我们server服务器端和client端都必须存在,只不过在server端,我们将AddlintsRequest和AddlintsResponse类对象用于实例化了回到函数并且作为参数传入用于处理request的回调函数中,在回调函数中我们处理request并且返回response。

    那client呢?在client端,我们必须自定义一个Addlints类类型对象,我们可以向Addlints对象的AddlintsRequest子对象传入信息,这一步就相当于我们将我们传入的信息封装进了request中以待向server传递。此外,我们还可以从Addlints类对象的AddlintsResponse子对象在client端中读取server处理request所返回给client客户端的response信息。

    Addlints自定义服务消息类类型对象在Server和client端中充当的角色如下所示:

    程序优化:从命令行读入信息

    int main(int argc, char* argv[])  
    {  
        if(argc != 3) {  
            ROS_INFO("input arg error......");  
            return 1;  
        }//判断参数个数是否正确  
        ......  
        hello_ros::Addlints ai;  
        ai.request.num1 = atoi(argv[1]); // 读取数据  
        ai.request.num2 = atoi(argv[2]); // 读取数据  
        ......  
    }  

    在main函数的参数中int argc表示参数的个数,如果我们不通过命令行给main函数传参,那么argc=1,这一个参数代表了main函数所在.cpp文件的调用地址指明了main函数的入口地址,这里我们不需要这个参数,我们需要的是除第一个main函数入口地址外的其他参数,从命令行输入参数的形式如下:

    上述命令输入格式为:rosrun package_name cpp_name arg1 arg2,其中main函数入口参数中argv是装有字符型指针的一维数组,其结构如下:

    由于我们需要的是int类型的参数,而argv[][]中存储的是char类型的字符,因此我们要使用atoi函数将char类型的数字转化为int类型的数字。

    服务要先于客户端启动,那不这样做行吗?如何改进?

    正常情况下不可以,为什么呢?因为服务通信讲究的是实时性,client发出的请求之后server收到了信息传输通道就可以建立,如果client想要连接的server还没有在master中完成注册或者根本还没有启动,client可没时间跟你浪费时间,client发出的请求一般都是要server实时响应的。如果server不响应client的实施请求,client端就会立刻返回请求发送失败,即如果你在未启动server之前就率先启动client,系统会返回如下错误:

    一对server/client可以这样干,但是ROS系统中节点那么多,在我们编写launch文件时可能弄错了某一对server/client节点的启动顺序,那直接报错,这也太麻烦了。于是ROS系统提供了两种确保server没启动时将client请求发送函数挂起的内置函数:

    client向master询问“接收我的请求信息的server启动没有呀?”

    对应函数如下:

    ros::service::waitForService("Addint");  

    只要是以ros为命名空间的都是和master有关的操作,这个函数就是client向master询问“接收我的请求信息的server启动没有呀?”的函数,如果master告诉client“大兄弟能接收你请求的server还没在我这里注册完信息或者根本没被启动,你要不先别发送请求先耐心等等吧”。一旦server启动,发送client请求的函数会被立刻执行,然后client会和server建立基于TCP/IP协议的一次性的通信关系(即一次请求只对应一次响应),输出结果如下:

    ① 服务端:

    ② 客户端:

    注意:这里的内置函数只能确保client发送请求的函数挂起,并没有改变“如果client先于server启动,服务通信就会失败的本质”!

    Client尝试着发送request试试,看看能否成功,如果不成功就等待

    对应函数如下:

    cli_obj1.waitForExistence(); // from the view of client  

    这段代码的含义就是不断地尝试与server取得联系,如果连接失败说明server还未启动或者还未在master中注册完自身信息,那发送client请求的函数会被挂起暂时不执行。

    使client请求发送函数挂起的内置函数在代码中的位置

    动作的发起者:client对象调用call函数

    1. bool flag = cli_obj1.call(ai);  

    这段代码表示了“client发送request”的动作,函数的输入参数是client下的Addlints对象,返回参数是一个bool类型的变量,表征着server是否接受请求并且返回响应信息。

    Call函数执行成功标志着如下图所示的部分已经被执行完成了:

    处理response并且进行下一步动作

    我这里是:如果执行成功(flag=true)并成功得到response就会将server发给client的response打印到命令行中;若执行失败(flag=false),则在命令行打印"request failed......"。具体实现程序如下:

    if(flag) {  
        ROS_INFO("sum:%d",ai.response.sum);  
    } else {  
        ROS_INFO("request failed......");  
        return 2;  
    }  

    我使用client客户端对象(即ros::ServiceClient类型的对象)调用的call函数的bool类型的返回值进行判断执行是否成功。

    展开全文
  • 之前博客《RO发布者S复习笔记之——系统框架及其编程规范》的(发布者节点和订阅者节点的创建和...与话题不同,服务是一次性消息通信。因此,当服务的请求和响应完成 时,两个节点的连接会被断开。 这种服务通常在让机
  • ros服务通信

    2021-10-23 14:09:45
    服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景: 机器人巡逻过程中,控制...

    服务通信简介:

    服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:

    机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人… 此时需要拍摄照片并留存。

    在上述场景中,就使用到了服务通信。

    一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果

    与上述应用类似的,服务通信更适用于对时时性有要求、具有一定逻辑处理的应用场景。

    一、服务通信理论模型

    服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:

    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。

    注意:
    
    1.客户端请求被处理时,需要保证服务器已经启动;
    
    2.服务端和客户端都可以存在多个。
    

    二、概念

    以请求响应的方式实现不同节点之间数据交互的通信模式。

    三、作用

    用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。

    四、案例

    实现两个数字的求和,客户端节点,运行会向服务器发送两个数字,服务器端节点接收两个数字求和并将结果响应回客户端。
    在这里插入图片描述
    五、案例实现

    需求:

    服务通信中,客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。
    

    (1)服务通信自定义srv

    srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:

    1.按照固定格式创建srv文件
    
    2.编辑配置文件
    
    3.编译生成中间文件
    

    服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用—分割,具体实现如下:

    功能包下新建 srv 目录,添加 xxx.srv 文件,内容:

    #客户端请求时发送的两个数字
    int32 num1
    int32 num2
    ---
    #服务器响应发送的数据
    int32 sum
    

    (2)编辑配置文件

    package.xml中添加编译依赖与执行依赖

    <build_depend>message_generation</build_depend>
    <exec_depend>message_runtime</exec_depend>
    
      <!- 
      exce_depend 以前对应的是 run_depend 现在非法
      -->
    

    CMakeLists.txt编辑 srv 相关配置

    find_package(catkin REQUIRED COMPONENTS
      roscpp
      rospy
      std_msgs
      message_generation
    )
    
    需要加入 message_generation,必须有 std_msgs
    
    add_service_files(
      FILES
      AddInts.srv
    )
    
    generate_messages(
      DEPENDENCIES
      std_msgs
    )
    

    注意: 官网没有在 catkin_package 中配置 message_runtime,经测试配置也可以

    (3)编译

    编译后的中间文件查看:

    C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)
    在这里插入图片描述Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/srv)
    在这里插入图片描述
    后续调用相关 srv 时,是从这些中间文件调用的

    (4)代码编写

    (4.1)服务端

    /*
        需求: 
            编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
            服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
            客户端再解析
    
        服务器实现:
            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;
    }
    
    

    (4.2)客户端

    /*
        需求: 
            编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
            服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
            客户端再解析
    
        服务器实现:
            1.包含头文件
            2.初始化 ROS 节点
            3.创建 ROS 句柄
            4.创建 客户端 对象
            5.请求服务,接收响应
    
    */
    // 1.包含头文件
    #include "ros/ros.h"
    #include "demo03_server_client/AddInts.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
    
        // 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3
        if (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");
        //等待服务启动成功
        //方式1
        ros::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;
    }
    
    

    (5)配置 CMakeLists.txt

    add_executable(AddInts_Server src/AddInts_Server.cpp)
    add_executable(AddInts_Client src/AddInts_Client.cpp)
    
    
    add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp)
    add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)
    
    
    target_link_libraries(AddInts_Server
      ${catkin_LIBRARIES}
    )
    target_link_libraries(AddInts_Client
      ${catkin_LIBRARIES}
    )
    

    (6)执行

    流程:
    
        需要先启动服务:rosrun 包名 服务
    
        然后再调用客户端 :rosrun 包名 客户端 参数1 参数2
    
    结果:
    
    会根据提交的数据响应相加后的结果。
    
    注意:
    
    如果先启动客户端,那么会导致运行失败
    
    优化:
    
    在客户端发送请求前添加:client.waitForExistence();
    
    或:ros::service::waitForService("AddInts");
    
    这是一个阻塞式函数,只有服务启动成功后才会继续执行
    
    此处可以使用 launch 文件优化,但是需要注意 args 传参特点
    
    展开全文
  • ROS服务通信

    2021-03-09 21:31:37
    服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景: 机器人巡逻过程中,控制...
  • 一、分别运行下述指令: roscore rosrun turtlesim turtlesim_...二、构建服务通信模型: 程序编写如下: /** * 该例程将请求/spawn服务服务数据类型turtlesim::Spawn */ #include <ros/ros.h> #include
  • ROS服务通信机制srv服务通信的目的ROS服务通信的实现ROS中的实现代码实现 服务通信的目的 1, 服务通信,可以用最简单的一张图来表示: 2, 用最简单的话来说,就是,你问,我答。 3, 应用场景, 比如人脸识别,...
  • 服务通信涉及到三个角色: ROS master(管理者)、Server(服务端)、Client(客户端) ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立...
  • ROS通信机制

    千次阅读 多人点赞 2019-08-29 15:55:53
    文章目录写在前面的话ROS通信机制话题通信机制服务通信机制共享参数机制话题与服务的区别 写在前面的话 文档没有任何商业因素,本着共享的精神进行分享,如有素材侵权,请给我留言; 文档都是自己平时看书或工作...
  • 创建的ros::NodeHandle 节点不能使用局部初始化,需要使用全局初始化,否则无法通信,具体原因不明!!!
  • ROS通信方式有三种,分别为话题(msg)、服务(srv)和活动(action),其中话题属于基本的发布/订阅通信方式,适用于单个节点发布消息,一个或多个节点接受消息的情况;服务属于实时获取结果的情况,应用于请求/...
  • ROS架构(五)——ROS通信机制

    千次阅读 2021-01-11 22:12:34
    ROS架构(四)——ROS通信机制 ROS的核心——分布式通信机制
  • 主要记录了ROS使用服务进行通信的全过程,以及自定义srv文件。
  • 在window下,建立与ubuntu系统中ROS通信,实现将window中的字符发个ubuntu中ROS系统
  • 在上一篇文章中简单了解了ROS话题通信机制,但是它内部的通信过程,很多人都是不知道的,今天我看了 胡春旭的书籍——《ROS机器人开发实践》,又加深了对ROS话题通信的理解,打算接着讲解一下其过程是怎么样的。...
  • ROS的四种通信架构

    千次阅读 多人点赞 2019-08-27 15:46:43
    ROS通信方式是ROS最为核心的概念,ROS系统的精髓就在于它提供的通信架构。ROS通信方式有以下四种: Topic 主题 Service 服务 Parameter Service 参数服务器 Actionlib 动作库 一 Topic ROS中的通信方式中,...
  • 本文主要介绍怎么编写代码实现在上层修改底层参数的相关知识...ros下使用rosserial和STM32F1/STM32F4系列进行通信(MDK5工程):https://blog.csdn.net/qq_36349536/article/details/82773064 STM32(MDK)中如何使用ROS
  • ROS服务中自定义数据类型 在上一篇文章中,描述了一种两层封装的点集传输服务消息类型,比较复杂。上一篇文章 事后我就在想何必包两层,直接在服务中利用提供的数据类型定义数组不就行了。所以动手试了一下: 自定义...
  • 介绍ROS的4种通信方式与消息文件的概略。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,026
精华内容 4,810
关键字:

ros服务通信