Um programa cliente

Sumário

  1. Declarando os tipos de requisição e resposta
  2. Criando um objeto cliente
  3. Criando objetos de requisição e resposta
  4. Chamando o serviço
  5. Declarando uma dependência

Chamar serviços a partir da linha de comando é conveniente para exploração e para tarefas que só precisam ser feitas ocasionalmente, mas obviamente é bem mais útil ser capaz de chamar serviços a partir do seu código. A Lista 8.1 mostra um curto exemplo de como fazer isso. Esse exemplo ilustra todos os elementos básicos de um programa cliente de serviço.

Lista 8.1

Um programa chamado spawn_turtle.cpp que chama um serviço.
// Este programa gera uma nova tartaruga turtlesim, chamando 
// o serviço apropriado.
#include <ros/ros.h>

// A classe srv para o serviço.
#include <turtlesim/Spawn.h>

int main(int argc, char ∗∗argv){
ros::init(argc, argv, "spawn_turtle");
ros::NodeHandle nh;

// Crie um objeto cliente para o serviço de spawn. Isso
// precisa saber o tipo de dados do serviço e seu
// nome.
ros::ServiceClient spawnClient
   = nh.serviceClient <turtlesim::Spawn>("spawn");

// Cria os objetos de solicitação e resposta.
turtlesim::Spawn::Request req;
turtlesim::Spawn::Response resp;

// Preenche os membros dos dados pedidos.
req.x = 2;
req.y = 3;
req.theta = M_PI/2;
req.name = "Leo";

// Chama o serviço. Não irá retornar até que
// o serviço esteja completo.
bool success = spawnClient.call(req, resp);

// Verifica se houve sucesso e usa a resposta
if(success){
ROS_INFO_STREAM("Spawned a turtle named "
  << resp.name);
}else{
ROS_ERROR_STREAM("Failed to spawn.");
}

}

Declarando os tipos de requisição e resposta

Assim como tipos de mensagem (relembre a Seção 3.3), todo dado de serviço tem um arquivo de cabeçalho C++ que precisamos incluir:

#include <package_name/type_name.h>

No exemplo, dizemos

#include <turtlesim/Spawn.h>

para incluir a definição de uma classe chamada turtlesim::Spawn, que define o tipo de dado — incluindo tanto as partes de requisição como de resposta — do serviço que queremos chamar.

Criando um objeto cliente

Depois de inicializar-se como um nó (ao chamar o ros::init e criar um objeto NodeHandle), nosso programa deve criar um objeto do tipo ros::ServiceClient, cujo trabalho é na verdade executar a chamada de serviço. A declaração de um ros::ServiceClient se parece com:

ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name);

Essa linha tem três partes importantes.

  • O node_handle é o objeto ros::NodeHandle usual. Estamos chamando o seu método serviceClient
  • O service_type é o nome do objeto de serviço definido no arquivo de cabeçalho que incluímos acima. Neste exemplo, é o turtlesim::Spawn.
  • O service_name é uma string que nomeia o serviço que queremos chamar. Esse deveria ser um nome relativo, mas também pode ser um nome global. O exemplo usa o nome relativo “spawn”.

Por padrão, criar esse objeto é relativamente econômico porque não faz muito, exceto guardar os detalhes sobre o serviço que precisaremos chamar depois.

⚠️ Note que criar um ros::Publisher não requer um tamanho de arquivo, ao contrário do análogo ros::ServiceClient. Essa diferença ocorre porque chamadas de serviço não retornam até que a resposta chegue. Pelo fato do cliente esperar para que a chamada de serviço seja completa, não há necessidade de manter uma fila de chamadas de serviço de saída.

Criando objetos de requisição e resposta

Uma vez que o ros::ServiceClient esteja construído, a próxima etapa é criar um objeto de requisição que contém os dados a serem enviados ao servidor. O cabeçalho que incluímos acima contém classes separadas para as partes de resposta e requisição do tipo de dado de serviço, chamado Request e Response, respectivamente. Estas classes devem ser referenciadas por meio do nome do pacote e do tipo de serviço da seguinte forma:

package_name::service_type::Request
package_name::service_type::Response

