はじめに
ETロボコン&EV3 Advent Calendar 2018 21日目の記事です.
私は中四国地区から「はれかぜR」という名前の学生チームで ETロボコン2018 に参加しました.先日行われたCS大会では,「TOPPERS賞」を頂くことができて大変満足しています.
チーム名に「R」が含まれているため,大会関係者の方々は粗方予想がついたかもしれませんが,「はれかぜR」のメンバーは ETロボコン2017 にも「はれかぜ」というチーム名で参加していました. ETロボコン2017 では目標であったCS大会に出場することができなかったため(内容は後述),チーム名に「Revenge」の意味を込めています.
「はれかぜ」という名前の由来は,CS大会出場時に提出したアピールポイントより
「晴れの国おかやま」の「晴れ」と,局地風の「広戸風」の「風」を繋げている.
そこには,チーム全体が晴れやかであり,ロボットが風のように速く走ることを願って命名した.
としています.中四国地区大会で質問を受けましたが,決してどこかのアニメから名前を頂いているわけではありません.
私が携わった仕事は,モデル図の制作やプログラムの設計・開発,動作テストを担当しました.
前置きが長くなりましたが,本記事では ETロボコン2017 を経て ETロボコン2018 では「どのようにしてリベンジを達成したのか」と,TOPPERS賞の受賞に繋がった「ケーブル脱落検知」について綴っていきます.
どのようにしてリベンジを達成したのか
私たちは ETロボコン2018 が2回目の参加となるため,前大会で失敗した要因を整理して同じ間違いをしないように努めました.
ETロボコン2017の振り返り
「はれかぜ」のメンバーが初めてETロボコンに出場したのは,昨年の大会でした.ETロボコンに出場した経験を持つ者がチームの中にいなかったため,「とりあえずサンプルプログラムを動かしてみよう」って感じで開発がスタートしました.このときはメンバー全員に大事な行事がなかったので開発できる時間は十分にありました.地区で開催される独自勉強会にも全て参加し,開発意欲はメンバー全員が持っていました.チームは5年間学業を共にしてきた者が集まったので,言いたいことがあれば遠慮なく言い合えるような関係でした.とても素晴らしい環境でした.
4月から開発をスタートした私たちは,サンプルプログラムに機能を追加していく形で開発を進めていました.もう一度言います.「サンプルプログラムに機能を追加していく」形で,開発を行っていきました.サンプルプログラムにコードを追加していくのに最初は抵抗があったのですが,チーム内で「動けば問題ない」って雰囲気が漂っていたので誰も気にせず開発が進みました.後々後悔することになります.
試走会1 (7月末) ではコース完走を達成でき,8月あたりから難所攻略用のコードを書き足していました.この時期あたりから,サンプルプログラムに機能拡張していったことに対して後悔し始めました.その理由は,ソースコードに冗長的な部分が増え,肥大化し始めて収拾がつかなくなったためです.リファクタリングしようにもできない状態でした.こんな感じで競技に力を入れすぎていた私たちは,「モデル図はすぐに完成するだろう」と軽視して,提出日の1週間前までモデルには手を付けませんでした.後々後悔することになります.
モデルはチームで役割分担をして作成していきました.「はれかぜ」のメンバーは私を含め3人だったので,私がUMLを用いたモデル図の作成,一人が工夫点の作成,もう一人がモデル全体のデザインを担当しました.モデル図を実際に描いてみると,何も分からずかなり苦労しました.なんとか試走会2 (8月末) までに完成したため,実行委員の方が開いてくださった「よろず相談会」でモデルレビューをしてもらいました.滅茶苦茶指摘されました.そして,走行も上手くいかずコース完走すらできなくなっていました.メンバー全員,落ち込んでいたのではないでしょうか.
といっても締切間近のモデルをどうにかしなければならず,実行委員の方が指摘してくださった点を全て直さなければという思いで,朝から夜遅くまでモデル図の修正作業をしていました.学校の先生にも確認してもらって,早くから作業を始めていればと後悔しながらモデル図の提出をしました.競技の方は,相変わらずソースコードの問題で開発効率も悪い状態でした.地区大会が台風の影響で延期になったにも関わらず,進捗が生めない日が続きました.
両コースの難所を攻略できるようになったのが大会当日の早朝 (オールしました) で,コースの走行を直さなかった私たちは,無念にも難所にすら到達できずリタイアになりました.しかし,「B-」という評価でしたが,Gold Model賞を受賞できたのは良かったです.
ここで,最初に「とても素晴らしい環境でした」と綴ったのを覚えていますか? ETロボコン2017 を終えて,率直な感想は「環境が良かったのに,何故こうなった???」でした.
明確な問題は潰せ
題目のとおり,昨年の二の舞を演じたくなかったため,ETロボコン2018 に出場することになった私たちは開発に取り掛かる前に前大会の反省をしました.
- サンプルコードを改変しプログラムを作成していったため,走行体のプログラム仕様を完全に理解していない(動けばいいという脳筋的発想)
- 競技を優先したため,プログラムを基にモデル図を作ることになり,モデル図が思うように作れない
- 現在取り組んでいる作業のゴールを決めていないため,泥沼化して時間が足りない
まだまだ問題はありましたが,とりあえず上記3点を私たちは重要視しました.
まず1つめについて,EV3RTの仕様を完全に理解していなかった私たちは,最初の取り組みとしてタスクの仕組みをゼロから勉強し直しました.理解した内容が正しいのかどうかを確かめるために作ったのが,本記事の後半部分にある「ケーブル脱落検知」です.実際に実装してみて,きちんと動作していることを確認できたときは自信がつきましたし,最初に行った取り込みについてTOPPERSプロジェクトに評価して頂けてとても嬉しかったです.その後は,大雑把にどんなクラスを作るかを先に考えて,サンプルプログラムに一度も触れず,一からプログラムを作成しました.常に「どうやればコードが綺麗に書けるか」を意識し,ファイル分割も丁寧に行ったので,修正がしやすいプログラムが作れたのではないかと思っています.
2つめの点について,先述したようにプログラムを作る前に大雑把に作成するクラスを考えていたため,モデル図の作成が物凄くやりやすかったです.しかも,プログラムを作っているときに,「クラスの構成ってどうだっけ?」って思うときが幾度かあって「モデル図が早く欲しい!!」と感じることが多かったので,早い段階からモデル図を作成できました.そのおかげで前大会と比べて圧倒的に8月は楽になりました.
3つめはメンバー同士でタスクの管理がしたかったので「Trello」を使いました.カードにコメントをつけることでメモ代わりになりますし,カードに期限を設けることで時間の管理をすることができました.
前大会と違う他の取り組みは,私自身ツールを作るのが大好きで(数自体は少ないですが),Tera Termで取得したログデータを毎度表計算ソフトに渡してグラフにするのが面倒だったので,ボタン1つで実行ファイルの送信が可能で,受信したログデータからリアルタイムでグラフを作成するツールを作りました.また,gitを使って開発を行ったため,一番良かったパラメータのバージョンへいつでも戻すことができました.これらの導入で開発効率が向上したと思います.
以上の取り組みの成果で,地区大会ではGold Model賞,総合優勝,IPA賞を頂け,CS大会でのTOPPERS賞に繋がったと思います.
ケーブル脱落検知
タスクをうまく利用できないか
ETロボコン2018 に出場するにあたって最初の取り組みとして,EV3RTのタスク処理について勉強しました.こちらのブログを参考にさせて頂きました.各記事にサンプルコードを掲載して下さっているため,とても分かりやすいと思います.
このブログを参考にタスクについて勉強して,倒立振子用とBluetooth通信用とは別にタスクを用意し,何かに利用できないか考えました.そこで,前大会でプログラムを開発しているときに,ケーブルの脱落が原因で走行体が暴走してしまう光景を何度も目にしていたことを思い出しました.外観では正常に接続しているように見えても,実際は脱落していたという経験をした方もいらっしゃるのではないでしょうか.そこで,私たちは「この問題を解決するのにタスクを上手く利用できるのでは?」と考え,ケーブル脱落検知処理を組み込むことにしました.
ケーブル脱落検知プログラム
まず,ケーブルが脱落している場合,プログラムの挙動がどうなるのかを実験してみることにしました.言語は C/C++ を使います.以下は実験用プログラムのソースコードです.公開されているサンプル「app.cpp」(もしくは「app.c」)にそのまま上書きしてください.
#include "ev3api.h"
#include "app.h"
static const sensor_port_t
sonar_sensor = EV3_PORT_2,
color_sensor = EV3_PORT_3,
gyro_sensor = EV3_PORT_4;
static const motor_port_t
left_motor = EV3_PORT_C,
right_motor = EV3_PORT_B,
tail_motor = EV3_PORT_A;
/// <summary>
/// センサポートの設定 (ケーブル接続の状態とは関係なし)
/// </summary>
void init(){
ev3_sensor_config(sonar_sensor, ULTRASONIC_SENSOR);
ev3_sensor_config(color_sensor, COLOR_SENSOR);
ev3_sensor_config(gyro_sensor, GYRO_SENSOR);
ev3_motor_config(left_motor, LARGE_MOTOR);
ev3_motor_config(right_motor, LARGE_MOTOR);
ev3_motor_config(tail_motor, LARGE_MOTOR);
}
/// <summary>
/// 引数 str を LCD に表示
/// </summary>
/// <param name="str">LCD描画文字列</param>
void draw(const char* str){
ev3_lcd_fill_rect(0, 0, EV3_LCD_WIDTH, EV3_LCD_HEIGHT, EV3_LCD_WHITE);
ev3_lcd_draw_string(str, 0, 0);
}
/// <summary>
/// メインタスク
/// </summary>
void main_task(intptr_t unused){
init();
ev3_lcd_set_font(EV3_FONT_MEDIUM);
draw("Ultrasonic");
ev3_ultrasonic_sensor_get_distance(sonar_sensor);
draw("Color");
ev3_color_sensor_get_color(color_sensor);
draw("Gyro");
ev3_gyro_sensor_get_angle(gyro_sensor);
draw("L_Motor");
ev3_motor_rotate(left_motor,1,5,true);
draw("R_Motor");
ev3_motor_rotate(right_motor,1,5,true);
draw("T_Motor");
ev3_motor_rotate(tail_motor,1,5,true);
draw("Complete");
ev3_speaker_play_tone(NOTE_D5, 200);
ext_tsk();
}
これをコンパイルして,以下の各状態でプログラムを実行してみてください.
- ケーブルをすべて接続している状態
- ジャイロセンサのみケーブルを外した状態
- ジャイロセンサと尻尾モータのケーブルを外した状態
どのような動作になりましたか?
1.の状態でプログラムを起動すると,EV3から音が鳴ることを確認できると思います.EV3から音が聞こえると,メインタスクが最後まで処理を実行し終えたことになります.
2.の場合は,LCDに「Gyro」と表示されてプログラムが停止しているように見えるはずです.ここからジャイロセンサのケーブルを接続すると,1.と同様に,EV3から音が鳴りプログラムが最後まで実行したことが確認できます.
3.の場合も,LCDに「Gyro」と表示した状態でプログラムは次の処理を行いません.ここで,先に尻尾モータのケーブルを取り付けてからジャイロセンサ側のケーブルを接続すると2.と同じ様子になるはずです.一方で,尻尾モータのケーブルを接続せず,ジャイロセンサ側のケーブルを取り付けると,LCDには「T_Motor」と表示されます.
これらから
EV3から接続された各種センサへある要求(測定値の取得,モータの回転等)を送信すると,センサが正しくEV3と接続できていない場合,プログラムはセンサが正常な動作をするまで次の処理を実行することができない
ということになります.
上記のサンプルプログラムでも,一応ケーブル脱落の確認ができています.しかし,1つずつケーブルの脱落情報が返ってくるのを待つより,一度にすべてのケーブル脱落状況を把握できたほうが楽だと思います.
そこで,一度にすべてのケーブルの接続状況を通知させるには__周期タスク__を利用すればよいと考えました.それは,ケーブルの脱落によって実行中のタスク (タスクAとする) の動作が停止している場合,周期タスク (タスクBとする) を利用することで強制的に別の処理を行うことができるためです.但し,タスクB で行う処理が終われば,次の呼び出しまで タスクA の処理を実行することになります. タスクA がケーブルの脱落によって停止した状態である場合,次に タスクB が呼び出されるまで何もできません.そこで, タスクB 内の処理で タスクA をリセットする処理を入れます. そうすることで, タスクA の実行中にケーブル脱落が原因で動作が停止しても タスクB がリセットしてくれるため, タスクA は停止した状態から免れることができます.
タスクを用いたケーブル脱落検知プログラムのソースコードは以下になります.(タッチセンサは仕様上脱落検知ができないため省いています.)
INCLUDE("app_common.cfg");
#include "app.h"
DOMAIN(TDOM_APP) {
CRE_TSK( MAIN_TASK, { TA_ACT, 0, main_task, TMIN_APP_TPRI + 2, STACK_SIZE, NULL } );
CRE_TSK(CHECK_ACCESS_TASK, { TA_NULL, 0, access_task, TMIN_APP_TPRI + 1, STACK_SIZE, NULL} );
EV3_CRE_CYC(OBSERVE_TASK_CYC, { TA_NULL, 0, observe_task, 500, 500} );
}
ATT_MOD("app.o");
#ifdef __cplusplus
extern "C" {
#endif
extern void main_task(intptr_t unused);
void access_task(intptr_t unused);
void observe_task(intptr_t unused);
#ifdef __cplusplus
}
#endif
#include "ev3api.h"
#include "app.h"
static const sensor_port_t
sonar_sensor = EV3_PORT_2,
color_sensor = EV3_PORT_3,
gyro_sensor = EV3_PORT_4;
static const motor_port_t
left_motor = EV3_PORT_C,
right_motor = EV3_PORT_B,
tail_motor = EV3_PORT_A;
/// <summary>
/// センサポートの設定 (ケーブル接続の状態とは関係なし)
/// </summary>
void init(){
ev3_sensor_config(sonar_sensor, ULTRASONIC_SENSOR);
ev3_sensor_config(color_sensor, COLOR_SENSOR);
ev3_sensor_config(gyro_sensor, GYRO_SENSOR);
ev3_motor_config(left_motor, LARGE_MOTOR);
ev3_motor_config(right_motor, LARGE_MOTOR);
ev3_motor_config(tail_motor, LARGE_MOTOR);
}
/// <summary>
/// ケーブル脱落検知に必要なタスクの起動
/// </summary>
void checkStart(){
ev3_sta_cyc(OBSERVE_TASK_CYC);
tslp_tsk(500);
}
/// <summary>
/// ケーブル脱落検知の終了通知 と タスクの停止
/// </summary>
void checkFinish(){
ev3_speaker_play_tone(NOTE_D5, 200);
ev3_stp_cyc(OBSERVE_TASK_CYC);
}
/// <summary>
/// メインタスク
/// </summary>
void main_task(intptr_t unused){
init();
ev3_lcd_set_font(EV3_FONT_MEDIUM);
checkStart();
checkFinish();
ext_tsk();
}
//---------------------------------------------------------------------------------------
// 脱落検知対象ポートの個数
const int CHECK_PORTS_COUNT = 6;
// 状態管理変数
int phase = -1;
// フラグ変数 (2進数で扱う)
int status = 0;
// LCD 描画用文字配列
char STATUS_PORTS[] = "XXXXXX";
/// <summary>
/// ケーブル脱落確認タスク
/// </summary>
void access_task(intptr_t unused){
while(true){
switch(phase){
case 0:
ev3_ultrasonic_sensor_get_distance(sonar_sensor);
break;
case 1:
ev3_color_sensor_get_color(color_sensor);
break;
case 2:
ev3_gyro_sensor_get_angle(gyro_sensor);
break;
case 3:
ev3_motor_rotate(left_motor,1,5,true);
break;
case 4:
ev3_motor_rotate(right_motor,1,5,true);
break;
case 5:
ev3_motor_rotate(tail_motor,1,5,true);
break;
}
if(phase < CHECK_PORTS_COUNT){
// ケーブルの正常接続を記録
status |= (0x01<<phase);
STATUS_PORTS[phase++] = 'O';
}
}
}
//---------------------------------------------------------------------------------------
// 状態管理変数の初期値
const int START_PHASE = 0;
// フラグ変数の受理状態
const int REQUIRED_NUMBER = 0b00111111;
/// <summary>
/// CHECK_ACCESS_TASK を監視するタスク
/// </summary>
void observe_task(intptr_t unused){
ter_tsk(CHECK_ACCESS_TASK);
if(REQUIRED_NUMBER == status){
// ケーブルが全て接続されている場合
ev3_lcd_fill_rect(0, 0, EV3_LCD_WIDTH, EV3_LCD_HEIGHT, EV3_LCD_WHITE);
ev3_motor_stop(left_motor, true);
ev3_motor_stop(right_motor, true);
ev3_motor_stop(tail_motor, true);
}else{
// ケーブルが脱落している場合
if(phase < START_PHASE || CHECK_PORTS_COUNT <= phase){
// 状態管理変数が異常な値を持つ or すべてのケーブルを確認した場合
// ケーブル脱落検知を最初から始められるように初期化する
ev3_lcd_fill_rect(0, 0, EV3_LCD_WIDTH, EV3_LCD_HEIGHT, EV3_LCD_WHITE);
char header[32], footer[32];
sprintf(header, "%c %c %c -", STATUS_PORTS[3], STATUS_PORTS[4], STATUS_PORTS[5]);
sprintf(footer, "- %c %c %c", STATUS_PORTS[0], STATUS_PORTS[1], STATUS_PORTS[2]);
ev3_lcd_draw_string(header, 0, 0);
ev3_lcd_draw_string(footer, 0, 105);
status = 0;
phase = START_PHASE;
}else{
// ケーブルが脱落して CHECK_ACCESS_TASK が止まっていた場合
// ケーブルの脱落位置を記録
STATUS_PORTS[phase++] = 'X';
}
act_tsk(CHECK_ACCESS_TASK);
}
}
ソースコード上にコメントを挿入しているため,その場所で何をしているかが分かると思います.それでは動作確認をしてみましょう.
まず,プログラムを実行する前に,全てのケーブルを抜いておきます.そこから,プログラムを実行すると,LCDに'X'(脱落を意味する)が各ポートの付近に表示されます.そして,1つずつケーブルを接続していくと,'X'から'O'(正常接続を意味する)に変わっていくことを確認できると思います.すべてのケーブルが正常に接続されたら,EV3から音が鳴ります.タスクを用いたケーブル脱落検知処理を用いることで,一度にすべての脱落状況を取得することができるようになりました.
但し,app.cfgに記述してある OBSERVE_TASK_CYC タスクの起動周期 (第5パラメータ) について,上記サンプルよりベストな値が存在すると思うので,各自で実験してみてください!!
今大会の反省
ETロボコン2018 に出場して,前大会より成績は良くなり,メンバー全員大きく成長できたのではないかと思います.前大会の失敗も上手く活かすことができました.しかし,まだまだ改善点は多くあります.
今大会で大きな問題となったのは,私が一人で開発を突っ走ってしまったことです.私はチームリーダーではなかったのですが,前大会の地区大会でGold Model賞を受賞できたことから,チーム内でモデル図を描くのは私という雰囲気になっていました.そこから,モデル図を書くことを意識してプログラムを開発していったため,私しかプログラムが変更できない状態になりました.
一人で突っ走って開発をしたために,メンバーを置いてきぼりにした感じになってしまったので,深く反省しております.このような開発は,エンジニアの育成とシステムの分析・設計やプログラムの開発に挑戦する機会を提供することを目的としたETロボコン開催の趣旨に反していると思います.懇親会で,他のチームでもよくある話だと伺ったのですが,私の中ではこれが一番改善したい問題です.
といっても,この開発方法はETロボコンの開催趣旨に反しているだけで,全てが悪いわけではないと思います.経験談になりますが,難所攻略のプログラムを作成しているときに,「こういう仕様だからこの機能の追加は面倒」って考えることがよくあって機能拡張が上手く行かないときが多々ありました.予めプログラムの構造を知っているため,柔軟な発想ができませんでした.しかし,プログラムの構造を共有できていなかったメンバーはそういう制限はお構いなしなので,結構無理な注文を言ってくれました.無理な提案に最初は結構イライラしていたのですが,プログラムが改善されたときはメンバー様様だと感じました.
こういう経験をしているので今大会の開発スタイルを全て否定することはできません.とりあえず,今大会ではもう少しメンバーと情報共有しておくべきだったと思います.
もう1つ個人的な反省ではありますが,モデル図を描くのに読んだ書籍が2冊だったのが少し問題だと思います.しかも,その2冊は前大会のときに読んだものでした.今大会では特に書籍は読まずにモデル図を描きました.その代わり,地区で開かれる「よろず相談会」にとてもお世話になりました.もっと多くの書籍に触れていれば,上述した開発スタイルにならずに済んだかもしれないです.
まとめ
ETロボコン2018 でリベンジを達成できたのは,やはり基礎は大事だと考え,初心に返ったことが大きいと思います.当たり前のことを問題点で列挙しましたが言葉では伝わらない部分もあり,ETロボコンに参加してこそ体験できることだと思います.
勉強会でよく聞く話ですが,コードからモデル図を描くのは厳しいし,そこで問題が見つかればプログラムの開発に大きな支障をきたします.前大会の私たちがそうでした.「締切に追われてピンチ!」や「リファクタリングでピンチ!」になりかねませんので,モデル図を意識して開発に臨みましょう.
CS大会の表彰式で,チームメンバーが多いところは有利だし強いと思いました.メンバー集めをしっかりしましょう!
次大会でリベンジしたいと考えているチームや出場経験がなく将来出場を検討している方々に,本記事が少しでも参考になれば幸いです.