Tutorial 4: Communications using services

Objective: This tutorial session is devoted to learn the ROS request/response remote procedure calls.

Contents

After this session you will be able to:

  • Understand the client/server mechanism.

  • Use the rqt service type browser tool to look for the standard service types.

  • Define non-standard services: the .srv files and the required commands in the CMakeLists.txt and package.xml files.

  • Write nodes able to call/offer a service..

References:

Complementary resources:

Provided packages:

This tutorial will use the following package:

Package foolturtle_server
Package foolturtle_server_interfaces
Package foolturtle_server_alone
Package urdf_tutorial
Package kinenikros2
Package rviz2_marker_demo
Package ur_description

Prepare the colcon workspace to work with it:

  1. If you have not yet done so, follow the steps in Setup to:

  • clone in your ~/data/git-projects folder the meta-repository containing all the required packages,

  • create the colcon workspace folders (use colcon_ws4 for the current tutorial).

  1. Follow the procedure shown in Procedure to prepare the exercises to import in the intro2ros/2025/teamXX/EXERCISE4 subgroup of your GitLab account the package you implemented to solve exercise 3 (change XX by your team number):

  1. In your git-projects folder clone the urdf_tutorial project of your GitLab account inside an exercise4 subfolder (change XX by your team number). You can use https:

git clone https://gitlab.com/intro2ros/2025/teamXX/exercise4/urdf_tutorial.git exercise4/urdf_tutorial

or ssh:

git clone git@gitlab.com:intro2ros/2025/teamXX/exercise4/urdf_tutorial.git exercise4/urdf_tutorial
  1. In your colcon_ws4/src folder create a symbolic link to the package to be used:

ln -s ~/data/git-projects/all_introduction_to_ros2_packages/foolturtle_server
ln -s ~/data/git-projects/all_introduction_to_ros2_packages/foolturtle_server_interfaces
ln -s ~/data/git-projects/all_introduction_to_ros2_packages/foolturtle_server_alone
ln -s ~/data/git-projects/exercise4/urdf_tutorial
ln -s ~/data/git-projects/all_introduction_to_ros2_packages/kinenikros2
ln -s ~/data/git-projects/all_introduction_to_ros2_packages/rviz2_marker_demo
ln -s ~/data/git-projects/all_introduction_to_ros2_packages/Universal_Robots_ROS2_Description

Communication features

In ROS 2, a service refers to a remote procedure call, i.e., a node (the service client) makes a remote procedure call (called a request) to another node (the service server) which takes some action (computing something, configuring hardware or software, changing its own behavior, etc.) and returns a result (called a response).

../_images/clients_server.png

ROS service calls communication has the following features:

  • It is bi-directional.

  • It one-to-one.

  • The calls are usually expected to return quickly, as the client is generally waiting on the result.

  • Are identified by a service name (there should only ever be one service server per service name).

A service data type determines the content of messages by a collection of named fields, and is divided into two parts, the request and the response, e.g., for a request composed by an integer and a string and a Bool as a response, it would be specified as:

uint32 number
string name
---
bool result

Exercise

Run the turtlesim_node:

ros2 run turtlesim turtlesim_node

Inspect the /spawn service and its interface:

ros2 service list
ros2 service type /spawn
ros2 interface show turtlesim/srv/Spawn

Call the service from the terminal:

ros2 service call /spawn turtlesim/srv/Spawn "{x: 1.0, y: 1.0, name: Speedy}"

Note that the service type has to be added to the call.

Recall that you can use the tab to autocomplete.

Service interfaces

Standard services

The std_srvs package contains three service types called Empty, SetBool and Trigger, which are common service patterns for sending a signal to a ROS node.

  • The Empty service does not exchange any actual data between the service and the client.

  • The SetBool service is used to set bolean values.

  • The Trigger service adds the possibility to check if triggering was successful or not.

Exercise

Use the rqt tool to browse the std_srvs types:

$ rqt -s rqt_srv
../_images/rqtsrv.png

Non-standard services

To allow a server to offer services with customized arguments and return values, a text file (with extension .srv) is generated with the input and output message types, e.g. here we will use a service (called ChangeVel) to change the maximum velocity to be commanded to a turtle in a turtlesim_node:

