Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

04 ROS Actions

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 20

Creating an action

Goal: Define an action in a ROS 2 package.


Tutorial level: Intermediate
Time: 5 minutes
Contents
 Background
 Prerequisites
 Tasks
 1 Defining an action
 2 Building an action
o Summary
o Next steps
o Related content
Background
You learned about actions previously in the Understanding ROS 2 actions tutorial. Like the other
communication types and their respective interfaces (topics/msg and services/srv), you can also
custom-define actions in your packages. This tutorial shows you how to define and build an action
that you can use with the action server and action client you will write in the next tutorial.
Prerequisites
You should have ROS 2 (Dashing or later) and colcon installed.
Set up a workspace and create a package named  action_tutorials_interfaces :
(Remember to source your ROS 2 installation first.)
LinuxmacOSWindows
mkdir -p action_ws/src
cd action_ws/src
ros2 pkg create action_tutorials_interfaces

Tasks
1 Defining an action
Actions are defined in  .action  files of the form:
# Request
---
# Result
---
# Feedback

An action definition is made up of three message definitions separated by  --- .


 A request message is sent from an action client to an action server initiating a new goal.
 A result message is sent from an action server to an action client when a goal is done.
 Feedback messages are periodically sent from an action server to an action client with
updates about a goal.
An instance of an action is typically referred to as a goal.
Say we want to define a new action “Fibonacci” for computing the Fibonacci sequence.
Create an  action  directory in our ROS 2 package  action_tutorials_interfaces :
LinuxmacOSWindows
cd action_tutorials_interfaces
mkdir action

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"
)

We should also add the required dependencies to our  package.xml :


<buildtool_depend>rosidl_default_generators</buildtool_depend>

<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.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Writing an action server and client (C++)


Goal: Implement an action server and client in C++.
Tutorial level: Intermediate
Time: 15 minutes
Contents
 Background
 Prerequisites
 Tasks
 1 Creating the action_tutorials_cpp package
 2 Writing an action server
 3 Writing an action client
o Summary
o Related content
Background
Actions are a form of asynchronous communication in ROS. Action clients send goal requests
to action servers. Action servers send goal feedback and results to action clients.
Prerequisites
You will need the  action_tutorials_interfaces  package and the  Fibonacci.action  interface defined in
the previous tutorial, Creating an action.
Tasks
1 Creating the action_tutorials_cpp package 
As we saw in the Creating your first ROS 2 package tutorial, we need to create a new package to
hold our C++ and supporting code.
1.1 Creating the action_tutorials_cpp package 
Go into the action workspace you created in the previous tutorial (remember to source the
workspace), and create a new package for the C++ action server:
LinuxmacOSWindows
cd ~/action_ws/src
ros2 pkg create --dependencies action_tutorials_interfaces rclcpp rclcpp_action rclcpp_components --
action_tutorials_cpp

1.2 Adding in visibility control


In order to make the package compile and work on Windows, we need to add in some “visibility
control”. For details on why this is needed, see here.
Open up  action_tutorials_cpp/include/action_tutorials_cpp/visibility_control.h , and put the following
code in:
#ifndef ACTION_TUTORIALS_CPP__VISIBILITY_CONTROL_H_
#define ACTION_TUTORIALS_CPP__VISIBILITY_CONTROL_H_

#ifdef __cplusplus
extern "C"
{
#endif

// This logic was borrowed (then namespaced) from the examples on the gcc wiki:
// https://gcc.gnu.org/wiki/Visibility

#if defined _WIN32 || defined __CYGWIN__


#ifdef __GNUC__
#define ACTION_TUTORIALS_CPP_EXPORT __attribute__ ((dllexport))
#define ACTION_TUTORIALS_CPP_IMPORT __attribute__ ((dllimport))
#else
#define ACTION_TUTORIALS_CPP_EXPORT __declspec(dllexport)
#define ACTION_TUTORIALS_CPP_IMPORT __declspec(dllimport)
#endif
#ifdef ACTION_TUTORIALS_CPP_BUILDING_DLL
#define ACTION_TUTORIALS_CPP_PUBLIC ACTION_TUTORIALS_CPP_EXPORT
#else
#define ACTION_TUTORIALS_CPP_PUBLIC ACTION_TUTORIALS_CPP_IMPORT
#endif
#define ACTION_TUTORIALS_CPP_PUBLIC_TYPE ACTION_TUTORIALS_CPP_PUBLIC
#define ACTION_TUTORIALS_CPP_LOCAL
#else
#define ACTION_TUTORIALS_CPP_EXPORT __attribute__ ((visibility("default")))
#define ACTION_TUTORIALS_CPP_IMPORT
#if __GNUC__ >= 4
#define ACTION_TUTORIALS_CPP_PUBLIC __attribute__ ((visibility("default")))
#define ACTION_TUTORIALS_CPP_LOCAL __attribute__ ((visibility("hidden")))
#else
#define ACTION_TUTORIALS_CPP_PUBLIC
#define ACTION_TUTORIALS_CPP_LOCAL
#endif
#define ACTION_TUTORIALS_CPP_PUBLIC_TYPE
#endif

#ifdef __cplusplus
}
#endif

