diff --git a/launch_ros/launch_ros/actions/composable_node_container.py b/launch_ros/launch_ros/actions/composable_node_container.py index d054d85b..a7125e77 100644 --- a/launch_ros/launch_ros/actions/composable_node_container.py +++ b/launch_ros/launch_ros/actions/composable_node_container.py @@ -79,15 +79,19 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: composable nodes load action if it applies. """ load_actions = None # type: Optional[List[Action]] + valid_composable_nodes = [] + for node_object in self.__composable_node_descriptions: + if node_object.condition() is None or node_object.condition().evaluate(context): + valid_composable_nodes.append(node_object) if ( - self.__composable_node_descriptions is not None and - len(self.__composable_node_descriptions) > 0 + valid_composable_nodes is not None and + len(valid_composable_nodes) > 0 ): from .load_composable_nodes import LoadComposableNodes # Perform load action once the container has started. load_actions = [ LoadComposableNodes( - composable_node_descriptions=self.__composable_node_descriptions, + composable_node_descriptions=valid_composable_nodes, target_container=self ) ] diff --git a/launch_ros/launch_ros/descriptions/composable_node.py b/launch_ros/launch_ros/descriptions/composable_node.py index 7f1bcc61..8ad35c5c 100644 --- a/launch_ros/launch_ros/descriptions/composable_node.py +++ b/launch_ros/launch_ros/descriptions/composable_node.py @@ -17,6 +17,8 @@ from typing import List from typing import Optional +from launch.condition import Condition +from launch.conditions import IfCondition, UnlessCondition from launch.frontend import Entity from launch.frontend import Parser from launch.some_substitutions_type import SomeSubstitutionsType @@ -43,6 +45,7 @@ def __init__( parameters: Optional[SomeParameters] = None, remappings: Optional[SomeRemapRules] = None, extra_arguments: Optional[SomeParameters] = None, + condition: Optional[Condition] = None, ) -> None: """ Initialize a ComposableNode description. @@ -54,6 +57,7 @@ def __init__( :param parameters: list of either paths to yaml files or dictionaries of parameters :param remappings: list of from/to pairs for remapping names :param extra_arguments: container specific arguments to be passed to the loaded node + :param condition: action will be executed if the condition evaluates to true """ self.__package = normalize_to_list_of_substitutions(package) self.__node_plugin = normalize_to_list_of_substitutions(plugin) @@ -78,6 +82,8 @@ def __init__( if extra_arguments: self.__extra_arguments = normalize_parameters(extra_arguments) + self.__condition = condition + @classmethod def parse(cls, parser: Parser, entity: Entity): """Parse composable_node.""" @@ -88,6 +94,19 @@ def parse(cls, parser: Parser, entity: Entity): kwargs['plugin'] = parser.parse_substitution(entity.get_attr('plugin')) kwargs['name'] = parser.parse_substitution(entity.get_attr('name')) + if_cond = entity.get_attr('if', optional=True) + unless_cond = entity.get_attr('unless', optional=True) + if if_cond is not None and unless_cond is not None: + raise RuntimeError("if and unless conditions can't be used simultaneously") + if if_cond is not None: + kwargs['condition'] = IfCondition( + predicate_expression=parser.parse_substitution(if_cond) + ) + if unless_cond is not None: + kwargs['condition'] = UnlessCondition( + predicate_expression=parser.parse_substitution(unless_cond) + ) + namespace = entity.get_attr('namespace', optional=True) if namespace is not None: kwargs['namespace'] = parser.parse_substitution(namespace) @@ -158,3 +177,7 @@ def remappings(self) -> Optional[RemapRules]: def extra_arguments(self) -> Optional[Parameters]: """Get container extra arguments YAML files or dicts with substitutions to be performed.""" return self.__extra_arguments + + def condition(self) -> Optional[Condition]: + """Getter for condition.""" + return self.__condition diff --git a/test_launch_ros/test/test_launch_ros/actions/test_composable_node_container.py b/test_launch_ros/test/test_launch_ros/actions/test_composable_node_container.py index 75d37498..319734a5 100644 --- a/test_launch_ros/test/test_launch_ros/actions/test_composable_node_container.py +++ b/test_launch_ros/test/test_launch_ros/actions/test_composable_node_container.py @@ -18,8 +18,8 @@ from launch import LaunchDescription from launch import LaunchService -from launch.actions import DeclareLaunchArgument -from launch.actions import GroupAction +from launch.actions import DeclareLaunchArgument, GroupAction +from launch.conditions import IfCondition, UnlessCondition from launch.substitutions import LaunchConfiguration from launch_ros.actions import ComposableNodeContainer from launch_ros.descriptions import ComposableNode @@ -119,3 +119,65 @@ def test_composable_node_container_in_group_with_launch_configuration_in_descrip context = _assert_launch_no_errors(actions) assert get_node_name_count(context, f'/{TEST_CONTAINER_NAMESPACE}/{TEST_CONTAINER_NAME}') == 1 assert get_node_name_count(context, f'/{TEST_NODE_NAMESPACE}/{TEST_NODE_NAME}') == 1 + + +def test_composable_node_container_if_condition(): + """Nominal test for launching a ComposableNodeContainer.""" + TEST_NODE_NAME_1 = 'test_component_container_node_name_1' + TEST_NODE_NAME_2 = 'test_component_container_node_name_2' + actions = [ + DeclareLaunchArgument(name='flag', default_value='False'), + ComposableNodeContainer( + package='rclcpp_components', + executable='component_container', + name=TEST_CONTAINER_NAME, + namespace=TEST_CONTAINER_NAMESPACE, + composable_node_descriptions=[ + ComposableNode( + package='composition', + plugin='composition::Listener', + name=TEST_NODE_NAME, + namespace=TEST_NODE_NAMESPACE, + condition=IfCondition(LaunchConfiguration('flag')) + ) + ], + ), + ] + + context = _assert_launch_no_errors(actions) + + assert get_node_name_count(context, f'/{TEST_CONTAINER_NAMESPACE}/{TEST_CONTAINER_NAME}') == 1 + assert get_node_name_count(context, f'/{TEST_NODE_NAMESPACE}/{TEST_NODE_NAME}') == 0 + + actions = [ + DeclareLaunchArgument(name='flag', default_value='False'), + ComposableNodeContainer( + package='rclcpp_components', + executable='component_container', + name=TEST_CONTAINER_NAME, + namespace=TEST_CONTAINER_NAMESPACE, + composable_node_descriptions=[ + ComposableNode( + package='composition', + plugin='composition::Listener', + name=TEST_NODE_NAME_1, + namespace=TEST_NODE_NAMESPACE, + condition=UnlessCondition(LaunchConfiguration('flag')) + ), + ComposableNode( + package='composition', + plugin='composition::Listener', + name=TEST_NODE_NAME_2, + namespace=TEST_NODE_NAMESPACE, + condition=IfCondition(LaunchConfiguration('flag')) + ) + + ], + ), + ] + + context = _assert_launch_no_errors(actions) + + assert get_node_name_count(context, f'/{TEST_CONTAINER_NAMESPACE}/{TEST_CONTAINER_NAME}') == 1 + assert get_node_name_count(context, f'/{TEST_NODE_NAMESPACE}/{TEST_NODE_NAME_1}') == 1 + assert get_node_name_count(context, f'/{TEST_NODE_NAMESPACE}/{TEST_NODE_NAME_2}') == 0