Skip to content

Commit a6c2064

Browse files
saikishormergify[bot]
authored andcommitted
Fix exclusive hardware control mode switching on controller failed activation (#1522)
(cherry picked from commit ff52562) # Conflicts: # controller_manager/doc/userdoc.rst # controller_manager/src/controller_manager.cpp # controller_manager/test/test_release_interfaces.cpp
1 parent 4603de8 commit a6c2064

8 files changed

Lines changed: 371 additions & 0 deletions

File tree

controller_manager/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,28 @@ if(BUILD_TESTING)
162162
DESTINATION lib
163163
)
164164

165+
add_library(test_controller_failed_activate SHARED
166+
test/test_controller_failed_activate/test_controller_failed_activate.cpp
167+
)
168+
target_link_libraries(test_controller_failed_activate PUBLIC
169+
controller_manager
170+
)
171+
target_compile_definitions(test_controller_failed_activate PRIVATE "CONTROLLER_MANAGER_BUILDING_DLL")
172+
pluginlib_export_plugin_description_file(
173+
controller_interface test/test_controller_failed_activate/test_controller_failed_activate.xml)
174+
install(
175+
TARGETS test_controller_failed_activate
176+
DESTINATION lib
177+
)
178+
165179
ament_add_gmock(test_release_interfaces
166180
test/test_release_interfaces.cpp
167181
APPEND_ENV AMENT_PREFIX_PATH=${ament_index_build_path}_$<CONFIG>
168182
)
169183
target_link_libraries(test_release_interfaces
170184
controller_manager
171185
test_controller_with_interfaces
186+
test_controller_failed_activate
172187
ros2_control_test_assets::ros2_control_test_assets
173188
)
174189

controller_manager/doc/userdoc.rst

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,84 @@ Note that not all controllers have to be restarted, e.g., broadcasters.
356356
Restarting hardware
357357
^^^^^^^^^^^^^^^^^^^^^
358358

