@@ -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+
21852311class TestControllerManagerControllerChainFailedUpdateCycle
21862312: public ControllerManagerFixture<controller_manager::ControllerManager>
21872313{
0 commit comments