摘要
ROS 机器人操作系统在机器人应用领域很流行, 依托代码开源和模块间协作等特性, 给机器人开发者带来了很大的方便. 我们的机器人 "miiboo" 中的大部分程序也采用 ROS 进行开发, 所以本文就重点对 ROS 基础知识进行详细的讲解, 给不熟悉 ROS 的朋友起到一个抛砖引玉的作用. 本章节主要内容:
1.ROS 是什么
2.ROS 系统整体架构
3. 在 ubuntu16.04 中安装 ROS kinetic
4. 如何编写 ROS 的第一个程序 hello_world
5. 编写简单的消息发布器和订阅器
6. 编写简单的 service 和 client
7. 理解 tf 的原理
8. 理解 roslaunch 在大型项目中的作用
9. 熟练使用 rviz
10. 在实际机器人上运行 ROS 高级功能预览
6. 编写简单的 service 和 client
上一节介绍了两个 ros 节点通过发布与订阅消息的通信方式, 现在就介绍 ros 节点间通信的另外一种方式服务. 我们将学到: 如何自定义服务类型, server 端节点编写, client 端节点编写等. 我就以实现两个整数求和为例, client 端节点向 server 端节点发送 a,b 的请求, server 端节点返回响应 sum=a+b 给 client 端节点, 通信网络结构如图 20.
(图 20)服务请求与响应 ROS 通信网络结构图
(1)功能包的创建
在 catkin_ws/src / 目录下新建功能包 service_example, 并在创建时显式的指明依赖 roscpp 和 std_msgs, 依赖 std_msgs 将作为基本数据类型用于定义我们的服务类型. 打开命令行终端, 输入命令:
- cd ~/catkin_ws/src/
- # 创建功能包 service_example 时, 显式的指明依赖 roscpp 和 std_msgs,
- # 依赖会被默认写到功能包的 CMakeLists.txt 和 package.xml 中
- catkin_create_pkg service_example roscpp std_msgs
(2)在功能包中创建自定义服务类型
通过前面的学习, 我们知道服务通信过程中服务的数据类型需要用户自己定义, 与消息不同, 节点并不提供标准服务类型. 服务类型的定义文件都是以 *.srv 为扩展名, 并且被放在功能包的 srv / 文件夹下.
服务类型定义:
首先, 在功能包 service_example 目录下新建 srv 目录, 然后在 service_example/srv / 目录中创建 AddTwoInts.srv 文件, 文件内容如下:
- int64 a
- int64 b
- ---
- int64 sum
服务类型编译配置:
定义好我们的服务类型后, 要想让该服务类型能在 c++,python 等代码中被使用, 必须要做相应的编译与运行配置. 编译依赖 message_generation, 运行依赖 message_runtime.
打开功能包中的 CMakeLists.txt 文件, 找到下面这段代码:
- find_package(catkin REQUIRED COMPONENTS
- roscpp
- std_msgs
- )
将 message_generation 添加进去, 添加后的代码如下:
- find_package(catkin REQUIRED COMPONENTS
- roscpp
- std_msgs
- message_generation
- )
继续, 找到这段代码:
- # add_service_files(
- # FILES
- # Service1.srv
- # Service2.srv
- # )
去掉这段代码的 #注释, 将自己的服务类型定义文件 AddTwoInts.srv 填入, 修改好后的代码如下:
- add_service_files(
- FILES
- AddTwoInts.srv
- )
继续, 找到这段代码:
- # generate_messages(
- # DEPENDENCIES
- # std_msgs
- # )
去掉这段代码的 #注释, generate_messages 的作用是自动创建我们自定义的消息类型 *.msg 与服务类型 *.srv 相对应的 *.h, 由于我们定义的服务类型使用了 std_msgs 中的 int64 基本类型, 所以必须向 generate_messages 指明该依赖, 修改好后的代码如下:
- generate_messages(
- DEPENDENCIES
- std_msgs
- )
然后打开功能包中的 package.xml 文件, 填入下面三句依赖:
- <build_depend>
- message_generation
- </build_depend>
- <build_export_depend>
- message_generation
- </build_export_depend>
- <exec_depend>
- message_runtime
- </exec_depend>
检查 ROS 是否识别新建的服务类型:
我们通过 <功能包名 / 服务类型名> 找到该服务, 打开命令行终端, 输入命令:
- source ~/catkin_ws/devel/setup.bash
- rossrv show service_example/AddTwoInts
看到下面的输出, 如图 21, 就说明新建服务类型能被 ROS 识别, 新建服务类型成功了.
(图 21)新建服务类型能被 ROS 识别
(3)功能包的源代码编写
功能包中需要编写两个独立可执行的节点, 一个节点用来作为 client 端发起请求, 另一个节点用来作为 server 端响应请求, 所以需要在新建的功能包 service_example/src / 目录下新建两个文件 server_node.cpp 和 client_node.cpp, 并将下面的代码分别填入.
首先, 介绍 server 节点 server_node.cpp, 代码内容如下:
- #include "ros/ros.h"
- #include "service_example/AddTwoInts.h"
- bool add_execute(service_example::AddTwoInts::Request &req,
- service_example::AddTwoInts::Response &res)
- {
- res.sum = req.a + req.b;
- ROS_INFO("recieve request: a=%ld,b=%ld",(long int)req.a,(long int)req.b);
- ROS_INFO("send response: sum=%ld",(long int)res.sum);
- return true;
- }
- int main(int argc,char **argv)
- {
- ros::init(argc,argv,"server_node");
- ros::NodeHandle nh;
- ros::ServiceServer service = nh.advertiseService("add_two_ints",add_execute);
- ROS_INFO("service is ready!!!");
- ros::spin();
- return 0;
- }
对 server 节点代码进行解析.
- #include "ros/ros.h"
- #include "service_example/AddTwoInts.h"
包含头文件 ros/ros.h, 这是 ROS 提供的 C++ 客户端库, 是必须包含的头文件, 就不多说了. service_example/AddTwoInts.h 是由编译系统自动根据我们的功能包和在功能包下创建的 *.srv 文件生成的对应的头文件, 包含这个头文件, 程序中就可以使用我们自定义服务的数据类型了.
bool add_execute(...)
这个函数实现两个 int64 整数求和的服务, 两个 int64 值从 request 获取, 返回求和结果装入 response 里, request 与 response 的具体数据类型都在前面创建的 *.srv 文件中被定义, 这个函数返回值为 bool 型.
- ros::init(argc,argv,"server_node");
- ros::NodeHandle nh;
初始化 ros 节点并指明节点的名称, 声明一个 ros 节点的句柄,, 就不多说了.
ros::ServiceServer service = nh.advertiseService("add_two_ints",add_execute);
这一句是创建服务, 并将服务加入到 ROS 网络中, 并且这个服务在 ROS 网络中以名称 add_two_ints 唯一标识, 以便于其他节点通过服务名称进行请求.
ros::spin();
这一句话让程序进入自循环的挂起状态, 从而让程序以最好的效率接收客户端的请求并调用回调函数, 就不多说了.
接着, 介绍 client 节点 client_node.cpp, 代码内容如下:
- #include "ros/ros.h"
- #include "service_example/AddTwoInts.h"
- #include <iostream>
- int main(int argc,char **argv)
- {
- ros::init(argc,argv,"client_node");
- ros::NodeHandle nh;
- ros::ServiceClient client =
- nh.serviceClient<service_example::AddTwoInts>("add_two_ints");
- service_example::AddTwoInts srv;
- while(ros::ok())
- {
- long int a_in,b_in;
- std::cout<<"please input a and b:";
- std::cin>>a_in>>b_in;
- srv.request.a = a_in;
- srv.request.b = b_in;
- if(client.call(srv))
- {
- ROS_INFO("sum=%ld",(long int)srv.response.sum);
- }
- else
- {
- ROS_INFO("failed to call service add_two_ints");
- }
- }
- return 0;
- }
对 client 节点代码进行解析.
之前解释过的类似的代码就不做过多的解释了, 这里重点解释一下前面没遇到过的代码.
- ros::ServiceClient client =
- nh.serviceClient<service_example::AddTwoInts>("add_two_ints");
这一句创建 client 对象, 用来向 ROS 网络中名称叫 add_two_ints 的 service 发起请求.
service_example::AddTwoInts srv;
定义了一个 service_example::AddTwoInts 服务类型的对象, 该对象中的成员正是我们在 *.srv 文件中定义的 a,b,sum, 我们将待请求的数据填充到数据成员 a,b, 请求成功后返回结果会被自动填充到数据成员 sum 中.
if(client.call(srv)){...}
这一句便是通过 client 的方法 call 来向 service 发起请求, 请求传入的参数 srv 在上面已经介绍过了.
(4)功能包的编译配置及编译
创建功能包 service_example 时, 显式的指明依赖 roscpp 和 std_msgs, 依赖会被默认写到功能包的 CMakeLists.txt 和 package.xml 中, 并且在功能包中创建 *.srv 服务类型时已经对服务的编译与运行做了相关配置, 所以只需要在 CMakeLists.txt 文件的末尾行加入以下几句用于声明可执行文件就可以了:
- add_executable(server_node src/server_node.cpp)
- target_link_libraries(server_node ${
- catkin_LIBRARIES
- })
- add_dependencies(server_node service_example_gencpp)
- add_executable(client_node src/client_node.cpp)
- target_link_libraries(client_node ${
- catkin_LIBRARIES
- })
- add_dependencies(client_node service_example_gencpp)
这里面的 add_executable 用于声明可执行文件. target_link_libraries 用于声明可执行文件创建时需要链接的库. add_dependencies 用于声明可执行文件的依赖项, 由于我们自定义了 *.srv,service_example_gencpp 的作用是让编译系统自动根据我们的功能包和在功能包下创建的 *.srv 文件生成的对应的头文件和库文件, service_example_gencpp 这个名称是由功能包名称 service_example 加上_gencpp 后缀而来的, 后缀很好理解: 生成 c++ 文件就是_gencpp,
生成 python 文件就是_genpy.
接下来, 就可以用下面的命令对功能包进行编译了:
- cd ~/catkin_ws/
- catkin_make -DCATKIN_WHITELIST_PACKAGES="service_example"
(5)功能包的启动运行
首先, 需要用 roscore 命令来启动 ROS 节点管理器, ROS 节点管理器是所有节点运行的基础.
打开命令行终端, 输入命令:
roscore
然后, 用 source devel/setup.bash 激活 catkin_ws 工作空间, 用 rosrun <package_name> <node_name > 启动功能包中的 server 节点.
再打开一个命令行终端, 分别输入命令:
- cd ~/catkin_ws/
- source devel/setup.bash
- rosrun service_example server_node
看到有输出 "servive is ready!!!", 就说明 server 节点已经正常启动并开始等待 client 节点向自己发起请求了, 如图 22.
(图 22)server 节点已经正常启动
最后, 用 source devel/setup.bash 激活 catkin_ws 工作空间, 用 rosrun <package_name> <node_name > 启动功能包中的 client 节点.
再打开一个命令行终端, 分别输入命令:
- cd ~/catkin_ws/
- source devel/setup.bash
- rosrun service_example client_node
看到有输出提示信息 "please input a and b:" 后, 键盘键入两个整数, 以空格分割, 输入完毕后回车. 如果看到输出信息 "sum=xxx", 就说明 client 节点向 server 端发起的请求得到了响应, 打印出来的 sum 就是响应结果, 这样就完成了一次服务请求的通信过程, 如图 23.
(图 23)client 节点已经正常启动
到这里, 我们编写的 server 和 client 就大功告成了, 为了加深对整个程序工作流程的理解, 我再把 server 与 client 的 ROS 通信网络结构图拿出来, 加深一下理解.
(图 24)服务请求与响应 ROS 通信网络结构图
来源: https://www.cnblogs.com/hiram-zhang/p/10390922.html