Node communication via services

In this tutorial, we are going to create two nodes that are going to communicate via services. You can see a service as a function that is going to be executed in a different node. Services have two main components: a service provider and a service consumer. A service provider is the node that offers the service to the rest of the world. The service consumers are the nodes that request the function offered by the provider. Note that in Ignition Transport the location of the service is hidden. The discovery layer of the library is in charge of discovering and keeping and updated list of services available.

In the next tutorial, one node will be the service provider that offers an echo service, whereas the other node will be the service consumer requesting an echo call.

mkdir ~/ign_transport_tutorial
cd ~/ign_transport_tutorial

Responser

Download the responser.cc file within the ign_transport_tutorial folder and open it with your favorite editor:

#include <iostream>
#include <string>
#include <ignition/msgs.hh>
#include <ignition/transport.hh>

//////////////////////////////////////////////////
/// \brief Provide an "echo" service.
void srvEcho(const ignition::msgs::StringMsg &_req,
  ignition::msgs::StringMsg &_rep, bool &_result)
{
  // Set the response's content.
  _rep.set_data(_req.data());

  // The response succeed.
  _result = true;
}

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Let's print the list of our network interfaces.
  std::cout << "List of network interfaces in this machine:" << std::endl;
  for (const auto &netIface : ignition::transport::determineInterfaces())
    std::cout << "\t" << netIface << std::endl;

  // Create a transport node.
  ignition::transport::Node node;
  std::string service = "/echo";

  // Advertise a service call.
  if (!node.Advertise(service, srvEcho))
  {
    std::cerr << "Error advertising service [" << service << "]" << std::endl;
    return -1;
  }

  // Zzzzzz.
  ignition::transport::waitForShutdown();
}

Walkthrough

#include <ignition/msgs.hh>
#include <ignition/transport.hh>

The line #include <ignition/transport.hh> contains the Ignition Transport header for using the transport library.

The next line includes the generated Protobuf code that we are going to use for our messages. We are going to use StringMsg type Protobuf messages for our services.

//////////////////////////////////////////////////
/// \brief Provide an "echo" service.
void srvEcho(const ignition::msgs::StringMsg &_req,
  ignition::msgs::StringMsg &_rep, bool &_result)
{
  // Set the response's content.
  _rep.set_data(_req.data());

  // The response succeed.
  _result = true;
}

As a service provider, our node needs to register a function callback that will execute every time a new service request is received. The signature of the callback is always similar to the one shown in this example with the exception of the Protobuf messages types for the _req (request) and _rep (response). The request parameter contains the input parameters of the request. The response message contains any resulting data from the service call. The _result parameter denotes if the overall service call was considered successful or not. In our example, as a simple echo service, we just fill the response with the same data contained in the request.

// Create a transport node.
ignition::transport::Node node;
std::string service = "/echo";

// Advertise a service call.
if (!node.Advertise(service, srvEcho))
{
  std::cerr << "Error advertising service [" << service << "]" << std::endl;
  return -1;
}

// Zzzzzz.
ignition::transport::waitForShutdown();

We declare a Node that will offer all the transport functionality. In our case, we are interested in offering a service, so the first step is to announce our service name. Once a service name is advertised, we can accept service requests.

If you don’t have any other tasks to do besides waiting for service requests, you can use the call waitForShutdown() that will block your current thread until you hit CTRL-C. Note that this function captures the SIGINT and SIGTERM signals.

Synchronous requester

Download the requester.cc file within the ign_transport_tutorial folder and open it with your favorite editor:

#include <iostream>
#include <ignition/msgs.hh>
#include <ignition/transport.hh>

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Create a transport node.
  ignition::transport::Node node;

  // Prepare the input parameters.
  ignition::msgs::StringMsg req;
  req.set_data("HELLO");

  ignition::msgs::StringMsg rep;
  bool result;
  unsigned int timeout = 5000;

  // Request the "/echo" service.
  bool executed = node.Request("/echo", req, timeout, rep, result);

  if (executed)
  {
    if (result)
      std::cout << "Response: [" << rep.data() << "]" << std::endl;
    else
      std::cout << "Service call failed" << std::endl;
  }
  else
    std::cerr << "Service call timed out" << std::endl;
}

