ROS专题 · 2024年3月4日 0

ROS 2基础概念#6:服务(Service)| ROS 2学习笔记

服务(Service)是 ROS 2 计算图中节点通信的另一种方法。 服务基于调用和响应模型,而不是主题的发布者-订阅者模型。 虽然主题允许节点订阅数据流并获取持续更新,但服务仅在客户端专门调用时才提供数据。

../../../_images/Service-SingleServiceClient.gif
../../../_images/Service-MultipleServiceClient.gif

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 窗口中海龟绘制的任何线条。../../../_images/clear.png

现在我们通过调用 /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:../../../_images/spawn1.png

总结

节点可以使用 ROS 2 中的服务进行通信。与主题(一种单向通信模式,其中节点发布可供一个或多个订阅者使用的信息)不同,服务是一种请求/响应模式,其中客户端向节点发出请求 提供服务,服务处理请求并生成响应。

一般不要在服务中使用连续通话的服务;这种情况下使用主题(topic)或者动作(action)会更合适。