Compare commits

...

11 Commits

Author SHA1 Message Date
mergify[bot]
daffa1a56c Avoid unecessary creation of MultiThreadedExecutor (#3090) (#3095)
(cherry picked from commit 8cd4d47ec5)

Signed-off-by: solonovamax <solonovamax@12oclockpoint.com>
Co-authored-by: solo <solonovamax@12oclockpoint.com>
2026-03-13 12:58:15 +01:00
mergify[bot]
8dd1ec2669 Fix component registering in subdirectories (#3064) (#3075)
(cherry picked from commit fc1afcb3bc)

Signed-off-by: pum1k <55055380+pum1k@users.noreply.github.com>
Co-authored-by: pum1k <55055380+pum1k@users.noreply.github.com>
2026-02-20 12:10:56 +01:00
Janosch Machowinski
dc4a1dbbca fix: Use default rcl allocator if allocator is std::allocator (#3069)
This fixes a bunch of warnings if using ASAN / valgrind on newer
OS versions. It also fixed a real bug, as giving the wrong size
on deallocate is undefined behavior according to the C++ standard.

This version of the patch keeps the behavior for users that
specified an own allocator the same and in therefore back portable.

Signed-off-by: Janosch Machowinski <J.Machowinski@cellumation.com>
Co-authored-by: Janosch Machowinski <j.machowinski@cellumation.com>
2026-02-18 09:52:29 +01:00
Koki Shinjo
6f96da6f99 Backport: Implement action generic client (#2759) (#3017)
* Implement action generic client (#2759)

Backport from rolling to jazzy.

This adds GenericClient for rclcpp_action, which allows creating
action clients without compile-time knowledge of the action type.

New files added:
- generic_client.hpp/cpp
- generic_client_goal_handle.hpp/cpp
- create_generic_client.hpp/cpp
- client_base.hpp (wrapper for jazzy compatibility)
- test_generic_client.cpp

Note: On jazzy, ClientBase remains in client.hpp (not separated).
client_base.hpp is a compatibility wrapper that includes client.hpp.

Original PR: https://github.com/ros2/rclcpp/pull/2759

Signed-off-by: Barry Xu <barry.xu@sony.com>

* Fix jazzy compatibility for generic action client

Add local implementation of get_action_typesupport_handle() which
is not available in jazzy's rclcpp (only in rolling).

Signed-off-by: Koki Shinjo <kshinjo@pfrobotics.jp>

* Remove client_base.hpp and use client.hpp directly in generic_client.hpp

As per review feedback, there is no need to create a separate
client_base.hpp since including client.hpp directly serves the
same purpose.

Signed-off-by: Koki Shinjo <kshinjo@pfrobotics.jp>
Signed-off-by: Barry Xu <barry.xu@sony.com>
Signed-off-by: Koki Shinjo <kshinjo@pfrobotics.jp>
Co-authored-by: Barry Xu <barry.xu@sony.com>
2026-02-13 15:25:54 +01:00
mergify[bot]
f7bcfafcbb fix: Various data races in test cases (#3057) (#3062)
(cherry picked from commit 6ff4d83498)

Signed-off-by: Janosch Machowinski <j.machowinski@cellumation.com>
Co-authored-by: Janosch Machowinski <jmachowinski@users.noreply.github.com>
Co-authored-by: Janosch Machowinski <j.machowinski@cellumation.com>
2026-02-13 09:48:01 +01:00
mergify[bot]
ea3229d463 fix: Fix data race in CallbackGroup::size() (#3056) (#3060)
The computation of the size was not protected by a mutex, leading
to possible data races.



(cherry picked from commit 4d950baa15)

Signed-off-by: Janosch Machowinski <J.Machowinski@cellumation.com>
Co-authored-by: Janosch Machowinski <jmachowinski@users.noreply.github.com>
Co-authored-by: Janosch Machowinski <J.Machowinski@cellumation.com>
2026-02-12 10:46:52 +01:00
Alejandro Hernandez Cordero
8564e013a4 28.1.17 2026-02-09 13:42:45 +01:00
Alejandro Hernandez Cordero
23e9b257ea Changelog
Signed-off-by: Alejandro Hernandez Cordero <ahcorde@gmail.com>
2026-02-09 13:42:40 +01:00
mergify[bot]
4259de8e17 print warning message on owner node if the parameter operation fails. (#3037) (#3039)
(cherry picked from commit f8a7ace7a8)

Signed-off-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
Co-authored-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
2026-01-30 15:13:56 +01:00
mergify[bot]
eaa1db2d56 remove test_static_executor_entities_collector.cpp (#3041) (#3046)
(cherry picked from commit db7469798f)

Signed-off-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
Co-authored-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
2026-01-30 15:04:10 +09:00
mergify[bot]
d6f70317c1 fix context in wait for message wait set (#3030) (#3032)
(cherry picked from commit fcc505f453)

Signed-off-by: Rahat Dhande <rahatchd@gmail.com>
Co-authored-by: Rahat Dhande <rahatchd@gmail.com>
2026-01-23 09:44:17 +01:00
31 changed files with 2664 additions and 656 deletions

View File

@@ -2,6 +2,13 @@
Changelog for package rclcpp
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28.1.17 (2026-02-09)
--------------------
* print warning message on owner node if the parameter operation fails. (`#3037 <https://github.com/ros2/rclcpp/issues/3037>`_) (`#3039 <https://github.com/ros2/rclcpp/issues/3039>`_)
* remove test_static_executor_entities_collector.cpp (`#3041 <https://github.com/ros2/rclcpp/issues/3041>`_) (`#3046 <https://github.com/ros2/rclcpp/issues/3046>`_)
* fix context in wait for message wait set (`#3030 <https://github.com/ros2/rclcpp/issues/3030>`_) (`#3032 <https://github.com/ros2/rclcpp/issues/3032>`_)
* Contributors: mergify[bot]
28.1.16 (2026-01-21)
--------------------
* Improve the robustness of the TopicEndpointInfo constructor (`#3013 <https://github.com/ros2/rclcpp/issues/3013>`_) (`#3015 <https://github.com/ros2/rclcpp/issues/3015>`_)

View File

@@ -18,6 +18,7 @@
#include <memory>
#include <stdexcept>
#include "rcl/allocator.h"
#include "rcl/types.h"
#include "rclcpp/allocator/allocator_common.hpp"
@@ -61,7 +62,12 @@ public:
message_allocator_ = std::make_shared<MessageAlloc>();
serialized_message_allocator_ = std::make_shared<SerializedMessageAlloc>();
buffer_allocator_ = std::make_shared<BufferAlloc>();
rcutils_allocator_ = allocator::get_rcl_allocator<char, BufferAlloc>(*buffer_allocator_.get());
if constexpr (std::is_same_v<Alloc, std::allocator<void>>) {
rcutils_allocator_ = rcl_get_default_allocator();
} else {
rcutils_allocator_ = allocator::get_rcl_allocator<char,
BufferAlloc>(*buffer_allocator_.get());
}
}
explicit MessageMemoryStrategy(std::shared_ptr<Alloc> allocator)
@@ -69,7 +75,12 @@ public:
message_allocator_ = std::make_shared<MessageAlloc>(*allocator.get());
serialized_message_allocator_ = std::make_shared<SerializedMessageAlloc>(*allocator.get());
buffer_allocator_ = std::make_shared<BufferAlloc>(*allocator.get());
rcutils_allocator_ = allocator::get_rcl_allocator<char, BufferAlloc>(*buffer_allocator_.get());
if constexpr (std::is_same_v<Alloc, std::allocator<void>>) {
rcutils_allocator_ = rcl_get_default_allocator();
} else {
rcutils_allocator_ = allocator::get_rcl_allocator<char,
BufferAlloc>(*buffer_allocator_.get());
}
}
virtual ~MessageMemoryStrategy() = default;

View File

@@ -123,6 +123,10 @@ private:
rcl_allocator_t
get_rcl_allocator() const
{
if constexpr (std::is_same_v<Allocator, std::allocator<void>>) {
return rcl_get_default_allocator();
}
if (!plain_allocator_storage_) {
plain_allocator_storage_ =
std::make_shared<PlainAllocator>(*this->get_allocator());

View File

@@ -426,7 +426,11 @@ public:
rcl_allocator_t get_allocator() override
{
return rclcpp::allocator::get_rcl_allocator<void *, VoidAlloc>(*allocator_.get());
if constexpr (std::is_same_v<Alloc, std::allocator<void>>) {
return rcl_get_default_allocator();
} else {
return rclcpp::allocator::get_rcl_allocator<void *, VoidAlloc>(*allocator_.get());
}
}
size_t number_of_ready_subscriptions() const override

View File

@@ -167,11 +167,15 @@ private:
rcl_allocator_t
get_rcl_allocator() const
{
if (!plain_allocator_storage_) {
plain_allocator_storage_ =
std::make_shared<PlainAllocator>(*this->get_allocator());
if constexpr (std::is_same_v<Allocator, std::allocator<void>>) {
return rcl_get_default_allocator();
} else {
if (!plain_allocator_storage_) {
plain_allocator_storage_ =
std::make_shared<PlainAllocator>(*this->get_allocator());
}
return rclcpp::allocator::get_rcl_allocator<char>(*plain_allocator_storage_);
}
return rclcpp::allocator::get_rcl_allocator<char>(*plain_allocator_storage_);
}
// This is a temporal workaround, to make sure that get_allocator()

View File

@@ -56,7 +56,7 @@ bool wait_for_message(
}
});
rclcpp::WaitSet wait_set;
rclcpp::WaitSet wait_set({}, {}, {}, {}, {}, {}, context);
wait_set.add_subscription(subscription);
RCPPUTILS_SCOPE_EXIT(wait_set.remove_subscription(subscription); );
wait_set.add_guard_condition(gc);

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>rclcpp</name>
<version>28.1.16</version>
<version>28.1.17</version>
<description>The ROS client library in C++.</description>
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>

View File

@@ -59,6 +59,7 @@ CallbackGroup::type() const
size_t
CallbackGroup::size() const
{
std::lock_guard<std::mutex> lock(mutex_);
return
subscription_ptrs_.size() +
service_ptrs_.size() +

View File

@@ -47,9 +47,9 @@ ParameterService::ParameterService(
response->values.push_back(param.get_value_message());
}
} catch (const rclcpp::exceptions::ParameterNotDeclaredException & ex) {
RCLCPP_DEBUG(rclcpp::get_logger("rclcpp"), "Failed to get parameters: %s", ex.what());
RCLCPP_WARN(rclcpp::get_logger("rclcpp"), "Failed to get parameters: %s", ex.what());
} catch (const rclcpp::exceptions::ParameterUninitializedException & ex) {
RCLCPP_DEBUG(rclcpp::get_logger("rclcpp"), "Failed to get parameters: %s", ex.what());
RCLCPP_WARN(rclcpp::get_logger("rclcpp"), "Failed to get parameters: %s", ex.what());
}
},
qos_profile, nullptr);
@@ -70,7 +70,7 @@ ParameterService::ParameterService(
return static_cast<rclcpp::ParameterType>(type);
});
} catch (const rclcpp::exceptions::ParameterNotDeclaredException & ex) {
RCLCPP_DEBUG(rclcpp::get_logger("rclcpp"), "Failed to get parameter types: %s", ex.what());
RCLCPP_WARN(rclcpp::get_logger("rclcpp"), "Failed to get parameter types: %s", ex.what());
}
},
qos_profile, nullptr);
@@ -91,7 +91,7 @@ ParameterService::ParameterService(
result = node_params->set_parameters_atomically(
{rclcpp::Parameter::from_parameter_msg(p)});
} catch (const rclcpp::exceptions::ParameterNotDeclaredException & ex) {
RCLCPP_DEBUG(rclcpp::get_logger("rclcpp"), "Failed to set parameter: %s", ex.what());
RCLCPP_WARN(rclcpp::get_logger("rclcpp"), "Failed to set parameter: %s", ex.what());
result.successful = false;
result.reason = ex.what();
}
@@ -119,7 +119,7 @@ ParameterService::ParameterService(
auto result = node_params->set_parameters_atomically(pvariants);
response->result = result;
} catch (const rclcpp::exceptions::ParameterNotDeclaredException & ex) {
RCLCPP_DEBUG(
RCLCPP_WARN(
rclcpp::get_logger("rclcpp"), "Failed to set parameters atomically: %s", ex.what());
response->result.successful = false;
response->result.reason = "One or more parameters were not declared before setting";
@@ -139,7 +139,7 @@ ParameterService::ParameterService(
auto descriptors = node_params->describe_parameters(request->names);
response->descriptors = descriptors;
} catch (const rclcpp::exceptions::ParameterNotDeclaredException & ex) {
RCLCPP_DEBUG(rclcpp::get_logger("rclcpp"), "Failed to describe parameters: %s", ex.what());
RCLCPP_WARN(rclcpp::get_logger("rclcpp"), "Failed to describe parameters: %s", ex.what());
}
},
qos_profile, nullptr);

View File

@@ -81,7 +81,7 @@ public:
rclcpp::Node::SharedPtr node;
rclcpp::Publisher<test_msgs::msg::Empty>::SharedPtr publisher;
rclcpp::Subscription<test_msgs::msg::Empty>::SharedPtr subscription;
int callback_count;
std::atomic<int> callback_count;
};
template<typename T>
@@ -152,7 +152,7 @@ TYPED_TEST(TestExecutors, spinWithTimer)
using ExecutorType = TypeParam;
ExecutorType executor;
bool timer_completed = false;
std::atomic<bool> timer_completed = false;
auto timer = this->node->create_wall_timer(1ms, [&]() {timer_completed = true;});
executor.add_node(this->node);
@@ -263,7 +263,7 @@ TYPED_TEST(TestExecutors, testSpinUntilFutureCompleteNoTimeout)
}
});
bool spin_exited = false;
std::atomic<bool> spin_exited = false;
// Timeout set to negative for no timeout.
std::thread spinner([&]() {
@@ -300,7 +300,7 @@ TYPED_TEST(TestExecutors, testSpinUntilFutureCompleteWithTimeout)
ExecutorType executor;
executor.add_node(this->node);
bool spin_exited = false;
std::atomic<bool> spin_exited = false;
// Needs to run longer than spin_until_future_complete's timeout.
std::future<void> future = std::async(
@@ -344,7 +344,7 @@ TYPED_TEST(TestExecutors, spinAll)
// Long timeout, but should not block test if spin_all works as expected as we cancel the
// executor.
bool spin_exited = false;
std::atomic<bool> spin_exited = false;
std::thread spinner([&spin_exited, &executor, this]() {
executor.spin_all(1s);
executor.remove_node(this->node, true);
@@ -592,7 +592,7 @@ TYPED_TEST(TestExecutors, testSpinUntilFutureCompleteInterrupted)
ExecutorType executor;
executor.add_node(this->node);
bool spin_exited = false;
std::atomic<bool> spin_exited = false;
// This needs to block longer than it takes to get to the shutdown call below and for
// spin_until_future_complete to return

View File

@@ -1,617 +0,0 @@
// Copyright 2020 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <gtest/gtest.h>
#include <chrono>
#include <memory>
#include <stdexcept>
#include "rclcpp/rclcpp.hpp"
#include "rcpputils/scope_exit.hpp"
#include "test_msgs/msg/empty.hpp"
#include "test_msgs/srv/empty.hpp"
#include "../../mocking_utils/patch.hpp"
#include "../../utils/rclcpp_gtest_macros.hpp"
namespace
{
struct NumberOfEntities
{
size_t subscriptions = 0;
size_t timers = 0;
size_t services = 0;
size_t clients = 0;
size_t waitables = 0;
};
std::unique_ptr<NumberOfEntities> get_number_of_default_entities(rclcpp::Node::SharedPtr node)
{
auto number_of_entities = std::make_unique<NumberOfEntities>();
node->for_each_callback_group(
[&number_of_entities](rclcpp::CallbackGroup::SharedPtr group)
{
if (!group->can_be_taken_from().load()) {
return;
}
group->find_subscription_ptrs_if(
[&number_of_entities](rclcpp::SubscriptionBase::SharedPtr &)
{
number_of_entities->subscriptions++; return false;
});
group->find_timer_ptrs_if(
[&number_of_entities](rclcpp::TimerBase::SharedPtr &)
{
number_of_entities->timers++; return false;
});
group->find_service_ptrs_if(
[&number_of_entities](rclcpp::ServiceBase::SharedPtr &)
{
number_of_entities->services++; return false;
});
group->find_client_ptrs_if(
[&number_of_entities](rclcpp::ClientBase::SharedPtr &)
{
number_of_entities->clients++; return false;
});
group->find_waitable_ptrs_if(
[&number_of_entities](rclcpp::Waitable::SharedPtr &)
{
number_of_entities->waitables++; return false;
});
});
return number_of_entities;
}
} // namespace
class TestStaticExecutorEntitiesCollector : public ::testing::Test
{
public:
void SetUp()
{
rclcpp::init(0, nullptr);
entities_collector_ =
std::make_shared<rclcpp::executors::StaticExecutorEntitiesCollector>();
}
void TearDown()
{
rclcpp::shutdown();
}
rclcpp::executors::StaticExecutorEntitiesCollector::SharedPtr entities_collector_;
};
TEST_F(TestStaticExecutorEntitiesCollector, construct_destruct) {
EXPECT_EQ(0u, entities_collector_->get_number_of_subscriptions());
EXPECT_EQ(0u, entities_collector_->get_number_of_timers());
EXPECT_EQ(0u, entities_collector_->get_number_of_services());
EXPECT_EQ(0u, entities_collector_->get_number_of_clients());
EXPECT_EQ(0u, entities_collector_->get_number_of_waitables());
}
TEST_F(TestStaticExecutorEntitiesCollector, add_remove_node) {
auto node1 = std::make_shared<rclcpp::Node>("node1", "ns");
EXPECT_NO_THROW(entities_collector_->add_node(node1->get_node_base_interface()));
// Check adding second time
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->add_node(node1->get_node_base_interface()),
std::runtime_error("Node has already been added to an executor."));
auto node2 = std::make_shared<rclcpp::Node>("node2", "ns");
EXPECT_FALSE(entities_collector_->remove_node(node2->get_node_base_interface()));
EXPECT_NO_THROW(entities_collector_->add_node(node2->get_node_base_interface()));
EXPECT_TRUE(entities_collector_->remove_node(node1->get_node_base_interface()));
EXPECT_FALSE(entities_collector_->remove_node(node1->get_node_base_interface()));
EXPECT_TRUE(entities_collector_->remove_node(node2->get_node_base_interface()));
auto node3 = std::make_shared<rclcpp::Node>("node3", "ns");
node3->get_node_base_interface()->get_associated_with_executor_atomic().exchange(true);
EXPECT_FALSE(entities_collector_->remove_node(node3->get_node_base_interface()));
}
TEST_F(TestStaticExecutorEntitiesCollector, init_bad_arguments) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
rclcpp::GuardCondition guard_condition(shared_context);
// Check memory strategy is nullptr
rclcpp::memory_strategy::MemoryStrategy::SharedPtr memory_strategy = nullptr;
EXPECT_THROW(
entities_collector_->init(&wait_set, memory_strategy),
std::runtime_error);
}
TEST_F(TestStaticExecutorEntitiesCollector, add_remove_basic_node) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
const auto expected_number_of_entities = get_number_of_default_entities(node);
EXPECT_NE(nullptr, expected_number_of_entities);
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
EXPECT_EQ(
expected_number_of_entities->subscriptions,
entities_collector_->get_number_of_subscriptions());
EXPECT_EQ(expected_number_of_entities->timers, entities_collector_->get_number_of_timers());
EXPECT_EQ(expected_number_of_entities->services, entities_collector_->get_number_of_services());
EXPECT_EQ(expected_number_of_entities->clients, entities_collector_->get_number_of_clients());
// One extra for the executor
EXPECT_EQ(
1u + expected_number_of_entities->waitables,
entities_collector_->get_number_of_waitables());
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
entities_collector_->init(&wait_set, memory_strategy);
EXPECT_EQ(0u, entities_collector_->get_number_of_subscriptions());
EXPECT_EQ(0u, entities_collector_->get_number_of_timers());
EXPECT_EQ(0u, entities_collector_->get_number_of_services());
EXPECT_EQ(0u, entities_collector_->get_number_of_clients());
// Still one for the executor
EXPECT_EQ(1u, entities_collector_->get_number_of_waitables());
}
TEST_F(TestStaticExecutorEntitiesCollector, add_remove_node_out_of_scope) {
rclcpp::Context::SharedPtr shared_context = nullptr;
{
auto node1 = std::make_shared<rclcpp::Node>("node1", "ns");
auto node2 = std::make_shared<rclcpp::Node>("node2", "ns");
auto node3 = std::make_shared<rclcpp::Node>("node3", "ns");
entities_collector_->add_node(node1->get_node_base_interface());
entities_collector_->add_node(node2->get_node_base_interface());
entities_collector_->add_node(node3->get_node_base_interface());
shared_context = node1->get_node_base_interface()->get_context();
}
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
// Expect weak_node pointers to be cleaned up and used
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
EXPECT_EQ(0u, entities_collector_->get_number_of_subscriptions());
EXPECT_EQ(0u, entities_collector_->get_number_of_timers());
EXPECT_EQ(0u, entities_collector_->get_number_of_services());
EXPECT_EQ(0u, entities_collector_->get_number_of_clients());
// Still one for the executor
EXPECT_EQ(1u, entities_collector_->get_number_of_waitables());
}
class TestWaitable : public rclcpp::Waitable
{
public:
void add_to_wait_set(rcl_wait_set_t &) override {}
bool is_ready(const rcl_wait_set_t &) override {return true;}
std::shared_ptr<void>
take_data() override
{
return nullptr;
}
void
execute(const std::shared_ptr<void> &) override {}
};
TEST_F(TestStaticExecutorEntitiesCollector, add_remove_node_with_entities) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
auto expected_number_of_entities = get_number_of_default_entities(node);
EXPECT_NE(nullptr, expected_number_of_entities);
// Create 1 of each entity type
auto subscription =
node->create_subscription<test_msgs::msg::Empty>(
"topic", rclcpp::QoS(10), [](test_msgs::msg::Empty::ConstSharedPtr) {});
auto timer =
node->create_wall_timer(std::chrono::seconds(60), []() {});
auto service =
node->create_service<test_msgs::srv::Empty>(
"service",
[](
const test_msgs::srv::Empty::Request::SharedPtr,
test_msgs::srv::Empty::Response::SharedPtr) {});
auto client = node->create_client<test_msgs::srv::Empty>("service");
auto waitable = std::make_shared<TestWaitable>();
// Adding a subscription could add another waitable, so we need to get the
// current number of waitables just before adding the new waitable.
expected_number_of_entities->waitables = get_number_of_default_entities(node)->waitables;
node->get_node_waitables_interface()->add_waitable(waitable, nullptr);
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
EXPECT_EQ(
1u + expected_number_of_entities->subscriptions,
entities_collector_->get_number_of_subscriptions());
EXPECT_EQ(1u + expected_number_of_entities->timers, entities_collector_->get_number_of_timers());
EXPECT_EQ(
1u + expected_number_of_entities->services,
entities_collector_->get_number_of_services());
EXPECT_EQ(
1u + expected_number_of_entities->clients,
entities_collector_->get_number_of_clients());
// One extra for the executor
EXPECT_EQ(
2u + expected_number_of_entities->waitables,
entities_collector_->get_number_of_waitables());
entities_collector_->remove_node(node->get_node_base_interface());
entities_collector_->init(&wait_set, memory_strategy);
EXPECT_EQ(0u, entities_collector_->get_number_of_subscriptions());
EXPECT_EQ(0u, entities_collector_->get_number_of_timers());
EXPECT_EQ(0u, entities_collector_->get_number_of_services());
EXPECT_EQ(0u, entities_collector_->get_number_of_clients());
// Still one for the executor
EXPECT_EQ(1u, entities_collector_->get_number_of_waitables());
}
TEST_F(TestStaticExecutorEntitiesCollector, add_callback_group) {
auto node = std::make_shared<rclcpp::Node>("node1", "ns");
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface());
ASSERT_EQ(entities_collector_->get_all_callback_groups().size(), 1u);
}
TEST_F(TestStaticExecutorEntitiesCollector, add_callback_group_after_add_node) {
auto node = std::make_shared<rclcpp::Node>("node1", "ns");
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_node(node->get_node_base_interface());
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface()),
std::runtime_error("Callback group has already been added to an executor."));
}
TEST_F(TestStaticExecutorEntitiesCollector, add_callback_group_twice) {
auto node = std::make_shared<rclcpp::Node>("node1", "ns");
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface());
ASSERT_EQ(entities_collector_->get_all_callback_groups().size(), 1u);
cb_group->get_associated_with_executor_atomic().exchange(false);
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface()),
std::runtime_error("Callback group was already added to executor."));
}
TEST_F(TestStaticExecutorEntitiesCollector, prepare_wait_set_rcl_wait_set_clear_error) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
{
auto mock = mocking_utils::patch_and_return("lib:rclcpp", rcl_wait_set_clear, RCL_RET_ERROR);
std::shared_ptr<void> data = entities_collector_->take_data();
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->execute(data),
std::runtime_error("Couldn't clear wait set"));
}
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
}
TEST_F(TestStaticExecutorEntitiesCollector, prepare_wait_set_rcl_wait_set_resize_error) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
{
auto mock = mocking_utils::patch_and_return("lib:rclcpp", rcl_wait_set_resize, RCL_RET_ERROR);
std::shared_ptr<void> data = entities_collector_->take_data();
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->execute(data),
std::runtime_error("Couldn't resize the wait set: error not set"));
}
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
}
TEST_F(TestStaticExecutorEntitiesCollector, refresh_wait_set_not_initialized) {
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->refresh_wait_set(std::chrono::nanoseconds(1000)),
std::runtime_error("Couldn't clear wait set"));
rcl_reset_error();
}
TEST_F(TestStaticExecutorEntitiesCollector, refresh_wait_set_rcl_wait_failed) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
{
auto mock = mocking_utils::patch_and_return("lib:rclcpp", rcl_wait, RCL_RET_ERROR);
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->refresh_wait_set(std::chrono::nanoseconds(1000)),
std::runtime_error("rcl_wait() failed: error not set"));
}
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
}
TEST_F(TestStaticExecutorEntitiesCollector, refresh_wait_set_add_handles_to_wait_set_failed) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
// Create 1 of each entity type
auto subscription =
node->create_subscription<test_msgs::msg::Empty>(
"topic", rclcpp::QoS(10), [](test_msgs::msg::Empty::ConstSharedPtr) {});
auto timer =
node->create_wall_timer(std::chrono::seconds(60), []() {});
auto service =
node->create_service<test_msgs::srv::Empty>(
"service",
[](
const test_msgs::srv::Empty::Request::SharedPtr,
test_msgs::srv::Empty::Response::SharedPtr) {});
auto client = node->create_client<test_msgs::srv::Empty>("service");
auto waitable = std::make_shared<TestWaitable>();
node->get_node_waitables_interface()->add_waitable(waitable, nullptr);
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
{
auto mock = mocking_utils::patch_and_return(
"lib:rclcpp", rcl_wait_set_add_subscription,
RCL_RET_ERROR);
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->refresh_wait_set(std::chrono::nanoseconds(1000)),
std::runtime_error("Couldn't fill wait set"));
}
entities_collector_->remove_node(node->get_node_base_interface());
}
TEST_F(TestStaticExecutorEntitiesCollector, add_to_wait_set_nullptr) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
}
TEST_F(TestStaticExecutorEntitiesCollector, fill_memory_strategy_invalid_group) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface());
ASSERT_EQ(entities_collector_->get_all_callback_groups().size(), 2u);
cb_group.reset();
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
ASSERT_EQ(entities_collector_->get_all_callback_groups().size(), 1u);
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
}
TEST_F(TestStaticExecutorEntitiesCollector, remove_callback_group_after_node) {
auto node = std::make_shared<rclcpp::Node>("node1", "ns");
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface());
ASSERT_EQ(entities_collector_->get_all_callback_groups().size(), 1u);
node.reset();
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->remove_callback_group(cb_group),
std::runtime_error("Node must not be deleted before its callback group(s)."));
}
TEST_F(TestStaticExecutorEntitiesCollector, remove_callback_group_twice) {
auto node = std::make_shared<rclcpp::Node>("node1", "ns");
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface());
ASSERT_EQ(entities_collector_->get_all_callback_groups().size(), 1u);
entities_collector_->remove_callback_group(cb_group);
RCLCPP_EXPECT_THROW_EQ(
entities_collector_->remove_callback_group(cb_group),
std::runtime_error("Callback group needs to be associated with executor."));
}
TEST_F(TestStaticExecutorEntitiesCollector, remove_node_opposite_order) {
auto node1 = std::make_shared<rclcpp::Node>("node1", "ns");
EXPECT_NO_THROW(entities_collector_->add_node(node1->get_node_base_interface()));
auto node2 = std::make_shared<rclcpp::Node>("node2", "ns");
EXPECT_NO_THROW(entities_collector_->add_node(node2->get_node_base_interface()));
EXPECT_TRUE(entities_collector_->remove_node(node2->get_node_base_interface()));
}
TEST_F(
TestStaticExecutorEntitiesCollector,
add_callback_groups_from_nodes_associated_to_executor_add) {
auto node = std::make_shared<rclcpp::Node>("node", "ns");
rclcpp::CallbackGroup::SharedPtr cb_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
entities_collector_->add_callback_group(cb_group, node->get_node_base_interface());
entities_collector_->add_node(node->get_node_base_interface());
rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
rcl_allocator_t allocator = rcl_get_default_allocator();
auto shared_context = node->get_node_base_interface()->get_context();
rcl_context_t * context = shared_context->get_rcl_context().get();
EXPECT_EQ(
RCL_RET_OK,
rcl_wait_set_init(&wait_set, 100, 100, 100, 100, 100, 100, context, allocator));
RCPPUTILS_SCOPE_EXIT({EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set));});
auto memory_strategy = rclcpp::memory_strategies::create_default_strategy();
rclcpp::GuardCondition guard_condition(shared_context);
entities_collector_->init(&wait_set, memory_strategy);
RCPPUTILS_SCOPE_EXIT(entities_collector_->fini());
cb_group->get_associated_with_executor_atomic().exchange(false);
std::shared_ptr<void> data = entities_collector_->take_data();
entities_collector_->execute(data);
EXPECT_TRUE(entities_collector_->remove_node(node->get_node_base_interface()));
}

