├── .pre-commit-config.yaml ├── .yamllint.yaml ├── LICENSE ├── README.md ├── donatello ├── config │ ├── alt_params.yaml │ ├── params.yaml │ └── sub_params.yaml ├── donatello │ ├── __init__.py │ ├── donatello_node.py │ ├── five_seconds.py │ └── manual_launch.py ├── launch │ ├── 01-single.launch.py │ ├── 02-param.launch.py │ ├── 03-params.launch.py │ ├── 04-command-params.launch.py │ ├── 05-arg.launch.py │ ├── 06a-substitutions.launch.py │ ├── 06b-substitutions.launch.py │ ├── 06c-substitutions.launch.py │ ├── 07-inclusive.launch.py │ ├── 08-conditional.launch.py │ ├── 09-dynamic-filename.launch.py │ ├── 10a-required.launch.py │ ├── 10b-required.launch.py │ └── 12-recursion.launch.py ├── package.xml ├── resource │ └── donatello ├── setup.cfg └── setup.py ├── raphael ├── CMakeLists.txt ├── config │ ├── alt_params.yaml │ ├── params.yaml │ └── sub_params.yaml ├── launch │ ├── 01-single.launch │ ├── 02-param.launch │ ├── 03-params.launch │ ├── 04-command-params.launch │ ├── 05-arg.launch │ ├── 06-substitutions.launch │ ├── 07-inclusive.launch │ ├── 08-conditional.launch │ ├── 09-dynamic-filename.launch │ ├── 10-required.launch │ └── 12-recursion.launch ├── package.xml └── scripts │ ├── five_seconds │ ├── manual_launch │ └── raphael_node ├── setup.cfg └── sync.py /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | hooks: 4 | - id: end-of-file-fixer 5 | - id: trailing-whitespace 6 | - id: check-merge-conflict 7 | - id: mixed-line-ending 8 | - id: check-executables-have-shebangs 9 | - id: check-shebang-scripts-are-executable 10 | - id: detect-private-key 11 | - id: destroyed-symlinks 12 | - id: check-symlinks 13 | - id: check-case-conflict 14 | - id: check-ast 15 | - id: double-quote-string-fixer 16 | - id: requirements-txt-fixer 17 | - id: check-xml 18 | - id: check-yaml 19 | rev: v5.0.0 20 | - repo: https://github.com/codespell-project/codespell 21 | hooks: 22 | - id: codespell 23 | args: 24 | - --write-changes 25 | rev: v2.4.1 26 | - repo: https://github.com/hhatto/autopep8 27 | hooks: 28 | - id: autopep8 29 | rev: v2.3.2 30 | - repo: https://github.com/PyCQA/flake8 31 | hooks: 32 | - id: flake8 33 | rev: 7.2.0 34 | - repo: https://github.com/adrienverge/yamllint 35 | hooks: 36 | - id: yamllint 37 | args: 38 | - --format 39 | - parsable 40 | - --strict 41 | rev: v1.37.0 42 | - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt 43 | hooks: 44 | - id: yamlfmt 45 | args: 46 | - --width 47 | - '120' 48 | - --implicit_start 49 | - --implicit_end 50 | - --mapping 51 | - '2' 52 | - --sequence 53 | - '2' 54 | - --offset 55 | - '0' 56 | rev: 0.2.3 57 | ci: 58 | autoupdate_schedule: quarterly 59 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | yaml-files: 2 | - '*.yaml' 3 | - '*.yml' 4 | rules: 5 | anchors: enable 6 | braces: enable 7 | brackets: enable 8 | colons: enable 9 | commas: enable 10 | comments: 11 | level: warning 12 | comments-indentation: 13 | level: warning 14 | document-end: disable 15 | document-start: disable 16 | empty-lines: enable 17 | empty-values: disable 18 | float-values: disable 19 | hyphens: enable 20 | indentation: disable 21 | key-duplicates: enable 22 | key-ordering: disable 23 | line-length: 24 | max: 120 25 | new-line-at-end-of-file: enable 26 | new-lines: enable 27 | octal-values: disable 28 | quoted-strings: disable 29 | trailing-spaces: enable 30 | truthy: 31 | level: warning 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, MetroRobots 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rosetta_launch 2 | A guide to understanding launch files in ROS 1 and ROS 2 3 | 4 | This repository contains two packages, named after adolescent genetic variants of turtles. 5 | * `raphael` is a ROS 1 package 6 | * `donatello` is a ROS 2 package 7 | 8 | Donatello is more technically advanced, but is sometimes harder to understand. 9 | 10 | ## 01 - Launch a Single Node 11 | Three things are required for the minimal example: 12 | * Name - Used for node name, regardless of executable name 13 | * Package Name - Name of the ROS Package 14 | * Type - Name of the executable in the ROS package 15 | 16 | ### ROS 1 17 | 18 | [source](raphael/launch/01-single.launch) 19 | ```xml 20 | 21 | 22 | 23 | ``` 24 | 25 | ### ROS 2 26 | 27 | [source](donatello/launch/01-single.launch.py) 28 | ```python 29 | from launch import LaunchDescription 30 | from launch_ros.actions import Node 31 | 32 | 33 | def generate_launch_description(): 34 | return LaunchDescription([ 35 | Node(name='does_machines', package='donatello', executable='donatello_node'), 36 | ]) 37 | ``` 38 | You can choose whether you want to use the `import X` or `from X import Y` syntax. This guide uses the latter for the `launch` and `launch_ros` imports. 39 | 40 | 41 | There are actually several different "styles" for adding nodes to a launch description. 42 | 1. You can construct the nodes directly in the list passed to the `LaunchDescription` constructor (as above) 43 | 2. You can assign the nodes to a variable and then put them in the `LaunchDescription` constructor at the end. 44 | ```python 45 | don_node = Node(name='does_machines', package='donatello', executable='donatello_node') 46 | return LaunchDescription([don_node]) 47 | ``` 48 | 3. You can construct the `LaunchDescription` first with no nodes, and then add them individually. 49 | ```python 50 | ld = LaunchDescription() 51 | ld.add_action(Node(name='does_machines', package='donatello', executable='donatello_node')) 52 | return ld 53 | ``` 54 | 55 | 56 | ## 02 - Set a parameter directly 57 | If we want to specify an exact value for a ROS parameter inside of the launch file, we need the parameter name and the parameter value. 58 | 59 | ### ROS 1 60 | [source](raphael/launch/02-param.launch) 61 | ```xml 62 | 63 | 64 | 65 | 66 | 67 | 68 | [leo, don, mike] 69 | 70 | weapon: sai 71 | 72 | 73 | 74 | ``` 75 | 76 | * We can set a global parameter in ROS 1 (e.g. `use_sim_time`) 77 | * The other parameters are set within the node, and thus will be given the node's namespace, i.e. the full parameter will be `/cool_but_rude/pizza`. 78 | * For simple types (`str|int|double|bool`), you add the `param` xml element with the name and value (e.g. `pizza`). The type of the parameter is automatically inferred. 79 | * For more complex types, you can specify the type as `yaml` and the value will be interpreted as yaml, (e.g. `brothers`) 80 | * The contents of a `rosparam` xml element can also be interpreted as yaml. You can specify the name as an attribute (e.g. `coworkers`) or specify a whole dictionary of names and values (e.g. `weapon`) 81 | 82 | ### ROS 2 83 | [source](donatello/launch/02-param.launch.py) 84 | ```python 85 | from launch import LaunchDescription 86 | from launch_ros.actions import Node 87 | 88 | 89 | def generate_launch_description(): 90 | return LaunchDescription([ 91 | Node(name='does_machines', 92 | package='donatello', 93 | executable='donatello_node', 94 | parameters=[{'pizza': 'mushrooms', 95 | 'brothers': ['leo', 'mike', 'raph']}]), 96 | ]) 97 | ``` 98 | * You cannot set global parameters in ROS 2. 99 | * The parameters are specified using the `parameters` argument within the Node, which takes a Python list. 100 | * To specify the values directly, we put them in a dictionary within the Python list. 101 | * Since the parameters are within Python code, the type is determined by their Python type. 102 | 103 | 104 | ## 03 - Load Parameters from YAML file 105 | 106 | To load parameters from a file, we need the full path to the yaml file. 107 | 108 | ### ROS 1 109 | [source](raphael/launch/03-params.launch) 110 | ```xml 111 | 112 | 113 | 114 | 115 | 116 | ``` 117 | * In the `rosparam` element, we now use `command="load"` and specify the full path with the `file` attribute. 118 | * To automatically get the full path to a file within a ROS package, we can use the special `$(find package_name)` syntax. 119 | 120 | 121 | ### ROS 2 122 | [source](donatello/launch/03-params.launch.py) 123 | ```python 124 | from launch import LaunchDescription 125 | from launch.substitutions import PathJoinSubstitution 126 | from launch_ros.actions import Node 127 | from launch_ros.substitutions import FindPackageShare 128 | 129 | 130 | def generate_launch_description(): 131 | return LaunchDescription([ 132 | Node(name='does_machines', 133 | package='donatello', 134 | executable='donatello_node', 135 | parameters=[PathJoinSubstitution(FindPackageShare('donatello'), 'config', 'params.yaml')]), 136 | ]) 137 | ``` 138 | 139 | * In the `parameters` argument of the Node, we previously specified a dictionary to give values to the parameters individually. To load the parameters from a file, we must specify the full path to the file, which we can do in three ways. 140 | 1. **FindPackageShare + PathJoinSubstitution** (as shown above) - Instead of a dictionary, we now specify the path as a combination of the share directory and the other directory components all joined together. (See more information on Substitutions in a section below) 141 | 2. **String** - You can pass in a string representing the path. This is often combined with `ament_index_python.packages.get_package_share_directory` and `os.path.join`, i.e. 142 | ```python 143 | parameters=[ 144 | os.path.join(get_package_share_directory('donatello'), 145 | 'config/params.yaml' 146 | ) 147 | ] 148 | ``` 149 | 3. **Path converted to a String** - Working with `pathlib` is often easier than using `os.path.join`. You can get a `pathlib.Path` of the share folder using `ament_index_python.package.get_package_share_path` i.e. 150 | ```python 151 | parameters=[ 152 | str(get_package_share_path('donatello') / 'config/params.yaml') 153 | ] 154 | ``` 155 | 156 | * While the latter two ways of specifying the path are common, if you are combining with other substitutions, the first option is the easiest to work with. 157 | 158 | 159 | ## 04 - Load Parameters from a Command 160 | Sometimes you will want to set parameters based on the results of running a command. This is very commonly seen when running `xacro` on your robot model and loading it in as a parameter. 161 | 162 | ### ROS 1 163 | [source](raphael/launch/04-command-params.launch) 164 | ```xml 165 | 166 | 167 | 168 | 169 | ``` 170 | 171 | ### ROS 2 172 | [source](donatello/launch/04-command-params.launch.py) 173 | ```python 174 | from launch import LaunchDescription 175 | from launch.substitutions import Command, PathJoinSubstitution 176 | from launch_ros.actions import Node 177 | from launch_ros.parameter_descriptions import ParameterValue 178 | from launch_ros.substitutions import FindPackageShare 179 | 180 | 181 | def generate_launch_description(): 182 | robot_description = ParameterValue( 183 | Command(['xacro ', PathJoinSubstitution(FindPackageShare('urdf_tutorial'), 'urdf', '01-myfirst.urdf')]), 184 | value_type=str) 185 | 186 | return LaunchDescription([ 187 | Node(package='robot_state_publisher', 188 | executable='robot_state_publisher', 189 | parameters=[{'robot_description': robot_description}]) 190 | ]) 191 | ``` 192 | 193 | * Note: In many cases, you can get away with not wrapping the `Command` in a `ParameterValue` object, but then the launch system will try to guess the value type, and if there happens to be a colon (`:`) in a string you're trying to load, [it will try to interpret it as YAML](https://github.com/ros2/launch_ros/issues/214) 194 | 195 | ## 05 - Set a Command Line Argument 196 | There are two different steps for using command line arguments: 197 | * Declaring the argument and its default value 198 | * Using the value 199 | 200 | ### ROS 1 201 | [source](raphael/launch/05-arg.launch) 202 | ```xml 203 | 204 | 205 | 206 | 207 | 208 | 209 | ``` 210 | 211 | * The argument is declared using the `` element. 212 | * The value can be using the dollar substitution syntax `$(arg pizza_type)` 213 | * When launching, you can see the value of the parameter printed to the terminal. 214 | ``` 215 | PARAMETERS 216 | * /cool_but_rude/pizza: pepperoni 217 | ``` 218 | * If we run `roslaunch raphael 05-arg.launch`, the parameter is set to pepperoni by default. 219 | * You can specify the value you want on the command line with `arg_name:=arg_value`, i.e. 220 | ```bash 221 | roslaunch raphael 05-arg.launch pizza_type:=meatball 222 | ``` 223 | 224 | ### ROS 2 225 | [source](donatello/launch/05-arg.launch.py) 226 | ```python 227 | from launch import LaunchDescription 228 | from launch.actions import DeclareLaunchArgument 229 | from launch.substitutions import LaunchConfiguration 230 | from launch_ros.actions import Node 231 | 232 | 233 | def generate_launch_description(): 234 | return LaunchDescription([ 235 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 236 | Node(name='does_machines', 237 | package='donatello', 238 | executable='donatello_node', 239 | parameters=[{'pizza': LaunchConfiguration('pizza_type')}]), 240 | ]) 241 | ``` 242 | * The argument is declared using the `DeclareLaunchArgument` action, which must be included in the `LaunchDescription` 243 | * The value can be used with the `LaunchConfiguration` object. 244 | * If we run `ros2 launch donatello 05-arg.launch.py` the parameter is set to `mushrooms` by default. 245 | * You can specify the value you want on the command line the same way as ROS 1, i.e. 246 | ```bash 247 | ros2 launch donatello 05-arg.launch.py pizza_type:=meatball 248 | ``` 249 | 250 | ## 06 - Using Substitutions Everywhere 251 | In the previous example, we were able to dynamically change our launch file by using substitutions, i.e. dynamically replacing the value of a variable. We also used `FindPackageShare` to dynamically find the path to a file. There are actually a lot of different things you can substitute in. 252 | 253 | | Name | ROS 1 command | ROS 2 command | ROS2 Object | Note | 254 | |-------------------------------|---------------|------------------------|---------------------|------| 255 | | Launch Argument | arg | var | LaunchConfiguration | [1] | 256 | | Anonymous ID Generation | anon | anon | AnonName | | 257 | | Environment Variable | env | env | EnvironmentVariable | | 258 | | Optional Environment Variable | optenv | env | EnvironmentVariable | [2] | 259 | | Command Execution | | command | Command | [3] | 260 | | Current Directory | dirname | dirname | ThisLaunchFileDir | | 261 | | ROS Package Location | find | | | [4] | 262 | | Current Launch File Path | | filename | ThisLaunchFile | [4] | 263 | | Executable Path | | find-exec | FindExecutable | [4] | 264 | | ROS Executable | | exec-in-pkg | ExecutableInPackage | [4] | 265 | | ROS Package Share Path | | find-pkg-share | FindPackageShare | [4] | 266 | | Expression Evaluation | eval | eval | PythonExpression | [5] | 267 | 268 | * [1] Only one with inexplicable different command in ROS 1 and 2 269 | * [2] To use `optenv` in ROS 2, just add the default value to the command as the second argument. 270 | * [3] There is not a general way to load the results of an arbitrary command in ROS 1, but you can load the value into a ROS Parameter (See section 04 above) 271 | * [4] ROS 2 if much finickier about where files actually are. There is no general folder that covers everything, thus the `find` command doesn't make sense. Instead its split up into other commands. 272 | * [5] In ROS 1, `eval` cannot be used in YAML files. 273 | 274 | ### ROS 1 275 | [source](raphael/launch/06-substitutions.launch) 276 | ```xml 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | ``` 287 | 288 | [parameter file source](raphael/config/sub_params.yaml) 289 | * [Additional Documentation](http://wiki.ros.org/roslaunch/XML/#substitution_args) 290 | * Just like with args, we can still use the dollar substitution syntax in any string in the XML, e.g. `$(COMMAND ARG1 ARG2...)` 291 | * We can also put dollar substitutions in yaml files, as long as we set `subst_value="true"` 292 | * It may be relatively obvious, but the substitution replaces the dollar expression with the evaluated value, and then the yaml is processed as normal. 293 | * Example:`version: ROS $(env ROS_VERSION)` sets `version` to `ROS 1` 294 | * You can use launch arguments within `eval` substitutions, but with a slightly different syntax. 295 | 296 | ### ROS 2 297 | There are multiple ways to do substitutions in ROS 2 Python launch files. 298 | 299 | [source](donatello/launch/06a-substitutions.launch.py) 300 | ```python 301 | from launch import LaunchDescription 302 | from launch.actions import DeclareLaunchArgument 303 | from launch.substitutions import AnonName, Command, EnvironmentVariable, FindExecutable, LaunchConfiguration 304 | from launch.substitutions import PythonExpression, ThisLaunchFile, ThisLaunchFileDir 305 | from launch_ros.actions import Node 306 | 307 | 308 | def generate_launch_description(): 309 | object_parameters = { 310 | 'pizza': LaunchConfiguration('pizza_type'), 311 | 'anonymous_name': AnonName('leo'), 312 | 'favorite_brother': EnvironmentVariable('BROTHER_NAME', default_value='mikey'), 313 | 'filename': ThisLaunchFile(), 314 | 'directory': ThisLaunchFileDir(), 315 | 'list_exec': FindExecutable(name='ls'), 316 | 'list_output': Command('ls'), 317 | 'version': ['ROS ', EnvironmentVariable('ROS_VERSION')], 318 | 'circumference': PythonExpression(['2.*3.1415*', LaunchConfiguration('radius')]), 319 | } 320 | 321 | return LaunchDescription([ 322 | DeclareLaunchArgument('radius', default_value='1.5'), 323 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 324 | Node(name='does_machines', 325 | package='donatello', 326 | executable='donatello_node', 327 | parameters=[object_parameters]), 328 | ]) 329 | ``` 330 | 331 | * The most Python-y way to do substitutions is by using the `launch.substitutions` classes, as seen above. 332 | * When evaluating the substitutions, lists of strings/substitutions are **concatenated**. For example, look at `version` above. 333 | * The parameter value is a list containing one string and one substitution. The `EnvironmentVariable` is evaluated, resulting in the list now being `['ROS ', 2]`, which are then combined so the final value is `'ROS 2'` 334 | * The same principles apply to the substitution within a substitution in `circumference`. 335 | 336 | [source](donatello/launch/06b-substitutions.launch.py) 337 | ```python 338 | from launch import LaunchDescription 339 | from launch.actions import DeclareLaunchArgument 340 | from launch_ros.actions import Node 341 | from launch.frontend.parse_substitution import parse_substitution 342 | 343 | 344 | def generate_launch_description(): 345 | text_params = { 346 | 'pizza': parse_substitution('$(var pizza_type)'), 347 | 'anonymous_name': parse_substitution('$(anon leo)'), 348 | 'favorite_brother': parse_substitution('$(env BROTHER_NAME mikey)'), 349 | 'filename': parse_substitution('$(filename)'), 350 | 'directory': parse_substitution('$(dirname)'), 351 | 'list_exec': parse_substitution('$(find-exec ls)'), 352 | 'list_output': parse_substitution('$(command ls)'), 353 | 'version': parse_substitution('ROS $(env ROS_VERSION)'), 354 | 'circumference': parse_substitution('$(eval 2.*3.1415*$(var radius))'), 355 | } 356 | 357 | return LaunchDescription([ 358 | DeclareLaunchArgument('radius', default_value='1.5'), 359 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 360 | Node(name='does_machines', 361 | package='donatello', 362 | executable='donatello_node', 363 | parameters=[text_params]), 364 | ]) 365 | ``` 366 | 367 | * You can also use the dollar substitution syntax as in ROS 1. 368 | * Using the `parse_substitution` method will result in lists and objects as in the previous example. 369 | 370 | [source](donatello/launch/06c-substitutions.launch.py) 371 | ```python 372 | from launch import LaunchDescription 373 | from launch.actions import DeclareLaunchArgument 374 | from launch.substitutions import PathJoinSubstitution 375 | from launch_ros.actions import Node 376 | from launch_ros.parameter_descriptions import ParameterFile 377 | from launch_ros.substitutions import FindPackageShare 378 | 379 | 380 | def generate_launch_description(): 381 | file_parameters = ParameterFile( 382 | param_file=PathJoinSubstitution(FindPackageShare('donatello'), 'config', 'sub_params.yaml'), 383 | allow_substs=True 384 | ) 385 | 386 | return LaunchDescription([ 387 | DeclareLaunchArgument('radius', default_value='1.5'), 388 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 389 | Node(name='does_machines', 390 | package='donatello', 391 | executable='donatello_node', 392 | parameters=[file_parameters]), 393 | ]) 394 | ``` 395 | 396 | [parameter file source](donatello/config/sub_params.yaml) 397 | * In ROS distributions [Galactic](https://docs.ros.org/en/galactic/Releases/Release-Galactic-Geochelone.html#use-launch-substitutions-in-parameter-files) and newer, you can also use the substitutions in YAML files as well, as long as `allow_substs` is set to True. 398 | 399 | ## 07 - Include Another Launch 400 | 401 | In more complex systems, it is often useful to have launch files that include other launch files, often including specific values for the launch arguments. 402 | 403 | ### ROS 1 404 | [source](raphael/launch/07-inclusive.launch) 405 | ```xml 406 | 407 | 408 | 409 | 410 | 411 | ``` 412 | ### ROS 2 413 | [source](donatello/launch/07-inclusive.launch.py) 414 | ```python 415 | from launch import LaunchDescription 416 | from launch.actions import IncludeLaunchDescription 417 | from launch.substitutions import PathJoinSubstitution 418 | from launch_ros.substitutions import FindPackageShare 419 | 420 | 421 | def generate_launch_description(): 422 | return LaunchDescription([ 423 | IncludeLaunchDescription(PathJoinSubstitution(FindPackageShare('donatello'), 'launch', '05-arg.launch.py'), 424 | launch_arguments={'pizza_type': 'peppers'}.items()), 425 | ]) 426 | ``` 427 | 428 | * The first argument to the `IncludeLaunchDescription` object in this case is a string representing the path to the launch file to include, which automatically gets converted to a [`launch.launch_description_source.PythonLaunchDescriptionSource`](https://github.com/ros2/launch/blob/b129eb65c9f03980c724b17200236290fa797816/launch/launch/launch_description_sources/python_launch_description_source.py) class, which is a subclass of [`launch.LaunchDescriptionSource`](https://github.com/ros2/launch/blob/b129eb65c9f03980c724b17200236290fa797816/launch/launch/launch_description_source.py#L30) 429 | * There are cases where you would want to construct the `PythonLaunchDescriptionSource` explicitly, or include a different type of `LaunchDescriptionSource`, but for now the string with the full path to the Python is the most straightforward option. 430 | 431 | ## 08 - Conditionally Include 432 | In this example, we combine the substitution functionality and the ability to include another launch file from the last two examples to evaluate an argument to determine which launch file to include. We're going to include both cases, i.e. including if the argument is true and if the argument is false, but you can do just one. 433 | 434 | ### ROS 1 435 | [source](raphael/launch/08-conditional.launch) 436 | ```xml 437 | 438 | 439 | 440 | 441 | 442 | ``` 443 | 444 | * [Additional Documentation](http://wiki.ros.org/roslaunch/XML/#if_and_unless_attributes) 445 | * If we run `roslaunch raphael 08-conditional.launch`, it will include `01-single.launch` (and no parameters are loaded). 446 | * To get the same result, we can also explicitly set `use_number_one` to `true` or `1`. 447 | * If we run `roslaunch raphael 08-conditional.launch use_number_one:=false` it will include `02-param.launch` (with the parameters set in that launch file) 448 | * We could have also set `use_number_one:=0`, but any other values besides 0, 1, true, false will result in `Value error: X is not a 'bool' type` 449 | * `if/unless` can be used on individual nodes as well. 450 | 451 | ### ROS 2 452 | [source](donatello/launch/08-conditional.launch.py) 453 | ```python 454 | from launch import LaunchDescription 455 | from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription 456 | from launch.conditions import IfCondition, UnlessCondition 457 | from launch.substitutions import LaunchConfiguration, PathJoinSubstitution 458 | from launch_ros.substitutions import FindPackageShare 459 | 460 | 461 | def generate_launch_description(): 462 | return LaunchDescription([ 463 | DeclareLaunchArgument('use_number_one', default_value='True'), 464 | IncludeLaunchDescription( 465 | PathJoinSubstitution(FindPackageShare('donatello'), 'launch', '01-single.launch.py'), 466 | condition=IfCondition(LaunchConfiguration('use_number_one')), 467 | ), 468 | IncludeLaunchDescription( 469 | PathJoinSubstitution(FindPackageShare('donatello'), 'launch', '02-param.launch.py'), 470 | condition=UnlessCondition(LaunchConfiguration('use_number_one')), 471 | ), 472 | ]) 473 | ``` 474 | * Same behaviors as in ROS 1: 475 | * `ros2 launch donatello 08-conditional.launch.py` 476 | * `ros2 launch donatello 08-conditional.launch.py use_number_one:=False` 477 | * Can raise `InvalidConditionExpressionError` if the argument is not a bool-ish type. 478 | * The `condition` parameter can be used on a wide array of Python launch objects, including individual nodes. 479 | 480 | 481 | ## 09 - Dynamic Filenames 482 | 483 | One other way to dynamically change the contents of a launch file by evaluating substitutions is by using substitutions to determine the filenames. 484 | 485 | ### ROS 1 486 | [source](raphael/launch/09-dynamic-filename.launch) 487 | ```xml 488 | 489 | 490 | 491 | 492 | 493 | 494 | ``` 495 | * Running `roslaunch raphael 09-dynamic-filename.launch` will load `params.yaml` by default. 496 | * Running `roslaunch raphael 09-dynamic-filename.launch config:=alt_params` will load `alt_params.yaml` 497 | * This method can also be used for changing the filename of an included launch file. 498 | 499 | ### ROS 2 500 | [source](donatello/launch/09-dynamic-filename.launch.py) 501 | ```python 502 | from launch import LaunchDescription 503 | from launch.actions import DeclareLaunchArgument 504 | from launch.substitutions import LaunchConfiguration, PathJoinSubstitution 505 | from launch_ros.actions import Node 506 | from launch_ros.substitutions import FindPackageShare 507 | 508 | 509 | def generate_launch_description(): 510 | dynamic_param_path = PathJoinSubstitution( 511 | FindPackageShare('donatello'), 512 | 'config', 513 | [LaunchConfiguration('config'), '.yaml'] 514 | ) 515 | 516 | return LaunchDescription([ 517 | DeclareLaunchArgument('config', default_value='params'), 518 | Node(package='donatello', 519 | executable='donatello_node', 520 | name='does_machines', 521 | parameters=[dynamic_param_path]), 522 | ]) 523 | ``` 524 | 525 | * Normally, (i.e. section 3 above) we pass a string of the parameters file into the list of parameters for the `Node`. 526 | * However, now we pass in a list (`dynamic_param_path`), which consists of a mix of strings and `launch.substitutions`, i.e. `LaunchConfiguration` 527 | * At runtime, the list will be evaluated and combined into one long string representing the path that will be loaded. 528 | 529 | ## 10 - Make A Node Required 530 | There are certain scenarios where you want to stop an entire launch file when a particular node is not running anymore. 531 | ### ROS 1 532 | [source](raphael/launch/10-required.launch) 533 | ```xml 534 | 535 | 536 | 537 | 538 | ``` 539 | * The node `five_seconds` will terminate after five seconds. 540 | * Because it is marked as `required`, the other nodes in the launch file (`cool_but_rude`) will also be terminated after five seconds. 541 | 542 | ### ROS 2 543 | The ROS 2 launch system adds a bit of flexibility to what to do when a node is shutdown. 544 | 545 | If you just want to replicate the behavior, you can add `on_exit=Shutdown()` to the Node. 546 | 547 | [source](donatello/launch/10a-required.launch.py) 548 | ```python 549 | from launch import LaunchDescription 550 | from launch.actions import Shutdown 551 | from launch_ros.actions import Node 552 | 553 | 554 | def generate_launch_description(): 555 | don_node = Node(name='does_machines', package='donatello', executable='donatello_node') 556 | five_node = Node(name='five_seconds', package='donatello', executable='five_seconds', on_exit=Shutdown()) 557 | return LaunchDescription([don_node, five_node]) 558 | ``` 559 | 560 | However, you can also get much more complex, and get the event handling to do things other than just shutting down. 561 | 562 | [source](donatello/launch/10b-required.launch.py) 563 | ```python 564 | from launch import LaunchDescription 565 | from launch.actions import RegisterEventHandler, LogInfo, EmitEvent, Shutdown 566 | from launch.event_handlers import OnProcessExit 567 | from launch_ros.actions import Node 568 | 569 | 570 | def generate_launch_description(): 571 | don_node = Node(name='does_machines', package='donatello', executable='donatello_node') 572 | five_node = Node(name='five_seconds', package='donatello', executable='five_seconds') 573 | handler = RegisterEventHandler( 574 | event_handler=OnProcessExit( 575 | target_action=five_node, 576 | on_exit=[ 577 | LogInfo( 578 | msg='Five node exited; tearing down entire system.'), 579 | EmitEvent( 580 | event=Shutdown())])) 581 | 582 | return LaunchDescription([don_node, five_node, handler]) 583 | ``` 584 | * Thanks to [Alex Moriarty](https://github.com/moriarty) for pointing me to the first method and [The Ubuntu Blog](https://ubuntu.com/blog/ros2-launch-required-nodes) for the second. 585 | 586 | ## 11 - Launch from Code 587 | If you want to start a launch file from a script, you can sometimes use the `subprocess` Python library, but that can lead to problems with the paths. This is how you can launch a launch file in the ROS-y way. 588 | 589 | ### ROS 1 590 | [source](raphael/scripts/manual_launch) 591 | ```python 592 | #!/usr/bin/python3 593 | from catkin.find_in_workspaces import find_in_workspaces 594 | from roslaunch.parent import ROSLaunchParent 595 | from roslaunch import rlutil 596 | 597 | 598 | def main(): 599 | uuid = rlutil.get_or_generate_uuid(None, False) 600 | launch_path = find_in_workspaces(project='raphael', path='launch/05-arg.launch', first_match_only=True)[0] 601 | launch_with_args = (launch_path, ['pizza_type:=extra_cheese']) 602 | 603 | p = ROSLaunchParent(uuid, [launch_with_args]) 604 | p.start() 605 | p.spin() 606 | 607 | 608 | if __name__ == '__main__': 609 | main() 610 | ``` 611 | * This version starts `roscore` 612 | 613 | ### ROS 2 614 | [source](donatello/donatello/manual_launch.py) 615 | ```python 616 | from ros2launch.api import get_share_file_path_from_package, launch_a_launch_file 617 | 618 | 619 | def main(): 620 | path = get_share_file_path_from_package(package_name='donatello', file_name='05-arg.launch.py') 621 | launch_arguments = ['pizza_type:=extra_cheese'] 622 | 623 | launch_a_launch_file( 624 | launch_file_path=path, 625 | # Note: launch_file_arguments is required! 626 | launch_file_arguments=launch_arguments, 627 | ) 628 | 629 | 630 | if __name__ == '__main__': 631 | main() 632 | ``` 633 | 634 | ## 12 - Recursion 635 | As suggested by [Martin Pecka](https://discourse.ros.org/t/rosetta-launch-everything-i-know-about-ros-1-and-ros-2-launch-files/29648/5). This example will enable you to launch 10 of the same node. 636 | 637 | ### ROS 1 638 | [source](raphael/launch/12-recursion.launch) 639 | ```xml 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | ``` 648 | ### ROS 2 649 | [source](donatello/launch/12-recursion.launch.py) 650 | ```python 651 | from ament_index_python.packages import get_package_share_path 652 | import launch 653 | from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument 654 | from launch.conditions import IfCondition 655 | from launch.substitutions import LaunchConfiguration, PythonExpression 656 | import launch_ros.actions 657 | 658 | 659 | def generate_launch_description(): 660 | return launch.LaunchDescription([ 661 | DeclareLaunchArgument('N', default_value='10'), 662 | launch_ros.actions.Node(name=['donatello_node', LaunchConfiguration('N')], 663 | package='donatello', executable='donatello_node'), 664 | IncludeLaunchDescription(str(get_package_share_path('donatello') / 'launch/12-recursion.launch.py'), 665 | condition=IfCondition(PythonExpression([LaunchConfiguration('N'), '>1'])), 666 | launch_arguments={'N': PythonExpression([LaunchConfiguration('N'), '-1'])}.items()), 667 | ]) 668 | ``` 669 | 670 | # Other Links 671 | * ROS 1 672 | * [ROS Wiki](http://wiki.ros.org/roslaunch/XML) with description of each XML element. 673 | * [Source Code](https://github.com/ros/ros_comm/tree/noetic-devel/tools/roslaunch/src/roslaunch) 674 | * [Architecture](http://wiki.ros.org/roslaunch/Architecture) 675 | * ROS 2 676 | * [Architecture](https://github.com/ros2/launch/blob/rolling/launch/doc/source/architecture.rst) 677 | * Source code for [launch](https://github.com/ros2/launch/tree/rolling/launch/launch) and [launch_ros](https://github.com/ros2/launch_ros/tree/rolling/launch_ros/launch_ros) which are different for...reasons. 678 | -------------------------------------------------------------------------------- /donatello/config/alt_params.yaml: -------------------------------------------------------------------------------- 1 | does_machines: 2 | ros__parameters: 3 | pizza: sausage 4 | brothers: [mike, leo, raph] 5 | -------------------------------------------------------------------------------- /donatello/config/params.yaml: -------------------------------------------------------------------------------- 1 | does_machines: 2 | ros__parameters: 3 | pizza: mushrooms 4 | brothers: [leo, raph, mike] 5 | -------------------------------------------------------------------------------- /donatello/config/sub_params.yaml: -------------------------------------------------------------------------------- 1 | pizza: $(var pizza_type) 2 | anonymous_name: $(anon leo) 3 | favorite_brother: $(env BROTHER_NAME mikey) 4 | filename: $(filename) 5 | directory: $(dirname) 6 | list_exec: $(find-exec ls) 7 | list_output: $(command ls) 8 | version: ROS $(env ROS_VERSION) 9 | circumference: $(eval 2.*3.1415*$(var radius)) 10 | -------------------------------------------------------------------------------- /donatello/donatello/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetroRobots/rosetta_launch/7b6e693dd475bc022e906898b828bdd7e5f39fd1/donatello/donatello/__init__.py -------------------------------------------------------------------------------- /donatello/donatello/donatello_node.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.node import Node 3 | 4 | 5 | def main(): 6 | try: 7 | rclpy.init() 8 | node = Node('donatello', 9 | allow_undeclared_parameters=True, 10 | automatically_declare_parameters_from_overrides=True) 11 | logger = node.get_logger() 12 | for param in node._parameters: 13 | v = node.get_parameter(param).value 14 | logger.info(f'Parameter {param}={v}') 15 | rclpy.spin(node) 16 | rclpy.shutdown() 17 | except KeyboardInterrupt: 18 | pass 19 | -------------------------------------------------------------------------------- /donatello/donatello/five_seconds.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.node import Node 3 | 4 | running = True 5 | 6 | 7 | def callback(): 8 | global running 9 | running = False 10 | 11 | 12 | def main(): 13 | rclpy.init() 14 | node = Node('five_seconds') 15 | node.create_timer(5.0, callback) 16 | while running: 17 | rclpy.spin_once(node) 18 | 19 | rclpy.shutdown() 20 | -------------------------------------------------------------------------------- /donatello/donatello/manual_launch.py: -------------------------------------------------------------------------------- 1 | from ros2launch.api import get_share_file_path_from_package, launch_a_launch_file 2 | 3 | 4 | def main(): 5 | path = get_share_file_path_from_package(package_name='donatello', file_name='05-arg.launch.py') 6 | launch_arguments = ['pizza_type:=extra_cheese'] 7 | 8 | launch_a_launch_file( 9 | launch_file_path=path, 10 | # Note: launch_file_arguments is required! 11 | launch_file_arguments=launch_arguments, 12 | ) 13 | 14 | 15 | if __name__ == '__main__': 16 | main() 17 | -------------------------------------------------------------------------------- /donatello/launch/01-single.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch_ros.actions import Node 3 | 4 | 5 | def generate_launch_description(): 6 | return LaunchDescription([ 7 | Node(name='does_machines', package='donatello', executable='donatello_node'), 8 | ]) 9 | -------------------------------------------------------------------------------- /donatello/launch/02-param.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch_ros.actions import Node 3 | 4 | 5 | def generate_launch_description(): 6 | return LaunchDescription([ 7 | Node(name='does_machines', 8 | package='donatello', 9 | executable='donatello_node', 10 | parameters=[{'pizza': 'mushrooms', 11 | 'brothers': ['leo', 'mike', 'raph']}]), 12 | ]) 13 | -------------------------------------------------------------------------------- /donatello/launch/03-params.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.substitutions import PathJoinSubstitution 3 | from launch_ros.actions import Node 4 | from launch_ros.substitutions import FindPackageShare 5 | 6 | 7 | def generate_launch_description(): 8 | return LaunchDescription([ 9 | Node(name='does_machines', 10 | package='donatello', 11 | executable='donatello_node', 12 | parameters=[PathJoinSubstitution(FindPackageShare('donatello'), 'config', 'params.yaml')]), 13 | ]) 14 | -------------------------------------------------------------------------------- /donatello/launch/04-command-params.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.substitutions import Command, PathJoinSubstitution 3 | from launch_ros.actions import Node 4 | from launch_ros.parameter_descriptions import ParameterValue 5 | from launch_ros.substitutions import FindPackageShare 6 | 7 | 8 | def generate_launch_description(): 9 | robot_description = ParameterValue( 10 | Command(['xacro ', PathJoinSubstitution(FindPackageShare('urdf_tutorial'), 'urdf', '01-myfirst.urdf')]), 11 | value_type=str) 12 | 13 | return LaunchDescription([ 14 | Node(package='robot_state_publisher', 15 | executable='robot_state_publisher', 16 | parameters=[{'robot_description': robot_description}]) 17 | ]) 18 | -------------------------------------------------------------------------------- /donatello/launch/05-arg.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import DeclareLaunchArgument 3 | from launch.substitutions import LaunchConfiguration 4 | from launch_ros.actions import Node 5 | 6 | 7 | def generate_launch_description(): 8 | return LaunchDescription([ 9 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 10 | Node(name='does_machines', 11 | package='donatello', 12 | executable='donatello_node', 13 | parameters=[{'pizza': LaunchConfiguration('pizza_type')}]), 14 | ]) 15 | -------------------------------------------------------------------------------- /donatello/launch/06a-substitutions.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import DeclareLaunchArgument 3 | from launch.substitutions import AnonName, Command, EnvironmentVariable, FindExecutable, LaunchConfiguration 4 | from launch.substitutions import PythonExpression, ThisLaunchFile, ThisLaunchFileDir 5 | from launch_ros.actions import Node 6 | 7 | 8 | def generate_launch_description(): 9 | object_parameters = { 10 | 'pizza': LaunchConfiguration('pizza_type'), 11 | 'anonymous_name': AnonName('leo'), 12 | 'favorite_brother': EnvironmentVariable('BROTHER_NAME', default_value='mikey'), 13 | 'filename': ThisLaunchFile(), 14 | 'directory': ThisLaunchFileDir(), 15 | 'list_exec': FindExecutable(name='ls'), 16 | 'list_output': Command('ls'), 17 | 'version': ['ROS ', EnvironmentVariable('ROS_VERSION')], 18 | 'circumference': PythonExpression(['2.*3.1415*', LaunchConfiguration('radius')]), 19 | } 20 | 21 | return LaunchDescription([ 22 | DeclareLaunchArgument('radius', default_value='1.5'), 23 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 24 | Node(name='does_machines', 25 | package='donatello', 26 | executable='donatello_node', 27 | parameters=[object_parameters]), 28 | ]) 29 | -------------------------------------------------------------------------------- /donatello/launch/06b-substitutions.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import DeclareLaunchArgument 3 | from launch_ros.actions import Node 4 | from launch.frontend.parse_substitution import parse_substitution 5 | 6 | 7 | def generate_launch_description(): 8 | text_params = { 9 | 'pizza': parse_substitution('$(var pizza_type)'), 10 | 'anonymous_name': parse_substitution('$(anon leo)'), 11 | 'favorite_brother': parse_substitution('$(env BROTHER_NAME mikey)'), 12 | 'filename': parse_substitution('$(filename)'), 13 | 'directory': parse_substitution('$(dirname)'), 14 | 'list_exec': parse_substitution('$(find-exec ls)'), 15 | 'list_output': parse_substitution('$(command ls)'), 16 | 'version': parse_substitution('ROS $(env ROS_VERSION)'), 17 | 'circumference': parse_substitution('$(eval 2.*3.1415*$(var radius))'), 18 | } 19 | 20 | return LaunchDescription([ 21 | DeclareLaunchArgument('radius', default_value='1.5'), 22 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 23 | Node(name='does_machines', 24 | package='donatello', 25 | executable='donatello_node', 26 | parameters=[text_params]), 27 | ]) 28 | -------------------------------------------------------------------------------- /donatello/launch/06c-substitutions.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import DeclareLaunchArgument 3 | from launch.substitutions import PathJoinSubstitution 4 | from launch_ros.actions import Node 5 | from launch_ros.parameter_descriptions import ParameterFile 6 | from launch_ros.substitutions import FindPackageShare 7 | 8 | 9 | def generate_launch_description(): 10 | file_parameters = ParameterFile( 11 | param_file=PathJoinSubstitution(FindPackageShare('donatello'), 'config', 'sub_params.yaml'), 12 | allow_substs=True 13 | ) 14 | 15 | return LaunchDescription([ 16 | DeclareLaunchArgument('radius', default_value='1.5'), 17 | DeclareLaunchArgument('pizza_type', default_value='mushrooms'), 18 | Node(name='does_machines', 19 | package='donatello', 20 | executable='donatello_node', 21 | parameters=[file_parameters]), 22 | ]) 23 | -------------------------------------------------------------------------------- /donatello/launch/07-inclusive.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import IncludeLaunchDescription 3 | from launch.substitutions import PathJoinSubstitution 4 | from launch_ros.substitutions import FindPackageShare 5 | 6 | 7 | def generate_launch_description(): 8 | return LaunchDescription([ 9 | IncludeLaunchDescription(PathJoinSubstitution(FindPackageShare('donatello'), 'launch', '05-arg.launch.py'), 10 | launch_arguments={'pizza_type': 'peppers'}.items()), 11 | ]) 12 | -------------------------------------------------------------------------------- /donatello/launch/08-conditional.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription 3 | from launch.conditions import IfCondition, UnlessCondition 4 | from launch.substitutions import LaunchConfiguration, PathJoinSubstitution 5 | from launch_ros.substitutions import FindPackageShare 6 | 7 | 8 | def generate_launch_description(): 9 | return LaunchDescription([ 10 | DeclareLaunchArgument('use_number_one', default_value='True'), 11 | IncludeLaunchDescription( 12 | PathJoinSubstitution(FindPackageShare('donatello'), 'launch', '01-single.launch.py'), 13 | condition=IfCondition(LaunchConfiguration('use_number_one')), 14 | ), 15 | IncludeLaunchDescription( 16 | PathJoinSubstitution(FindPackageShare('donatello'), 'launch', '02-param.launch.py'), 17 | condition=UnlessCondition(LaunchConfiguration('use_number_one')), 18 | ), 19 | ]) 20 | -------------------------------------------------------------------------------- /donatello/launch/09-dynamic-filename.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import DeclareLaunchArgument 3 | from launch.substitutions import LaunchConfiguration, PathJoinSubstitution 4 | from launch_ros.actions import Node 5 | from launch_ros.substitutions import FindPackageShare 6 | 7 | 8 | def generate_launch_description(): 9 | dynamic_param_path = PathJoinSubstitution( 10 | FindPackageShare('donatello'), 11 | 'config', 12 | [LaunchConfiguration('config'), '.yaml'] 13 | ) 14 | 15 | return LaunchDescription([ 16 | DeclareLaunchArgument('config', default_value='params'), 17 | Node(package='donatello', 18 | executable='donatello_node', 19 | name='does_machines', 20 | parameters=[dynamic_param_path]), 21 | ]) 22 | -------------------------------------------------------------------------------- /donatello/launch/10a-required.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import Shutdown 3 | from launch_ros.actions import Node 4 | 5 | 6 | def generate_launch_description(): 7 | don_node = Node(name='does_machines', package='donatello', executable='donatello_node') 8 | five_node = Node(name='five_seconds', package='donatello', executable='five_seconds', on_exit=Shutdown()) 9 | return LaunchDescription([don_node, five_node]) 10 | -------------------------------------------------------------------------------- /donatello/launch/10b-required.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch.actions import RegisterEventHandler, LogInfo, EmitEvent, Shutdown 3 | from launch.event_handlers import OnProcessExit 4 | from launch_ros.actions import Node 5 | 6 | 7 | def generate_launch_description(): 8 | don_node = Node(name='does_machines', package='donatello', executable='donatello_node') 9 | five_node = Node(name='five_seconds', package='donatello', executable='five_seconds') 10 | handler = RegisterEventHandler( 11 | event_handler=OnProcessExit( 12 | target_action=five_node, 13 | on_exit=[ 14 | LogInfo( 15 | msg='Five node exited; tearing down entire system.'), 16 | EmitEvent( 17 | event=Shutdown())])) 18 | 19 | return LaunchDescription([don_node, five_node, handler]) 20 | -------------------------------------------------------------------------------- /donatello/launch/12-recursion.launch.py: -------------------------------------------------------------------------------- 1 | from ament_index_python.packages import get_package_share_path 2 | import launch 3 | from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument 4 | from launch.conditions import IfCondition 5 | from launch.substitutions import LaunchConfiguration, PythonExpression 6 | import launch_ros.actions 7 | 8 | 9 | def generate_launch_description(): 10 | return launch.LaunchDescription([ 11 | DeclareLaunchArgument('N', default_value='10'), 12 | launch_ros.actions.Node(name=['donatello_node', LaunchConfiguration('N')], 13 | package='donatello', executable='donatello_node'), 14 | IncludeLaunchDescription(str(get_package_share_path('donatello') / 'launch/12-recursion.launch.py'), 15 | condition=IfCondition(PythonExpression([LaunchConfiguration('N'), '>1'])), 16 | launch_arguments={'N': PythonExpression([LaunchConfiguration('N'), '-1'])}.items()), 17 | ]) 18 | -------------------------------------------------------------------------------- /donatello/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | donatello 5 | 0.0.0 6 | ROS 2 Launch example code for rosetta_launch 7 | David V. Lu!! 8 | BSD 9 | rclpy 10 | robot_state_publisher 11 | urdf_tutorial 12 | xacro 13 | 14 | ament_python 15 | 16 | 17 | -------------------------------------------------------------------------------- /donatello/resource/donatello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetroRobots/rosetta_launch/7b6e693dd475bc022e906898b828bdd7e5f39fd1/donatello/resource/donatello -------------------------------------------------------------------------------- /donatello/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/donatello 3 | 4 | [install] 5 | install_scripts=$base/lib/donatello 6 | 7 | [flake8] 8 | max_line_length=120 9 | -------------------------------------------------------------------------------- /donatello/setup.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from setuptools import setup 3 | 4 | package_name = 'donatello' 5 | 6 | setup( 7 | name=package_name, 8 | version='0.0.0', 9 | packages=[package_name], 10 | data_files=[ 11 | ('share/ament_index/resource_index/packages', ['resource/' + package_name]), 12 | ('share/' + package_name, ['package.xml']), 13 | ('share/' + package_name + '/launch', glob('launch/*.launch.py')), 14 | ('share/' + package_name + '/config', glob('config/*.yaml')), 15 | ], 16 | install_requires=['setuptools'], 17 | zip_safe=True, 18 | maintainer='David V. Lu!!', 19 | maintainer_email='davidvlu@gmail.com', 20 | description='ROS 2 Launch example code for rosetta_launch', 21 | license='BSD', 22 | tests_require=['pytest'], 23 | entry_points={ 24 | 'console_scripts': [ 25 | 'donatello_node = donatello.donatello_node:main', 26 | 'five_seconds = donatello.five_seconds:main', 27 | 'manual_launch = donatello.manual_launch:main', 28 | ] 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /raphael/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0.2) 2 | project(raphael) 3 | 4 | find_package(catkin REQUIRED) 5 | catkin_package() 6 | 7 | install(DIRECTORY config 8 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 9 | ) 10 | install(DIRECTORY launch 11 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 12 | ) 13 | catkin_install_python(PROGRAMS scripts/five_seconds scripts/raphael_node 14 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 15 | ) 16 | -------------------------------------------------------------------------------- /raphael/config/alt_params.yaml: -------------------------------------------------------------------------------- 1 | pizza: bacon 2 | brothers: [mike, leo, don] 3 | -------------------------------------------------------------------------------- /raphael/config/params.yaml: -------------------------------------------------------------------------------- 1 | pizza: pepperoni 2 | brothers: [leo, don, mike] 3 | coworkers: 4 | - leo 5 | - don 6 | - mike 7 | weapon: sai 8 | -------------------------------------------------------------------------------- /raphael/config/sub_params.yaml: -------------------------------------------------------------------------------- 1 | anonymous_name: $(anon leo) 2 | favorite_brother: $(optenv BROTHER_NAME mikey) 3 | package_location: $(find rospy) 4 | directory: $(dirname) 5 | version: ROS $(env ROS_VERSION) 6 | -------------------------------------------------------------------------------- /raphael/launch/01-single.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /raphael/launch/02-param.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | [leo, don, mike] 8 | 9 | weapon: sai 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /raphael/launch/03-params.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /raphael/launch/04-command-params.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /raphael/launch/05-arg.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /raphael/launch/06-substitutions.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /raphael/launch/07-inclusive.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /raphael/launch/08-conditional.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /raphael/launch/09-dynamic-filename.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /raphael/launch/10-required.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /raphael/launch/12-recursion.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /raphael/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | raphael 4 | 0.0.0 5 | ROS 1 Launch example code for rosetta_launch 6 | David V. Lu!! 7 | BSD 8 | catkin 9 | robot_state_publisher 10 | rospy 11 | urdf_tutorial 12 | xacro 13 | 14 | -------------------------------------------------------------------------------- /raphael/scripts/five_seconds: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import rospy 4 | rospy.init_node('five_seconds') 5 | rospy.sleep(5) 6 | -------------------------------------------------------------------------------- /raphael/scripts/manual_launch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from catkin.find_in_workspaces import find_in_workspaces 3 | from roslaunch.parent import ROSLaunchParent 4 | from roslaunch import rlutil 5 | 6 | 7 | def main(): 8 | uuid = rlutil.get_or_generate_uuid(None, False) 9 | launch_path = find_in_workspaces(project='raphael', path='launch/05-arg.launch', first_match_only=True)[0] 10 | launch_with_args = (launch_path, ['pizza_type:=extra_cheese']) 11 | 12 | p = ROSLaunchParent(uuid, [launch_with_args]) 13 | p.start() 14 | p.spin() 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /raphael/scripts/raphael_node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import rospy 4 | rospy.init_node('raphael') 5 | params = rospy.get_param('~', {}) 6 | for k, v in params.items(): 7 | rospy.loginfo(f'Parameter {k}={v}') 8 | rospy.spin() 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max_line_length=120 3 | -------------------------------------------------------------------------------- /sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import pathlib 3 | import re 4 | 5 | SOURCE_PATTERN = re.compile( 6 | r'(' 7 | r'\[source\]\(([^)]+)\)\n' # source link 8 | r'(' 9 | r'```(\w*)\n' # First line of source 10 | r'([^`]*)' # Contents 11 | r'\n```' 12 | r'|[^`]' 13 | r'))', re.DOTALL) 14 | 15 | fn = 'README.md' 16 | s = open(fn).read() 17 | for whole_match, filepath, source_block, source_type, contents in SOURCE_PATTERN.findall(s): 18 | if not pathlib.Path(filepath).exists(): 19 | continue 20 | 21 | if len(source_block) == 1: 22 | trail = source_block 23 | else: 24 | trail = '' 25 | contents = open(filepath).read().rstrip() 26 | if '' in contents: 27 | source_type = 'xml' 28 | else: 29 | source_type = 'python' 30 | new_match = f'[source]({filepath})\n```{source_type}\n{contents}\n```{trail}' 31 | s = s.replace(whole_match, new_match) 32 | 33 | with open(fn, 'w') as f: 34 | f.write(s) 35 | --------------------------------------------------------------------------------