ロボットのプログラムにレイヤードアーキテクチャを採用してみる

TL;DR

  • アーキテクチャに沿って記述することである程度書きやすくなる
  • ロボットのプログラムという性質による限界を許容する必要がある
  • DI・依存を気にかけてコーディングする場合にPythonはかなりしんどい

動機

以前からロボットを作っているのですが、実装が複雑になるにつれて何かしらの構造に従った方が書きやすいと思った為、ここ最近趣味の方で学習していたアーキテクチャを採用してみました。あまりがっつりドメイン駆動でアーキテクチャを組むというよりは、各レイヤーの責務を意識したかった、というくらいの気持ちです。

また、ハードウェアを検討する際に、以前はCPUの乗ったボード(Jetson nano)から直接命令を送っていたのですが、故障のリスクを考えた場合に計算機能とハードウェアの中継器となるボードを分離した方が良いと考え、WindowsPC+Arduinoで実装することにしました。それにあたり、ロジックとハードウェアに接続する部分の実装を綺麗に分けたかったりしました。

細かい部分としてはサーボの角度やロボットの意味的な角度をそれぞれ値オブジェクトとして管理したかった点があります。サーボ角だと90度を基準に左右に動かしますが、ロボットの首の角度としては0度を基準として-90~90度ほどの可動域が考えられ、この値を実装でミスしてしまうとありえない角度にサーボが動いてしまう懸念がありました。floatで扱うが意味も値域も異なる為、それぞれ型を持ちたかったわけです。

加えて、ロボットのモデルが計算論低神経科学の枠組み(もといマーのモデル)に合致するのではないか?と思ったので、どこまでできそうか試した次第です。

godiva-frappuccino.hatenablog.com

ドメインモデル・ユースケースを組み立てる

まずはロボットがどの様なものか考えていきます。ハードウェア的に組み立ててこんな感じかな…というイメージを作っていきます。ロボットの上半身を考えると、胸と首があって、それぞれDegreesの角度を持ちます。それと同時に、内部的にはサーボモータが自由度の数だけ存在し、サーボが持つ状態としてサーボ角があります。サーボは出力先なのでInfrastructure層で管理するべきでは?という問いがあるのですが、後ほど言及します。

 

これでイメージはついたのですが、実際どう動くのか?というイメージがつきづらいです。今現在の想定としては、自律操作と人間による遠隔操作が両方できると嬉しいのですが、特に前者の場合ロボットは常に動き続ける為、基本的にはループを回したり、分散されたモジュールがメッセージを送りあう形になります。レイヤードアーキテクチャの実装例として知っているものはWebアプリケーションのようにRequest/Response形式で、ものによってはstate-lessな単発の通信をするイメージがあります(無知ゆえに…)。そんなわけでただ適用するのは難しいので、別スレッドでループしてロボットを制御し続けるモジュールが必要になりました。逆に、発話させるみたいな行動は意味的に扱いやすいしある時刻において動作を実施すればよいので扱いやすいですね。

UseCaseを考えると、遠隔操作で「ある一点を見る」「自律操作を開始する」などが挙げられます。もう少し高度なユースケースも思いつくのですが、現状必要ないので割愛、ただ自律と遠隔を統合できると嬉しいです。

リクエストはApiからUseCase経由で降ってくるんですが、Domain層ではリクエストの窓口を用意し、動作を生成するモジュールと共に統合され、インフラ層経由で外部にサーボ値を送るイメージです。ここでスレッドは3つ必要で、Api層からのリクエストを待つメインスレッド、動作を生成するMovingGeneratorをループしておくスレッド、それらを統合して常にループしながらサーボ値を外部に送り続けるIntegratorのスレッドが必要になりました。こう考えると、やっぱりROSみたいに分散して管理した方が楽になる感じがしますね。こういったところからも適したパターンのみ採用した方が良い、という学びがあります。

Infrastructure層問題

