sábado, 9 de abril de 2022

ROS 2: Actions

Definition

         As we saw on fundamentals, ROS actions are another type of communication that is intended to handle long running tasks. It is divided into three parts:  a goal, a feedback and a result.


Creating an Action

        We define an action by creating a file called *.action file with the format:

            (request)
            ---
            (result)
            ---
            (feedback)

  • 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.
        Let's create a package called fib_action_interface that will hold the data formats for an Action that will calculate and return fibonacci sequence for a number.

ros2 pkg create --build-type ament_cmake fib_action_interface
    Now, make a dir called actions and create a fib.action file on it.

mkdir -p src/fib_action_interface/actions
touch src/fib_action_interface/actions/fib.action
        Let's edit the Fibonacci.action file and put the following format inside of it:
            int32 order
            ---
            int32[] sequence
            ---
            int32[] partial_sequence

        Now, edit the CMakeList.txt file and add before ament_package() :


find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
"action/Fibonacci.action"
)


        Also, add the following to package.xml:

  <buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>action_msgs</depend>
<member_of_group>rosidl_interface_packages</member_of_group>


        Let's now compile and check
colcon build --packages-select fib_action_interface
. install/setup.bash
ros2 interface show fib_action_interface/action/Fibonacci
int32 order
---
int32[] sequence
---
int32[] partial_sequence

        Nice, the Action is registered in ROS2. So, now, let's build the actual action code. A skeleton file for an action could be implemented by the following:


#include <functional>
#include <memory>
#include <thread>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <rclcpp_components/register_node_macro.hpp>

#include "fib_action_interface/action/fibonacci.hpp"

namespace fibonacci_action
{
using Fibonacci = fib_action_interface::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;
using std::placeholders::_1;
using std::placeholders::_2;

class FibonacciActionServer : public rclcpp::Node
{
public:
explicit FibonacciActionServer(const rclcpp::NodeOptions &options =
            rclcpp::NodeOptions()) : Node("FibonacciActionServer", options)
{
this->server = rclcpp_action::create_server<Fibonacci>(
this,
"fibonacci",
std::bind(&FibonacciActionServer::cb_goal, this, _1, _2),
std::bind(&FibonacciActionServer::cb_cancel, this, _1),
std::bind(&FibonacciActionServer::cb_accepted, this, _1));
}

private:
rclcpp_action::Server<Fibonacci>::SharedPtr server;

rclcpp_action::GoalResponse cb_goal(
            const rclcpp_action::GoalUUID &uuid,
std::shared_ptr<const Fibonacci::Goal> goal)
{
//handles goal request, returning the goal response
}

rclcpp_action::CancelResponse cb_cancel(
            const std::shared_ptr<GoalHandleFibonacci> goal_handler)
{
//handles action cancel, returning the cancel response
}

void cb_accepted(
            const std::shared_ptr<GoalHandleFibonacci> goal_handler)
{
//handles accepted state, where we need to execute
//the heavy code, so we create another thread to
//execute it
std::thread{
std::bind(&FibonacciActionServer::cb_accepted_exec,
                this, _1),
                goal_handler}.detach();
}

void cb_accepted_exec(
            const std::shared_ptr<GoalHandleFibonacci> goal_handler){
// background threat to execute the action
};
};
}

// Registering the server as a node
RCLCPP_COMPONENTS_REGISTER_NODE(fibonacci_action::FibonacciActionServer)


        After that, we just need to make sure package.xml has all the dependencies set


<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>fib_action_interface</buildtool_depend>
<buildtool_depend>rclcpp</buildtool_depend>
<buildtool_depend>rclcpp_action</buildtool_depend>
<buildtool_depend>rclcpp_components</buildtool_depend>


    And then, we do configure the CMakeLists.txt with the following definitions:


