Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daffa1a56c | ||
|
|
8dd1ec2669 | ||
|
|
dc4a1dbbca | ||
|
|
6f96da6f99 | ||
|
|
f7bcfafcbb | ||
|
|
ea3229d463 | ||
|
|
8564e013a4 | ||
|
|
23e9b257ea | ||
|
|
4259de8e17 | ||
|
|
eaa1db2d56 | ||
|
|
d6f70317c1 |
@@ -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>`_)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() +
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>`_)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
333
rclcpp_action/include/rclcpp_action/generic_client.hpp
Normal file
333
rclcpp_action/include/rclcpp_action/generic_client.hpp
Normal 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_
|
||||
@@ -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_
|
||||
@@ -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>
|
||||
|
||||
129
rclcpp_action/src/create_generic_client.cpp
Normal file
129
rclcpp_action/src/create_generic_client.cpp
Normal 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
|
||||
482
rclcpp_action/src/generic_client.cpp
Normal file
482
rclcpp_action/src/generic_client.cpp
Normal 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
|
||||
159
rclcpp_action/src/generic_client_goal_handle.cpp
Normal file
159
rclcpp_action/src/generic_client_goal_handle.cpp
Normal 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
|
||||
1169
rclcpp_action/test/test_generic_client.cpp
Normal file
1169
rclcpp_action/test/test_generic_client.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,9 @@
|
||||
Changelog for package rclcpp_components
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
28.1.17 (2026-02-09)
|
||||
--------------------
|
||||
|
||||
28.1.16 (2026-01-21)
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ Changelog for package rclcpp_lifecycle
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
28.1.17 (2026-02-09)
|
||||
--------------------
|
||||
|
||||
28.1.16 (2026-01-21)
|
||||
--------------------
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user