まず、Infrastructure層にはMockとArduinoにシリアル通信でメッセージを送るRepositoryがあります(普段永続化に使われているRepositoryという名称が気に食わず、Connectorと名付けました)。個人的にはMockに切り替えることで実際のハードウェア接続しなくても論理的にロボットが動作しているかテストできるのが嬉しかったです。Unityでシミュレータを作ればそこにロボットの姿勢を流すこともできますしね。

ただ、サーボモータはハードウェアの実現の仕方でドメインロジックとは関係ないのでは?という先の疑問があるのですが、個人的にはドメインで表現する知識足りえるものが2つあったので、ドメイン層に実装するべきだと思いました。

まず一つ目に、ロボットの首の向きのような意味的な角度とサーボの角度が異なる点にあります。自分の作っているものはリンク機構で差動するようになっており、首をかしげるroll方向の動きに二つのサーボが必要になります。動かし方自体はシンプルですがその動きの変換をInfrastructure層でやるのは違和感があります。

二つ目に、メッセージの送り方にも知識がある点が挙げられます。10fps程度で姿勢を送るくらいなら実際の問題はないのですが、例えば1秒間で右から左に首を向けるといった命令を行う際に実際のサーボの動きとしては間を補完して0.1秒ずつ動かすことになります。Webアプリケーションのイメージだとバッチ処理になるのでしょうか。厳密にドメイン駆動アーキテクチャをやるとRepositoryの作業はモデルを保存するのですが、それだとパフォーマンス面で効率が良くないです。具体的な実装方法はInfrastructureに任せるにしても、ドメイン層側でBatchServoOutputみたいなインターフェースは欲しいかな…問う感じになります。実際バッチっぽい動きをドメイン層でやって都度都度モデルをInfrastructureに投げてもよいですが、ちょっとDDD好きがSQL周りを気にしていない人を見て驚いた…みたいなTweetをみたので気にしておきます。結局はアーキテクチャを綺麗にするのが最終目的ではなく、パフォーマンスも業務要件になり得ますし、CQRSみたいな大胆な方法でUseCaseの都合を優先してあげたり、トランザクションドメインの方で気にする場合も発生しうるのではないかと思います。UseCaseの方でUnitOfWork使ってよしなにできると嬉しいですが、今回の場合だとUseCase都合というよりはドメインに持っているサーボの知識になってしまうのかなと思います。

静的型付け言語を使おう

多分この文章を読んでいる人はそうですね…となりそうですが、Pythonだとかなりきついです。Pythonにも型ヒントの機能とDIコンテナを作る為のライブラリは提供されているのですが、C#のinterfaceみたいな強制力がなく、Abstract Classを継承したはいいものの(VSCode拡張機能辺りを使わないと)実行時エラーでしか検知できなかったり、型の恩恵を受けてこその開発のスムーズさに欠ける感じがします。

今後の話

ほぼ趣味の範疇でコードを綺麗にしたのですが、見通しはよくなった感じがします。今まで首の3自由度だったものが5自由度に増える予定なのですが、サーボのモデルを実装したことでそれを使えば上半身の2自由度のモデルも同様に作れますし、値をArudinoに送る方法もinterface経由で一つの窓口に送れば良いことが明確になり、ロジックの分散を回避できているのは結構嬉しいです。何より単体テストができるようになったのでロジックの修正の時にハードを動かさなくてもよくなったのは大きいです。

しばらく大きめの改修はないので他のアプリケーションを進めたり、ハードウェアの機構の設計をしないといけないので趣味的な改修は一旦お休みです。人形の作業にハードウェアの作業に(このプログラムはサーバサイドアプリケーションなので)HMDで動くクライアントアプリケーションをきっちり作らないといけなかったり、といった作業が待っています。正直コードを綺麗にしている場合じゃないので、早めに機構の設計をしてモデリング・材料購入・印刷などを進めていきます。概念的な部分が進んできたので、ようやく目に見える形のあるものができそうなので、楽しみですね。