ROS 六、服务端与客户端


ROS 6:服务端客户端实现

一、客户端实现

实现目标:使用客户端发送一个请求,请求turtlesim的spawn服务来新建一直小海龟,并给定其初始化的坐标位置。发送requext的代码是阻塞式的,需要等到response才会继续执行后续的语句。

服务模型

创建一个客户端的流程大体为:

graph TB
a(初始化节点)
a -->b(创建client实例)
b -->c(发布服务请求数据)
c -->d(等待server处理之后的应答结果)

首先,按照惯例,我们创建一个功能包:

cd ~/ros_ws/src
catkin_create_pkg learn_server roscp rospy std_msgs geometry_msgs turtlesim

在功能包的src底下创建第一个cpp文件

c++实现

代码实现

一、导入头文件,初始化节点并创建句柄。

由于本次需要使用turtlesim的spawn服务,所以需要导入对应的头文件:

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

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

二、然后就是需要客户端查询是否存在spawn服务,如果没有就会阻塞在这一句代码一直等待。

如果spawn服务出现了,就创建一个客户端,用来给spawn服务发送请求的,请求的数据类型是turtlesim::spawn

// 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
ros::service::waitForService("/spawn");
ros::ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");
//尖括号存放数据类型,后面跟上请求服务的名字

三、封装好请求数据。/spawn里需要存储以下类型的数据,初始化好预设值。

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

四、发送请求数据。使用call方法,将封装好的请求数据发送出去。

call也是阻塞性的函数,在数据发布出去之后,会一直等待服务器的反馈数据。获取到服务器的反馈之后1,才会去执行下一句代码。

由于在turtlesim成功调用服务之后,会返回这只turtle的名字,所以我们需要获取返回的turtle名。(可见ROS二:基本命令行工具

// 请求服务调用
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;

配置编译文件

在CMakeLists.txt输入以下文本,添家要编译的文件以及链接库。

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

运行结果

[ INFO] [1658379402.461341999]: Call service to spwan turtle[x:2.000000, y:2.000000, theta:2.000000,name:turtle2]
[ INFO] [1658379402.512673909]: Spwan turtle successfully [name:turtle2]

可以在turtlesim_node界面看到新建海龟成功的日志信息

[ INFO] [1658379402.512145247]: Spawning turtle [turtle2] at x=[2.000000], y=[2.000000], theta=[2.000000]

以及在图形界面看到新生成的海龟。

python实现

# -*- coding: utf-8 -*-
import rospy
import sys
from turtlesim.srv import Spawn

def turtle_client():
    rospy.init_node('turtle_client')

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

        # 请求服务调用,输入请求数据
        # 请求数据,类似cpp的call方法。response获取返回的turtle名字
        response = add_turtle(2.0, 2.0, 2.0, "turtle2") 
        return response.name
    except rospy.ServiceException, e:
        print "Service call failed: %s"%e
        return 0

if __name__ == "__main__":
    print "Spwan turtle successfully [name:%s]" %(turtle_client())

二、服务端实现

实现目标:给海龟发速度指令。服务端通过获取客户端的指令控制海龟的运动与否。

服务端实现的大致流程为:

graph TB
a(初始化节点)
a -->b(创建server)
b -->c(循环等待服务请求)
c -->d(获取到请求 进入回调函数)
d -->e(在回调函数中完成服务功能 并反馈应答数据)

c++实现

我们如果想要知道Trigger里面包含什么类型,可以用rossrv show指令查看。

rossrv show std_srvs/Trigger 
---
bool success
string message

由于service包含有request和response两个部分,三横线 "—"以上是request的结构内容,以下是response的内容。

可以看到Trigger的response结构里包含有一个布尔的flag,以及字符串类型的消息。

代码实现

导入头文件

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>    //topic数据类型
#include <std_srvs/Trigger.h>       //service数据类型

声明publisher等一些全局变量:

ros::Publisher turtle_pub;          //创建全局的publisher
bool pub_turtle_sport_flag = false; //turtle是否运动的flag标记

主函数

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

    // 自己起一个名为/turtle_command的server,注册回调函数CommandCallBack
    ros::ServiceServer command_service = node.advertiseService("/turtle_command",CommandCallBack);

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

    // 设置循环的频率
    ROS_INFO("Waiting to receive command");
    ros::Rate loop_rate(10);

然后写循环,判断标志符并根据标志发布指令

while(ros::ok){

    // 查看一次回调函数队列,看是否有服务数据传进来
    ros::spinOnce();

    // 如果标志为true,则发布速度指令
    if(pub_turtle_sport_flag)
    {
        geometry_msgs::Twist vel_msg;
        vel_msg.linear.x = 0.5;
        vel_msg.angular.z = 0.2;
        turtle_pub.publish(vel_msg);
    }

    //按照循环频率延时
    loop_rate.sleep();
}

主函数写完,写回调函数。

// service回调函数,输入参数req,输出参数res
bool CommandCallBack(std_srvs::Trigger::Request &req, std_srvs::Trigger::Response &res)
{
    pub_turtle_sport_flag = !pub_turtle_sport_flag;

    // 显示请求数据
    ROS_INFO("Publish command [%s]", pub_turtle_sport_flag==true?"Yes":"No");

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

    return true;
}

配置编译文件

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

运行结果

首先,按照惯例,运行roscorerosrun turtlesim turtlesim_node,并使用rosrun <功能包名> <可执行文件名>运行自己的代码,然后新建终端,运行以下指令,给自己的service发送空消息

rosservice call /turtle_command "{}"

每运行一次上述指令,布尔值的flag:pub_turtle_sport_flag会自己取反,控制小乌龟是否按照topic运行。

python实现

由于python中并没有上述c++代码中ros::spinOnce();的机制,只有不断循环的rospy.spin(),所以在开发商存在一定的区别。

所以我们需要使用多线程机制进行开发。

载入头文件,并初始化全局变量:

# -*- coding: utf-8 -*-
import rospy
import thread,time
from geometry_msgs.msg import Twist
from std_srvs.srv import Trigger, TriggerResponse

pubCommand = False;
turtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)

从主函数开始介绍

if __name__ == "__main__":
    turtle_server()

首先是建立第一个函数,初始化节点以及创建server。

def turtle_server():
    rospy.init_node('turtle_server_node')

    # 创建一个名为/turtle_command的server,注册回调函数CommandCallback
    ser_turtle = rospy.Service('/turtle_command', Trigger, CommandCallback)

    # 循环等待回调函数
    print "Ready to receive turtle command."

    # 创建线程。第一个是线程函数,第二个是传给这个函数的参数。
    # 由于我们不需要传参,后面的括号无需填写
    thread.start_new_thread(turtle_thread, ())
    rospy.spin()

众所周知,线程所在的起始位置是在某个函数的开头,所以为其封装个函数

def turtle_thread():
    while True:
        if pub_Command:
            vel_msg = Twist()
            vel_msg.linear.x = 0.8
            vel_msg.angular.z = 0.5
            turtle_vel_pub.publish(vel_msg)
        time.sleep(0.1)

当全局变量flag获取到反馈的时候,就需要调用回调函数。所以我们为其封装个回调函数

def CommandCallback(req):
    global pub_Command
    pub_Command = bool(1-pub_Command)

    # 显示请求
    rospy.loginfo("Publish turtle command![%d]", pub_Command)

    # 反馈数据
    return TriggerResponse(True, "Change turtle command state!")

虽然实现的效果和c++是一样的,但是注意实现方式是很有区别的,一个是封装了ros::spinOnce();,一个是使用多线程


文章作者: 拓佑豪
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 拓佑豪 !
评论
  目录