本系列文章是与大家分享一下,我在学习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;
};
(2)在main函数里第一步就是进行节点的初始化,命名为turtle_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;
}
(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),我们发现编译生成了三个头文件
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;
}
11、接下来是在CMakeLists.txt中配置编译规则,add_executable将我们的cpp文件,编译成可执行文件,target_link_libraries将可执行文件与库进行链接,add_dependencies跟动态生成的头文件做依赖
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