Сервисы: запрос и ответ

Не всё в роботе — потоки данных; иногда нужен разовый вопрос с ответом. Для этого есть сервисы.

Сервис (service) — это связь «запрос → ответ» (request/response): узел-клиент посылает запрос, узел-сервер обрабатывает его и возвращает результат.

Когда топиков недостаточно

Топики идеальны для непрерывных потоков: лидар без устали шлёт сканы, и никто не «отвечает». Но бывают задачи другого рода — разовые команды с подтверждением. «Включи свет на роботе и скажи, получилось ли». «Сбрось одометрию в ноль». «Сделай снимок и верни путь к файлу». Здесь нужен ответ. Публиковать такое в топик неудобно: непонятно, выполнилось ли, и нет обратной связи. Это работа для сервиса.

ТопикСервис
поток, многие-ко-многимразовый вызов, один-к-одному
без ответаобязателен ответ
данные датчиковкоманды, запросы состояния

Файлы .srv

Структуру сервиса описывают в файле .srv. Он состоит из двух частей, разделённых ---: сверху — поля запроса, снизу — поля ответа.

# example_interfaces/srv/SetBool
bool data        # запрос: включить (true) / выключить (false)
---
bool success     # ответ: получилось ли
string message   # ответ: пояснение

Вызов сервиса из терминала

# список сервисов
ros2 service list

# вызвать сервис вручную
ros2 service call /enable_light example_interfaces/srv/SetBool "{data: true}"

Вывод:

response:
example_interfaces.srv.SetBool_Response(success=True, message='Свет включён')

Сервер на rclpy

Код узла нельзя запустить в браузере (language-text):

import rclpy
from rclpy.node import Node
from example_interfaces.srv import SetBool

class LightServer(Node):
    def __init__(self):
        super().__init__('light_server')
        self.srv = self.create_service(
            SetBool, '/enable_light', self.handle)

    def handle(self, request, response):
        state = 'включён' if request.data else 'выключен'
        response.success = True
        response.message = f'Свет {state}'
        return response

Как работает под капотом

Вызов сервиса синхронен по смыслу: клиент ждёт ответа. В rclpy, чтобы не заблокировать executor, обычно используют асинхронный вызов call_async, который возвращает future — обещание ответа. Когда сервер ответит, future заполнится результатом. Под капотом DDS гарантирует, что запрос дойдёт ровно до одного сервера, а ответ вернётся именно тому клиенту, кто спрашивал. Если сервера нет, клиент будет ждать или получит таймаут — поэтому перед вызовом проверяют доступность через wait_for_service.

Частые ошибки

  • Использовать сервис для непрерывного потока. Для данных датчика это топик; сервис — для разовых команд.
  • Долгая работа в обработчике сервиса. Если задача длительная (поездка к точке) — это уже не сервис, а действие (action).
  • Синхронный call в spin. Может привести к взаимоблокировке; используйте call_async с future.

Итоги

  • Сервис — разовая связь запрос/ответ, один-к-одному.
  • Структура — файл .srv с частями запроса и ответа через ---.
  • Применяют для команд и запросов состояния, где нужен ответ.
  • Для длительных задач сервис не подходит — нужен action.
Проверьте себя
1. Чем сервис отличается от топика?
AСервис быстрее
BСервис — разовый запрос/ответ, топик — непрерывный поток
CСервис работает только на C++
DРазницы нет
2. Как устроен файл .srv?
AОдин список полей
BДве части — запрос и ответ, разделённые ---
CЭто бинарный файл
DСписок узлов
3. Что лучше выбрать для длительной задачи вроде поездки к точке?
AСервис
BДействие (action)
CТопик без ответа
DПараметр