diff --git a/launch/launch/actions/__init__.py b/launch/launch/actions/__init__.py index ae0577e3..98da8e05 100644 --- a/launch/launch/actions/__init__.py +++ b/launch/launch/actions/__init__.py @@ -19,6 +19,7 @@ from .emit_event import EmitEvent from .execute_local import ExecuteLocal from .execute_process import ExecuteProcess +from .for_loop import ForLoop from .group_action import GroupAction from .include_launch_description import IncludeLaunchDescription from .log_info import LogInfo @@ -45,6 +46,7 @@ 'EmitEvent', 'ExecuteLocal', 'ExecuteProcess', + 'ForLoop', 'GroupAction', 'IncludeLaunchDescription', 'LogInfo', diff --git a/launch/launch/actions/for_loop.py b/launch/launch/actions/for_loop.py new file mode 100644 index 00000000..54a46b66 --- /dev/null +++ b/launch/launch/actions/for_loop.py @@ -0,0 +1,113 @@ +# Copyright 2024 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for the ForLoop action.""" + +from typing import Callable +from typing import List +from typing import Optional +from typing import Text + + +from ..action import Action +from ..launch_context import LaunchContext +from ..launch_description_entity import LaunchDescriptionEntity +from ..logging import get_logger +from ..substitutions import LaunchConfiguration + + +class ForLoop(Action): + """ + Action that instantiates entities through a function N times based on a launch argument. + + A DeclareLaunchArgument must be created before this action to define the number of iterations + in the for-loop, i.e., N iterations. For each loop iteration, the provided callback function is + called with the index value, going from 0 to N (exclusive). + + Simple example: + + .. code-block:: python + + def for_i(i: int): + return [ + LogInfo(msg=['i=', str(i)]), + ] + + LaunchDescription([ + DeclareLaunchArgument('num', default_value='2'), + ForLoop('num', function=for_i), + ]) + + This would ouput the following log messages by default: + + .. code-block:: text + + i=0 + i=1 + + If the launch argument was set to 5 (num:=5), then it would output: + + .. code-block:: text + + i=0 + i=1 + i=2 + i=3 + i=4 + """ + + def __init__( + self, + launch_argument_name: str, + *, + function: Callable[[int], Optional[List[LaunchDescriptionEntity]]], + **kwargs, + ) -> None: + """ + Create a ForLoop. + + :param launch_argument_name: the name of the launch argument that defines the length of the + for-loop + :param function: a function that receives an index value and returns entities + """ + super().__init__(**kwargs) + self._launch_argument_name = launch_argument_name + self._function = function + self._logger = get_logger(__name__) + + @property + def launch_argument_name(self) -> str: + return self._launch_argument_name + + @property + def function(self) -> Callable[[int], Optional[List[LaunchDescriptionEntity]]]: + return self._function + + def describe(self) -> Text: + return ( + type(self).__name__ + + f"(launch_argument_name='{self._launch_argument_name}', function={self._function})" + ) + + def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEntity]]: + # Get the for-loop length and convert to int + num = int(LaunchConfiguration(self._launch_argument_name).perform(context)) + self._logger.debug(f'for-loop length={num}') + + entities = [] + for i in range(num): + i_entities = self._function(i) + if i_entities: + entities.extend(i_entities) + return entities