View File

@@ -134,3 +134,32 @@ TEST(TestUtilities, wait_for_last_message) {
rclcpp::shutdown();
}
TEST(TestUtilities, wait_for_message_custom_context) {
auto context = std::make_shared<rclcpp::Context>();
context->init(0, nullptr);
auto node_opt = rclcpp::NodeOptions().context(context);
auto node = std::make_shared<rclcpp::Node>("wait_for_message_custom_context_node", node_opt);
using MsgT = test_msgs::msg::Strings;
auto pub = node->create_publisher<MsgT>("wait_for_message_topic", 10);
MsgT out;
auto received = false;
auto wait = std::async(
[&]() {
auto ret = rclcpp::wait_for_message(out, node, "wait_for_message_topic", 5s);
EXPECT_TRUE(ret);
received = true;
});
for (auto i = 0u; i < 10 && received == false; ++i) {
pub->publish(*get_messages_strings()[0]);
std::this_thread::sleep_for(1s);
}
ASSERT_TRUE(received);
EXPECT_EQ(out, *get_messages_strings()[0]);
context->shutdown("test complete");
}

View File

@@ -3,6 +3,9 @@ Changelog for package rclcpp_action
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28.1.17 (2026-02-09)
--------------------
28.1.16 (2026-01-21)
--------------------
* Update exception documentation for goal cancellation in ServerGoalHandle (`#3019 <https://github.com/ros2/rclcpp/issues/3019>`_) (`#3023 <https://github.com/ros2/rclcpp/issues/3023>`_)

