RoboTry

ROSを使ってロボットを動かすぞ

ROS2 Tutorialsをやってみた ~Launchファイルの作成~

今回は以下を実施します。

  • ROS 2 Tutorials (気になるところをメモします)
    • Introspection with command-line tools
    • Passing ROS arguments to nodes via the command-line
    • Launching/monitoring multiple nodes with Launch
  • The launch documentation (気になるところをメモします)
  • Launchファイルの作成
    • Publisher、Subscriberの起動
    • LogInfoの実行
    • TimerActionの実行

前回までのあらすじ

前回はColconのTutorialと、パッケージの作成をしました。

robotry.hatenablog.com

ROS 2 Tutorials

このページを実施します。

Tutorials · ros2/ros2 Wiki · GitHub

Introspection with command-line tools

デーモンの確認

# ターミナルを初めて起動したあと
$ . ~/ros2_overlay_ws/install/setup.zsh 
$ ros2 daemon status
The daemon is not running

# トピックを流してみる
$  ros2 topic pub /chatter std_msgs/String "data: Hello world"
publisher: beginning loop
publishing #1: std_msgs.msg.String(data='Hello world')

publishing #2: std_msgs.msg.String(data='Hello world')

publishing #3: std_msgs.msg.String(data='Hello world')

publishing #4: std_msgs.msg.String(data='Hello world')

^C

# デーモンが起動したことの確認
$ ros2 daemon status  
The daemon is not running

Passing ROS arguments to nodes via the command-line

Name remapping

  • ノードに関わる名前(ノード名、トピック名、サービス名)は静的に変更できる
    • 動的に変更できない
    • $ ros2 run demo_nodes_cpp talker __ns:=/demo __node:=my_talker chatter:=my_topic
    • 詳細はここに書いてある
    • Remapping Names
  • ROS2では1つのプロセスで複数のノードを起動できる。それに対しても名前変更可能
    • 詳細はCompositionのチュートリアルで理解する
    • $ ros2 run composition manual_composition talker:__node:=my_talker listener:__node:=my_listener

Logger configuration

  • ログのレベルを引数で指定できる(詳細はLoggingのデモで理解する)
    • $ ros2 run demo_nodes_cpp listener __log_level:=debug

Paramteres

Launching/monitoring multiple nodes with Launch

The launch documentation

launchの詳細なドキュメントはここ

launch/architecture.rst at master · ros2/launch · GitHub

上に挙げた2つのlaunchファイルと見比べながら読み解きます

Launch Entities and Launch Descriptions

  • launchのメインオブジェクトはlaunch.LaunchDescriptionEntity
    • 2つ目の例でreturnしてるやつ
    • launchの仕方、非同期なイベントへの対応をここに記述する
    • visitという、起動中(launching)に呼ばれるメソッドを持つ
    • describeという、振る舞いを考える際(introspection)に呼ばれるメソッドを持つ
    • asyncio.Futureというメソッドを、非同期な活動を持ったときに返す
  • visitしたとき、次にvisitされる実体(entities)が生成される
  • launch.LaunchDescriptionlaunch.Actionの集合を持つ
    • actionsはイベント対応時に実行される
  • launch descirptionとactionsはlaunch.Substituionへの参照をもつ
    • substitutions ではlaunchの設定や環境変数を得たり、Pythonの式を評価したりする
  • launch descriptionとactionsは、直接実行されるだけでなくlaunch.LaunchServiceから起動(launch)することもできる
    • launch serviceはイベントループを操作したり、actionsを発行したりする

Actions

  • Actionsはプロセスを実行したり、設定値をセットしたりできる
  • Actionsは追加のactionsを生成できる
  • Actionsは引数を持つ
    • 引数はlaunch.Substitutionが与えることができる
Basic Actions
  • 10個以上のactionsがある(ここでは上記の2例で使われているactionsをピックアップする)
  • launch.actions.RegisterEventHandlerlaunch.EventHandlerを登録する
    • EventHandlerはユーザが定義できるラムダ式で、イベントを対処する
    • イベントは1つのイベントだったり、イベントの集合だったりする
    • 1つ目の例ではevent_handlers.OnStateTransitionが登録されている
    • 2つ目の例ではevent_handlers.OnProcessExitが登録されている
  • launch.actions.LogInfoはユーザが定義したメッセージのログを取る
    • LogInfo以外にもLogWarmもある(たぶんLogDebugもあると思う)
    • 1つ目の例で使っている
  • launch.actions.EmitEventlaunch.Eventを発行する
    • 1つ目の例ではevents.lifecycle.ChangeStateを発行している
    • 2つ目の例ではevents.Shutdownを発行している

Substitutions

  • 上記2例では使われていないので読み飛ばす

