Skip to content

Commit 2296fd8

Browse files
authored
Make sleep in ros2_control node optional (#3213)
1 parent e279cf2 commit 2296fd8

7 files changed

Lines changed: 371 additions & 41 deletions

File tree

controller_manager/CMakeLists.txt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ target_link_libraries(controller_manager PUBLIC
5454
${std_msgs_TARGETS}
5555
${controller_manager_msgs_TARGETS})
5656

57-
add_executable(ros2_control_node src/ros2_control_node.cpp)
57+
add_executable(ros2_control_node
58+
src/ros2_control_node.cpp
59+
src/sleeping_policies.cpp
60+
)
5861
target_link_libraries(ros2_control_node PRIVATE
5962
controller_manager
6063
)
@@ -278,6 +281,17 @@ if(BUILD_TESTING)
278281
ros2_control_test_assets::ros2_control_test_assets
279282
)
280283

284+
ament_add_gmock(test_sleeping_policies
285+
test/test_sleeping_policies.cpp
286+
src/sleeping_policies.cpp
287+
TIMEOUT 60
288+
APPEND_ENV AMENT_PREFIX_PATH=${ament_index_build_path}_$<CONFIG>
289+
)
290+
target_link_libraries(test_sleeping_policies
291+
controller_manager
292+
ros2_control_test_assets::ros2_control_test_assets
293+
)
294+
281295
find_package(ament_cmake_pytest REQUIRED)
282296
install(FILES test/test_ros2_control_node.yaml
283297
DESTINATION test)

controller_manager/doc/userdoc.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,18 @@ overruns.manage (optional; bool; default: true)
484484
If an overrun is detected, the controller manager will print a warning message to the console.
485485
When used with ``use_sim_time`` set to true, this parameter is ignored and the overrun handling is disabled.
486486

487+
hardware_synchronization.expect_blocking_read_write (optional; bool; default: false)
488+
If true, the controller manager will not sleep actively. Use this, when there is a hardware
489+
interface running that will block during its read or write operation until receiving new data
490+
from the hardware.
491+
492+
hardware_synchronization.minimum_cycle_time (optional; double; default: 0.0001)
493+
The minimum sleep time in seconds for the control node's real-time loop. This is used to
494+
prevent the control node from running too fast, which can cause high CPU
495+
usage if the hardware doesn't block in read / write. This is only used when
496+
``expect_blocking_read_write`` is set to true.
497+
If the cycle is shorter than this, it will sleep for this period and print a warning.
498+
487499
Concepts
488500
-----------
489501

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2026 ROS2-Control Development Team
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#pragma once
16+
17+
#include <memory>
18+
19+
#include <rclcpp/logging.hpp>
20+
21+
#include "controller_manager/controller_manager.hpp"
22+
23+
namespace controller_manager
24+
{
25+
struct ControlLoopTimingConfig
26+
{
27+
bool use_sim_time{false};
28+
bool manage_overruns{true};
29+
bool expect_blocking_read_write{false};
30+
double minimum_cycle_time{0.0001};
31+
};
32+
33+
struct ControlLoopState
34+
{
35+
rclcpp::Time previous_time;
36+
std::chrono::steady_clock::time_point next_iteration_time;
37+
std::chrono::nanoseconds period{0};
38+
rclcpp::Time cycle_end_time; //< The time when work was done in the current cycle.
39+
};
40+
} // namespace controller_manager
41+
42+
bool sleep_for_sim_time(
43+
std::shared_ptr<controller_manager::ControllerManager> cm,
44+
controller_manager::ControlLoopState & state);
45+
46+
void sleep_for_blocking_read_write(
47+
std::shared_ptr<controller_manager::ControllerManager> cm,
48+
const controller_manager::ControlLoopTimingConfig & config,
49+
controller_manager::ControlLoopState & state);
50+
51+
void sleep_for_periodic_cycle(
52+
std::shared_ptr<controller_manager::ControllerManager> cm,
53+
const controller_manager::ControlLoopTimingConfig & config,
54+
controller_manager::ControlLoopState & state);

controller_manager/package.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<exec_depend>sensor_msgs</exec_depend>
4141