View File

@@ -24,6 +24,9 @@ endif()
add_library(${PROJECT_NAME}
src/client.cpp
src/create_generic_client.cpp
src/generic_client.cpp
src/generic_client_goal_handle.cpp
src/qos.cpp
src/server.cpp
src/server_goal_handle.cpp
@@ -92,6 +95,20 @@ if(BUILD_TESTING)
)
endif()
ament_add_gtest(test_generic_client test/test_generic_client.cpp TIMEOUT 180)
ament_add_test_label(test_generic_client mimick)
if(TARGET test_generic_client)
target_link_libraries(test_generic_client
${PROJECT_NAME}
mimick
rcl::rcl
rcl_action::rcl_action
rclcpp::rclcpp
rcutils::rcutils
${test_msgs_TARGETS}
)
endif()
ament_add_gtest(test_server test/test_server.cpp TIMEOUT 180)
if(TARGET test_server)
target_link_libraries(test_server

View File

@@ -0,0 +1,83 @@
// Copyright 2025 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef RCLCPP_ACTION__CREATE_GENERIC_CLIENT_HPP_
#define RCLCPP_ACTION__CREATE_GENERIC_CLIENT_HPP_
#include <string>
#include "rclcpp_action/generic_client.hpp"
namespace rclcpp_action
{
/// Create an action generic client.
/**
* This function is equivalent to \sa create_generic_client()` however is using the individual
* node interfaces to create the client.
*
* \param[in] node_base_interface The node base interface of the corresponding node.
* \param[in] node_graph_interface The node graph interface of the corresponding node.
* \param[in] node_logging_interface The node logging interface of the corresponding node.
* \param[in] node_waitables_interface The node waitables interface of the corresponding node.
* \param[in] name The action name.
* \param[in] type The action type.
* \param[in] group The action client will be added to this callback group.
* If `nullptr`, then the action client is added to the default callback group.
* \param[in] options Options to pass to the underlying `rcl_action_client_t`.
* \return newly created generic client.
*/
RCLCPP_ACTION_PUBLIC
typename GenericClient::SharedPtr
create_generic_client(
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base_interface,
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph_interface,
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging_interface,
rclcpp::node_interfaces::NodeWaitablesInterface::SharedPtr node_waitables_interface,
const std::string & name,
const std::string & type,
rclcpp::CallbackGroup::SharedPtr group = nullptr,
const rcl_action_client_options_t & options = rcl_action_client_get_default_options());
/// Create an action generic client.
/**
* \param[in] node The action client will be added to this node.
* \param[in] name The action name.
* \param[in] type The action type.
* \param[in] group The action client will be added to this callback group.
* If `nullptr`, then the action client is added to the default callback group.
* \param[in] options Options to pass to the underlying `rcl_action_client_t`.
* \return newly created generic client.
*/
template<typename NodeT>
typename GenericClient::SharedPtr
create_generic_client(
NodeT node,
const std::string & name,
const std::string & type,
rclcpp::CallbackGroup::SharedPtr group = nullptr,
const rcl_action_client_options_t & options = rcl_action_client_get_default_options())
{
return rclcpp_action::create_generic_client(
node->get_node_base_interface(),
node->get_node_graph_interface(),
node->get_node_logging_interface(),
node->get_node_waitables_interface(),
name,
type,
group,
options);
}
} // namespace rclcpp_action
#endif // RCLCPP_ACTION__CREATE_GENERIC_CLIENT_HPP_

View File

@@ -0,0 +1,333 @@
// Copyright 2025 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef RCLCPP_ACTION__GENERIC_CLIENT_HPP_
#define RCLCPP_ACTION__GENERIC_CLIENT_HPP_
#include <cstddef>
#include <functional>
#include <future>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include "action_msgs/srv/cancel_goal.hpp"
#include "action_msgs/msg/goal_info.hpp"
#include "action_msgs/msg/goal_status_array.hpp"
#include "rclcpp_action/client.hpp"
#include "rclcpp_action/generic_client_goal_handle.hpp"
#include "rcpputils/shared_library.hpp"
#include "rosidl_typesupport_introspection_cpp/identifier.hpp"
#include "rosidl_typesupport_introspection_cpp/message_introspection.hpp"
#include "unique_identifier_msgs/msg/uuid.hpp"
namespace rclcpp_action
{
/// Action Generic Client
/**
* This class creates an action generic client.
*
* To create an instance of an action client use `rclcpp_action::create_generic_client()`.
*/
class GenericClient : public ClientBase
{
public:
RCLCPP_SMART_PTR_DEFINITIONS_NOT_COPYABLE(GenericClient)
using Goal = void *; // Deserialized data pointer of goal
using GoalRequest = void *; // Deserialized data pointer of goal request (uuid + Goal)
using CancelRequest = typename action_msgs::srv::CancelGoal_Request;
using CancelResponse = typename action_msgs::srv::CancelGoal_Response;
using WrappedResult = typename GenericClientGoalHandle::WrappedResult;
using GoalResponseCallback = std::function<void (typename GenericClientGoalHandle::SharedPtr)>;
using FeedbackCallback = typename GenericClientGoalHandle::FeedbackCallback;
using ResultCallback = typename GenericClientGoalHandle::ResultCallback;
using CancelCallback = std::function<void (CancelResponse::SharedPtr)>;
using IntrospectionMessageMembersPtr =
const rosidl_typesupport_introspection_cpp::MessageMembers *;
/// Options for sending a goal.
/**
* This struct is used to pass parameters to the function `async_send_goal`.
*/
struct SendGoalOptions
{
SendGoalOptions()
: goal_response_callback(nullptr),
feedback_callback(nullptr),
result_callback(nullptr)
{
}
/// Function called when the goal is accepted or rejected.
/**
* Takes a single argument that is a goal handle shared pointer.
* If the goal is accepted, then the pointer points to a valid goal handle.
* If the goal is rejected, then pointer has the value `nullptr`.
*/
GoalResponseCallback goal_response_callback;
/// Function called whenever feedback is received for the goal.
FeedbackCallback feedback_callback;
/// Function called when the result for the goal is received.
ResultCallback result_callback;
};
/// Construct an action generic client.
/**
* This constructs an action generic client, but it will not work until it has been added to a
* node.
* Use `rclcpp_action::create_generic_client()` to both construct and add to a node.
*
* \param[in] node_base A pointer to the base interface of a node.
* \param[in] node_graph A pointer to an interface that allows getting graph information about
* a node.
* \param[in] node_logging A pointer to an interface that allows getting a node's logger.
* \param[in] action_name The action name.
* \param[in] typesupport_lib A pointer to type support library for the action type
* \param[in] action_typesupport_handle the action type support handle
* \param[in] client_options Options to pass to the underlying `rcl_action::rcl_action_client_t`.
*/
RCLCPP_ACTION_PUBLIC
GenericClient(
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base,
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph,
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging,
const std::string & action_name,
std::shared_ptr<rcpputils::SharedLibrary> typesupport_lib,
const rosidl_action_type_support_t * action_typesupport_handle,
const rcl_action_client_options_t & client_options = rcl_action_client_get_default_options());
/// Send an action goal and asynchronously get the result.
/**
* If the goal is accepted by an action server, the returned future is set to a `GenericClientGoalHandle::SharedPtr`.
* If the goal is rejected by an action server, then the future is set to a `nullptr`.
*
* The goal handle in the future is used to monitor the status of the goal and get the final result.
*
* If callbacks were set in @param options, you will receive callbacks, as long as you hold a reference
* to the shared pointer contained in the returned future, or rclcpp_action::GenericClient is destroyed. Dropping
* the shared pointer to the goal handle will not cancel the goal. In order to cancel it, you must explicitly
* call async_cancel_goal.
*
* WARNING this method has inconsistent behaviour and a memory leak bug.
* If you set the result callback in @param options, the handle will be self referencing, and you will receive
* callbacks even though you do not hold a reference to the shared pointer. In this case, the self reference will
* be deleted if the result callback was received. If there is no result callback, there will be a memory leak.
*
* To prevent the memory leak, you may call stop_callbacks() explicit. This will delete the self reference.
*
* \param[in] goal The goal.
* \param[in] goal_size The size of goal.
* \param[in] options Options for sending the goal request. Contains references to callbacks for
* the goal response (accepted/rejected), feedback, and the final result.
* \return A future that completes when the goal has been accepted or rejected.
* If the goal is rejected, then the result will be a `nullptr`.
*/
RCLCPP_ACTION_PUBLIC
std::shared_future<typename GenericClientGoalHandle::SharedPtr>
async_send_goal(
const Goal goal,
size_t goal_size,
const SendGoalOptions & options = SendGoalOptions());
/// Send an action goal request and asynchronously get the result.
/**
* \param[in] goal_request The goal request (uuid+goal).
* \param[in] options Options for sending the goal request. Contains references to callbacks for
* the goal response (accepted/rejected), feedback, and the final result.
* \return A future that completes when the goal has been accepted or rejected.
* If the goal is rejected, then the result will be a `nullptr`.
*/
RCLCPP_ACTION_PUBLIC
std::shared_future<typename GenericClientGoalHandle::SharedPtr>
async_send_goal(
const GoalRequest goal_request,
const SendGoalOptions & options = SendGoalOptions());
/// Asynchronously get the result for an active goal.
/**
* \throws exceptions::UnknownGoalHandleError If the goal unknown or already reached a terminal
* state, or if there was an error requesting the result.
* \param[in] goal_handle The goal handle for which to get the result.
* \param[in] result_callback Optional callback that is called when the result is received.
* \return A future that is set to the goal result when the goal is finished.
*/
RCLCPP_ACTION_PUBLIC
std::shared_future<WrappedResult>
async_get_result(
typename GenericClientGoalHandle::SharedPtr goal_handle,
ResultCallback result_callback = nullptr);
/// Asynchronously request a goal be canceled.
/**
* \throws exceptions::UnknownGoalHandleError If the goal is unknown or already reached a
* terminal state.
* \param[in] goal_handle The goal handle requesting to be canceled.
* \param[in] cancel_callback Optional callback that is called when the response is received.
* The callback takes one parameter: a shared pointer to the CancelResponse message.
* \return A future to a CancelResponse message that is set when the request has been
* acknowledged by an action server.
* See
* <a href="https://github.com/ros2/rcl_interfaces/blob/master/action_msgs/srv/CancelGoal.srv">
* action_msgs/CancelGoal.srv</a>.
*/
RCLCPP_ACTION_PUBLIC
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel_goal(
typename GenericClientGoalHandle::SharedPtr goal_handle,
CancelCallback cancel_callback = nullptr);
/// Asynchronously request for all goals to be canceled.
/**
* \param[in] cancel_callback Optional callback that is called when the response is received.
* The callback takes one parameter: a shared pointer to the CancelResponse message.
* \return A future to a CancelResponse message that is set when the request has been
* acknowledged by an action server.
* See
* <a href="https://github.com/ros2/rcl_interfaces/blob/master/action_msgs/srv/CancelGoal.srv">
* action_msgs/CancelGoal.srv</a>.
*/
RCLCPP_ACTION_PUBLIC
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel_all_goals(CancelCallback cancel_callback = nullptr);
/// Stops the callbacks for the goal in a thread safe way
/**
* This will NOT cancel the goal, it will only stop the callbacks.
*
* After the call to this function, it is guaranteed that there
* will be no more callbacks from the goal. This is not guaranteed
* if multiple threads are involved, and the goal_handle is just
* dropped.
*
* \param[in] goal_handle The goal were the callbacks shall be stopped
*/
RCLCPP_ACTION_PUBLIC
void
stop_callbacks(typename GenericClientGoalHandle::SharedPtr goal_handle);
/// Stops the callbacks for the goal in a thread safe way
/**
* For further information see stop_callbacks(typename GenericGoalHandle::SharedPtr goal_handle)
*/
RCLCPP_ACTION_PUBLIC
void
stop_callbacks(const GoalUUID & goal_id);
/// Asynchronously request all goals at or before a specified time be canceled.
/**
* \param[in] stamp The timestamp for the cancel goal request.
* \param[in] cancel_callback Optional callback that is called when the response is received.
* The callback takes one parameter: a shared pointer to the CancelResponse message.
* \return A future to a CancelResponse message that is set when the request has been
* acknowledged by an action server.
* See
* <a href="https://github.com/ros2/rcl_interfaces/blob/master/action_msgs/srv/CancelGoal.srv">
* action_msgs/CancelGoal.srv</a>.
*/
RCLCPP_ACTION_PUBLIC
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel_goals_before(
const rclcpp::Time & stamp,
CancelCallback cancel_callback = nullptr);
RCLCPP_ACTION_PUBLIC
virtual
~GenericClient();
private:
/// \internal
std::shared_ptr<void>
create_message(IntrospectionMessageMembersPtr message_members) const;
/// \internal
std::shared_ptr<void>
create_goal_response() const override
{
return create_message(goal_service_response_type_members_);
}
/// \internal
std::shared_ptr<void>
create_result_request() const
{
return create_message(result_service_request_type_members_);
}
/// \internal
std::shared_ptr<void>
create_result_response() const override
{
return create_message(result_service_response_type_members_);
}
/// \internal
std::shared_ptr<void>
create_cancel_response() const override
{
return create_message(cancel_service_response_type_members_);
}
/// \internal
std::shared_ptr<void>
create_feedback_message() const override
{
return create_message(feedback_type_members_);
}
/// \internal
void
handle_feedback_message(std::shared_ptr<void> message) override;
/// \internal
std::shared_ptr<void>
create_status_message() const override
{
using GoalStatusMessage = action_msgs::msg::GoalStatusArray;
return std::shared_ptr<void>(new GoalStatusMessage());
}
/// \internal
void
handle_status_message(std::shared_ptr<void> message) override;
/// \internal
void
make_result_aware(typename GenericClientGoalHandle::SharedPtr goal_handle);
/// \internal
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel(
typename CancelRequest::SharedPtr cancel_request,
CancelCallback cancel_callback = nullptr);
std::shared_ptr<rcpputils::SharedLibrary> ts_lib_;
IntrospectionMessageMembersPtr goal_service_request_type_members_;
IntrospectionMessageMembersPtr goal_service_response_type_members_;
IntrospectionMessageMembersPtr result_service_request_type_members_;
IntrospectionMessageMembersPtr result_service_response_type_members_;
IntrospectionMessageMembersPtr cancel_service_response_type_members_;
IntrospectionMessageMembersPtr feedback_type_members_;
std::map<GoalUUID, typename GenericClientGoalHandle::WeakPtr> goal_handles_;
std::recursive_mutex goal_handles_mutex_;
};
} // namespace rclcpp_action
#endif // RCLCPP_ACTION__GENERIC_CLIENT_HPP_

View File

@@ -0,0 +1,163 @@
// Copyright 2025 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef RCLCPP_ACTION__GENERIC_CLIENT_GOAL_HANDLE_HPP_
#define RCLCPP_ACTION__GENERIC_CLIENT_GOAL_HANDLE_HPP_
#include <cstdint>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include "action_msgs/msg/goal_status.hpp"
#include "rclcpp/macros.hpp"
#include "rclcpp/time.hpp"
#include "rclcpp_action/exceptions.hpp"
#include "rclcpp_action/types.hpp"
#include "rclcpp_action/visibility_control.hpp"
namespace rclcpp_action
{
// Forward declarations
class GenericClient;
class GenericClientGoalHandle
{
public:
RCLCPP_SMART_PTR_DEFINITIONS_NOT_COPYABLE(GenericClientGoalHandle)
/// The possible statuses that an action goal can finish with.
enum class ResultCode : int8_t
{
UNKNOWN = action_msgs::msg::GoalStatus::STATUS_UNKNOWN,
SUCCEEDED = action_msgs::msg::GoalStatus::STATUS_SUCCEEDED,
CANCELED = action_msgs::msg::GoalStatus::STATUS_CANCELED,
ABORTED = action_msgs::msg::GoalStatus::STATUS_ABORTED
};
// A wrapper that defines the result of an action
struct WrappedResult
{
/// The unique identifier of the goal
GoalUUID goal_id;
/// A status to indicate if the goal was canceled, aborted, or succeeded
ResultCode code;
/// User defined fields sent back with an action
const void * result;
/// hold shared pointer for result response message.
std::shared_ptr<void> result_response;
};
using FeedbackCallback =
std::function<void (
typename GenericClientGoalHandle::SharedPtr,
const void *)>;
using ResultCallback = std::function<void (const WrappedResult & result)>;
RCLCPP_ACTION_PUBLIC
virtual
~GenericClientGoalHandle();
/// Get the unique ID for the goal.
RCLCPP_ACTION_PUBLIC
const GoalUUID &
get_goal_id() const;
/// Get the time when the goal was accepted.
RCLCPP_ACTION_PUBLIC
rclcpp::Time
get_goal_stamp() const;
/// Get the goal status code.
RCLCPP_ACTION_PUBLIC
int8_t
get_status();
/// Check if an action client has subscribed to feedback for the goal.
RCLCPP_ACTION_PUBLIC
bool
is_feedback_aware();
/// Check if an action client has requested the result for the goal.
RCLCPP_ACTION_PUBLIC
bool
is_result_aware();
private:
// The templated Client creates goal handles
friend class GenericClient;
GenericClientGoalHandle(
const GoalInfo & info,
FeedbackCallback feedback_callback,
ResultCallback result_callback);
void
set_feedback_callback(FeedbackCallback callback);
void
set_result_callback(ResultCallback callback);
void
call_feedback_callback(
GenericClientGoalHandle::SharedPtr shared_this,
const void * feedback_message);
/// Get a future to the goal result.
/**
* This method should not be called if the `ignore_result` flag was set when
* sending the original goal request (see Client::async_send_goal).
*
* `is_result_aware()` can be used to check if it is safe to call this method.
*
* \throws exceptions::UnawareGoalHandleError If the the goal handle is unaware of the result.
* \return A future to the result.
*/
std::shared_future<WrappedResult>
async_get_result();
/// Returns the previous value of awareness
bool
set_result_awareness(bool awareness);
void
set_status(int8_t status);
void
set_result(const WrappedResult & wrapped_result);
void
invalidate(const exceptions::UnawareGoalHandleError & ex);
bool
is_invalidated() const;
GoalInfo info_;
std::exception_ptr invalidate_exception_{nullptr};
bool is_result_aware_{false};
std::promise<WrappedResult> result_promise_;
std::shared_future<WrappedResult> result_future_;
FeedbackCallback feedback_callback_{nullptr};
ResultCallback result_callback_{nullptr};
int8_t status_{GoalStatus::STATUS_ACCEPTED};
std::recursive_mutex handle_mutex_;
};
} // namespace rclcpp_action
#endif // RCLCPP_ACTION__GENERIC_CLIENT_GOAL_HANDLE_HPP_

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>rclcpp_action</name>
<version>28.1.16</version>
<version>28.1.17</version>
<description>Adds action APIs for C++.</description>
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>

View File

@@ -0,0 +1,129 @@
// Copyright 2025 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <memory>
#include <sstream>
#include <string>
#include <tuple>
#include "rclcpp/node.hpp"
#include "rclcpp/typesupport_helpers.hpp"
#include "rcpputils/shared_library.hpp"
#include "rosidl_runtime_c/action_type_support_struct.h"
#include "rclcpp_action/create_generic_client.hpp"
#include "rclcpp_action/generic_client.hpp"
namespace rclcpp_action
{
namespace
{
// Local implementation of get_action_typesupport_handle for jazzy compatibility.
// This function is available in rclcpp on rolling but not on jazzy.
const rosidl_action_type_support_t * get_action_typesupport_handle(
const std::string & type,
const std::string & typesupport_identifier,
rcpputils::SharedLibrary & library)
{
std::string package_name;
std::string middle_module;
std::string type_name;
std::tie(package_name, middle_module, type_name) = rclcpp::extract_type_identifier(type);
if (middle_module.empty()) {
middle_module = "action";
}
auto mk_error = [&package_name, &type_name](auto reason) {
std::stringstream rcutils_dynamic_loading_error;
rcutils_dynamic_loading_error <<
"Something went wrong loading the typesupport library for action type " <<
package_name << "/" << type_name << ". " << reason;
return rcutils_dynamic_loading_error.str();
};
try {
std::string symbol_name = typesupport_identifier + "__get_action_type_support_handle__" +
package_name + "__" + middle_module + "__" + type_name;
const rosidl_action_type_support_t * (* get_ts)() = nullptr;
// This will throw runtime_error if the symbol was not found.
get_ts = reinterpret_cast<decltype(get_ts)>(library.get_symbol(symbol_name));
return get_ts();
} catch (std::runtime_error &) {
throw std::runtime_error{mk_error("Library could not be found.")};
}
}
} // namespace
typename GenericClient::SharedPtr
create_generic_client(
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base_interface,
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph_interface,
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging_interface,
rclcpp::node_interfaces::NodeWaitablesInterface::SharedPtr node_waitables_interface,
const std::string & name,
const std::string & type,
rclcpp::CallbackGroup::SharedPtr group,
const rcl_action_client_options_t & options)
{
std::weak_ptr<rclcpp::node_interfaces::NodeWaitablesInterface> weak_node =
node_waitables_interface;
std::weak_ptr<rclcpp::CallbackGroup> weak_group = group;
bool group_is_null = (nullptr == group.get());
auto deleter = [weak_node, weak_group, group_is_null](GenericClient * ptr)
{
if (nullptr == ptr) {
return;
}
auto shared_node = weak_node.lock();
if (shared_node) {
// API expects a shared pointer, give it one with a deleter that does nothing.
std::shared_ptr<GenericClient> fake_shared_ptr(ptr, [](GenericClient *) {});
if (group_is_null) {
// Was added to default group
shared_node->remove_waitable(fake_shared_ptr, nullptr);
} else {
// Was added to a specific group
auto shared_group = weak_group.lock();
if (shared_group) {
shared_node->remove_waitable(fake_shared_ptr, shared_group);
}
}
}
delete ptr;
};
auto typesupport_lib = rclcpp::get_typesupport_library(type, "rosidl_typesupport_cpp");
auto action_typesupport_handle = get_action_typesupport_handle(
type, "rosidl_typesupport_cpp", *typesupport_lib);
std::shared_ptr<GenericClient> action_client(
new GenericClient(
node_base_interface,
node_graph_interface,
node_logging_interface,
name,
typesupport_lib,
action_typesupport_handle,
options),
deleter);
node_waitables_interface->add_waitable(action_client, group);
return action_client;
}
} // namespace rclcpp_action

View File

@@ -0,0 +1,482 @@
// Copyright 2025 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <future>
#include <memory>
#include <mutex>
#include <string>
#include "rclcpp_action/generic_client.hpp"
namespace rclcpp_action
{
GenericClient::GenericClient(
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base,
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph,
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging,
const std::string & action_name,
std::shared_ptr<rcpputils::SharedLibrary> typesupport_lib,
const rosidl_action_type_support_t * action_typesupport_handle,
const rcl_action_client_options_t & client_options)
: ClientBase(
node_base, node_graph, node_logging, action_name,
action_typesupport_handle,
client_options),
ts_lib_(std::move(typesupport_lib))
{
auto goal_service_request_type_support_intro = get_message_typesupport_handle(
action_typesupport_handle->goal_service_type_support->request_typesupport,
rosidl_typesupport_introspection_cpp::typesupport_identifier);
goal_service_request_type_members_ =
static_cast<IntrospectionMessageMembersPtr>(goal_service_request_type_support_intro->data);
auto goal_service_response_type_support_intro = get_message_typesupport_handle(
action_typesupport_handle->goal_service_type_support->response_typesupport,
rosidl_typesupport_introspection_cpp::typesupport_identifier);
goal_service_response_type_members_ =
static_cast<IntrospectionMessageMembersPtr>(goal_service_response_type_support_intro->data);
auto result_service_request_type_support_intro = get_message_typesupport_handle(
action_typesupport_handle->result_service_type_support->request_typesupport,
rosidl_typesupport_introspection_cpp::typesupport_identifier);
result_service_request_type_members_ =
static_cast<IntrospectionMessageMembersPtr>(result_service_request_type_support_intro->data);
auto result_service_response_type_support_intro = get_message_typesupport_handle(
action_typesupport_handle->result_service_type_support->response_typesupport,
rosidl_typesupport_introspection_cpp::typesupport_identifier);
result_service_response_type_members_ =
static_cast<IntrospectionMessageMembersPtr>(result_service_response_type_support_intro->data);
auto cancel_service_reponse_type_support_intro = get_message_typesupport_handle(
action_typesupport_handle->cancel_service_type_support->response_typesupport,
rosidl_typesupport_introspection_cpp::typesupport_identifier);
cancel_service_response_type_members_ =
static_cast<IntrospectionMessageMembersPtr>(cancel_service_reponse_type_support_intro->data);
auto feedback_type_support_intro = get_message_typesupport_handle(
action_typesupport_handle->feedback_message_type_support,
rosidl_typesupport_introspection_cpp::typesupport_identifier);
feedback_type_members_ =
static_cast<IntrospectionMessageMembersPtr>(feedback_type_support_intro->data);
}
std::shared_future<typename GenericClientGoalHandle::SharedPtr>
GenericClient::async_send_goal(
const Goal goal, size_t goal_size, const SendGoalOptions & options)
{
// uuid + goal
size_t goal_request_size = sizeof(rclcpp_action::GoalUUID) + goal_size;
std::shared_ptr<uint8_t> goal_request_msg(
new uint8_t[goal_request_size], std::default_delete<uint8_t[]>());
rclcpp_action::GoalUUID * uuid =
reinterpret_cast<rclcpp_action::GoalUUID *>(goal_request_msg.get());
*uuid = this->generate_goal_id();
std::memcpy(
goal_request_msg.get() + sizeof(rclcpp_action::GoalUUID),
goal, goal_size);
return async_send_goal(static_cast<void *>(goal_request_msg.get()), options);
}
std::shared_future<typename GenericClientGoalHandle::SharedPtr>
GenericClient::async_send_goal(
const GoalRequest goal_request,
const SendGoalOptions & options)
{
// Put promise in the heap to move it around.
auto promise = std::make_shared<std::promise<typename GenericClientGoalHandle::SharedPtr>>();
std::shared_future<typename GenericClientGoalHandle::SharedPtr> future(promise->get_future());
auto do_nothing = [](void *ptr) {
(void)ptr;
};
std::shared_ptr<void> goal_request_msg(const_cast<void *>(goal_request), do_nothing);
GoalUUID uuid = *reinterpret_cast<GoalUUID *>(goal_request);
this->send_goal_request(
goal_request_msg,
[this, uuid, options, promise](std::shared_ptr<void> response) mutable
{
size_t response_accepted_offset = 0;
size_t response_timestamp_offset = 0;
for(uint32_t i = 0; i < goal_service_response_type_members_->member_count_; i++) {
if (!std::strcmp(goal_service_response_type_members_->members_[i].name_, "accepted")) {
response_accepted_offset = goal_service_response_type_members_->members_[i].offset_;
continue;
}
if (!std::strcmp(goal_service_response_type_members_->members_[i].name_, "stamp")) {
response_timestamp_offset = goal_service_response_type_members_->members_[i].offset_;
continue;
}
}
bool response_accepted = false;
std::memcpy(
static_cast<bool *>(&response_accepted),
static_cast<bool *>(static_cast<bool *>(response.get()) + response_accepted_offset),
sizeof(bool));
if (!response_accepted) {
promise->set_value(nullptr);
if (options.goal_response_callback) {
options.goal_response_callback(nullptr);
}
return;
}
action_msgs::msg::GoalInfo goal_info;
goal_info.goal_id.uuid = uuid;
std::memcpy(
static_cast<void *>(&goal_info.stamp),
static_cast<void *>(static_cast<char *>(response.get()) + response_timestamp_offset),
sizeof(goal_info.stamp));
// Do not use std::make_shared as friendship cannot be forwarded.
std::shared_ptr<GenericClientGoalHandle> goal_handle(
new GenericClientGoalHandle(
goal_info, options.feedback_callback, options.result_callback));
{
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
goal_handles_[goal_handle->get_goal_id()] = goal_handle;
}
promise->set_value(goal_handle);
if (options.goal_response_callback) {
options.goal_response_callback(goal_handle);
}
if (options.result_callback) {
this->make_result_aware(goal_handle);
}
});
// TODO(jacobperron): Encapsulate into it's own function and
// consider exposing an option to disable this cleanup
// To prevent the list from growing out of control, forget about any goals
// with no more user references
{
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
auto goal_handle_it = goal_handles_.begin();
while (goal_handle_it != goal_handles_.end()) {
if (!goal_handle_it->second.lock()) {
RCLCPP_DEBUG(
this->get_logger(),
"Dropping weak reference to goal handle during send_goal()");
goal_handle_it = goal_handles_.erase(goal_handle_it);
} else {
++goal_handle_it;
}
}
}
return future;
}
std::shared_future<GenericClient::WrappedResult>
GenericClient::async_get_result(
typename GenericClientGoalHandle::SharedPtr goal_handle,
ResultCallback result_callback)
{
std::lock_guard<std::recursive_mutex> lock(goal_handles_mutex_);
if (goal_handles_.count(goal_handle->get_goal_id()) == 0) {
throw exceptions::UnknownGoalHandleError();
}
if (goal_handle->is_invalidated()) {
// This case can happen if there was a failure to send the result request
// during the goal response callback
throw goal_handle->invalidate_exception_;
}
if (result_callback) {
// This will override any previously registered callback
goal_handle->set_result_callback(result_callback);
}
this->make_result_aware(goal_handle);
return goal_handle->async_get_result();
}
std::shared_future<typename GenericClient::CancelResponse::SharedPtr>
GenericClient::async_cancel_goal(
typename GenericClientGoalHandle::SharedPtr goal_handle,
CancelCallback cancel_callback)
{
std::lock_guard<std::recursive_mutex> lock(goal_handles_mutex_);
if (goal_handles_.count(goal_handle->get_goal_id()) == 0) {
throw exceptions::UnknownGoalHandleError();
}
auto cancel_request = std::make_shared<CancelRequest>();
cancel_request->goal_info.goal_id.uuid = goal_handle->get_goal_id();
return async_cancel(cancel_request, cancel_callback);
}
std::shared_future<typename GenericClient::CancelResponse::SharedPtr>
GenericClient::async_cancel_all_goals(CancelCallback cancel_callback)
{
auto cancel_request = std::make_shared<CancelRequest>();
std::fill(
cancel_request->goal_info.goal_id.uuid.begin(),
cancel_request->goal_info.goal_id.uuid.end(), 0u);
return async_cancel(cancel_request, cancel_callback);
}
void
GenericClient::stop_callbacks(typename GenericClientGoalHandle::SharedPtr goal_handle)
{
goal_handle->set_feedback_callback(typename GenericClientGoalHandle::FeedbackCallback());
goal_handle->set_result_callback(typename GenericClientGoalHandle::ResultCallback());
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
const GoalUUID & goal_id = goal_handle->get_goal_id();
auto it = goal_handles_.find(goal_id);
if (goal_handles_.end() == it) {
// someone else already deleted the entry
// e.g. the result callback
RCLCPP_DEBUG(
this->get_logger(),
"Given goal is unknown. Ignoring...");
return;
}
goal_handles_.erase(it);
}
void GenericClient::stop_callbacks(const GoalUUID & goal_id)
{
typename GenericClientGoalHandle::SharedPtr goal_handle;
{
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
auto it = goal_handles_.find(goal_id);
if (goal_handles_.end() == it) {
// someone else already deleted the entry
// e.g. the result callback
RCLCPP_DEBUG(
this->get_logger(),
"Given goal is unknown. Ignoring...");
return;
}
goal_handle = it->second.lock();
}
if (goal_handle) {
stop_callbacks(goal_handle);
}
}
std::shared_future<typename GenericClient::CancelResponse::SharedPtr>
GenericClient::async_cancel_goals_before(
const rclcpp::Time & stamp,
CancelCallback cancel_callback)
{
auto cancel_request = std::make_shared<CancelRequest>();
// std::fill(cancel_request->goal_info.goal_id.uuid, 0u);
std::fill(
cancel_request->goal_info.goal_id.uuid.begin(),
cancel_request->goal_info.goal_id.uuid.end(), 0u);
cancel_request->goal_info.stamp = stamp;
return async_cancel(cancel_request, cancel_callback);
}
GenericClient::~GenericClient()
{
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
auto it = goal_handles_.begin();
while (it != goal_handles_.end()) {
typename GenericClientGoalHandle::SharedPtr goal_handle = it->second.lock();
if (goal_handle) {
goal_handle->invalidate(exceptions::UnawareGoalHandleError());
}
it = goal_handles_.erase(it);
}
}
std::shared_ptr<void>
GenericClient::create_message(IntrospectionMessageMembersPtr message_members) const
{
void * message = new uint8_t[message_members->size_of_];
message_members->init_function(message, rosidl_runtime_cpp::MessageInitialization::ZERO);
return std::shared_ptr<void>(
message,
[message_members](void * p)
{
message_members->fini_function(p);
delete[] reinterpret_cast<uint8_t *>(p);
});
}
void
GenericClient::handle_feedback_message(std::shared_ptr<void> message)
{
size_t goal_id_offset = 0;
size_t feedback_offset = 0;
for (uint32_t i = 0; i < feedback_type_members_->member_count_; i++) {
if (!strcmp(feedback_type_members_->members_[i].name_, "goal_id")) {
goal_id_offset = feedback_type_members_->members_[i].offset_;
continue;
}
if (!strcmp(feedback_type_members_->members_[i].name_, "feedback")) {
feedback_offset = feedback_type_members_->members_[i].offset_;
continue;
}
}
auto * uuid = reinterpret_cast<unique_identifier_msgs::msg::UUID *>(
static_cast<char *>(message.get()) + goal_id_offset);
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
const GoalUUID & goal_id = uuid->uuid;
if (goal_handles_.count(goal_id) == 0) {
RCLCPP_DEBUG(
this->get_logger(),
"Received feedback for unknown goal. Ignoring...");
return;
}
typename GenericClientGoalHandle::SharedPtr goal_handle = goal_handles_[goal_id].lock();
// Forget about the goal if there are no more user references
if (!goal_handle) {
RCLCPP_DEBUG(
this->get_logger(),
"Dropping weak reference to goal handle during feedback callback");
goal_handles_.erase(goal_id);
return;
}
void * feedback = static_cast<void *>(reinterpret_cast<char *>(message.get()) + feedback_offset);
goal_handle->call_feedback_callback(goal_handle, feedback);
}
void
GenericClient::handle_status_message(std::shared_ptr<void> message)
{
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
using GoalStatusMessage = action_msgs::msg::GoalStatusArray;
auto status_message = std::static_pointer_cast<GoalStatusMessage>(message);
for (const GoalStatus & status : status_message->status_list) {
const GoalUUID & goal_id = status.goal_info.goal_id.uuid;
if (goal_handles_.count(goal_id) == 0) {
RCLCPP_DEBUG(
this->get_logger(),
"Received status for unknown goal. Ignoring...");
continue;
}
typename GenericClientGoalHandle::SharedPtr goal_handle = goal_handles_[goal_id].lock();
// Forget about the goal if there are no more user references
if (!goal_handle) {
RCLCPP_DEBUG(
this->get_logger(),
"Dropping weak reference to goal handle during status callback");
goal_handles_.erase(goal_id);
continue;
}
goal_handle->set_status(status.status);
}
}
void
GenericClient::make_result_aware(typename GenericClientGoalHandle::SharedPtr goal_handle)
{
// Avoid making more than one request
if (goal_handle->set_result_awareness(true)) {
return;
}
auto goal_result_request = create_result_request();
size_t goal_id_offset = 0;
for (uint32_t i = 0; i < goal_service_request_type_members_->member_count_; i++) {
if (!strcmp(goal_service_request_type_members_->members_[i].name_, "goal_id")) {
goal_id_offset = goal_service_request_type_members_->members_[i].offset_;
break;
}
}
// Set uuid for result request message
auto * uuid = reinterpret_cast<unique_identifier_msgs::msg::UUID *>(
static_cast<char *>(goal_result_request.get()) + goal_id_offset);
uuid->uuid = goal_handle->get_goal_id();
try {
this->send_result_request(
std::static_pointer_cast<void>(goal_result_request),
[goal_handle, this](std::shared_ptr<void> response) mutable
{
// Wrap the response in a struct with the fields a user cares about
WrappedResult wrapped_result;
// Get the offsets for the status and result fields
size_t status_offset;
size_t result_offset;
for (uint32_t i = 0; i < result_service_response_type_members_->member_count_; i++) {
if (!std::strcmp(result_service_response_type_members_->members_[i].name_, "result")) {
result_offset = result_service_response_type_members_->members_[i].offset_;
continue;
}
if (!std::strcmp(result_service_response_type_members_->members_[i].name_, "status")) {
status_offset = result_service_response_type_members_->members_[i].offset_;
continue;
}
}
// the result part will not be copied here since it is hard to quickly obtain the size of
// the result part. Instead, a pointer will be used to reference the result part.
wrapped_result.result = reinterpret_cast<char *>(response.get()) + result_offset;
std::memcpy(
static_cast<void *>(&wrapped_result.code),
static_cast<void *>(reinterpret_cast<char *>(response.get()) + status_offset),
sizeof(int8_t)); // ROS_TYPE_INT8
wrapped_result.goal_id = goal_handle->get_goal_id();
// Ensure that the original result response message is not released since result part
// is not copied.
wrapped_result.result_response = response;
goal_handle->set_result(wrapped_result);
std::lock_guard<std::recursive_mutex> lock(goal_handles_mutex_);
goal_handles_.erase(goal_handle->get_goal_id());
});
} catch (rclcpp::exceptions::RCLError & ex) {
// This will cause an exception when the user tries to access the result
goal_handle->invalidate(exceptions::UnawareGoalHandleError(ex.message));
}
}
std::shared_future<typename GenericClient::CancelResponse::SharedPtr>
GenericClient::async_cancel(
typename CancelRequest::SharedPtr cancel_request,
CancelCallback cancel_callback)
{
// Put promise in the heap to move it around.
auto promise = std::make_shared<std::promise<typename CancelResponse::SharedPtr>>();
std::shared_future<typename CancelResponse::SharedPtr> future(promise->get_future());
this->send_cancel_request(
std::static_pointer_cast<void>(cancel_request),
[cancel_callback, promise](std::shared_ptr<void> response) mutable
{
auto cancel_response = std::static_pointer_cast<CancelResponse>(response);
promise->set_value(cancel_response);
if (cancel_callback) {
cancel_callback(cancel_response);
}
});
return future;
}
} // namespace rclcpp_action

View File

@@ -0,0 +1,159 @@
// Copyright 2025 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <cstdint>
#include <future>
#include <mutex>
#include "rclcpp_action/generic_client_goal_handle.hpp"
#include "rclcpp/logging.hpp"
namespace rclcpp_action
{
GenericClientGoalHandle::GenericClientGoalHandle(
const GoalInfo & info, FeedbackCallback feedback_callback, ResultCallback result_callback)
: info_(info),
result_future_(result_promise_.get_future()),
feedback_callback_(feedback_callback),
result_callback_(result_callback)
{
}
GenericClientGoalHandle::~GenericClientGoalHandle()
{}
const GoalUUID &
GenericClientGoalHandle::get_goal_id() const
{
return info_.goal_id.uuid;
}
rclcpp::Time
GenericClientGoalHandle::get_goal_stamp() const
{
return info_.stamp;
}
std::shared_future<typename GenericClientGoalHandle::WrappedResult>
GenericClientGoalHandle::async_get_result()
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
if (!is_result_aware_) {
throw exceptions::UnawareGoalHandleError();
}
return result_future_;
}
void
GenericClientGoalHandle::set_result(const WrappedResult & wrapped_result)
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
status_ = static_cast<int8_t>(wrapped_result.code);
result_promise_.set_value(wrapped_result);
if (result_callback_) {
result_callback_(wrapped_result);
result_callback_ = ResultCallback();
}
}
void
GenericClientGoalHandle::set_feedback_callback(FeedbackCallback callback)
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
feedback_callback_ = callback;
}
void
GenericClientGoalHandle::set_result_callback(ResultCallback callback)
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
result_callback_ = callback;
}
int8_t
GenericClientGoalHandle::get_status()
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
return status_;
}
void
GenericClientGoalHandle::set_status(int8_t status)
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
status_ = status;
}
bool
GenericClientGoalHandle::is_feedback_aware()
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
return feedback_callback_ != nullptr;
}
bool
GenericClientGoalHandle::is_result_aware()
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
return is_result_aware_;
}
bool
GenericClientGoalHandle::set_result_awareness(bool awareness)
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
bool previous = is_result_aware_;
is_result_aware_ = awareness;
return previous;
}
void
GenericClientGoalHandle::invalidate(const exceptions::UnawareGoalHandleError & ex)
{
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
// Guard against multiple calls
if (is_invalidated()) {
return;
}
is_result_aware_ = false;
invalidate_exception_ = std::make_exception_ptr(ex);
status_ = GoalStatus::STATUS_UNKNOWN;
result_promise_.set_exception(invalidate_exception_);
}
bool
GenericClientGoalHandle::is_invalidated() const
{
return invalidate_exception_ != nullptr;
}
void
GenericClientGoalHandle::call_feedback_callback(
typename GenericClientGoalHandle::SharedPtr shared_this,
const void * feedback_message)
{
if (shared_this.get() != this) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp_action"), "Sent feedback to wrong goal handle.");
return;
}
std::lock_guard<std::recursive_mutex> guard(handle_mutex_);
if (nullptr == feedback_callback_) {
// Normal, some feedback messages may arrive after the goal result.
RCLCPP_DEBUG(rclcpp::get_logger("rclcpp_action"), "Received feedback but goal ignores it.");
return;
}
feedback_callback_(shared_this, feedback_message);
}
} // namespace rclcpp_action

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,9 @@
Changelog for package rclcpp_components
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28.1.17 (2026-02-09)
--------------------
28.1.16 (2026-01-21)
--------------------

