Creating ROS services

This page provides a step-by-step guide to create a ROS service from already existing code. Note that this tutorial includes some specific steps that ensures your service can be used by GRIP!

Let’s consider that you have a script that randomly generates the position of the end-effector of the robot within a given area. To do so, let’s assume that you have the following python code:
#!/usr/bin/env python

import random

def generate_ee_position(min_x, max_x, min_y, max_y, min_z, max_z):
    """
        Generate a random position within the input area
    """
    x_value = random.uniform(min_x, max_x)
    y_value = random.uniform(min_y, max_y)
    z_value = random.uniform(min_z, max_z)

    return [x_value, y_value, z_value]

The C++ equivalent of this code would be the following:

#include <random>
#include <vector>

// You can define here as many functions as you want

float random_float(float min, float max)
{
    // this  function assumes max > min, you may want
    // more robust error checking for a non-debug build
    assert(max > min);
    float random = ((float) rand()) / (float) RAND_MAX;

    float range = max - min;
    return (random * range) + min;
}

std::vector<float> generate_ee_position(float min_x, float max_x, float min_y, float max_y, float min_z, float max_z)
{
    float x_value = random_float(min_x, max_x);
    float y_value = random_float(min_y, max_y);
    float z_value = random_float(min_z, max_z);

    std::vector<float> output_vector{ x_value, y_value, z_value };

    return output_vector;
}

The following instructions hold whether your code is implmented in C++ or Python!

1. Copy the server_template folder

Instead of creating a ROS package from scratch, we are going to start from an existing one. First create an empty folder in /home/projects/shadow/base/src, which name will be the name of the ROS package containing your code and copy the content of this repository inside it. You can also initialize a repository if you want to keep it in git. In our case, we named our ROS package example_package. This folder contains five folders and seven files, as follows:

+-- action
|
+-- include
|   +-- server_template
|       template_action_server.hpp
|
+-- msg
|
+-- scripts
|   template_action_server.cpp
|   template_action_server.py
|   template_service_server.cpp
|   template_service_server.py
|
+-- srv
CMakeLists.txt
package.xml

If your code is written in C++, all the headers (.hpp files) must be placed in /include/server_template, while your .cpp files can be located in scripts. If you are using python, just add your .py files in scripts.

2. Change the package description

Replace all the information (i.e. the description, the maintainer, the author and the name) of the package.xml file. In line 2 of CMakeLists.txt, modify what’s inside project(). The name inside project() in the CMakeLists.txt and the name inside <name> tage of package.xml must correspond to the name of your folder!. We encourage you to just replace server_template by the name of your package in those two files.

Warning

Make sure to change the name of the folder in include as well!

To make sure everything is working fine, please run the following:
cd /home/projects/shadow_robot/base
catkin_make

If it does not finish successfully, it might be that the name of the folder and the name you changed in package.xml and CMakeLists.txt don’t match!

3. Create msg and srv files

The srv file will contain the backbone of what should be received and sent by the server. In order to be fully compatible with GRIP, the srv file must respect the following format:
<msg_type> input
---
int8 outcome
<msg_type> returned_object
<msg_type> is left to your preference, i.e. it’s implementation-dependent. You can use all the built-in types or create your own msg file.
In our case, the function we want to wrap inside a ROS service expects 6 values as input and outputs 3 values. For this reason, we are going to create a new file, GeneratePosition.srv, in the srv folder with the following content
float32[] input
---
int8 outcome
float32[] returned_object
Before forgetting, let’s edit the CMakeLists.txt file. Go to line 67 and add the name of your srv file after FILES (if you have several, add one per line). In our case, we would get
add_service_files(
  FILES
  GeneratePosition.srv
)
To make sure everything works so far, run
cd /home/user/projects/shadow_robot/base
catkin_make
source devel/setup.bash

4. Filling the templates

If the code you want to wrap inside ROS services is written in C++, you will find the template_service_server.cpp file in scripts. If you are using python, the template file is template_service_server.py. Although the syntaxes are different, the steps are exactly the same (and described in the files). You can either copy/paste and rename these files or just rename them. In our case we are going to create the file generate_ee_position_server.py (python version) and generate_ee_position_server.cpp (C++ version).

4.1 Import the generated srv files

Make sure to change the occurences of server_template by your package name (e.g. example_package). Then, change srvName by the name of your srv file without the extension (e.g. GeneratePosition).

4.2 Add your code

Add your code in the file. It can either directly be inside the function my_function (feel free to change its name), or create a new function, class or whatever you prefer.
In Python, if you want to access the values stored in the input field of your srv file inside my_function, you can use input_values = request.input and carry out any operation on the variable input_values.
Similarly, in C++, you can use <variable_type> input_values = request.input;.

4.3 Return the result

Make sure to fill and return the response of your server, keeping in mind that it must contain the fields outcome and returned_object.

4.4 Call your code when the script is executed

Change the name of the ROS node that will run your server (string inside rospy.init_node() or ros::init()). The last step is to make sure its content is valid. The first argument sets the name of the service, the second one (not in C++) should be the name of your srv file and the last one should be the name of the function returning the server response.

In our example, the generate_ee_position_server.py looks like this:

#!/usr/bin/env python

import rospy
# Change server_template by the name of your ROS package and srvName by the name of your srv file (without the .srv)
from example_package.srv import GeneratePosition, GeneratePositionResponse

