04 ROS Actions
04 ROS Actions
04 ROS Actions
Tasks
1 Defining an action
Actions are defined in .action files of the form:
# Request
---
# Result
---
# Feedback
Within the action directory, create a file called Fibonacci.action with the following contents:
int32 order
---
int32[] sequence
---
int32[] partial_sequence
The goal request is the order of the Fibonacci sequence we want to compute, the result is the
final sequence , and the feedback is the partial_sequence computed so far.
2 Building an action
Before we can use the new Fibonacci action type in our code, we must pass the definition to the
rosidl code generation pipeline.
This is accomplished by adding the following lines to our CMakeLists.txt before
the ament_package() line, in the action_tutorials_interfaces :
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"action/Fibonacci.action"
)
<depend>action_msgs</depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Note, we need to depend on action_msgs since action definitions include additional metadata (e.g.
goal IDs).
We should now be able to build the package containing the Fibonacci action definition:
# Change to the root of the workspace
cd ~/action_ws
# Build
colcon build
We’re done!
By convention, action types will be prefixed by their package name and the word action . So when
we want to refer to our new action, it will have the full
name action_tutorials_interfaces/action/Fibonacci .
We can check that our action built successfully with the command line tool:
# Source our workspace
# On Windows: call install/setup.bat
. install/setup.bash
# Check that our action definition exists
ros2 interface show action_tutorials_interfaces/action/Fibonacci
You should see the Fibonacci action definition printed to the screen.
Summary
In this tutorial, you learned the structure of an action definition. You also learned how to correctly
build a new action interface using CMakeLists.txt and package.xml , and how to verify a successful
build.
Next steps
Next, let’s utilize your newly defined action interface by creating an action service and client
(in Python or C++).
Related content
For more detailed information about ROS actions, please refer to the design article.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#ifdef __cplusplus
extern "C"
{
#endif
// This logic was borrowed (then namespaced) from the examples on the gcc wiki:
// https://gcc.gnu.org/wiki/Visibility
#ifdef __cplusplus
}
#endif
#endif // ACTION_TUTORIALS_CPP__VISIBILITY_CONTROL_H_
#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"
#include "action_tutorials_cpp/visibility_control.h"
namespace action_tutorials_cpp
{
class FibonacciActionServer : public rclcpp::Node
{
public:
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;
ACTION_TUTORIALS_CPP_PUBLIC
explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
: Node("fibonacci_action_server", options)
{
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;
}
loop_rate.sleep();
}
} // namespace action_tutorials_cpp
RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionServer)
The first few lines include all of the headers we need to compile.
Next we create a class that is a derived class of rclcpp::Node :
class FibonacciActionServer : public rclcpp::Node
The constructor for the FibonacciActionServer class initializes the node name
as fibonacci_action_server :
explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
: Node("fibonacci_action_server", options)
This implementation just tells the client that it accepted the cancellation.
The last of the callbacks accepts a new goal and starts processing it:
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();
}
Since the execution is a long-running operation, we spawn off a thread to do the actual work and
return from handle_accepted quickly.
All further processing and updates are done in the execute method in the new thread:
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>();
loop_rate.sleep();
}
This work thread processes one sequence number of the Fibonacci sequence every second,
publishing a feedback update for each step. When it has finished processing, it marks
the goal_handle as succeeded, and quits.
We now have a fully functioning action server. Let’s get it built and running.
2.2 Compiling the action server
In the previous section we put the action server code into place. To get it to compile and run, we
need to do a couple of additional things.
First we need to setup the CMakeLists.txt so that the action server is compiled. Open
up action_tutorials_cpp/CMakeLists.txt , and add the following right after the find_package calls:
add_library(action_server SHARED
src/fibonacci_action_server.cpp)
target_include_directories(action_server PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_compile_definitions(action_server
PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")
ament_target_dependencies(action_server
"action_tutorials_interfaces"
"rclcpp"
"rclcpp_action"
"rclcpp_components")
rclcpp_components_register_node(action_server PLUGIN "action_tutorials_cpp::FibonacciActionServer"
EXECUTABLE fibonacci_action_server)
install(TARGETS
action_server
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
And now we can compile the package. Go to the top-level of the action_ws , and run:
colcon build
This should compile the entire workspace, including the fibonacci_action_server in
the action_tutorials_cpp package.
2.3 Running the action server
Now that we have the action server built, we can run it. Source the workspace we just built
( action_ws ), and try to run the action server:
ros2 run action_tutorials_cpp fibonacci_action_server
#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"
namespace action_tutorials_cpp
{
class FibonacciActionClient : public rclcpp::Node
{
public:
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<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();
}
private:
rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_;
rclcpp::TimerBase::SharedPtr timer_;
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());
}
} // namespace action_tutorials_cpp
RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionClient)
The first few lines include all of the headers we need to compile.
Next we create a class that is a derived class of rclcpp::Node :
class FibonacciActionClient : public rclcpp::Node
The constructor for the FibonacciActionClient class initializes the node name
as fibonacci_action_client :
explicit FibonacciActionClient(const rclcpp::NodeOptions & options)
: Node("fibonacci_action_client", options)
this->timer_->cancel();
if (!this->client_ptr_->wait_for_action_server()) {
RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
rclcpp::shutdown();
}
Assuming the goal was accepted by the server, it will start processing. Any feedback to the client
will be handled by the feedback_callback :
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());
}
When the server is finished processing, it will return a result to the client. The result is handled by
the result_callback :
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();
}
We now have a fully functioning action client. Let’s get it built and running.
3.2 Compiling the action client
In the previous section we put the action client code into place. To get it to compile and run, we
need to do a couple of additional things.
First we need to setup the CMakeLists.txt so that the action client is compiled. Open
up action_tutorials_cpp/CMakeLists.txt , and add the following right after the find_package calls:
add_library(action_client SHARED
src/fibonacci_action_client.cpp)
target_include_directories(action_client PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_compile_definitions(action_client
PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")
ament_target_dependencies(action_client
"action_tutorials_interfaces"
"rclcpp"
"rclcpp_action"
"rclcpp_components")
rclcpp_components_register_node(action_client PLUGIN "action_tutorials_cpp::FibonacciActionClient"
EXECUTABLE fibonacci_action_client)
install(TARGETS
action_client
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
And now we can compile the package. Go to the top-level of the action_ws , and run:
colcon build
This should compile the entire workspace, including the fibonacci_action_client in
the action_tutorials_cpp package.
3.3 Running the action client
Now that we have the action client built, we can run it. First make sure that an action server is
running in a separate terminal. Now source the workspace we just built ( action_ws ), and try to run
the action client:
ros2 run action_tutorials_cpp fibonacci_action_client
You should see logged messages for the goal being accepted, feedback being printed, and the
final result.
Summary
In this tutorial, you put together a C++ action server and action client line by line, and configured
them to exchange goals, feedback, and results.
Related content
There are several ways you could write an action server and client in C++; check out
the minimal_action_server and minimal_action_client packages in the ros2/examples repo.
For more detailed information about ROS actions, please refer to the design article.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
class FibonacciActionServer(Node):
def __init__(self):
super().__init__('fibonacci_action_server')
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback)
def main(args=None):
rclpy.init(args=args)
fibonacci_action_server = FibonacciActionServer()
rclpy.spin(fibonacci_action_server)
if __name__ == '__main__':
main()
Line 8 defines a class FibonacciActionServer that is a subclass of Node . The class is initialized by
calling the Node constructor, naming our node fibonacci_action_server :
super().__init__('fibonacci_action_server')
This is the method that will be called to execute a goal once it is accepted.
Let’s try running our action server:
LinuxmacOSWindows
python3 fibonacci_action_server.py
In another terminal, we can use the command line interface to send a goal:
ros2 action send_goal fibonacci action_tutorials_interfaces/action/Fibonacci "{order: 5}"
In the terminal that is running the action server, you should see a logged message “Executing
goal…” followed by a warning that the goal state was not set. By default, if the goal handle state is
not set in the execute callback it assumes the aborted state.
We can use the method succeed() on the goal handle to indicate that the goal was successful:
def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
goal_handle.succeed()
result = Fibonacci.Result()
return result
Now if you restart the action server and send another goal, you should see the goal finished with
the status SUCCEEDED .
Now let’s make our goal execution actually compute and return the requested Fibonacci
sequence:
def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
sequence = [0, 1]
goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = sequence
return result
After computing the sequence, we assign it to the result message field before returning.
Again, restart the action server and send another goal. You should see the goal finish with the
proper result sequence.
1.2 Publishing feedback
One of the nice things about actions is the ability to provide feedback to an action client during
goal execution. We can make our action server publish feedback for action clients by calling the
goal handle’s publish_feedback() method.
We’ll replace the sequence variable, and use a feedback message to store the sequence instead.
After every update of the feedback message in the for-loop, we publish the feedback message and
sleep for dramatic effect:
import time
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
class FibonacciActionServer(Node):
def __init__(self):
super().__init__('fibonacci_action_server')
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback)
feedback_msg = Fibonacci.Feedback()
feedback_msg.partial_sequence = [0, 1]
goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = feedback_msg.partial_sequence
return result
def main(args=None):
rclpy.init(args=args)
fibonacci_action_server = FibonacciActionServer()
rclpy.spin(fibonacci_action_server)
if __name__ == '__main__':
main()
After restarting the action server, we can confirm that feedback is now published by using the
command line tool with the --feedback option:
ros2 action send_goal --feedback fibonacci action_tutorials_interfaces/action/Fibonacci "{order: 5}"
class FibonacciActionClient(Node):
def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')
self._action_client.wait_for_server()
return self._action_client.send_goal_async(goal_msg)
def main(args=None):
rclpy.init(args=args)
action_client = FibonacciActionClient()
future = action_client.send_goal(10)
rclpy.spin_until_future_complete(action_client, future)
if __name__ == '__main__':
main()
We’ve defined a class FibonacciActionClient that is a subclass of Node . The class is initialized by
calling the Node constructor, naming our node fibonacci_action_client :
super().__init__('fibonacci_action_client')
Also in the class constructor, we create an action client using the custom action definition from the
previous tutorial on Creating an action:
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')
self._action_client.wait_for_server()
return self._action_client.send_goal_async(goal_msg)
This method waits for the action server to be available, then sends a goal to the server. It returns a
future that we can later wait on.
After the class definition, we define a function main() that initializes ROS 2 and creates an
instance of our FibonacciActionClient node. It then sends a goal and waits until that goal has been
completed.
Finally, we call main() in the entry point of our Python program.
Let’s test our action client by first running the action server built earlier:
LinuxmacOSWindows
python3 fibonacci_action_server.py
You should see messages printed by the action server as it successfully executes the goal:
[INFO] [fibonacci_action_server]: Executing goal...
[INFO] [fibonacci_action_server]: Feedback: array('i', [0, 1, 1])
[INFO] [fibonacci_action_server]: Feedback: array('i', [0, 1, 1, 2])
[INFO] [fibonacci_action_server]: Feedback: array('i', [0, 1, 1, 2, 3])
[INFO] [fibonacci_action_server]: Feedback: array('i', [0, 1, 1, 2, 3, 5])
# etc.
The action client should start up, and then quickly finish. At this point, we have a functioning action
client, but we don’t see any results or get any feedback.
2.1 Getting a result
So we can send a goal, but how do we know when it is completed? We can get the result
information with a couple steps. First, we need to get a goal handle for the goal we sent. Then, we
can use the goal handle to request the result.
Here’s the complete code for this example:
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
class FibonacciActionClient(Node):
def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')
self._action_client.wait_for_server()
self._send_goal_future = self._action_client.send_goal_async(goal_msg)
self._send_goal_future.add_done_callback(self.goal_response_callback)
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def main(args=None):
rclpy.init(args=args)
action_client = FibonacciActionClient()
action_client.send_goal(10)
rclpy.spin(action_client)
if __name__ == '__main__':
main()
Now that we’ve got a goal handle, we can use it to request the result with the
method get_result_async(). Similar to sending the goal, we will get a future that will complete
when the result is ready. Let’s register a callback just like we did for the goal response:
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
In the callback, we log the result sequence and shutdown ROS 2 for a clean exit:
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info('Result: {0}'.format(result.sequence))
rclpy.shutdown()
With an action server running in a separate terminal, go ahead and try running our Fibonacci
action client!
LinuxmacOSWindows
python3 fibonacci_action_client.py
You should see logged messages for the goal being accepted and the final result.
2.2 Getting feedback
Our action client can send goals. Nice! But it would be great if we could get some feedback about
the goals we send from the action server.
Here’s the complete code for this example:
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
class FibonacciActionClient(Node):
def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')
self._action_client.wait_for_server()
self._send_goal_future = self._action_client.send_goal_async(goal_msg,
feedback_callback=self.feedback_callback)
self._send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def main(args=None):
rclpy.init(args=args)
action_client = FibonacciActionClient()
action_client.send_goal(10)
rclpy.spin(action_client)
if __name__ == '__main__':
main()
In the callback we get the feedback portion of the message and print the partial_sequence field to
the screen.
We need to register the callback with the action client. This is achieved by additionally passing the
callback to the action client when we send a goal:
self._send_goal_future = self._action_client.send_goal_async(goal_msg,
feedback_callback=self.feedback_callback)
We’re all set. If we run our action client, you should see feedback being printed to the screen.
Summary
In this tutorial, you put together a Python action server and action client line by line, and
configured them to exchange goals, feedback, and results.
Related content
There are several ways you could write an action server and client in Python; check out
the minimal_action_server and minimal_action_client packages in the ros2/examples repo.
For more detailed information about ROS actions, please refer to the design article.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%