ROS on WSL
ROS on WSLのお話をしましょう。
ROS on WSLとは
ROS on WSLとは
以下は、「単なる“ロボット用OS”ではない「ROS(Robot Operating System)」の概要と新世代の「ROS 2.0」 」から引用しました。
ROSはもともと、SAIL(Stanford Artificial Intelligence Laboratory:スタンフォード人工知能研究所)が「STAIR」(STanford AI Robot)というロボットを開発するために開発したものである。
このSTAIRの開発にはWillow Garageという会社が協力しており、ROSの開発は同社が主体となっていた。
その後、Willow Garageは2012年にOSRF(Open Source Robotics Foundation)を設立してROSの管理を移管。
以後はこのOSRFがオープンソースでROSの開発とメンテナンスを行っている。
このROSそのものは基本的にUbuntuの上で動作する。
対応するハードウェアは非常に多く(一覧はこちらを見てもらうのが早い)、加えて自分で作成したロボットをROSで制御する、というケースも多い。
そして、ROSの特徴の1つはソフトウェアライブラリの多さである。
一覧はこちらから見ることができるが、現時点での推奨リリースであるKinetic Kameに対応したものだけでこれだけある。
ROSそのものも順次更新されており、2010年1月に初公開されたROS 1.0以降、Box Turtle、C Turtle、Diamondback……とアップデートされている。
現在の最新は2017年5月にリリースされたLunar Loggerheadであるが、その前のKinetic Kameが安定しており、2021年までのサポート(Lunar Loggerheadは2018年まで)があることから、 現在はKinetic Kameの利用が推奨されている。
研究のROSから実用のROS 2.0へ
ちなみにこのROSと並行して、2015年から「ROS 2.0」の開発も進んでいる。最初のリリース(alpha 1)は2015年8月にあり、2016年12月にはbeta 1がリリース、現在は最新のbeta 3(2017年9月)が出ており、2017年12月にはbetaの付かない最初のオフィシャルリリースが予定されている。
ROSとROS2の間に互換性はない(共存は可能)が、これには訳がある。1つは分散型通信システムの問題、1つはROSの使われ方の変化だ。
ROSの開発当時、適切な分散型通信システムは存在していなかったので、ROSはこれを独自に開発した。ただその後、幾つかの分散型リアルタイム通信システムが普及を見せたこともあり、ROS 2.0ではその1つである「DDS(Data Distribution Service)」を採用している。DDSの採用によって、ROSとROS2に互換性は無くなっている。
もう1つ、ROSのAPIそのものが、設計時の想定と実際の使われ方に距離が生まれてきたことも互換性が確保されなかった理由といえる。APIそのものを変更すれば実態に即したものとなるが、既存ソフトウェアは動作しなくなる。そのため、別のAPIを設計する方が適切と判断されたのだ。
具体的にいえばROSはユースケースとして、「1体のロボットの制御」「ワークステーションクラスの制御ボードを使って管理」「リアルタイム性は特に求めない」「ネットワークが常につながっている」「主に研究などの目的」といったユースケースや要件を想定していた。
登場時はこれで十分であったが、昨今はよりニーズが多様化している。これに向けてROS 2.0ではユースケースや要件として、「複数ロボットによる協調動作のサポート」「マイコンレベルのコントローラーを直接利用可能」「リアルタイム動作のサポート」「劣悪なネットワーク環境でも動作可能」「製品レベルでの利用」を挙げており、こうしたニーズには既存のROSを拡張するよりも、新しく開発したほうが良いと判断されたためだ。
そんな訳でROSは主にロボットの形をした機器の制御に使われるが、実は分散型システムでは広くROSを利用する余地があり、意外なところでもROS(や今後はROS 2.0)が動いている、ということになるだろう。
ROS on WSLのWindows環境へのインストール
「Windows 10 Bash on Ubuntu on WindowsでROSを動かす!」を参考にインストールします。
以下は、「ROSの基本的な開発 on Python」から引用しました。
ROSのファイルシステム
ワークスペースの作成と設定
ROSのワークスペースを作ります。以下のコマンドを入力してください。
mkdir -p /mnt/c/watanabe/ros/src
また、環境変数「$ROS_PACKAGE_PATH」に自分のワクスペースを追加するために、以下のコマンドを実行します。
cd /mnt/c/watanabe/ros/ source /mnt/c/watanabe/ros/devel/setup.bash
次に作成したディレクトリを「catkin」ワークスペースとして初期化します。 以下のコマンドで、「/mnt/c/watanabe/ros/src」にCMakeLists.txtが生成されます。
cd /mnt/c/watanabe/ros/src catkin_init_workspace
この段階では、ワークスペースは空ですが、以下のように「catkin_make」コマンドでワークスペースはビルドされ、 ワークスペースに必要なファイルやディレクトリが生成されます。
cd /mnt/c/watanabe/ros catkin_make
catkin_makeしたら以下のようなファイル階層になっていると思います。
ros |--build |--devel |--src
- build
- ビルドに関する設定やmakeのlogのファイルが入っています。
- devel
- 実行ファイルやmakeによって生成されたものが入っています。
- src
- ユーザがソースコードを保存する場所です。
これでワークスペースはできました。
パッケージ(Package)
ROSでは任意の機能をつめたソフトウェアの集合をパッケージと呼びます。
一つのパッケージには、「ROSプログラム」「設定ファイルや起動ファイル」「サードパーティのプログラム」などが含まれます。
パッケージを操作する場合は、主に以下のコマンドを使用します。
-
catkin_create_pkg:
新しいパッケージの作成 -
catkin_make:
パッケージのビルド(実行ファイルやライブラリ等のコンパイル) -
rosdep:
パッケージが要求するシステムの依存関係のインストール
パッケージの雛型はcatkin_create_pkgで作成することができます。
またcatkin_create_pkgのコマンド引数は以下のとおりです。
catkin_create_pkg [package_name] [depend1] [depends2] ...
「chapter8」パッケージの作成
コンソールに以下のコマンドを入力してください。
cd /mnt/c/watanabe/ros/src catkin_create_pkg chapter8 roscpp rospy
このコマンドで、新たなパッケージを「chapter8」と言う名前で生成できました。また依存関係を持たせるパッケージはroscpp、rospyということになります。
このコマンドを実行すると、「/mnt/c/watanabe/ros/src」に「chapter8」ディレクトリが作成され、以下のようなファイルやディレクトリが作成されます。
-
CMakeList.txt:
CMakeのビルドファイル -
package.xml:
パッケージのマニフェスト・ファイル -
include:
ヘッダーファイルを置くディレクトリ -
src:
ソースファイルを置くディレクトリ
その他、必要に応じて、以下のようなファイルやディレクトリを生成します。
-
msg:
メッセージ型の定義ファイルを置くディレクトリ -
srv:
サービス型の定義ファイルを置くディレクトリ
ROS特有の「package.xml」ファイルには、パッケージ名・バージョン番号・メンテナ・作者・ライセンス・依存パッケージなどが記述されています。
必要に応じて、パッケージの依存関係などを追加記述することができます。ただし、パッケージの依存関係は「CMakeLists.txt」の「find_package()」にも追記する必要があります。
続いて、 ワークスペースでもう一度catkin_makeしましょう。
cd /mnt/c/watanabe/ros catkin_make
プログラムのソースコードを書いた後は、catkin_makeコマンドでビルドします。
また、catkin_makeコマンドでビルドする際には、「rosdep」コマンドを使用して、パッケージが要求するシステムの依存関係をインストールする必要があるかも知れません。
以下のように実行すると「package.xml」ファイルの記述に基づいて依存関係を解決し、必要なライブラリをインストールできます。
rosdep install package_name
必要に応じて、以下のコマンドで「rosdep」のキャッシュ情報をアップデ-トします。
rosdep update
ROSのデータ通信
ノード(Node)
トピック(Topic)
メッセージ(Message)
サービス(Service)
「package.xml」ファイルに
PublisherとSubscriber
「ROSのトピック通信におけるPublisherとSubscriberの内部動作(概要)」から引用しました。
ROSの内部動作
ROSは大きくMiddleware層とApplication層に分けられます。
ROSを利用する場合、Middleware層を理解しなくても十分なため、Middleware層の内部資料はほぼないと思います。
ROSの内部動作
ROSノードを立ち上げると、mainスレッドを含め通常は計5つのスレッドが立ち上がります。
その中でトピック(Pub/Sub)通信に関連するのはmainスレッドとpoll managerスレッドです。
mainスレッドはROSノードのアプリケーション処理(普段みんなが書いているようなコード)実行を担当し、 poll managerスレッドがMiddleware層の通信まわりの処理をバックグラウンドで担当してくれます。
通信の概念としてはトピック通信をしていますが、ノード≒プロセスなのでプロセス間通信をする必要があり、実際はsocket通信で実装され逐次的に各ノードと通信しています。
トピック通信の概念では恰も同時に通信しているように見えますが、実際は逐次的にsocket通信を行っています。
Publisherの仕組み
Publisherがチュートリアルのtalkerの様にpublishメソッドをコールすると
chatter_pub.publish(msg);
内部でデータをシリアライズし、下記の図のようにpublish queueというリングバッファにシリアライズされたデータが格納されます。
poll managerは常にポーリングをしており、mainスレッドがpublish queueにデータを格納した後、poll managerにシグナルを発行し、poll managerスレッドが送信処理をはじめます。
poll managerスレッドはpublish queueないの最も古いデータを参照し、自身につながっているsubscriberの数だけ用意されたwrite bufferへキューイングします。
その後、各write bufferの中身をエンキューしてsocket通信のsendシステムコールでsubscriberへ送信しています。
Publisherの仕組み
この時のpublish queueサイズ(=write bufferサイズ?)が送信バッファになります。基本的に最新のデータを必要とすることが多いのでバッファサイズは1で良いと思います。
データのロストが致命的になる場合やkalman filter, particle filterなど時系列処理がある場合は送信バッファサイズを上げてももいいかもしれません。
Subscriberの仕組み
Subscriberの内部動作は結構ややこしいです・・・。
Subscribe時は、poll managerがPublisherと繋がっているソケットをポーリングし続け、データが来たらsubscription queueというリングバッファにデータを格納し、internal callback queueにsubscription queueのアドレスを格納します。
重要:この時、subscription queue(リングバッファ)の要素を上書きした場合はinternal callback queueにsubscription queueのアドレスを格納しません。
その後、mainスレッドはros::spin()によってinternal callback queueを監視しておりinternal callback queueの要素をデキューし対応するsubscription queueから最も古いデータを参照してcallback関数を実行します。
Subscriberの仕組み
subscription queueのサイズが受信バッファになっています。
複数のトピックを購読した場合、一つ問題が発生します。
callback関数の処理が間に合わずsubscription queueのサイズをオーバーする(上書き)時、internal callback queueの順序は固定されているにも関わらず、 subscription queueの中身は自由に変わってしまうということです。
つまり、上記の図で言う上のsubscription queueに最新のデータがたくさん入っているにも関わらず、下のsubscription queueのデータばかり参照されcallback関数が呼ばれ、 スタベーション状態になる可能性があります。
これらを防ぐには受信バッファのサイズをそれぞれ1にすることでsubscription queueのサイズをオーバーする(上書き)場合でも交互に呼ばれるようになり、スタベーションを防げます。
もしくは、internal callback queueという一つのキューでまとめるのではなく別にcallback queueを作成し、callback queueごとに優先度をつけることでも解決できます。
こちらのほうが、ディベロッパーとしての自由度は高いです。複数スレッドでそれぞれのcallback queueの監視とかも出来ます。
参考 http://wiki.ros.org/roscpp/Overview/Callbacks%20and%20Spinning
TFとRViz
TF(transform)とは
TF(transform)とは「ある座標系」と「他の座標系」の位置関係を表すものです。
座標系は部品と部品の位置関係のように変化しないものでもいいですし、部屋のある一点から見たロボットの位置のように常に変化するものでも良いです。
例として次のようなロボットを考えてみます。
車輪ロボットとセンサ
このとき、ロボットの重心位置においた座標系からセンサの座標系に行くには「右に20cm、上に10cm」等と表現できます。これが「TF」です。
TFの特徴
複数のTFをつなげることが出来る
TFは「あるもの」から「他のもの」への位置関係を表すものですが、普通ロボットなり自動車なりは沢山の部品で出来ています。
それらを1つの基準点(たとえば上の図のロボット座標系)からすべて表すのは大変です。
そこで、TFを数珠つなぎに書くことで、関係を分かりやすくすることが出来ます。例えば先程のロボットなら次のような座標系が作れそうです。
ロボットの座標系
座標系の名前は「frame(フレーム)」と呼びます。
「map」フレームは常に動かない座標系です。ロボットなら部屋のある点、自動車なら道路などがこれにあたります。
黄色の線は「あるフレーム」から「他のフレーム」へのTFがある事を示しています。
元になるフレームを「parent frame(親フレーム)」または単に「frame」、目的のフレームを「child frame(子フレーム)」と呼びます。
つながっているTF同士の位置関係は自動計算される
繋がっている2つのフレーム間のTFは、ROSが自動で計算してくれます。
任意の位置関係をいつでも取得することが可能です。
※逆に、途中で切れていると目的のフレームが表せなくなります
一定時間経つと捨てられる
これは重要な特徴なのですが、TFは一度誰かが発信して一定時間経つと無効になります。
ずっと表示させたければすべてのTFを常に発信し続ける必要があります。
静的TFと動的TF
TFには例えばロボットのセンサの位置関係のように永久に変わらないものと、部屋からみたロボットの位置のように変わり続けるものがあります。
前者を静的TF、後者を動的TFと言います。
TFを発信する
静的TFを発信するコードを書いてみます。
最も簡単なのは次のようなlaunchファイルを作ることです。
<launch> <node pkg="tf" type="static_transform_publisher" name="robot_to_sensor" args="0.2 0 0.1 0 0 0 robot sensor 10" /> </launch>
「<node」 から 「/>」までがTFを発信するプログラムを実行する部分です。
それぞれの引数の意味は次のとおりです。
引数 | 値の例 | 備考 |
pkg | “tf” (固定) | パッケージの名前 |
type | “static_transform_publisher” (固定) | プログラムの名前 |
name | “(好きな名前)” | 実行時の名前 |
args | (例)”0.2 0 0.1 0 0 0 robot sensor 10″ | x方向、y方向、z方向、ヨー、ピッチ、ロール、親フレーム、子フレーム、発信の間隔[Hz] |
nameが同じプログラムを同時に作るとエラーになります。
これを自分のパッケージ→lanchフォルダの中に入れて実行すると、argsで指定したTFが発信されます。
RVizとは
「blueprint」に出ている以下の「alexa-skills-kit-color-expert」をROS on WSLから起動しよう。
ROSのPCL(Point Cloud Library)
ROSのPCL(Point Cloud Library)とは。
以下は、「ROSの基本的な開発 on Python」から引用しました。
ROSのPCL(Point Cloud Library)とは
ROSのコーディング
ここではROSの具体的なコーディングを行います。
以下は、「ROSの基本的な開発 on Python」から引用しました。
ROS message
ROSでは処理に必要なデータは基本的にメッセージとして通信を行ないます。
そのメッセージのデータ構造はさまざまなデータ型から任意のものを選択して独自のデータ構造にすることができます。 たとえば、データ型には以下のものがあります。
- int8, int16, int32, int64 (plus uint*)
- float32, float64
- string
- time, duration
- other msg files
- variable-length array[] and fixed-length array[C]
またメッセージに関するクラスが定義されているヘッダーは/opt/ros/indigo/include内にあります。
簡単なメッセージファイルを作ってみましょう。
cd ~/test_catkin_ws/src/test_pkg mkdir msg; cd msg touch Adder.msg emacs Adder.msg
メッセージファイルAdder.msgの中には以下の記述をしてください。 このmsgファイルでは32bitのsigned int型の変数を1個,sensor_msgs/Image型の変数を1個保有していることになります。
Adder.msg
int32 arg_x int32 arg_y
このメッセージファイルを元にメッセージ型を定義するヘッダファイルが生成されます。 ヘッダファイル生成の設定を行うため以下のファイルを編集してください。
cd ~/test_catkin_ws/src/test_pkg emacs CMakeLists.txt
CMakeLists.txt
#該当意部分がはコメント解除して適宜修正 #7行目あたり find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs + message_generation ) #45行目あたり ## Generate messages in the 'msg' folder add_message_files( FILES - # Message1.msg - # Message2.msg + Adder.msg ) #66行目あたり generate_messages( DEPENDENCIES std_msgs ) #104行目あたり catkin_package( # INCLUDE_DIRS include LIBRARIES ros_adder CATKIN_DEPENDS roscpp rospy std_msgs DEPENDS system_lib )
catkin_makeしましょう。
cd ~/ros_ws catkin_make
catkin_makeに成功すると以下のディレクトリにメッセージを定義したPythonコードが生成されます。
ls ~/ros_ws/devel/lib/python2.7/dist-packages/ros_adder/msg/ _Adder.py __init__.py
ROSのノードを記述する
ROSのノードをPythonで記述していきます。
cd ~/ros_ws/src/ros_adder/ mkdir scripts; cd scripts touch para_in.py adder.py
- para_in.py:処理に必要なデータをPublishするPublisher
- adder.py:publisherから受け取ったデータを表示するSubscriber
Publisherを作る
以下に示すコードがPublisherとなります。 なお、コード中のAPIの説明などはコメントによって記しています。
para_in.py
#!/usr/bin/python # -*- coding: utf-8 -*- # license removed for brevity # pythonでROSのソフトウェアを記述するときにimportするモジュール import rospy # 自分で定義したmessageファイルから生成されたモジュール from ros_adder.msg import Adder def para_in(): # 初期化宣言 : このソフトウェアは"para_in"という名前 rospy.init_node('para_in', anonymous=True) # nodeの宣言 : publisherのインスタンスを作る # input_dataというtopicにAdder型のmessageを送るPublisherをつくった pub = rospy.Publisher('input_data', Adder, queue_size=100) # 1秒間にpublishする数の設定 r = rospy.Rate(5) para_x = 0 para_y = 2 # Adder型のmessageのインスタンスを作る msg = Adder() # ctl + Cで終了しない限りwhileループでpublishし続ける while not rospy.is_shutdown(): msg.arg_x = para_x msg.arg_y = para_y # publishする関数 pub.publish(msg) print "published arg_x=%d arg_y=%d"%(msg.arg_x,msg.arg_y) para_x += 1 para_y += 1 r.sleep() if __name__ == '__main__': try: para_in() except rospy.ROSInterruptException: pass
Subscriberをつくる
以下に示すコードがSubscriberとなります。 なお、コード中のAPIの説明などはコメントによって記し、Publisherと同じ部分のコメントは省いています。
adder.py
#!/usr/bin/python # -*- coding: utf-8 -*- # license removed for brevity import rospy from ros_adder.msg import Adder # Subscribeする対象のトピックが更新されたら呼び出されるコールバック関数 # 引数にはトピックにPublishされるメッセージの型と同じ型を定義する def callback(data): # 受けとったmessageの中身を足し算して出力 print data.arg_x + data.arg_y def adder(): rospy.init_node('adder', anonymous=True) # Subscriberとしてimage_dataというトピックに対してSubscribeし、トピックが更新されたときは # callbackという名前のコールバック関数を実行する rospy.Subscriber('input_data', Adder, callback) # トピック更新の待ちうけを行う関数 rospy.spin() if __name__ == '__main__': adder()
2つのファイルの記述が終わったら、ビルドしましょう。
cd ~/ros_ws/ catkin_make
ROSで作ったノードを実行してみる
ビルドが成功したら、さっそく実行してみましょう。 現在開いているコンソールほかにもう2つのコンソールを開き以下のコマンドを上からそれぞれ入力してください。
1つ目のコンソール
ROSではroscoreというコマンドを始めに起動することでさまざまなソフトウェアをスタートすることができます。 具体的にはroscoreはネームサービスなどを行います。
cd ~/ros_ws source devel/setup.bash roscore
2つ目のコンソール
ROSでは各ノードの実行はrosrunというコマンドによって実行されます。 ROSにおいて単体のノード実行は基本的にrosrunで行います。 subscriberを起動します。
cd ~/ros_ws source devel/setup.bash rosrun ros_adder adder.py
3つ目のコンソール
publisherを起動します。
cd ~/ros_ws source devel/setup.bash rosrun ros_adder para_in.py
起動に成功したら以下のような結果が得られます。
実行を止めたいときはCtrl + Cで止まります。
root@localhost:~/ros_ws# rosrun ros_adder para_in.py published arg_x=0 arg_y=2 published arg_x=1 arg_y=3 published arg_x=2 arg_y=4 published arg_x=3 arg_y=5 published arg_x=4 arg_y=6 published arg_x=5 arg_y=7 root@localhost:~/ros_ws# rosrun ros_adder adder 2 4 6 8 10 12 14
コマンドまとめ
- catkin_make - ワークスペース内のパッケージを一括ビルドするコマンド
- catkin_create_pkg - ROSにおけるパッケージの雛形を作るコマンド
- roscore - ROSのネームサービス、マスタ
- rosrun - ROSの単体ノードを起動する際に使用するコマンド
森の小径1