ROS Resources: Documentation | Support | Discussion Forum | Index | Service Status | ros @ Robotics Stack Exchange
Ask Your Question
2

ROS2 launch file: how to convert LaunchArgument to string

asked 2022-02-17 16:30:11 -0500

AndyZe gravatar image

updated 2022-02-17 16:36:08 -0500

I want to convert either a LaunchConfiguration or a LaunchArgument to a string so I can use it in a filepath later. How to do it?

Example:

support_package = LaunchConfiguration("support_package")

declared_arguments = []

declared_arguments.append(
    DeclareLaunchArgument(
        "support_package",
        description="Name of the support package",
        choices=["abb_irb_1200_support"],
    )
)

robot_description_config = xacro.process_file(
    os.path.join(
        get_package_share_directory(support_package),  # FAILURE AT THIS LINE
        "urdf",
        "irb1200_5_90.xacro",
    )
)

The failure is:

Caught exception when trying to load file of format [py]: expected string or bytes-like object

I'm not much of a Python programmer, but an str() function would be great.

Related question: https://answers.ros.org/question/3407...

edit retag flag offensive close merge delete

2 Answers

Sort by ยป oldest newest most voted
4

answered 2022-02-23 15:19:26 -0500

ChuiV gravatar image

Unfortunately, an str() method would not be that easy. The LaunchConfiguration is a Substitutible object, and doesn't derive from str You're going to need to use the launch.actions.OpaqueFunction action as it will give you a LaunchContext object from which you can perform substitutions:

from pathlib import Path
from launch import LaunchDescription, LaunchContext, LaunchService
from launch.actions import OpaqueFunction, DeclareLaunchArgument, ExecuteProcess
from launch.substitutions import LaunchConfiguration


robot_description_path = Path('destination.urdf')


def render_xacro(context: LaunchContext, support_package):
    support_package_str = context.perform_substitution(support_package)
    # render xacro... just dumping the support_package value in there for example.
    robot_description_config = support_package_str
    robot_description_path.write_text(robot_description_config)
    print(f'wrote robot description to {robot_description_path}')


def generate_launch_description():
    return LaunchDescription([
        DeclareLaunchArgument('support_package', default_value='hello'),

        OpaqueFunction(function=render_xacro, args=[LaunchConfiguration('support_package')]),
        ExecuteProcess(cmd=['cat', str(robot_description_path)],
                       output='screen'),
    ])


if __name__ == '__main__':
    ls = LaunchService()
    ls.include_launch_description(generate_launch_description())
    ls.run()

which outputs:

[INFO] [launch]: Default logging verbosity is set to INFO
wrote robot description to destination.urdf
[INFO] [cat-1]: process started with pid [31440]
[INFO] [cat-1]: process has finished cleanly [pid 31440]
[cat-1] hello
edit flag offensive delete link more

Comments

It seems like you are onto something and I appreciate it! But, something similar to this still does not work in my launch file. Is it b/c I don't have a main? Why so difficult? @William

support_package_path = Path('')

def load_srdf(context: LaunchContext, support_package):
    # Convert to string
    support_package_str = context.perform_substitution(support_package)
    support_package_path.write_text(support_package_str)

def generate_launch_description():
    DeclareLaunchArgument(
        "moveit_config_package",
        default_value=["abb_irb1200_5_90_moveit_config"]
    )
    moveit_config_package = LaunchConfiguration("moveit_config_package")
    # Get string path to the SRDF
    OpaqueFunction(function=load_srdf, args=[moveit_config_package])
    print(str(support_package_path))

Prints a dot b/c the path was not evaluated:

. Caught exception when trying to load file of format [py]: '.' is not a valid package

AndyZe gravatar image AndyZe  ( 2022-03-16 10:17:46 -0500 )edit
3

