ししかわです。
社員研修の一環で二足歩行ロボットを作って、競技会「Humanoid Autonomous Challenge(HAC)」に参加します。
M5Stackの画像認識モジュール「M5Stack UnitV2」をスタンドアロン(単体)で使って、ロボットを制御しよう!というマニアックな計画を進めています。
前回、認識、プランニング、制御のモジュール間の連携をmicro-ROSを使って実装しようと決めました。
今回はmicro-ROSのチュートリアルに沿ってプログラムをビルドしてUnitV2で動かしてみます。micro-ROSの説明は前回の記事を参照してください。
micro-ROSのチュートリアルの場所
micro-ROSの公式チュートリアルが次のサイトで公開されています。
左カラムの目次から「First Steps Tutorials」を開くと、最初のステップとして次の項目があります
しかしUnitV2はこのどちらにも当てはまりません。UnitV2のOSはLinuxなので前者が当てはまりそうに見えますが、ここでのホストマシンはx86アーキテクチャを想定しています。またプログラムをビルドするマシン=実行するマシンとなっていますので、非力なUnitV2では実行が困難です。
代わりにarmアーキテクチャのRaspberry Pi向けに動作するサンプルアプリケーション群があるので、こちらのビルド手順を参考にします。
micro-ROSの環境構築
まずは環境構築を行います。UnitV2Frameworkのビルドと考え方は一緒で「コンパイラツールチェイン」「フレームワーク本体と依存ライブラリのソースコード」が必要です。micro-ROSではmicro_ros_setupというツールを使ってこれら一式をかんたんに用意できます。
使用したOSとROS2のバージョンは次のとおりです。
- Ubuntu 20.04.3 64bit
- ROS2 Foxy Fitzroy
ROS2環境構築(Docker)
micro_ros_setupを使うにはROS2環境が必要です。ここではDockerを使って準備しましょう。
次に示すようにDockerfileを作成します。ros:foxy のDockerイメージをベースとして、前述のmicro_ros_setupをGitHubからクローン、ビルドしています。
FROM ros:foxy env ROS_DISTRO foxy # for source command SHELL ["/bin/bash", "-c"] # microros WORKDIR /extern RUN git clone -b ${ROS_DISTRO} https://github.com/micro-ROS/micro_ros_setup.git src/micro_ros_setup RUN apt-get update \ && rosdep update \ && source /opt/ros/${ROS_DISTRO}/setup.bash \ && rosdep install --from-path src --ignore-src -y \ && apt-get install python3-pip --no-install-recommends -y \ && apt-get clean && rm -rf /var/lib/opt/lists/* RUN source /opt/ros/${ROS_DISTRO}/setup.bash \ && colcon build
ビルドのためにいくつか工夫しています。
ROS関連コマンド(Dockerfile中のrosdepとcolconが該当)を実行するためには、setup.bashというスクリプトを読み込み、必要な環境変数をセットする必要があります。しかしDockerfileのRUN命令に与えたコマンドは、通常bashではなくshで実行されるため、sourceコマンドが使えません。そこでSHELL命令を介してbashを使うよう指定します。また環境変数はRUNをまたいで引き継がれないため、ひとつのRUN命令の中でsourceコマンドと後続のコマンドを&&で結合して書きます。
Dockerfileが書けたらDockerイメージをビルドし、現在のディレクトリをマウントしてコンテナを起動します。このコンテナはmicro-ROSアプリケーションのビルドに使うだけで、この中でROSノードを動かすわけではないのでポートフォワード(-pオプション)は必要ありません。
$ docker build -t micro_ros_setup . $ docker run -it --rm -v $(pwd):/code micro_ros_setup
Dockerコンテナを開き、次のコマンドを実行すればmicro_ros_setupコマンドが使える状態になります。
(container)$ source /extern/install/local_setup.bash
ちょっと紛らわしいですが、ここで準備したmicro_ros_setupはあくまでmicro-ROSアプリケーションのビルド環境を生成するツールで、ビルド環境自身ではありません。micro_ros_setupを使ってターゲットとなるマイコンやアーキテクチャ、OSに応じたビルド環境(本記事ではarmhfのCPUで動作するLinux)を準備し、実際にアプリケーションをビルドできます。
micro_ros_setupのワークフロー
micro_ros_setupの使い方は4つのステップに分かれています。
- ビルド環境の生成(create_firmware_ws.sh):対象のハードウェアプラットフォームのために必要なクロスコンパイルのツールチェインや、その他必要な全てのコードをダウンロードします。すぐに動かせるmicro-ROSのサンプルアプリケーション群も同時にダウンロードします。
- アプリケーションの準備(configure_firmware.sh):ツールチェインを使ってビルドするアプリケーションを選択します。エージェントのIPアドレスとポートや、メッセージの転送方法の指定も行います。
- アプリケーションのビルド(build_firmware.sh):クロスコンパイルを実行してバイナリを生成します。
- アプリケーションの書き込み(flash_firmware.sh):生成されたバイナリを対象プラットフォームに書き込みます。
Raspbian buster (32bit)でビルドするとUnitV2でも動く
micro_ros_setupが提供するRaspbian(現Raspberry Pi OS)buster(32bit)向けのビルド環境がUnitV2でも動作することを確認しました。以降ではRaspberry Pi向けの環境構築方法を説明します。
Raspberry Pi向けmicro-ROSアプリケーションのビルド
1. Raspberry Pi向けmicro-ROSビルド環境の生成
まずは1番目のcreate_firmware_ws.shです。使い方は次のとおりです。
ros2 run micro_ros_setup create_firmware_ws.sh <パッケージ名> [<プラットフォーム名>]
次のコマンドを実行してArmhfアーキテクチャのRaspberry Pi向けの環境を作ります。
(container)$ ros2 run micro_ros_setup create_firmware_ws.sh raspbian buster_v7
コマンド実行後、firmwareディレクトリの中に環境が入ります。
# treeコマンドはコンテナに後からインストールした # apt-get update && apt-get install -y tree (contianer)$ tree -L 2 ./firmware/ ./firmware/ ├── COLCON_IGNORE ├── PLATFORM ├── dev_ws │ ├── ament │ ├── build │ ├── install │ ├── log │ ├── ros2 │ ├── ros2.repos │ └── xcompiler ├── mcu_ws │ ├── colcon.meta │ ├── ros2 │ └── ros2.repos └── raspbian_apps ├── 3rd-party-licenses.txt ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── agent ├── agent_lite ├── toolchain.cmake ├── weather_agent └── weather_publisher
firmware配下の3つのディレクトリはそれぞれ次の役割を持ちます
- dev_ws: クロスコンパイルのためのビルドツールなどが入ります。
- mcu_ws: 指定したプラットフォーム向け(ここではarmhfのRaspberry Pi)のアプリケーションのソースコードが入ります。
- raspbian_apps: 雛形となるサンプルアプリケーション群のソースコードです。
dev_wsの中にあるament、mcu_wsの中にあるcolconはいずれもROS2のビルドツールです。ビルドの内部処理の説明は省きます。書籍「ROS2ではじめよう 次世代ロボットプログラミング」が参考になります。
2. アプリケーションの準備
2番目のconfigure_firmware.shでは、雛形を指定してアプリケーションのワークスペース(mcu_ws)を準備します。
ros2 run micro_ros_setup configure_firmware.sh [アプリケーション名] [オプション]
「アプリケーション名」にはraspbian_apps内のアプリケーションが入ります。が、create_firmware_ws.shで自動的に取ってきたraspbian_appsはサンプルの数が少ないので、私がforkしたものを使います。次のコマンドでソースを取得してきます。
(container)$ cd firmware/raspbian_apps (container)$ git checkout -b sskw/foxy foxy (container)$ git pull https://github.com/meganetaaan/raspbian_apps foxy
raspbian_appsのうちint32_publisherを指定してアプリケーションのワークスペースを生成します。
(container)$ ros2 run micro_ros_setup configure_firmware.sh int32_publisher
3. アプリケーションのビルド
2.で指定したint32_publisherはint32型のトピックを1秒毎に発行するノードです。実装方法については公式チュートリアルや、本ブログの今後の記事で詳しく紹介するので、ここでは特にソースをいじらずそのままビルドしてみましょう。次のコマンドを実行します。
(container)$ ros2 run micro_ros_setup build_firmware.sh ... --- Finished <<< int32_publisher [1.02s] Summary: 36 packages finished [30.9s] 20 packages had stderr output: action_msgs builtin_interfaces example_interfaces int32_publisher rcl rcl_interfaces rcl_logging_noop rcl_logging_spdlog rcl_yaml_param_parser rclc rcpputils rmw rmw_implementation rmw_microxrcedds rosidl_runtime_c rosidl_typesupport_c rosidl_typesupport_microxrcedds_c std_msgs test_msgs unique_identifier_msgs
依存パッケージのビルド時に警告が出ます(20 packages had stderr output: ~の行)が、無視して構いません。
4. アプリケーションの書き込み…は手動で。
最後に書き込みです。flash_firmware.shコマンドでプラットフォームに応じた書き込みコマンドを実行してくれますが、Raspberry Piの場合は使いません。既にバイナリはfirmware/binディレクトリにできているので、scpなどを使ってUnitV2にコピーすればOKです。
# scpコマンドはコンテナに後からインストールした # apt-get update && apt-get install -y openssh-client (container)$ cd firmware/bin (contianer)$ scp int32_publisher m5stack@unitv2:/home/m5stack/
Agentのビルド
Agentとはmicro-ROSで作成したアプリケーションのメッセージ転送を担当するプログラムです(図中赤枠)。
ROS2のノード間の通信はDDSというミドルウェアによって執り行われます。micro-ROSではDDSによる通信処理をROS2 Agentとして切り離し、別のマシンで実行できるようになっています。こうすることでROSノード(マイコン)側のメモリ消費を抑えて運用できます。詳しくはmicro-ROS公式サイトの解説や、DDSのミドルウェアを開発するeProsimaによるメモリ使用量分析の資料が参考になります。
今回はUnitV2スタンドアロンで動作させたいので両方ともUnitV2上で実行します。前記のワークフローのステップ2~4を繰り返しながら、agent_liteというサンプルアプリケーションをビルドしていきます。
1点注意ですが、configure_firmware.shを実行するとmcu_wsの内容が上書きされます。今回はint32_publisherの中身を特にいじっていないので問題ありませんが、2.と3.の間にmcu_wsの中身を変更した場合は、ディレクトリの名前を変えるなどして退避しておく必要があるでしょう。
int32_pubilsherとagent_liteで完全にディレクトリを分けてしまえば(手順1からやり直せば)異なるアプリケーションで上書きされることはなくなります。しかし一部の設定はconfigure_firmware.shを再度実行しないと反映できないものもあり、この場合はやはりmcu_wsの内容がそっくり置き換わってしまいます(このあたり若干使いづらく感じているのですが、もっと良いやり方があるのかまだ分かっていません)。
(container)$ ros2 run micro_ros_setup configure_firmware.sh agent_lite (container)$ ros2 run micro_ros_setup build_firmware.sh (container)$ scp firmware/bin/micro_ros_agent_lite m5stack@unitv2:/home/m5stack/
アプリケーションの実行
前記の手順を終えると、UnitV2上に「int32_publisher」と「micro_ros_agent_lite」の2つのプログラムがあるはずです。
(unitv2.local)$ cd /home/m5stack/ (unitv2.local)$ ls -l ... int32_publisher ... micro_ros_agent_lite ...
まずmicro_ros_agent_liteを実行します。起動時にポート番号(–port 8888)とmicro-ROSノードとの通信プロトコル(udp4)、使用するミドルウェア(–moddleware ced)を指定します。
(unitv2.local)$ ./micro_ros_agent_lite udp4 --port 8888 --middleware ced & ./micro_ros_agent_lite: /lib/libstdc++.so.6: no version information available (required by ./micro_ros_agent_lite) # ビルド時と実行時でlibcのバージョンに差異があるために出る警告 [1642040171.165949] info | UDPv4AgentLinux.cpp | init | running... | port: 8888 [1642040171.167701] info | Root.cpp | set_verbose_level | logger setup | verbose_level: 4
続いてint32_publisherノードを実行します。「Sent: (番号)…」のログが出ていればノードは動作しています。
(unitv2.local)$ ./int32_publisher unitv2% ./int32_publisher [1642040203.083178] info | Root.cpp | create_client | create | client_key: 0x72BCCAA5, session_id: 0x81 [1642040203.083368] info | SessionManager.hpp | establish_session | session established | client_key: 0x72BCCAA5, address: 127.0.0.1:17292 [1642040203.083953] info | ProxyClient.cpp | create_participant | participant created | client_key: 0x72BCCAA5, participant_id: 0x000(1) [1642040203.084647] info | ProxyClient.cpp | create_topic | topic created | client_key: 0x72BCCAA5, topic_id: 0x000(2), participant_id: 0x000(1) [1642040203.085233] info | ProxyClient.cpp | create_publisher | publisher created | client_key: 0x72BCCAA5, publisher_id: 0x000(3), participant_id: 0x000(1) [1642040203.085735] info | ProxyClient.cpp | create_datawriter | datawriter created | client_key: 0x72BCCAA5, datawriter_id: 0x000(5), publisher_id: 0x000(3) Sent: 0 Sent: 1 Sent: 2
トピックが実際に発行されているのか確認したければ、実際にlistenerノードを実装して動かすのもよいですが、micro_ros_agent_liteのログレベルを上げて(–verbose 6)実行することで「micro-ROSノードからのメッセージ受診」と「ROS2のDDSネットワークへのメッセージ送信」のログを見ることが可能です。
(unitv2.local)$ ./micro_ros_agent_lite udp4 --port 8888 --middleware ced --verbose 6 && ./int32_publisher [1642040705.534014] debug | UDPv4AgentLinux.cpp | recv_message | [==>> UDP <<==] | client_key: 0x5050D130, len: 13, data: 0000: 81 00 00 00 0B 01 05 00 03 00 04 00 80 [1642040705.534171] debug | DataWriter.cpp | write | [** <<DDS>> **] | client_key: 0x00000000, len: 4, data: 0000: 00 00 00 00 [1642040705.534331] debug | UDPv4AgentLinux.cpp | send_message | [** <<UDP>> **] | client_key: 0x5050D130, len: 13, data: 0000: 81 00 00 00 0A 01 05 00 04 00 00 01 80 [1642040705.534424] debug | UDPv4AgentLinux.cpp | send_message | [** <<UDP>> **] | client_key: 0x5050D130, len: 13, data: 0000: 81 00 00 00 0A 01 05 00 05 00 00 00 80 Sent: 0 ...
以上、UnitV2でのmicro-ROSの動かし方について説明しました。
次回からはUnitV2Frameworkなどと連携させつつ、ロボットの認識や制御モジュールをmicro-ROSノードとしていきます。