MoriKen's Journal

MoriKen's Journal

アラサー社会人博士による徒然日記。技術についてつらつら。だけだとコンテンツが貧弱なので、会社公認で大学院博士課程に進学した経緯や、独学でTOEICを475→910にしたノウハウを共有します。

【ROS 2】シングルプロセスで複数のノードを構成する(公式文書和訳)

Sponsored Link

ROS 2 公式文書(英語) 日本語訳シリーズです。

本ブログの日本語翻訳版のトップページは以下のリンクを参照下さい。

www.moriken254.com

※2019/05/07 現在のものです。

ROS 1 - ノード vs. ノードレット

ROS 1 では、ROS ノードとしても ROS ノードレットとしてもコードを書くことができます。 ROS 1 ノードは実行ファイルとしてコンパイルされます。一方、ROS 1 ノードレットは共有ライブラリとしてコンパイルされており、共有ライブラリは実行時にコンテナプロセスによってロードされます。

ROS 2 - 統一されたAPI

ROS 2 では、ノードレットとしてコードを書くことが推奨されます - それを「コンポーネント」と呼びます。これにより、既存のコードに対しても ROS 2 共通のコンセプト(例:ライフサイクル)を容易に追加できます。ノードとノードレッドで API が異なるという最大の欠点は、ROS 2では解決されています - どちらのアプローチでもあっても、ROS 2 なら同じ API を使用できるのです。

ノードのように、「コード内に main を記述する」というスタイルを取ることも可能ではあるのですが、一般的には推奨されません。

プロセスレイアウトを展開時の決定にすることで、ユーザーは次のいずれかを選択できます。

  • 別々のプロセスで複数のノードを実行する。

    • 各ノードのデバッグの容易さや、プロセス/障害を分離できるという利点が活かせる。
  • 単一のプロセスで複数のノードを実行する(プロセス内通信を参照)。

    • 低オーバーヘッドや、その他オプションのよる通信の効率化が可能。

更に、ros2 launchは、特殊な起動アクションを通して、これらのアクションを自動化することもできます。

コンポーネントを書く

コンポーネントは共有ライブラリに組み込まれているだけなので、main 関数はありません(Talker のソースコードを参照)。コンポーネントは通常、rclcpp::Nodeのサブクラスです。スレッドで制御されていないため、コンストラクタ内で長時間実行されているタスクや、ブロッキングタスク(イベント待ちのタスク)も実行するべきではありません。代わりに、タイマを使って定期的に通知を受信することができます。さらに、パブリッシャ、サブスクライバ、サーバ、およびクライアントを作成できます。

このようなクラスをコンポーネントにすることの意義は、そのクラスがパッケージrclcpp_componentsからのマクロを使用して登録される点です(ソースコードの最後の行を参照)。これにより、そのライブラリが実行中のプロセスにロードされているとき、そのコンポーネントが検出可能になります - それは一種のエントリポイントとして機能します。

更に、コンポーネント作成の際には、他のツールから検出できるように、インデックスに登録する必要があります。

add_library(talker_component SHARED
   src/talker_component.cpp)
rclcpp_components_register_nodes(talker_component "composition::Talker")
# To register multiple components in the same shared library, use multiple calls
# rclcpp_components_register_nodes(talker_component "composition::Talker2")

注意:component_containerが目的のコンポーネントを見つけることができるようにするには、対応するワークスペースを提供しているシェルから実行または起動する必要があります。

コンポーネントを使う

コンポジションパッケージには、コンポーネントを使用する方法がいくつかあります。 代表的な3つを以下に示します。

  1. プログラムやコマンドラインからの呼び出し

    • コンテナプロセスを起動し、コンテナによって提供された ROS サービス load_node を呼び出します。
    • ROS サービスは渡されたパッケージ名とライブラリ名に合致するコンポーネントをロードし、実行中のプロセス内でそれを開始します。
    • プログラムで ROS サービスを呼び出す代わりに、コマンドラインツールと引数を組み合わせて ROS サービスを呼び出すこともできます。
  2. カスタム実行ファイルからの呼び出し

    • コンパイル時に認識される複数のノードを含むカスタム実行可能ファイルを作成します。この方法では、各コンポーネントにヘッダーファイルが必要です(プログラムが小さければ必ずしも必要ではありません)。
  3. launch による呼び出し

    • launch ファイルを作成し、ros2 launchを使用して、複数のコンポーネントがロードされたコンテナプロセスを作成します。

