Skip to content

Commit dfab414

Browse files
committed
Fix exclusive hardware control mode switching on controller failed activation (#1522)
(cherry picked from commit ff52562)
1 parent 4603de8 commit dfab414

8 files changed

Lines changed: 268 additions & 4 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: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,17 @@ Note that not all controllers have to be restarted, e.g., broadcasters.
356356
Restarting hardware
357357
^^^^^^^^^^^^^^^^^^^^^
358358

359-
If hardware gets restarted then you should go through its lifecycle again.
360-
This can be simply achieved by returning ``ERROR`` from ``write`` and ``read`` methods of interface implementation.
361-
**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.
359+
If hardware gets restarted then you should go through its lifecycle again in order to reconfigure and export the interfaces
360+
361+
Hardware and Controller Errors
362+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
363+
364+
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.
365+
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.
366+
367+
Factors that affect Determinism
368+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
369+
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:
370+
371+
* 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.
372+

controller_manager/src/controller_manager.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,7 @@ void ControllerManager::activate_controllers()
15781578
{
15791579
std::vector<ControllerSpec> & rt_controller_list =
15801580
rt_controllers_wrapper_.update_and_get_used_by_rt_list();
1581+
std::vector<std::string> failed_controllers_command_interfaces;
15811582
for (const auto & controller_name : activate_request_)
15821583
{
15831584
auto found_it = std::find_if(
@@ -1682,10 +1683,16 @@ void ControllerManager::activate_controllers()
16821683
{
16831684
RCLCPP_ERROR(
16841685
get_logger(),
1685-
"After activation, controller '%s' is in state '%s' (%d), expected '%s' (%d).",
1686+
"After activation, controller '%s' is in state '%s' (%d), expected '%s' (%d). Releasing "
1687+
"interfaces!",
16861688
controller->get_node()->get_name(), new_state.label().c_str(), new_state.id(),
16871689
hardware_interface::lifecycle_state_names::ACTIVE,
16881690
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE);
1691+
controller->release_interfaces();
1692+
failed_controllers_command_interfaces.insert(
1693+
failed_controllers_command_interfaces.end(), command_interface_names.begin(),
1694+
command_interface_names.end());
1695+
continue;
16891696
}
16901697

16911698
// if it is a chainable controller, make the reference interfaces available on activation
@@ -1694,6 +1701,18 @@ void ControllerManager::activate_controllers()
16941701
resource_manager_->make_controller_reference_interfaces_available(controller_name);
16951702
}
16961703
}
1704+
// Now prepare and perform the stop interface switching as this is needed for exclusive
1705+
// interfaces
1706+
if (
1707+
!failed_controllers_command_interfaces.empty() &&
1708+
(!resource_manager_->prepare_command_mode_switch({}, failed_controllers_command_interfaces) ||
1709+
!resource_manager_->perform_command_mode_switch({}, failed_controllers_command_interfaces)))
1710+
{
1711+
RCLCPP_ERROR(
1712+
get_logger(),
1713+
"Error switching back the interfaces in the hardware when the controller activation "
1714+
"failed.");
1715+
}
16971716
// All controllers activated, switching done
16981717
switch_params_.do_switch = false;
16991718
}
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>

controller_manager/test/test_release_interfaces.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#include "controller_manager/controller_manager.hpp"
2323
#include "controller_manager_test_common.hpp"
2424
#include "lifecycle_msgs/msg/state.hpp"
25+
#include "test_controller/test_controller.hpp"
26+
#include "test_controller_failed_activate/test_controller_failed_activate.hpp"
2527
#include "test_controller_with_interfaces/test_controller_with_interfaces.hpp"
2628

2729
using ::testing::_;
@@ -199,3 +201,72 @@ TEST_F(TestReleaseInterfaces, switch_controllers_same_interface)
199201
abstract_test_controller2.c->get_state().id());
200202
}
201203
}
204+
205+
TEST_F(TestReleaseExclusiveInterfaces, test_exclusive_interface_switching_failure)
206+
{
207+
std::string controller_type =
208+
test_controller_failed_activate::TEST_CONTROLLER_WITH_INTERFACES_CLASS_NAME;
209+
210+
// Load two controllers of different names
211+
std::string controller_name1 = "test_controller1";
212+
std::string controller_name2 = "test_controller2";
213+
ASSERT_NO_THROW(cm_->load_controller(controller_name1, controller_type));
214+
ASSERT_NO_THROW(cm_->load_controller(
215+
controller_name2, test_controller_with_interfaces::TEST_CONTROLLER_WITH_INTERFACES_CLASS_NAME));
216+
ASSERT_EQ(2u, cm_->get_loaded_controllers().size());
217+
controller_manager::ControllerSpec abstract_test_controller1 = cm_->get_loaded_controllers()[0];
218+
controller_manager::ControllerSpec abstract_test_controller2 = cm_->get_loaded_controllers()[1];
219+
220+
// Configure controllers
221+
ASSERT_EQ(controller_interface::return_type::OK, cm_->configure_controller(controller_name1));
222+
ASSERT_EQ(controller_interface::return_type::OK, cm_->configure_controller(controller_name2));
223+
224+
ASSERT_EQ(
225+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
226+
abstract_test_controller1.c->get_lifecycle_state().id());
227+
ASSERT_EQ(
228+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
229+
abstract_test_controller2.c->get_lifecycle_state().id());
230+
231+
{
232+
// Test starting the first controller
233+
// test_controller1 activation always fails
234+
RCLCPP_INFO(cm_->get_logger(), "Starting controller #1");
235+
std::vector<std::string> start_controllers = {controller_name1};
236+
std::vector<std::string> stop_controllers = {};
237+
auto switch_future = std::async(
238+
std::launch::async, &controller_manager::ControllerManager::switch_controller, cm_,
239+
start_controllers, stop_controllers, STRICT, true, rclcpp::Duration(0, 0));
240+
ASSERT_EQ(std::future_status::timeout, switch_future.wait_for(std::chrono::milliseconds(100)))
241+
<< "switch_controller should be blocking until next update cycle";
242+
ControllerManagerRunner cm_runner(this);
243+
EXPECT_EQ(controller_interface::return_type::ERROR, switch_future.get());
244+
ASSERT_EQ(
245+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
246+
abstract_test_controller1.c->get_lifecycle_state().id());
247+
ASSERT_EQ(
248+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
249+
abstract_test_controller2.c->get_lifecycle_state().id());
250+
}
251+
252+
{
253+
// Test starting the second controller, interfaces should have been released
254+
// test_controller2 always successfully activates
255+
RCLCPP_INFO(cm_->get_logger(), "Starting controller #2");
256+
std::vector<std::string> start_controllers = {controller_name2};
257+
std::vector<std::string> stop_controllers = {};
258+
auto switch_future = std::async(
259+
std::launch::async, &controller_manager::ControllerManager::switch_controller, cm_,
260+
start_controllers, stop_controllers, STRICT, true, rclcpp::Duration(0, 0));
261+
ASSERT_EQ(std::future_status::timeout, switch_future.wait_for(std::chrono::milliseconds(100)))
262+
<< "switch_controller should be blocking until next update cycle";
263+
ControllerManagerRunner cm_runner(this);
264+
EXPECT_EQ(controller_interface::return_type::OK, switch_future.get());
265+
ASSERT_EQ(
266+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
267+
abstract_test_controller1.c->get_lifecycle_state().id());
268+
ASSERT_EQ(
269+
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE,
270+
abstract_test_controller2.c->get_lifecycle_state().id());
271+
}
272+
}

hardware_interface_testing/test/test_components/test_components.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
Test Actuator
66
</description>
77
</class>
8+
<class name="test_actuator_exclusive_interfaces" type="TestActuatorExclusiveInterfaces" base_class_type="hardware_interface::ActuatorInterface">
9+
<description>
10+
Test Actuator
11+
</description>
12+
</class>
13+
814

915
<class name="test_sensor" type="TestSensor" base_class_type="hardware_interface::SensorInterface">
1016
<description>

0 commit comments

Comments
 (0)