この記事は、GROOVE Xアドベントカレンダー2024 の7日目の記事です。
GROOVE X で自己位置推定のソフトウェア開発をしている Mチーム のtakadaです。
(Mチームのお仕事については「LOVOTのマップの作り方」を参照)
ロボット開発の現場では、「テスト」がいかに重要か、日々痛感しています。特に、自己位置推定のような複雑なシステムでは、リリース前にどれだけしっかりテストできるかが、実機でのパフォーマンスや信頼性に直結します。
Mチームでは、シミュレータを活用したCI(継続的インテグレーション)のテスト環境を導入しています。今回の記事では、その背景や構成、そして導入してみて感じたメリットや課題について紹介したいと思います。
少しでも同じ課題に悩む方の参考になれば幸いです!
なぜシミュレータが必要か?
スクラム開発を採用しているGROOVE Xでは、各ソフトウェアチームは2週間のスプリントごとに成果物となるソフトのdaily OS(社内向けに毎日ビルドされるOS)へのリリースが求められます。daily OSは社内用とはいえ、他の開発チームへの影響を考えると、バグを残せないためリリース前の動作確認は重要です。
(daily OSについては「LOVOTのOSのつくりかた」を参照)
当初はリリース前に実機を使ったテストを行っていましたが、以下のような基本的な動作を確認するだけでも1回のテストに約2時間かかっていました!
- 機体からマップを削除
- LOVOTに新しいマップを作ってもらう
- できたマップで、自己位置推定が正しく機能しているかを確認する
もちろんユニットテストなどで担保されている部分も多いのですが、自己位置推定(localization)や座標変換システム(tf)、意思決定エンジン(NeoDM)といった複数のサービスが連動する結合テストは実機でしか確認できず、リリースの負担となっていました。
この課題を解決するため、2年ほど前からシミュレータをCIテストとして導入しています。
シミュレータを使うことで以下のメリットがあります。
- テスト環境を統一できる
- LOVOTを好きな位置に簡単に移動可能
- groundtruth(真の位置情報)が取得しやすい
これが自己位置推定のテストではとても大事!
現状全ての機能のテストがカバーされているわけではないのですが、前述したような基本機能(マップ作成から自己位置推定)のテストが自動化され、リリースのハードルが大きく下がりました。
次に、このシミュレータの構成について紹介します。
シミュレータの構成
CIテストにはGazeboをROS環境上で動作させて、LOVOTを模擬しています。 ROS環境では、センサデータ(カメラ画像、IMU、ToF測距情報)やアクチュエータ指示値(ホイール回転速度、サーボ角)などは当然rostopicでやり取りされます。
一方、LOVOTの実機では、センサデータ・アクチュエータ指示値の入出力にはgRPC通信が使用されています(センサデータの一部はHTTPや共有メモリ経由のものもあります)。
(gRPCについては「LOVOT と gRPC」を参照)
そこで、rostopicとgRPCを仲介するブリッジを立てて、シミュレータとテスト対象となるサービスを接続しています。
これにより、テスト対象のサービスはほぼプロダクトコードそのままでシミュレータ環境上で動作させることができるようになり、より実運用に近い形でのテストが可能になっています。
シミュレータとテスト対象のサービスはDocker Composeを使い、独立したコンテナとして管理しています。 この構成により、サービス間の依存関係を切り分けたまま一貫した環境で動作させることが可能です。 また、テストケース自体はPython上でpytestを用いて記述しており、Docker Composeで起動したサービスと連携して動作確認を行っています。
一例として自己位置推定を評価するテストコードは以下のように書かれています。
# simulatorのfixtureによって、docker-compose経由でシミュレータが実行される async def test_localization_accuracy(simulator): # 機体の位置を初期位置にリセット initial_pose = "0.5,0.5,0.0,0.0,0.0,0.0,1.0" await simulator.simulation.reset_pose(initial_pose) # 2分間、自己位置の推定結果をモニタリング tf_monitor = simulator.transforms_monitor localization_errors = defaultdict(lambda: np.empty((0, 2))) with trio.move_on_after(120): async for _ in tf_monitor.sync(): # gazeboから得られる真の自己位置情報 groundtruth = tf_monitor.groundtruth_world # LOVOTの自己位置推定器が推定した位置情報 localized_w = tf_monitor.localized_world # 推定誤差を算出 error = _compare_transforms_2d(groundtruth, localized_w) localization_errors = np.append(localization_errors, [error], axis=0) # 平均で、位置が0.2m未満、角度が15度未満の誤差であることを確認 rmse = np.sqrt(np.mean(localization_errors ** 2, axis=0)) assert np.all(rmse < np.array([0.2, np.radians(15)]))
CIとしての構成
前の章で構築したシミュレータによるテストはJenkinsを利用してdailyで自動実行されています。
Jenkins上には2つのpipelineがあり、
まずテスト実行前に、テスト対象のパッケージがインストールされたDockerイメージをビルドします。(上図のmonitor-os-update
)
ここでDockerイメージにインストールされるパッケージは、その日にビルドされたdaily OSに含まれているパッケージが対象になります。
続いて、そのイメージをもとに、pytestで記述されたテストケースが実行されます(同simulator-based-test
)。
テスト完了するとslackで通知が来る仕組みです。
毎朝実行されるので、failedの通知が来ると不具合解析から一日の仕事がスタートすることになります。。
テスト失敗時のデバッグ支援
テストが失敗した際の解析を効率化するため、pytestの結果と合わせて以下の情報を保存しています。
- 各サービスのログ
- シミュレータ環境内の録画動画
多くの場合、ログメッセージを確認するだけで原因が特定できますが、まれにシミュレーション中にLOVOTが転倒するなど、動作そのものを確認しないと原因がわからないケースもあります。そのような場合、録画された動画が役立ちます。
よかったことと課題
CI化してよかったこと
CI環境を構築したことで、以下のようなメリットを得られました。
- スプリント毎の実機テストから解放
従来、2時間ほどかかっていた実機を使ったテストが不要となり、リリース前の負担が大幅に軽減されました。その結果、スプリントごとのリリースがよりスムーズになりました。 - バグを早期に検知
マップ作成に失敗するような致命的なバグを含め、いくつかのバグを発見することができています。これにより、製品の品質を維持しつつ、リリースサイクルの効率を向上させることが可能になりました。
課題
一方で、いくつか課題も残っています。
- 解析の難易度が高い
テストが失敗した際、pytestのログ、各サービスのログ、録画した動画を突き合わせて確認する必要があります。しかし、それぞれが同期された状態で保存されておらず、どのログがどの動作に対応しているのかを把握するのに時間がかかります。今後は、ログや動画を時間軸で同期して保存できる仕組みを整備していきたいと考えています。 - 実時間が必要
一連のテストケースの実行に3時間弱かかっています!
再生速度を早めてテスト時間を短縮できるんじゃないの?と思われた方もいそうですが、各サービスの実装上の問題やシミュレータのパフォーマンスの問題があり、現状は等倍速での試験しかできていません。 - 実機とはやっぱり違う
対象のサービス自体はプロダクションコードそのままテストが行えていますが、よりハードウェアに近い低レイヤーのサービスやファームウェアといったソフトウェアはテスト対象に含まれていません。そういったサービスとの結合試験は未だに実機でのテストが必要になっています。
尚、今回紹介したシミュレータによるCIテストは社内向けのdaily OSを対象にしています。
ユーザ向けにOSを配信する前には実機を使ったテストを行っており、実環境でも本当に問題がないかを確認することで品質を担保しています。
(QA試験については「QAチームとLOVOT 3.0」を参照)
さいごに
GROOVE Xでは一緒に働いてくれる仲間を募集中です。