1  float32 max_vel
2  ---
3  bool ret
4  float32 time_of_change

Some naming conventions have to be followed for defining interfaces:

  • Naming of messages and services: Each file contains a single message or service. Message files use the extension .msg, service files use the extension .srv. Both file names must use an upper camel case name and only consist of alphanumeric characters.

  • Naming of fields: Field names must be lowercase alphanumeric characters with underscores for separating words. They must start with an alphabetic character, they must not end with an underscore and never have two consecutive underscores.

    Note that also the standard message types are lowercase, e.g. float64, while the standard messages that wrap them have the first letter uppercase, e.g. Float64.

  • Header files: The files generated from message generation are snake case (i.e. the names of the header files are all lowercases and include an underscore before every upercase in the original camelcase name).

Defining services in a dedicated package

In section Custom messages we created a separate package to define a custom message, because it is a good practice to define the interfaces of a package (the custom messages, services and actions) in a separate package.

If a package is called my_package then the interfaces will be defined in a package called my_package_interfaces.

In order to generate new custom messages and services and make them available to other packages, the use of rosidl packages is required. These packages contain a set of tools and libraries to define interfaces (message types, services and actions).

For this, the CMakeLists.txt file should have the following lines:

 1 #  Add the following lines to generate the interfaces (msg and srv)
 2 find_package(rosidl_default_generators REQUIRED)
 3 set(msg_files
 4    "msg/MyMessage.msg"
 5    )
 6 set(srv_files
 7    "msg/MyService.srv"
 8    )
 9 rosidl_generate_interfaces(${PROJECT_NAME}
10    ${msg_files}
11    ${srv_files}
12    #DEPENDENCIES xxxxxx  Add here any package that above messages may depend on, e.g. geometry_msgs
13 )

And the package.xml file should have the following lines:

1  <!--Add the following four lines to generate custom messages and services-->
2  <buildtool_depend>rosidl_default_generators</buildtool_depend>
3  <exec_depend>rosidl_default_runtime</exec_depend>
4  <member_of_group>rosidl_interface_packages</member_of_group>

Exercise

Take a look at the package foolturtle_server_interfaces:

  • the srv folder

  • the CMakeLists.txt file

  • the package.xml file

Exercise

Build the foolturtle_server_interfaces package and check the availability of the service ChangeVel:

colcon build --packages-up-to foolturtle_server_interfaces
ros2 interface show foolturtle_server_interfaces/srv/ChangeVel

Then, the package using these interfaces should include the dependency in the CMakeLists.txt file:

1  find_package(my_package_interfaces REQUIRED)
2  set(dependencies
3   ....
4   my_package_interfaces
5  )

And also in the package.xml the dependency on this package should be made explicit:

1 <depend>my_package_interfaces</depend>

Exercise

Take a look at the package foolturtle_server that will use the services defined in foolturtle_server_interfaces:

  • the CMakeLists.txt file

  • the package.xml file

Defining services in the same package

For small packages with simple services, these can be defined in the same package, i.e. the package will include a srv folder with the definition of the services (likewise if custom messages where necessary):

my_package/
   package.xml
   CMakeLists.txt
   include/my_package/
   src/
   srv/

The CMakeLists.txt file will need to be extended by adding lines 1, 17-21 below. The rosidl_get_typesupport_target() returns the name of a typesupport target, which is used in a target_link_libraries() call to make the target depend on generated code for interfaces generated in the same CMake project.

 1 find_package(builtin_interfaces REQUIRED)
 2
 3 #  Add the following lines to generate the interfaces (msg and srv)
 4 find_package(rosidl_default_generators REQUIRED)
 5 set(msg_files
 6    "msg/MyMessage.msg"
 7    )
 8 set(srv_files
 9    "msg/MyService.srv"
10    )
11 rosidl_generate_interfaces(${PROJECT_NAME}
12    ${msg_files}
13    ${srv_files}
14    #DEPENDENCIES xxxxxx  Add here any package that above messages may depend on, e.g. geometry_msgs
15 )
16
17 # Use generated message in own package
18 rosidl_get_typesupport_target(cpp_typesupport_target
19          ${PROJECT_NAME} rosidl_typesupport_cpp)
20 # Link against the interface
21 target_link_libraries(publish_address_book "${cpp_typesupport_target}")

