【ROS×Python】Launchでノード起動構成をラクラク管理!

 ※ 本記事は広告・プロモーションを含みます。
Python

たっきん( X (旧Twitter) )です!

今日はROSノードの起動構成を楽に管理できるLaunch機能について紹介したいと思います。

ROSをある程度使いこなしてくると誰しもが面倒くさいと感じることが出てくると思います。

PC画面が複数ターミナルで埋め尽くされる問題です。

ROSは複数ノードが集まって1つの機能を成すため、開発途中のデバック作業などで必然的に複数ノードを起動させたり停止させたりを繰り返すことが多くなってきます。

また、ノードの起動は基本的にコマンドベースで操作することになるため、下図のように画面がターミナルだらけになってしまうことも珍しくありません。

気が付くと画面がこんな感じになってるのはあるあるですよね~

画面内に敷き詰められた複数のターミナルをいちいち切り替えて操作するのはかなり煩わしと感じるはずです。
そう感じ始めたら、Launch機能の出番ですね!

Launch機能で出来ること

Launch機能で出来ることを簡単に説明すると、「どのノードをどんな条件で起動するのかを一括して指定できるようになる」です。

起動したいノードの指定や起動条件はlaunchファイルに記述するのですが、一度記述してしまえば後はlaunchファイルを実行・停止するだけで、複数ノードの起動・停止が同時に行えるようになります。

また、ノード起動前にパラメータ値の設定も記述することができるため、コマンドラインでノード起動する際に、いちいちコマンドライン引数でパラメータ値を設定しなければならないような煩わしさもなくなります。

さらに、launchファイルの記述にはPython言語を使用するため、Pythonの制御構文を使って起動条件を分岐させることもできます。

Launch機能で出来ること

  • どのノードをどんな条件で起動するのかを一括指定できる
  • ノード起動前にパラメータ値の設定も可能。
  • launchファイルはPython言語で記述するため、起動条件を制御構文で分岐させることができる

実際にlaunch機能を使ってみる

それでは、実際にLaunch機能を使ってノードを起動してみましょう!

以降で紹介するサンプルコードは全てROSパッケージ化した状態でGitHubに載せています。

自分の環境で実行して挙動を確認してみたい場合は下記からクローンして使用してください。

本ページで使用するコード

シンプルな起動構成の例

今回起動するノードは「【ROS×Python】ROSパラメータ(Parameter)を実装しよう!」の実例で使用した下記2つのノードになります。

記事の中で上記2つのノードを起動するために下記2つのコマンドを使用しました。

$ ros2 run ros2_example bb_param --ros-args -p sma:=5 -p sigma:=1.0 --remap __ns:=/name_space_1
$ ros2 run ros2_example bb_param --ros-args -p sma:=10 -p sigma:=2.0 --remap __ns:=/name_space_2

これらの起動構成をlaunch機能を使って起動するにはどうすればよいかを順を追って説明していきますね!

①launchディレクトリ作成

ROSパッケージのルートディレクトリ直下に「launch」「params」という名前のディクトリを作成します。

用途はlaunchファイルとパラメータYAMLファイル格納用です。

  • launch: launchファイル格納用
  • params: パラメータYAMLファイル格納用

②setupファイル修正

次はsetup.pyファイルpackage.xmlファイルの2つを修正していきます。

setup.py

launchファイルパラメータYAMLファイルの格納先をセットアップツールへ通知するための設定を行います。

ハイライト行(2,14,15行目)を追加

from setuptools import find_packages, setup
from glob import glob


package_name = "ros2_example"

