Skip to content

Commit 5ee7274

Browse files
Merge branch 'jazzy' into mergify/bp/jazzy/pr-2452
2 parents 8789ff2 + 332f48f commit 5ee7274

11 files changed

Lines changed: 167 additions & 23 deletions

File tree

doc/migration.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ hardware_interface
100100
* ``test_components`` was moved to its own package. Update the dependencies if you are using them. (`#1325 <https://github.com/ros-controls/ros2_control/pull/1325>`_)
101101
* With (`#1683 <https://github.com/ros-controls/ros2_control/pull/1683>`_) the ``rclcpp_lifecycle::State & get_state()`` and ``void set_state(const rclcpp_lifecycle::State & new_state)`` are replaced by ``rclcpp_lifecycle::State & get_lifecycle_state()`` and ``void set_lifecycle_state(const rclcpp_lifecycle::State & new_state)``. This change affects controllers and hardware. This is related to (`#1240 <https://github.com/ros-controls/ros2_control/pull/1240>`_) as variant support introduces ``get_state`` and ``set_state`` methods for setting/getting state of handles.
102102
* A new ``get_optional`` that returns a ``std::optional`` was added to the ``CommandInterface`` and ``StateInterface``. This can be used to check if the value is available or not. (`#1976 <https://github.com/ros-controls/ros2_control/pull/1976>`_ and `#2061 <https://github.com/ros-controls/ros2_control/pull/2061>`_)
103+
* The ``thread_priority`` variable in the ``HardwareInfo`` struct has been deprecated in favor of newly
104+
introduced ``async_params`` variable that has more options in the ``HardwareComponentParams`` struct.
105+
The deprecated ``thread_priority`` variable will be removed in a future release. (`# 2477 <https://github.com/ros-controls/ros2_control/pull/2477>`_).
103106

104107
Adaption of Command-/StateInterfaces
105108
***************************************

doc/release_notes.rst

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,19 @@ hardware_interface
175175
* The hardware components can be easily introspect the internal member variables using the macro ``REGISTER_ROS2_CONTROL_INTROSPECTION`` (`#1918 <https://github.com/ros-controls/ros2_control/pull/1918>`_)
176176
* Added new ``get_optional`` method that returns ``std::optional`` of the templated type, and this can be used to check if the value is available or not (`#1976 <https://github.com/ros-controls/ros2_control/pull/1976>`_ and `#2061 <https://github.com/ros-controls/ros2_control/pull/2061>`_)
177177
* Added hardware components execution time and periodicity statistics diagnostics (`#2086 <https://github.com/ros-controls/ros2_control/pull/2086>`_)
178+
* Changes from `(PR #1688) <https://github.com/ros-controls/ros2_control/pull/1688>`_ for an overview of related changes and discussion refer to `(PR #1240) <https://github.com/ros-controls/ros2_control/pull/1240>`_:
179+
180+
* ``Command-/StateInterfaces`` are now created and exported automatically by the framework via the ``on_export_command_interfaces()`` or ``on_export_state_interfaces()`` methods based on the interfaces defined in the ``ros2_control`` XML-tag, which gets parsed and the ``InterfaceDescription`` is created accordingly (check the `hardware_info.hpp <https://github.com/ros-controls/ros2_control/tree/{REPOS_FILE_BRANCH}/hardware_interface/include/hardware_interface/hardware_info.hpp>`__).
181+
* The memory for storing the value of a ``Command-/StateInterfaces`` is no longer allocated in the hardware but instead in the ``Command-/StateInterfaces`` itself.
182+
* To access the automatically created ``Command-/StateInterfaces`` we provide the ``std::unordered_map<std::string, InterfaceDescription>``, where the string is the fully qualified name of the interface and the ``InterfaceDescription`` is the configuration of the interface. The ``std::unordered_map<>`` are divided into ``type_state_interfaces_`` and ``type_command_interfaces_`` where type can be: ``joint``, ``sensor``, ``gpio`` and ``unlisted``. E.g. the ``CommandInterfaces`` for all joints can be found in the ``joint_command_interfaces_`` map. The ``unlisted`` includes all interfaces not listed in the ``ros2_control`` XML-tag but were created by overriding the ``export_unlisted_command_interfaces()`` or ``export_unlisted_state_interfaces()`` function by creating some custom ``Command-/StateInterfaces``.
183+
184+
* The ``prepare_command_mode_switch`` and ``perform_command_mode_switch`` methods will now only receive the start/stop interfaces that belong to the hardware component instead of everything (`#2120 <https://github.com/ros-controls/ros2_control/pull/2120>`_)
185+
* 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>`_).
178186

179187
joint_limits
180188
************
181189
* Add header to import limits from standard URDF definition (`#1298 <https://github.com/ros-controls/ros2_control/pull/1298>`_)
182190

