Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions rclcpp_lifecycle/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -254,13 +255,40 @@ 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 DurationRepT, typename DurationT, typename CallbackT>
typename LifecycleWallTimer<CallbackT>::SharedPtr create_lifecycle_wall_timer(
std::chrono::duration<DurationRepT, DurationT> 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 DurationRepT = int64_t, typename DurationT = std::milli,
typename CallbackT>
typename LifecycleGenericTimer<CallbackT>::SharedPtr
create_lifecycle_timer(
std::chrono::duration<DurationRepT, DurationT> 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.
* \param[in] callback User-defined callback function.
* \param[in] group Callback group to execute this timer's callback in.
*/
template<typename DurationRepT = int64_t, typename DurationT = std::milli, typename CallbackT>
[[deprecated("Use create_lifecycle_wall_timer() instead. It returns a "
"managed timer instead of a regular one")]]
typename rclcpp::WallTimer<CallbackT>::SharedPtr
create_wall_timer(
std::chrono::duration<DurationRepT, DurationT> period,
Expand All @@ -274,6 +302,8 @@ class LifecycleNode : public node_interfaces::LifecycleNodeInterface,
* \param[in] group Callback group to execute this timer's callback in.
*/
template<typename DurationRepT = int64_t, typename DurationT = std::milli, typename CallbackT>
[[deprecated("Use create_lifecycle_timer() instead. It returns a managed "
"timer instead of a regular one")]]
typename rclcpp::GenericTimer<CallbackT>::SharedPtr
create_timer(
std::chrono::duration<DurationRepT, DurationT> period,
Expand Down
31 changes: 31 additions & 0 deletions rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_node_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,37 @@ LifecycleNode::create_subscription(
msg_mem_strat);
}

template<typename DurationRepT, typename DurationT, typename CallbackT>
typename LifecycleWallTimer<CallbackT>::SharedPtr
LifecycleNode::create_lifecycle_wall_timer(
std::chrono::duration<DurationRepT, DurationT> period, CallbackT callback,
rclcpp::CallbackGroup::SharedPtr group)
{
auto timer = std::make_shared<LifecycleWallTimer<CallbackT>>(
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 DurationRepT, typename DurationT, typename CallbackT>
typename LifecycleGenericTimer<CallbackT>::SharedPtr
LifecycleNode::create_lifecycle_timer(
std::chrono::duration<DurationRepT, DurationT> period, CallbackT callback,
rclcpp::CallbackGroup::SharedPtr group)
{
auto timer = std::make_shared<LifecycleGenericTimer<CallbackT>>(
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 DurationRepT, typename DurationT, typename CallbackT>
typename rclcpp::WallTimer<CallbackT>::SharedPtr
LifecycleNode::create_wall_timer(
Expand Down
86 changes: 86 additions & 0 deletions rclcpp_lifecycle/include/rclcpp_lifecycle/lifecycle_timer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 "rclcpp/timer.hpp"

#include "rclcpp_lifecycle/managed_entity.hpp"

namespace rclcpp_lifecycle
{

template<typename FunctorT>
class LifecycleGenericTimer : public SimpleManagedEntity,
public rclcpp::GenericTimer<FunctorT> {
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<FunctorT>(clock, period, std::move(callback),
context, false) {}

void on_activate() override
{
SimpleManagedEntity::on_activate();
rclcpp::GenericTimer<FunctorT>::reset();
}

void on_deactivate() override
{
SimpleManagedEntity::on_deactivate();
rclcpp::GenericTimer<FunctorT>::cancel();
}

protected:
RCLCPP_DISABLE_COPY(LifecycleGenericTimer)
};

template<typename FunctorT>
class LifecycleWallTimer : public LifecycleGenericTimer<FunctorT> {
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<FunctorT>(
std::make_shared<rclcpp::Clock>(RCL_STEADY_TIME), period,
std::move(callback), context) {}

protected:
RCLCPP_DISABLE_COPY(LifecycleWallTimer)
};

} // namespace rclcpp_lifecycle

#endif
134 changes: 134 additions & 0 deletions rclcpp_lifecycle/test/test_lifecycle_timer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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 <chrono>
#include <gtest/gtest.h>

#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<TimerType> {
protected:
static void SetUpTestCase() {rclcpp::init(0, nullptr);}
static void TearDownTestCase() {rclcpp::shutdown();}
};

TEST_P(TestLifecycleTimer, timer_becomes_activated_and_deactivated_with_node) {
bool is_executed = false;
auto node = std::make_shared<rclcpp_lifecycle::LifecycleNode>("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<void()> callback = [&]() {is_executed = true;};
rclcpp_lifecycle::LifecycleGenericTimer<std::function<void()>>::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<rclcpp::executors::SingleThreadedExecutor>();
exec->add_node(node->get_node_base_interface());

auto spinner = std::thread([&]() {exec->spin();});

// 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";
ret = error;

exec->cancel();
spinner.join();
}

INSTANTIATE_TEST_SUITE_P(
PerTimerType, TestLifecycleTimer,
::testing::Values(TimerType::WALL_TIMER, TimerType::GENERIC_TIMER),
[](const ::testing::TestParamInfo<TimerType> & 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");
});
Loading