You can think of ros2 launch in 2 steps: Description, and Execution (maybe there's better names, but those names make sense to me).

In the description step, Launch will accumulate a list of everything it needs to do, but doesn't actually do anything.

In the Execution step, it actually runs everything from the description.

So by the time the Description step finishes, it just knows that there is a launch Argument ("moveit_config_package"), and an OpaqueFunction(which isn't run yet at this point.) Also note at this point, your print(str(support_package_path)) has been run, and we haven't written the support package string into that file yet!

After the Description step, we finally start to execute stuff. Now we fetch the value of the launch argument and write the value to the file.

The error you listed above seems to be related to how you're calling ...(more)

ChuiV gravatar image ChuiV  ( 2022-03-16 10:29:50 -0500 )edit
2

Thanks, that's a brilliant response. I think I will copy what's done in the UR package. https://github.com/UniversalRobots/Un...

AndyZe gravatar image AndyZe  ( 2022-03-16 10:34:26 -0500 )edit

Sounds like @ChuiV already helped you, and he's 100% right. I said something similar in a comment on the other question: https://answers.ros.org/question/3407...

Sorry it's not intuitive to you, but there are reasons it is set up like this.

William gravatar image William  ( 2022-03-21 11:31:10 -0500 )edit
0

answered 2023-07-14 10:34:38 -0500

chives_onion gravatar image

From @AndyZe 's comment, I took inspiration from the way the UR package defines nodes within an OpaqueFunction. My desire was to pass launch arguments to help define file names assembled as strings for launching gazebo worlds. I convert the LaunchConfiguration object to a string of the value it held with context.perform_substitution, as shown in the first three lines of the launch_setup function:

from launch import LaunchDescription, conditions
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from ament_index_python.packages import get_package_share_directory
from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument, OpaqueFunction
from launch.launch_description_sources import AnyLaunchDescriptionSource, PythonLaunchDescriptionSource
import os


def launch_setup(context, *args, **kwargs):
    arg_robot_name = context.perform_substitution(LaunchConfiguration('robot_name'))
    arg_world_name = context.perform_substitution(LaunchConfiguration('world_name'))
    arg_simulated = context.perform_substitution(LaunchConfiguration('simulated'))



    launch_world = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            os.path.join(get_package_share_directory(
                'ros_gz_sim'), 'launch/gz_sim.launch.py')
        ),
        launch_arguments={'gz_args': '-r {}'.format(arg_world_name)}.items()
    )

    launch_robot = IncludeLaunchDescription(
        AnyLaunchDescriptionSource(
            PathJoinSubstitution(
                [get_package_share_directory('my_gazebo'), 'launch', '{}_gazebo.launch.xml'.format(arg_robot_name)]
            )
        ),
        condition=conditions.IfCondition(LaunchConfiguration('spawn_robot')),
        launch_arguments={'simulated': '{}'.format(arg_simulated)}.items()
    )

    nodes_to_start = [launch_world, launch_robot]
    return nodes_to_start


def generate_launch_description():
    declared_arguments = []
    declared_arguments.append(
        DeclareLaunchArgument(name='simulated', default_value='true',
                                  description='if time is simulated')
    )
    declared_arguments.append(
        DeclareLaunchArgument(name='world_name', default_value='empty.sdf')        
    )
    declared_arguments.append(
            DeclareLaunchArgument(name='spawn_robot', default_value='true',
                                  description='if agent should be generated, set to false to spawn only world')        
    )
    declared_arguments.append(
            DeclareLaunchArgument(name='robot_name', default_value='my',
                                  description='robot name prefix used to call specific launch file; used with spawn_robot')        
    )            
    return LaunchDescription(declared_arguments + [OpaqueFunction(function=launch_setup)])

I might be missing some helpful methods included in the launch package, but this worked for me.

edit flag offensive delete link more

Comments

1

I do also want to point out that this particular example doesn't actually need to use the OpaqueFunction, and can be re-factored to about half the size. In general, you can pass a tuple of strings and substitution types as another substitution and launch will concatenate them together: https://answers.ros.org/question/4032... In short, '-r {}'.format(arg_world_name) can be changed to ('-r', LaunchConfiguration('world_name')), '{}'.format(arg_simulated) can be replaced with LaunchConfiguration('simulated'). The PathJoinSubstitution was giving me an issue, but that whole thing can be replaced with (get_package_share_directory('my_gazebo'), '/launch/', LaunchConfiguration('robot_name'), '_gazebo.launch.xml').

ChuiV gravatar image ChuiV  ( 2023-07-25 09:28:18 -0500 )edit

Question Tools

1 follower

Stats

Asked: 2022-02-17 16:30:11 -0500

Seen: 5,244 times

Last updated: Jul 14 '23