183-
Adaption of Command-/StateInterfaces
184-
***************************************
185-
Changes from `(PR #1688) <https://github.com/ros-controls/ros2_control/pull/1688>`_ for an overview of related changes and discussion refer to `(PR #1240) <https://github.com/ros-controls/ros2_control/pull/1240>`_.
186-
187-
* ``Command-/StateInterfaces`` are now created and exported automatically by the framework via the ``on_export_command_interfaces()`` or ``on_export_state_interfaces()`` methods based on the interfaces defined in the ``ros2_control`` XML-tag, which gets parsed and the ``InterfaceDescription`` is created accordingly (check the `hardware_info.hpp <https://github.com/ros-controls/ros2_control/tree/{REPOS_FILE_BRANCH}/hardware_interface/include/hardware_interface/hardware_info.hpp>`__).
188-
* The memory for storing the value of a ``Command-/StateInterfaces`` is no longer allocated in the hardware but instead in the ``Command-/StateInterfaces`` itself.
189-
* To access the automatically created ``Command-/StateInterfaces`` we provide the ``std::unordered_map<std::string, InterfaceDescription>``, where the string is the fully qualified name of the interface and the ``InterfaceDescription`` is the configuration of the interface. The ``std::unordered_map<>`` are divided into ``type_state_interfaces_`` and ``type_command_interfaces_`` where type can be: ``joint``, ``sensor``, ``gpio`` and ``unlisted``. E.g. the ``CommandInterfaces`` for all joints can be found in the ``joint_command_interfaces_`` map. The ``unlisted`` includes all interfaces not listed in the ``ros2_control`` XML-tag but were created by overriding the ``export_unlisted_command_interfaces()`` or ``export_unlisted_state_interfaces()`` function by creating some custom ``Command-/StateInterfaces``.
190-
* The ``prepare_command_mode_switch`` and ``perform_command_mode_switch`` methods will now only receive the start/stop interfaces that belong to the hardware component instead of everything (`#2120 <https://github.com/ros-controls/ros2_control/pull/2120>`_)
191-
192191
ros2controlcli
193192
**************
194193
* Spawner colours were added to ``list_controllers`` depending upon active or inactive (`#1409 <https://github.com/ros-controls/ros2_control/pull/1409>`_)

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
@@ -140,13 +140,25 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
140140
logger_ = logger_copy.get_child(
141141
"hardware_component." + params.hardware_info.type + "." + params.hardware_info.name);
142142
info_ = params.hardware_info;
143-
if (info_.is_async)
143+
if (params.hardware_info.is_async)
144144
{
145-
RCLCPP_INFO_STREAM(
146-
get_logger(), "Starting async handler with scheduler priority: " << info_.thread_priority);
145+
realtime_tools::AsyncFunctionHandlerParams async_thread_params;
146+
async_thread_params.thread_priority = info_.async_params.thread_priority;
147+
async_thread_params.scheduling_policy =
148+
realtime_tools::AsyncSchedulingPolicy(info_.async_params.scheduling_policy);
149+
async_thread_params.cpu_affinity_cores = info_.async_params.cpu_affinity_cores;
150+
async_thread_params.clock = params.clock;
151+
async_thread_params.logger = params.logger;
152+
async_thread_params.exec_rate = params.hardware_info.rw_rate;
153+
async_thread_params.print_warnings = info_.async_params.print_warnings;
154+
RCLCPP_INFO(
155+
get_logger(), "Starting async handler with scheduler priority: %d and policy : %s",
156+
info_.async_params.thread_priority,
157+
async_thread_params.scheduling_policy.to_string().c_str());
147158
async_handler_ = std::make_unique<realtime_tools::AsyncFunctionHandler<return_type>>();
159+
const bool is_sensor_type = (info_.type == "sensor");
148160
async_handler_->init(
149-
[this](const rclcpp::Time & time, const rclcpp::Duration & period)
161+
[this, is_sensor_type](const rclcpp::Time & time, const rclcpp::Duration & period)
150162
{
151163
const auto read_start_time = std::chrono::steady_clock::now();
152164
const auto ret_read = read(time, period);
@@ -159,7 +171,9 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
159171
{
160172
return ret_read;
161173
}
162-
if (info_.type != "sensor")
174+
if (
175+
!is_sensor_type &&
176+
this->get_lifecycle_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
163177
{
164178
const auto write_start_time = std::chrono::steady_clock::now();
165179
const auto ret_write = write(time, period);
@@ -173,7 +187,7 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
173187
}
174188
return return_type::OK;
175189
},
176-
info_.thread_priority);
190+
async_thread_params);
177191
async_handler_->start_thread();
178192
}
179193