setup(
    name=package_name,
    version="0.0.0",
    packages=find_packages(exclude=["test"]),
    data_files=[
        ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
        ("share/" + package_name, ["package.xml"]),
        ("share/" + package_name + "/launch", glob("launch/*.py")),
        ("share/" + package_name + "/params", glob("params/*.yaml")),
    ],
    install_requires=["setuptools"],
    zip_safe=True,
    ・・・
package.xml

依存パッケージにlaunchシステムのモジュール「launch_ros」を追加します。

ハイライト行(11行目)を追加

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>ros2_example</name>
  <version>0.0.0</version>
  <description>ros2_example package</description>
  <maintainer email="takkin.takilog@gmail.com">takkin</maintainer>
  <license>MIT</license>

  <exec_depend>rclpy</exec_depend>
  <exec_depend>launch_ros</exec_depend>
  <exec_depend>ros2_example_msgs</exec_depend>
  
  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

③launchファイル作成

起動したいノードの指定や起動条件はlaunchファイルに記述していきます。

launchディレクトリ配下にファイル名bb_param_value_launch.pyを作成して下記のように記述します。

from launch.launch_description import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description() -> LaunchDescription:
    # ノード・アクションの定義
    node_act1 = Node(
        package="ros2_example",
        executable="bb_param",
        name=None,
        namespace="name_space_1",
        parameters=[{"sma": 5, "sigma": 1.0}],
        output="screen",
    )
    node_act2 = Node(
        package="ros2_example",
        executable="bb_param",
        name=None,
        namespace="name_space_2",
        parameters=[{"sma": 10, "sigma": 2.0}],
        output="screen",
    )

    # LaunchDescriptionオブジェクトの生成
    ld = LaunchDescription()

    # ノード・アクションの追加
    ld.add_action(node_act1)
    ld.add_action(node_act2)

    return ld

まずはlaunchモジュールをimportします。

from launch.launch_description import LaunchDescription
from launch_ros.actions import Node

次は関数generate_launch_description()を定義してLaunchDescriptionオブジェクトを返り値とします。

LaunchDescriptionオブジェクトにアクションを追加していくのですが、このアクションにノードの起動構成や条件を定義していきます。

def generate_launch_description() -> LaunchDescription:

    # LaunchDescriptionオブジェクトの生成
    ld = LaunchDescription()

    # ノード・アクションの追加
    ld.add_action(・・・)
    ld.add_action(・・・)

    return ld

1つ目のアクション定義はbb_paramノード(sma=5, sigma=1.0)です。
launch_ros.actions.Nodeに起動構成を記述していきます。
ここでは、起動ノード名や名前空間(ネームスペース)、パラメータなどを設定することができます。

node_act1 = Node(
    package="ros2_example",
    executable="bb_param",
    name=None,
    namespace="name_space_1",
    parameters=[{"sma": 5, "sigma": 1.0}],
    output="screen",
)

launch_ros.actions.Node()の引数説明

  • package:
    パッケージ名
  • executable:
    ノードの実行ファイル名
  • name:
    起動ノード名を変更したい場合に設定する。
    Noneまたは、記述を省略した場合はデフォルト・ノード名で起動する。
  • namespace:
    指定すると指定した名前空間(ネームスペース)でノードが起動する。
  • parameters:
    パラメータ値を設定する。
    直接値で指定する場合は辞書形式で
    [ { "parameter_name1": value1, "parameter_name2": value2, … } ]
    のように指定する。
    YAMLファイルの指定も可能で、["dir/***.yaml"]のようにパスで指定する。
  • output:
    “screen”を指定すると標準出力にログを出力する。
    デフォルト値は”log”で、この場合はログファイルに出力される。

2つ目のアクション定義もbb_paramノード(sma=10, sigma=2.0)です。

1つ目と同じ要領で記述していきます。

node_act2 = Node(
    package="ros2_example",
    executable="bb_param",
    name=None,
    namespace="name_space_2",
    parameters=[{"sma": 10, "sigma": 2.0}],
    output="screen",
)

全てのアクション定義が終わったらLaunchDescriptionオブジェクトに定義したアクションを追加していきます。

# ノード・アクションの追加
ld.add_action(node_act1)
ld.add_action(node_act2)

④launchファイルからノードを起動

では、作成したlaunchファイルからノードを起動してみましょう!

まずはcolconビルドします。

$ colcon build --symlink-install

ビルドが終わったらワークスペース情報を更新します。

$ source install/local_setup.bash

launchの実行は下記のコマンドになります。

$ ros2 launch ros2_example bb_param_value_launch.py

下記のように表示されれば正常に動作しています。

[INFO] [launch]: All log files can be found below /home/***
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [bb_param-1]: process started with pid [14944]
[INFO] [bb_param-2]: process started with pid [14946]
[bb_param-2] [INFO][name_space_2.bb_param]: bb_param start!
[bb_param-1] [INFO][name_space_1.bb_param]: bb_param start!
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]

1つのターミナルに2つのノードが起動できているのがわかると思います。

このようにLaunch機能を使うことで複数ノードを条件指定で容易に起動できるようになります。
よって、ノードの起動は基本的にros2 runは使用しないでros2 launchを使うようにしましょう!

応用①:YAMLファイルからパラメータを指定して実行

1つ目のbb_param_value_launch.pyではパラメータ値を直接指定しました。

node_act1 = Node(
    package="ros2_example",
    executable="bb_param",
    name=None,
    namespace="name_space_1",
    parameters=[{"sma": 5, "sigma": 1.0}],
    output="screen",
)
node_act2 = Node(
    package="ros2_example",
    executable="bb_param",
    name=None,
    namespace="name_space_2",
    parameters=[{"sma": 10, "sigma": 2.0}],
    output="screen",
)

ここでは応用としてパラメータを直接指定ではなく、YAMLファイルを読み込ませてlaunch実行する方法を紹介します。

まずは、YAMLファイルを準備します。

paramsディレクトリ配下にbb_param.yamlというファイル名を作成します。

ファイルの中身は下記です。

/name_space_1/bb_param:
  ros__parameters:
    sma: 5
    sigma: 1.0

/name_space_2/bb_param:
  ros__parameters:
    sma: 10
    sigma: 2.0

次はファイル名bb_param_yaml_launch.pyを作成し、下記のように記述します。

bb_param_value_launch.pyから変更した行をハイライトしています。)

import os
from ament_index_python.packages import get_package_share_directory
from launch.launch_description import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description() -> LaunchDescription:
    # 読み込ませるYAMLファイル名の定義
    BB_PARAM_YAML = "bb_param.yaml"

    # パッケージ・ディレクトリの取得
    package_dir = get_package_share_directory("ros2_example")

    # YAMLファイル格納場所のフルパスを取得
    yaml_file = os.path.join(package_dir, "params", BB_PARAM_YAML)

    # ノード・アクションの定義
    node_act1 = Node(
        package="ros2_example",
        executable="bb_param",
        name=None,
        namespace="name_space_1",
        parameters=[yaml_file],
        output="screen",
    )
    node_act2 = Node(
        package="ros2_example",
        executable="bb_param",
        name=None,
        namespace="name_space_2",
        parameters=[yaml_file],
        output="screen",
    )

    # LaunchDescriptionオブジェクトの生成
    ld = LaunchDescription()

    # ノード・アクションの追加
    ld.add_action(node_act1)
    ld.add_action(node_act2)

    return ld

YAMLファイルはYAMLファイルが保存されているフルパスを指定する必要があります。

直接指定してもよいですが、下記のようにget_package_share_directory()os.path.join()を使用した方が、簡単で確実だと思います。

# 読み込ませるYAMLファイル名の定義
BB_PARAM_YAML = "bb_param.yaml"

# パッケージ・ディレクトリの取得
package_dir = get_package_share_directory("ros2_example")

# YAMLファイル格納場所のフルパスを取得
yaml_file = os.path.join(package_dir, "params", BB_PARAM_YAML)

あとはフルパスの変数を下記のように指定するだけです。

node_act1 = Node(
    package="ros2_example",
    executable="bb_param",
    name=None,
    namespace="name_space_1",
    parameters=[yaml_file],
    output="screen",
)
node_act2 = Node(
    package="ros2_example",
    executable="bb_param",
    name=None,
    namespace="name_space_2",
    parameters=[yaml_file],
    output="screen",
)

そしたら同じようにlaunchファイルを実行してみましょう!

$ ros2 launch ros2_example bb_param_yaml_launch.py

値の直接指定時と同じ結果になるはずです。

[INFO] [launch]: All log files can be found below /home/***
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [bb_param-1]: process started with pid [14997]
[INFO] [bb_param-2]: process started with pid [14999]
[bb_param-2] [INFO][name_space_2.bb_param]: bb_param start!
[bb_param-1] [INFO][name_space_1.bb_param]: bb_param start!
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][name_space_1.bb_param]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][name_space_2.bb_param]: sma:[10] sigma:[2.0]

応用②:起動ノード名を変更して実行

もう少し応用してみましょう。

【ROS×Python】ROSパラメータ(Parameter)を実装しよう!」の記事で「同じノード名を持つノードは同じ名前空間で起動するとノード名が一意に識別できなくなり不都合を生じる」と説明したのを覚えているでしょうか?

これまでノード名が重複しないように名前空間(ネームスペース)を設定してノードを起動してきましたが、名前空間を設定しないで起動するノード名を変更することでも同じように対処することができます。

ここでは起動するノード名を

  • bb_paramノード(sma= 5, sigma=1.0) → bb_param_1
  • bb_paramノード(sma=10, sigma=2.0) → bb_param_2

のように変更して実行してみましょう!

ファイル名bb_param_yaml_wo_ns_launch.pyを作成し、下記のように記述します。

bb_param_yaml_launch.pyから変更した行をハイライトしています。)

import os
from ament_index_python.packages import get_package_share_directory
from launch.launch_description import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description() -> LaunchDescription:
    # 読み込ませるYAMLファイル名の定義
    BB_PARAM_YAML = "bb_param_without_ns.yaml"

    # パッケージ・ディレクトリの取得
    package_dir = get_package_share_directory("ros2_example")

    # YAMLファイル格納場所のフルパスを取得
    yaml_file = os.path.join(package_dir, "params", BB_PARAM_YAML)

    # ノード・アクションの定義
    node_act1 = Node(
        package="ros2_example",
        executable="bb_param",
        name="bb_param_1",
        namespace=None,
        parameters=[yaml_file],
        output="screen",
    )
    node_act2 = Node(
        package="ros2_example",
        executable="bb_param",
        name="bb_param_2",
        namespace=None,
        parameters=[yaml_file],
        output="screen",
    )

    # LaunchDescriptionオブジェクトの生成
    ld = LaunchDescription()

    # ノード・アクションの追加
    ld.add_action(node_act1)
    ld.add_action(node_act2)

    return ld

起動する名前空間、ノード名が変わるので、YAMLファイル変更内容に合わせて変える必要があります。

ファイル名bb_param_without_ns.yamlを作成し、下記のように記述します。

bb_param.yamlから変更した行をハイライトしています。)

/bb_param_1:
  ros__parameters:
    sma: 5
    sigma: 1.0

/bb_param_2:
  ros__parameters:
    sma: 10
    sigma: 2.0

それでは実行してみましょう。

$ ros2 launch ros2_example bb_param_yaml_wo_ns_launch.py

ノード名が変更されて実行されているのがわかります。

[INFO] [launch]: All log files can be found below /home/***
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [bb_param-1]: process started with pid [16144]
[INFO] [bb_param-2]: process started with pid [16146]
[bb_param-1] [INFO][bb_param_1]: bb_param start!
[bb_param-2] [INFO][bb_param_2]: bb_param start!
[bb_param-1] [INFO][bb_param_1]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][bb_param_2]: sma:[10] sigma:[2.0]
[bb_param-2] [INFO][bb_param_2]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][bb_param_1]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][bb_param_2]: sma:[10] sigma:[2.0]
[bb_param-1] [INFO][bb_param_1]: sma:[5] sigma:[1.0]
[bb_param-1] [INFO][bb_param_1]: sma:[5] sigma:[1.0]
[bb_param-2] [INFO][bb_param_2]: sma:[10] sigma:[2.0]

ノード名が重複する場合に、起動ノード名変更方法をとるか、名前空間を指定する方法をとるかは好みの問題となるので好きな方を選べばよいと思います。

参考までに、両者で実装した場合のノードグラフは下記のような表示となります。

起動ノード名変更パターン

名前空間の指定パターン

さて、ここまでシンプルな起動構成でlaunchファイルからノードを起動する実装例を紹介してきました。

しかし、これはまだ実践的な使用方法ではありません。

次のページではより実践的な使用方法としてlaunchファイルからlaunchファイルへパラメータを渡して実行するlaunchのネスト(入れ子)構造を紹介していきます。

スポンサーリンク

コメント

タイトルとURLをコピーしました