The package.xml file should include the line 6 below to show the build dependance on the package builtin_interfaces:

1  <!--Add the following four lines to generate custom messages and services-->
2  <buildtool_depend>rosidl_default_generators</buildtool_depend>
3  <exec_depend>rosidl_default_runtime</exec_depend>
4  <member_of_group>rosidl_interface_packages</member_of_group>
5  <!--Add the following four lines to generate custom messages and services inside the package-->
6  <build_depend>builtin_interfaces</build_depend>

More details in this ROS 2 tutorial section: Use an interface from the same package.

Exercise

Take a look at the package foolturtle_server_alone:

  • the srv folder

  • the CMakeLists.txt file

  • the package.xml file

A server program

The foolturtle_server package

This package is an variant of the package foolturtle that, besides having the pubsub node publishing random velocities to command the motion of a turtle1, it also offers a service to change the maximum absolute velocity.

The definition of the service is done in the foolturtle_server_interfaces package.

The package also has different clients to call this service, as commented below.

The code of pubsub node that defines a service to change the maximum value of the published random command is shown next.

Note:

  • The header file included: #include “foolturtle_server_interfaces/srv/change_vel.hpp”

  • The change_vel service created in lines 24-25.

  • The change_vel_callback function to be called when a request to the service is received (a random delay has been added to test later the client behaviors).

 1  #include "rclcpp/rclcpp.hpp"
 2  #include "geometry_msgs/msg/twist.hpp"
 3  #include "foolturtle_server_interfaces/srv/change_vel.hpp"
 4  #include "rclcpp/duration.hpp"
 5  #include <stdlib.h> // For rand() and RAND_MAX
 6  #include <chrono>
 7  #include <thread>
 8
 9  using namespace std::chrono_literals;
10  using std::placeholders::_1;
11  using std::placeholders::_2;
12
13  class PublisherNode : public rclcpp::Node
14  {
15     public:
16     PublisherNode() : Node("pubvel_node")
17     {
18        pubvel_ = create_publisher<geometry_msgs::msg::Twist>("turtle1/cmd_vel", 10);
19        //to set a publishing frequency of 2Hz
20        timer_ = create_wall_timer(
21           500ms, std::bind(&PublisherNode::timer_callback, this));
22        //seed the random number generator.
23        srand(time(0));
24        //create the service
25        change_vel_service_ = create_service<foolturtle_server_interfaces::srv::ChangeVel>("change_vel",
26                 std::bind(&PublisherNode::change_vel_callback, this, _1, _2));
27        maxvel_ = 1.0;
28     }
29
30     void timer_callback()
31     {
32        // Create and fill in the message.  The other four
33        // fields, which are ignored by turtlesim, default to 0.
34        msg_.linear.x = double(rand())*maxvel_/double(RAND_MAX);
35        msg_.angular.z = 2*double(rand())*maxvel_/double(RAND_MAX) - 1;
36        pubvel_->publish(msg_);
37        // Send a message to rosout with the details.
38        RCLCPP_INFO_STREAM(get_logger(), "Sending random velocity command:"
39           << " linear=" << msg_.linear.x
40           << " angular=" << msg_.angular.z);
41     }
42
43     bool change_vel_callback(const std::shared_ptr<foolturtle_server_interfaces::srv::ChangeVel::Request> request,
44              std::shared_ptr<foolturtle_server_interfaces::srv::ChangeVel::Response> response)
45     {
46        //add some extra random delay ranging from 1s to up to 4s
47        // Set the delay time in seconds (expressed as a float number)
48        float delay_seconds = 1.0+double(rand())*3/double(RAND_MAX);
49        RCLCPP_INFO_STREAM(get_logger(), "change_vel_callback will be delayed by " << delay_seconds );
50        // Convert the delay time to nanoseconds and create a Duration object
51        auto delay_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
52           std::chrono::duration<float>(delay_seconds));
53        std::this_thread::sleep_for(delay_duration);
54
55        //use the request values and set the response
56        maxvel_ = request->max_vel;
57        response->ret = true;
58        response->time_of_change = get_clock()->now().seconds();
59        RCLCPP_INFO_STREAM(get_logger(), "MAXIMUM VELOCITY CHANGED TO " << maxvel_ );
60        return true;
61     }
62
63     private:
64        rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pubvel_;
65        rclcpp::Service<foolturtle_server_interfaces::srv::ChangeVel>::SharedPtr change_vel_service_;
66        rclcpp::TimerBase::SharedPtr timer_;
67        geometry_msgs::msg::Twist msg_;
68        double maxvel_;
69  };
70
71  int main(int argc, char * argv[])
72  {
73     rclcpp::init(argc, argv);
74
75     auto node = std::make_shared<PublisherNode>();
76     rclcpp::spin(node);
77     rclcpp::shutdown();
78     return 0;
79  }