#endif // ACTION_TUTORIALS_CPP__VISIBILITY_CONTROL_H_

2 Writing an action server


Let’s focus on writing an action server that computes the Fibonacci sequence using the action we
created in the Creating an action tutorial.
2.1 Writing the action server code
Open up  action_tutorials_cpp/src/fibonacci_action_server.cpp , and put the following code in:
#include <functional>
#include <memory>
#include <thread>

#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;
}

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

} // 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)

The constructor also instantiates a new action server:


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));

An action server requires 6 things:


1. The templated action type name:  Fibonacci .
2. A ROS 2 node to add the action to:  this .
3. The action name:  'fibonacci' .
4. A callback function for handling goals:  handle_goal
5. A callback function for handling cancellation:  handle_cancel .
6. A callback function for handling goal accept:  handle_accept .
The implementation of the various callbacks is next in the file. Note that all of the callbacks need to
return quickly, otherwise we risk starving the executor.
We start with the callback for handling new goals:
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;
}

This implementation just accepts all goals.


Next up is the callback for dealing with cancellation:
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;
}

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>();

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");
}
}

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

3 Writing an action client


3.1 Writing the action client code
Open up  action_tutorials_cpp/src/fibonacci_action_client.cpp , and put the following code in:
#include <functional>
#include <future>
#include <memory>
#include <string>
#include <sstream>

#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>;

explicit FibonacciActionClient(const rclcpp::NodeOptions & options)


: Node("fibonacci_action_client", options)
{
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(std::shared_future<GoalHandleFibonacci::SharedPtr> future)


{
auto goal_handle = future.get();
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

} // 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)

The constructor also instantiates a new action client:


this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(
this,
"fibonacci");

An action client requires 3 things:


1. The templated action type name:  Fibonacci .
2. A ROS 2 node to add the action client to:  this .
3. The action name:  'fibonacci' .
We also instantiate a ROS timer that will kick off the one and only call to  send_goal :
this->timer_ = this->create_wall_timer(
std::chrono::milliseconds(500),
std::bind(&FibonacciActionClient::send_goal, this));

When the timer expires, it will call  send_goal :


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);
}

This function does the following:


1. Cancels the timer (so it is only called once).
2. Waits for the action server to come up.
3. Instantiates a new  Fibonacci::Goal .
4. Sets the response, feedback, and result callbacks.
5. Sends the goal to the server.
When the server receives and accepts the goal, it will send a response to the client. That response
is handled by  goal_response_callback :
void goal_response_callback(std::shared_future<GoalHandleFibonacci::SharedPtr> future)
{
auto goal_handle = future.get();
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");
}
}

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.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Writing an action server and client (Python) 


Goal: Implement an action server and client in Python.
Tutorial level: Intermediate
Time: 15 minutes
Contents
 Background
 Prerequisites
 Tasks
 1 Writing an action server
 2 Writing an action client
o Summary
o Related content
Background
Actions are a form of asynchronous communication in ROS 2. Action clients send goal requests
to action servers. Action servers send goal feedback and results to action clients.
Prerequisites
You will need the  action_tutorials_interfaces  package and the  Fibonacci.action  interface defined in
the previous tutorial, Creating an action.
Tasks
1 Writing an action server
Let’s focus on writing an action server that computes the Fibonacci sequence using the action we
created in the Creating an action tutorial.
Until now, you’ve created packages and used  ros2 run  to run your nodes. To keep things simple in
this tutorial, however, we’ll scope the action server to a single file. If you’d like to see what a
complete package for the actions tutorials looks like, check out action_tutorials.
Open a new file in your home directory, let’s call it  fibonacci_action_server.py , and add the following
code:
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node

