Skip to content

Commit aebc2b8

Browse files
authored
Add detach async policy for rate critical frameworks (#2477)
1 parent 5722fd4 commit aebc2b8

File tree

11 files changed

+161
-14
lines changed

11 files changed

+161
-14
lines changed

doc/migration.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ hardware_interface
5454
have been updated from passing a ``const HardwareInfo &`` to passing a ``const
5555
HardwareComponentParams &`` (`# 2323 <https://github.com/ros-controls/ros2_control/pull/2323>`_).
5656
The old signatures are deprecated and will be removed in a future release.
57+
58+
* The ``thread_priority`` variable in the ``HardwareInfo`` struct has been deprecated in favor of newly
59+
introduced ``async_params`` variable that has more options in the ``HardwareComponentParams`` struct.
60+
The deprecated ``thread_priority`` variable will be removed in a future release. (`# 2477 <https://github.com/ros-controls/ros2_control/pull/2477>`_).

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ hardware_interface
1717
* The hardware interface is now treated similarly as ERROR, when a hardware component returns ERROR on the read cycle (`#2334 <https://github.com/ros-controls/ros2_control/pull/2334>`_).
1818
* The controllers are now deactivated when a hardware component returns DEACTIVATE on the write cycle. The parameter ``deactivate_controllers_on_hardware_self_deactivate`` is added to control this behavior temporarily. It is recommended to set this parameter to true in order to avoid controllers to use inactive hardware components and to avoid any unexpected behavior. This feature parameter will be removed in future releases and will be defaulted to true (`#2334 <https://github.com/ros-controls/ros2_control/pull/2334>`_ & `#2501 <https://github.com/ros-controls/ros2_control/pull/2501>`_).
1919
* The controllers are not allowed to be activated when the hardware component is in INACTIVE state. The parameter ``allow_controller_activation_with_inactive_hardware`` is added to control this behavior temporarily. It is recommended to set this parameter to false in order to avoid controllers to use inactive hardware components and to avoid any unexpected behavior. This feature parameter will be removed in future releases and will be defaulted to false (`#2347 <https://github.com/ros-controls/ros2_control/pull/2347>`_).
20+
* The asynchronous components now support two scheduling policies: ``synchronized`` and ``detached`` and other properties to configure them (`#2477 <https://github.com/ros-controls/ros2_control/pull/2477>`_).
2021

2122
ros2controlcli
2223
**************

hardware_interface/doc/asynchronous_components.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ Parameters
1313
The following parameters can be set in the ``ros2_control`` hardware configuration to run the hardware component asynchronously:
1414

1515
* ``is_async``: (optional) If set to ``true``, the hardware component will run asynchronously. Default is ``false``.
16+
17+
Under the ``ros2_control`` tag, a ``properties`` tag can be added to specify the following parameters of the asynchronous hardware component:
18+
1619
* ``thread_priority``: (optional) The priority of the thread that runs the hardware component. The priority is an integer value between 0 and 99. The default value is 50.
20+
* ``affinity``: (optional) The CPU affinity of the thread that runs the hardware component. The affinity is a list of CPU core IDs. The default value is an empty list, which means that the thread can run on any CPU core.
21+
* ``scheduling_policy``: (optional) The scheduling policy of the thread that runs the hardware component. The scheduling policy can be one of the following values:
22+
* ``synchronized`` (default): The thread will run with the synchronized with the main controller_manager thread. The controller_manager is responsible for triggering the read and write calls of the hardware component.
23+
* ``detached``: The thread will run independently of the main controller_manager thread. The hardware component will manage its own timing for triggering the read and write calls.
24+
* ``print_warnings``: (optional) If set to ``true``, a warning will be printed if the thread is not able to meet its timing requirements. Default is ``true``.
1725

1826
.. note::
1927
The thread priority is only used when the hardware component is run asynchronously.
@@ -59,7 +67,10 @@ For a RRBot with multimodal gripper and external sensor:
5967
<state_interface name="digital_input2"/>
6068
</gpio>
6169
</ros2_control>
62-
<ros2_control name="MultimodalGripper" type="actuator" is_async="true" thread_priority="30">
70+
<ros2_control name="MultimodalGripper" type="actuator" is_async="true">
71+
<properties>
72+
<async affinity="[2,4]" scheduling_policy="synchronized" print_warnings="true" thread_priority="30"/>
73+
</properties>
6374
<hardware>
6475
<plugin>ros2_control_demo_hardware/MultimodalGripper</plugin>
6576
</hardware>

hardware_interface/include/hardware_interface/hardware_component_interface.hpp

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,25 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
131131
logger_ = logger_copy.get_child(
132132
"hardware_component." + params.hardware_info.type + "." + params.hardware_info.name);
133133
info_ = params.hardware_info;
134-
if (info_.is_async)
134+
if (params.hardware_info.is_async)
135135
{
136-
RCLCPP_INFO_STREAM(
137-
get_logger(), "Starting async handler with scheduler priority: " << info_.thread_priority);
136+
realtime_tools::AsyncFunctionHandlerParams async_thread_params;
137+
async_thread_params.thread_priority = info_.async_params.thread_priority;
138+
async_thread_params.scheduling_policy =
139+
realtime_tools::AsyncSchedulingPolicy(info_.async_params.scheduling_policy);
140+
async_thread_params.cpu_affinity_cores = info_.async_params.cpu_affinity_cores;
141+
async_thread_params.clock = params.clock;
142+
async_thread_params.logger = params.logger;
143+
async_thread_params.exec_rate = params.hardware_info.rw_rate;
144+
async_thread_params.print_warnings = info_.async_params.print_warnings;
145+
RCLCPP_INFO(
146+
get_logger(), "Starting async handler with scheduler priority: %d and policy : %s",
147+
info_.async_params.thread_priority,
148+
async_thread_params.scheduling_policy.to_string().c_str());
138149
async_handler_ = std::make_unique<realtime_tools::AsyncFunctionHandler<return_type>>();
150+
const bool is_sensor_type = (info_.type == "sensor");
139151
async_handler_->init(
140-
[this](const rclcpp::Time & time, const rclcpp::Duration & period)
152+
[this, is_sensor_type](const rclcpp::Time & time, const rclcpp::Duration & period)
141153
{
142154
const auto read_start_time = std::chrono::steady_clock::now();
143155
const auto ret_read = read(time, period);
@@ -150,7 +162,9 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
150162
{
151163
return ret_read;
152164
}
153-
if (info_.type != "sensor")
165+
if (
166+
!is_sensor_type &&
167+
this->get_lifecycle_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
154168
{
155169
const auto write_start_time = std::chrono::steady_clock::now();
156170
const auto ret_write = write(time, period);
@@ -164,7 +178,7 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
164178
}
165179
return return_type::OK;
166180
},
167-
info_.thread_priority);
181+
async_thread_params);
168182
async_handler_->start_thread();
169183
}
170184

@@ -485,8 +499,8 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
485499
status.successful = result.first;
486500
if (!status.successful)
487501
{
488-
RCLCPP_WARN(
489-
get_logger(),
502+
RCLCPP_WARN_EXPRESSION(
503+
get_logger(), info_.async_params.print_warnings,
490504
"Trigger read/write called while the previous async trigger is still in progress for "
491505
"hardware interface : '%s'. Failed to trigger read/write cycle!",
492506
info_.name.c_str());
@@ -739,6 +753,19 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
739753
*/
740754
const HardwareInfo & get_hardware_info() const { return info_; }
741755

756+
/// Pause any asynchronous operations.
757+
/**
758+
* This method is called to pause any ongoing asynchronous operations, such as read/write cycles.
759+
* It is typically used during lifecycle transitions or when the hardware needs to be paused.
760+
*/
761+
void pause_async_operations()
762+
{
763+
if (async_handler_)
764+
{
765+
async_handler_->pause_execution();
766+
}
767+
}
768+
742769
/// Prepare for the activation of the hardware.
743770
/**
744771
* This method is called before the hardware is activated by the resource manager.

hardware_interface/include/hardware_interface/hardware_info.hpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,18 @@ struct InterfaceDescription
247247
HandleDataType get_data_type() const { return HandleDataType(interface_info.data_type); }
248248
};
249249

250+
struct HardwareAsyncParams
251+
{
252+
/// Thread priority for the async worker thread
253+
int thread_priority = 50;
254+
/// Scheduling policy for the async worker thread
255+
std::string scheduling_policy = "synchronized";
256+
/// CPU affinity cores for the async worker thread
257+
std::vector<int> cpu_affinity_cores = {};
258+
/// Whether to print warnings when the async thread doesn't meet its deadline
259+
bool print_warnings = true;
260+
};
261+
250262
/// This structure stores information about hardware defined in a robot's URDF.
251263
struct HardwareInfo
252264
{
@@ -261,7 +273,9 @@ struct HardwareInfo
261273
/// Component is async
262274
bool is_async;
263275
/// Async thread priority
264-
int thread_priority;
276+
[[deprecated("Use async_params instead.")]] int thread_priority;
277+
/// Async Parameters
278+
HardwareAsyncParams async_params;
265279
/// Name of the pluginlib plugin of the hardware that will be loaded.
266280
std::string hardware_plugin_name;
267281
/// (Optional) Key-value pairs for hardware parameters.

hardware_interface/include/hardware_interface/lexical_casts.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ namespace hardware_interface
3232
*/
3333
double stod(const std::string & s);
3434

35+
/**
36+
* \brief Convert a string to lower case.
37+
* \param string The input string.
38+
* \return The lower case version of the input string.
39+
*/
40+
std::string to_lower_case(const std::string & string);
41+
42+
/**
43+
* \brief Parse a boolean value from a string.
44+
* \param bool_string The input string, can be "true", "false" (case insensitive)
45+
* \return The parsed boolean value.
46+
* \throws std::invalid_argument if the string is not a valid boolean representation.
47+
*/
3548
bool parse_bool(const std::string & bool_string);
3649

3750
template <typename T>

hardware_interface/src/component_parser.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ constexpr const auto kStateInterfaceTag = "state_interface";
5555
constexpr const auto kMinTag = "min";
5656
constexpr const auto kMaxTag = "max";
5757
constexpr const auto kLimitsTag = "limits";
58+
constexpr const auto kPropertiesTag = "properties";
59+
constexpr const auto kAsyncTag = "async";
5860
constexpr const auto kEnableAttribute = "enable";
5961
constexpr const auto kInitialValueTag = "initial_value";
6062
constexpr const auto kMimicAttribute = "mimic";
@@ -68,6 +70,9 @@ constexpr const auto kOffsetAttribute = "offset";
6870
constexpr const auto kReadWriteRateAttribute = "rw_rate";
6971
constexpr const auto kIsAsyncAttribute = "is_async";
7072
constexpr const auto kThreadPriorityAttribute = "thread_priority";
73+
constexpr const auto kAffinityCoresAttribute = "affinity";
74+
constexpr const auto kSchedulingPolicyAttribute = "scheduling_policy";
75+
constexpr const auto kPrintWarningsAttribute = "print_warnings";
7176

7277
} // namespace
7378

@@ -672,6 +677,7 @@ HardwareInfo parse_resource_from_xml(
672677
hardware.is_async = parse_is_async_attribute(ros2_control_it);
673678
hardware.thread_priority = hardware.is_async ? parse_thread_priority_attribute(ros2_control_it)
674679
: std::numeric_limits<int>::max();
680+
hardware.async_params.thread_priority = hardware.thread_priority;
675681

676682
// Parse everything under ros2_control tag
677683
hardware.hardware_plugin_name = "";
@@ -698,6 +704,42 @@ HardwareInfo parse_resource_from_xml(
698704
hardware.hardware_parameters = parse_parameters_from_xml(params_it);
699705
}
700706
}
707+
else if (std::string(kPropertiesTag) == ros2_control_child_it->Name())
708+
{
709+
const auto * async_it = ros2_control_child_it->FirstChildElement(kAsyncTag);
710+
if (async_it)
711+
{
712+
// Async properties are defined
713+
try
714+
{
715+
if (async_it->FindAttribute(kAffinityCoresAttribute))
716+
{
717+
hardware.async_params.cpu_affinity_cores =
718+
parse_array<int>(get_attribute_value(async_it, kAffinityCoresAttribute, kAsyncTag));
719+
}
720+
if (async_it->FindAttribute(kSchedulingPolicyAttribute))
721+
{
722+
hardware.async_params.scheduling_policy =
723+
to_lower_case(get_attribute_value(async_it, kSchedulingPolicyAttribute, kAsyncTag));
724+
}
725+
if (async_it->FindAttribute(kThreadPriorityAttribute))
726+
{
727+
hardware.async_params.thread_priority = parse_thread_priority_attribute(async_it);
728+
hardware.thread_priority = hardware.async_params.thread_priority;
729+
}
730+
if (async_it->FindAttribute(kPrintWarningsAttribute))
731+
{
732+
hardware.async_params.print_warnings =
733+
parse_bool(get_attribute_value(async_it, kPrintWarningsAttribute, kAsyncTag));
734+
}
735+
}
736+
catch (const std::exception & e)
737+
{
738+
throw std::runtime_error(
739+
fmt::format(FMT_COMPILE("Error parsing {} tag: {}"), kAsyncTag, e.what()));
740+
}
741+
}
742+
}
701743
else if (std::string(kJointTag) == ros2_control_child_it->Name())
702744
{
703745
hardware.joints.push_back(parse_component_from_xml(ros2_control_child_it));

hardware_interface/src/hardware_component.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const rclcpp_lifecycle::State & HardwareComponent::configure()
9797
std::unique_lock<std::recursive_mutex> lock(component_mutex_);
9898
if (impl_->get_lifecycle_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED)
9999
{
100+
impl_->pause_async_operations();
100101
switch (impl_->on_configure(impl_->get_lifecycle_state()))
101102
{
102103
case CallbackReturn::SUCCESS:
@@ -124,6 +125,7 @@ const rclcpp_lifecycle::State & HardwareComponent::cleanup()
124125
impl_->enable_introspection(false);
125126
if (impl_->get_lifecycle_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE)
126127
{
128+
impl_->pause_async_operations();
127129
switch (impl_->on_cleanup(impl_->get_lifecycle_state()))
128130
{
129131
case CallbackReturn::SUCCESS:
@@ -149,6 +151,7 @@ const rclcpp_lifecycle::State & HardwareComponent::shutdown()
149151
impl_->get_lifecycle_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN &&
150152
impl_->get_lifecycle_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_FINALIZED)
151153
{
154+
impl_->pause_async_operations();
152155
switch (impl_->on_shutdown(impl_->get_lifecycle_state()))
153156
{
154157
case CallbackReturn::SUCCESS:
@@ -177,6 +180,7 @@ const rclcpp_lifecycle::State & HardwareComponent::activate()
177180
}
178181
if (impl_->get_lifecycle_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE)
179182
{
183+
impl_->pause_async_operations();
180184
impl_->prepare_for_activation();
181185
switch (impl_->on_activate(impl_->get_lifecycle_state()))
182186
{
@@ -205,6 +209,7 @@ const rclcpp_lifecycle::State & HardwareComponent::deactivate()
205209
impl_->enable_introspection(false);
206210
if (impl_->get_lifecycle_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
207211
{
212+
impl_->pause_async_operations();
208213
switch (impl_->on_deactivate(impl_->get_lifecycle_state()))
209214
{
210215
case CallbackReturn::SUCCESS:
@@ -233,6 +238,7 @@ const rclcpp_lifecycle::State & HardwareComponent::error()
233238
impl_->get_lifecycle_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNKNOWN &&
234239
impl_->get_lifecycle_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_UNCONFIGURED)
235240
{
241+
impl_->pause_async_operations();
236242
switch (impl_->on_error(impl_->get_lifecycle_state()))
237243
{
238244
case CallbackReturn::SUCCESS:

hardware_interface/src/lexical_casts.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,19 @@ double stod(const std::string & s)
6262
throw std::invalid_argument("Failed converting string to real number");
6363
}
6464

65+
std::string to_lower_case(const std::string & string)
66+
{
67+
std::string lower_case_string = string;
68+
std::transform(
69+
lower_case_string.begin(), lower_case_string.end(), lower_case_string.begin(),
70+
[](unsigned char c) { return std::tolower(c); });
71+
return lower_case_string;
72+
}
73+
6574
bool parse_bool(const std::string & bool_string)
6675
{
6776
// Copy input to temp and make lowercase
68-
std::string temp = bool_string;
69-
std::transform(
70-
temp.begin(), temp.end(), temp.begin(), [](unsigned char c) { return std::tolower(c); });
77+
std::string temp = to_lower_case(bool_string);
7178

7279
if (temp == "true")
7380
{

hardware_interface/test/test_component_parser.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,13 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_async_components)
14311431
ASSERT_THAT(hardware_info.joints, SizeIs(1));
14321432
ASSERT_TRUE(hardware_info.is_async);
14331433
ASSERT_EQ(hardware_info.thread_priority, 30);
1434+
ASSERT_EQ(hardware_info.async_params.thread_priority, 30);
1435+
ASSERT_EQ(hardware_info.async_params.scheduling_policy, "detached");
1436+
ASSERT_FALSE(hardware_info.async_params.print_warnings);
1437+
ASSERT_EQ(3u, hardware_info.async_params.cpu_affinity_cores.size());
1438+
ASSERT_THAT(
1439+
hardware_info.async_params.cpu_affinity_cores,
1440+
testing::ContainerEq(std::vector<int>({2, 4, 6})));
14341441

14351442
EXPECT_EQ(hardware_info.joints[0].name, "joint1");
14361443
EXPECT_EQ(hardware_info.joints[0].type, "joint");
@@ -1444,6 +1451,10 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_async_components)
14441451
ASSERT_THAT(hardware_info.sensors, SizeIs(1));
14451452
ASSERT_TRUE(hardware_info.is_async);
14461453
ASSERT_EQ(hardware_info.thread_priority, 50);
1454+
ASSERT_EQ(hardware_info.async_params.thread_priority, 50);
1455+
ASSERT_EQ(hardware_info.async_params.scheduling_policy, "synchronized");
1456+
ASSERT_TRUE(hardware_info.async_params.print_warnings);
1457+
ASSERT_TRUE(hardware_info.async_params.cpu_affinity_cores.empty());
14471458

14481459
EXPECT_EQ(hardware_info.sensors[0].name, "sensor1");
14491460
EXPECT_EQ(hardware_info.sensors[0].type, "sensor");
@@ -1468,6 +1479,11 @@ TEST_F(TestComponentParser, successfully_parse_valid_urdf_async_components)
14681479
EXPECT_EQ(hardware_info.gpios[0].type, "gpio");
14691480
ASSERT_TRUE(hardware_info.is_async);
14701481
ASSERT_EQ(hardware_info.thread_priority, 70);
1482+
ASSERT_EQ(hardware_info.async_params.thread_priority, 70);
1483+
ASSERT_EQ(hardware_info.async_params.scheduling_policy, "synchronized");
1484+
ASSERT_EQ(1u, hardware_info.async_params.cpu_affinity_cores.size());
1485+
ASSERT_THAT(
1486+
hardware_info.async_params.cpu_affinity_cores, testing::ContainerEq(std::vector<int>({1})));
14711487
}
14721488

14731489
TEST_F(TestComponentParser, successfully_parse_parameter_empty)

0 commit comments

Comments
 (0)