View File

@@ -13,9 +13,20 @@
# limitations under the License.
# register node plugins
list(REMOVE_DUPLICATES _RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES)
foreach(resource_index ${_RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES})
# The internal data is stored in a project directory scoped properties to allow
# registering the components from nested scopes in CMake, where variables
# would not propagate out.
get_property(_rclcpp_components_package_resource_indices
DIRECTORY "${PROJECT_SOURCE_DIR}"
PROPERTY _RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES
)
list(REMOVE_DUPLICATES _rclcpp_components_package_resource_indices)
foreach(resource_index ${_rclcpp_components_package_resource_indices})
get_property(_rclcpp_components_nodes
DIRECTORY "${PROJECT_SOURCE_DIR}"
PROPERTY "_RCLCPP_COMPONENTS_${resource_index}__NODES"
)
ament_index_register_resource(
${resource_index} CONTENT "${_RCLCPP_COMPONENTS_${resource_index}__NODES}")
${resource_index} CONTENT "${_rclcpp_components_nodes}")
endforeach()

View File

@@ -59,15 +59,19 @@ macro(rclcpp_components_register_node target)
set(component ${ARGS_PLUGIN})
set(node ${ARGS_EXECUTABLE})
_rclcpp_components_register_package_hook()
set(_path "lib")
set(library_name "$<TARGET_FILE_NAME:${target}>")
if(WIN32)
set(_path "bin")
endif()
set(_RCLCPP_COMPONENTS_${resource_index}__NODES
"${_RCLCPP_COMPONENTS_${resource_index}__NODES}${component};${_path}/$<TARGET_FILE_NAME:${target}>\n")
list(APPEND _RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES ${resource_index})
set_property(
DIRECTORY "${PROJECT_SOURCE_DIR}"
APPEND_STRING PROPERTY _RCLCPP_COMPONENTS_${resource_index}__NODES
"${component};${_path}/$<TARGET_FILE_NAME:${target}>\n")
set_property(
DIRECTORY "${PROJECT_SOURCE_DIR}"
APPEND PROPERTY _RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES
${resource_index})
if(ARGS_NO_UNDEFINED_SYMBOLS AND WIN32)
message(WARNING "NO_UNDEFINED_SYMBOLS is enabled for target \"${target}\", but this is unsupported on windows.")

