本篇博客是慕课《ROS机器人操作系统入门》的第三篇笔记,慕课网址是这里。本篇笔记主要记录Service在C++和Python下的两种实现方式以及需要注意的问题。
1.任务描述与分析
任务是在之前利用WebSocket实现过的模拟登录功能。用户在客户端输入用户名和密码并发送给服务器端,服务器端收到用户名和密码后将其与正确的进行比较,若一致返回登录成功,否则失败。
针对这种需求很明显使用Service-Client通信方式最为合适。具体而言,可以新建一个叫login
的包(package
),这个包中有server_login
和client_login
两个节点(Node
),server_login
用于处理用户发送的用户名和密码,并返回登录结果,
为了方便,定义用户名是xuhui
,密码是123456
,这样才能登录成功。
client_login
用于客户端发送用户名和密码。定义Service
的名称是login_info
,Service
消息的文件名是user.srv
,格式是string
类型的username
、password
以及返回string
类型的登录状态state
。
2.Service的C++实现
以下步骤是在创建好了工作空间(本机工作空间路径/root/catkin_ws
)的情况下继续的,如果还没有创建好工作空间,可以参考这篇笔记创建工作空间,然后再回来。
(1)创建Package
创建Package
有多种方式,这里介绍最原始的手动和使用RoboWare这个IDE的两种不同方式。
a.终端中手动创建
在终端中输入如下内容即可创建一个Package
。
cd catkin_ws/src/
catkin_create_pkg login roscpp std_msgs message_generation message_runtime
第一行是切换到src
目录下,第二行是在当前目录下创建一个叫login
的包,并且包含roscpp
、std_msgs
、message_generation
、message_runtime
依赖,后两个是用到Service
的包都要包含的依赖。
b.RoboWare自动创建
打开RoboWare,在”资源管理器”中找到src
文件夹,右键单击选择”新建ROS包”,输入名称即可创建Package
。创建完成后右键Package
选择”编辑依赖的ROS包列表”,输入roscpp
、std_msgs
、message_generation
、message_runtime
(空格隔开)即可添加依赖。
(2)创建自定义消息文件
a.手动创建文件
在login
包的根目录下新建一个srv
文件夹,在其中新建一个叫做user.srv
的文件即可。
b.RoboWare创建文件
在”资源管理器”中找到login
文件夹,右键单击选择”新建Srv文件夹”,在新建的srv
文件夹上右键选择”新建SRV文件”,输入名称即可创建。
以上两种方式文件创建好后在文件中写入如下内容并保存。
string username
string password
---
string state
需要注意的是Service
的基本数据类型和常规的C++有些区别。例如在Service
中int
进一步细分成了int6
、int32
、int64
,float
细分成了float32
和float64
。所以在Service
文件中如果写int
是会报错的。
(3)编译自定义消息文件
a.手动方式
在编译自定义消息文件为头文件之前,需要修改login
包的CMakeLists.txt
文件,有以下需要修改的地方。
首先在自动生成的CMakeLists.txt
文件中找到被注释掉的add_service_files(FILES Service1.srv)
,将其取消注释,并将自定义的srv
文件名称填入,这行命令用于向项目中添加刚刚自定义的服务文件。
然后找到被注释掉的generate_messages(DEPENDENCIES std_msgs)
将其取消注释,这行命令用于生成自定义的消息文件。
修改后的文件如下。
b.RoboWare方式
如果是在RoboWare里通过菜单新建的srv
文件则无需修改,RoboWare已经自动修改好了CMakeLists.txt
中的对应内容。
以上两种方式完成了CMake文件的修改之后即可以调用catkin_make
进行编译了。在工作空间的根目录下catkin_make
即可。之后我们可以在/root/catkin_ws/devel/include/login
找到生成的三个user.h
、userRequest.h
、userResponse.h
头文件了,后续调用它即可。
(4)创建server_login节点
a.手动方法
在包的根目录下新建一个src
文件夹,然后在其中新建一个server.cpp
文件,并在刚刚修改的CMakeLists.txt
文件的末尾添加如下内容,用于添加可执行文件:
add_executable(server_login
src/server.cpp
)
add_dependencies(server_login ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(server_login
${catkin_LIBRARIES}
)
b.RoboWare方法
除了手动添加,也可以在RoboWare中自动添加。具体做法是在RoboWare的资源管理器中找到login
,右键选择”新建Src文件夹”,在src
文件夹上右键,然后选择”新建CPP源文件”,输入名称为server_login.cpp
并回车,然后会弹出一个选择的对话框如下。
这是让你指定新建的这个cpp
文件是作为一个库文件还是作为一个可执行文件,选择不同选项后,程序会自动帮你在CMakeLists.txt
文件中填好相关内容。当然,如果不选也可以,自己手动填写就可以了。这里由于我们是希望生成一个可执行文件的,所以选择”加入到新的可执行文件中”即可。
(5)编写server_login节点代码
对于一个Service的服务端,需要以下几个步骤:初始化ROS节点(ros::init()
)、新建NodeHandle(ros::NodeHandle
)、定义回调函数、构造ServiceServer
(nh.advertiseService()
)、循环执行(ros::spin()
)。具体代码如下:
# include<ros/ros.h>
# include<login/user.h>
// 用于处理的回调函数,回调函数需要引用形式传入Request和Response,Response为返回数据
bool handle_function(login::user::Request &req,login::user::Response &res){
ROS_INFO("Request from %s with password %s",req.username.c_str(),req.password.c_str());
// 构建正确的登录信息
std::string name = "xuhui";
std::string passwd = "123456";
if(name == req.username and passwd == req.password){
res.state = "\nLogin success!\nWelcome my master " + req.username + ":)";
}else{
res.state = "\nLogin fail!\nYou are not my master. -_-";
}
return true;
}
int main(int argc, char *argv[])
{
// 初始化节点,指定节点名称
ros::init(argc, argv, "server_login");
// 创建节点句柄,用于控制节点
ros::NodeHandle nh;
// 创建服务端,并指定服务名称
ros::ServiceServer service = nh.advertiseService("login_info",handle_function);
// 循环执行
ros::spin();
return 0;
}
(6)创建client_login节点
a.手动方法
在src
文件夹中新建一个client.cpp
文件,并在CMakeLists.txt
文件末尾添加如下内容,用于添加可执行文件:
add_executable(client_login
src/client.cpp
)
add_dependencies(client_login ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(client_login
${catkin_LIBRARIES}
)
b.RoboWare方法
除了手动添加,也可以在RoboWare中自动添加。具体做法是在RoboWare的资源管理器中找到login
,在src
文件夹上右键,然后选择”新建CPP源文件”,输入名称为client_login.cpp
并回车。选择”加入到新的可执行文件中”即可。
(7)编写client_login节点代码
对于一个Service的客户端,需要以下几个步骤:初始化ROS节点(ros::init()
)、新建NodeHandle
(ros::NodeHandle
)、构造ServiceClient
(nh.serviceClient()
)、发送消息(client.call()
)。具体代码如下:
# include<ros/ros.h>
# include<login/user.h>
int main(int argc, char *argv[])
{
// 初始化节点并指定节点名称
ros::init(argc, argv, "client_login");
// 创建句柄
ros::NodeHandle nh;
// 创建客户端并指定其发送的服务端名称
ros::ServiceClient client = nh.serviceClient<login::user>("login_info");
// 获取信息
std::string name;
std::string passwd;
std::cout<<"Input username:"<<std::endl;
std::cin>>name;
std::cout<<"Input password:"<<std::endl;
std::cin>>passwd;
// 新建一个消息,传入内容
login::user info;
info.request.username = name;
info.request.password = passwd;
// 注意这里call函数返回的就是在服务端定义的回调函数的返回值(true or false)
if (client.call(info))
{
ROS_INFO("%s",info.response.state.c_str());
}else{
ROS_ERROR("Failed to call service");
}
return 0;
}
(8)编译运行测试
最后在Catkin工作空间根目录下打开终端,使用catkin_make
进行编译,编译成功后如下:
然后先输入roscore
打开Master
,然后在新终端中输入rosrun login server_login
启动server_login
节点,新终端中输入rosrun login client_login
启动client_login
节点。如下:
可以看到成功运行了刚刚写的两个节点,并且实现了服务消息的收发与处理。最后简单说一下,生成的两个节点的可执行文件放在了/root/catkin_ws/devel/lib/login
里,直接在终端中运行这个可执行文件也是可以的(Master
已经运行的情况下)。
3.Topic的Python实现
(1)新建Package
a.手动方法
在终端中输入如下内容即可创建一个Package。
cd catkin_ws/src/
catkin_create_pkg login rospy std_msgs message_generation message_runtime
第一行是切换到src
目录下,第二行是在当前目录下创建一个叫login
的包,并且包含rospy
、std_msgs
、message_generation
、message_runtime
依赖,后两个是用到Service
的包都要包含的依赖。
b.RoboWare方法
打开RoboWare,在”资源管理器”中找到src
文件夹,右键单击选择”新建ROS包”,输入名称即可创建Package
。创建完成后右键Package
选择”编辑依赖的ROS包列表”,输入rospy
、std_msgs
、message_generation
、message_runtime
(空格隔开)即可添加依赖。
(2)新建消息文件
与前面自定义消息文件内容步骤一模一样。
(3)编译消息文件
与前面编译自定义消息文件内容步骤一模一样。
(4)新建server_login节点文件
根据之前的习惯,src
文件夹里一般放C/C++文件,Python代码一般放在scripts
文件夹里。
a.手动方法
在package
的根目录下创建一个scripts
文件夹,然后新建一个server_login.py
文件。由于Python是脚本,并不需要CMake编译,因此创建好文件就可以写代码了。
b.RoboWare方法
在包名上右键点击,选择”新建文件夹”,输入名称为scripts
,在scripts
文件夹上右键选择”新建文件”,输入server_login.py
即可。
(5)编写server_login代码
参考之前笔记说过的rospy
的API,代码如下。
# coding=utf-8
# 注意service模块的加载方式,from 包名.srv import *
# 其中srv指的是在包根目录下的srv文件夹,也即srv模块
import rospy
from login.srv import *
def handle_function(req):
# 回调函数传入的是user类型的请求,它的返回值直接就是userResponse类型的结果
rospy.loginfo("Request from %s with password %s",
req.username, req.password)
if req.username == "xuhui" and req.password == "123456":
return userResponse("\nLogin success!\nWelcome my master "+req.username+":)")
else:
return userResponse("\nLogin fail!\nYou are not my master. -_-")
def server_login():
# 初始化节点
rospy.init_node("server_login")
# 构造服务,指定服务名称,消息类型以及回调函数
ser = rospy.Service("login_info", user, handle_function)
rospy.loginfo("Ready to handle the request:")
# 循环执行
rospy.spin()
if __name__ == "__main__":
server_login()
(6)新建client_login节点文件
a.手动方法
在scripts
文件夹下创建client_login.py
即可。
b.RoboWare方法
在scripts
文件夹上右键选择”新建文件”,输入client_login.py
即可。
(7)编写client_login代码
Python代码如下:
# coding=utf-8
import rospy
from login.srv import *
def client_login():
# 初始化节点,指定名称
rospy.init_node("client_login")
# 等待指定的可用服务(阻塞)
rospy.wait_for_service("login_info")
# 输入信息
name = raw_input("Input username:\n")
passwd = raw_input("Input password:\n")
try:
# 构造客户端,指定服务端名称和传输的消息类型
client = rospy.ServiceProxy("login_info", user)
# 客户端发送消息,并返回结果
resp = client.call(name, passwd)
rospy.loginfo("%s", resp.state)
except rospy.ServiceException, e:
rospy.logwarn("Service call failed:%s" % e)
if __name__ == "__main__":
client_login()
(8)运行测试
最后在终端中输入roscore
启动Master
,在scripts
目录下打开新终端然后分别输入python server_login.py
启动服务节点,python client_login.py
启动客户节点。运行结果如下:
4.Service的C++与Python实现比较
通过上面的介绍可以看到,ROS中Service的使用总体而言分为四步:新建包、自定义Service消息、编写代码以及编译运行。而且它和上一篇中介绍的Topic使用方法也十分相似。
对于C++和Python在实现起来整体步骤基本是相同的,只是由于API差异导致在代码编写这一步差别较大。其它步骤相同或有细微差别,如由于Python不需要编译因此没有catkin_make
环节。具体对比如下表:
步骤 | C++ | Python |
---|---|---|
创建Package | 依赖roscpp |
依赖rospy |
创建消息文件 | 相同 | 相同 |
编译消息文件 | 步骤相同,结果不同。生成xxx.h 、xxxRequest.h 、xxxResponse.h 三个头文件,放在catkin_ws/devel/include/pkg_name |
步骤相同,结果不同。生成.py 文件,放在catkin_ws/devel/lib/python2.7/dist-packages/pkg_name |
创建server_login节点 | 在src 下 |
在scripts 下 |
编写server_login节点代码 | 使用C++ API | 使用Python API |
创建client_login节点 | 在src 下 |
在scripts 下 |
编写client_login节点代码 | 使用C++ API | 使用Python API |
运行测试 | 运行前需要调用catkin_make 编译 |
无需编译直接运行脚本 |
如果一个项目中既会用到C++也会用到Python,那么在一开始新建Package
的时候就同时依赖roscpp
和rospy
,这样方便一些。此外在写代码的过程中注意区分Service名称、Service中传输的数据类型名称以及Service文件的名称。对于节点需要注意的是节点名称、CPP文件名称、以及CMake生成可执行文件名称的区分。
最后将所有代码上传到了Github,点击查看。
本文作者原创,未经许可不得转载,谢谢配合