@@ -493,8 +507,8 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
493507
status.successful = result.first;
494508
if (!status.successful)
495509
{
496-
RCLCPP_WARN(
497-
get_logger(),
510+
RCLCPP_WARN_EXPRESSION(
511+
get_logger(), info_.async_params.print_warnings,
498512
"Trigger read/write called while the previous async trigger is still in progress for "
499513
"hardware interface : '%s'. Failed to trigger read/write cycle!",
500514
info_.name.c_str());
@@ -714,6 +728,19 @@ class HardwareComponentInterface : public rclcpp_lifecycle::node_interfaces::Lif
714728
*/
715729
const HardwareInfo & get_hardware_info() const { return info_; }
716730

731+
/// Pause any asynchronous operations.
732+
/**
733+
* This method is called to pause any ongoing asynchronous operations, such as read/write cycles.
734+
* It is typically used during lifecycle transitions or when the hardware needs to be paused.
735+
*/
736+
void pause_async_operations()
737+
{
738+
if (async_handler_)
739+
{
740+
async_handler_->pause_execution();
741+
}
742+
}
743+
717744
/// Prepare for the activation of the hardware.
718745
/**
719746
* 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
@@ -229,6 +229,18 @@ struct InterfaceDescription
229229
HandleDataType get_data_type() const { return HandleDataType(interface_info.data_type); }
230230
};
231231

232+
struct HardwareAsyncParams
233+
{
234+
/// Thread priority for the async worker thread
235+
int thread_priority = 50;
236+
/// Scheduling policy for the async worker thread
237+
std::string scheduling_policy = "synchronized";
238+
/// CPU affinity cores for the async worker thread
239+
std::vector<int> cpu_affinity_cores = {};
240+
/// Whether to print warnings when the async thread doesn't meet its deadline
241+
bool print_warnings = true;
242+
};
243+
232244
/// This structure stores information about hardware defined in a robot's URDF.
233245
struct HardwareInfo
234246
{
@@ -243,7 +255,9 @@ struct HardwareInfo
243255
/// Component is async
244256
bool is_async;
245257
/// Async thread priority
246-
int thread_priority;
258+
[[deprecated("Use async_params instead.")]] int thread_priority;
259+
/// Async Parameters
260+
HardwareAsyncParams async_params;
247261
/// Name of the pluginlib plugin of the hardware that will be loaded.
248262
std::string hardware_plugin_name;
249263
/// (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:

0 commit comments

Comments
 (0)