View File

@@ -47,7 +47,6 @@ macro(rclcpp_components_register_nodes target)
endif()
if(${ARGC} GREATER 0)
_rclcpp_components_register_package_hook()
set(_unique_names)
foreach(_arg ${ARGS_UNPARSED_ARGUMENTS})
if(_arg IN_LIST _unique_names)
@@ -63,9 +62,14 @@ macro(rclcpp_components_register_nodes target)
else()
set(_path "lib")
endif()
set(_RCLCPP_COMPONENTS_${resource_index}__NODES
"${_RCLCPP_COMPONENTS_${resource_index}__NODES}${_arg};${_path}/$<TARGET_FILE_NAME:${target}>\n")
list(APPEND _RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES ${resource_index})
set_property(
DIRECTORY "${PROJECT_SOURCE_DIR}"
APPEND_STRING PROPERTY _RCLCPP_COMPONENTS_${resource_index}__NODES
"${_arg};${_path}/$<TARGET_FILE_NAME:${target}>\n")
set_property(
DIRECTORY "${PROJECT_SOURCE_DIR}"
APPEND PROPERTY _RCLCPP_COMPONENTS_PACKAGE_RESOURCE_INDICES
${resource_index})
endforeach()
endif()
endmacro()

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>rclcpp_components</name>
<version>28.1.16</version>
<version>28.1.17</version>
<description>Package containing tools for dynamically loadable components</description>
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>