from action_tutorials_interfaces.action import Fibonacci

class FibonacciActionServer(Node):

def __init__(self):
super().__init__('fibonacci_action_server')
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback)

def execute_callback(self, goal_handle):


self.get_logger().info('Executing goal...')
result = Fibonacci.Result()
return result

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')

In the constructor we also instantiate a new action server:


self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback)

An action server requires four arguments:


1. A ROS 2 node to add the action client to:  self .
2. The type of the action:  Fibonacci  (imported in line 5).
3. The action name:  'fibonacci' .
4. A callback function for executing accepted goals:  self.execute_callback . This
callback must return a result message for the action type.
We also define an  execute_callback  method in our class:
def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
result = Fibonacci.Result()
return result

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]

for i in range(1, goal_handle.request.order):


sequence.append(sequence[i] + sequence[i-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

from action_tutorials_interfaces.action import Fibonacci

class FibonacciActionServer(Node):

def __init__(self):
super().__init__('fibonacci_action_server')
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback)

def execute_callback(self, goal_handle):


self.get_logger().info('Executing goal...')

feedback_msg = Fibonacci.Feedback()
feedback_msg.partial_sequence = [0, 1]

for i in range(1, goal_handle.request.order):


feedback_msg.partial_sequence.append(
feedback_msg.partial_sequence[i] + feedback_msg.partial_sequence[i-1])
self.get_logger().info('Feedback: {0}'.format(feedback_msg.partial_sequence))
goal_handle.publish_feedback(feedback_msg)
time.sleep(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}"

2 Writing an action client


We’ll also scope the action client to a single file. Open a new file, let’s call
it  fibonacci_action_client.py , and add the following boilerplate code:
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node

from action_tutorials_interfaces.action import Fibonacci

class FibonacciActionClient(Node):

def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')

def send_goal(self, order):


goal_msg = Fibonacci.Goal()
goal_msg.order = order

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')

We create an  ActionClient  by passing it three arguments:


1. A ROS 2 node to add the action client to:  self
2. The type of the action:  Fibonacci
3. The action name:  'fibonacci'
Our action client will be able to communicate with action servers of the same action name and
type.
We also define a method  send_goal  in the  FibonacciActionClient  class:
def send_goal(self, order):
goal_msg = Fibonacci.Goal()
goal_msg.order = order

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

In another terminal, run the action client:


LinuxmacOSWindows
python3 fibonacci_action_client.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

from action_tutorials_interfaces.action import Fibonacci

class FibonacciActionClient(Node):

def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')

def send_goal(self, order):


goal_msg = Fibonacci.Goal()
goal_msg.order = order

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)

def goal_response_callback(self, future):


goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return

self.get_logger().info('Goal accepted :)')

self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)

def get_result_callback(self, future):


result = future.result().result
self.get_logger().info('Result: {0}'.format(result.sequence))
rclpy.shutdown()

def main(args=None):
rclpy.init(args=args)

action_client = FibonacciActionClient()

action_client.send_goal(10)

rclpy.spin(action_client)

if __name__ == '__main__':
main()

The ActionClient.send_goal_async() method returns a future to a goal handle. First we register a


callback for when the future is complete:
self._send_goal_future.add_done_callback(self.goal_response_callback)
Note that the future is completed when an action server accepts or rejects the goal request. Let’s
look at the  goal_response_callback  in more detail. We can check to see if the goal was rejected and
return early since we know there will be no result:
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return

self.get_logger().info('Goal accepted :)')

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

from action_tutorials_interfaces.action import Fibonacci

class FibonacciActionClient(Node):

def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')

def send_goal(self, order):


goal_msg = Fibonacci.Goal()
goal_msg.order = order

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_logger().info('Goal accepted :)')

self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)

def get_result_callback(self, future):


result = future.result().result
self.get_logger().info('Result: {0}'.format(result.sequence))
rclpy.shutdown()

def feedback_callback(self, feedback_msg):


feedback = feedback_msg.feedback
self.get_logger().info('Received feedback: {0}'.format(feedback.partial_sequence))

def main(args=None):
rclpy.init(args=args)

action_client = FibonacciActionClient()

action_client.send_goal(10)

rclpy.spin(action_client)

if __name__ == '__main__':
main()

Here’s the callback function for feedback messages:


def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info('Received feedback: {0}'.format(feedback.partial_sequence))

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.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

You might also like