4242
<test_depend>ament_cmake_gmock</test_depend>
43+
<test_depend>diagnostic_msgs</test_depend>
4344
<test_depend>ament_cmake_pytest</test_depend>
4445
<test_depend>example_interfaces</test_depend>
4546
<test_depend>hardware_interface_testing</test_depend>

controller_manager/src/ros2_control_node.cpp

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <thread>
2020

2121
#include "controller_manager/controller_manager.hpp"
22+
#include "controller_manager/sleeping_policies.hpp"
2223
#include "rclcpp/executors.hpp"
2324
#include "realtime_tools/realtime_helpers.hpp"
2425

@@ -79,8 +80,20 @@ int main(int argc, char ** argv)
7980
cm->get_logger(), "Spawning %s RT thread with scheduler priority: %d", cm->get_name(),
8081
thread_priority);
8182

83+
const controller_manager::ControlLoopTimingConfig timing_config{
84+
.use_sim_time = use_sim_time,
85+
.manage_overruns = manage_overruns,
86+
.expect_blocking_read_write =
87+
cm->get_parameter_or<bool>("hardware_synchronization.expect_blocking_read_write", false),
88+
.minimum_cycle_time =
89+
cm->get_parameter_or<double>("hardware_synchronization.minimum_cycle_time", 0.0001),
90+
};
91+
RCLCPP_INFO_EXPRESSION(
92+
cm->get_logger(), timing_config.expect_blocking_read_write,
93+
"Synchronizing control loop with hardware.");
94+
8295
std::thread cm_thread(
83-
[cm, thread_priority, use_sim_time, manage_overruns]()
96+
[cm, thread_priority, timing_config]()
8497
{
8598
rclcpp::Parameter cpu_affinity_param;
8699
if (cm->get_parameter("cpu_affinity", cpu_affinity_param))
@@ -124,63 +137,39 @@ int main(int argc, char ** argv)
124137
cm->get_clock()->wait_until_started();
125138
cm->get_clock()->sleep_for(rclcpp::Duration::from_seconds(1.0 / cm->get_update_rate()));
126139

127-
// for calculating sleep time
128-
auto const period = std::chrono::nanoseconds(1'000'000'000 / cm->get_update_rate());
129-
130-
// for calculating the measured period of the loop
131-
rclcpp::Time previous_time = cm->get_trigger_clock()->now();
132-
std::this_thread::sleep_for(period);
133-
134-
std::chrono::steady_clock::time_point next_iteration_time{std::chrono::steady_clock::now()};
135-
140+
controller_manager::ControlLoopState state;
141+
state.period = std::chrono::nanoseconds(1'000'000'000 / cm->get_update_rate());
142+
state.previous_time = cm->get_trigger_clock()->now();
143+
std::this_thread::sleep_for(state.period);
144+
state.next_iteration_time = std::chrono::steady_clock::now();
136145
while (rclcpp::ok())
137146
{
138147
// calculate measured period
139148
auto const current_time = cm->get_trigger_clock()->now();
140-
auto const measured_period = current_time - previous_time;
141-
previous_time = current_time;
149+
auto const measured_period = current_time - state.previous_time;
150+
state.previous_time = current_time;
142151

143152
// execute update loop
144153
cm->read(cm->get_trigger_clock()->now(), measured_period);
145154
cm->update(cm->get_trigger_clock()->now(), measured_period);
146155
cm->write(cm->get_trigger_clock()->now(), measured_period);
156+
state.cycle_end_time = cm->get_trigger_clock()->now();
147157

148158
// wait until we hit the end of the period
149-
if (use_sim_time)
159+
if (timing_config.use_sim_time)
150160
{
151-
try
152-
{
153-
cm->get_clock()->sleep_until(current_time + period);
154-
}
155-
catch (const std::runtime_error & e)
161+
if (!sleep_for_sim_time(cm, state))
156162
{
157-
RCLCPP_ERROR(
158-
cm->get_logger(),
159-
"sleep_until failed with error: %s. Exiting control loop and aborting....", e.what());
160163
break;
161164
}
162165
}
166+
else if (timing_config.expect_blocking_read_write)
167+
{
168+
sleep_for_blocking_read_write(cm, timing_config, state);
169+
}
163170
else
164171
{
165-
next_iteration_time += period;
166-
const auto time_now = std::chrono::steady_clock::now();
167-
if (manage_overruns && next_iteration_time < time_now)
168-
{
169-
const double time_diff =
170-
static_cast<double>(
171-
std::chrono::duration_cast<std::chrono::nanoseconds>(time_now - next_iteration_time)
172-
.count()) /
173-
1.e6;
174-
const double cm_period = 1.e3 / static_cast<double>(cm->get_update_rate());
175-
const int overrun_count = static_cast<int>(std::ceil(time_diff / cm_period));
176-
RCLCPP_WARN_THROTTLE(
177-
cm->get_logger(), *cm->get_clock(), 1000,
178-
"Overrun detected! The controller manager missed its desired rate of %d Hz. The loop "
179-
"took %f ms (missed cycles : %d).",
180-
cm->get_update_rate(), time_diff + cm_period, overrun_count + 1);
181-
next_iteration_time += (overrun_count * period);
182-
}
183-
std::this_thread::sleep_until(next_iteration_time);
172+
sleep_for_periodic_cycle(cm, timing_config, state);
184173
}
185174
}
186175
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2026 ROS2-Control Development Team
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "controller_manager/sleeping_policies.hpp"
16+
17+
bool sleep_for_sim_time(
18+
std::shared_ptr<controller_manager::ControllerManager> cm,
19+
controller_manager::ControlLoopState & state)
20+
{
21+
// Implement sleep logic for simulated time
22+
try
23+
{
24+
cm->get_clock()->sleep_until(state.previous_time + state.period);
25+
}
26+
catch (const std::runtime_error & e)
27+
{
28+
RCLCPP_ERROR(
29+
cm->get_logger(), "sleep_until failed with error: %s. Exiting control loop and aborting....",
30+
e.what());
31+
return false;
32+
}
33+
return true; // Placeholder return value
34+
}
35+
36+
void sleep_for_blocking_read_write(
37+
std::shared_ptr<controller_manager::ControllerManager> cm,
38+
const controller_manager::ControlLoopTimingConfig & config,
39+
controller_manager::ControlLoopState & state)
40+
{
41+
// Implement sleep logic for blocking read/write
42+
if (
43+
(state.cycle_end_time - state.previous_time).nanoseconds() <
44+
static_cast<rcl_duration_value_t>(config.minimum_cycle_time * 1e9))
45+
{
46+
RCLCPP_WARN_THROTTLE(
47+
cm->get_logger(), *cm->get_clock(), 1000,
48+
"Last control cycle was shorter than the minimum cycle time while blocking read or "
49+
"write is configured. Is the hardware interface not blocking correctly? Note: This "
50+
"might happen when the hardware component that should block is not active.");
51+
std::this_thread::sleep_for(
52+
std::chrono::microseconds(static_cast<int>(config.minimum_cycle_time * 1e6)));
53+
}
54+
}
55+
56+
void sleep_for_periodic_cycle(
57+
std::shared_ptr<controller_manager::ControllerManager> cm,
58+
const controller_manager::ControlLoopTimingConfig & config,
59+
controller_manager::ControlLoopState & state)
60+
{
61+
state.next_iteration_time += state.period;
62+
const auto time_now = std::chrono::steady_clock::now();
63+
if (config.manage_overruns && state.next_iteration_time < time_now)
64+
{
65+
const double time_diff =
66+
static_cast<double>(
67+
std::chrono::duration_cast<std::chrono::nanoseconds>(time_now - state.next_iteration_time)
68+
.count()) /
69+
1.e6;
70+
const double cm_period = 1.e3 / static_cast<double>(cm->get_update_rate());
71+
const int overrun_count = static_cast<int>(std::ceil(time_diff / cm_period));
72+
RCLCPP_WARN_THROTTLE(
73+
cm->get_logger(), *cm->get_clock(), 1000,
74+
"Overrun detected! The controller manager missed its desired rate of %d Hz. The loop "
75+
"took %f ms (missed cycles : %d).",
76+
cm->get_update_rate(), time_diff + cm_period, overrun_count + 1);
77+
state.next_iteration_time += (overrun_count * state.period);
78+
}
79+
std::this_thread::sleep_until(state.next_iteration_time);
80+
}

0 commit comments

Comments
 (0)