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:

Provided packages:

This tutorial will use the following package:

Package my_action_tutorials_cpp
Package my_action_tutorials_interfaces
Package pantilt_action_tutorials_cpp
Package pantilt_action_tutorials_interfaces

Prepare the colcon workspace to work with it:

  1. 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).

  1. 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:

../_images/goal_state_machine.png
  • 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:

../_images/actions_schema.png
  • 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:

../_images/Action-SingleActionClient1.gif

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

Exercise

Check the structure of this action using rqt:

rqt -s action
../_images/rqt_trutlesim_action.png

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.

  • The ServerGoalHandle class

    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_feedback to send an update about the progress of a goal.

    • abort to indicate that a goal could not be reached and has been aborted.

    • succeed to indicate that a goal has succeeded.

    • canceled to indicate that a goal has been canceled.

    • execute to indicate that the server is starting to execute a goal.

    • get_goal to get the user provided message describing the goal.

  • The action Client class

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_goal function, 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.

  • The ClientGoalHandle class

    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:

  1. In the c++ files include the following header file:

#include "rclcpp_action/rclcpp_action.hpp"
  1. In the CMakeLists.txt include the dependency of rclcpp_action:

find_package(rclcpp_action REQUIRED)

set(dependencies
    rclcpp
    rclcpp_action
    ....
)
  1. 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:

  1. 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_goal

  • A callback function for handling cancellation: handle_cancel.

  • A callback function for handling goal accept: handle_accept.

  1. Regarding the callback functions:

  • The implementation of the handle_goal callback just accepts all goals.

  • The implementation of the handle_cancel callback just accepts the cancellation.

  • The implementation of the handle_accept accepts 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:

  1. 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_goal that:

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

  1. Regarding the callback functions:

  • The goal_response_callback function handles the response form the server when it receives and accepts the goal; in this implementation just prints this response.

  • The feedback_callback function handles the feedback received by the server; in this case prints the value of feedback->partial_sequence.

  • The result_callback function 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.