ROS学习笔记(三)—–客户端Client的编程实现、服务端Server的编程实现、服务数据的定义与使用

89
0
2020年11月16日 10时54分

   本系列文章是与大家分享一下,我在学习ROS入门过程中所做的笔记,目前主要的学习资料是古月老师(胡春旭老师)的 ROS入门21讲(文章中部分图片来自于21讲课件),以及其编著的《ROS机器人开发实践》,当然其中也加入了我自己的理解和其他的相关资料,本系列共五篇文章,本篇文章是本系列第三篇文章,本篇文章主要介绍客户端Client的编程实现、服务端Server的编程实现、服务数据的定义与使用


八、客户端Client的编程实现

   1、服务模型(客户端/服务器)

在这里插入图片描述

   2、本部分内容要实现编写一个客户端,发送产生一个新海龟的请求Request,给我们的海龟仿真器(Server端),Server端接收到后,产生一个新的海龟,然后返回一个Response给客户端Client,本例中中间的Service是之前用过的Spawn,他的数据结构在turtlesim里面自定义的turtlesim::Spawn

在这里插入图片描述

   3、首先我们先创建一个新的功能包,取名为learning_service ,

cd ~/catkin_ws/src
catkin_create_pkg learning_service roscpp rospy std_msgs geometry_msgs turtlesim

在这里插入图片描述

   4、将我们编写好的实现以上功能的c++文件,放到刚才创建的功能包的src文件夹下,我们来看一下这个c++程序的内容

在这里插入图片描述

/**
 * 该例程将请求/spawn服务,服务数据类型turtlesim::Spawn
 */

#include <ros/ros.h>
#include <turtlesim/Spawn.h>

int main(int argc, char** argv)
{
    // 初始化ROS节点
    ros::init(argc, argv, "turtle_spawn");

    // 创建节点句柄
    ros::NodeHandle node;

    // 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
    ros::service::waitForService("/spawn");
    ros::ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");

    // 初始化turtlesim::Spawn的请求数据
    turtlesim::Spawn srv;
    srv.request.x = 2.0;
    srv.request.y = 2.0;
    srv.request.name = "turtle2";

    // 请求服务调用
    ROS_INFO("Call service to spwan turtle[x:%0.6f, y:%0.6f, name:%s]", 
             srv.request.x, srv.request.y, srv.request.name.c_str());

    add_turtle.call(srv);

    // 显示服务调用结果
    ROS_INFO("Spwan turtle successfully [name:%s]", srv.response.name.c_str());

    return 0;
};

   (1)因为我们用到了turtlesim::Spawn这样一个数据类型,所以要包含他的头文件#include <turtlesim/Spawn.h>,至于#include <ros/ros.h>就不用多说了

   (2)在main函数里第一步就是进行节点的初始化,命名为turtle_spawn,然后创建节点句柄,这个跟之前都是一样的

   (3)接下来就是创建跟服务相关的代码,ros::service::wait ForService(“/spawn”);是去查询我们系统里是否有spawn这样一个服务,如果找不到会一直在这里等待,只有服务在系统中存在了,我们才能去请求服务,紧接着我们就创建了一个客户端 ros::ServiceClient add_turtle = node.serviceClient (“/spawn”);这个客户端是用来给spawn这个服务发送请求的,请求的数据类型是turtlesim:: Spawn

   (4)接下来就是封装我们请求的数据,定义一个turtlesim::Spawn数据类型的数据srv,在Spawn数据类型里有新产生的海龟的xy坐标,初始角度,海龟的名字,在这里我们只定义了新产生海龟的位置和名字,没有定义角度,它默认为0

   (5)紧接着我们要发送一个日志,告诉终端我们要发送这样一个请求了

   (6)之前在topic通讯里面我们用publisher或者subscriber,在服务里,请求的客户端他发的方法叫call,通过这样一个方法我们把封装好的这样一个数据src发送出去,call把数据src发送出去后,会在这里等待服务器给我一个反馈,直到服务器告诉我们海龟已经产生成功了,这一句语句才会执行结束,去执行下一句语句

   (7)接下来的ROS_INFO语句就是把创建成功的信息在终端显示出来

   5、接下来是配置客户端代码编译规则,同样是把下面两行代码添加到CMakeLists.txt文件中,位置如下所示,同样第一个是编译规则把turtle_spawn .cpp编译成turtle_spawn这样一个规则,第二个是设置turtle_spawn去编译的时候需要用到的一些库