Exercise

Build the foolturtle_server package, run the turtlesim_node and pubsub nodes and call the service from the terminal:

colcon build --packages-up-to foolturtle_server
source install/setup.bash
ros2 run turtlesim turtlesim_node
ros2 run foolturtle_server pubvel
ros2 service call /change_vel foolturtle_server_interfaces/srv/ChangeVel "{max_vel: 5.0}"

The foolturtle_server_alone package

The foolturtle_server_alone package has the same functionality as the foolturtle_server package but now the definition of the service is done in the same foolturtle_server_alone package.

A client program

The service calls in ROS 2 services are asynchronous. The client thread is not stuck when calling the service, since the service call immediately return future, a value that indicates whether the call and response is finished (not the value of the response itself).

The future variable can be queried for the response in different ways, giving rise to different behaviors:

  • By waiting until a response is available, which makes the service call synchronous.

  • By iteratively checking the status of the variable

  • By using a callback to handle the response.

These are analyzed in the following subsections.

Synchronous calls

A synchronous client will block the calling thread when sending a request to a service until a response has been received; nothing else can happen on that thread during the call. The call can take arbitrary amounts of time to complete. Once complete, the response returns directly to the client.

To call a service in this way the rclcpp::spin_until_future_complete function should be used:

 1  auto result = service_client_->async_send_request(request);
 2  // Wait for the result.
 3  if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), result) ==
 4    rclcpp::FutureReturnCode::SUCCESS)
 5  {
 6    //Service called - Response received
 7    auto my_result = result.get();
 8    ....
 9  } else {
10    //Failed to call the service
11    ....
12  }

Take a look at the client.cpp file of the foolturtle_server package:

 1  #include "rclcpp/rclcpp.hpp"
 2  #include "foolturtle_server_alone/srv/change_vel.hpp"
 3
 4  using namespace std::chrono_literals;
 5
 6  class ClientNode : public rclcpp::Node
 7  {
 8     public:
 9     ClientNode() : Node("client_node")
10     {
11        //create the client
12        change_vel_service_client_ = create_client<foolturtle_server_alone::srv::ChangeVel>("change_vel");
13        //setup the scene
14        change_vel_turtle("turtle1", 2.0);
15     }
16     void change_vel_turtle(std::string turtlename, float maxvel)
17     {
18        auto request = std::make_shared<foolturtle_server_alone::srv::ChangeVel::Request>();
19        request->max_vel = maxvel;
20
21        //spawn_client_.
22        while (!change_vel_service_client_->wait_for_service(1s)) {
23           if (!rclcpp::ok()) {
24           RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
25           return;
26           }
27           RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "ChangeVel service not available, waiting again...");
28        }
29
30        auto result = change_vel_service_client_->async_send_request(request);
31        // Wait for the result.
32        if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), result) ==
33           rclcpp::FutureReturnCode::SUCCESS)
34        {
35           RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Velocity of turtle %s set to %f", turtlename.c_str(), maxvel);
36        } else {
37           RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service ChangeVel");
38        }
39     }
40
41     private:
42        rclcpp::Client<foolturtle_server_alone::srv::ChangeVel>::SharedPtr change_vel_service_client_;
43  };
44
45  int main(int argc, char * argv[])
46  {
47     rclcpp::init(argc, argv);
48
49     auto node = std::make_shared<ClientNode>();
50     //rclcpp::spin(node);
51
52     //Change the maxvel of the turtle each 5 seconds by calling the change_vel_turtle service
53     //with a random max value between 1 and 5.
54     rclcpp::Rate rate(0.2);
55     double maxvel;
56     while(rclcpp::ok()){
57        maxvel = 1.0+double(rand())*5/double(RAND_MAX);
58        rclcpp::spin_some(node);
59        RCLCPP_INFO( node->get_logger(), "MAX VEL set to %f", maxvel);
60        node->change_vel_turtle("turtle1", maxvel);
61        rate.sleep();
62     }
63
64     rclcpp::shutdown();
65     return 0;
66  }

