Tutorial 6: Communications using actions
Objective: This tutorial session is devoted to learn the ROS synchronous request/response remote procedure calls with feedback and preemption.
Contents
After this session you will be able to:
Understand the action files and know how to configure the package to use actions.
Understand the action server mechanism: the action server object and the callback functions.
Understand the action client mechanism: the client object, the goal setting and the server preemption.
Write nodes able to call/offer an action service.
References:
This tutorial is partially based on the following resources:
ROS 2 Documentation - Concepts: Actions
ROS 2 Design - Articles: Actions
ROS 2 Documentation - Tutorials: Understanding actions
ROS 2 Documentation - Tutorials: Creating an action
ROS 2 Documentation - Tutorials: Writing an action server and client (C++)
Provided packages:
This tutorial will use the following package:
Package my_action_tutorials_cppPackage my_action_tutorials_interfacesPackage pantilt_action_tutorials_cppPackage pantilt_action_tutorials_interfaces
Prepare the colcon workspace to work with it:
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_ws6 for the current tutorial).
In your colcon_ws6/src folder create a symbolic link to the packages to be used:
ln -s ~/data/git-projects/all_introduction_to_ros2_packages/my_action_tutorials_cpp ln -s ~/data/git-projects/all_introduction_to_ros2_packages/my_action_tutorials_interfaces ln -s ~/data/git-projects/all_introduction_to_ros2_packages/pantilt_action_tutorials_cpp ln -s ~/data/git-projects/all_introduction_to_ros2_packages/pantilt_action_tutorials_interfaces
Communication features
Actions are one of the communication types in ROS 2, together with services and topics.
Actions are to be used for long-running remote procedure calls because of their features:
they provide feedback,
the have the ability to cancel or preempt the goal.
For short running remote procedure calls, services should be considered instead (see Tutorial 4: Communications using services).
Communication using actions requires:
The action server: The action server is the entity that will accept the remote procedure request and perform some procedure on it. It is also responsible for sending out feedback as the action progresses and should react to cancellation/preemption requests. The action server maintains a state machine for each goal it accepts from a client:
The action client: An action client is an entity that will request a remote action server to perform a procedure on its behalf.
The action messages consist of three parts, as reflected in the Action Interface Definition:
int32 request
---
int32 response
---
int32 feedback
Actions are built on topics and services:
Send Goal Service: The purpose of this service is to send a goal to the action server.
Request: description of the goal and a Universally Unique Identifier (UUID) for the goal ID
Response: whether goal was accepted or rejected and the time when the goal was accepted
Cancel Goal Service: The purpose of this service is to request the cancellation of one or more goals on the action server.
Request: goal ID and timestamp
Response: response code and a list of goals that have transitioned to the CANCELING state
Get Result Service: The purpose of this service is to get the final result of a goal.
Request: goal ID
Response: status of goal and user defined result
Goal Status Topic: This topic is published by the server to broadcast the status of goals it has accepted. The purpose of the topic is for introspection; it is not used by the action client. Messages are published when transitions from one status to another occur. The possible statuses are:
Accepted - The goal has been accepted by the action server
Executing - The goal is currently being executing by the action server
Canceling - The action server will try to cancel the indicated goal
Succeeded - The action server successfully reached the goal
Aborted - The action server failed reached the goal
Canceled - The action server successfully canceled the goal
Feedback Topic: This topic is published by the server to send progress feedback about the goal that is user-defined as part of the Action Interface Definition.
The flow of communications are illustrated in the following animated gig figure; the “action client” node sends a goal to the “action server” node that acknowledges the goal and returns a stream of feedback and a result:
Using actions
From the command line
Actions, like topics and services, are introspectable from the command line.
The command line tool, ros2 action, will be able to:
list action names associated with any running action servers or action clients
list action servers and action clients
display active goals on an action server
display the arguments for an action goal
display the type of an action’s goal, feedback, and result
find actions by action type
echo feedback, status, and result for an action goal
call an action, display feedback as it is received, display the result when received, and cancel the action (when the tool is terminated prematurely).
The turtlesim_node (from the turtlesim package) offers an action service to rotate the turtle. Run the node and check the action and its type:
ros2 action list -t
ros2 action info /turtle1/rotate_absolute
ros2 interface show turtlesim/action/RotateAbsolute
Now let’s send an action goal from the command line:
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 1.57}"
We can see the feedback by adding --feedback to the call:
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback
Exercise
In a terminal run a call to the action and in a second terminal run immediately after a similar call but with a different sign. Is the first call being aborted when the second call arrives or is the second call ignored because the server was busy with the first call? The behaviors of the server can be programmed in any sense.
The Fibonacci action example
The following example, taken from the ROS 2 Tutorial Writing an action server and client (C++) uses two packages:
The my_action_tutorials_cpp package that implements an action server that computes the Fibonacci sequence and an action client that calls it.
The my_action_tutorials_interfaces that defines the action Fibonacci.action:
int32 order --- int32[] sequence --- int32[] partial_sequence
Exercise
Build the packages and runt the server and the client:
colcon build --packages-up-to my_action_tutorials_cpp
ros2 run my_action_tutorials_cpp fibonacci_action_server
ros2 run my_action_tutorials_cpp fibonacci_action_client
The action server and the action client will be explored in the next sections.
Action interfaces
It is a good practice to define the interfaces of a package (the custom messages, services and actions) in a separate package, as we did for messages (see section Custom messages) and for services (see section Defining services in a dedicated 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, services and actions 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:
# Add the following lines to generate the interfaces (msg and srv)
find_package(rosidl_default_generators REQUIRED)
set(msg_files
"msg/MyMessage.msg"
)
set(srv_files
"srv/MyService.srv"
)
set(action_files
"action/MyAction.action"
)
rosidl_generate_interfaces(${PROJECT_NAME}
${msg_files}
${srv_files}
${action_files}
#DEPENDENCIES xxxxxx Add here any package that above messages may depend on, e.g. geometry_msgs
)
And the package.xml file should have the following lines:
<!--Add the following four lines to generate custom messages and services-->
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Exercise
Take a look at the package my_action_tutorials_interfaces:
the action folder
the CMakeLists.txt file
the package.xml file
Exercise
Build the my_action_tutorials_interfaces package and check the availability of the action Fibonacci:
colcon build --packages-up-to my_action_tutorials_interfaces
ros2 interface show my_action_tutorials_interfaces/action/Fibonacci
Then, the package using these interfaces should include the dependency in the CMakeLists.txt file:
find_package(my_package_interfaces REQUIRED)
set(dependencies
....
my_package_interfaces
)
And also in the package.xml the dependency on this package should be made explicit:
<depend>my_package_interfaces</depend>
Exercise
Take a look at the package my_actions_tutorial_cpp that will use the actions defined in my_action_tutorials_interfaces:
the CMakeLists.txt file
the package.xml file
The Action Client Library: rclcpp_action
Action servers and action clients use the ROS Action Client library rclcpp_action.
Main classes
The class rclcpp_action::Server< ActionT > creates an action server for the action ActionT using
rclcpp_action::create_server().This constructor specifies:
The templated action type name
The action name
A callback function for handling goals petitions (
handle_goal) and decide whether to reject or to accept (and execute) them.A callback function for handling cancellation (
handle_cancel) and decide whether to cancel it or not.A callback function for handling a goal acceptance (
handle_accept) and call the execute function to execute it.
-
This class is used to interact with goals on a server (i.e. to check the status of a goal as well as set the result). The Server will create an instance of this class when a goal has been accepted and give it to the user in their handle_accepted callback.
Its main Public Member Functions are:
publish_feedbackto send an update about the progress of a goal.abortto indicate that a goal could not be reached and has been aborted.succeedto indicate that a goal has succeeded.canceledto indicate that a goal has been canceled.executeto indicate that the server is starting to execute a goal.get_goalto get the user provided message describing the goal.
The class rclcpp_action::Client< ActionT > creates an action client for the action ActionT using
rclcpp_action::create_client().It contains the following Public Member Functions to interact with the server:
async_send_goal: To send an action goal and asynchronously get the result.
async_get_result: To asynchronously get the result for an active goal.
async_cancel_goal: To asynchronously request a goal be canceled.
async_cancel_all_goals: To asynchronously request for all goals to be canceled.To configure the
async_send_goalfunction, the following parameters are set using the SendGoalOptions struct:
goal_response_callback: Function called when the goal is accepted or rejected.
feedback_callback: Function called whenever feedback is received for the goal.
result_callback: Function called when the result for the goal is received.
-
This class is used to interact with goals sent from action clients (i.e. to check the status of a goal as well as set the result). The Client will create an instance of this class and return it to the user (via a future) after calling Client::async_send_goal.
How to use the library
When creating a package that has nodes that implement action servers or an action clients it is required to:
In the c++ files include the following header file:
#include "rclcpp_action/rclcpp_action.hpp"
In the CMakeLists.txt include the dependency of rclcpp_action:
find_package(rclcpp_action REQUIRED) set(dependencies rclcpp rclcpp_action .... )
In the package.xml include the dependency of rclcpp_action:
<depend>rclcpp_action</depend>
An action server example
Take a look at the my_action_tutorials_cpp/src/fibonacci_action_server.cpp file shown below.
Note the constructor arguments and the callback functions implementation:
The constructor of the class FibonacciActionServer specifies:
The templated action type name:
Fibonacci.A ROS 2 node to add the action to:
this.The action name:
fibonacci.A callback function for handling goals:
handle_goalA callback function for handling cancellation:
handle_cancel.A callback function for handling goal accept:
handle_accept.
Regarding the callback functions:
The implementation of the
handle_goalcallback just accepts all goals.The implementation of the
handle_cancelcallback just accepts the cancellation.The implementation of the
handle_acceptaccepts a new goal and starts processing in a separate thread where the execute function does the work, i.e. to process one sequence number of the Fibonacci sequence every second and publish a feedback update for each step.
#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
class FibonacciActionServer : public rclcpp::Node
{
public:
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;
FibonacciActionServer()
: Node("fibonacci_action_server")
{
using namespace std::placeholders;
this->action_server_ = rclcpp_action::create_server<Fibonacci>(
this,
"fibonacci",
std::bind(&FibonacciActionServer::handle_goal, this, _1, _2),
std::bind(&FibonacciActionServer::handle_cancel, this, _1),
std::bind(&FibonacciActionServer::handle_accepted, this, _1));
}
private:
rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const Fibonacci::Goal> goal)
{
RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order);
(void)uuid;
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandleFibonacci> goal_handle)
{
RCLCPP_INFO(this->get_logger(), "Received request to cancel goal");
(void)goal_handle;
return rclcpp_action::CancelResponse::ACCEPT;
}
void handle_accepted(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
{
using namespace std::placeholders;
// this needs to return quickly to avoid blocking the executor, so spin up a new thread
std::thread{std::bind(&FibonacciActionServer::execute, this, _1), goal_handle}.detach();
}
void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
{
RCLCPP_INFO(this->get_logger(), "Executing goal");
rclcpp::Rate loop_rate(1);
const auto goal = goal_handle->get_goal();
auto feedback = std::make_shared<Fibonacci::Feedback>();
auto & sequence = feedback->partial_sequence;
sequence.push_back(0);
sequence.push_back(1);
auto result = std::make_shared<Fibonacci::Result>();
for (int i = 1; (i < goal->order) && rclcpp::ok(); ++i) {
// Check if there is a cancel request
if (goal_handle->is_canceling()) {
result->sequence = sequence;
goal_handle->canceled(result);
RCLCPP_INFO(this->get_logger(), "Goal canceled");
return;
}
// Update sequence
sequence.push_back(sequence[i] + sequence[i - 1]);
// Publish feedback
goal_handle->publish_feedback(feedback);
RCLCPP_INFO(this->get_logger(), "Publish feedback");
loop_rate.sleep();
}
// Check if goal is done
if (rclcpp::ok()) {
result->sequence = sequence;
goal_handle->succeed(result);
RCLCPP_INFO(this->get_logger(), "Goal succeeded");
}
}
}; // class FibonacciActionServer
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<FibonacciActionServer>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
An action client example
Take a look at the my_action_tutorials_cpp/src/fibonacci_action_client.cpp shown below.
Note the constructor arguments and the callback functions implementation:
The constructor of the class FibonacciActionClient specifies:
The templated action type name:
Fibonacci.A ROS 2 node to add the action client to:
this.The action name:
fibonacci.And creates a ROS timer that will kick off the one and only call to
send_goalthat:
Cancels the timer (so it is only called once).
Waits for the action server to come up.
Instantiates a new Fibonacci::Goal.
Sets the response, feedback, and result callbacks.
Sends the goal to the server.
Regarding the callback functions:
The
goal_response_callbackfunction handles the response form the server when it receives and accepts the goal; in this implementation just prints this response.The
feedback_callbackfunction handles the feedback received by the server; in this case prints the value of feedback->partial_sequence.The
result_callbackfunction handles the response received from the server when it is finished processing; in this case it prints the value of result->sequence.
#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
class FibonacciActionClient : public rclcpp::Node
{
public:
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>;
explicit FibonacciActionClient()
: Node("fibonacci_action_client")
{
this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(
this,
"fibonacci");
this->timer_ = this->create_wall_timer(
std::chrono::milliseconds(500),
std::bind(&FibonacciActionClient::send_goal, this));
}
void send_goal()
{
using namespace std::placeholders;
this->timer_->cancel();
if (!this->client_ptr_->wait_for_action_server()) {
RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
rclcpp::shutdown();
}
auto goal_msg = Fibonacci::Goal();
goal_msg.order = 10;
RCLCPP_INFO(this->get_logger(), "Sending goal");
auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
send_goal_options.goal_response_callback =
std::bind(&FibonacciActionClient::goal_response_callback, this, _1);
send_goal_options.feedback_callback =
std::bind(&FibonacciActionClient::feedback_callback, this, _1, _2);
send_goal_options.result_callback =
std::bind(&FibonacciActionClient::result_callback, this, _1);
this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
}
private:
rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_;
rclcpp::TimerBase::SharedPtr timer_;
void goal_response_callback(const GoalHandleFibonacci::SharedPtr & goal_handle)
{
if (!goal_handle) {
RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server");
} else {
RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result");
}
}
void feedback_callback(
GoalHandleFibonacci::SharedPtr,
const std::shared_ptr<const Fibonacci::Feedback> feedback)
{
std::stringstream ss;
ss << "Next number in sequence received: ";
for (auto number : feedback->partial_sequence) {
ss << number << " ";
}
RCLCPP_INFO(this->get_logger(), ss.str().c_str());
}
void result_callback(const GoalHandleFibonacci::WrappedResult & result)
{
switch (result.code) {
case rclcpp_action::ResultCode::SUCCEEDED:
break;
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
return;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
return;
default:
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
return;
}
std::stringstream ss;
ss << "Result received: ";
for (auto number : result.result->sequence) {
ss << number << " ";
}
RCLCPP_INFO(this->get_logger(), ss.str().c_str());
rclcpp::shutdown();
}
}; // class FibonacciActionClient
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<FibonacciActionClient>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
The pan-tilt example
The following example, implemented by the packages pantilt_action_tutorials_cpp and pantilt_action_tutorials_interfaces, illustrates an action server that is responsible of moving a pan-and-tilt robotic structure by publishing the joint states, and an action client that calls the action service to move a given joint to a desired angle incrementally with a given step size.
Exercise
Build the packages and instrospect the action defined:
colcon build --packages-up-to pantilt_action_tutorials_cpp
ros2 action info /pantilt
ros2 interface show pantilt_action_tutorials_interfaces/action/PantiltAction
Exercise
Test the action server and the action client (in separate terminals). Check for different desired angle values and verify that the motion is cancelled when the actual angle reaches 90 degrees.
ros2 launch pantilt_action_tutorials_cpp pantilt_action_server.launch.py
ros2 launch pantilt_action_tutorials_cpp pantilt_action_client_arguments.launch.py joint_name:="tilt_joint" desired_angle:=70 stepsize:=5
Exercise
Test the action server and the action client (in separate terminals). Check for different behavior when sending consecutive petitions to the action server, according to the flag passed as launch argument.
ros2 launch pantilt_action_tutorials_cpp pantilt_action_server_arguments.launch.py mode:="REJECT"
ros2 launch pantilt_action_tutorials_cpp pantilt_action_client_arguments.launch.py joint_name:="tilt_joint" desired_angle:=70 stepsize:=5
ros2 launch pantilt_action_tutorials_cpp pantilt_action_client_arguments.launch.py joint_name:="tilt_joint" desired_angle:=-70 stepsize:=5
ros2 launch pantilt_action_tutorials_cpp pantilt_action_server_arguments.launch.py mode:="ABORT"
ros2 launch pantilt_action_tutorials_cpp pantilt_action_client_arguments.launch.py joint_name:="tilt_joint" desired_angle:=70 stepsize:=5
ros2 launch pantilt_action_tutorials_cpp pantilt_action_client_arguments.launch.py joint_name:="tilt_joint" desired_angle:=-70 stepsize:=5
Exercise
Take a look at the action client:
Note that in the feedback_callback function the goal is cancelled when the feedback angle value is greater or equal than 90.
Take a look at the action server:
Note that the handle_goal function rejects a new goal if a motion is being executed or aborts the current one.
Note in the execute function that the while loop is stop when there is a cancellation petition from the client or an abort due to the acceptance of a new goal petition.

