Services¶
In this exercise, we will create a custom service by defining a .srv file. Then we will write server and client nodes to utilize this service.
Motivation¶
The first type of ROS communication that we explored was a one-way interaction called messages which are sent over channels called topics. Now we are going to explore a different communication type, which is a two-way interaction via a request from one node to another and a response from that node to the first. In this module we will create a service server (waits for request and comes up with response) and client (makes request for info then waits for response).
Reference Example¶
Further Information and Resources¶
Scan-N-Plan Application: Problem Statement¶
We now have a base ROS node which is subscribing to some information and we want to build on this node. In addition we want this node to serve as a sub-function to another “main” node. The original vision node will now be responsible for subscribing to the AR information and responding to requests from the main workcell node.
Your goal is to create a more intricate system of nodes:
Update the vision node to include a service server
Create a new node which will eventually run the Scan-N-Plan App
First, we’ll create the new node (myworkcell_core) with only a simple service client. We will later add more code to build up the full application control node.
Scan-N-Plan Application: Guidance¶
Create Service Definition¶
Similar to the message file located in the fake_ar_publisher package, we need to create a service file. The following is a generic structure of a service file:
#request --- #response
Create a folder called
srvinside yourmyworkcell_corepackage (at same level as the package’ssrcfolder)cd ~/ros2_ws/src/myworkcell_core mkdir srv
Create a file (gedit or QT) called
LocalizePart.srvinside thesrvfolder.Inside the file, define the service as outlined above with a request of type
stringnamedbase_frameand a response that has ageometry_msgs/Posefield namedpose, and a boolean field namedsuccess:#request string base_frame --- #response geometry_msgs/Pose pose bool success
It is good practice to include a response field that explicitly indicates success or failure of the service call. Services often also include a string field that can provide a more detailed description of the error to the calling client.
Edit the package’s
CMakeLists.txtandpackage.xmlto add dependencies on key packages:The
rosidl_default_generatorspackage is required to build C++ code from the .srv file created in the previous stepThe
rosidl_default_runtimepackage provides runtime dependencies for new messagesgeometry_msgsprovides thePosemessage type used in our service definition
Edit the package’s
CMakeLists.txtfile to add the new build-time dependencies alongside the existingfind_packagerules:find_package(geometry_msgs REQUIRED) find_package(rosidl_default_generators REQUIRED)
Edit the
package.xmlfile to add the appropriate build/run dependencies:<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <depend>geometry_msgs</depend>
Also add a special tag that tells the ROS build system this package defines ROS interfaces.
<member_of_group>rosidl_interface_packages</member_of_group>
This is generally placed with the package metadata, before the dependencies.
Edit the package’s
CMakeLists.txtto add rules to generate the new service files:The following CMake function is invoked with the service file you created and creates the target to generate the interface-specific code:
rosidl_generate_interfaces(${PROJECT_NAME} "srv/LocalizePart.srv" DEPENDENCIES geometry_msgs )
Right after
rosidl_generate_interfaces, add the following line to enable executables in this package to access your generated messages.rosidl_get_typesupport_target(cpp_typesupport_target ${PROJECT_NAME} "rosidl_typesupport_cpp")
Add the following line after the call to
ament_target_dependenciesfor the the vision_node. This make the vision_node target depend on the generated interface target so that the generated code can be used in the node:target_link_libraries(vision_node "${cpp_typesupport_target}")
Now you have a service defined in you package and you can attempt to build the code to generate the service:
colcon build
Service Server¶
Edit
vision_node.cpp; remember that the ros documentation is a resource.Add the header for the service we just created
#include <myworkcell_core/srv/localize_part.hpp>
_Note the slightly different naming convention used for the Service Definition (
LocalizePart) and generated header file (localize_part.hpp).
In the
Localizerclass, create a callback method namedlocalizePart. Remember that your request and response types were defined in theLocalizePart.srvfile. The arguments to the callback function are the request and response type, with the general structure ofPackage::ServiceName::Request::SharedPtrorPackage::ServiceName::Response::SharedPtr.void localizePart(myworkcell_core::srv::LocalizePart::Request::SharedPtr req, myworkcell_core::srv::LocalizePart::Response::SharedPtr res) { }
Now add code to the
localizePartcallback function to fill in the Service Response. Eventually, this callback will transform the pose received from thefake_ar_publisher(invisionCallback) into the frame specifed in the Service Request. For now, we will skip the frame-transform, and just pass through the data received fromfake_ar_publisher. Copy the pose measurement received fromfake_ar_publisher(saved tolast_msg_) directly to the Service Response.void localizePart(myworkcell_core::srv::LocalizePart::Request::SharedPtr req, myworkcell_core::srv::LocalizePart::Response::SharedPtr res) { // Read last message fake_ar_publisher::msg::ARMarker::SharedPtr p = last_msg_; if (! p) { RCLCPP_ERROR(this->get_logger(), "no data"); res->success = false; return; } res->pose = p->pose.pose; res->success = true; }
Now we need to create the server object that interfaces between this callback function and other ROS nodes. Add a
roscpp::Servicemember variable namedserver_near the otherLocalizerclass member variables:rclcpp::Service<myworkcell_core::srv::LocalizePart>::SharedPtr server_;
In the
Localizerclass constructor, initialize the server object:server_ = this->create_service<myworkcell_core::srv::LocalizePart>( "localize_part", std::bind(&Localizer::localizePart, this, std::placeholders::_1, std::placeholders::_2));
Note the use of
std::bindagain to reference a callback function that is a method of a class object. The use of twostd::placeholdersarguments indicates the callback will have two arguments when it is called.You should also comment out the
RCLCPP_INFOcall in yourvisionCallbackfunction, to avoid cluttering the screen with useless info.Build the updated
vision_node, to make sure there are no compile errors. You may see a warning about unusedService::reqinput… this is expected, since we haven’t yet used the request data in our callback function.
Service Client¶
Create a new node (inside the
myworkcell_core/srcdirectory), namedmyworkcell_node.cpp. This will eventually be our main “application node”, that controls the sequence of actions in our Scan & Plan task. The first action we’ll implement is to request the position of the AR target from the Vision Node’sLocalizePartservice we created above.Be sure to include the standard ros header as well as the header for the
LocalizePartservice:#include <rclcpp/rclcpp.hpp> #include <myworkcell_core/srv/localize_part.hpp>
Create a standard C++ main function, with typical ROS initialization:
int main(int argc, char **argv) { // This must be called before anything else ROS-related rclcpp::init(argc, argv); rclcpp::shutdown(); return 0; }
Following best-practice guidance, we will create a new
ScanNPlanclass (derived fromrclcpp::Node) to contain the functionality of our new node. Create a skeleton structure of this class, with an empty constructor and a private area for some internal/private variables. Make sure to place this above yourmainfunction.class ScanNPlan : public rclcpp::Node { public: ScanNPlan() : Node("scan_n_plan") { } private: // Planning components };
Within your new ScanNPlan class, define a ROS Service Client as a private member variable of the class. This client object will allow us to connect to the vision node’s Service Server. Initialize the Client in the ScanNPlan constructor. Both the service type (in the variable declaration) and the service name (in the object constructor) should match what we set up in the vision_node server earlier.
class ScanNPlan : public rclcpp::Node { public: ScanNPlan() : Node("scan_n_plan") { vision_client_ = this->create_client<myworkcell_core::srv::LocalizePart>("localize_part"); } private: // Planning components rclcpp::Client<myworkcell_core::srv::LocalizePart>::SharedPtr vision_client_; };
Create a void function within the ScanNPlan class named
start, with no arguments. This will eventually contain most of our application workflow.class ScanNPlan : public rclcpp::Node { public: ... void start() { RCLCPP_INFO(get_logger(), "Attempting to localize part"); } ... };
When the node is first created, it takes a few seconds to connect to other nodes and detremine what services and topics are available. Attempting to call the service during this time may cause the call to hang indefinitely. Add a call to wait for the service to be available before submitting the request:
class ScanNPlan : public rclcpp::Node { ... void start() { RCLCPP_INFO(get_logger(), "Attempting to localize part"); // Wait for service to be available if (!vision_client_->wait_for_service(std::chrono::seconds(5))) { RCLCPP_ERROR(get_logger(), "Unable to find localize_part service. Start vision_node first."); return; } ... };
Now add code to prepare the request data and call the service. For now, the request fields are left empty. They will be added in a later exercise.
class ScanNPlan : public rclcpp::Node { public: ... void start() { RCLCPP_INFO(get_logger(), "Attempting to localize part"); // Wait for service to be available if (!vision_client_->wait_for_service(std::chrono::seconds(5))) { RCLCPP_ERROR(get_logger(), "Unable to find localize_part service. Start vision_node first."); return; } // Create a request for the LocalizePart service call auto request = std::make_shared<myworkcell_core::srv::LocalizePart::Request>(); auto future = vision_client_->async_send_request(request); } ... };
The
async_send_requestfunction is used on thevision_client_object to initiate the service call. Notice that it doesn’t return a response object, but instead, something called afuture. This is because the function immediately returns after sending the request and does not wait for a response from the server. It is our responsibility to wait for the response to arrive, which will be known by the future object becoming ‘complete’.Now add the code that will wait until the service response arrives and then prints the response. This is a bit complex, because we are turning the asynchronous ROS2 service call into a synchronous blocking call.
void start() { RCLCPP_INFO(get_logger(), "Attempting to localize part"); // Wait for service to be available if (!vision_client_->wait_for_service(std::chrono::seconds(5))) { RCLCPP_ERROR(get_logger(), "Unable to find localize_part service. Start vision_node first."); return; } // Create a request for the LocalizePart service call auto request = std::make_shared<myworkcell_core::srv::LocalizePart::Request>(); auto future = vision_client_->async_send_request(request); if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) != rclcpp::FutureReturnCode::SUCCESS) { RCLCPP_ERROR(this->get_logger(), "Failed to receive LocalizePart service response"); return; } auto response = future.get(); if (! response->success) { RCLCPP_ERROR(this->get_logger(), "LocalizePart service failed"); return; } RCLCPP_INFO(this->get_logger(), "Part Localized: x: %f, y: %f, z: %f", response->pose.position.x, response->pose.position.y, response->pose.position.z); }
Note that the waiting function is a form of spinning, the same as the
rclcpp::spinfunction used in the vision_node. This spin function allows the backend ROS code to run, listening for the incoming service response and managing other node-maintenance tasks.Important: You can trigger deadlocks if you call ros “spin” functions simultaneously in multiple places. For example, you may need a more complex “spin” design if you are attempting to wait for a service response inside a subscriber’s callback function. There are ways around this limitation but it is beyond the scope of this material.
An alternative to the synchronous wait shown above is to provide a callback function along with the service request. For some applications, this asynchronous design might make more sense.
Now back in
myworkcell_node’smainfunction, instantiate an object of theScanNPlanclass and call the object’sstartfunction.// Create the ScanNPlan node auto app = std::make_shared<ScanNPlan>(); app->start();
Edit the package’s
CMakeLists.txtto build the new node (executable), with its associated dependencies. Add the following rules to the appropriate sections, directly under the matching rules forvision_node:add_executable(myworkcell_node src/myworkcell_node.cpp) ament_target_dependencies(myworkcell_node rclcpp) target_link_libraries(myworkcell_node "${cpp_typesupport_target}")
Also add
myworkcell_nodeto the existing call toinstallalong withvision_node:install(TARGETS vision_node myworkcell_node ... )
Build the nodes to check for any compile-time errors:
colcon build
Use New Service¶
Enter each of these commands in their own terminal:
ros2 run fake_ar_publisher fake_ar_publisher_node ros2 run myworkcell_core vision_node ros2 run myworkcell_core myworkcell_node
If you get errors that the ros2 commands can’t find certain shared objects or executables, try re-sourcing the setup file (
source ~/ros2_ws/install/setup.bash). This is required since we added a new service-definition and a new node.The values reported by the localization service should match the values published by the fake_ar_publisher node that you observed earlier (through
ros2 topic echoorrqt_plot).