ROS2——手把手教你编写一个服务
服务简介
上一篇文章讲的话题通信是基于订阅/发布机制的,无论有没有订阅者,发布者都会周期发布数据,这种模式适合持续数据的收发,比如传感器数据。机器人系统中还有另外一些配置性质的数据,并不需要周期处理,此时就要用到另外一种ROS通信方式——服务(Service)。服务是基于客户端/服务器模型的通信机制,服务器端只在接收到客户端请求时才会提供反馈数据。
服务有着以下特点:
下面, 我们将从一个实例出发, 手把手写一段服务程序
案例描述
来看一个案例: (以下均使用-er
表示名词, 虽然没有这个词)
- 有一些穷人
Poorer
和一个发放免费汉堡的爱心机构Organization
.
Organization
总共有一定数量的汉堡.
Poorer
可以为家人代领汉堡, 家里有n个人, 就可以领取2n个汉堡.
- 当然, 还有一些有钱人
Richer
想蹭吃蹭喝, Organization
不会给这些人发放食物
- 当汉堡发完了,
Organization
发出通知, 不再发放.
以上案例实际上就实现了一个服务, Organization
为服务端, Poorer
和Richer
为客户端
自定义服务接口
上一篇文章我们自定义了话题接口, 服务与话题不同, 服务是双向的, 所以要定义来回两个数据类型
编辑.srv文件
cd到工作空间的/src
文件夹下, 新建接口包
1 2
| cd ros2_ws/src ros2 pkg create service_interfaces --build-type ament_cmake
|
cd进入service_interfaces
文件夹, 新建Organization.srv
文件(首字母要求大写)
1 2
| mkdir srv touch srv/Organization.srv
|
编辑Organization.srv
来回两个数据中间用---
分隔开
1 2 3 4 5 6 7 8 9
| # 是穷人还是富人 string status # 家里的人数 uint32 num_of_people --- # 是否发出成功 bool success # 发出的汉堡数量 uint32 num_of_hamburger
|
修改Cmakelist.txt
注意:rosidl_generate_interfaces()
必须在 ament_package()
前
1 2 3 4 5 6 7 8 9
| find_package(rosidl_default_generators REQUIRED)
find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "srv/Calculate.srv" DEPENDENCIES )
|
修改package.xml
1 2 3 4
| <build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>
|
编译并刷新环境变量
1 2
| colcon build --packages-select service_interfaces source ~/.bashrc
|
详细查看接口
要想详细查看接口, 可以使用ros2 interface
命令, 前面的博客已经讲过, 不再赘述
服务程序实现
下面来编写这段程序
新建工作空间
1 2
| mkdir -p ros2_ws/src cd ros2_ws/src
|
mkdir -p
: 递归创建目录,即使上级目录不存在,会按目录层级自动创建目录
新建功能包
1
| ros2 pkg create poor_and_organization --build-type ament_cmake --dependencies rclcpp service_interfaces
|
使用ament_cmake
作为编译类型, 并使用依赖rclcpp
和前文定义的service_interfaces
接口包
在ros2_ws/src/poor_and_organization/src
下创建Poor.cpp
和Organization.cpp
编写服务端节点
直接献出程序, 每句都有注释, 看不懂你打我
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| #include "rclcpp/rclcpp.hpp" #include "service_interfaces/srv/calculate.hpp"
using std::placeholders::_1; using std::placeholders::_2;
class Organization : public rclcpp::Node {
public: Organization() : Node("Organization"), NumOfAll(100) { RCLCPP_INFO(this->get_logger(), "大家好, 我们是热心组织, 我们只给poorer发汉堡."); callback_group_organization = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); Organization_Server = this->create_service<service_interfaces::srv::Calculate>("Calculate", std::bind(&Organization::organization_callback,this,_1,_2), rmw_qos_profile_services_default, callback_group_organization); } private: size_t NumOfAll;
rclcpp::CallbackGroup::SharedPtr callback_group_organization;
rclcpp::Service<service_interfaces::srv::Calculate>::SharedPtr Organization_Server;
void organization_callback(const service_interfaces::srv::Calculate::Request::SharedPtr request, const service_interfaces::srv::Calculate::Response::SharedPtr response) { if(request->status == "Poorer") { RCLCPP_INFO(this->get_logger(), "收到一个来自%s的请求,他家有%d个人.", request->status.c_str(), request->num_of_people); unsigned int NumOfRequired = request->num_of_people*2; if(NumOfRequired > NumOfAll) { RCLCPP_INFO(this->get_logger(), "当前汉堡库里只剩%d个汉堡啦! 已经不够分了, 请明日再来.", NumOfRequired); response->success = false; } else { NumOfAll -= NumOfRequired; response->num_of_hamburger = NumOfRequired; response->success = true; RCLCPP_INFO(this->get_logger(), "成功送出%d个汉堡~ 还剩余%d个汉堡", NumOfRequired, NumOfAll); } } else { response->success = false; response->num_of_hamburger = 0; RCLCPP_INFO(this->get_logger(), "收到一个非法请求,这人是个%s, 不满足送汉堡资格.", request->status.c_str()); } } };
int main(int argc, char **argv) { rclcpp::init(argc, argv); auto node = std::make_shared<Organization>(); rclcpp::executors::MultiThreadedExecutor exector; exector.add_node(node); exector.spin(); rclcpp::shutdown(); return 0; }
|
编写客户端Poor节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| #include "rclcpp/rclcpp.hpp" #include "service_interfaces/srv/calculate.hpp"
using namespace std::chrono_literals;
using std::placeholders::_1;
class Poorer : public rclcpp::Node { public: Poorer() : Node("Poorer") { RCLCPP_INFO(this->get_logger(), "我是一个来领汉堡的人."); Poorer_Client = this->create_client<service_interfaces::srv::Calculate>("Calculate"); }
int take_hamburger(int argc, char **argv) { RCLCPP_INFO(this->get_logger(), "现在去领取汉堡"); auto request = std::make_shared<service_interfaces::srv::Calculate::Request>(); while (!Poorer_Client->wait_for_service(1s)) { if (!rclcpp::ok()) { RCLCPP_ERROR(this->get_logger(), "等待被打断, 不等了"); rclcpp::shutdown(); return 1; } RCLCPP_INFO(this->get_logger(), "等待热心组织开门"); }
if (argc != 3) { RCLCPP_ERROR(this->get_logger(), "输入格式错误, 格式为: 什么人 几口人. 例如: ros2 run poor_and_organization Poor_node Poorer 3"); rclcpp::shutdown(); return 1; } else { request->status = argv[1]; request->num_of_people = atoi(argv[2]); RCLCPP_INFO(this->get_logger(), "我是%s, 我家有%d个人", request->status.c_str(), request->num_of_people); }
Poorer_Client->async_send_request(request, std::bind(&Poorer::poorer_callback, this, _1));
return 0; } private: rclcpp::Client<service_interfaces::srv::Calculate>::SharedPtr Poorer_Client;
void poorer_callback(rclcpp::Client<service_interfaces::srv::Calculate>::SharedFuture response) { auto result = response.get(); if(result->success == true) { RCLCPP_INFO(this->get_logger(), "成功领取%d个汉堡", result->num_of_hamburger); rclcpp::shutdown(); } else { RCLCPP_INFO(this->get_logger(), "领取汉堡失败, 原因可能为: 1.你不是Poorer 2.汉堡不够了"); rclcpp::shutdown(); } } };
int main(int argc, char **argv) { rclcpp::init(argc, argv); auto node = std::make_shared<Poorer>(); node->take_hamburger(argc, argv); rclcpp::spin(node); rclcpp::shutdown(); return 0; }
|
Cmakelist.txt
1 2 3 4 5 6 7 8
| find_package(rclcpp REQUIRED) find_package(service_interfaces REQUIRED)
add_executable(Poor_node src/Poor.cpp) ament_target_dependencies(Poor_node rclcpp service_interfaces)
add_executable(Organization_node src/Organization.cpp) ament_target_dependencies(Organization_node rclcpp service_interfaces)
|
将编译好的文件安装到install/poor_and_organization/lib/poor_and_organization
下
1 2 3 4 5
| install(TARGETS Poor_node Organization_node DESTINATION lib/${PROJECT_NAME} )
|
package.xml
同样地, 新建功能包的时候没有加--dependencies rclcpp service_interfaces
等功能包, 则需要手动添加, 放置于<package>
标签下
1 2
| <depend>rclcpp</depend> <depend>service_interfaces</depend>
|
也可自行修改下面这些声明, 与实现功能无关, 但是最好写全
1 2 3 4
| <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="fanziqi@fanziqi.site">fanziqi</maintainer> <license>TODO: License declaration</license>
|
编译
--packages-select
指定编译customer_and_kfc
功能包
1
| colcon build --packages-select poor_and_organization
|
刷新环境
前文应该已经echo了环境, 如果没有, 请添加echo "source /ros2_ws/install/setup.zsh" >> ~/.bashrc
运行
新建一个终端窗口, 运行Organization服务端节点
1
| ros2 run poor_and_organization Organization_node
|
再另新建一个终端, 运行Poor客户端节点
第一个参数为节点名称Poor_node
, 第二个参数为人员类型Poorer
, 第三个参数为家里人数5
1
| ros2 run poor_and_organization Poor_node Poorer 5
|
此时应该可以看见:
Organization服务端: 成功发出10个汉堡

