Skip to content

Writing an action server and client (C++)

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. [ROS2 doc https://docs.ros.org/en/galactic/Tutorials/Intermediate/Writing-an-Action-Server-Client/Cpp.html_] [ROS2 github examples]

They are one of the communication types in ROS 2 that are intended for long-running tasks. They consist of three parts: a goal, feedback, and a result.

Actions are built on topics and services. Their functionality is similar to services, except actions can be canceled. They also provide steady feedback, as opposed to services which return a single response.

Setup a workspace and basic commands

Source the ROS2 environment

source /opt/ros/galactic/setup.bash

Create a workspace

mkdir -p ~/<workspace_name>/src
cd ~/<workspace_name>

Resolve dependencies

cd ~/<workspace_name>
rosdep install -i --from-path src --rosdistro galactic -y

Build the workspace

cd ~/<workspace_name>
colcon build

Source the setup file

cd ~/<workspace_name>
. install/local_setup.bash

Creating an action

First setup an interface package:

cd ~/<workspace_name>/src
ros2 pkg create <interface_name>

In the package, create a folder action and a .action file.

vcd ~/<workspace_name>/src
mkdir -p <interface_name>/action && touch <interface_name>/action/<action_name>.action

Define the action in the .action file of the form:

<type> <Request>
---
<type> <Result>
---
<type> <Feedback>

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

  1. A request message is sent from an action client to an action server initiating a new goal.
  2. A result message is sent from an action server to an action client when a goal is done.
  3. Feedback messages are periodically sent from an action server to an action client with updates about a goal.

Building an action

Before we can use the new 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 the <interface_name>/CMakeLists.txt before the ament_package() line:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
"action/<action_name>.action"
)

And adding the required dependencies to the package.xml file after the others :

<buildtool_depend>rosidl_default_generators</buildtool_depend>

<depend>action_msgs</depend>

<member_of_group>rosidl_interface_packages</member_of_group>

You'll have to fill in the description and license in the package.xml

After that we can build the workspace from his root.

cd ~/<workspace_name>
colcon build

You can check that the action has been built successfully by running:

cd ~/<workspace_name>
# On Windows: call install/setup.bat
. install/setup.bash
ros2 interface show <interface_name>/action/<action_name>

Creating action package

Go into the workspace and create a new package:

cd ~/<workspace_name>/src
ros2 pkg create --dependencies <interface_name> rclcpp rclcpp_action rclcpp_components -- <action_package_name>

Write action server

Create <action_package_name>/src/<action_server_name>.cpp file and put code in, below is an example of a HelloWorld server:

He simply prints "Server Started" and then waits for a goal to be sent to it. When a goal is received, it prints "Received goal request with order X" and then print "Hello World!" goal's time and sends feedback every second until the goal is reached. When the goal is reached, it prints "Goal succeeded" and then sends the result to the client.

  • <interface_name> is interface_name
  • <action_name> is Hello
  • <Request> is request, <Result> is result, <Feedback> is feedback with all <type> being int32.
#include "interface_name/action/hello.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"

class ActionServer : public rclcpp::Node
{
    public:
    using ActionHello = interface_name::action::Hello;
    using GoalHandle = rclcpp_action::ServerGoalHandle<ActionHello>;

    explicit ActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
    : Node("action_server_node", options)
    {
        using namespace std::placeholders;

        printf("Server Started!\n");

        this->action_server_ = rclcpp_action::create_server<ActionHello>(
        this->get_node_base_interface(),
        this->get_node_clock_interface(),
        this->get_node_logging_interface(),
        this->get_node_waitables_interface(),
        "action_name",
        std::bind(&ActionServer::handle_goal, this, _1, _2),
        std::bind(&ActionServer::handle_cancel, this, _1),
        std::bind(&ActionServer::handle_accepted, this, _1));
    }

    private:
    rclcpp_action::Server<ActionHello>::SharedPtr action_server_;

