Tags: ROS

本篇博客是慕课《ROS机器人操作系统入门》的第三篇笔记,慕课网址是这里。本篇笔记主要记录Service在C++和Python下的两种实现方式以及需要注意的问题。

1.任务描述与分析

任务是在之前利用WebSocket实现过的模拟登录功能。用户在客户端输入用户名和密码并发送给服务器端,服务器端收到用户名和密码后将其与正确的进行比较,若一致返回登录成功,否则失败。

针对这种需求很明显使用Service-Client通信方式最为合适。具体而言,可以新建一个叫login的包(package),这个包中有server_loginclient_login两个节点(Node),server_login用于处理用户发送的用户名和密码,并返回登录结果, 为了方便,定义用户名是xuhui,密码是123456,这样才能登录成功。 client_login用于客户端发送用户名和密码。定义Service的名称是login_infoService消息的文件名是user.srv,格式是string类型的usernamepassword以及返回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的包,并且包含roscppstd_msgsmessage_generationmessage_runtime依赖,后两个是用到Service的包都要包含的依赖。

b.RoboWare自动创建

打开RoboWare,在”资源管理器”中找到src文件夹,右键单击选择”新建ROS包”,输入名称即可创建Package。创建完成后右键Package选择”编辑依赖的ROS包列表”,输入roscppstd_msgsmessage_generationmessage_runtime(空格隔开)即可添加依赖。

(2)创建自定义消息文件
a.手动创建文件

login包的根目录下新建一个srv文件夹,在其中新建一个叫做user.srv的文件即可。

b.RoboWare创建文件

在”资源管理器”中找到login文件夹,右键单击选择”新建Srv文件夹”,在新建的srv文件夹上右键选择”新建SRV文件”,输入名称即可创建。

以上两种方式文件创建好后在文件中写入如下内容并保存。

string username
string password
---
string state

需要注意的是Service的基本数据类型和常规的C++有些区别。例如在Serviceint进一步细分成了int6int32int64float细分成了float32float64。所以在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.huserRequest.huserResponse.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的包,并且包含rospystd_msgsmessage_generationmessage_runtime依赖,后两个是用到Service的包都要包含的依赖。

b.RoboWare方法

打开RoboWare,在”资源管理器”中找到src文件夹,右键单击选择”新建ROS包”,输入名称即可创建Package。创建完成后右键Package选择”编辑依赖的ROS包列表”,输入rospystd_msgsmessage_generationmessage_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.hxxxRequest.hxxxResponse.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的时候就同时依赖roscpprospy,这样方便一些。此外在写代码的过程中注意区分Service名称、Service中传输的数据类型名称以及Service文件的名称。对于节点需要注意的是节点名称、CPP文件名称、以及CMake生成可执行文件名称的区分。

最后将所有代码上传到了Github,点击查看

本文作者原创,未经许可不得转载,谢谢配合

返回顶部