環境
本記事は以下の環境を想定して記述している。
項目 | 値 |
---|---|
OS | Ubuntu 22.04 |
ROS | ROS 2 Humble |
概要
このページでは、ROS 2で使われるlaunchシステムの目的とその機能について、概念的な説明をする。
その後、2つのPublisherをまとめて起動するlaunchファイルと、2つのSubscriberをまとめて起動するlaunchファイルを記述し、実行結果を確認する。
launchファイルは同じ内容のものをPythonとXMLの2種類で記述する。
本記事で解説していること
- launchファイルの目的
- Pub&Sub通信のページで実装したPublisherとSubscriberを同時に起動するlaunchファイル(Python形式及びXML形式)
- launchファイルを記述する言語(Python, XML, YAML)による違いについて
本記事ではlaunchファイルの概念と基本的な作成方法を説明することに焦点を当てる。
そのため、本記事で実装するlaunchファイルは単純なものとし、別の記事でlaunchシステムを活用した様々な機能を紹介する。
前提
本記事の内容は、Pub&Sub通信で作成したディレクトリ群があることを前提にしている。
また、ワークスペースの名称はros2_lecture_ws
であると仮定して説明する。
前提としているパッケージと今回作成するパッケージの構成を以下に示す。
$ cd ~/ros2_lecture_ws/src/
$ tree # ディレクトリ構成を表示するコマンド
.
├── pub_sub_comm
│ ├── CMakeLists.txt
│ ├── include
│ │ └── pub_sub_comm
│ ├── package.xml
│ └── src
│ ├── simple_listener.cpp
│ └── simple_talker.cpp
├── ros2_launch_1 # この記事で作成するパッケージ
│ ├── CMakeLists.txt
│ ├── launch
│ │ ├── publisher_nodes.launch.py
│ │ ├── publisher_nodes.launch.xml
│ │ ├── subscriber_nodes.launch.py
│ │ └── subscriber_nodes.launch.xml
│ ├── package.xml
│ └── README.md
launchファイル
説明
launchファイルは、複数のROSノードをまとめて起動し、それらの実行を制御するためのファイルである。
ROS 2の場合、プログラムの基本単位となるノードはros2 run <パッケージ名> <実行ファイル名>
というコマンドで実行できる。
この場合、ノード数だけターミナルを起動し、人が都度都度コマンドを実行する必要がある。
launchファイルを用いると、1つのターミナルで複数のノードをまとめて起動できるようになる。
また、launchシステムにはノードやそれらの間の通信を制御する様々な機能があり、柔軟で再利用可能なプログラムの構成を記述できる。
launchファイルで実現できる機能の一例を以下に示す。
- 条件に応じて起動するノードを選択する
- 他のlaunchファイルをインクルードする
- launchファイルに対する引数を定義する
- ノード名やトピック名をremap(*注)する
- ノード名やトピック名にnamespace(名前空間)を追加する
(*注):remapとは、ある名称を別の名称に置き換えることである。例えば、/chatter1
という名前でソースコード中に実装されたトピックを/chatter1_remapped
という名前としてノードを起動することがremapにあたる。
パッケージの作成
launchファイルはパッケージの中に実装するため、まずはパッケージを作成する。
以下のように、ros2 pkg create
コマンドでパッケージを作成する。
本記事ではpub_sub_comm
に依存したパッケージを作成するため、オプションで依存パッケージを指定している。(--dependencies pub_sub_comm
の部分)。
# パッケージを作成
$ cd ~/ros2_lecture_ws/src
$ ros2 pkg create ros2_launch_1 --build-type ament_cmake --dependencies pub_sub_comm
Pythonでの書き方
ROS 2ではPythonでlaunchファイルが記述されることが多いため、まずPythonを用いた実装を示す。
ros2_launch_1
のパッケージにlaunch
というディレクトリを作成し、その中にpublisher_nodes.launch.py
というlaunchファイルを作成する。
# ディレクトリを作成し、ファイルを新規作成
cd ~/ros2_lecture_ws/src/ros2_launch_1
mkdir launch
cd launch
touch publisher_nodes.launch.py
このlaunchファイルでは、同一の処理を行うPublisherノードを2つ起動する。
実装したファイルの中身を以下に示す。launchファイルへのリンクはこちら。
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# launchの構成を示すLaunchDescription型の変数の定義
ld = LaunchDescription()
# publisher nodeを、"talker_renamed1"という名前で定義
pub_node1 = Node(
package='pub_sub_comm',
executable='talker',
name='talker_renamed1',
namespace='namespace_app1', # namespace_app1というnamespaceを追加
remappings=[('chatter', 'chatter_app1')] # chatterトピックをchatter_app1トピックにremap
)
# publisher nodeを、"talker_renamed2"という名前で定義
pub_node2 = Node(
package='pub_sub_comm',
executable='talker',
name='talker_renamed2',
namespace='namespace_app2', # namespace_app2というnamespaceを追加
remappings=[('chatter', 'chatter_app2')] # chatterトピックをchatter_app2トピックにremap
)
# LaunchDescriptionに、起動したいノードを追加する
ld.add_action(pub_node1)
ld.add_action(pub_node2)
# launch構成を返すようにする
return ld
Pythonで記述するlaunchファイルでは、launch.LaunchDescription
型のオブジェクトを返す、generate_launch_description()
という名前の関数を定義する。
関数内ではlaunch.Action
型を継承したクラスであるlaunch_ros.Node
型の変数をLaunchDescription
型の変数ld
に追加している。
ノードはtalker_renamed1
、talker_renamed2
という名前にリネームしてから起動している。
同じ名前のノードを複数起動することはできないが、ノード名をリネームすることで、同一の処理を行うノードを複数起動できるようになる。
また、1つ目のノードにはnamespace_app1
、2つ目のノードにはnamespace_app2
というnamespaceを追加している。
これにより、トピック名やノード名を階層的に扱うことが可能になり、大規模なROS 2システムが扱いやすくなる。
さらにトピック名についても、2つのノードで異なる名前にリマップしている。
これにより、ノードのソースコードで実装したトピック名を変更することが容易になり、ノードの再利用性が高まる。
同様にして、subscriber_nodes.launch.py
を作成する。
このlaunchファイルでは、pub_sub_comm
パッケージのノードlistener
を、listener_renamed1
、listener_renamed2
という別個のノードとして起動する。
本記事中では実装は省略するが、launchファイルへのリンクをこちらに示す。
XMLでの書き方
上記と同じ内容のlaunchファイルをXMLで実装する。
publisher_nodes.launch.py
を作成した場所に、publisher_nodes.launch.xml
という名前のlaunchファイルを作成する。
# ファイルを新規作成
cd ~/ros2_lecture_ws/src/ros2_launch_1/launch
touch publisher_nodes.launch.xml
実装したファイルの中身を以下に示す。launchファイルへのリンクはこちら。
<launch>
<!-- publisher nodeを、"talker_renamed1"という名前で定義 -->
<node pkg="pub_sub_comm" exec="talker" name="talker_renamed1" namespace="namespace_app1">
<remap from="chatter" to="chatter_app1"/>
</node>
<!-- publisher nodeを、"talker_renamed2"という名前で定義 -->
<node pkg="pub_sub_comm" exec="talker" name="talker_renamed2" namespace="namespace_app2">
<remap from="chatter" to="chatter_app2"/>
</node>
</launch>
XMLで記述する場合、要素と属性の組み合わせで情報を記述する。
実行
設定ファイルの更新
パッケージ内の設定ファイルに、追加したlaunchファイルに関する記述を追加する。
package.xmlには、下記の部分を追記する。
このようにすることで、パッケージのビルド後にros2 launch
コマンドが通ることと、すべてのlaunchファイルのフォーマットが認識されることが保証される。
追記する行とその周辺を以下に示す。
<depend>pub_sub_comm</depend>
+
+ <exec_depend>ros2launch</exec_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
CMakeLists.txtには、以下の部分を追記する。
このようにすることで、launchファイルがワークスペースにインストールされる。
追記する行とその周辺を以下に示す。
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(pub_sub_comm REQUIRED)
+
+ # install launch files
+ install(
+ DIRECTORY launch
+ DESTINATION share/${PROJECT_NAME}/
+ )
ビルド
ワークスペースのトップディレクトリに移動し、colcon build
コマンドを実行する(*注)。
cd ~/ros2_lecture_ws
colcon build --packages-select pub_sub_comm ros2_launch_1
. install/setup.bash
(*注):launchファイルをワークスペースにインストールし、launchファイルをros2 launch
コマンドとして実行するためには、colcon build
コマンドの実行が必要である。パッケージの中にコンパイルやリンクを必要とするファイルがない場合でも、コマンドの実行が必要であることに注意する。
Pythonでの起動方法
ros2 launch <package_name> <launch_file>
という構文で、launchファイルを実行できる。
# 構文:ros2 launch <package_name> <launch_file>
# 1つ目のターミナル:Publisherの同時起動
ros2 launch ros2_launch_1 publisher_nodes.launch.py
# 2つ目のターミナル:Subscriberの同時起動
ros2 launch ros2_launch_1 subscriber_nodes.launch.py
XMLでの起動方法
Pythonのときと同様に、指定するファイル名をxmlにすることで、同様に実行できる。
# 構文:ros2 launch <package_name> <launch_file>
# 1つ目のターミナル:Publisherの同時起動
ros2 launch ros2_launch_1 publisher_nodes.launch.xml
# 2つ目のターミナル:Subscriberの同時起動
ros2 launch ros2_launch_1 subscriber_nodes.launch.xml
どちらの方法でも、1つのターミナルで同時にPublisherとSubscriberを起動することができる。
ROS 2システム上で使用するlaunchファイルのフォーマットは統一されていなくてもよい。
例えば、以下の状況においてA.launch
とB.launch
を同時に起動できる。
- あるlaunchファイル
A.launch
はPythonで書かれており、ノードA1
とA2
を起動する - 別のlaunchファイル
B.launch
はXMLで書かれており、ノードB1
とB2
を起動する
また、ROS 2インタフェースが合っていれば、ノードA1
とB1
の通信ができる。
つまり、ノードはどのlaunchファイルから起動されたかに関係なく通信できる。
結果
ターミナルを開き、ワークスペースのセットアップスクリプトを実行してから、Publisherを起動する。
すると、ログ出力から、異なる名前の2つのPublisherが同時に起動しており、同じ内容のトピックをPublishしていることがわかる。
$ ros2 launch ros2_launch_1 publisher_nodes.launch.py
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [talker-1]: process started with pid [3601]
[INFO] [talker-2]: process started with pid [3603]
[talker-2] [INFO] [1710231655.950076094] [namespace_app2.talker_renamed2]: publish: hello, world!
[talker-1] [INFO] [1710231655.950079658] [namespace_app1.talker_renamed1]: publish: hello, world!
[talker-2] [INFO] [1710231656.050054310] [namespace_app2.talker_renamed2]: publish: hello, world!
[talker-1] [INFO] [1710231656.050082519] [namespace_app1.talker_renamed1]: publish: hello, world!
[talker-2] [INFO] [1710231656.149976967] [namespace_app2.talker_renamed2]: publish: hello, world!
[talker-1] [INFO] [1710231656.150041808] [namespace_app1.talker_renamed1]: publish: hello, world!
[talker-2] [INFO] [1710231656.250055714] [namespace_app2.talker_renamed2]: publish: hello, world!
[talker-1] [INFO] [1710231656.250088787] [namespace_app1.talker_renamed1]: publish: hello, world!
[talker-2] [INFO] [1710231656.350064832] [namespace_app2.talker_renamed2]: publish: hello, world!
[talker-1] [INFO] [1710231656.350067788] [namespace_app1.talker_renamed1]: publish: hello, world!
同様に、別のターミナルを起動し、以下のコマンドでSubscriberを起動する。
Publisherと同様に、listener_renamed1
とlistener_renamed2
のノードが起動しており、ともにメッセージを受け取っていることがわかる。
$ ros2 launch ros2_launch_1 subscriber_nodes.launch.py
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [listener-1]: process started with pid [3626]
[INFO] [listener-2]: process started with pid [3628]
[listener-1] [INFO] [1710231660.649992085] [namespace_app1.listener_renamed1]: subscribe: hello, world!
[listener-2] [INFO] [1710231660.650022050] [namespace_app2.listener_renamed2]: subscribe: hello, world!
[listener-1] [INFO] [1710231660.750524197] [namespace_app1.listener_renamed1]: subscribe: hello, world!
[listener-2] [INFO] [1710231660.750588888] [namespace_app2.listener_renamed2]: subscribe: hello, world!
[listener-1] [INFO] [1710231660.850591883] [namespace_app1.listener_renamed1]: subscribe: hello, world!
[listener-2] [INFO] [1710231660.850660257] [namespace_app2.listener_renamed2]: subscribe: hello, world!
[listener-1] [INFO] [1710231660.950597642] [namespace_app1.listener_renamed1]: subscribe: hello, world!
[listener-2] [INFO] [1710231660.950683223] [namespace_app2.listener_renamed2]: subscribe: hello, world!
[listener-1] [INFO] [1710231661.050594018] [namespace_app1.listener_renamed1]: subscribe: hello, world!
ノードとトピックの関係を図示するのに、rqt_graph
コマンドを使用できる。
3つ目のターミナルを開き、以下のコマンドを実行する。
$ rqt_graph
すると、以下のようなグラフが表示される。
以下の図は、丸がノード、四角がトピックを示している。
図より、2組のPublisherとSubscriberが、別々のnamespaceをつけられた状態で通信していることが分かる。
例えば、ノード/namespace_app1/taker_renamed1
は、トピック/namespace_app1/chatter_app1
にメッセージをPublishしている。
launchファイルを記述する言語
現在のROS 2ではlaunchファイルの記述にPython, XML, YAMLの3種類の形式が使用できる。ROS 1ではXMLを用いることが標準であるが、ROS 2ではPythonを用いることが多い。
その理由としては、以下の点が挙げられる。
- Pythonはスクリプト言語であるため、制御構造の扱いやすさ、言語のライブラリ、デバッグのしやすさなどの点で優れている。
- ROS 2のlaunchシステム自体がPythonで実装されているため、より低レベルの機能を利用できる。
上記の長所の裏返しとして、本記事の2つのlaunchファイルを比較して分かるように、Pythonで記述したlaunchファイルは複雑で冗長になりやすい面がある。
なお、ROS 1のlaunchファイルをROS 2のXML形式のlaunchファイルに移行する際には公式サイトのガイドが参考になる。