    rclcpp_action::GoalResponse handle_goal(
        const rclcpp_action::GoalUUID & uuid,
        std::shared_ptr<const ActionHello::Goal> goal)
    {
        RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->request);
        (void)uuid;
        if (goal->request > 100) {
        return rclcpp_action::GoalResponse::REJECT;
        }
        return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
    }

    rclcpp_action::CancelResponse handle_cancel(
        const std::shared_ptr<GoalHandle> 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<GoalHandle> 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(&ActionServer::execute, this, _1), goal_handle}.detach();
    }

    void execute(const std::shared_ptr<GoalHandle> 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<ActionHello::Feedback>();
        auto result = std::make_shared<ActionHello::Result>();

        for (int i = 1; (i <= goal->request) && rclcpp::ok(); ++i) {
        // Check if there is a cancel request
        if (goal_handle->is_canceling()) {
            result->result = -1;
            goal_handle->canceled(result);
            RCLCPP_INFO(this->get_logger(), "Goal canceled");
            return;
        }
        printf("ActionHello World!\n");

        // Update sequence
        feedback->feedback = i;
        // Publish feedback
        goal_handle->publish_feedback(feedback);
        RCLCPP_INFO(this->get_logger(), "Publish feedback");

        loop_rate.sleep();
        }

        // Check if goal is result
        if (rclcpp::ok()) {
        result->result = 1;
        goal_handle->succeed(result);
        RCLCPP_INFO(this->get_logger(), "Goal succeeded");
        }
    }
};  // class ActionServer

int main(int argc, char ** argv)
{
    rclcpp::init(argc, argv);

    auto action_server = std::make_shared<ActionServer>();

    rclcpp::spin(action_server);

    rclcpp::shutdown();
    return 0;
}

Write action client

Open up <action_package_name>/src/<action_client_name>.cpp and put code in, here is an example of a HelloWorld client:

This client code sends a goal to the server, here 4, and then wait for the result. When the result is received, it prints "Result received: %d" and then shut down the node.

  • <interface_name> is interface_name
  • <action_name> is Hello
  • <Request> is request, <Result> is result, <Feedback> is feedback with all <type> being int32.
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <interface_name/action/hello.hpp>

using ActionHello = interface_name::action::Hello;

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto node = rclcpp::Node::make_shared("action_client_node");

    // Create a client for the Hello action
    auto client = rclcpp_action::create_client<ActionHello>(
        node, "action_name");

    // Wait for the action server to become available
    if (!client->wait_for_action_server(std::chrono::seconds(5))) {
        RCLCPP_ERROR(node->get_logger(), "Action server not available after waiting");
        return 1;
    }

    // Create a goal message
    auto goal = ActionHello::Goal();
    goal.request = 4;

    // Send the goal and wait for the result
    auto send_goal_future = client->async_send_goal(goal);
    if (rclcpp::spin_until_future_complete(node, send_goal_future) !=
        rclcpp::FutureReturnCode::SUCCESS)
    {
        RCLCPP_ERROR(node->get_logger(), "Failed to send goal");
        return 1;
    }

    auto goal_handle = send_goal_future.get();
    if (!goal_handle) {
        RCLCPP_ERROR(node->get_logger(), "Goal was rejected by server");
        return 1;
    }

    // Wait for the result
    auto result_future = client->async_get_result(goal_handle);
    if (rclcpp::spin_until_future_complete(node, result_future) !=
        rclcpp::FutureReturnCode::SUCCESS)
    {
        RCLCPP_ERROR(node->get_logger(), "Failed to get result");
        return 1;
    }

    auto result = result_future.get();
    if (result.code == rclcpp_action::ResultCode::SUCCEEDED) {
        RCLCPP_INFO(node->get_logger(), "Result: %d", result.result->result);
    } else {
        RCLCPP_ERROR(node->get_logger(), "Action did not succeed");
    }

    rclcpp::shutdown();
    return 0;
}

Update Compile file

Open up <action_package_name>/CMakeLists.txt and add the following lines after the find_package lines:

add_executable(<action_server_name> src/<action_server_name>.cpp)
ament_target_dependencies(<action_server_name>
"rclcpp"
"rclcpp_action"
"<interface_name>")

add_executable(<action_client_name> src/<action_client_name>.cpp)
ament_target_dependencies(<action_client_name>
"rclcpp"
"rclcpp_action"
"<interface_name>")
And the following lines after the BUILD_TESTING lines:
install(TARGETS
    <action_server_name>
    <action_client_name>
DESTINATION lib/${PROJECT_NAME})

Build

Build the workspace:

cd ~/<workspace_name>
colcon build
. install/setup.bash

Start server and client

ros2 run <action_package_name> <action_server_name>
ros2 run <action_package_name> <action_client_name>

# Send an action goal to the server from terminal
ros2 action send_goal <action_name> <action_type> "{<action_field>: <value>}"
# For example:
ros2 action send_goal /action_name interface_name/action/Hello "{request: 8}"

Debugging

  • If you get an error in the action server/client cpp file that can not find the action .hpp file, try to see if it generates the .hpp file with a different name in the install folder install/<interface_name>/include/<interface_name>/action/<name_you_want>.hpp.

  • If you can not start, don't forget to source the setup.bash file in the install folder with . install/setup.bash.