デモ

デモは、rclcpp_componentsros2component、およびコンポジションパッケージの実行可能ファイルを使用し、以下のコマンドで実行できます。

利用可能なコンポーネントの検出

どのコンポーネントがワークスペースに登録され使用可能になっているかを確認するには、シェルで次のコマンドを実行します。

$ ros2 component types
composition
  composition::Talker
  composition::Listener
  composition::Server
  composition::Client

パブリッシャとサブスクライバで ROS サービス(1.)を使ったランタイムコンポジション

シェルで、コンポーネントコンテナを起動します。

ros2 run rclcpp_components component_container

ros2コマンドラインツールを使ってコンテナが実行されていることを確認します。

$ ros2 component list
/ComponentManager

2番目のシェル(talker のソースコードを参照)このコマンドは、ロードされたコンポーネントの一意の ID とノード名を返します。

$ ros2 component load /ComponentManager composition composition::Talker
1 /talker

これで、このシェルは、コンポーネントがロードされたというメッセージと、メッセージをパブリッシュするためのメッセージを繰り返し表示するはずです。

2番目のシェルの別のコマンド(listener のソースコードを参照)

$ ros2 component load /ComponentManager composition composition::Listener
2 /listener

ros2コマンドラインユーティリティを使用して、コンテナの状態を調べることができます。

$ ros2 component list
/ComponentManager
   1  /talker
   2  /listener

最初のシェルが、各受信メッセージの繰り返し出力するはずです。

サーバとクライアントでROSサービス(1.)を使ったランタイムコンポジション

サーバとクライアントの例も、パブリッシャとサブスクライバと同様です。

最初のシェルでは:

ros2 run rclcpp_components component_container

2番目のシェル(サーバクライアントのソースコードを参照)

ros2 component load /ComponentManager composition composition::Server
ros2 component load /ComponentManager composition composition::Client

この場合、クライアントはサーバーに要求を送信し、サーバーはその要求を処理して応答を返し、クライアントは受信した応答を出力します。

ROSサービス(2.)を使ったコンパイル時の合成

このデモでは、複数のコンポーネントを実行する単一の実行可能ファイルをコンパイルするのに、同じ共有ライブラリを再利用できることを示します。実行可能ファイルには、上記の4つのコンポーネントすべて(トーカーとリスナー、およびサーバーとクライアント)が含まれます。

シェル呼び出しの場合(ソースコードを参照):

ros2 run composition manual_composition
  • manual_composition.cpp
// Copyright 2016 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <memory>

#include "composition/client_component.hpp"
#include "composition/listener_component.hpp"
#include "composition/talker_component.hpp"
#include "composition/server_component.hpp"
#include "rclcpp/rclcpp.hpp"

int main(int argc, char * argv[])
{
  // Force flush of the stdout buffer.
  setvbuf(stdout, NULL, _IONBF, BUFSIZ);

  // Initialize any global resources needed by the middleware and the client library.
  // This will also parse command line arguments one day (as of Beta 1 they are not used).
  // You must call this before using any other part of the ROS system.
  // This should be called once per process.
  rclcpp::init(argc, argv);

  // Create an executor that will be responsible for execution of callbacks for a set of nodes.
  // With this version, all callbacks will be called from within this thread (the main one).
  rclcpp::executors::SingleThreadedExecutor exec;
  rclcpp::NodeOptions options;

  // Add some nodes to the executor which provide work for the executor during its "spin" function.
  // An example of available work is executing a subscription callback, or a timer callback.
  auto talker = std::make_shared<composition::Talker>(options);
  exec.add_node(talker);
  auto listener = std::make_shared<composition::Listener>(options);
  exec.add_node(listener);
  auto server = std::make_shared<composition::Server>(options);
  exec.add_node(server);
  auto client = std::make_shared<composition::Client>(options);
  exec.add_node(client);

  // spin will block until work comes in, execute work as it becomes available, and keep blocking.
  // It will only be interrupted by Ctrl-C.
  exec.spin();

  rclcpp::shutdown();

  return 0;
}

