From 0eae6ffbaabb514c9a80e6add1f49f16ade72a2c Mon Sep 17 00:00:00 2001 From: Tatsuro Sakaguchi Date: Fri, 6 Dec 2024 07:48:38 +0900 Subject: [PATCH 1/6] Set env path (#659) Signed-off-by: Tatsuro Sakaguchi --- ros_gz_sim/ros_gz_sim/actions/gzserver.py | 75 ++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/ros_gz_sim/ros_gz_sim/actions/gzserver.py b/ros_gz_sim/ros_gz_sim/actions/gzserver.py index 2665ea4d..c8b33c3a 100644 --- a/ros_gz_sim/ros_gz_sim/actions/gzserver.py +++ b/ros_gz_sim/ros_gz_sim/actions/gzserver.py @@ -14,11 +14,14 @@ """Module for the GzServer action.""" +import os from typing import List from typing import Optional +from ament_index_python.packages import get_package_share_directory +from catkin_pkg.package import InvalidPackage, PACKAGE_MANIFEST_FILENAME, parse_package from launch.action import Action -from launch.actions import GroupAction +from launch.actions import GroupAction, SetEnvironmentVariable from launch.conditions import IfCondition from launch.frontend import Entity, expose_action, Parser from launch.launch_context import LaunchContext @@ -26,6 +29,59 @@ from launch.substitutions import PythonExpression from launch_ros.actions import ComposableNodeContainer, LoadComposableNodes, Node from launch_ros.descriptions import ComposableNode +from ros2pkg.api import get_package_names + + +""" +Search for model, plugin and media paths exported by packages. + +e.g. + + + +${prefix} is replaced by package's share directory in install. + +Thus the required directory needs to be installed from CMakeLists.txt +e.g. install(DIRECTORY models + DESTINATION share/${PROJECT_NAME}) +""" + + +class GazeboRosPaths: + + @staticmethod + def get_paths(): + gazebo_model_path = [] + gazebo_plugin_path = [] + gazebo_media_path = [] + + for package_name in get_package_names(): + package_share_path = get_package_share_directory(package_name) + package_file_path = os.path.join(package_share_path, PACKAGE_MANIFEST_FILENAME) + if os.path.isfile(package_file_path): + try: + package = parse_package(package_file_path) + except InvalidPackage: + continue + for export in package.exports: + if export.tagname == 'gazebo_ros': + if 'gazebo_model_path' in export.attributes: + xml_path = export.attributes['gazebo_model_path'] + xml_path = xml_path.replace('${prefix}', package_share_path) + gazebo_model_path.append(xml_path) + if 'plugin_path' in export.attributes: + xml_path = export.attributes['plugin_path'] + xml_path = xml_path.replace('${prefix}', package_share_path) + gazebo_plugin_path.append(xml_path) + if 'gazebo_media_path' in export.attributes: + xml_path = export.attributes['gazebo_media_path'] + xml_path = xml_path.replace('${prefix}', package_share_path) + gazebo_media_path.append(xml_path) + + gazebo_model_path = os.pathsep.join(gazebo_model_path + gazebo_media_path) + gazebo_plugin_path = os.pathsep.join(gazebo_plugin_path) + + return gazebo_model_path, gazebo_plugin_path @expose_action('gz_server') @@ -117,6 +173,21 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: if isinstance(self.__create_own_container, list): self.__create_own_container = self.__create_own_container[0] + model_paths, plugin_paths = GazeboRosPaths.get_paths() + system_plugin_path_env = SetEnvironmentVariable( + 'GZ_SIM_SYSTEM_PLUGIN_PATH', + os.pathsep.join([ + os.environ.get('GZ_SIM_SYSTEM_PLUGIN_PATH', default=''), + os.environ.get('LD_LIBRARY_PATH', default=''), + plugin_paths, + ])) + resource_path_env = SetEnvironmentVariable( + 'GZ_SIM_RESOURCE_PATH', + os.pathsep.join([ + os.environ.get('GZ_SIM_RESOURCE_PATH', default=''), + model_paths, + ])) + # Standard node configuration load_nodes = GroupAction( condition=IfCondition(PythonExpression(['not ', self.__use_composition])), @@ -174,6 +245,8 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: ) return [ + system_plugin_path_env, + resource_path_env, load_nodes, load_composable_nodes_with_container, load_composable_nodes_without_container From 63b651a53e30c53474e9e2bf429e417147827b64 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Sat, 7 Dec 2024 00:01:04 +0100 Subject: [PATCH 2/6] Garden EOL (#662) Signed-off-by: Addisu Z. Taddese --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a2cf24e2..44712ff9 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,26 @@ ROS 2 version | Gazebo version | Branch | Binaries hosted at -- | -- | -- | -- Foxy | Citadel | [foxy](https://github.com/gazebosim/ros_gz/tree/foxy) | https://packages.ros.org -Foxy | Edifice | [foxy](https://github.com/gazebosim/ros_gz/tree/foxy) | only from source -Galactic | Edifice | [galactic](https://github.com/gazebosim/ros_gz/tree/galactic) | https://packages.ros.org +Foxy | Edifice | [foxy](https://github.com/gazebosim/ros_gz/tree/foxy) | only from source [^2] +Galactic | Edifice | [galactic](https://github.com/gazebosim/ros_gz/tree/galactic) | https://packages.ros.org [^2] Galactic | Fortress | [galactic](https://github.com/gazebosim/ros_gz/tree/galactic) | only from source Humble | Fortress | [humble](https://github.com/gazebosim/ros_gz/tree/humble) | https://packages.ros.org -Humble | Garden | [humble](https://github.com/gazebosim/ros_gz/tree/humble) | [gazebo packages](https://gazebosim.org/docs/latest/ros_installation#gazebo-garden-with-ros-2-humble-iron-or-rolling-use-with-caution-)[^1] +Humble | Garden | [humble](https://github.com/gazebosim/ros_gz/tree/humble) | [gazebo packages](https://gazebosim.org/docs/latest/ros_installation#gazebo-garden-with-ros-2-humble-iron-or-rolling-use-with-caution-)[^1] [^2] Humble | Harmonic | [humble](https://github.com/gazebosim/ros_gz/tree/humble) | [gazebo packages](https://gazebosim.org/docs/harmonic/ros_installation#-gazebo-harmonic-with-ros-2-humble-iron-or-rolling-use-with-caution-)[^1] Iron | Fortress | [humble](https://github.com/gazebosim/ros_gz/tree/iron) | https://packages.ros.org -Iron | Garden | [humble](https://github.com/gazebosim/ros_gz/tree/iron) | only from source +Iron | Garden | [humble](https://github.com/gazebosim/ros_gz/tree/iron) | only from source [^2] Iron | Harmonic | [humble](https://github.com/gazebosim/ros_gz/tree/iron) | only from source -Jazzy | Garden | [ros2](https://github.com/gazebosim/ros_gz/tree/ros2) | only from source +Jazzy | Garden | [ros2](https://github.com/gazebosim/ros_gz/tree/ros2) | only from source [^2] Jazzy | Harmonic | [jazzy](https://github.com/gazebosim/ros_gz/tree/jazzy) | https://packages.ros.org Rolling | Fortress | [humble](https://github.com/gazebosim/ros_gz/tree/humble) | https://packages.ros.org -Rolling | Garden | [ros2](https://github.com/gazebosim/ros_gz/tree/ros2) | only from source +Rolling | Garden | [ros2](https://github.com/gazebosim/ros_gz/tree/ros2) | only from source [^2] Rolling | Harmonic | [ros2](https://github.com/gazebosim/ros_gz/tree/ros2) | only from source -[^1]: Binaries for these pairings are provided from a the packages.osrfoundation.org repository. Refer to https://gazebosim.org/docs/latest/ros_installation for installation instructions. +[^1]: Binaries for these pairings are provided from the packages.osrfoundation.org repository. Refer to https://gazebosim.org/docs/latest/ros_installation for installation instructions. +[^2]: Note that the Gazebo version on this row has reached end-of-life. For information on ROS(1) and Gazebo compatibility, refer to the [noetic branch README](https://github.com/gazebosim/ros_gz/tree/noetic) -> Please [ticket an issue](https://github.com/gazebosim/ros_gz/issues/) if you'd like support to be added for some combination. - [Details about the renaming process](README_RENAME.md) from `ign` to `gz` . **Note**: The `ros_ign` prefixed packages are shim packages that redirect to their `ros_gz` counterpart. @@ -86,7 +85,7 @@ Be sure you've installed #### Gazebo -Install either [Fortress, Garden, or Harmonic](https://gazebosim.org/docs). +Install either [Fortress, Harmonic or Ionic](https://gazebosim.org/docs). Set the `GZ_VERSION` environment variable to the Gazebo version you'd like to compile against. For example: @@ -97,7 +96,7 @@ like to compile against. For example: #### Compile ros_gz -The following steps are for Linux and OSX. +The following steps are for Linux and macOS. 1. Create a colcon workspace: From a0c04de1c0479222ad21f07a4640e3b79f5e6c6c Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 10 Dec 2024 11:05:43 -0600 Subject: [PATCH 3/6] Improve argument parsing in Actions The `RosGzBridge` and `GzServer` now support different spellings for boolean arguments (`True`, `true`). This also simplifies how conditionals are used to create composable nodes by evaluating the conditionals and using them as regular Python booleans instead of relying on `PythonExpression`. It was actually the `PythonExpression` that was preventing support of boolean arguments spelled `true`/`false`. Signed-off-by: Addisu Z. Taddese --- .../ros_gz_bridge/actions/ros_gz_bridge.py | 168 +++++++++--------- ros_gz_sim/ros_gz_sim/actions/gzserver.py | 155 ++++++++-------- 2 files changed, 156 insertions(+), 167 deletions(-) diff --git a/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py b/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py index bcebee0c..3149250b 100644 --- a/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py +++ b/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py @@ -14,16 +14,14 @@ """Module for the ros_gz bridge action.""" -from typing import List -from typing import Optional +from typing import Dict, List, Optional, Union from launch.action import Action -from launch.actions import GroupAction -from launch.conditions import IfCondition from launch.frontend import Entity, expose_action, Parser from launch.launch_context import LaunchContext from launch.some_substitutions_type import SomeSubstitutionsType -from launch.substitutions import PythonExpression +from launch.substitutions import TextSubstitution +from launch.utilities.type_utils import normalize_typed_substitution, perform_typed_substitution from launch_ros.actions import ComposableNodeContainer, LoadComposableNodes, Node from launch_ros.descriptions import ComposableNode @@ -37,13 +35,13 @@ def __init__( *, bridge_name: SomeSubstitutionsType, config_file: SomeSubstitutionsType, - container_name: Optional[SomeSubstitutionsType] = 'ros_gz_container', - create_own_container: Optional[SomeSubstitutionsType] = 'False', - namespace: Optional[SomeSubstitutionsType] = '', - use_composition: Optional[SomeSubstitutionsType] = 'False', - use_respawn: Optional[SomeSubstitutionsType] = 'False', - log_level: Optional[SomeSubstitutionsType] = 'info', - bridge_params: Optional[SomeSubstitutionsType] = '', + container_name: SomeSubstitutionsType = 'ros_gz_container', + create_own_container: Union[bool, SomeSubstitutionsType] = False, + namespace: SomeSubstitutionsType = '', + use_composition: Union[bool, SomeSubstitutionsType] = False, + use_respawn: Union[bool, SomeSubstitutionsType] = False, + log_level: SomeSubstitutionsType = 'info', + bridge_params: SomeSubstitutionsType = '', **kwargs ) -> None: """ @@ -60,20 +58,36 @@ def __init__( :param: bridge_params Extra parameters to pass to the bridge. """ super().__init__(**kwargs) + self.__bridge_name = bridge_name self.__config_file = config_file self.__container_name = container_name - self.__create_own_container = create_own_container self.__namespace = namespace - self.__use_composition = use_composition - self.__use_respawn = use_respawn + + # This is here to allow using strings or booleans as values for boolean variables when the Action is used from Python + # i.e., this allows users to do: + # RosGzBridge(bridge_name='bridge1', use_composition='true', create_own_container=True) + # Note that use_composition is set to a string while create_own_container is set to a boolean. The reverse would also work. + # At some point, we might want to deprecate this and only allow setting booleans since that's + # what users would expect when calling this from Python + if isinstance(create_own_container, str): + self.__create_own_container = normalize_typed_substitution(TextSubstitution(text=create_own_container), bool) + else: + self.__create_own_container = normalize_typed_substitution(create_own_container, bool) + + if isinstance(use_composition, str): + self.__use_composition = normalize_typed_substitution(TextSubstitution(text=use_composition), bool) + else: + self.__use_composition = normalize_typed_substitution(use_composition, bool) + + self.__use_respawn = normalize_typed_substitution(use_respawn, bool) self.__log_level = log_level self.__bridge_params = bridge_params @classmethod def parse(cls, entity: Entity, parser: Parser): """Parse ros_gz_bridge.""" - _, kwargs = super().parse(entity, parser) + kwargs:Dict = super().parse(entity, parser)[1] bridge_name = entity.get_attr( 'bridge_name', data_type=str, @@ -169,77 +183,59 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: bridge_params_pairs = simplified_bridge_params.split(',') parsed_bridge_params = dict(pair.split(':') for pair in bridge_params_pairs) - if isinstance(self.__use_composition, list): - self.__use_composition = self.__use_composition[0] - - if isinstance(self.__create_own_container, list): - self.__create_own_container = self.__create_own_container[0] - - if isinstance(self.__use_respawn, list): - self.__use_respawn = self.__use_respawn[0] - - # Standard node configuration - load_nodes = GroupAction( - condition=IfCondition(PythonExpression(['not ', self.__use_composition])), - actions=[ - Node( - package='ros_gz_bridge', - executable='bridge_node', - name=self.__bridge_name, - namespace=self.__namespace, - output='screen', - respawn=bool(self.__use_respawn), - respawn_delay=2.0, - parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], - arguments=['--ros-args', '--log-level', self.__log_level], - ), - ], - ) + use_composition_eval = perform_typed_substitution(context, self.__use_composition, bool) + create_own_container_eval = perform_typed_substitution(context, self.__create_own_container, bool) + + launch_descriptions: List[Action] = [] + + if not use_composition_eval: + # Standard node configuration + launch_descriptions.append(Node( + package='ros_gz_bridge', + executable='bridge_node', + name=self.__bridge_name, + namespace=self.__namespace, + output='screen', + respawn=perform_typed_substitution(context, self.__use_respawn, bool), + respawn_delay=2.0, + parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], + arguments=['--ros-args', '--log-level', self.__log_level], + )) # Composable node with container configuration - load_composable_nodes_with_container = ComposableNodeContainer( - condition=IfCondition( - PythonExpression([self.__use_composition, ' and ', self.__create_own_container]) - ), - name=self.__container_name, - namespace='', - package='rclcpp_components', - executable='component_container', - composable_node_descriptions=[ - ComposableNode( - package='ros_gz_bridge', - plugin='ros_gz_bridge::RosGzBridge', - name=self.__bridge_name, - namespace=self.__namespace, - parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], - extra_arguments=[{'use_intra_process_comms': True}], - ), - ], - output='screen', - ) + if use_composition_eval and create_own_container_eval: + launch_descriptions.append(ComposableNodeContainer( + name=self.__container_name, + namespace='', + package='rclcpp_components', + executable='component_container', + composable_node_descriptions=[ + ComposableNode( + package='ros_gz_bridge', + plugin='ros_gz_bridge::RosGzBridge', + name=self.__bridge_name, + namespace=self.__namespace, + parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], + extra_arguments=[{'use_intra_process_comms': True}], + ), + ], + output='screen', + )) # Composable node without container configuration - load_composable_nodes_without_container = LoadComposableNodes( - condition=IfCondition( - PythonExpression( - [self.__use_composition, ' and not ', self.__create_own_container] - ) - ), - target_container=self.__container_name, - composable_node_descriptions=[ - ComposableNode( - package='ros_gz_bridge', - plugin='ros_gz_bridge::RosGzBridge', - name=self.__bridge_name, - namespace=self.__namespace, - parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], - extra_arguments=[{'use_intra_process_comms': True}], - ), - ], - ) - - return [ - load_nodes, - load_composable_nodes_with_container, - load_composable_nodes_without_container - ] + if use_composition_eval and not create_own_container_eval: + launch_descriptions.append(LoadComposableNodes( + target_container=self.__container_name, + composable_node_descriptions=[ + ComposableNode( + package='ros_gz_bridge', + plugin='ros_gz_bridge::RosGzBridge', + name=self.__bridge_name, + namespace=self.__namespace, + parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], + extra_arguments=[{'use_intra_process_comms': True}], + ), + ], + )) + + return launch_descriptions diff --git a/ros_gz_sim/ros_gz_sim/actions/gzserver.py b/ros_gz_sim/ros_gz_sim/actions/gzserver.py index c8b33c3a..975a7883 100644 --- a/ros_gz_sim/ros_gz_sim/actions/gzserver.py +++ b/ros_gz_sim/ros_gz_sim/actions/gzserver.py @@ -15,18 +15,17 @@ """Module for the GzServer action.""" import os -from typing import List -from typing import Optional +from typing import Dict, List, Optional, Union from ament_index_python.packages import get_package_share_directory from catkin_pkg.package import InvalidPackage, PACKAGE_MANIFEST_FILENAME, parse_package from launch.action import Action -from launch.actions import GroupAction, SetEnvironmentVariable -from launch.conditions import IfCondition +from launch.actions import SetEnvironmentVariable from launch.frontend import Entity, expose_action, Parser from launch.launch_context import LaunchContext from launch.some_substitutions_type import SomeSubstitutionsType -from launch.substitutions import PythonExpression +from launch.substitutions import TextSubstitution +from launch.utilities.type_utils import normalize_typed_substitution, perform_typed_substitution from launch_ros.actions import ComposableNodeContainer, LoadComposableNodes, Node from launch_ros.descriptions import ComposableNode from ros2pkg.api import get_package_names @@ -91,11 +90,11 @@ class GzServer(Action): def __init__( self, *, - world_sdf_file: Optional[SomeSubstitutionsType] = '', - world_sdf_string: Optional[SomeSubstitutionsType] = '', - container_name: Optional[SomeSubstitutionsType] = 'ros_gz_container', - create_own_container: Optional[SomeSubstitutionsType] = 'False', - use_composition: Optional[SomeSubstitutionsType] = 'False', + world_sdf_file: SomeSubstitutionsType = '', + world_sdf_string: SomeSubstitutionsType = '', + container_name: SomeSubstitutionsType = 'ros_gz_container', + create_own_container: Union[bool, SomeSubstitutionsType] = False, + use_composition: Union[bool, SomeSubstitutionsType] = False, **kwargs ) -> None: """ @@ -114,13 +113,24 @@ def __init__( self.__world_sdf_file = world_sdf_file self.__world_sdf_string = world_sdf_string self.__container_name = container_name - self.__create_own_container = create_own_container - self.__use_composition = use_composition + + + # This is here to allow using strings or booleans as values for boolean variables when the Action is used from Python + # See the RosGzBridge.__init__ function for more details. + if isinstance(create_own_container, str): + self.__create_own_container = normalize_typed_substitution(TextSubstitution(text=create_own_container), bool) + else: + self.__create_own_container = normalize_typed_substitution(create_own_container, bool) + + if isinstance(use_composition, str): + self.__use_composition = normalize_typed_substitution(TextSubstitution(text=use_composition), bool) + else: + self.__use_composition = normalize_typed_substitution(use_composition, bool) @classmethod def parse(cls, entity: Entity, parser: Parser): """Parse gz_server.""" - _, kwargs = super().parse(entity, parser) + kwargs:Dict = super().parse(entity, parser)[1] world_sdf_file = entity.get_attr( 'world_sdf_file', data_type=str, @@ -167,87 +177,70 @@ def parse(cls, entity: Entity, parser: Parser): def execute(self, context: LaunchContext) -> Optional[List[Action]]: """Execute the action.""" - if isinstance(self.__use_composition, list): - self.__use_composition = self.__use_composition[0] - if isinstance(self.__create_own_container, list): - self.__create_own_container = self.__create_own_container[0] + launch_descriptions: List[Action] = [] model_paths, plugin_paths = GazeboRosPaths.get_paths() - system_plugin_path_env = SetEnvironmentVariable( + launch_descriptions.append(SetEnvironmentVariable( 'GZ_SIM_SYSTEM_PLUGIN_PATH', os.pathsep.join([ os.environ.get('GZ_SIM_SYSTEM_PLUGIN_PATH', default=''), os.environ.get('LD_LIBRARY_PATH', default=''), plugin_paths, - ])) - resource_path_env = SetEnvironmentVariable( + ]))) + launch_descriptions.append(SetEnvironmentVariable( 'GZ_SIM_RESOURCE_PATH', os.pathsep.join([ os.environ.get('GZ_SIM_RESOURCE_PATH', default=''), model_paths, - ])) - - # Standard node configuration - load_nodes = GroupAction( - condition=IfCondition(PythonExpression(['not ', self.__use_composition])), - actions=[ - Node( - package='ros_gz_sim', - executable='gzserver', - output='screen', - parameters=[{'world_sdf_file': self.__world_sdf_file, - 'world_sdf_string': self.__world_sdf_string}], - ), - ], - ) + ]))) + + use_composition_eval = perform_typed_substitution(context, self.__use_composition, bool) + create_own_container_eval = perform_typed_substitution(context, self.__create_own_container, bool) + if not use_composition_eval: + # Standard node configuration + launch_descriptions.append(Node( + package='ros_gz_sim', + executable='gzserver', + output='screen', + parameters=[{'world_sdf_file': self.__world_sdf_file, + 'world_sdf_string': self.__world_sdf_string}], + )) # Composable node with container configuration - load_composable_nodes_with_container = ComposableNodeContainer( - condition=IfCondition( - PythonExpression([self.__use_composition, ' and ', self.__create_own_container]) - ), - name=self.__container_name, - namespace='', - package='rclcpp_components', - executable='component_container', - composable_node_descriptions=[ - ComposableNode( - package='ros_gz_sim', - plugin='ros_gz_sim::GzServer', - name='gz_server', - parameters=[{'world_sdf_file': self.__world_sdf_file, - 'world_sdf_string': self.__world_sdf_string}], - extra_arguments=[{'use_intra_process_comms': True}], - ), - ], - output='screen', - ) + if use_composition_eval and create_own_container_eval: + launch_descriptions.append(ComposableNodeContainer( + name=self.__container_name, + namespace='', + package='rclcpp_components', + executable='component_container', + composable_node_descriptions=[ + ComposableNode( + package='ros_gz_sim', + plugin='ros_gz_sim::GzServer', + name='gz_server', + parameters=[{'world_sdf_file': self.__world_sdf_file, + 'world_sdf_string': self.__world_sdf_string}], + extra_arguments=[{'use_intra_process_comms': True}], + ), + ], + output='screen', + )) # Composable node without container configuration - load_composable_nodes_without_container = LoadComposableNodes( - condition=IfCondition( - PythonExpression( - [self.__use_composition, ' and not ', self.__create_own_container] - ) - ), - target_container=self.__container_name, - composable_node_descriptions=[ - ComposableNode( - package='ros_gz_sim', - plugin='ros_gz_sim::GzServer', - name='gz_server', - parameters=[{'world_sdf_file': self.__world_sdf_file, - 'world_sdf_string': self.__world_sdf_string}], - extra_arguments=[{'use_intra_process_comms': True}], - ), - ], - ) - - return [ - system_plugin_path_env, - resource_path_env, - load_nodes, - load_composable_nodes_with_container, - load_composable_nodes_without_container - ] + if use_composition_eval and not create_own_container_eval: + launch_descriptions.append(LoadComposableNodes( + target_container=self.__container_name, + composable_node_descriptions=[ + ComposableNode( + package='ros_gz_sim', + plugin='ros_gz_sim::GzServer', + name='gz_server', + parameters=[{'world_sdf_file': self.__world_sdf_file, + 'world_sdf_string': self.__world_sdf_string}], + extra_arguments=[{'use_intra_process_comms': True}], + ), + ], + )) + + return launch_descriptions From ff96f9762452360621b627e55d984e718e7523ee Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 10 Dec 2024 13:38:43 -0600 Subject: [PATCH 4/6] Fix linter errors Signed-off-by: Addisu Z. Taddese --- .../ros_gz_bridge/actions/ros_gz_bridge.py | 33 ++++++++++++------- ros_gz_sim/ros_gz_sim/actions/gzserver.py | 28 ++++++++++------ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py b/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py index 3149250b..bd931698 100644 --- a/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py +++ b/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py @@ -64,19 +64,26 @@ def __init__( self.__container_name = container_name self.__namespace = namespace - # This is here to allow using strings or booleans as values for boolean variables when the Action is used from Python - # i.e., this allows users to do: + # This is here to allow using strings or booleans as values for boolean variables when + # the Action is used from Python i.e., this allows users to do: # RosGzBridge(bridge_name='bridge1', use_composition='true', create_own_container=True) - # Note that use_composition is set to a string while create_own_container is set to a boolean. The reverse would also work. - # At some point, we might want to deprecate this and only allow setting booleans since that's - # what users would expect when calling this from Python + # Note that use_composition is set to a string while create_own_container is set to a + # boolean. The reverse would also work. + # At some point, we might want to deprecate this and only allow setting booleans since + # that's what users would expect when calling this from Python if isinstance(create_own_container, str): - self.__create_own_container = normalize_typed_substitution(TextSubstitution(text=create_own_container), bool) + self.__create_own_container = normalize_typed_substitution( + TextSubstitution(text=create_own_container), bool + ) else: - self.__create_own_container = normalize_typed_substitution(create_own_container, bool) + self.__create_own_container = normalize_typed_substitution( + create_own_container, bool + ) if isinstance(use_composition, str): - self.__use_composition = normalize_typed_substitution(TextSubstitution(text=use_composition), bool) + self.__use_composition = normalize_typed_substitution( + TextSubstitution(text=use_composition), bool + ) else: self.__use_composition = normalize_typed_substitution(use_composition, bool) @@ -87,7 +94,7 @@ def __init__( @classmethod def parse(cls, entity: Entity, parser: Parser): """Parse ros_gz_bridge.""" - kwargs:Dict = super().parse(entity, parser)[1] + kwargs: Dict = super().parse(entity, parser)[1] bridge_name = entity.get_attr( 'bridge_name', data_type=str, @@ -183,8 +190,12 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: bridge_params_pairs = simplified_bridge_params.split(',') parsed_bridge_params = dict(pair.split(':') for pair in bridge_params_pairs) - use_composition_eval = perform_typed_substitution(context, self.__use_composition, bool) - create_own_container_eval = perform_typed_substitution(context, self.__create_own_container, bool) + use_composition_eval = perform_typed_substitution( + context, self.__use_composition, bool + ) + create_own_container_eval = perform_typed_substitution( + context, self.__create_own_container, bool + ) launch_descriptions: List[Action] = [] diff --git a/ros_gz_sim/ros_gz_sim/actions/gzserver.py b/ros_gz_sim/ros_gz_sim/actions/gzserver.py index 975a7883..7e3ee3d3 100644 --- a/ros_gz_sim/ros_gz_sim/actions/gzserver.py +++ b/ros_gz_sim/ros_gz_sim/actions/gzserver.py @@ -114,23 +114,28 @@ def __init__( self.__world_sdf_string = world_sdf_string self.__container_name = container_name - - # This is here to allow using strings or booleans as values for boolean variables when the Action is used from Python - # See the RosGzBridge.__init__ function for more details. + # This is here to allow using strings or booleans as values for boolean variables when + # the Action is used from Python See the RosGzBridge.__init__ function for more details. if isinstance(create_own_container, str): - self.__create_own_container = normalize_typed_substitution(TextSubstitution(text=create_own_container), bool) + self.__create_own_container = normalize_typed_substitution( + TextSubstitution(text=create_own_container), bool + ) else: - self.__create_own_container = normalize_typed_substitution(create_own_container, bool) + self.__create_own_container = normalize_typed_substitution( + create_own_container, bool + ) if isinstance(use_composition, str): - self.__use_composition = normalize_typed_substitution(TextSubstitution(text=use_composition), bool) + self.__use_composition = normalize_typed_substitution( + TextSubstitution(text=use_composition), bool + ) else: self.__use_composition = normalize_typed_substitution(use_composition, bool) @classmethod def parse(cls, entity: Entity, parser: Parser): """Parse gz_server.""" - kwargs:Dict = super().parse(entity, parser)[1] + kwargs: Dict = super().parse(entity, parser)[1] world_sdf_file = entity.get_attr( 'world_sdf_file', data_type=str, @@ -177,7 +182,6 @@ def parse(cls, entity: Entity, parser: Parser): def execute(self, context: LaunchContext) -> Optional[List[Action]]: """Execute the action.""" - launch_descriptions: List[Action] = [] model_paths, plugin_paths = GazeboRosPaths.get_paths() @@ -195,8 +199,12 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: model_paths, ]))) - use_composition_eval = perform_typed_substitution(context, self.__use_composition, bool) - create_own_container_eval = perform_typed_substitution(context, self.__create_own_container, bool) + use_composition_eval = perform_typed_substitution( + context, self.__use_composition, bool + ) + create_own_container_eval = perform_typed_substitution( + context, self.__create_own_container, bool + ) if not use_composition_eval: # Standard node configuration launch_descriptions.append(Node( From 819c943dbd040b2d78555eb135ddefa334828d6a Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 10 Dec 2024 15:01:15 -0600 Subject: [PATCH 5/6] Improve parameter handling for RosGzBridge Signed-off-by: Addisu Z. Taddese --- .../ros_gz_bridge/actions/ros_gz_bridge.py | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py b/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py index bd931698..ee28a2f4 100644 --- a/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py +++ b/ros_gz_bridge/ros_gz_bridge/actions/ros_gz_bridge.py @@ -14,16 +14,18 @@ """Module for the ros_gz bridge action.""" -from typing import Dict, List, Optional, Union +from typing import cast, Dict, List, Optional, Union from launch.action import Action from launch.frontend import Entity, expose_action, Parser from launch.launch_context import LaunchContext from launch.some_substitutions_type import SomeSubstitutionsType from launch.substitutions import TextSubstitution +from launch.utilities import ensure_argument_type from launch.utilities.type_utils import normalize_typed_substitution, perform_typed_substitution from launch_ros.actions import ComposableNodeContainer, LoadComposableNodes, Node from launch_ros.descriptions import ComposableNode +from launch_ros.parameters_type import SomeParameters @expose_action('ros_gz_bridge') @@ -41,7 +43,7 @@ def __init__( use_composition: Union[bool, SomeSubstitutionsType] = False, use_respawn: Union[bool, SomeSubstitutionsType] = False, log_level: SomeSubstitutionsType = 'info', - bridge_params: SomeSubstitutionsType = '', + bridge_params: Optional[SomeParameters] = None, **kwargs ) -> None: """ @@ -89,7 +91,13 @@ def __init__( self.__use_respawn = normalize_typed_substitution(use_respawn, bool) self.__log_level = log_level - self.__bridge_params = bridge_params + self.__bridge_params = [{'config_file': self.__config_file}] + if bridge_params is not None: + # This handling of bridge_params was copied from launch_ros/actions/node.py + ensure_argument_type(bridge_params, (list), 'bridge_params', 'RosGzBridge') + # All elements in the list are paths to files with parameters (or substitutions that + # evaluate to paths), or dictionaries of parameters (fields can be substitutions). + self.__bridge_params.extend(cast(list, bridge_params)) @classmethod def parse(cls, entity: Entity, parser: Parser): @@ -128,9 +136,7 @@ def parse(cls, entity: Entity, parser: Parser): 'log_level', data_type=str, optional=True) - bridge_params = entity.get_attr( - 'bridge_params', data_type=str, - optional=True) + parameters = entity.get_attr('param', data_type=List[Entity], optional=True) if isinstance(bridge_name, str): bridge_name = parser.parse_substitution(bridge_name) @@ -165,31 +171,13 @@ def parse(cls, entity: Entity, parser: Parser): log_level = parser.parse_substitution(log_level) kwargs['log_level'] = log_level - if isinstance(bridge_params, str): - bridge_params = parser.parse_substitution(bridge_params) - kwargs['bridge_params'] = bridge_params + if parameters is not None: + kwargs['bridge_params'] = Node.parse_nested_parameters(parameters, parser) return cls, kwargs def execute(self, context: LaunchContext) -> Optional[List[Action]]: """Execute the action.""" - if hasattr(self.__bridge_params, 'perform'): - string_bridge_params = self.__bridge_params.perform(context) - elif isinstance(self.__bridge_params, list): - if hasattr(self.__bridge_params[0], 'perform'): - string_bridge_params = self.__bridge_params[0].perform(context) - else: - string_bridge_params = str(self.__bridge_params) - # Remove unnecessary symbols - simplified_bridge_params = string_bridge_params.translate( - {ord(i): None for i in '{} "\''} - ) - # Parse to dictionary - parsed_bridge_params = {} - if simplified_bridge_params: - bridge_params_pairs = simplified_bridge_params.split(',') - parsed_bridge_params = dict(pair.split(':') for pair in bridge_params_pairs) - use_composition_eval = perform_typed_substitution( context, self.__use_composition, bool ) @@ -209,7 +197,7 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: output='screen', respawn=perform_typed_substitution(context, self.__use_respawn, bool), respawn_delay=2.0, - parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], + parameters=self.__bridge_params, arguments=['--ros-args', '--log-level', self.__log_level], )) @@ -226,7 +214,7 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: plugin='ros_gz_bridge::RosGzBridge', name=self.__bridge_name, namespace=self.__namespace, - parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], + parameters=self.__bridge_params, extra_arguments=[{'use_intra_process_comms': True}], ), ], @@ -243,7 +231,7 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]: plugin='ros_gz_bridge::RosGzBridge', name=self.__bridge_name, namespace=self.__namespace, - parameters=[{'config_file': self.__config_file, **parsed_bridge_params}], + parameters=self.__bridge_params, extra_arguments=[{'use_intra_process_comms': True}], ), ], From 13b6640373a8415b473e4caeeef28ee87473d06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Ag=C3=BCero?= Date: Fri, 13 Dec 2024 15:07:45 +0100 Subject: [PATCH 6/6] Refactor air pressure demo (#632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor air pressure demo Signed-off-by: Carlos Agüero Co-authored-by: Addisu Z. Taddese Co-authored-by: Alejandro Hernández Cordero --- ros_gz_sim_demos/CMakeLists.txt | 6 ++ ros_gz_sim_demos/README.md | 2 +- ros_gz_sim_demos/config/air_pressure.yaml | 6 ++ .../launch/air_pressure.launch.py | 63 ------------------- .../launch/air_pressure.launch.xml | 16 +++++ 5 files changed, 29 insertions(+), 64 deletions(-) create mode 100644 ros_gz_sim_demos/config/air_pressure.yaml delete mode 100644 ros_gz_sim_demos/launch/air_pressure.launch.py create mode 100644 ros_gz_sim_demos/launch/air_pressure.launch.xml diff --git a/ros_gz_sim_demos/CMakeLists.txt b/ros_gz_sim_demos/CMakeLists.txt index 8e0f580a..9b69104b 100644 --- a/ros_gz_sim_demos/CMakeLists.txt +++ b/ros_gz_sim_demos/CMakeLists.txt @@ -9,6 +9,12 @@ if(BUILD_TESTING) ament_lint_auto_find_test_dependencies() endif() +install( + DIRECTORY + config/ + DESTINATION share/${PROJECT_NAME}/config +) + install( DIRECTORY launch/ diff --git a/ros_gz_sim_demos/README.md b/ros_gz_sim_demos/README.md index c404a4dc..62c670f9 100644 --- a/ros_gz_sim_demos/README.md +++ b/ros_gz_sim_demos/README.md @@ -14,7 +14,7 @@ There's a convenient launch file, try for example: Publishes fluid pressure readings. - ros2 launch ros_gz_sim_demos air_pressure.launch.py + ros2 launch ros_gz_sim_demos air_pressure.launch.xml This demo also shows the use of custom QoS parameters. The sensor data is published as as "best-effort", so trying to subscribe to "reliable" data won't diff --git a/ros_gz_sim_demos/config/air_pressure.yaml b/ros_gz_sim_demos/config/air_pressure.yaml new file mode 100644 index 00000000..36ad5f67 --- /dev/null +++ b/ros_gz_sim_demos/config/air_pressure.yaml @@ -0,0 +1,6 @@ +# Air pressure bridge configuration. +- topic_name: "air_pressure" + ros_type_name: "sensor_msgs/msg/FluidPressure" + gz_type_name: "gz.msgs.FluidPressure" + lazy: true + direction: GZ_TO_ROS diff --git a/ros_gz_sim_demos/launch/air_pressure.launch.py b/ros_gz_sim_demos/launch/air_pressure.launch.py deleted file mode 100644 index 2a422b34..00000000 --- a/ros_gz_sim_demos/launch/air_pressure.launch.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2019 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. - -"""Launch Gazebo Sim with command line arguments.""" - -import os - -from ament_index_python.packages import get_package_share_directory - -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument -from launch.actions import IncludeLaunchDescription -from launch.conditions import IfCondition -from launch.launch_description_sources import PythonLaunchDescriptionSource -from launch.substitutions import LaunchConfiguration - -from launch_ros.actions import Node - - -def generate_launch_description(): - - pkg_ros_gz_sim = get_package_share_directory('ros_gz_sim') - - # Bridge - bridge = Node( - package='ros_gz_bridge', - executable='parameter_bridge', - arguments=['/air_pressure@sensor_msgs/msg/FluidPressure@gz.msgs.FluidPressure'], - parameters=[{'qos_overrides./air_pressure.publisher.reliability': 'best_effort'}], - output='screen' - ) - - gz_sim = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(pkg_ros_gz_sim, 'launch', 'gz_sim.launch.py')), - launch_arguments={'gz_args': '-r sensors.sdf'}.items(), - ) - - # RQt - rqt = Node( - package='rqt_topic', - executable='rqt_topic', - arguments=['-t'], - condition=IfCondition(LaunchConfiguration('rqt')) - ) - return LaunchDescription([ - gz_sim, - DeclareLaunchArgument('rqt', default_value='true', - description='Open RQt.'), - bridge, - rqt - ]) diff --git a/ros_gz_sim_demos/launch/air_pressure.launch.xml b/ros_gz_sim_demos/launch/air_pressure.launch.xml new file mode 100644 index 00000000..9fd33397 --- /dev/null +++ b/ros_gz_sim_demos/launch/air_pressure.launch.xml @@ -0,0 +1,16 @@ + + + + + + + + + +