The Launch Service

  • 主に3つのサービスを担う
    • launch description を組み込む(include)
    • イベントループを回す
    • シャットダウンする
      • 動いてるactionsとevent handlersを止める
      • イベントループを止める
  • 代表的な使用例は
    • launch descriptionを作るとき(プログラムやマークアップファイルによる生成)
    • launch serviceを作るとき
    • launch descriptionをlaunch serviceに組み込むとき
    • shutdown時に呼ばれるsignal handlerを追加するとき
    • launch service のイベントループを回すとき
  • 1つ目の例でlaunch serviceが使われている
    • 最初にデフォルトのlaunch descriptionを読み込んでいる
    • 次に、複数のactionsを追加したlaunch descriptionを読み込んでいる
    • 最後にreturn ls.run()でイベントループを回している
  • 対照的に、2つ目の例ではlaunch serviceが使われていない
    • return launch.LaunchDescriptionを使用している
  • 別スレッドでサーバを立てると動的にlaunch serviceを読み込めるみたいだけど、よくわからない

Event Handlers

  • Event ahndlersは主に2つのメソッドを定義する
    • launch.EventHandler.matchesはeventを入力し、eventが適合するとTrueを返す
    • launch.EventHandler.handleはeventとlaunch contextを入力し、launch.LaunchDescriptionEntityのリストを返す(任意)

拡張するときのポイント

  • 新しいactions
    • launch.Actionクラスを直接/間接的に継承すること
  • 新しいevents
    • launch.Eventクラスを直接/間接的に継承すること
  • 新しいsubstitutions
    • launch.Substitutionクラスを直接/間接的に継承すること
  • launch descriptionの実体(entities)
    • launch.LaunchDescriptionEntityクラスを直接/間接的に継承すること
  • 将来的にはsetuptoolsのentry_pointみたいな拡張方法が使えるようになるみたい

Launchファイルを実装する

読んでるだけだと理解できないので、 前回作成したPub/Subノード用のlaunchファイルを作成します。

作成したパッケージがこちらです。

github.com

各クラス・メソッドについてのドキュメントが見つからなかったので、 ソースコードを読みながらTimerAction機能を追加しました。

GitHub - ros2/launch: Tools for launching ROS nodes.

パッケージディレクト

  • launch、cpp、pythonを同じ階層にしたかったが実現できなかった
    • launchをcpp(first_cpp_pkg)の一部として組み込むことで解決した
first_ros2_pkg
├── README.md
├── cpp
│   ├── CMakeLists.txt
│   ├── first_sub_node.cpp
│   ├── launch
│   │   └── pub_sub.launch.py
│   └── package.xml
└── python
    ├── first_pub_node.py
    ├── package.xml
    ├── setup.cfg
    └── setup.py

launchファイル(pub_sub.launch.py)

from launch import LaunchDescription

import launch.actions
import launch_ros.actions


def generate_launch_description():
    return LaunchDescription([
        # Launch時にメッセージを出力
        launch.actions.LogInfo(
            msg="launch publisher node and subscriber node"),

        # Launchしてから5秒後にメッセージを出力
        launch.actions.TimerAction(period=5.0,actions=[
            launch.actions.LogInfo(
                msg="----------This is a TimerAction!!!---------"),
            ]),

        # Publisherノードを起動
        # first_pub_nodeは別パッケージのノードだが、問題なく起動する
        launch_ros.actions.Node(
            package='first_python_pkg',
            node_executable='first_pub_node',
            output='screen'),

        # Subscriberノードを起動
        launch_ros.actions.Node(
            package='first_cpp_pkg',
            node_executable='first_sub_node',
            output='screen'),
        ])

CMakeLists.txt

~~~中略~~~
add_executable(first_sub_node first_sub_node.cpp)
ament_target_dependencies(first_sub_node rclcpp std_msgs)

install(TARGETS
    first_sub_node
    DESTINATION lib/${PROJECT_NAME})

# launchディレクトリをshareへインストールする記述を追加
install(DIRECTORY
    launch
    DESTINATION share/${PROJECT_NAME})

ament_package()

package.xml

~~~中略~~~
  <exec_depend>rclcpp</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  
  <!-- 追加 -->
  <exec_depend>launch_ros</exec_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

実行結果

$ ros2 launch first_cpp_pkg pub_sub.launch.py
[INFO] [launch.user]: launch publisher node and subscriber node    # LogInfoが働いている
[INFO] [launch]: process[first_pub_node-1]: started with pid [828]
[INFO] [launch]: process[first_sub_node-2]: started with pid [829]
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 0"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 0'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 1"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 1'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 2"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 2'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 3"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 3'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 4"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 4'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 5"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 5'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 6"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 6'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 7"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 7'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 8"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 8'
[INFO] [launch.user]: ----------This is a TimerAction!!!---------   # TimerActionを使ってlaunchしてから5秒後にメッセージを出力している
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 9"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 9'
[INFO] [first_pub_node]: Publishing: "Hello World! ^^: 10"
[INFO] [first_sub_node]: I heard: 'Hello World! ^^: 10'

PublisherとSubscriberが起動したこと、 LogInfoが機能したこと、 TimerActionが機能したことが確認できました。

次のステップ

次回はROS 2 TutorialsのWorking with multiple RMW implementationsからやっていきます。