これにより、サーバとクライアントだけでなく、talker と listener の両方のペアからのメッセージが繰り返し表示されます。注:手動で作成したコンポーネントは、ros2コマンドラインツールの出力には反映されません。

dlopen を使った実行時の合成

このデモは、一般的なコンテナプロセスを作成し、ROS インタフェースを使用せずにロードするライブラリを明示的に渡すことで、1.の代替方法を示します。プロセスは各ライブラリをオープンし、ライブラリのソースコード内の各「rclcpp :: Node」クラスのインスタンスを1つ作成します。

  • Linux シェル呼び出しの場合:
ros2 run composition dlopen_composition `ros2 pkg prefix composition`/lib/libtalker_component.so `ros2 pkg prefix composition`/lib/liblistener_component.so

OSX シェル呼び出しの場合:

ros2 run composition dlopen_composition `ros2 pkg prefix composition`/lib/libtalker_component.dylib `ros2 pkg prefix composition`/lib/liblistener_component.dylib

Windows の cmd.exe 呼び出しの場合

ros2 pkg prefix composition

により、コンポジションがインストールされている場所へのパスを取得してから、以下のコマンドを実行します。

ros2 run composition dlopen_composition <path_to_composition_install>\bin\talker_component.dll <path_to_composition_install>\bin\listener_component.dll

これで、シェルは送受信されたメッセージごとに繰り返し出力するはずです。

注:dlopen 構成のコンポーネントは、ros2 コマンドラインツールの出力には反映されません。

launch アクションを使用した合成

コマンドラインツールはコンポーネント設定のデバッグと診断に役立ちますが、コンポーネントのセットを同時に起動するほうがもっと便利です。これを自動化するために、ros2 launchの機能を使うことができます。

ros2 launch composition composition_demo.launch.py

発展ネタ

コンポーネントの基本的な操作について説明したので、もう少し高度なネタについて説明します。

コンポーネントをアンロードする

最初のシェルで、コンポーネントコンテナを起動します。

ros2 run rclcpp_components component_container

ros2コマンドラインツールを使ってコンテナが実行されていることを確認します。

$ ros2 component list
/ComponentManager

2番目のシェル(talker のソースコードを参照)このコマンドは、ロードされたコンポーネントに対するユニークな ID とノード名を返します。

$ ros2 component load /ComponentManager composition composition::Talker
1 /talker
$ ros2 component load /ComponentManager composition composition::Listener
2 /listener

このユニークな ID を使用して、コンポーネントコンテナからノードをアンロードします。

$ ros2 component unload /ComponentManager 1 2
Unloaded component 1 from /ComponentManager container
Unloaded component 2 from /ComponentManager container

最初のシェルで、talker と listener からの繰り返しメッセージが停止したことを確認します。

コンテナ名と名前空間のリマップ

コンポーネントマネージャの名前と名前空間は、標準のコマンドライン引数でリマップできます。

ros2 run rclcpp_components component_container __node:=MyContainer __ns:=/ns

2番目のシェルでは、更新されたコンテナ名を使ってコンポーネントをロードできます。

ros2 component load /ns/MyContainer composition composition::Listener

注:コンテナーの名前空間のリマップは、ロードされたコンポーネントには影響しません。

コンポーネント名と名前空間をリマップ

コンポーネント名と名前空間は load コマンドの引数で調整することができます。

最初のシェルで、コンポーネントコンテナを起動します。

ros2 run rclcpp_components component_container

名前と名前空間を再マッピングする方法の例

ros2 component load /ComponentManager composition composition::Talker --node-name talker2
# Remap namespace
ros2 component load /ComponentManager composition composition::Talker --node-namespace /ns
# Remap both
ros2 component load /ComponentManager composition composition::Talker --node-name talker3 --node-namespace /ns2

対応するエントリがros2 component listで表示されます。

$ ros2 component list
/ComponentManager
   1  /talker2
   2  /ns/talker
   3  /ns2/talker3

注:コンテナーの名前空間のリマップは、ロードされたコンポーネントには影響しません。

翻訳元文書

index.ros.org

関連文書

www.moriken254.com

www.moriken254.com