add_executable(turtle_spawn src/turtle_spawn.cpp)
target_link_libraries(turtle_spawn ${catkin_LIBRARIES})

在这里插入图片描述
在这里插入图片描述

   6、接下来就可以返回到工作空间的根目录下,对他进行编译了,编译完成后在devel的lib下面就可以看到新产生的功能包,他里面就是刚才的代码产生的新的可执行文件turtle_spawn,接下里我们就可以去运行了,同样我们要先运行roscore,再运行海龟的仿真节点,再运行刚才我们实现的程序节点,通过出现去发布server这样的request请求,产生一只新的海龟

cd ~/catkin_ws
catkin_make
source devel/setup.bash
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_spawn

在这里插入图片描述

   7、Python代码如下:

!/usr/bin/env python
# -*- coding: utf-8 -*-
# 该例程将请求/spawn服务,服务数据类型turtlesim::Spawn

import sys
import rospy
from turtlesim.srv import Spawn

def turtle_spawn():
    # ROS节点初始化
    rospy.init_node('turtle_spawn')

    # 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
    rospy.wait_for_service('/spawn')
    try:
        add_turtle = rospy.ServiceProxy('/spawn', Spawn)

        # 请求服务调用,输入请求数据
        response = add_turtle(2.0, 2.0, 0.0, "turtle2")
        return response.name
    except rospy.ServiceException, e:
        print "Service call failed: %s"%e

if __name__ == "__main__":
    #服务调用并显示调用结果
    print "Spwan turtle successfully [name:%s]" %(turtle_spawn())

九、服务端Server的编程实现

   1、服务模型

在这里插入图片描述

   2、本例中通过要实现让Server发送速度的指令,client端发布request去控制Service是否要去给海龟发送指令,client端相当于海龟运动和停止的开关,当他发一个Request后海龟就运动,再发一个海龟就停止,Server端接收指令,并完成海龟运动指令的发送,在这过程中用到的是我们自定义的一个Service,取名为/turtle_command ,这个Service的数据类型我们用到的是ROS中针对服务的标准的定义,叫做Trigger,相当于一个触发,当Server这一端收到触发信号之后,来控制海龟是否发送速度指令,同时返回一个Response告诉Client你的请求是否已经执行成功了,本例中Server这一端既包含了Server这一端的实现,同时也包含了一个topic的Publisher的实现,所以本例中的Server端包含两个内容。

   3、服务器的实现流程

在这里插入图片描述

   4、同样需要把我们的c++代码放在之前创建的功能包learning_service的src文件夹中

在这里插入图片描述

/**

 * 该例程将执行/turtle_command服务,服务数据类型std_srvs/Trigger

 */

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

ros::Publisher turtle_vel_pub;
bool pubCommand = false;
// service回调函数,输入参数req,输出参数res