359+
<<<<<<< HEAD
359360
If hardware gets restarted then you should go through its lifecycle again.
360361
This can be simply achieved by returning ``ERROR`` from ``write`` and ``read`` methods of interface implementation.
361362
**NOT IMPLEMENTED YET - PLEASE STOP/RESTART ALL CONTROLLERS MANUALLY FOR NOW** The controller manager detects that and stops all the controllers that are commanding that hardware and restarts broadcasters that are listening to its states.
363+
=======
364+
If hardware gets restarted then you should go through its lifecycle again in order to reconfigure and export the interfaces
365+
366+
Hardware and Controller Errors
367+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
368+
369+
If the hardware during it's ``read`` or ``write`` method returns ``return_type::ERROR``, the controller manager will stop all controllers that are using the hardware's command and state interfaces.
370+
Likewise, if a controller returns ``return_type::ERROR`` from its ``update`` method, the controller manager will deactivate the respective controller. In future, the controller manager will try to start any fallback controllers if available.
371+
372+
Factors that affect Determinism
373+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
374+
When run under the conditions determined in the above section, the determinism is assured up to the limitations of the hardware and the real-time kernel. However, there are some situations that can affect determinism:
375+
376+
* When a controller fails to activate, the controller_manager will call the methods ``prepare_command_mode_switch`` and ``perform_command_mode_switch`` to stop the started interfaces. These calls can cause jitter in the main control loop.
377+
378+
Support for Asynchronous Updates
379+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
380+
For some applications, it is desirable to run a controller at a lower frequency than the controller manager's update rate. For instance, if the ``update_rate`` for the controller manager is 100Hz, the sum of the execution times of all controllers' ``update`` calls and hardware components ``read`` and ``write`` calls must be below 10ms. If one controller requires 15ms of execution time, it cannot be executed synchronously without affecting the overall system update rate. Running a controller asynchronously can be beneficial in this scenario.
381+
382+
The async update support is transparent to each controller implementation. A controller can be enabled for asynchronous updates by setting the ``is_async`` parameter to ``true``. The controller manager will load the controller accordingly. For example:
383+
384+
.. code-block:: yaml
385+
386+
controller_manager:
387+
ros__parameters:
388+
update_rate: 100 # Hz
389+
...
390+
391+
example_async_controller:
392+
ros__parameters:
393+
type: example_controller/ExampleAsyncController
394+
is_async: true
395+
update_rate: 20 # Hz
396+
...
397+
398+
will result in the controller being loaded and configured to run at 20Hz, while the controller manager runs at 100Hz. The description of the parameters can be found in the `Common Controller Parameters <https://control.ros.org/master/doc/ros2_controllers/doc/controllers_index.html#common-controller-parameters>`_ section of the ros2_controllers documentation.
399+
400+
Scheduling Behavior
401+
----------------------
402+
From a design perspective, the controller manager functions as a scheduler that triggers updates for asynchronous controllers during the control loop.
403+
404+
In this case, the ``ControllerInterfaceBase`` calls ``AsyncFunctionHandler`` to handle the actual ``update`` callback of the controller, which is the same mechanism used by the resource manager to support read/write operations for asynchronous hardware. When a controller is configured to run asynchronously, the controller interface creates an async handler during the controller's configuration and binds it to the controller's update method. The async handler thread created by the controller interface has either the same thread priority as the controller manager or the priority specified by the ``thread_priority`` parameter. When triggered by the controller manager, the async handler evaluates if the previous trigger is successfully finished and then calls the update method.
405+
406+
If the update takes significant time and another update is triggered while the previous update is still running, the result of the previous update will be used. When this situation occurs, the controller manager will print a missing update cycle message, informing the user that they need to lower their controller's frequency as the computation is taking longer than initially estimated, as shown in the following example:
407+
408+
.. code-block:: console
409+
410+
[ros2_control_node-1] [WARN] [1741626670.311533972] [example_async_controller]: The controller missed xx update cycles out of yy total triggers.
411+
412+
If the async controller's update method throws an unhandled exception, the controller manager will handle it the same way as the synchronous controllers, deactivating the controller. It will also print an error message, similar to the following:
413+
414+
.. code-block:: console
415+
416+
[ros2_control_node-1] [ERROR] [1741629098.352771957] [AsyncFunctionHandler]: AsyncFunctionHandler: Exception caught in the async callback thread!
417+
...
418+
[ros2_control_node-1] [ERROR] [1741629098.352874151] [controller_manager]: Caught exception of type : St13runtime_error while updating controller
419+
[ros2_control_node-1] [ERROR] [1741629098.352940701] [controller_manager]: Deactivating controllers : [example_async_controller] as their update resulted in an error!
420+
421+
Monitoring and Tuning
422+
----------------------
423+
424+
ros2_control ``controller_interface`` has a ``ControllerUpdateStats`` structure which can be used to monitor the controller update rate and the missed update cycles. The data is published to the ``/diagnostics`` topic. This can be used to fine tune the controller update rate.
425+
426+
427+
Different Clocks used by Controller Manager
428+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
429+
430+
The controller manager internally uses the following two different clocks for a non-simulation setup:
431+
432+
- ``RCL_ROS_TIME``: This clock is used mostly in the non-realtime loops.
433+
- ``RCL_STEADY_TIME``: This clock is used mostly in the realtime loops for the ``read``, ``update``, and ``write`` loops. However, when the controller manager is used in a simulation environment, the ``RCL_ROS_TIME`` clock is used for triggering the ``read``, ``update``, and ``write`` loops.
434+
435+
The ``time`` argument in the ``read`` and ``write`` methods of the hardware components is of type ``RCL_STEADY_TIME``, as most of the hardware expects the time to be monotonic and not affected by the system time changes. However, the ``time`` argument in the ``update`` method of the controller is of type ``RCL_ROS_TIME`` as the controller is the one that interacts with other nodes or topics to receive the commands or publish the state. This ``time`` argument can be used by the controllers to validate the received commands or to publish the state at the correct timestamp.
436+
The ``period`` argument in the ``read``, ``update`` and ``write`` methods is calculated using the trigger clock of type ``RCL_STEADY_TIME`` so it is always monotonic.
437+
438+
The reason behind using different clocks is to avoid the issues related to the affect of system time changes in the realtime loops. The ``ros2_control_node`` now also detects the overruns caused by the system time changes and longer execution times of the controllers and hardware components. The controller manager will print a warning message if the controller or hardware component misses the update cycle due to the system time changes or longer execution times.
439+
>>>>>>> ff52562 (Fix exclusive hardware control mode switching on controller failed activation (#1522))

controller_manager/src/controller_manager.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,9 +1576,14 @@ void ControllerManager::switch_chained_mode(
15761576

15771577
void ControllerManager::activate_controllers()
15781578
{
1579+
<<<<<<< HEAD
15791580
std::vector<ControllerSpec> & rt_controller_list =
15801581
rt_controllers_wrapper_.update_and_get_used_by_rt_list();
15811582
for (const auto & controller_name : activate_request_)
1583+
=======
1584+
std::vector<std::string> failed_controllers_command_interfaces;
1585+
for (const auto & controller_name : controllers_to_activate)
1586+
>>>>>>> ff52562 (Fix exclusive hardware control mode switching on controller failed activation (#1522))
15821587
{
15831588
auto found_it = std::find_if(
15841589
rt_controller_list.begin(), rt_controller_list.end(),
@@ -1677,6 +1682,7 @@ void ControllerManager::activate_controllers()
16771682
}
16781683
controller->assign_interfaces(std::move(command_loans), std::move(state_loans));
16791684

1685+
<<<<<<< HEAD
16801686
const auto new_state = controller->get_node()->activate();
16811687
if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
16821688
{
@@ -1686,6 +1692,41 @@ void ControllerManager::activate_controllers()
16861692
controller->get_node()->get_name(), new_state.label().c_str(), new_state.id(),
16871693
hardware_interface::lifecycle_state_names::ACTIVE,
16881694
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
1695+
=======
1696+
auto new_state = controller->get_lifecycle_state();
1697+
try
1698+
{
1699+
found_it->periodicity_statistics->reset();
1700+
found_it->execution_time_statistics->reset();
1701+
new_state = controller->get_node()->activate();
1702+
}
1703+
catch (const std::exception & e)
1704+
{
1705+
RCLCPP_ERROR(
1706+
get_logger(), "Caught exception of type : %s while activating the controller '%s': %s",
1707+
typeid(e).name(), controller_name.c_str(), e.what());
1708+
}
1709+
catch (...)
1710+
{
1711+
RCLCPP_ERROR(
1712+
get_logger(), "Caught unknown exception while activating the controller '%s'",
1713+
controller_name.c_str());
1714+
}
1715+
if (new_state.id() != lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE)
1716+
{
1717+
RCLCPP_ERROR(
1718+
get_logger(),
1719+
"After activation, controller '%s' is in state '%s' (%d), expected '%s' (%d). Releasing "
1720+
"interfaces!",
1721+
controller->get_node()->get_name(), new_state.label().c_str(), new_state.id(),
1722+
hardware_interface::lifecycle_state_names::ACTIVE,
1723+
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
1724+
controller->release_interfaces();
1725+
failed_controllers_command_interfaces.insert(
1726+
failed_controllers_command_interfaces.end(), command_interface_names.begin(),
1727+
command_interface_names.end());
1728+
continue;
1729+
>>>>>>> ff52562 (Fix exclusive hardware control mode switching on controller failed activation (#1522))
16891730
}
16901731

16911732
// if it is a chainable controller, make the reference interfaces available on activation
@@ -1694,8 +1735,23 @@ void ControllerManager::activate_controllers()
16941735
resource_manager_->make_controller_reference_interfaces_available(controller_name);
16951736
}
16961737
}
1738+
<<<<<<< HEAD
16971739
// All controllers activated, switching done
16981740
switch_params_.do_switch = false;
1741+
=======
1742+
// Now prepare and perform the stop interface switching as this is needed for exclusive
1743+
// interfaces
1744+
if (
1745+
!failed_controllers_command_interfaces.empty() &&
1746+
(!resource_manager_->prepare_command_mode_switch({}, failed_controllers_command_interfaces) ||
1747+
!resource_manager_->perform_command_mode_switch({}, failed_controllers_command_interfaces)))
1748+
{
1749+
RCLCPP_ERROR(
1750+
get_logger(),
1751+
"Error switching back the interfaces in the hardware when the controller activation "
1752+
"failed.");
1753+
}
1754+
>>>>>>> ff52562 (Fix exclusive hardware control mode switching on controller failed activation (#1522))
16991755
}
17001756

17011757
void ControllerManager::activate_controllers_asap()
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2021 Department of Engineering Cybernetics, NTNU.
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 "test_controller_failed_activate.hpp"
16+
17+
#include <memory>
18+
#include <string>
19+
20+
#include "lifecycle_msgs/msg/transition.hpp"
21+
22+
namespace test_controller_failed_activate
23+
{
24+
TestControllerFailedActivate::TestControllerFailedActivate()
25+
: controller_interface::ControllerInterface()
26+
{
27+
}
28+
29+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
30+
TestControllerFailedActivate::on_init()
31+
{
32+
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
33+
}
34+
35+
controller_interface::return_type TestControllerFailedActivate::update(
36+
const rclcpp::Time & /*time*/, const rclcpp::Duration & /*period*/)
37+
{
38+
return controller_interface::return_type::OK;
39+
}
40+
41+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
42+
TestControllerFailedActivate::on_configure(const rclcpp_lifecycle::State & /*previous_state&*/)
43+
{
44+
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
45+
}
46+
47+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
48+
TestControllerFailedActivate::on_activate(const rclcpp_lifecycle::State & /*previous_state&*/)
49+
{
50+
// Simply simulate a controller that can not be activated
51+
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::FAILURE;
52+
}
53+
54+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
55+
TestControllerFailedActivate::on_cleanup(const rclcpp_lifecycle::State & /*previous_state*/)
56+
{
57+
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
58+
}
59+
60+
} // namespace test_controller_failed_activate
61+
62+
#include "pluginlib/class_list_macros.hpp"
63+
64+
PLUGINLIB_EXPORT_CLASS(
65+
test_controller_failed_activate::TestControllerFailedActivate,
66+
controller_interface::ControllerInterface)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2020 Department of Engineering Cybernetics, NTNU
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+
#ifndef TEST_CONTROLLER_FAILED_ACTIVATE__TEST_CONTROLLER_FAILED_ACTIVATE_HPP_
16+
#define TEST_CONTROLLER_FAILED_ACTIVATE__TEST_CONTROLLER_FAILED_ACTIVATE_HPP_
17+
18+
#include <memory>
19+
#include <string>
20+
21+
#include "controller_manager/controller_manager.hpp"
22+
23+
namespace test_controller_failed_activate
24+
{
25+
// Corresponds to the name listed within the pluginglib xml
26+
constexpr char TEST_CONTROLLER_WITH_INTERFACES_CLASS_NAME[] =
27+
"controller_manager/test_controller_failed_activate";
28+
// Corresponds to the command interface to claim
29+
constexpr char TEST_CONTROLLER_COMMAND_INTERFACE[] = "joint2/velocity";
30+
class TestControllerFailedActivate : public controller_interface::ControllerInterface
31+
{
32+
public:
33+
TestControllerFailedActivate();
34+
35+
virtual ~TestControllerFailedActivate() = default;
36+
37+
controller_interface::InterfaceConfiguration command_interface_configuration() const override
38+
{
39+
return controller_interface::InterfaceConfiguration{
40+
controller_interface::interface_configuration_type::INDIVIDUAL,
41+
{TEST_CONTROLLER_COMMAND_INTERFACE}};
42+
}
43+
44+
controller_interface::InterfaceConfiguration state_interface_configuration() const override
45+
{
46+
return controller_interface::InterfaceConfiguration{
47+
controller_interface::interface_configuration_type::NONE};
48+
}
49+
50+
controller_interface::return_type update(
51+
const rclcpp::Time & time, const rclcpp::Duration & period) override;
52+
53+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_init() override;
54+
55+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_configure(
56+
const rclcpp_lifecycle::State & previous_state) override;
57+
58+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_activate(
59+
const rclcpp_lifecycle::State & previous_state) override;
60+
61+
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn on_cleanup(
62+
const rclcpp_lifecycle::State & previous_state) override;
63+
};
64+
65+
} // namespace test_controller_failed_activate
66+
67+
#endif // TEST_CONTROLLER_FAILED_ACTIVATE__TEST_CONTROLLER_FAILED_ACTIVATE_HPP_
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<library path="test_controller_failed_activate">
2+
3+
<class name="controller_manager/test_controller_failed_activate" type="test_controller_failed_activate::TestControllerFailedActivate" base_class_type="controller_interface::ControllerInterface">
4+
<description>
5+
Controller used for testing
6+
</description>
7+
</class>
8+
9+
</library>

0 commit comments

Comments
 (0)