Asynchronous calls

Async calls are entirely safe and the recommended method of calling services. They can be made from anywhere without running the risk of blocking other ROS and non-ROS processes, unlike sync calls.

One option is to wait for the answer for a while and then check the status of the variable future:

  • The method wait_for returns std::future_status::ready if a response has been received while waiting the specified amount of time; if this time is elapsed without response then the method returns std::future_status::timeout.

 1   auto result = service_client_->async_send_request(request);
 2   //auto status = result.wait_for(5s);//if the node is spinning
 3   auto status = result.wait_for(50ms);
 4   for(int i=0; i<100;i++){
 5     rclcpp::spin_some(this->get_node_base_interface());
 6     status = result.wait_for(50ms);
 7     if(status != std::future_status::ready) break;
 8   }
 9
10   bool success = false;
11
12   if (status == std::future_status::ready) {//get the result
13     auto my_result = result.get();
14     ....
15   }
16   else{// handle failure
17     ....
18   }

Another option is to iteratively check the status of the variable future in a loop, so as to allow the thread to keep doing things:

  • A while loop can be defined to keep testing for the response to be ready.

  • The method wait_for is used to define the while condition. The function waits for some small amount of time and if no response has been received then it returns std::future_status::timeout which allows you to enter the loop to do whatever you are interested in.

  • A global timeout has to be defined to be able not to get stuck forever if no response is received.

The code could be like this:

 1   auto result = service_client_->async_send_request(request);
 2   while (result.wait_for(some_amount_of_time) == std::future_status::timeout)
 3   {
 4     rclcpp::spin_some(this->get_node_base_interface());
 5
 6     //do something here like writing a log message before checking again
 7
 8     // check if the maximum timeout has been elapsed or not. If so, exit the waiting loop
 9     if ( elapsed > timeout) {
10       ...
11       break ;
12     }
13   }
14   if (not timed out){//get the result
15     auto my_result = result.get();
16     ....
17   }
18   else{ //handle failure
19     ....
20   }

Exercise

Take a look at the clientasync.cpp file of the foolturtle_server package.

A final option is to use a callback to handle the response:

  • The callback is defined by a lambda function.

  • The callback will handle the result when ready.

  • The thread can continue after the service call, it is not blocked.

 1   using ServiceResponseFuture =
 2     rclcpp::Client<foolturtle_server_alone::srv::ChangeVel>::SharedFutureWithRequest;
 3   //Define a lambda function to handle the response to the service call
 4   auto response_received_callback =
 5     [logger = this->get_logger()](ServiceResponseFuture future) {
 6       auto my_result = future.get();
 7       .....
 8     };
 9   //Service call that includes a callback function to capture the service call response.
10   auto result = service_client_->async_send_request(
11     request, std::move(response_received_callback));
12   //The thread can continue after the call, it is not blocked.

Exercise

Take a look at the clientasync2.cpp file of the foolturtle_server package.

Exercise

Run the different clients to check how they behave when calling to the change_vel service, which has been coded to add an arbitrary delay in sending the response.

  • The sync client gets stuck waiting for the response.

  • The async client keeps printing messages while waiting for the response.

  • The async2 client just calls the service and continues.

ros2 launch foolturtle_server foolturtle_server_sync_client_launch.py
ros2 launch foolturtle_server foolturtle_server_async_client_launch.py
ros2 launch foolturtle_server foolturtle_server_async2_client_launch.py

Callbacks and Callback Groups

Execution management in ROS 2 is handled by Executors, as discussed in Section Executors. The role of an executor is to check for available work and to execute it. An Executor uses one or more threads of the underlying operating system to invoke the callbacks of subscriptions, timers and services (as seen in previous sections), on incoming messages and events.

