Skip to content

Commit f229f6c

Browse files
authored
Fix duplicate fallback controllers from the controller chain (#3108) (#3327)
1 parent d305b87 commit f229f6c

2 files changed

Lines changed: 127 additions & 1 deletion

File tree

controller_manager/src/controller_manager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3459,7 +3459,7 @@ controller_interface::return_type ControllerManager::update(
34593459
{
34603460
for (const auto & fallback_controller : ctrl_it->info.fallback_controllers_names)
34613461
{
3462-
rt_buffer_.fallback_controllers_list.push_back(fallback_controller);
3462+
ros2_control::add_item(rt_buffer_.fallback_controllers_list, fallback_controller);
34633463
get_active_controllers_using_command_interfaces_of_controller(
34643464
fallback_controller, rt_controller_list,
34653465
rt_buffer_.activate_controllers_using_interfaces_list, resource_manager_);

controller_manager/test/test_controller_manager.cpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,132 @@ TEST_F(
21822182
}
21832183
}
21842184

2185+
TEST_F(
2186+
TestControllerManagerFallbackControllers,
2187+
test_fallback_controllers_no_duplicates_when_chain_group_shares_same_fallback)
2188+
{
2189+
// Regression test: when multiple controllers in a chain group all have the same fallback
2190+
// controller configured, the fallback list must contain unique entries
2191+
2192+
const auto strictness = controller_manager_msgs::srv::SwitchController::Request::STRICT;
2193+
controller_interface::InterfaceConfiguration cmd_itfs_cfg;
2194+
controller_interface::InterfaceConfiguration state_itfs_cfg;
2195+
cmd_itfs_cfg.type = controller_interface::interface_configuration_type::INDIVIDUAL;
2196+
state_itfs_cfg.type = controller_interface::interface_configuration_type::INDIVIDUAL;
2197+
2198+
const std::string chainable_controller_name = "test_chainable_controller_1";
2199+
const std::string primary_controller_name = "test_controller_1";
2200+
const std::string fallback_controller_name = "test_controller_2";
2201+
2202+
// Chainable controller: uses joint1/position hardware, exports modified_joint1/position ref itf
2203+
auto chainable_controller =
2204+
std::make_shared<test_chainable_controller::TestChainableController>();
2205+
cmd_itfs_cfg.names = {"joint1/position"};
2206+
state_itfs_cfg.names = {"joint2/velocity"};
2207+
chainable_controller->set_command_interface_configuration(cmd_itfs_cfg);
2208+
chainable_controller->set_state_interface_configuration(state_itfs_cfg);
2209+
chainable_controller->set_reference_interface_names({"modified_joint1/position"});
2210+
chainable_controller->set_exported_state_interface_names({"modified_joint2/velocity"});
2211+
2212+
// Primary controller: uses chainable controller's reference interface → forms a chain group
2213+
auto primary_controller = std::make_shared<test_controller::TestController>();
2214+
cmd_itfs_cfg.names = {chainable_controller_name + "/modified_joint1/position"};
2215+
primary_controller->set_command_interface_configuration(cmd_itfs_cfg);
2216+
2217+
// Fallback controller: uses joint1/position directly
2218+
auto fallback_controller = std::make_shared<test_controller::TestController>();
2219+
cmd_itfs_cfg.names = {"joint1/position"};
2220+
fallback_controller->set_command_interface_configuration(cmd_itfs_cfg);
2221+
2222+
{
2223+
ControllerManagerRunner cm_runner(this);
2224+
controller_manager::ControllerSpec controller_spec;
2225+
2226+
// Add chainable controller with fallback
2227+
controller_spec.c = chainable_controller;
2228+
controller_spec.info.name = chainable_controller_name;
2229+
controller_spec.info.type = "test_chainable_controller::TestChainableController";
2230+
controller_spec.info.fallback_controllers_names = {fallback_controller_name};
2231+
controller_spec.last_update_cycle_time = std::make_shared<rclcpp::Time>(0);
2232+
cm_->add_controller(controller_spec);
2233+
2234+
// Add primary controller with the same fallback
2235+
controller_spec.c = primary_controller;
2236+
controller_spec.info.name = primary_controller_name;
2237+
controller_spec.info.type = "test_controller::TestController";
2238+
controller_spec.info.fallback_controllers_names = {fallback_controller_name};
2239+
controller_spec.last_update_cycle_time = std::make_shared<rclcpp::Time>(0);
2240+
cm_->add_controller(controller_spec);
2241+
2242+
// Add fallback controller (no fallback of its own)
2243+
controller_spec.c = fallback_controller;
2244+
controller_spec.info.name = fallback_controller_name;
2245+
controller_spec.info.type = "test_controller::TestController";
2246+
controller_spec.info.fallback_controllers_names = {};
2247+
controller_spec.last_update_cycle_time = std::make_shared<rclcpp::Time>(0);
2248+
cm_->add_controller(controller_spec);
2249+
}
2250+
2251+
EXPECT_EQ(3u, cm_->get_loaded_controllers().size());
2252+
2253+
{
2254+
ControllerManagerRunner cm_runner(this);
2255+
EXPECT_EQ(
2256+
controller_interface::return_type::OK, cm_->configure_controller(chainable_controller_name));
2257+
EXPECT_EQ(
2258+
controller_interface::return_type::OK, cm_->configure_controller(primary_controller_name));
2259+
EXPECT_EQ(
2260+
controller_interface::return_type::OK, cm_->configure_controller(fallback_controller_name));
2261+
}
2262+
2263+
// Activate chainable and primary controllers
2264+
std::vector<std::string> start_controllers = {chainable_controller_name, primary_controller_name};
2265+
std::vector<std::string> stop_controllers = {};
2266+
auto switch_future = std::async(
2267+
std::launch::async, &controller_manager::ControllerManager::switch_controller, cm_,
2268+
start_controllers, stop_controllers, strictness, true, rclcpp::Duration(0, 0));
2269+
2270+
ASSERT_EQ(std::future_status::timeout, switch_future.wait_for(std::chrono::milliseconds(100)))
2271+
<< "switch_controller should be blocking until next update cycle";
2272+
EXPECT_EQ(
2273+
controller_interface::return_type::OK,
2274+
cm_->update(time_, rclcpp::Duration::from_seconds(0.01)));
2275+
{
2276+
ControllerManagerRunner cm_runner(this);
2277+
EXPECT_EQ(controller_interface::return_type::OK, switch_future.get());
2278+
}
2279+
2280+
EXPECT_EQ(
2281+
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE,
2282+
chainable_controller->get_lifecycle_state().id());
2283+
EXPECT_EQ(
2284+
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE,
2285+
primary_controller->get_lifecycle_state().id());
2286+
EXPECT_EQ(
2287+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
2288+
fallback_controller->get_lifecycle_state().id());
2289+
2290+
// Make primary_controller fail — this deactivates the whole chain group (primary + chainable).
2291+
// Both are in deactivate_controllers_list and both have fallback_controller_name as fallback.
2292+
// Without the fix, fallback_controller_name would appear twice in fallback_controllers_list,
2293+
// causing the second activate attempt to fail with "command interface already claimed".
2294+
primary_controller->set_external_commands_for_testing({std::numeric_limits<double>::quiet_NaN()});
2295+
EXPECT_EQ(
2296+
controller_interface::return_type::ERROR,
2297+
cm_->update(time_, rclcpp::Duration::from_seconds(0.01)));
2298+
2299+
// Both chain members must be deactivated, fallback must be active (activated exactly once)
2300+
EXPECT_EQ(
2301+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
2302+
primary_controller->get_lifecycle_state().id());
2303+
EXPECT_EQ(
2304+
lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE,
2305+
chainable_controller->get_lifecycle_state().id());
2306+
EXPECT_EQ(
2307+
lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE,
2308+
fallback_controller->get_lifecycle_state().id());
2309+
}
2310+
21852311
class TestControllerManagerControllerChainFailedUpdateCycle
21862312
: public ControllerManagerFixture<controller_manager::ControllerManager>
21872313
{

0 commit comments

Comments
 (0)