Skip to content

Commit b53bf25

Browse files
Merge branch 'master' into feat/jtc-jerk-limits
2 parents 98f03d1 + 2296fd8 commit b53bf25

44 files changed

Lines changed: 817 additions & 74 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ repos:
133133
exclude: CHANGELOG\.rst|\.(svg|pyc|drawio)$
134134

135135
- repo: https://github.com/python-jsonschema/check-jsonschema
136-
rev: 0.37.2
136+
rev: 0.37.4
137137
hooks:
138138
- id: check-github-workflows
139139
args: ["--verbose"]

controller_interface/CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changelog for package controller_interface
33
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44

5+
6.8.0 (2026-07-01)
6+
------------------
7+
* Improve docstring of the semantic_components for consistency (`#3338 <https://github.com/ros-controls/ros2_control/issues/3338>`_)
8+
* Improve docstring of the controller_interface for consistency (`#3318 <https://github.com/ros-controls/ros2_control/issues/3318>`_)
9+
* Contributors: mcereda
10+
511
6.7.1 (2026-05-12)
612
------------------
713
* Fix the out of bound access of std::vector in ChainableController and others (`#3287 <https://github.com/ros-controls/ros2_control/issues/3287>`_)

controller_interface/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
33
<package format="2">
44
<name>controller_interface</name>
5-
<version>6.7.1</version>
5+
<version>6.8.0</version>
66
<description>Base classes for controllers and syntax cookies for supporting common sensor types in controllers and broadcasters</description>
77
<maintainer email="bence.magyar.robotics@gmail.com">Bence Magyar</maintainer>
88
<maintainer email="denis@stoglrobotics.de">Denis Štogl</maintainer>

controller_manager/CHANGELOG.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
Changelog for package controller_manager
33
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44

5+
6.8.0 (2026-07-01)
6+
------------------
7+
* Unload controller upon sigterm (`#3406 <https://github.com/ros-controls/ros2_control/issues/3406>`_)
8+
* document Ubuntu realtime kernel on Raspberry Pi (`#3397 <https://github.com/ros-controls/ros2_control/issues/3397>`_)
9+
* Wait to terminate test until spawner exits (`#3373 <https://github.com/ros-controls/ros2_control/issues/3373>`_)
10+
* Increase timeout of test_spawner_unspawner (`#3381 <https://github.com/ros-controls/ros2_control/issues/3381>`_)
11+
* fix typo in state and reference export methods (`#3347 <https://github.com/ros-controls/ros2_control/issues/3347>`_)
12+
* Enforce cleanup_controller on more exit points of configure_controller (`#3192 <https://github.com/ros-controls/ros2_control/issues/3192>`_)
13+
* Fix the initialization of the parsed resource managers (`#3346 <https://github.com/ros-controls/ros2_control/issues/3346>`_)
14+
* Controller Manager recovery from invalid URDF errors (`#2775 <https://github.com/ros-controls/ros2_control/issues/2775>`_)
15+
* Fix duplicate fallback controllers from the controller chain (`#3108 <https://github.com/ros-controls/ros2_control/issues/3108>`_)
16+
* Added default value to the fallback param declarations (`#3270 <https://github.com/ros-controls/ros2_control/issues/3270>`_)
17+
* Contributors: Christian Rauch, Christoph Fröhlich, Kevin Gilliam, Lucas Alvarez, Sai Kishor Kothakota, VitezGabriela
18+
519
6.7.1 (2026-05-12)
620
------------------
721
* Fix size assertion in test_chainable_controller (`#3284 <https://github.com/ros-controls/ros2_control/issues/3284>`_)

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/controller_manager/spawner.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def parse_args_advanced(args):
108108
"-u",
109109
"--unload-on-kill",
110110
action="store_true",
111-
help="Deactivate the active controllers and unload them on kill",
111+
help="Deactivate the active controllers and unload them on SIGINT or SIGTERM",
112112
)
113113
global_parser.add_argument("-h", "--help", action="store_true", help="Show help")
114114

@@ -239,7 +239,7 @@ def parse_native_args(args):
239239
parser.add_argument(
240240
"-u",
241241
"--unload-on-kill",
242-
help="Wait until this application is interrupted and unload controller",
242+
help="Wait until this application is interrupted (SIGINT or SIGTERM) and deactivate/unload controllers",
243243
action="store_true",
244244
)
245245
parser.add_argument(
@@ -358,6 +358,13 @@ def get_ros_params_files(argv):
358358
activate_as_group = global_args.activate_as_group
359359
unload_on_kill = global_args.unload_on_kill
360360
node = None
361+
lock = None
362+
363+
def _on_shutdown_signal(signum, frame):
364+
raise KeyboardInterrupt()
365+
366+
signal.signal(signal.SIGINT, _on_shutdown_signal)
367+
signal.signal(signal.SIGTERM, _on_shutdown_signal)
361368

362369
if spawner_ros_params_files:
363370
for controller in controllers:
@@ -574,6 +581,7 @@ def get_ros_params_files(argv):
574581
# second KeyboardInterrupt during the signal.signal() call itself.
575582
try:
576583
signal.signal(signal.SIGINT, signal.SIG_IGN)
584+
signal.signal(signal.SIGTERM, signal.SIG_IGN)
577585
except (KeyboardInterrupt, Exception):
578586
pass
579587
if unload_on_kill:
@@ -632,7 +640,7 @@ def get_ros_params_files(argv):
632640
finally:
633641
if node:
634642
node.destroy_node()
635-
if lock.is_locked:
643+
if lock is not None and lock.is_locked:
636644
lock.release()
637645
rclpy.shutdown()
638646

controller_manager/doc/userdoc.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ There are two scripts to interact with controller manager from launch files:
182182
Controller param file to be loaded into controller node before configure. Pass multiple times to load different files for different controllers or to override the parameters of the same controller.
183183
--load-only Only load the controller and leave unconfigured.
184184
--inactive Load and configure the controller, however do not activate them
185-
-u, --unload-on-kill Wait until this application is interrupted and unload controller
185+
-u, --unload-on-kill Wait until this application is interrupted (SIGINT or SIGTERM) and deactivate/unload controllers
186186
--controller-manager-timeout CONTROLLER_MANAGER_TIMEOUT
187187
Time to wait for the controller manager service to be available
188188
--switch-timeout SWITCH_TIMEOUT
@@ -267,7 +267,7 @@ The ``spawner`` now supports per controller arguments, while parsing the argumen
267267
--activate-as-group Activate controllers as a group
268268
--switch-asap, --no-switch-asap
269269
Switch controllers as soon as possible
270-
-u, --unload-on-kill Deactivate the active controllers and unload them on kill
270+
-u, --unload-on-kill Deactivate the active controllers and unload them on SIGINT or SIGTERM
271271
-h, --help Show help
272272
273273
Controller Options:
@@ -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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
33
<package format="2">
44
<name>controller_manager</name>
5-
<version>6.7.1</version>
5+
<version>6.8.0</version>
66
<description>The main runnable entrypoint of ros2_control and home of controller management and resource management.</description>
77
<maintainer email="bence.magyar.robotics@gmail.com">Bence Magyar</maintainer>
88
<maintainer email="denis@stoglrobotics.de">Denis Štogl</maintainer>
@@ -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
});

0 commit comments

Comments
 (0)