ROS 2のcomposable nodeを含むlaunchをPythonからXMLに置き換える(Humble)

この記事ではROS 2 Humbleから使えるlaunch xmlの記述方法について紹介したいと思います。

# ROS 2におけるlaunchの形式
ROS 2で使えるlaunchの形式はPython, XML, YAMLの３つがあります。
ただしすべて同じ機能が使えるわけではなく、すべての機能はPythonで実装されており、
expose_action/expose_substitutionのデコレーターが付与されてるものが、
XML/YAMLでtag/keyとして使えるようになっています。

Galacticまでは、composable nodeのloadやcontainerの起動をするには、
冗長なPythonのlaunchを書く必要があったのですが、Humbleからは
XMLで記述できるようになりました。(私の同僚の@kenji-miyake さんが実装してくれました。)

# Pythonでの記述方法
まずは従来のPythonでの記述方法から見ていきます。
```
from launch import LaunchDescription
from launch_ros.actions import LoadComposableNodes, ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

def generate_launch_description():
    container = ComposableNodeContainer(
        name="my_container",
        package="rclcpp_components",
        executable="component_container",
        namespace="",
        arguments="test_args",
        composable_node_descriptions=[
            ComposableNode(
                package="demo_nodes_cpp",
                plugin="demo_nodes_cpp::ParameterBlackboard",
                name="board1",
                parameters=[
                    {"test": "aaa"}
                ],
                extra_arguments=[{"use_intra_process_comms": True}],
            ),
        ],
        output="screen",
    )

    load_composable_nodes = LoadComposableNodes(
        target_container="my_container",
        composable_node_descriptions=[
            ComposableNode(
                package="demo_nodes_cpp",
                plugin="demo_nodes_cpp::ParameterBlackboard",
                name="board2",
                namespace="test_namespace",
                parameters=[
                    {"test": "aaa"}
                ],
                extra_arguments=[{"use_intra_process_comms": True}],
            ),
        ],
    )

    return LaunchDescription([
        container,
        load_composable_nodes,
    ])
```
`ComposableNodeContainer`がcomposable nodeをloadするためのcontainerです。
`composable_node_descriptions`にloadするcomposable nodeを記述します。
またすでにcontainerが起動している場合は、`LoadComposableNodes`で`target_container`にcomposable nodeをloadしたいcontainerを指定することで、`composable_node_descriptions`に記述したnode群をloadすることができます。

# XMLでの記述方法
上記のPythonで記述されたlaunchをXMLで置き換えたものが以下になります。
```
<launch>
  <node_container pkg="rclcpp_components" exec="component_container" name="my_container" namespace="" args="test_args">
    <composable_node pkg="demo_nodes_cpp" plugin="demo_nodes_cpp::ParameterBlackboard" name="board1" namespace="test_namespace">
      <param name="test" value="aaa" />
      <extra_arg name="use_intra_process_comms" value="true" />
    </composable_node>
  </node_container>

  <load_composable_node target="my_container">
    <composable_node pkg="demo_nodes_cpp" plugin="demo_nodes_cpp::ParameterBlackboard" name="board2" namespace="test_namespace">
      <param name="test" value="aaa" />
      <extra_arg name="use_intra_process_comms" value="true" />
    </composable_node>
  </load_composable_node>
</launch>
```
PythonのclassとXMLのtagの対応関係は以下のようになっています。

- `ComposableNodeContainer` &rarr; `node_container` ([exposeの実装](https://github.com/ros2/launch_ros/blob/f142fd335eaa19a54a449b38c929d3cd37075c63/launch_ros/launch_ros/actions/composable_node_container.py#L32-L33))
- `ComposableNode` &rarr; `composable_node`
- `LoadComposableNodes` &rarr; `load_composable_node` ([exposeの実装](https://github.com/ros2/launch_ros/blob/f142fd335eaa19a54a449b38c929d3cd37075c63/launch_ros/launch_ros/actions/load_composable_nodes.py#L54-L55))

(`composable_node`のみ`node_container`/`load_composable_node`の下の`composable_node` entityを読みにいくという実装だったため、exposeの記述はありませんでした。[実装箇所](https://github.com/ros2/launch_ros/blob/f142fd335eaa19a54a449b38c929d3cd37075c63/launch_ros/launch_ros/actions/composable_node_container.py#L63-L64))

コードの記述量が減り、だいぶスッキリしました。