本記事はETロボコン Advent Calendar 2016の9日目の記事です。本日12/25クリスマスです。完走に向けて空き枠を埋めます。
はじめに
今年、私はETロボコン関西地区大会のダミーカー(EV3Way)を担当し、実装言語にmrubyを選択しました。mruby環境の情報が不足しており、私自身も非常に苦労したので得られたノウハウを書き残すことにしました。
筆者自身、mrubyを使った製品の設計・実装経験がなく、mrubyの中身についても断片的な知識しか無い状態ですので、情報に誤りがありましたらご指摘いただけると幸いです。
結果
Lコース/Rコースともに「リタイア」でした。
念の為補足しますが、「mrubyを選択したからまともに走れなかった」ということではなく筆者の技術不足、検証不足、関西地区大会直前に時間を取れなかった、などの要因が重なった結果だと思っています。
やったこと
1. モデリング
選手時代の経験を元に、今年の競技規約に従ってクラス図・シーケンス図を書き起こしました。
2. プラットフォームの選択
今回はmruby-toppers-ev3rtを使わせていただくことにしました。加えて、mruby-tiny-io/mruby-rtos-toppersも合わせて使いました。
mruby on EV3RT + TECSでも良かったのですが、
- 既に環境構築されたC言語の環境をそのまま使いまわせそうにない
- C言語による拡張を行う方法がわからない
- 筆者がmruby-toppers-ev3rtのことをよく知っている
という事情から今回は選択しませんでした。
3. サンプルプログラムを動かす
ひとまず倒立走行できることと自作のクラス(距離計測用のクラス)が動作することを確認しました。(ちなみにこの時点で2016年3月です)
4. モデルに沿って各クラスを実装
コースを区間に分割してそれを切り替えながら走行できるよう、クラスを追加しました。
5. 中四国地区の壁ドン・チキンレースに参戦
腕試しということで、お隣の中四国地区でGWに開催された独自勉強会にお邪魔して壁ドン・チキンレースに参戦しました。
この壁ドン・チキンレースは良く出来ていて、この競技を完全攻略するためには以下技術要素が必要です。
- 完全停止状態からの走行開始・倒立制御
- エンコーダを利用した直進走行・走行距離の算出
- 超音波センサによる壁との距離の取得
- 輝度センサによるライン検出
これらがGWの時点でできていれば、9月の地区大会までにガッツリ走行調整&モデルの改善ができるでしょう。
尚、私自身の競技結果は「スタート直後に転倒」でした。思えばこのあたりから、不安な兆候が散見されていたんですね...
6. 地区大会に向けて本番コースで調整
5月〜7月はあまり作業ができず、8月に本番コースで調整開始しました。
低速で基本走行はできるものの大きく2つの問題に直面しました。
- スピードを上げるとコースアウトする
- スタート時に転倒しやすい
これらの問題は後述の「性能の問題」に起因しているものと推測しています。
mrubyを使って困ったこと
困ったこと1. 性能の問題
mrubyを選択した時に真っ先に挙がる懸念点が性能です。最終的に私が作った走行体は処理周期4msを守れていませんでした(1秒に1回程度の割合で周期超過が発生)。
EV3Wayでは12/19の記事に書いてある通り、4ms周期を厳守する必要があります。
サンプルプログラムレベルであれば4ms周期の動作が可能なのですが、クラスを増やしていく過程でメソッド呼び出しが増え、処理によって作成されるオブジェクトが増え、結果として4ms周期を維持できなったようです。
性能面に関してはいくつか考察があります。
考察1. ブロックを多用すると性能は落ちる
Rubyの良さの一つに「ブロック(無名関数)が使える」という点があります。ブロックを使用するとETロボコンでよく使用する「指定した距離走行したらこのメソッドをコールバックして」という実装を簡単に書けます。
ただ、ブロックを多用すると性能は落ちるようです。ブロックの生成・削除に時間を要するためと思われます。
対策としてブロックではなくProcを使用し、同じ処理内容であればProcオブジェクトを使い回すようにしました。
考察2. PID制御のような高頻度で行う小数演算はC言語化したほうが良い
小数演算をすると、Floatオブジェクトが大量に生成され、その分ガベージコレクションの処理時間が長くなります。
対策としてPID制御を行うmrbgemsを作成しました。以下に公開しているので、良ければ使用してください。
mruby-pid:PID control library in mruby.
考察3. ガベージコレクションの設定は安易にいじらないほうが良い
処理が4msに間に合わない原因は、オブジェクトが増えたタイミングでガベージコレクションが動作する点にあると推測しました。が、ガベージコレクションそのものを止めてしまうとあっという間にメモリリークが発生してしまいます。
ガベージコレクションについては以下ページが参考になります。GC_STEP_SIZE・GC_STEP_RATIOを変更しました。
mruby/GC処理を読む - Code Reading Wiki
が、なんだかんだでデフォルトの設定が最も安定していたように思います。(正直、根拠が無い部分なので有識者の方がいたらアドバイスが欲しいところ)
困ったこと2. マルチタスク化が難しい
前述の性能にも関連するのですが、mrubyは現状シングルタスクでの動作が前提となっています。マルチタスクで動作させるためにはそれぞれのタスクで別々のVirtual Machineを動かすことになります。
この場合、タスク間の情報共有が問題となります。今回、DataQueue・EventFlagを利用するためのmrbgemsを使用しましたが、独自構造のデータをDataQueueで受け渡すのはハードルが高そうです。
mruby-rtos-toppers:RTOS (TOPPERS) library for mruby.
個人的にはガベージコレクションのみ低優先タスクで動作させられるようになると、デバイスアクセス中の時間を有効活用できるのではないか?と思案しています。(実現しないまま終わってしまいました)
mrubyを使って良かったこと
続いて良かったことを書きます。
関西地区大会のステージで司会者の方に「mrubyの良さって何ですか?」と聞かれて答えに困ったのですが、非エンジニアな方にmrubyの良さを伝えるのは難しいですね。
良かったこと1. 実装しやすい・読みやすい
Rubyの特徴である読みやすい・書きやすいという恩恵を受けることができました。「思考の流れとコードの流れが一致している」とよく言われますが、まさにそれを実感しました。加えて、型宣言が不要ですし、メソッド呼び出し時の()なども省略可能なため、コードに打ち込む情報が必要最低限となり高い生産性を維持できました。
C++では手間をかけて実装したDSLもRubyであれば簡単に実現できました。私が作成したDSLは以下のようなものです。
# コースの構成を記述する
Course.scenario "R Course" do
# 最初の区間情報を記述する
section(forward:@config.forward, line:(:right), balance:{}, tails:0).to event(distance:60)
# 次の区間情報を記述する
section(forward:@config.forward - 20, line:(:right), balance:{}, tails:0).to event(distance:80)
section(forward:@config.forward, line:(:right), balance:{}, tails:0).to event(direction:80)
section(forward:@config.forward, line:(:right), balance:{}, tails:0).to event(distance:40)
# ・・・省略
end
良かったこと2. プログラム書き換えが高速に行える
プログラムの書き換えは、全プラットフォームの中でmrubyが最速でしょう。プログラムを書き換える際は、mrubyのソースコード(.rb)を書き換え、mrubyの中間言語ファイル(.mrb)にコンパイル後、microSDカードにコピーする必要がありますが、これらの作業に要する時間はわずか数秒以内です。
走行調整時間が限られているETロボコンにおいて、プログラムの書き換えを高速に行えることは大きなアドバンテージになります。
良かったこと3. ユニットテストしやすい
mrubyを選択したことで、ユニットテストを簡単に記述できました。RubyではRSpecというテストフレームワークがあり、これを活用することで簡単にテストできるようになりました。
また、ユニットテストでMockを活用する際、C/C++よりも簡単にMockを定義できました。
もう一度、mrubyを選択するなら
今年の反省点を踏まえて、mrubyを使用する際の注意点・やっておいたほうが良いことをまとめます。
1. 処理性能を測定する手段を用意しておく
4msの周期を守れなくなった場合にそれが一目でわかる仕組みを用意した方が良いです。例えば、4msを超過した場合はLEDを赤く点灯する、LCDにログを吐き出すといった方法です。
加えて、処理時間を計測したいシチュエーションが頻発するため、処理時間をマイクロ秒単位で計測する仕組みがあると便利です。
2. Rubyで実現する範囲、C言語で実現する範囲を検討する
4ms周期で行う必要のある処理は極力C言語化したほうが良いです。ただ、この基準だとほとんどがC言語の実装となるかもしれません。
まずは基本走行部分のみ作成し、この時点で4msを守れていないようであれば下層部分から順番にC言語化する、という手順がベターです。基本走行部分を実装した段階で処理性能に目を向けず、本番コースで調整を始めると泥沼にハマります。
3. オフシーズン中に基本走行部分を作っておく
mrubyに限った話ではないですが、オフシーズン中(4月まで)に基本走行部分を作っておくべきです。12/5の記事にて来年度の走行体は本年度と大きく変わらないことが発表されました。
基本走行部分がオフシーズン中に出来上がっていると、後は競技規約に合わせて検証・モデリングすることに注力できるようになります。4月に一から実装を始めるチームと比べて圧倒的有利であることは言うまでもありません。
4. mrubyの動向をウォッチする
mrubyのリポジトリは現在も定期的にpull requestがある状況ですので、念の為ウォッチしておいた方が良いです。軽量Rubyフォーラムのサイトからも情報が得られることがあるので、定期的にサイトをチェックしましょう。
また、mrbgemsも有志の方によって次々と開発されています。githubやTwitterでmrubyというキーワードで定期的に情報収集すると、便利なmrbgemをいち早く見つけられることでしょう。
まとめ
以上、乱文ではありますがmrubyを使ってみてわかったことを記事にしました。結果は残念でしたが、普段とは違った環境でプログラミングできたことが私にとっては非常に有意義でした。
来年、私の屍を乗り越え、チャンピオンシップ大会で爆走するmruby走行体が見られることを心から祈っています。