# You can add here any other import statement you might need for your code
import random


# You can define here as many functions as you want
def generate_ee_position(min_x, max_x, min_y, max_y, min_z, max_z):
    """
        Generate a random position within the input area
    """
    x_value = random.uniform(min_x, max_x)
    y_value = random.uniform(min_y, max_y)
    z_value = random.uniform(min_z, max_z)

    return [x_value, y_value, z_value]


# Function that is going to be linked to the service server
def randomly_generate_end_effector_position(request):
    """
        This function (feel free to change its name) must contain your code.
        It MUST have a single input argument, that will be the request part of the srv, so DO NOT change it.
        You can call other functions inside it without any problem
    """
    # Change srvName by the name of your srv
    response = GeneratePositionResponse()

    # Get the input list
    input_values = request.input
    # Make sure the input list has 6 values, otherwise fill the response with a negative outcome (indexed as 1 here)
    if len(input_values) != 6:
        response.outcome = 1
        # Return an empty list
        response.returned_object = list()
    else:
        response.returned_object = generate_ee_position(input_values[0], input_values[1], input_values[2],
                                                        input_values[3], input_values[4], input_values[5])
        # Success outcome
        response.outcome = 0
    # Return the response
    return response

if __name__ == '__main__':
    # Initialise the node with a specific name (please change it to match your service)
    rospy.init_node('python_generate_ee_position_service_server')
    # Set the name of the service, specify which kind of srv will trigger it and what function will be run.
    # Change the name of the server with one that matches the content of your code, set the second argument to the name
    # of the srv file, and the last one should be the name of the function that runs your code.
    service = rospy.Service("ee_position_generation", GeneratePosition, randomly_generate_end_effector_position)
    rospy.spin()

Similarly, here is the content of generate_ee_position_server.cpp:

#include <ros/ros.h>
/**
Change server_template by the name of your ROS package and srvName by the name of your srv file (without the .srv)
*/
#include <example_package/GeneratePosition.h>

// You can add here any other include statement you might need for your code
#include <random>
#include <vector>

// You can define here as many functions as you want

float random_float(float min, float max)
{
    // this function assumes max > min, you may want more robust error checking for a non-debug build
    assert(max > min);
    float random = ((float) rand()) / (float) RAND_MAX;

    float range = max - min;
    return (random * range) + min;
}

std::vector<float> generate_ee_position(float min_x, float max_x, float min_y, float max_y, float min_z, float max_z)
{
    float x_value = random_float(min_x, max_x);
    float y_value = random_float(min_y, max_y);
    float z_value = random_float(min_z, max_z);

    std::vector<float> output_vector{ x_value, y_value, z_value };

    return output_vector;
}

// Function that is going to be linked to the service server
// This function (feel free to change its name) must contain your code.
// It MUST have two paramters, one for the request the srv, and the other one for the response.
// You can call other functions inside it without any problem
bool randomly_generate_end_effector_position(example_package::GeneratePosition::Request &request,
                                             example_package::GeneratePosition::Response &response)
{
  // Get the input list
  std::vector<float> input_values = request.input;

  // Make sure the input list has 6 values, otherwise fill the response with a negative outcome (indexed as 1 here)
  if (input_values.size() != 6)
  {
    response.outcome = 1;
    // Return an empty list
    std::vector<float> empty_vector;
    response.returned_object = empty_vector;
  }
  else
  {
    response.returned_object = generate_ee_position(input_values[0], input_values[1], input_values[2],
                                                    input_values[3], input_values[4], input_values[5]);
    // Success outcome
    response.outcome = 0;
  }

  // Return true to avoid having runtime errors on the client side
  return true;
}

int main(int argc, char **argv)
{
  // Initialise the node with a specific name (please change it to match your service)
  ros::init(argc, argv, "cpp_generate_ee_position_service_server");
  // Create a node handler
  ros::NodeHandle node_handle;
  /**
  Set the name of the service and what function will be run and received
  Change the name of the server with one that matches the content of your code, and the last one should be
  the function that runs your code.
  */
  ros::ServiceServer service = node_handle.advertiseService("ee_position_generation", randomly_generate_end_effector_position);
  ros::spin();

  return 0;
}

5. Make sure the server is executable

5.1 For Python

If you have created your server from template_service_server.py, be sure to make your new file executable with chmod +x:

chmod +x /home/user/projects/shadow_robot/base/src/example_package/scripts/generate_ee_position_server.py

Otherwise, ROS won’t be able to locate your node.

5.2 For C++

In order to make ROS aware of your newly created server, we need to slightly modify CMakeLists.txt. In the Build section of this file (you can go to line 167), add these three lines for each cpp service file you have created:

add_executable(<node_name> scripts/<cpp_file_name>)
target_link_libraries(<node_name> ${catkin_LIBRARIES})
add_dependencies(<node_name> <package_name>)

In our case, we would have:

add_executable(cpp_generate_ee_position_service_server scripts/generate_ee_position_server.cpp)
target_link_libraries(cpp_generate_ee_position_service_server ${catkin_LIBRARIES})
add_dependencies(cpp_generate_ee_position_service_server example_package)

5.3 Common step

The last thing you need to do is to recompile your ROS package:

cd /home/projects/shadow_robot/base
catkin_make
source devel/setup.bash

And here you are! You have successfully wrapped your code in a ROS service fully compatible with GRIP!