Make discrete requests between ROS 1 nodes for one-off tasks
In our previous ROS 1 basics tutorial, we covered how ROS nodes communicate with each other by publishing and subscribing to message streams on topics.
Another common way nodes can share information with each other is via services. With ROS 1 services, one or many "client" nodes can make requests to a "server" node and wait for its response. These make services great for performing on-demand tasks – like performing on-the-fly computations or simple one-time tasks.
In this tutorial, we'll learn how to build a client and server node in C++, and implement a service for them to pass data to each other.
Unlike topics, which follow a publish-subscribe model to send each other information, services follow a request-response model.
With services, a client node must first call the server in order to send a request. This means that in the server-client mode, nodes do not use a communication stream until it’s absolutely needed, preventing unnecessary bandwidth usage. Then, the client node must wait for a response from the server (or until a timeout is reached). For this reason, services can confirm communication between nodes.
Services consist of two parts - a request and a response. Each part can be empty or contain as many data fields that you want to share between nodes.
To define a service, you must create a .srv
file in your ROS 1 package’s srv
directory that contains both those components:
# Request
---
# Response
Let’s start by exploring three different service definitions in the std_srvs
package:
Empty.srv
– Empty request and response. The only data that is passed from client to server is the call itself.Trigger.srv
– Empty request and a response with success
and message
fields. This allows the server to send more information to the client.SetBool.srv
– Request with data
field and response with success
and message
fields.The more fields a service contains, the more information the client and server can exchange with each other.
Let’s suppose you want to create a routine check for your robot – for example, you may want to verify at start-up that your robot's servo motors are all working properly.
If you were to create a publisher for this task, your node could publish its check results before a corresponding subscriber becomes available. As a result, you'd miss the check results and be left in the dark about the status of your motors.
But if you were to create a service instead, your server node wouldn't broadcast any data before it is explicitly requested by a client node. Using a service ensures that you do not miss crucial information due to inopportune timing.
In the service scenario, the client node could be a diagnostics node that gathers robot state data at start-up. The server node could be a motor node that controls the robot’s servo motors. If all goes well, and the actuators are working properly, the server node would deliver a response saying that all checks passed. If any motors encountered an issue, however, the response would raise an alarm.
The client will wait for a response within a specified timeout period.
Now that we’ve discussed how when you might use ROS services, let’s dive into some actual code.
Let's use the Trigger.srv
we saw before. Remember that this service definition contains no field in the request. The client is not sending any data to the server when it makes this request - it is just a trigger. The server, however, will respond with success
and message
fields.
We’ll begin by creating our own ROS 1 package. Navigate to the src
folder of your ros1_ws
and create a package named srv_client_checks
:
$ cd ros1_ws/src/
$ catkin_create_pkg srv_client_checks roscpp std_srvs
language-bash
Let's start out with our server motor_node, which will run the necessary check on the servo motors.
Go to the src
folder of your new package and create a file named motor_node.cpp
:
// Include the ROS library and trigger service type
#include "ros/ros.h"
#include "std_srvs/srv/trigger.h"
language-cpp
Define two functions: moveMotorToMinAndMax
and doChecks
.
moveMotorToMinAndMax
will simulate the motors moving to their maximum and minimum positions. If the motor successfully reaches both limits, the function will return true
; otherwise, it will return false
.
bool moveMotorToMinAndMax(int motor_id){
bool reached_min = false;
bool reached_max = false;
// Add code here that moves the motor to its min and max positions
if (reached_min && reached_max){
return true;
} else {
return false;
}
}
language-cpp
The doChecks
server callback receives two inputs (a request
and response
) and will run when the server is activated by a client. It iterates over all available motors to perform the moveMotorToMinAndMax
check on each one, then sends back a response:
// ROS helps pass the request and response between client and server
void doChecks(std_srvs::srv::Trigger::Request &request, std_srvs::srv::Trigger::Response &response){
// Prepare response
response.success = true;
response.message = "";
ROS_INFO("Received request to check motors...");
// Iterate over all motors (e.g. 5) to perform the check
for (int i = 0; i < 5; i++) {
ROS_INFO("Checking motor %i",i);
auto res = moveMotorToMinAndMax(i);
// If something fails, change response `success` to false and add info to the `message`
if (!res) {
response.success = false;
response.message += "\nMotor"+std::to_string(i)+" Failed! - ";
}
}
ROS_INFO("Sending back response...");
// Always return true if call succeeded
return true;
}
language-cpp
Finally, we create a main
function as our entrypoint:
int main(int argc, char **argv) {
// Initiate the ROS1 library, passing the node name
ros::init(argc, argv, "motor_node");
// Create a NodeHandle
ros::NodeHandle n;
// Create the "checks" service with a doChecks callback
ros::ServiceServer service = n.advertiseService("checks", doChecks);
ROS_INFO("Ready to check motors");
// Spin the node until it's terminated
ros::spin(node);
ros::shutdown();
return 0;
}
language-cpp
Now that we have a server node, let's move on to the client node that will request a response from it.
Create a diagnostics_node.cpp
file in your package's src
folder, and include the following libraries:
#include "ros/ros.h"
#include "std_srvs/srv/trigger.h"
language-cpp
Let's start with our main
function:
int main(int argc, char **argv) {
// Initiate the ROS library, passing the node name
ros::init(argc, argv, "diagnostics_node");
// Create a NodeHandle
ros::NodeHandle n;
// Create client inside the node to call our "checks" server node
ros::ServiceClient client=n.serviceClient<std_srvs::Trigger>("checks");
language-cpp
Next, let’s create a request that the client can send to the server using the Trigger.srv
service:
// Create the request, which is empty
std_srvs::Trigger srv_call;
language-cpp
Before we send the request, let's make sure the server is active – otherwise, our request will be lost. The function wait_for_service()
requires a timeout, which is the maximum time a client will wait for a response.
// Wait for the server for a maximum of 5 seconds
ros::service::waitForService("checks", ros::Duration(5));
language-cpp
Once the server is on, the program will continue and send a request:
if (client.call(srv_call)) {
ROS_INFO("Server call successful! Response was %d: %s", srv_call.response.success, srv_call.response.message.c_str());
} else {
ROS_ERROR("Failed to call 'checks' service");
}
return 0;
}
language-cpp
If client.call(srv_call)
returns true
, we know that the server has responded correctly, and we can process that response. Otherwise, we'll get an error message telling us that we failed to call the "checks" service.
Add the executables to your compilation file, and install them in your path, so that the rosrun
command can find them.
In the CMakeList.txt
file, make sure that you've the following:
# Tell compiler that we require the roscpp and std_srvs packages
find_package(catkin REQUIRED COMPONENTS
roscpp
std_srvs
)
# IMPORTANT: Executables must be defined after these lines
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES srv_client_checks
# CATKIN_DEPENDS roscpp std_srvs
# DEPENDS system_lib
)
# Add the nodes as executables with their dependencies
add_executable(motor_node src/motor_node.cpp)
target_link_libraries(motor_node ${catkin_LIBRARIES})
add_executable(diagnostics_node src/diagnostics_node.cpp)
target_link_libraries(diagnostics_node ${catkin_LIBRARIES})
Return to the root folder of your workspace to compile with your new nodes:
$ cd ros1_ws/
$ catkin_make
language-bash
We can finally run both our client and server nodes to see if they can communicate with each other properly!
First, start up your ROS master in a terminal window:
$ roscore
language-bash
You will need two other terminal windows – remember to source install/setup.bash
in each one before running any ROS 1 command:
$ cd ros1_ws/
$ source install/setup.bash
language-bash
The first terminal will contain the client diagnostics_node
:
$ rosrun srv_client_checks diagnostics_node
language-bash
This node will wait for motor_node
to appear before requesting the checks.
Next, run the server motor_node
to execute the checks:
$ rosrun srv_client_checks motor_node
language-bash
When motor_node
executes and the server activates, it performs its checks. In this case, a warning shows because we don't have any motors – there is no possible way for the checks to succeed!
Left: Client diagnostics_node
. Right: Server motor_node
.
You've now created a server-client communication that checks your actuators and sensors during start-up, making sure that everything is working properly before continuing.
Continue playing around with running your nodes to see how they behave. For example, what would you expect to happen if you run the server node before the client?
We hope you enjoyed this tutorial and now have a better understand of how you can use ROS 1 services in your own robotics project. Continue learning with our ROS tutorials and other articles on our blog.
As always, feel free to check our docs or reach out to us directly in our Discord community to learn how our tools can help you accelerate your ROS development!