駆け抜けるROS2
はじめに
こんにちは、ROS2で自律航行システム等を開発している片岡というものです。
さて、自分は数十個のROS2パッケージを開発して様々なソフトウェアを実装してきましたが、何度も何度もROS2の仕様やあまりドキュメントがない点に苦しめられてきました。
今回はROS2を一切やったことない人がROS2開発を始めるにあたって何から手をつけていくと良いかと自分が考えていることについててまとめていきたいと思います。
あくまで自分の私見ですので、これが絶対に正しいというものではないのでそこはご了承ください。
よく出てくる疑問
まずは、自分がTwitterや勉強会等でよく見る疑問に関して個人的な回答をしておきたく思います。
ROS2をやる前にROS1をやったほうがいいのか?
Yesです。ただし正確に言うとPub/Sub型の通信モデルでマイクロサービスアーキテクチャでソフトウェアを組んだ経験を積んでおいたほうがよいという意味です。
MQTTでIoT関連のことをされていたりSwaggerやGRPCでWebアプリケーションを作っていた方に関してはあまり深くROS1を触らずともROS2に行ける可能性が高いと考えます。
ただし、現状C++以外のクライアントが後述するComponentに対応しておらずC++での開発経験はマストだと思います。
ROS2のツールはどのくらい揃っているのか?
ある程度は揃ってきています。
筆者がよく使うのは以下のようなものでしょうか?
ログデータを管理するrosbag2
データ可視化用のrviz2
自己診断用ros2 docotr command
正直、ホビーユースのロボットを作る程度にはほぼ問題ないレベルでツールが揃っています。
ですが完全にブラックボックスとしてヘビーユースしたいという方々からみると、rosbagにも足りていない機能があったりrvizにも高頻度でmarkerを出すと消えるといった不具合があったりして「不具合があれば自分で直す、ない機能は作ってしまえ」という精神がないと厳しいです。
ドキュメントも正直そこまで追いついているわけではありません。
ROS2はROS1より性能がいいのか?
ROBOSYM 2020にfuRoの原先生が出された各種ロボットミドルウェア性能評価に関する発表[1]があります。
今より少し古いROS2なので、今より少し性能は落ちると思いますが非常に参考になると考えます。
原先生が作られた以下の表より自分は以下のように結論付けています。
- Table3,4を比較するとプロセス間通信をした場合かなりのケースにおいてROS1のほうが性能が良い。
- ただし、データサイズが小さく、Cyclone DDSを使った場合にはROS2の方が性能がでるケースがある。
- Executorを使ってプロセス内通信を使った上でデータ量が小さいなら、Cyclone DDS、データ量が多いトピックならQoSを設定した上でFast RTPSを使えばを使えばほぼROS1よりパフォーマンスがでる。
- (推測ではあるが)おそらく一部のDDSはすべてUDPで通信しており(Conntextは10MByte以外の項目においてプロセス内通信、プロセス外通信どちらでもレイテンシおよび受信抜けの値がかなり近い)、設定によってはExecutorを使っても高速化されなかったりごく稀に悪化するケースもある。
また、ROS1との比較ではありませんがなぜROS2でプロセス間通信をしてしまうとROS1より悪化するケースがあるかの分析にはarxivに投稿されたこちらの論文[2]も非常に参考になりました。
上記論文のFig6を参照すると、DDSの通信によるレイテンシ(ネットワークを介したDDSトランスポートと、実際にメッセージを受信するためのファンクションコールで発生するレイテンシー、帯グラフの赤い部分)も大きいのですが、RCLCPP Notification Delay(新しいデータが利用可能であることをDDSがROS2に通知してから、実際に検索が開始されるまでの時間差、帯グラフのオレンジ色の部分)とDDS(DDSの通信データをROS2のメッセージ型)が非常に効いていることがわかります。
グラフを見る限りではRCLCPP Notification DelayによってDDSによる遅延と同程度の遅延が発生していることがわかります。
つまりデータは届いているがその後処理に時間がかかっておりユーザーが作成するアプリケーション部分に到達するころにはROS1より性能が落ちているケースもありうる。
ということではないかと推測しています。
で?ROS2って何がいいの?
自分はROS2の良さは大きく以下の2点だと考えています。
- かなり細かいチューニングができる
- C++を使う限りにおいては誰が書いても同じようなコードになるため他人のコードを読みやすい
それぞれ解説をしていくと、ROS2にはQoSの概念があり適切に通信の設定を決めることでパフォーマンスを突き詰めることができます。ROS1はOptionが3つとリングバッファのサイズを指定するパラメータしかなく、パフォーマンスチューニングをやりたくてもできないという問題がありました。
また、Nodeletにより大容量トピックの通信をプロセス内通信にすることもできましたが、クラッシュしたときにデバッグが大変だったり、rqt_graphがおかしくなったりとあまりよろしくない副作用も発生しました。
ROS2におけるNodeletの後継機能であるComponentはComponent Containerに乗せることでクラッシュしたらクラッシュしたComponentだけをContainerから切り離してアプリケーションの実行を継続できます。
また、Componentはrclcpp::Nodeクラス等のNodeInterfaceを持つクラスを継承して作るため実装にたいして制約がかかります。
そのため、ROS1のようにroscpp::NodeHandleを好きなところに何個でも作って引っ張り回し、自分で自由にスレッドを切って好き勝手できたプログラミング環境と比較して他人のコードの構造把握をするときに「ここを見れば何やってるかわかる」という箇所が明確であるため非常に他人のコードを取り込んだりレビューしたりするプロセスを高速化できます。
で、結局どっちを使うといいの?
これはもう開発したいものの内容とその方のバックグラウンドに依存します。
以下に自分の私見ではありますが、こういう感じじゃないかな〜と思っているフローチャートを書いたので乗せておきます。
参考になれば幸いです。
なぜこのようなフローチャートになるかというと、ROS2はまだまだ未完成で足りていないツールも多いですし、後述するComponentとExecutorというパフォーマンスを上げていくための仕組みを使えるのが現状C++とCのクライアントライブラリにしかありません。
ドキュメントの量等を考慮すると実質的にはC++で書かないとどうしようもないというのが現状かなと認識してます。
pythonでもかけなくはないですが、大幅にパフォーマンスを落とすことを覚悟する必要があります。
また、ドキュメントされていない仕様やTipsも多く、困ったらヘッダーを読んで自分のノードのコンパイルを通すこともしばしばです。
「ロボットのアルゴリズムの研究がやりたい!」という目的でROS2を使うとおそらくデバッグの段階で「なんで俺はアルゴリズムの研究したいのに延々C++やCmakeと格闘してるんだ。。。。。」と発狂しそうになるでしょう。
ROS1のEOLまでまだ4年もあるので、趣味開発等で知見を蓄えておいてROS2慣れてきたしもう移行してもいいだろうというタイミングがきた時に移行を行うのがベストかなと思います。
駆け抜けるROS2
ここまでいろいろROS2の利点欠点、どんな人に合うか、どんな人には合わないかを述べてきましたが、ここからはROS2を最短で書けるようになるにはここは抑えておいたほうがいいんじゃないかというポイントを独断と偏見でピックアップしたいと思います。
Component指向
ROS2ではComponentという単位でノードを開発し、Executorという実行機の上でComponentを動作させることによりシングルプロセスの上に複数のノードを乗せることができます。これによりコンポーネント間通信を排して効率よくノード間通信が行えます。
詳細はこちらのドキュメントを参照してください。
Componentの実装上で一点気をつけておいたほうがいいのは、
- 自分でstd::thread等を使ってスレッドを切らない
- service等を呼び出すときにはできるだけ非同期呼び出しを使う
の二点です。
Componentの中ではCallback関数はCallback Groupという形が管理され、Executor側でSpinしてallbackを回します。
自作スレッドを切ってしまうと、Callback Groupに登録されていないスレッドが存在することになるため、ノードが正常に終了しない可能性があります。
また、rclcppの一部の同期呼び出しAPIは何故かその場でSpinして結果の受け取りを待とうとするため、Executor側でSpinしてさらにそのコールバックの中でSpinすることになりサービスの発行元と受信元が同じExecutor上にあった場合等にデッドロックが発生しえます。
ROS2のサービスは非同期での呼び出しに対応しているため、そちらを使用したほうが後からトラブルは少ないかなと思います。
ちなみに、こちらの記事のように自分以下のようなrclcpp::spinを書いた場合
#include <rclcpp/rclcpp.hpp>
#include "minimal_subscriber.hpp"
int main(int argc, char * argv[]){
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
こちらの関数が呼び出され
SingleThreadedExecutorの上に一個のComponentが乗ったプロセスができあがります。
ユーザーから見ると1ノード1プロセスとなりシンプルですが、Executorのspinには実行コストが高くあまり多用するとパフォーマンスを大きく落とす原因になるのでExecutorの上によく通信するモジュール同士は乗せておいたほうがよいでしょう。
Component指向でかけてドキュメントが十分に揃っているクライアントはrclcppしか現状見当たらないため、ロボット開発者の方がROS2を使う場合、実質的にほぼC++以外の選択肢はない状況かなと思います。Pythonでもかけないことはないですが、大幅にパフォーマンスを落とすことと、若干PureなPython Packageと作り方の異なるパッケージの作り方に慣れたり、いろいろ手探りでやっていく必要があります。
launch.py
ROS2ではlaunchファイルがpythonになるという話がありましたが、自分はこれはミスリーディングだと考えています。
ROS2ではxml、yamlといった様々なフォーマットのlaunchファイルがサポートされており、その仕組みはyamlやxmlをパースしてLaunchDescription型というジョブキューを作成し、LaunchServiceというPythonのクラスに引き渡すことで実現されています。
つなりlaunch.pyはROS1時代に存在していたroslaunch APIをROS2向けに再設計したものでありlaunch.pyは露出されたAPIをダイレクトに叩いていることと等価にあたります。
すべてのlaunchファイルフォーマットのバックエンドにはlaunch.pyが存在しているため、launch.pyを使えばxmlやyamlでできることはなんでもできることになりますし、逆にxmlやyamlには実装されていないような拡張も使用できます。
なので、自分は個人的にはlaunch.pyの設計に大きな不満はありますが、パフォーマンスのためにlaunch.pyを使用しています。
vendor package
vendor packageはpure cmakeのソースコードを取り込むためのROS2パッケージ作成方法です。
詳細はこちらに乗せてあります。
vendor packageを使えば他の方が作成してくれたpure cmakeで作られたライブラリを簡単にバージョン指定までした上でROS2の世界に持ってくることができるため、開発を大幅に効率化できます。
ros2_control,hardware_interface
ROS2と実機のインターフェースを作成するためのパッケージ群、理解しておくと簡単に実機とROS2をつなげるようになります。
以前rosjpで勉強会が開催された際の資料が非常に参考になります。
また、Hardware Interfaceに関しても自分がDynamixelのHardware Interfaceを書きましたので、参考になればと思います。
上記の4項目が使える状態にないればあとはロボットの上で動かすアルゴリズムをひたすらcomponentの上に実装していくだけです!!
楽しい!!
✌ ('ω' ✌ )三 ✌ ('ω') ✌ 三( ✌ 'ω') ✌
最後に
なんかzennはいいらしいぞという話を聞いたのと、ちょうど自分と同じような問題意識を持っている人が現れたので投稿してみるかーということで書いてみました。
自分はROS2はハマりどころとROS1との違いさえわかっていればROS1より圧倒的に書きやすいミドルウェアだと感じますし、これからもROS2を使っていくと思います。
今後のROS2開発者の助けになれば幸いです。
Discussion