Walkthrough

// Create a transport node.
ignition::transport::Node node;

// Prepare the input parameters.
ignition::msgs::StringMsg req;
req.set_data("HELLO");

ignition::msgs::StringMsg rep;
bool result;
unsigned int timeout = 5000;

We declare the Node that allows us to request a service. Next, we declare and fill the message used as an input parameter for our echo request. Then, we declare the Protobuf message that will contain the response and the variable that will tell us if the service request succeed or failed. In this example, we will use a synchronous request, meaning that our code will block until the response is received or a timeout expires. The value of the timeout is expressed in milliseconds.

// Request the "/echo" service.
bool executed = node.Request("/echo", req, timeout, rep, result);

if (executed)
{
  if (result)
    std::cout << "Response: [" << rep.data() << "]" << std::endl;
  else
    std::cout << "Service call failed" << std::endl;
}
else
  std::cerr << "Service call timed out" << std::endl;

In this section of the code we use the method Request() for forwarding the service call to any service provider of the service /echo. Ignition Transport will find a node, communicate the input data, capture the response and pass it to your output parameter. The return value will tell you if the request expired or the response was received. The result value will tell you if the service provider considered the operation valid.

Imagine for example that we are using a division service, where our input message contains the numerator and denominator. If there are no nodes offering this service, our request will timeout (return value false). On the other hand, if there’s at least one node providing the service, the request will return true signaling that the request was received. However, if we set our denominator to 0 in the input message, result will be false reporting that something went wrong in the request. If the input parameters are valid, we’ll receive a result value of true and we can use our response message.

Asynchronous requester

Download the requester_async.cc file within the ign_transport_tutorial folder and open it with your favorite editor:

#include <iostream>
#include <ignition/msgs.hh>
#include <ignition/transport.hh>

//////////////////////////////////////////////////
/// \brief Service response callback.
void responseCb(const ignition::msgs::StringMsg &_rep, const bool _result)
{
  if (_result)
    std::cout << "Response: [" << _rep.data() << "]" << std::endl;
  else
    std::cerr << "Service call failed" << std::endl;
}

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Create a transport node.
  ignition::transport::Node node;

  // Prepare the input parameters.
  ignition::msgs::StringMsg req;
  req.set_data("HELLO");

  std::cout << "Press <CTRL-C> to exit" << std::endl;

  // Request the "/echo" service.
  node.Request("/echo", req, responseCb);

  // Zzzzzz.
  ignition::transport::waitForShutdown();
}

Walkthrough

//////////////////////////////////////////////////
/// \brief Service response callback.
void responseCb(const ignition::msgs::StringMsg &_rep, const bool _result)
{
  if (_result)
    std::cout << "Response: [" << _rep.data() << "]" << std::endl;
  else
    std::cerr << "Service call failed" << std::endl;
}