Callbacks are part of the code that can be executed only by an executor, or by an invocation of one of the spinning functions (spin, spin once, and spin until future complete).

This section discusses how to correctly manage the callback execution in order to:

  • avoid possible deadlocks that may appear because one callback, whose response is needed, cannot be executed because the executor is busy waiting for that response,

  • avoid runtime errors that appear if we call functions like spin_some or spin_until_future_complete from within a node that has already been associated with an executor.

The execution of callbacks can be controlled with Callback Groups, which are of two types:

  • MutuallyExclusive: the callbacks added to this group cannot be executed simultaneously; this can be used to avoid data-races when accessing shared data without the need of using mutexes.

  • Reentrant: callbacks in this kind of group can be executed concurrently by different threads; this can be used to avoid deadlock situations.

By default, a ROS 2 application uses only one MutuallyExclusive callback group, even if associated to a multithreaded executor, which can lead to deadlocks.

To solve this we should create additional callback groups and use a Multithreaded spinner. If the callbacks that need to be executed concurrently or with parallelism are stored in separated groups, or into a group of type “Reentrant”, the spinner will be able to allocate different threads for each of them.

Obviously, the preferred option would be to avoid blocking operations within callbacks, i.e. if the code is written in a way that guarantees that each callback is executed quickly without ever waiting for other callbacks to occur, deadlocks simply won’t occur. But if blocking operations cannot be avoided, the use of Callback Groups is the solution.

As an example the following snippet shows the definition of a Callback Group of time reentrant, and a service that uses it because its callback has a blocking operation.

1  //Callback group cb_group_ defined as Reentrant so as to allow the callbacks to be executed concurrently
2  //see https://leanpub.com/averyinformaljourneythroughros2 section 3.3
3  this->cb_group_ = create_callback_group(rclcpp::CallbackGroupType::Reentrant);
4
5  //Service to control state of the gripper
6  GripperOrder_service_ = create_service<test_hardware_package_ros2::srv::GripperOrder>(
7     "GripperOrder",
8     std::bind(&GripperServerNode::handle_GripperOrder, this, std::placeholders::_1, std::placeholders::_2), rmw_qos_profile_services_default, this->cb_group_);

And the definition of the executor is:

 1int main(int argc, char *argv[])
 2{
 3   rclcpp::init(argc, argv);
 4   auto node = std::make_shared<GripperServerNode>();
 5
 6   //Use multithread executor to handle the callback groups
 7   rclcpp::executors::MultiThreadedExecutor executor;
 8
 9   executor.add_node(node);
10   executor.spin();
11
12   rclcpp::shutdown();
13   return 0;
14}

Take a look at the following example of the A very informal journey through ROS 2 book, where there is an application with two nodes: the first one starts a timer and, twice per second, asks the second node if it’s alive using a service. The second node simply hosts the is alive service. The code is: https://github.com/nirwester/ros2_journey_examples/tree/master/callbacks_and_executors

EXERCISE 4

To solve Exercise 4 you must modify the urdf_tutorial package you implemented in exercise 3 that you have cloned in your git-projects/exercise4 folder.

Recall to keep commiting the changes when coding the solution (do small commits with good commit messages!) and edit the README.md file in the package explaining your solution (Markdown Cheat Sheet).

Exercise

Modify exercise 3 so as not to broadcast a static grasp transform but to broadcast a dynamic grasp transform set by a service.

  1. Implement a urdf_tutorial_interfaces package to define the service with the following SetGraspTransform.srv file:

int32 object_id
geometry_msgs/TransformStamped object_grasp_transform
---
bool ret
  1. Implement a node in a file called custom_grasps_tf2_broadcaster.cpp that offers the service called set_grasp_transform to broadcast a transform called ur_grasp to grasp a given object.

  2. Implement a client in a file called demo_grasp_client.cpp that:

    1. Calls the service set_grasp_transform to set the following grasp transform between object 12 frame (tf_12) and the ur_grasp frame:

      (x,y,z,R,P,Y)=(0, 0, 0.15, 3.14, 0.0, 0.0)

    2. Calls the inverse kinematics service kinenik_server of package kinenikros2 to compute the joint values to put the robot end effector

    3. Publishes the joint values

../_images/ex4.png