Cada uma dessas classes tem dados membros que são compatíveis com os campos do tipo de serviço. (Lembre-se que a saída do rossrv pode listar estes campos e seus tipos de dados para nós.) Esses campos são mapeados para tipos C++ da mesma forma que os campos de mensagem são. O construtor Request provê valores padrão sem significado para estes campos, de modo que devemos designar um valor para cada campo. No exemplo, nós criamos um objeto turtlesim::Spawn::Request e definimos valores para seus campos x, y,theta, and name.

Precisaremos também de um objeto Response — no exemplo, um turtlesim::Spawn::Response— mas, já que esta informação deveria vir do servidor, não devemos tentar preencher seus membros de dados.

⚠️ O cabeçalho do tipo de serviço também define uma única clase (uma struct na realidade) chamada

package_name::service_type

que contém tanto dados Request e Response como membros. Um objeto dessa classe é usualmente chamado srv. Se você preferir — assim como muitos autores de tutoriais online aparentemente fazem — você pode passar um objeto dessa classe para o método call introduzido abaixo, ao invés de separar os objetos Request e Response.

Chamando o serviço

Uma vez que temos um ServiceClient, um Request completo e um Response, podemos de fato chamar o serviço:

bool success = service_client.call(request, response)

Esse método faz o trabalho de localizar o nó do servidor, transmitindo os dados requisitados, aguardando pela resposta, e guardando os dados de resposta providos pelo Response.

O método de chamada retorna um valor booleano que nos diz se a chamada de serviço foi completada com sucesso. Falhas podem ocorrer devido a problemas com a infraestrutura ROS — por exemplo, ao tentar chamar um serviço não oferecido por nenhum nó — ou por razões específicas de um serviço individual. No exemplo, uma chamada malsucedida comumente indica que outra tartaruga existe com o nome requisitado.

⚠️ Um erro comum é falhar na checagem do valor retornado por uma call. Isso pode levar a problemas inesperados se o serviço call falhar. Leva apenas um ou dois minutos para adicionar um código que checa esse valor e chama o ROS_ERROR_STREAM quando o serviço de chamada falha. É bem provável que esse investimento de tempo será recompensado com uma depuração mais fácil no futuro.

⏩ Por padrão, o processo de encontrar e conectar-se a um nó servidor ocorre dentro do método call. Essa conexão é utilizada para este tipo de chamada de serviço e finalizada antes do retorno de call. ROS também suporta o conceito de clientes de serviço persistentes, no qual o construtor do ros::ServiceClient estabelece uma conexão com o servidor, que então é reutilizada para todas as chamadas subsequentes para aquele objeto cliente. Um cliente de serviço persistente pode ser criado ao passar como true o segundo parâmetro do construtor (o qual nós autorizamos a ser falso por padrão nos exemplos anteriores)

ros::ServiceClient client = node_handle.advertise<service_type>(
service_name, true);

A utilização de clientes persistentes é moderadamente desaconselhada pela documentação, porque os ganhos de desempenho tendem a ser bem pequenos — Os experimentos informais do autor mostraram uma melhoria de somente 10% — e o sistema resultante pode ser menos robusto a reinicializações ou mudanças no nó servidor.

Após a chamada de serviço ser completada com sucesso, você acessa os dados de resposta a partir do objeto Request que você passou para a call. No exemplo, a resposta inclui somente um eco do campo name da requisição.

Declarando uma dependência

Isto é tudo o que há no código do cliente. No entanto, para fazer com que o catkin_make compile corretamente um programa cliente, devemos assegurar que o pacote do programa declara a dependência no pacote que detém o tipo de serviço. Tais dependências, as quais são as mesmas que precisávamos para tipos de mensagens (relembre-se da Seção 3.3.3), necessitam de edições no arquivo CMakeLists.txt e ao manifesto, package.xml. Para compilar o programa exemplo, devemos garantir que a linha do find_package no CMakeLists.txt menciona o pacote turtlesim:

find_package(catkin REQUIRED COMPONENTS roscpp turtlesim)

No package.xml, devemos garantir que os elementos build_depend e run_depend existem e nomeiam o pacote:

<build_depend>turtlesim</build_depend>
<run_depend>turtlesim</run_depend>

Após completar estas mudanças, o usual catkin_make deve compilar o programa.