diff --git a/rclcpp_lifecycle/CMakeLists.txt b/rclcpp_lifecycle/CMakeLists.txt index b625194817..cf738e334f 100644 --- a/rclcpp_lifecycle/CMakeLists.txt +++ b/rclcpp_lifecycle/CMakeLists.txt @@ -105,6 +105,11 @@ if(BUILD_TESTING) target_link_libraries(test_lifecycle_node_errors ${PROJECT_NAME} mimick rcl_lifecycle::rcl_lifecycle) endif() + ament_add_gtest(test_lifecycle_timer test/test_lifecycle_timer.cpp) + if(TARGET test_lifecycle_timer) + target_link_libraries(test_lifecycle_timer ${PROJECT_NAME} rcl_lifecycle::rcl_lifecycle rclcpp::rclcpp) + endif() + ament_add_gtest(test_lifecycle_publisher test/test_lifecycle_publisher.cpp) if(TARGET test_lifecycle_publisher) target_link_libraries(test_lifecycle_publisher ${PROJECT_NAME} rcl_lifecycle::rcl_lifecycle rclcpp::rclcpp test_msgs::test_msgs) diff --git a/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node.hpp b/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node.hpp index 43ff0e3fa4..9078712e59 100644 --- a/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node.hpp +++ b/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node.hpp @@ -88,6 +88,7 @@ #include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" #include "rclcpp_lifecycle/lifecycle_publisher.hpp" +#include "rclcpp_lifecycle/lifecycle_timer.hpp" #include "rclcpp_lifecycle/state.hpp" #include "rclcpp_lifecycle/transition.hpp" #include "rclcpp_lifecycle/visibility_control.h" @@ -254,6 +255,31 @@ class LifecycleNode : public node_interfaces::LifecycleNodeInterface, ) ); + /// Create a managed timer that uses the wall clock to drive the callback. + /** + * \param[in] period Time interval between triggers of the callback. + * \param[in] callback User-defined callback function. + * \param[in] group Callback group to execute this timer's callback in. + */ + template + typename LifecycleWallTimer::SharedPtr create_lifecycle_wall_timer( + std::chrono::duration period, CallbackT callback, + rclcpp::CallbackGroup::SharedPtr group = nullptr); + + /// Create a managed timer that uses the node clock to drive the callback. + /** + * \param[in] period Time interval between triggers of the callback. + * \param[in] callback User-defined callback function. + * \param[in] group Callback group to execute this timer's callback in. + */ + template + typename LifecycleGenericTimer::SharedPtr + create_lifecycle_timer( + std::chrono::duration period, + CallbackT callback, + rclcpp::CallbackGroup::SharedPtr group = nullptr); + /// Create a timer that uses the wall clock to drive the callback. /** * \param[in] period Time interval between triggers of the callback. @@ -261,6 +287,8 @@ class LifecycleNode : public node_interfaces::LifecycleNodeInterface, * \param[in] group Callback group to execute this timer's callback in. */ template + [[deprecated("Use create_lifecycle_wall_timer() instead. It returns a " + "managed timer instead of a regular one")]] typename rclcpp::WallTimer::SharedPtr create_wall_timer( std::chrono::duration period, @@ -274,6 +302,8 @@ class LifecycleNode : public node_interfaces::LifecycleNodeInterface, * \param[in] group Callback group to execute this timer's callback in. */ template + [[deprecated("Use create_lifecycle_timer() instead. It returns a managed " + "timer instead of a regular one")]] typename rclcpp::GenericTimer::SharedPtr create_timer( std::chrono::duration period, diff --git a/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node_impl.hpp b/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node_impl.hpp index c2de16e3df..d691270d9f 100644 --- a/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node_impl.hpp +++ b/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node_impl.hpp @@ -90,6 +90,37 @@ LifecycleNode::create_subscription( msg_mem_strat); } +template +typename LifecycleWallTimer::SharedPtr +LifecycleNode::create_lifecycle_wall_timer( + std::chrono::duration period, CallbackT callback, + rclcpp::CallbackGroup::SharedPtr group) +{ + auto timer = std::make_shared>( + period, std::move(callback), this->node_base_->get_context()); + + this->node_timers_->add_timer(timer, group); + this->add_managed_entity(timer); + + return timer; +} + +template +typename LifecycleGenericTimer::SharedPtr +LifecycleNode::create_lifecycle_timer( + std::chrono::duration period, CallbackT callback, + rclcpp::CallbackGroup::SharedPtr group) +{ + auto timer = std::make_shared>( + this->get_clock(), period, std::move(callback), + this->node_base_->get_context()); + + this->node_timers_->add_timer(timer, group); + this->add_managed_entity(timer); + + return timer; +} + template typename rclcpp::WallTimer::SharedPtr LifecycleNode::create_wall_timer( diff --git a/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_timer.hpp b/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_timer.hpp new file mode 100644 index 0000000000..4c2a970578 --- /dev/null +++ b/rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_timer.hpp @@ -0,0 +1,90 @@ +// Copyright 2026 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. + +#ifndef RCLCPP_LIFECYCLE__LIFECYCLE_TIMER_HPP_ +#define RCLCPP_LIFECYCLE__LIFECYCLE_TIMER_HPP_ + +#include +#include +#include + +#include "rclcpp/timer.hpp" + +#include "rclcpp_lifecycle/managed_entity.hpp" + +namespace rclcpp_lifecycle +{ + +template +class LifecycleGenericTimer : public SimpleManagedEntity, + public rclcpp::GenericTimer { +public: + RCLCPP_SMART_PTR_DEFINITIONS(LifecycleGenericTimer) + + /// Default constructor. + /** + * \param[in] clock The clock providing the current time. + * \param[in] period The interval at which the timer fires. + * \param[in] callback User-specified callback function. + * \param[in] context custom context to be used. + */ + explicit LifecycleGenericTimer( + rclcpp::Clock::SharedPtr clock, + std::chrono::nanoseconds period, + FunctorT && callback, + rclcpp::Context::SharedPtr context) + : rclcpp::GenericTimer(clock, period, std::move(callback), + context, false) {} + + void on_activate() override + { + SimpleManagedEntity::on_activate(); + rclcpp::GenericTimer::reset(); + } + + void on_deactivate() override + { + SimpleManagedEntity::on_deactivate(); + rclcpp::GenericTimer::cancel(); + } + +protected: + RCLCPP_DISABLE_COPY(LifecycleGenericTimer) +}; + +template +class LifecycleWallTimer : public LifecycleGenericTimer { +public: + RCLCPP_SMART_PTR_DEFINITIONS(LifecycleWallTimer) + + /// Default constructor. + /** + * \param period The interval at which the timer fires + * \param callback The callback function to execute every interval + * \param context node context + */ + LifecycleWallTimer( + std::chrono::nanoseconds period, FunctorT && callback, + rclcpp::Context::SharedPtr context) + : LifecycleGenericTimer( + std::make_shared(RCL_STEADY_TIME), period, + std::move(callback), context) {} + +protected: + RCLCPP_DISABLE_COPY(LifecycleWallTimer) +}; + +} // namespace rclcpp_lifecycle + +#endif // RCLCPP_LIFECYCLE__LIFECYCLE_TIMER_HPP_ diff --git a/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp b/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp index ec9596a1d6..9efffcf36e 100644 --- a/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp +++ b/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp @@ -60,13 +60,14 @@ class EmptyLifecycleNode : public rclcpp_lifecycle::LifecycleNode switch (timer_type) { case TimerType::WALL_TIMER: { - auto timer = create_wall_timer(std::chrono::seconds(1), []() {}); + auto timer = + create_lifecycle_wall_timer(std::chrono::seconds(1), []() {}); add_timer_handle(timer); break; } case TimerType::GENERIC_TIMER: { - auto timer = create_timer(std::chrono::seconds(1), []() {}); + auto timer = create_lifecycle_timer(std::chrono::seconds(1), []() {}); add_timer_handle(timer); break; } diff --git a/rclcpp_lifecycle/test/test_lifecycle_timer.cpp b/rclcpp_lifecycle/test/test_lifecycle_timer.cpp new file mode 100644 index 0000000000..b9c3dc52dc --- /dev/null +++ b/rclcpp_lifecycle/test/test_lifecycle_timer.cpp @@ -0,0 +1,135 @@ +// Copyright 2026 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 +#include + +#include "rclcpp/executors/single_threaded_executor.hpp" + +#include "lifecycle_msgs/msg/state.hpp" +#include "lifecycle_msgs/msg/transition.hpp" + +#include "rclcpp_lifecycle/lifecycle_node.hpp" + +using lifecycle_msgs::msg::State; +using lifecycle_msgs::msg::Transition; + +using namespace std::chrono_literals; + +enum class TimerType +{ + WALL_TIMER, + GENERIC_TIMER, +}; + +class TestLifecycleTimer : public ::testing::TestWithParam { +protected: + static void SetUpTestCase() {rclcpp::init(0, nullptr);} + static void TearDownTestCase() {rclcpp::shutdown();} +}; + +TEST_P(TestLifecycleTimer, timer_becomes_activated_and_deactivated_with_node) { + std::atomic_bool is_executed = false; + auto node = std::make_shared("node"); + + ASSERT_EQ(State::PRIMARY_STATE_UNCONFIGURED, node->get_current_state().id()); + + auto success = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface:: + CallbackReturn::SUCCESS; + auto error = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface:: + CallbackReturn::ERROR; + auto ret = error; + + node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_CONFIGURE), ret); + ASSERT_EQ(success, ret) << "node transition failed"; + ret = error; + + TimerType timer_type = GetParam(); + std::function callback = [&]() {is_executed = true;}; + rclcpp_lifecycle::LifecycleGenericTimer>::SharedPtr + timer; + switch (timer_type) { + case TimerType::WALL_TIMER: { + timer = node->create_lifecycle_wall_timer(100ms, callback); + break; + } + case TimerType::GENERIC_TIMER: { + timer = node->create_lifecycle_timer(100ms, callback); + break; + } + } + + auto exec = std::make_shared(); + exec->add_node(node->get_node_base_interface()); + + auto spinner = std::thread([&]() {exec->spin();}); + + auto cleanup = rcpputils::make_scope_exit([&]() { + exec->cancel(); + spinner.join(); + }); + + // Timer should not be executed until the node is activated + ASSERT_FALSE(timer->is_activated()) + << "managed timer is active while its node is not"; + std::this_thread::sleep_for(500ms); + ASSERT_FALSE(is_executed) + << "managed timer was executed while node was inactive"; + + node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_ACTIVATE), ret); + ASSERT_EQ(success, ret) << "node transition failed"; + ret = error; + + // Now, the timer should be activated and its callback should be executed + ASSERT_TRUE(timer->is_activated()) + << "managed timer should be activated with the node"; + std::this_thread::sleep_for(500ms); + ASSERT_TRUE(is_executed) + << "managed timer was not executed while the node was active"; + + node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_DEACTIVATE), ret); + ASSERT_EQ(success, ret) << "node transition failed"; + ret = error; + is_executed = false; + + // Timer should not be executed after deactivation + ASSERT_FALSE(timer->is_activated()) + << "managed timer is active while its node is not"; + std::this_thread::sleep_for(500ms); + ASSERT_FALSE(is_executed) + << "managed timer was executed while node was inactive"; + + node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_INACTIVE_SHUTDOWN), + ret); + ASSERT_EQ(success, ret) << "node transition failed"; +} + +INSTANTIATE_TEST_SUITE_P( + PerTimerType, TestLifecycleTimer, + ::testing::Values(TimerType::WALL_TIMER, TimerType::GENERIC_TIMER), + [](const ::testing::TestParamInfo & info) -> std::string { + switch (info.param) { + case TimerType::WALL_TIMER: + return std::string("wall_timer"); + case TimerType::GENERIC_TIMER: + return std::string("generic_timer"); + default: + break; + } + return std::string("unknown"); + });