【ROS2】launchファイルの引数でノード/launchファイルの実行有無を制御する

Python launchファイルでの話

# 概要

- ノード/launchファイルを呼び出す際の`condition`引数に以下のインスタンスを指定する
    - `launch.conditions.IflessCondition`
    - `launch.conditions.UnlessCondition`
- 上記クラスは以下のように実体化する
    ```python
    IflessCondition(expression)
    UnlessCondition(expression)
    ```
- `expression`には最終的に文字列として`true`とか`false`とかになるようなものを指定する。詳細は、[判定について](#判定について)を参照のこと
    - OK例
        ```python
        IflessCondition("true")
        IflessCondition(str(True))
        # launchファイルの引数を使った条件を書きたい場合はlaunch_ros.substitutions.PythonExpressionを使うと良い
        IflessCondition(PythonExpression(["1 == 1"]))
        ```
    - NG例
        ```python
        IflessCondition(True) # エラー
        IflessCondition("1 == 1") # エラー
        ```
- 名前からわかるとおり以下のように使い分ける
    - `IflessCondition(expression)` : `expression`が`true`の場合に呼び出したい
    - `UnlessCondition(expression)` : `expression`が`false`の場合に呼び出したい

# ソースコード

最初のコードを見れば大丈夫だと思う(参考に、呼び出しているlaunchファイル、ノードのコードも載せている)
test_conditionパッケージ配下に作成している

## 実行有無を制御しているlaunchファイル

引数`condition`で起動するノード、launchファイルを制御するようになっている
具体的には以下のノード、launchファイルを指定している
- 常に起動するノード
- `condition = true`のときに起動するノード (`IfCondition`を指定)
- `condition = false`のときに起動するノード (`UnlessCondition`を指定)
- `condition = true`のときに呼び出されるlaunchファイル (`IfCondition`を指定)
- `condition = false`のときに呼び出されるlaunchファイル (`UnlessCondition`を指定)

```python:test_condition.launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
# 実行可否を指定するためのパッケージ
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare



def generate_launch_description():
    # 実行可否を指定するための引数を定義
    condition_argument = DeclareLaunchArgument("condition", default_value="true")
    condition = LaunchConfiguration('condition')

    return LaunchDescription([
        condition_argument,
        Node(
            package='test_condition',
            executable='test_condition',
            name='node_no_condition',
            output='screen',
            parameters=[
                {'print_message': 'Node w/ no condition!'}
            ]
        ),
        # conditionがtrueの場合に起動するノード
        Node(
            package='test_condition',
            executable='test_condition',
            name='node_if_condition',
            output='screen',
            parameters=[
                {'print_message': 'Node w/ IfCondition!'}
            ],
            condition=IfCondition(condition) # ここでconditionがtrueのときに起動するように指定
        ),
        # conditionがfalseの場合に起動するノード
        Node(
            package='test_condition',
            executable='test_condition',
            name='node_unless_condition',
            output='screen',
            parameters=[
                {'print_message': 'Node w/ UnlessCondition!'}
            ],
            condition=UnlessCondition(condition) # ここでconditionがfalseのときに起動するように指定
        ),
        # conditionがtrueの場合に呼び出されるlaunchファイル
        IncludeLaunchDescription(
            PathJoinSubstitution(
                [FindPackageShare("test_condition"), "launch", "base.launch.py"]
            ),
            launch_arguments=[
                ('print_message', 'Launch file w/ IfCondition!')
            ],
            condition=IfCondition(condition) # ここでconditionがtrueのときに呼び出すように指定
        ),
        # conditionがfalseの場合に呼び出されるlaunchファイル
        IncludeLaunchDescription(
            PathJoinSubstitution(
                [FindPackageShare("test_condition"), "launch", "base.launch.py"]
            ),
            launch_arguments=[
                ('print_message', 'Launch file w/ UnlessCondition!')
            ],
            condition=UnlessCondition(condition) # ここでconditionがfalseのときに呼び出すように指定
        )
    ])
```

## 呼び出しているノード(参考)

パラメータ`print_message`で指定された文字列を表示するだけのノードである

```python:test_condition.py
import rclpy
from rclpy.node import Node

class TestCondition(Node):
    def __init__(self):
        super().__init__('test_condition_node')

        print_message = self.declare_parameter(
            'print_message', 'N/A'
        ).get_parameter_value().string_value

        # print_messageで指定された文字列を表示するのみ
        self.get_logger().info(print_message)

def main():
    rclpy.init()
    TestCondition()
    rclpy.shutdown()
```

## 呼び出しているlaunchファイル(参考)

引数として与えられた`print_message`をそのまま表示するだけのlaunchファイル

```python:base.launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
    # わかりやすくするために、ノードが表示するメッセージを引数として定義
    print_message_argument = DeclareLaunchArgument("print_message", default_value="N/A")
    print_message = LaunchConfiguration("print_message")

    # 引数として与えられた文字列をそのままノードに渡している
    return LaunchDescription([
        print_message_argument,
        Node(
            package="test_condition",
            executable="test_condition",
            name="launch",
            output="screen",
            parameters=[
                {"print_message": print_message}
            ]
        )
    ])
```

## 実行結果

`condition:=true`とした場合、IfConditionを使っているノード、launchファイルのみが起動している
```console
$ ros2 launch test_condition test_condition.launch.py condition:=true
[test_condition-3] [INFO] [1.1] [launch]: Launch file w/ IfCondition!
[test_condition-1] [INFO] [1.1] [node_no_condition]: Node w/ no condition!
[test_condition-2] [INFO] [1.1] [node_if_condition]: Node w/ IfCondition!
```

`condition:=false`とした場合、IfConditionを使っているノード、launchファイルのみが起動している
```console
$ ros2 launch test_condition test_condition.launch.py condition:=false
[test_condition-3] [INFO] [1.1] [launch]: Launch file w/ UnlessCondition!
[test_condition-2] [INFO] [1.1] [node_unless_condition]: Node w/ UnlessCondition!
[test_condition-1] [INFO] [1.1] [node_no_condition]: Node w/ no condition!
```

(実行結果は省略している)

# 判定について

[該当コード](https://github.com/ros2/launch/blob/foxy/launch/launch/conditions/evaluate_condition_expression_impl.py#L29)のコメントにあるように、以下のようになる

| 最終的な文字列 | 判定結果 |
| :-: | :-: |
| true ※1 | 真 |
| 1 | 真 |
| false ※1 | 偽 |
| 0 | 偽 |
| 上記以外 | エラー ※2 |

※1 最終的な文字列が大文字か小文字かとは無関係に判定される(eg. `True`でも真と判定される)
※2 `launch.substitutions.text_substitution.TextSubstitution`とか言うエラーが出る

# 参考

- [複数条件の指定方法](https://answers.ros.org/question/394181/multiple-conditions-for-ifcondition-in-ros2-launch-script/)
- [launchファイルから他のlaunchファイルを呼ぶ](https://ar-ray.hatenablog.com/entry/2021/08/15/203449)