View File

@@ -25,6 +25,8 @@ macro(_rclcpp_components_register_package_hook)
endif()
endmacro()
_rclcpp_components_register_package_hook()
get_filename_component(@PROJECT_NAME@_SHARE_DIR "${@PROJECT_NAME@_DIR}" DIRECTORY)
set(@PROJECT_NAME@_NODE_TEMPLATE "${@PROJECT_NAME@_SHARE_DIR}/node_main.cpp.in")

View File

@@ -23,16 +23,16 @@ int main(int argc, char * argv[])
/// Component container with a multi-threaded executor.
rclcpp::init(argc, argv);
auto exec = std::make_shared<rclcpp::executors::MultiThreadedExecutor>();
auto node = std::make_shared<rclcpp_components::ComponentManager>();
rclcpp::executors::MultiThreadedExecutor::SharedPtr exec = nullptr;
const auto node = std::make_shared<rclcpp_components::ComponentManager>();
if (node->has_parameter("thread_num")) {
const auto thread_num = node->get_parameter("thread_num").as_int();
exec = std::make_shared<rclcpp::executors::MultiThreadedExecutor>(
rclcpp::ExecutorOptions{}, thread_num);
node->set_executor(exec);
} else {
node->set_executor(exec);
exec = std::make_shared<rclcpp::executors::MultiThreadedExecutor>();
}
node->set_executor(exec);
exec->add_node(node);
exec->spin();
}

View File

@@ -3,6 +3,9 @@ Changelog for package rclcpp_lifecycle
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28.1.17 (2026-02-09)
--------------------
28.1.16 (2026-01-21)
--------------------

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>rclcpp_lifecycle</name>
<version>28.1.16</version>
<version>28.1.17</version>
<description>Package containing a prototype for lifecycle implementation</description>
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>