Modelo cliente – servidor no ROS? Precisamos mesmo?
Agora explicando como trabalhar com o ROS service:
E vamos ao exemplo no ROS:
Vamos ao ROS nodes agora:
Testando o serviço gerado com o ROS:
Olá caro leitor, nesse texto continuaremos a explorar mais algumas capacidades do ROS, o sistema operacional para robôs. Neste artigo vamos falar um pouco de um sistema alternativo ao método publish-subscribe de comunicação do ROS, os ROS Services.
Um exemplo? Um manipulador holonômico de 7 graus de liberdade precisa movimentar seu end-effector (junta que realiza o trabalho de fato) de um ponto A para um ponto B obedecendo uma série de limitações impostas pelo ambiente. Porém, antes de mover para esse ponto, o ROS node que controla esse robô precisa fazer uma série de cálculos e planejar a trajetória do manipulador, gerando uma lista de configurações para cada junta até que o ponto B seja alcançado ou reportando um aviso de que essa trajetória não pode ser planejada. Observando a figura temos um exemplo desse tipo de manipulador PUMA 560 planejando a trajetória:
Figura 1: Manipulador PUMA 560 e um exemplo de trajetória
Agora considerem que temos uma aplicação contendo três ROS nodes, um para tarefa (execution), outro para planejar o movimento (planner) e um de controle (control). Pensando no ROS node execution, uma vez em que conhecemos o ponto A e o ponto B, esse node não pode simplesmente pedir ao control para executar o movimento das juntas, primeiro ele deve perguntar ao planner se a trajetória é legal e como deve realizá-la, não podendo tomar qualquer ação até que o planner responda.
Seria possível estabelecer esse fluxo utilizando o sistema pub-sub que já conhecemos, porém teríamos que lidar com o trabalho extra de sincronizar o ROS node execution com a resposta do planner antes de comunicar com o control. É nesse momento que os ROS services são indicados, estes módulos permitem que um determinado ROS node faça uma requisição (cliente) a um serviço e suspenda sua execução até que aquele (servidor) responda com o resultado da operação.
Os ROS services são oferecidos por todas client libraries do ROS, seja em C++ ou Python, não sendo necessário qualquer instalação extra caso o ROS já esteja instalado no seu computador ou no seu computador embedded (Raspberry por exemplo…).
De forma similar as ROS messages, um ROS service precisa de um arquivo descrevendo o servidor bem como quais os tipos de dados que irão trafegar na requisição e qual tipo de dado é devolvido na resposta. Esses arquivos são colocados na pasta srv, usando a extensão .srv, abaixo um exemplo de um arquivo desse tipo que utilizei para criar um serviço capaz de calcular a cinemática inversa de um manipulador KUKA KR210:
geometry_msgs/Pose[] poses --- trajectory_msgs/JointTrajectoryPoint[] points
O arquivo descritor é dividido em duas seções, antes do marcador —, onde o tipo de dado da requisição é colocado, no caso do arquivo acima o servidor de cinemática inversa recebe uma lista de posições e devolve uma outra de lista de posições. Percebam ainda que é possível utilizar tipos complexos de dados como o geometry e trajectory messages que são construídos a partir do sistema primitivo de mensagens do ROS.
Com o descritor de serviço definido, basta agora utilizar o módulo de serviços e escrever seu ROS node como de costume. A partir do arquivo descritor um módulo em C++ ou Python vai ser gerado de forma que o usuário consiga incluir e manipular o tipo de dado no código do seu ROS node.
Vamos criar aqui dois ROS nodes, sendo um cliente que faz requisições e só publica o resultado da requisição após receber a resposta do ROS node responsável pelo serviço, ao mesmo tempo teremos o ROS servidor publicando no mesmo tópico que está processando a requisição. O usuário irá perceber que as mensagens estarão sempre sincronizadas já que o ROS node cliente também estará sincronizado com o ROS node servidor.
Para começarmos esse exemplo, vá para a sua worskpace do ROS e crie um ROS package, com o nome de sua preferência. Não sabe do que estou falando? Então você precisa visitar o tópico de bases do ROS começando por este artigo, e ao terminar volte aqui para continuarmos.
Uma vez criada, crie a pasta srv dentro do diretório topo do seu package, dentro dela crie o arquivo test_service.srv e edite-o da seguinte maneira:
string in --- string out
Com o descritor criado, agora precisamos avisar ao build system do ROS que esse serviço precisa ser gerado, para isso primeiro vamos alterar o conhecido arquivo CMakeLists.txt no diretório raiz do seu package e devemos adicionar os pacotes para geração de mensagens:
find_package(catkin REQUIRED COMPONENTS roscpp std_msgs roslaunch nodelet message_runtime message_generation )
E incluir o serviço que desejamos gerar:
add_service_files( FILES test_service.srv ) generate_messages( DEPENDENCIES std_msgs )
test_service.srv
Iremos voltar logo mais nesse arquivo quando adicionarmos o código dos nossos nodes, agora vamos ao arquivo package.xml que também está na raiz do seu package, adicione as seguintes linhas:
Basicamente estamos adicionando no nosso package a obrigatoriedade de considerar as dependências de geração de mensagem, com isso sendo possível chamar o comando para gerar os serviços durante o build.
Vamos testar se o serviço está sendo gerado, para isso, no diretório raiz da workspace (atenção, não é do package, são dois níveis acima) e no terminal execute o build e verifique se o serviço foi gerado, você deve obter o conteúdo do arquivo .srv:
Figura 2: Testando o serviço adicionado
Agora que temos nosso serviço pronto, vá para o diretório do seu package e na pasta src, adicione dois arquivos, server_node.cpp e client_node.cpp. Comecemos pelo client_node, adicione o código abaixo:
#include "ros/ros.h" #include "test_package/test_service.h" #include <iostream> #include <iostream> #include <sstream> int main(int argc, char **argv) { ros::init(argc, argv, "demo_service_client"); ros::NodeHandle n; ros::Rate loop_rate(10); ros::ServiceClient client = n.serviceClient<test_package::test_service>("test_service"); while (ros::ok()) { test_package::test_service srv; std::stringstream ss; ss << "Sending from Here"; srv.request.in = ss.str(); if (client.call(srv)) { ROS_INFO("From Client [%s], Server says [%s]",srv.request.in.c_str(),srv.response.out.c_str()); } else { ROS_ERROR("Failed to call service"); return 1; } ros::spinOnce(); loop_rate.sleep(); } return 0; }
#include "test_package/test_service.h"
ros::ServiceClient client = n.serviceClient<test_package::test_service>("test_service");
test_package::test_service srv;
ROS_INFO("From Client [%s], Server says [%s]",srv.request.in.c_str(),srv.response.out.c_str());
E agora em server_node, cole o código abaixo:
#include "ros/ros.h" #include "test_package/test_service.h" #include <iostream> #include <sstream> bool test_service_callback(test_package::test_service::Request &req, test_package::test_service::Response &res) { std::stringstream ss; ss << "Received Here"; res.out = ss.str(); ROS_INFO("From Client [%s], Server says [%s]",req.in.c_str(),res.out.c_str()); return true; } int main(int argc, char **argv) { ros::init(argc, argv, "ros_server_node"); ros::NodeHandle n; ros::ServiceServer service = n.advertiseService("test_service", test_service_callback); ROS_INFO("Ready to receive from client."); ros::spin(); return 0; }
bool test_service_callback(test_package::test_service::Request &req,
test_package::test_service::Response &res) {
ROS_INFO("From Client [%s], Server says [%s]",req.in.c_str(),res.out.c_str());
ros::init(argc, argv, "ros_server_node");
ros::ServiceServer service = n.advertiseService("test_service", test_service_callback);
Agora volte ao arquivo CMakeLists.txt e adicione as linhas para que os arquivos sejam compilados e os executáveis gerados:
add_executable(server_node src/server_node.cpp) add_executable(client_node src/client_node.cpp) add_dependencies(server_node test_package_generate_messages_cpp) add_dependencies(client_node test_package_generate_messages_cpp) target_link_libraries( server_node ${catkin_LIBRARIES} ) target_link_libraries( client_node ${catkin_LIBRARIES} )
add_executable(server_node src/server_node.cpp)
add_executable(client_node src/client_node.cpp)
add_dependencies(server_node test_package_generate_messages_cpp)
add_dependencies(client_node test_package_generate_messages_cpp)
O comando add_dependecies informa ao build systems para gerar os arquivos de código fonte relativos ao serviço que configuramos no nosso package. E agora estamos prontos para fazer o build no nosso exemplo de serviço ROS. Invoque o comando catkin_make para fazer o build do seu package.
Se você leu até aqui antes de implementar qualquer coisa e quer apenas testar, tenho uma notícia para você, o package de teste completo encontra-se no meu github e você pode clonar ele direto na sua workspace, para isso basta checar esse link aqui. Ou se fez todos os passos anteriores e conseguiu compilar seu package, então vamos agora ver as coisas funcionando, primeiramente você vai precisar abrir três janelas de terminal que chamarei respectivamente de core, server e client.
De preferência em todas elas vá para o diretório raiz da sua catkin workspace, primeiramente vamos subir o ROS no seu PC ou dispositivo embarcado, para isso no terminal core faça como no exemplo abaixo invocando o roscore:
Figura 3: Lançando o roscore
Na janela server dispare a execução do ROS node servidor, como no exemplo:
Figura 4: Disparando o server.
O servidor irá aguardar a comunicação do ROS cliente, que pode ser executado como no exemplo abaixo, para isso vá para a janela client:
Figura 5: Disparando o ROS node cliente
Veja que de imediato o cliente vai fazer requisição ao server que vai dar uma resposta a ele, além disso voltando na janela server poderemos ver que o fluxo de comunicação também é mostrado:
Figura 6: Fluxo de comunicação no ROS node servidor
Ele estava aguardando conexão do cliente, e agora encontra-se publicando a atividade entre as requisições com isso efetivamente temos um servidor pronto para ser incrementado com sua aplicação de robótica que precisa do modelo-cliente servidor.
O objetivo desse artigo era explorar mais uma faceta do sistema de comunicação do ROS, os serviços, capazes de implementar um modelo cliente servidor de comunicação entre os ROS nodes, exemplificamos onde esse tipo de modelo pode ser útil e apresentamos ao leitor um exemplo funcional seguido dos passos para adicionar um serviço em um ROS package. Com esse conhecimento sobre ROS adquirido nos artigos aqui publicados o leitor já vai obter bagagem suficiente para os próximos textos, fiquem ligados pois nas próximas publicações vamos testar vários pacotes ROS interessantes que podem dar aquele incremento no seu próximo projeto de Robô, então fiquem ligados!