Um programa cliente
Sumário
- Declarando os tipos de requisição e resposta
- Criando um objeto cliente
- Criando objetos de requisição e resposta
- Chamando o serviço
- 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 objetoros::NodeHandle
usual. Estamos chamando o seu métodoserviceClient
- O
service_type
é o nome do objeto de serviço definido no arquivo de cabeçalho que incluímos acima. Neste exemplo, é oturtlesim::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álogoros::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
eResponse
como membros. Um objeto dessa classe é usualmente chamadosrv
. Se você preferir — assim como muitos autores de tutoriais online aparentemente fazem — você pode passar um objeto dessa classe para o métodocall
introduzido abaixo, ao invés de separar os objetosRequest
eResponse
.
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çocall
falhar. Leva apenas um ou dois minutos para adicionar um código que checa esse valor e chama oROS_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 decall
. ROS também suporta o conceito de clientes de serviço persistentes, no qual o construtor doros::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 comotrue
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.