Poor客户端: 成功领取10个汉堡

也可以手动发布客户端一下
1
| ros2 service call /Calculate service_interfaces/srv/Calculate "{num_of_people: 40, status: 'Poorer'}"
|
Poor客户端:

Organization服务端:

此时如果再申请20个汉堡, 会发生什么呢?
1
| ros2 run poor_and_organization Poor_node Poorer 10
|
Poor客户端: 提示领取失败

Organization服务端: 提示不够分了

这时候有一个Richer来骗汉堡吃
1
| ros2 run poor_and_organization Poor_node Richer 1
|
Poor客户端: 提示领取失败

Organization服务端: 就算有汉堡也不分给Richer

这时, Organization关门下班了(Ctrl+C关闭Organization_Node节点)
又来了一个人想要领汉堡:
1
| ros2 run poor_and_organization Poor_node Poorer 2
|
Poor客户端: 提示等待开门, 不想等待的话直接按Ctrl+C退出

经验证, 需求全部实现~
服务常用命令
使用 ros2 service
查看服务列表
具体点
查看服务的接口
1
| ros2 service type <service_name>
|
查看接口的服务
1
| ros2 service find <type_name>
|
查看服务接口类型
1
| ros2 interface show ***/srv/***
|
手动调用服务
前文使用过, 还记得吗
1
| ros2 service call <service_name> <service_type> <arguments>
|