cmake_minimum_required(VERSION 3.8)
project(fibonacci_action)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(fib_action_interface REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(rclcpp_components REQUIRED)


add_library(action_server SHARED
src/fibonacci.cpp)

target_include_directories(action_server PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)

ament_target_dependencies(action_server
"fib_action_interface"
"rclcpp"
"rclcpp_action"
"rclcpp_components")

rclcpp_components_register_node(action_server PLUGIN
"fibonacci_action::FibonacciActionServer"
EXECUTABLE fibonacci_action_server)

install(TARGETS
action_server
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)

ament_package()



    Finally, let's compile this package:

colcon build --packages-select fibonacci_action
    
    Now, we need to create a client to consume this action. Let's create a fibonacci_client.cpp. A skeleton file for an action client could be implemented by the following:

        

#include <functional>
#include <memory>
#include <thread>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <rclcpp_components/register_node_macro.hpp>

#include "fib_action_interface/action/fibonacci.hpp"

namespace fibonacci_action
{
using Fibonacci = fib_action_interface::action::Fibonacci;
using GoalHandleFibonacci =
         rclcpp_action::ClientGoalHandle<Fibonacci>;
using std::placeholders::_1;
using std::placeholders::_2;

class FibonacciActionClient : public rclcpp::Node
{
public:
explicit FibonacciActionClient(
            const rclcpp::NodeOptions &options =
rclcpp::NodeOptions()) :
                  Node("FibonacciActionClient", options)
{
this->client = rclcpp_action::create_client<Fibonacci>(
this,
"fibonacci");
}

void request_action()
{
using namespace std::placeholders;

if (!this->client->wait_for_action_server())
{
RCLCPP_ERROR(this->get_logger(),
                    "Action server not available after waiting");
rclcpp::shutdown();
}

// Build options using the client
auto send_goal_options =
                rclcpp_action::Client<Fibonacci>::SendGoalOptions();
// Setting the action's callbacks in the options object
send_goal_options.goal_response_callback =
    std::bind(&FibonacciActionClient::cb_goal_response,
                this, _1);

send_goal_options.feedback_callback =
    std::bind(&FibonacciActionClient::cb_feedback,
                 this, _1, _2);
send_goal_options.result_callback =
    std::bind(&FibonacciActionClient::cb_result,
                 this, _1);

// building the request message
auto goal_msg = Fibonacci::Goal();
goal_msg.order = 10; // 10 position fibonnaci
RCLCPP_INFO(this->get_logger(), "Sending goal");

// use client to call for action
this->client->async_send_goal(goal_msg, send_goal_options);
}

private:
rclcpp_action::Client<Fibonacci>::SharedPtr client;

void cb_goal_response(
            std::shared_future<GoalHandleFibonacci::SharedPtr> future)
{
// handles goal request, returning the goal response
}

void cb_feedback(GoalHandleFibonacci::SharedPtr,
const std::shared_ptr<const Fibonacci::Feedback> feedback)
{
// handles the action cancel, returning the cancel response
}

void cb_result(const GoalHandleFibonacci::WrappedResult &result)
{
// handles the action result
}
};
}

// Registering the client as a node
RCLCPP_COMPONENTS_REGISTER_NODE(fibonacci_action::FibonacciActionClient)



A complete client node would be something  like:


#include <functional>
#include <memory>
#include <thread>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <rclcpp_components/register_node_macro.hpp>

#include "fib_action_interface/action/fibonacci.hpp"

namespace fibonacci_action
{
using Fibonacci = fib_action_interface::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>;
using std::placeholders::_1;
using std::placeholders::_2;

class FibonacciActionClient : public rclcpp::Node
{
public:
explicit FibonacciActionClient(const rclcpp::NodeOptions &options =
rclcpp::NodeOptions()) :
            Node("FibonacciActionClient", options)
{
this->client = rclcpp_action::create_client<Fibonacci>(
this,
"fibonacci");

this->timer = this->create_wall_timer(
                std::chrono::milliseconds(500), [this]() -> void
{ this->request_fibonacci_sequence(10); });
}

void request_fibonacci_sequence(int seq_order)
{
using namespace std::placeholders;

if (!this->client->wait_for_action_server())
{
RCLCPP_ERROR(this->get_logger(),
                    "Action server not available after waiting");
rclcpp::shutdown();
}

// Build options using the client
auto send_goal_options =
                rclcpp_action::Client<Fibonacci>::SendGoalOptions();

// Setting the action's callbacks in the options object
send_goal_options.goal_response_callback =
std::bind(&FibonacciActionClient::cb_goal_response,
                    this, _1);

send_goal_options.feedback_callback =
std::bind(&FibonacciActionClient::cb_feedback,
                    this, _1, _2);

send_goal_options.result_callback =
std::bind(&FibonacciActionClient::cb_result,
                    this, _1);

// building the request message
auto goal_msg = Fibonacci::Goal();
goal_msg.order = seq_order; // 10 position fibonnaci
RCLCPP_INFO(this->get_logger(), "Sending goal");

// use client to call for action
this->client->async_send_goal(goal_msg, send_goal_options);
}

private:
rclcpp_action::Client<Fibonacci>::SharedPtr client;
rclcpp::TimerBase::SharedPtr timer;

void cb_goal_response(
            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 cb_feedback(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 cb_result(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();
}
};
}

// Registering the client as a node
RCLCPP_COMPONENTS_REGISTER_NODE(fibonacci_action::FibonacciActionClient)





And we get the execution results as:

. install/setup.bash
ros2 run fibonacci_action fibonacci_action_server
. install/setup.bash
ros2 run fibonacci_action fibonacci_action_client
[INFO] [1649540533.111528166] [FibonacciActionClient]: Next number in sequence received: 0 1 1 2 3 5 8 13 21 34 
[INFO] [1649540533.111687467] [FibonacciActionClient]: Next number in sequence received: 0 1 1 2 3 5 8 13 21 34 55 
[INFO] [1649540533.113418847] [FibonacciActionClient]: Result received: 0 1 1 2 3 5 8 13 21 34 55 


Nenhum comentário:

Postar um comentário

ROS2: Executables, Plugins and Components

    Introduction       In ROS-2, we have basically three types of code-run generation:  an executable, a plugin and a component.       An ex...