bool commandCallback(std_srvs::Trigger::Request  &req,

                     std_srvs::Trigger::Response &res)
{
    pubCommand = !pubCommand;
   // 显示请求数据
   ROS_INFO("Publish turtle velocity command [%s]", pubCommand==true?"Yes":"No");

    // 设置反馈数据
    res.success = true;
    res.message = "Change turtle command state!";

    return true;
}
int main(int argc, char **argv)
{
    // ROS节点初始化
    ros::init(argc, argv, "turtle_command_server");
    // 创建节点句柄
    ros::NodeHandle n;

    // 创建一个名为/turtle_command的server,注册回调函数commandCallback
    ros::ServiceServer command_service = n.advertiseService("/turtle_command", commandCallback);

    // 创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
    turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);

    // 循环等待回调函数
    ROS_INFO("Ready to receive turtle command.");
    // 设置循环的频率
    ros::Rate loop_rate(10);

    while(ros::ok())
    {
        // 查看一次回调函数队列
        ros::spinOnce();
        // 如果标志为true,则发布速度指令
        if(pubCommand)
        {
            geometry_msgs::Twist vel_msg;
            vel_msg.linear.x = 0.5;
            vel_msg.angular.z = 0.2;
            turtle_vel_pub.publish(vel_msg);
        }
        //按照循环频率延时
        loop_rate.sleep();
    }
    return 0;
}

   (1)本例程需要包含这三个头文件#include <ros/ros.h> #include 和 #include <std_srvs/Trigger.h>

   (2)ros::Publisher turtle_vel_pub;创建一个全局的Publisher,bool pubCommand = false;创建一个bool类型的标志位pubCommand 记录海龟是运动还是停止,默认false是停止

   (3)同样在面函数中,需要先初始化节点,创建节点句柄

   (4)接下来就是创建名为/turtle_command的server,并注册回调函数comm andCallback,跟在topic中的subscriber比较像,同样我们不知道信息什么时候进来,也需要通过回调函数来处理,一旦我们Server一端接到request进来后就调用回调函数

   (5)接下来要创建一个topic的publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10

   (6)接下来的 ROS_INFO(“Ready to receive turtle command.”);发布了一个日志信息

   (7)设置循环的频率为1秒10hz的频率去发送: ros::Rate loop_rate(10);

   (8)接下来在while循环里,通过ros::spinOnce(); 去查看回调函数队列,看是否有消息进来了,如果有消息进来,就去调用回调函数

   (9)接下来的if判断,如果标志位是ture的话就发布速度指令,海龟就会运动,否则海龟就不会运动,这个标志位pubCommand是在回调函数里去做处理的,当Client这一端有请求发出来后,在回调函数里,第一步就将标志位pubCommand取反,也就是说pubCommand充当了一个开关的作用,接下来的ROS_INFO语句把我们的日志打出来,告诉我们当前收到的指令是开还是关,同时我们需要向Client一端反馈一个应答,告诉他是执行成功了还是失败了,res.success = true; res.mess age = “Change turtle command state!”其中的success 和mess 是在Trigger里面定义的

   (10)我们通过rossrv show std_srvs/Trigger命令可以查看std_srvs/Trigger的定义,三个横线作为区隔,三个横线上面是request的内容,下面是Client的内容,当前例子中request的内容是空的

在这里插入图片描述

   (11)bool commandCallback(std_srvs::Trigger::Request &req std_srvs::Trigger::Response &res)中的参数就是请求内容和应答内容(本例中的请求内容是空的)

   5、接下来同样是设置我们的编译规则,将如下的语句放到CMakeLists.txt中(位置如下所示)

add_executable(turtle_command_server src/turtle_command_server.cpp)
target_link_libraries(turtle_command_server ${catkin_LIBRARIES})

在这里插入图片描述

   6、接下来就可以在工作空间下去编译,并执行以上内容了

  cd ~/catkin_ws
  catkin_make
  source devel/setup.bash
  roscore
  rosrun turtlesim turtlesim_node
  rosrun learning_service turtle_command_server
  rosservice call /turtle_command "{}"

   7、因为我们之前设置了自动source,所以我们直接运行roscore打开节点管理器,运行rosrun turtlesim turtlesim_node启动海龟仿真器,运行rosrun learning _service turtle_command_server启动刚才我们编写的服务端,然后通过命令行工具的rosservice call命令,后面跟的第一个参数是我们写好的服务的名字/turtle_command ,然后双击tab键,可以自动补全,该服务的数据内容,在本例中请求数据是空的。运行一次海龟开始运动,再运行一次海龟停止运动,如此循环。

在这里插入图片描述


十、服务数据的定义与使用

   1、在本例的模型中,client端要去发布显示某个人信息的请求,通过我们自定义的service数据把信息发出去,在server端我们就可以接收到这个请求,同时包含人的名字,年龄,性别等信息。通过日志显示出来,同时response反馈显示的结果,这期间用的service是我们自定义的/show_person,用到的数据类型是learning_service::Person

在这里插入图片描述

   2、在message里面我们定义了.msg这样的文件,服务也是类似的,我们去定义.srv这样的文件,服务跟信息的区别是服务有反馈的内容,需要用三个横线将其分成两部分,三个横线以上时request的内容,三个横线以下是response的内容

   3、本列中的数据内容如下:

string name
uint8 age
uint8 sex

uint8 unknown =0
uint8 male = 1
uint8 female = 2
---
string result

   4、首先,我们需要在我们的功能包下创建新的文件夹srv,在该文件夹下,运用touch命令(touch Person.srv)创建Person.srv文件,将以上的数据内容添加到该文件中

在这里插入图片描述
在这里插入图片描述

   5、 接下来就是编译的步骤了,跟之前message的步骤一样,先在在package.xml中添加功能包依赖,配置动态生成信息的功能包,位置如下所示:

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

在这里插入图片描述

   6、在CMakeLists.txt添加编译选项,现在文件的find_package内加入message_generation,通过find_package找到功能包作为依赖,接下来是添加定义的srv文件,让编译器知道根据那个srv文件产生头文件,第一个add_service _files后面是我们之前编好的Person.srv文件名,它会自动搜索srv文件夹下的文件,第二个 generate_messages会根据文件的定义Person.srv以及产生的依赖std_msgs去生成对应的头文件,最后在catkin_package中添加编译的依赖message_runtime

  find_package( …… message_generation) 

  add_service_files(FILES Person.srv)
  generate_messages(DEPENDENCIES std_msgs)

  catkin_package(…… message_runtime)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

   7、接下来就是返回到工作空间下,编译生成我们需要的头文件了(catkin_make),我们发现编译生成了三个头文件

在这里插入图片描述


” class=”reference-link”>   8、接下来就是去使用我们定义的数据了,将我们编写好的服务端和客户端的c++代码放到功能包的src文件夹下在这里插入图片描述

   9、Client的代码

/**
 * 该例程将请求/show_person服务,服务数据类型learning_service::Person
 */

#include <ros/ros.h>
#include "learning_service/Person.h"

int main(int argc, char** argv)
{
    // 初始化ROS节点
    ros::init(argc, argv, "person_client");

    // 创建节点句柄
    ros::NodeHandle node;

    // 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
    ros::service::waitForService("/show_person");
    ros::ServiceClient person_client = node.serviceClient<learning_service::Person>("/show_person");

    // 初始化learning_service::Person的请求数据
    learning_service::Person srv;
    srv.request.name = "Tom";
    srv.request.age  = 20;
    srv.request.sex  = learning_service::Person::Request::male;

    // 请求服务调用
    ROS_INFO("Call service to show person[name:%s, age:%d, sex:%d]", 
             srv.request.name.c_str(), srv.request.age, srv.request.sex);

    person_client.call(srv);

    // 显示服务调用结果
    ROS_INFO("Show person result : %s", srv.response.result.c_str());

    return 0;
};

   10、Server的代码

/**

 * 该例程将执行/show_person服务,服务数据类型learning_service::Person

 */
#include <ros/ros.h>
#include "learning_service/Person.h"
// service回调函数,输入参数req,输出参数res

bool personCallback(learning_service::Person::Request  &req,

                     learning_service::Person::Response &res)
{
    // 显示请求数据
    ROS_INFO("Person: name:%s  age:%d  sex:%d", req.name.c_str(), req.age, req.sex);
    // 设置反馈数据
    res.result = "OK";
    return true;
}

int main(int argc, char **argv)
{
    // ROS节点初始化
    ros::init(argc, argv, "person_server");
    // 创建节点句柄
    ros::NodeHandle n;
   // 创建一个名为/show_person的server,注册回调函数personCallback
    ros::ServiceServer person_service = n.advertiseService("/show_person", personCallback);

    // 循环等待回调函数
    ROS_INFO("Ready to show person informtion.");
    ros::spin();
    return 0;

}

在这里插入图片描述

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

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

在这里插入图片描述

   12、接下来就是去做编译和运行我们编写的代码了(题外话,当我们运行一个新的程序时,最好将之前运行的roscore关掉,重新运行一下,这样会规避一些不必要的错误)

cd ~/catkin_ws
catkin_make
source devel/setup.bash
roscore
rosrun learning_service person_server
rosrun learning_service person_client

在这里插入图片描述

发表评论

后才能评论