We need to register a function callback that will execute when we receive our service response. The signature of the callback is always similar to the one shown in this example with the only exception of the Protobuf message type used in the response. You should create a function callback with the appropriate Protobuf type depending on the response type of the service requested. In our case, we know that the service /echo will answer with a Protobuf StringMsg` type.

// Create a transport node.
ignition::transport::Node node;

// Prepare the input parameters.
ignition::msgs::StringMsg req;
req.set_data("HELLO");

// Request the "/echo" service.
node.Request("/echo", req, responseCb);

In this section of the code we declare a node and a Protobuf message that is filled with the input parameters for our request. Next, we just use the asynchronous variant of the Request() method that forwards a service call to any service provider of the service /echo. Ignition Transport will find a node, communicate the data, capture the response and pass it to your callback, in addition of the service call result. Note that this variant of Request() is asynchronous, so your code will not block while your service request is handled.

Oneway responser

Not all the service requests require a response. In these cases we can use a oneway service to process service requests without sending back responses. Oneway services don’t accept any output parameters nor the requests have to wait for the response.

Download the responser_oneway.cc file within the ign_transport_tutorial folder and open it with your favorite editor:

#include <iostream>
#include <string>
#include <ignition/transport.hh>
#include <ignition/msgs.hh>

//////////////////////////////////////////////////
void srvOneway(const ignition::msgs::StringMsg &_req)
{
  std::cout << "Request received: [" << _req.data() << "]" << std::endl;
}

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Create a transport node.
  ignition::transport::Node node;
  std::string service = "/oneway";

  // Advertise a oneway service.
  if (!node.Advertise(service, srvOneway))
  {
    std::cerr << "Error advertising service [" << service << "]" << std::endl;
    return -1;
  }

  // Zzzzzz.
  ignition::transport::waitForShutdown();
}

Walkthrough

//////////////////////////////////////////////////
void srvOneway(const ignition::msgs::StringMsg &_req)
{
  std::cout << "Request received: [" << _req.data() << "]" << std::endl;
}

As a oneway service provider, our node needs to advertise a service that doesn’t send a response back. The signature of the callback contains only one parameter that is the input parameter, _req (request). We don’t need _rep (response) or _result as there is no response expected. In our example, the value of the input parameter is printed on the screen.

// Create a transport node.
ignition::transport::Node node;
std::string service = "/oneway";

// Advertise a oneway service.
if (!node.Advertise(service, srvOneway))
{
  std::cerr << "Error advertising service [" << service << "]" << std::endl;
  return -1;
}

We declare a Node that will offer all the transport functionality. In our case, we are interested in offering a oneway service, so the first step is to announce our service name. Once a service name is advertised, we can accept service requests.

Oneway requester

This case is similar to the oneway service provider. This code can be used for requesting a service that does not need a response back. We don’t need any output parameters in this case nor we have to wait for the response.

Download the requester_oneway.cc file within the ign_transport_tutorial folder and open it with your favorite editor:

#include <iostream>
#include <ignition/transport.hh>
#include <ignition/msgs.hh>

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Create a transport node.
  ignition::transport::Node node;

  // Prepare the input parameters.
  ignition::msgs::StringMsg req;
  req.set_data("HELLO");

  // Request the "/oneway" service.
  bool executed = node.Request("/oneway", req);

  if (!executed)
    std::cerr << "Service call failed" << std::endl;
}

Walkthrough

// Create a transport node.
ignition::transport::Node node;

// Prepare the input parameters.
ignition::msgs::StringMsg req;
req.set_data("HELLO");

// Request the "/oneway" service.
bool executed = node.Request("/oneway", req);

if (!executed)
std::cerr << "Service call failed" << std::endl;

First of all we declare a node and a Protobuf message that is filled with the input parameters for our /oneway service. Next, we just use the oneway variant of the Request() method that forwards a service call to any service provider of the service /oneway. Ignition Transport will find a node and communicate the data without waiting for the response. The return value of Request() indicates if the request was successfully queued. Note that this variant of Request() is also asynchronous, so your code will not block while your service request is handled.

Service without input parameter

Sometimes we want to receive some result but don’t have any input parameter to send.

Download the responser_no_input.cc
file within the ign_transport_tutorial folder and open it with your

favorite editor:

#include <iostream>
#include <string>
#include <ignition/msgs.hh>
#include <ignition/transport.hh>

//////////////////////////////////////////////////
/// \brief Provide a "quote" service.
/// Well OK, it's just single-quote service but do you really need more?
void srvQuote(ignition::msgs::StringMsg &_rep, bool &_result)
{
  std::string awesomeQuote = "This is it! This is the answer. It says here..."
    "that a bolt of lightning is going to strike the clock tower at precisely "
    "10:04pm, next Saturday night! If...If we could somehow...harness this "
    "lightning...channel it...into the flux capacitor...it just might work. "
    "Next Saturday night, we're sending you back to the future!";

  // Set the response's content.
  _rep.set_data(awesomeQuote);

  // The response succeed.
  _result = true;
}

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Create a transport node.
  ignition::transport::Node node;
  std::string service = "/quote";

  // Advertise a service call.
  if (!node.Advertise(service, srvQuote))
  {
    std::cerr << "Error advertising service [" << service << "]" << std::endl;
    return -1;
  }

  // Zzzzzz.
  ignition::transport::waitForShutdown();
}

Walkthrough

void srvQuote(ignition::msgs::StringMsg &_rep, bool &_result)

Service doesn’t receive anything. The signature of the callback contains two parameters _rep (response) and _result. In our example, we return the quote.

// Create a transport node.
ignition::transport::Node node;
std::string service = "/quote";

// Advertise a service call.
if (!node.Advertise(service, srvQuote))
{
  std::cerr << "Error advertising service [" << service << "]" << std::endl;
  return -1;
}

// Zzzzzz.
ignition::transport::waitForShutdown();

We declare a Node that will offer all the transport functionality. In our case, we are interested in offering service without input, so the first step is to announce the service name. Once a service name is advertised, we can accept service requests.

Empty requester sync and async

This case is similar to the service without input parameter. We don’t send any request.

Download the requester_no_input.cc file within the ign_transport_tutorial folder and open it with your favorite editor:

#include <iostream>
#include <ignition/msgs.hh>
#include <ignition/transport.hh>

//////////////////////////////////////////////////
int main(int argc, char **argv)
{
  // Create a transport node.
  ignition::transport::Node node;

  ignition::msgs::StringMsg rep;
  bool result;
  unsigned int timeout = 5000;

  // Request the "/quote" service.
  bool executed = node.Request("/quote", timeout, rep, result);

  if (executed)
  {
    if (result)
      std::cout << "Response: [" << rep.data() << "]" << std::endl;
    else
      std::cout << "Service call failed" << std::endl;
  }
  else
    std::cerr << "Service call timed out" << std::endl;
}

Walkthrough

First of all we declare a node and a message that will contain the response from /quote service. Next, we use the variant without input parameter of the Request() method. The return value of Request() indicates whether the request timed out or reached the service provider and result shows if the service was successfully executed.

We also have the async version for service request without input. You should download requester_no_input.cc file within the ign_transport_tutorial folder.

Building the code

Download the CMakeLists.txt file within the ign_transport_tutorial folder. Then, download CMakeLists.txt and stringmsg.proto inside the msgs directory.

Once you have all your files, go ahead and create a build/ folder within the ign_transport_tutorial directory.

mkdir build
cd build

Run cmake and build the code.

cmake ..
make responser responser_oneway requester requester_async requester_oneway
make responser_no_input requester_no_input requester_async_no_input

Running the examples

Open three new terminals and from your build/ directory run the executables.

From terminal 1:

./responser

From terminal 2:

./requester

From terminal 3:

./requester_async

In your requester terminals, you should expect an output similar to this one, showing that your requesters have received their responses:

caguero@turtlebot:~/ign_transport_tutorial/build$ ./requester
Response: [Hello World!]
caguero@turtlebot:~/ign_transport_tutorial/build$ ./requester_async
Response: [Hello World!]

For running the oneway examples, open two terminals and from your build/ directory run the executables.

From terminal 1:

./responser_oneway

From terminal 2:

./requester_oneway

In your responser terminal, you should expect an output similar to this one, showing that your service provider has received a request:

caguero@turtlebot:~/ign_transport_tutorial/build$ ./responser_oneway
Request received: [HELLO]

For running the examples without input, open three terminals and from your build/ directory run the executables.

From terminal 1:

./responser_no_input

From terminal 2:

./requester_no_input

From terminal 3:

./requester_async_no_input

In your requesters’ terminals, you should expect an output similar to this one, showing that you have received a response:

caguero@turtlebot:~/ign_transport_tutorial/build$ ./requester_no_input
Response: [This is it! This is the answer. It says here...that a bolt of
lightning is going to strike the clock tower at precisely 10:04pm, next
Saturday night! If...If we could somehow...harness this lightning...channel
it...into the flux capacitor...it just might work. Next Saturday night,
we're sending you back to the future!]