服务(Service)是 ROS 2 计算图中节点通信的另一种方法。 服务基于调用和响应模型,而不是主题的发布者-订阅者模型。 虽然主题允许节点订阅数据流并获取持续更新,但服务仅在客户端专门调用时才提供数据。
ROS 2服务的基本概念
ROS 2服务定义了一种一对一的通信模式。在这种模式下,一个节点可以作为服务服务器(Service Server),另一个节点可以作为服务客户端(Service Client)。服务客户端向服务服务器发起请求,并同步等待服务器的响应。
服务通信基于预先定义的服务类型进行,服务类型定义了请求和响应的结构。这些类型在ROS 2中通过.srv
文件定义,类似于消息类型是通过.msg
文件定义的。.srv
文件的每一部分分别定义了请求和响应的数据结构。
服务的类型
ROS 2服务包含如下两种类型
- 服务服务器(Service Server)
- 服务客户端(Service Client)
在ROS 2中,服务指的是远程过程调用。 换句话说,一个节点可以对另一个节点进行远程过程调用,该节点将进行计算并返回结果。
此结构反映在服务消息定义的外观中:
uint32 request
---
uint32 response
在 ROS 2 中,服务预计会快速返回,因为客户端通常正在等待结果。 服务永远不应该用于运行时间较长的进程,特别是在特殊情况下可能需要抢占的进程。 如果您的服务将进行长时间运行的计算,请考虑改用动作(action)。
服务由服务名称标识,该名称看起来很像主题名称(但位于不同的命名空间中)。
一个服务由两部分组成:服务服务器和服务客户端。
服务服务器
服务服务器是接受远程过程请求并对其执行某些计算的实体。 例如,假设 ROS 2 消息包含以下内容:
uint32 a
uint32 b
---
uint32 sum
服务服务器将是接收此消息、将 a 和 b 加在一起并返回总和的实体。
注意:
每个服务名称只能有一个服务服务器。 当多个服务服务器具有相同的服务名称时,未定义哪个服务服务器将接收客户端请求。
服务客户端
服务客户端是请求远程服务服务器代表其执行计算的实体。 从上面的示例来看,服务客户端是创建包含 a 和 b 的初始消息的实体,并等待服务服务器计算总和并返回结果。
与服务服务器不同,可以有任意数量的服务客户端使用相同的服务名称。
服务(Service)描述规范
服务在 ROS 包的 srv/ 目录中的 .srv 文件中进行描述和定义。服务描述文件由请求和响应消息类型组成,以—分隔。 任何两个以 — 连接的 .msg 文件都是合法的服务说明。
下面一个非常简单的服务示例,它接收一个字符串并返回一个字符串:
string str
---
string str
当然,也可能变得更复杂(如果您想引用同一包中的消息,则不得提及包名称):
#request constants
int8 FOO=1
int8 BAR=2
#request fields
int8 foobar
another_pkg/AnotherMessage msg
---
#response constants
uint32 SECRET=123456
#response fields
another_pkg/YetAnotherMessage val
CustomMessageDefinedInThisPackage value
uint32 an_integer
你也可以把一个服务嵌套进另外一个服务中。
服务实例分析
假设我们正在开发一个机器人应用,在这个应用中,机器人需要从一个传感器服务中获取温度读数。我们可以定义一个GetTemperature.srv
服务,其请求部分为空(不需要输入参数),响应部分包含一个浮点数表示的温度值。
定义.srv
文件
# GetTemperature.srv
float64 temperature
服务服务器
服务服务器的回调函数接收到请求后,会从传感器获取温度读数,并将这个值作为响应返回给客户端。
include "rclcpp/rclcpp.hpp"
include "example_interfaces/srv/get_temperature.hpp"
void handle_temperature_request(
const std::shared_ptr request,
std::shared_ptr response)
{
response->temperature = read_sensor_temperature(); // 假设这个函数从传感器读取温度
RCLCPP_INFO(rclcpp::get_logger("server"), "Sending back response: [%f]", response->temperature);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("temperature_sensor_server");
auto server = node->create_service("get_temperature", handle_temperature_request);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
服务客户端
客户端发起请求时不需要提供任何数据,它只需要等待温度读数的响应。
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/get_temperature.hpp"
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("temperature_sensor_client");
auto client = node->create_client<example_interfaces::srv::GetTemperature>("get_temperature");
auto request = std::make_shared<example_interfaces::srv::GetTemperature::Request>();
auto future = client->async_send_request(request);
// 等待响应
if (rclcpp::spin_until_future_complete(node, future)
ROS 2服务命令行工具
在使用如下的命令行工具前,先启动两个 turtlesim 节点: /turtlesim
和 /teleop_turtle
。
打开一个 Terminal 并运行:
ros2 run turtlesim turtlesim_node
打开另外一个 Terminal 并运行:
ros2 run turtlesim turtle_teleop_key
列举所有的ROS 2服务
在新终端中运行如下命令将返回系统中当前活动的所有服务的列表:
ros2 service list
将得到类似如下的结果:
/clear
/kill
/reset
/spawn
/teleop_turtle/describe_parameters
/teleop_turtle/get_parameter_types
/teleop_turtle/get_parameters
/teleop_turtle/list_parameters
/teleop_turtle/set_parameters
/teleop_turtle/set_parameters_atomically
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/describe_parameters
/turtlesim/get_parameter_types
/turtlesim/get_parameters
/turtlesim/list_parameters
/turtlesim/set_parameters
/turtlesim/set_parameters_atomically
您将看到两个节点都有相同的六个服务,其名称中包含参数。 ROS 2 中的几乎每个节点都有这些参数构建的基础设施服务。现在,让我们关注turtlesim特定的服务:/clear, /kill, /reset, /spawn, /turtle1/set_pen, /turtle1/teleport_absolute和/turtle1/teleport_relative。
查看服务类型
服务具有描述服务的请求和响应数据的结构的类型。 服务类型的定义与主题类型类似,不同之处在于服务类型有两部分:一个用于请求的消息,另一个用于响应。
要找出服务的类型,请使用以下命令:
ros2 service type <service_name>
我们来看看turtlesim的/clear服务。 在新终端中输入命令:
ros2 service type /clear
将看到如下的结果:
std_srvs/srv/Empty
Empty 类型表示服务调用在发出请求时不发送任何数据,在接收响应时不接收任何数据。
查看所有服务类型
要同时查看所有活动服务的类型,您可以将 –show-types 选项(缩写为 -t)附加到 list 命令:
ros2 service list -t
将看到如下的结果:
/clear [std_srvs/srv/Empty]
/kill [turtlesim/srv/Kill]
/reset [std_srvs/srv/Empty]
/spawn [turtlesim/srv/Spawn]
...
/turtle1/set_pen [turtlesim/srv/SetPen]
/turtle1/teleport_absolute [turtlesim/srv/TeleportAbsolute]
/turtle1/teleport_relative [turtlesim/srv/TeleportRelative]
...
寻找服务
如果要查找特定类型的所有服务,可以使用以下命令:
ros2 service find <type_name>
例如,您可以像这样找到所有 Empty 类型服务:
ros2 service find std_srvs/srv/Empty
将看到如下的结果:
/clear
/reset
查看服务接口参数
ros2 interface show <type_name>.srv
在 /clear 服务的 Empty 类型上尝试此操作:
ros2 interface show std_srvs/srv/Empty.srv
将看到如下的结果:
---
— 将请求结构与响应结构分开。 但是空类型不会发送或接收任何数据。 所以它的结构是空白的。
让我们检查一下具有发送和接收数据类型的服务,例如 /spawn。 从 ros2 service list -t 的结果我们知道/spawn的类型是turtlesim/srv/Spawn。
要查看 /spawn 服务的请求和响应参数,请运行以下命令:
ros2 interface show turtlesim/srv/Spawn
将得到如下返回结果:
float32 x
float32 y
float32 theta
string name # Optional. A unique name will be created and returned if this is empty
---
string name
— 行上方的信息告诉我们调用 /spawn 所需的参数。 x、y 和 theta 确定生成的Turtle的 2D 姿势,并且名称显然是可选的。
在这种情况下,您不需要了解该行下方的信息,但它可以帮助您了解从调用中获得的响应的数据类型。
调用ROS 2服务
现在您已经知道什么是服务类型、如何查找服务类型以及如何查找该类型参数的结构,您可以使用以下方式调用服务:
ros2 service call <service_name> <service_type> <arguments>
部分是可选的。 例如,空类型服务没有任何参数:
ros2 service call /clear std_srvs/srv/Empty
此命令将清除turtlesim 窗口中海龟绘制的任何线条。
现在我们通过调用 /spawn 并设置参数来生成新的 turtle。 从命令行进行的服务调用中的输入需要采用 YAML 语法。
ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: ''}"
您将获得正在发生的情况的方法风格视图,然后是服务响应:
requester: making request: turtlesim.srv.Spawn_Request(x=2.0, y=2.0, theta=0.2, name='')
response:
turtlesim.srv.Spawn_Response(name='turtle2')
你的turtlesim窗口将立即更新为新生成的 turtle:
总结
节点可以使用 ROS 2 中的服务进行通信。与主题(一种单向通信模式,其中节点发布可供一个或多个订阅者使用的信息)不同,服务是一种请求/响应模式,其中客户端向节点发出请求 提供服务,服务处理请求并生成响应。
一般不要在服务中使用连续通话的服务;这种情况下使用主题(topic)或者动作(action)会更合适。