2012b/Member/ddnp/Mission1
をテンプレートにして作成
[
トップ
] [
新規
|
一覧
|
検索
|
最終更新
|
ヘルプ
|
ログイン
]
開始行:
#contents
*ライントレス[#linetrace]
**まずはラインに沿って動かしてみる[#tracing]
とりあえず、ラインに沿って動くことが出来なければボールを運ぶも何も無いので、ラインをトレースするプログラムを描いてみる。
そこで、練習用として、次のようなコースを用意した。
この時使った機体は、マニュアルに設計図の乗っているデフォ子(量産型)。
&ref(2012b/Member/ddnp/Mission1/practice_course.jpg, 100%, 練習用コース);&ref(2012b/Member/ddnp/Mission1/default_bot.JPG, 100%, 練習用ロボ、デフォ子);
***黒ライントレス[#black_trace]
光センサーは、光の反射量を読み取るため、黒い部分の値は低く、白い部分の値は高い数値が出る。値は0〜100。
そこで、光センサーが白い部分を読み取ったら、つまり光センサーの値がある一定値を超えたら、旋回するようにしてみる。
コースの黒いところから、白いところまでの値を事前に読み取り、以下のプログラムを作成。
黒いところと白いところで、値の違いは大体38〜51。よって、40〜50あたりで判定を行うよう、しきい値を設定する。
//---------------------//
// 定数定義
//---------------------//
#define WHITE 50 // 白い部分のしきい値
task main()
{
//-----------------------//
// 初期化
//-----------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT);
while(true)
{
OnFwd(OUT_AC); // 常に前進
// 白い場所を読み取っている間旋回
while(SENSOR_2 >= WHITE)
{
// 反時計回りに旋回
OnFwd(OUT_A);
OnRev(OUT_C);
}
}
}
ちなみに、上記のプログラムでは、「黒い部分に入るまで旋回する」というような処理を行なっているが、
「白い部分に入ったら、黒が濃い部分を読み取るまで旋回する」とした方が、ライン中心に沿ってトレスする様になり、スムーズになるようだ。
また、白いラインに入った際、少し時間を空けてから黒い部分を探しだすようにすると、直線では少々スピードが上がる。
ただし、開けすぎて白い部分で旋回し続けるようにならないよう注意。特に急カーブ等。
つまり、定数定義に以下の二文を追加し、
#define BLACK 40 // 黒い部分のしきい値
#define STRAIGHT 10 // 白い部分読み取りの際の遊び
「白い部分を読み取っている間旋回」するプログラムを、以下の様に書き換える
// 白い部分を読み取った時
if(SENSOR_2 >= WHITE)
{
Wait(STRAIGHT); // 少しだけ直進
// 黒いラインの中心付近に来るまで旋回
while(SENSOR_2 <= BLACK)
{
// 反時計回りに旋回
OnFwd(OUT_A);
OnRev(OUT_C);
}
}
これで、多少スムーズにライントレスするようになるだろう。
ちなみに、BLACKの値をあまり低く設定すると、中心からずれやすい。黒い場所を読み取ってから次の動作に移るまで、ごく微小の遅延があるようだ。
よって、BLACKの値は多少高めでも問題無いだろう。
ところで、このプログラムには大きな問題がある。左カーブ時はいいが、右カーブ時に半回転し、逆走する事態が発生する。
そこで、ある程度探しても黒線が見つからなかった場合、逆回りして黒線を探すように改造してみる。
そこで書いたのが、以下のプログラム。
//---------------------//
// 変数定義
//---------------------//
#define BLACK 42 // 黒のしきい値
#define WHITE 50 // 白のしきい値
#define RADIO_SIGHT 60 // 黒い部分を探す際の視野
#define STRAIGHT 10 // 黒い部分を探す処理に移る前の遊び
//--------------------------//
//
// その場で旋回する関数
//
// cw 旋回方向を指定する変数 奇数なら右回り(clock wise)
//-------------------------//
void stay_turn (int cw)
{
// 時計回り
if(cw % 2)
{
OnFwd(OUT_A);
OnRev(OUT_C);
}
// 反時計回り
else
{
OnFwd(OUT_C);
OnRev(OUT_A);
}
}
//-------------------------//
//
// メイン関数
//
//-------------------------//
task main ()
{
//----------------------//
// 初期化
//----------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT); // 光センサーを設定
//----------------------//
// 変数定義
//----------------------//
int rotate = 1; // 回転方向を指定 奇数は右回り
int times = 0; // 旋回回数を記憶 かつ視野を増加させる
int switch = 1; // 処理を抜けるための変数
int timer = 0; // タイマーの時刻を記憶
//-----------------------//
// 黒ライントレス
//-----------------------//
while(switch)
{
// 黒い間は直進
while(SENSOR_2 <= WHITE)
{
OnFwd(OUT_AC);
}
Wait(STRAIGHT);
// 白い部分に来た時
if(SENSOR_2 > WHITE)
{
times = 0; // 回転回数初期化
// 黒い部分を探しながら、カーブの方向を決定する
while( (SENSOR_2 > BLACK) && (times <= 3) )
{
ClearTimer(0); // 旋回時間測定用のタイマーをセット(タイマー0)
// 視野の分だけ黒い場所を探す
while ( (SENSOR_2 > BLACK) && (FastTimer(0) <= RADIO_SIGHT * times) )
{
stay_turn(rotate); // 旋回
}
// 黒い部分が見つからなかった場合
if(SENSOR_2 > BLACK)
{
rotate ++; // 逆回転を指定
times ++; // 回転回数と視野を増加
}
}
// 黒い線が見つからなかった時(一回転以上した時)
if(times > 3)
{
// 完全に白い場所に出たということで、処理を中止。あきらめろ。お前はよくやった。もう休むんだ。
switch = 0;
}
}
}
// 停止
Off(OUT_AC);
}
黒線を探す際の視野というのは、どの程度回転させるかを示している。
急カーブでコースアウトした際に、回転する方向は合っていても、視野が狭ければ逆側を探索しだすため、効率が落ちてしまう。
最悪、逆探索をした際に近くにある別の線を読みとる場合も考えられる。
なので、視野の値は、急カーブでも一回目の旋回で曲がりきれるくらいの値にするのが好ましい。
このプログラムでは、大体120度くらい旋回するようにしてある。
何はともあれ、これで黒線をトレースすることができた。めでたしめでたし。
と、思うじゃん?
先生「白と黒の境界をトレスする方が効率が良いです」
( ゚д゚) ...
( ゚д゚) ...&ruby(どういうことなの…?){What do you mean...?};
***境界ライントレス[#border_trace]
黒ライントレスの方法では、ラインから外れた場合、ただがむしゃらに回転してラインを探すほか無かった。
要するに、カーブが右カーブなのか左カーブなのかを判別する能力がないわけだ。
しかし、黒線と白い部分の間を進む、つまり境界線をトレースするようにすると、面白いくらいにロボットが賢くなる。
#ref(2012b/Member/ddnp/Mission1/border_trace.gif, 100%, 境界トレスの仕組み)
例えば、ロボットに「特に白い部分を感知したら右に、特に黒い部分を感知したら左に回って、境界線を探すようにする」
とプログラムしたとすると、ロボットは黒いラインの左側をトレースするようになる。
これによって最も嬉しいことは、ロボット自身で「境界線のどちら側にずれているのか」を判別できるようになることだ。
これは、ロボットが右カーブと左カーブを判別することが出来るようになることにもつながり、非常にスマートな方法だと言える。
早速、プログラムを書いて動かしてみよう。
//----------------------------//
//
// 定数定義
//
//----------------------------//
#define DEEPBLACK 40 // 濃い黒のしきい値
#define BLACK 43 // 黒のしきい値
#define WHITE 47 // 白のしきい値
#define DEEPWHITE 50 // 濃い白のしきい値
//----------------------------//
//
// メイン処理
//
//----------------------------//
task main()
{
//----------------------------//
// 初期化
//----------------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT);
//----------------------------//
// 境界ライントレス
//----------------------------//
while(true)
{
// 白側に出た時
if(SENSOR_2 > DEEPWHITE)
{
// 境界に近づくまで回転
while(SENSOR_2 <= WHITE)
{
// 時計回り
OnFwd(OUT_A);
OnRev(OUT_C);
}
}
// 黒側に出た時
else if(SENSOR_2 < DEEPBLACK)
{
// 境界に近づくまで回転
while(SENSOR_2 >= BLACK)
{
// 反時計回り
OnFwd(OUT_C);
OnRev(OUT_A);
}
}
// 境界線上は直進
else
{
OnFwd(OUT_AC);
}
}
}
黒トレスと比べて、すっきりしたプログラムになった。
境界ライントレスについては、これで問題なく動作したので、しばらくこのプログラムを使うことにしよう。
*実際のコースで動作させる [#c883296f]
**スタートからゴールまでのライントレス[#from_start_to_goal]
***課題の題材となったコースと注意するポイント[#checkpoint]
下図を参照。
このコースの端からスタートして、もう片方の端にあるゴールにボールをシュートすることとなる。
#ref(2012b/Member/ddnp/Mission1/course_1.jpg, 100%, 課題コース)
S字カーブ、直角カーブ、急カーブ、交差点と、入り組んだコースとなっている上、
S字カーブの真ん中に障害物を設置し、それに触れないように進まなければならない。
まずは、シュートの前段階として、スタート地点からゴール地点まで行き着くようなロボットを完成させることとする。
そのためにクリアしなければいけない課題として、以下のことが想定される。
-S字の障害物を避けるため、本体を小さくする。
-急カーブを曲がれるよう工夫する。
-交差点と直角カーブの区別を明確にする。
-ゴールを判別し、ゴールに標準を合わせる。
とりあえず、次の項では本体を小さくすることに焦点を当ててみる。
***その前にデフォ子の追悼式[#see_you_default_bot]
この日、授業の初めの方から付き合って下さったデフォ子様の解体の日となりました。
お集まりの皆様方には、デフォ子様との想い出の日々が以下都合により割愛
***本体を小さくする[#make_the_robot_small]
とりあえず、実際に本体を作らず、紙面上で実験。
ブロックやシャフトを使い、縦長、横長など様々な大きさの枠を作ってはラインにそって手で動かしてみると、以下の事がわかった。
-デフォ子に使われているタイヤは太すぎて、使うには難しい。
-左右のモーターが多少大きいので、付ける位置を近づけるなどして、横幅を小さくした方がいい。
-旋回する時中心となる点から、機体が前や後ろに長すぎると、内輪差などの影響で障害物にぶつかる。
まぁぶっちゃけ全体的にコンパクトになってればなってるほど後先の事を考えなくていいと感じたので、可能な限り小さくすることを考えてみる。
結果的に、以下の三点を実施。
-二輪に変更
--実際にはバランスを取るために三輪。ただ三番目のタイヤは、どちらかというと支え棒といったほうが正しい。
-モーター同士をできる限りくっつけてみた。
--しかし不安定性が増したので、上下に固定用のブロックを足して、強化している。
--同時に、車軸を固定するパーツ&ref(2012b/Member/ddnp/Mission1/parts.gif, 100%, シャフト固定パーツ);が使えなくなった。代用として車軸を左右から押さえつけてはめ込むようにしたが、問題ない様子
-RCX本体が縦長なので、デフォ子のように寝かせるのでなく、立ててみることにした
--中心でなく後ろ側に寄せて立てることで、前側に光センサーやボールを掴むパーツを配置できるくらいの余裕を作る。
--嬉しいことに、寝かせるより立てたほうが、本体ががっちり固定されて扱いやすいことが判明。嬉しい誤算。
--センサーが離れてしまったのでコードを延長したのだが、延長部分がRCXの裏側に張り付くのでまとめやすい。これまた嬉しい誤算。
--ただし、これによって重心が後ろに傾いた。何も影響がなければいいが…。
--欠点としては、スイッチ類が押しにくかったり、View値が読みづらかったりする。
これを考慮して出来た機体がこれ。
#ref(2012b/Member/ddnp/Mission1/half_of_robot.jpg, 100%, 半完成ロボット)
さらに、光センサーの位置についても考慮してある。
出来るだけ正確に読み取れるように、地面に近づけ、本体の中心に近い位置に設置してある。
このことにより、光センサーが本体の中心からより離れている場合と比べて、多少回転が早くても誤作動が少なくなる。
つまり、
#ref(2012b/Member/ddnp/Mission1/degree_difference.gif, 100%, 回転の軸からより近い場所にある光センサーの読み取り距離の差)
上図のように、光センサーの位置によって、回転した時間が同じでも、光センサーが移動する距離に差が出てくる。
これはつまり、回転の中心に光センサーを寄せるほど、ゆっくりと読み取りが出来るということである。
よって、多少早い動作をしても、光センサーの読み取りミスが生じることは無くなるはずである。
ただし、欠点もある。近くに寄せ過ぎると、光センサーの読み取りが一点に集中し過ぎて、
ラインがほぼ真下にあるのに読み込めないという事態が発生しうる。よって、調度良い距離を見つけることが必要となる。
今回の機体では、大体写真の位置で読み取るよう設定した。
&ref(2012b/Member/ddnp/Mission1/read_distance.jpg, 100%, 光センサーの地面からの近さ);&ref(2012b/Member/ddnp/Mission1/read_area.jpg, 100%, 光センサーの中心の軸からの距離);
これにて基盤は完成。
***ゴールを目指す[#aim_the_goal]
機体は出来た。ということで、テストも兼ねてラインをトレースしてみることにする。
試しに、先ほど作った境界トレスのプログラムを動作させてみたところ、トレース自体に問題はなかった。
唯一変更した点は、光センサーのしきい値くらいである。
それにしても、再利用できてよかった。
さて、ラインはトレースできても、ゴールまで行きつけなければ仕方がない。
この時点で、自分がクリアすべき課題は
+コース途中の交差点をどう通過するか
+ゴールを判定するにはどうするか
の2つであった。
#ref(2012b/Member/ddnp/Mission1/course_2.jpg, 100%, 通過すべき交差点の位置)
まずは、途中の通過すべき交差点の処理を考える。
普通なら、交差点であることを判別して、黒い線を跨ぐような処理をすればいいのだが、
ちょっとした思いつきで、交差点の判別をしないで超えてみることにした。
使用するのは、前に作った黒トレスのプログラム。
交差点に突入する前に、黒ライントレスを開始し、
通り過ぎた頃に黒ライントレスから境界ライントレスに移行するよう、タイマー機能を使用して実装してみる。
ただし、黒ライントレスから境界ライントレスに移行する時に、ライン左側にいなければ逆走を開始してしまうため、
本来は黒ライントレス時の回転方向を決定する変数を利用して、ラインどちら側にいるかを判定する。
//--------------------------//
//
// 定数定義
//
//--------------------------//
#define DEEPBLACK 40 // 濃い黒のしきい値
#define BLACK 45 // 黒のしきい値
#define WHITE 50 // 白のしきい値
#define DEEPWHITE 53 // 濃い白のしきい値
#define CHANGE_BLACK 1100 // 境界→黒トレス切り替え時間
#define CHANGE_OUTLINE 400 // 黒→境界トレス切り替え時間
#define SEESIGHT 120 // 黒トレス時のライン捜索時間
//---------------------------//
//
// メイン処理
//
//---------------------------//
task main()
{
//-------------------------//
// 初期化
//-------------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT); // 光センサーを設定
ClearTimer(0); // 境界・黒ライントレスの入れ替わり時間を測定
//----------------------//
// 変数定義
//----------------------//
int rotate = 1; // 回転方向を指定 奇数は右回り
int times = 0; // 旋回回数を記憶 かつ視野を増加させる
//-------------------------//
// ライントレス
//-------------------------//
while(true)
{
// 指定時間外かつライン左側で輪郭線ライントレス
if((FastTimer(0) <= CHANGE_BLACK) || (FastTimer(0) >= CHANGE_BLACK+CHANGE_OUTLINE && clockwise % 2))
{
//---------------------------//
// 輪郭線ライントレス
//---------------------------//
・・・
・・
・
}
// 指定時間内かつライン右側で黒ライントレス
else if(FastTimer(0) > CHANGE_BLACK || FastTimer(0) < CHANGE_BLACK+CHANGE_OUTLINE && !(clockwise % 2))
{
//----------------------------//
// 黒ライントレス
//----------------------------//
・・・
・・
・
}
}
}
ライントレス処理は上記とほぼ同じなので省略。
途中、プログラムの入ったUSBメモリをなくして作りなおしたので、変数名が変わっているくらい。
USBメモリの管理には気をつけましょう。
これにて、目論見どおり交差点を渡ることが出来た。
#ref(2012b/Member/ddnp/Mission1/course_3.jpg, 100%, シュートの位置)
次は、ゴール、つまりシュートする位置を判別出来るようにしなければならない。
といっても、シュートする位置を示す特徴が、交差点くらいしかないので、結局交差点を判別するプログラムを考えることに。
そこで、境界ライントレス時に、旋回する時間が長い場合は、直角カーブまたは交差点と判別するプログラムを書くことにした。
#ref(2012b/Member/ddnp/Mission1/course_4.jpg, 100%, コーナーの数)
地形的な特徴と、黒ライントレスの処理があることから、黒側→白側に向かって旋回している時にこの処理を行えば、
ゴールまで計3つのコーナーを通過することになるので、3つ目の直角でシュートを行えるようにプログラムを改造してみる。
定数と変数を付け加え、
#define CORNER_TIME 40 // コーナーと判別する時間
int backturn = 0; // コーナーを曲がるのにかかった時間を記憶
int corner_times = 0; // 曲がったコーナーの数
境界ライントレスの、反時計回り旋回時のプログラムをこのように書き換えた。
// 黒側→白側
else if(SENSOR_2 < DEEPBLACK)
{
ClearTimer(2); // コーナーを曲がっている時間を測定
// 白側へ旋回
while(SENSOR_2 < BLACK)
{
// 左旋回
OnFwd(OUT_C);
OnRev(OUT_A);
}
// 曲がるのに時間がかかった場合、直角だと判断
if(FastTimer(2) >= CORNER_TIME)
{
backturn = FastTimer(2)+5; // 曲がるのにかかった時間を記憶
corner_times++; // コーナーの数を数える
PlayTone(440, 30); // コーナーの数を数えたことを知らせる
}
}
これで直角を判別してくれるようになった。
直角を判定した時には、機体はコーナーを曲がりきってしまっているので、
シュート処理するときにはゴールの方向へ機体を向ける必要がある。
そこで、曲がるまでにかかった時間を記憶して、シュート処理の時に、その時間だけ逆旋回させればよい。
しかし、処理の遅延なのか、実際には多少ズレが生じるので、そのズレの解消のために、タイマーの時間にさらに5を足している。
さらに、シュート処理に移るときは、ライントレスをする必要はないので、
コーナーを3回以上曲がった場合に処理を抜けるように、ループ処理を変更してみる。
つまり、
while(true)
の一文を、
while(corner_times < 3)
に変更する。
ここで、再び問題が発生。
コーナーで、「2回以上コーナーを曲がったことにしてしまう処理が時たま発生する。」というもの。これではゴール前でシュート処理に移行してしまう。
どうやら、トレスの方法に、「色が濃い場所までいってから、薄いところまで軌道を修正する方法」を用いているため、
コーナーに入る角度によって、コーナーのトレスに2,3回軌道修正をしなければいけない状況があるようだ。
コーナーと判定する時間を45くらいまで増やしてみたところ、今度はコーナー自体を判別しなくなる場合が出てきてしまったので、
定数でどうにかするには心もとない。
ということで、一度コーナーを判定したら、しばらくコーナー判定を無視するようにプログラムを改善してみた。
定数と変数をさらに追加。
#define CORNER_WAIT 1000 // コーナー判定を無視する時間
int corner_wait = -800; // コーナーを曲がった時の時間
コーナー判定プログラムは次のようにした。
// コーナーを判定且つ以前コーナーに来てからしばらく経過している場合
if(FastTimer(2) >= CORNER_TIME && FastTimer(0) >= corner_wait+CORNER_WAIT)
{
backturn = FastTimer(2)+5; // 曲がるのにかかった時間を記憶
corner_times++; // コーナーの数を数える
PlayTone(440, 30); // コーナーの数を数えたことを知らせる
corner_wait = FastTimer(0); // コーナーを曲がったときの時間を記憶
}
コーナーを曲がるときの条件式に合わせて、コーナーを曲がった時の時間の初期値を-800にしている。
コレがないと一個目のコーナーを判別できない可能性が高い。
改善前もコーナー以外で誤動作することはなかったが、こうすることでさらに確実に、コーナーのみを選び出すことが出来るだろう。
ところがどっこい今度も謎の問題が発生
図の部分だけ、曲がりきることが出来ない問題が発生した。
#ref(2012b/Member/ddnp/Mission1/course_5.jpg, 100%, 謎の現象 発生位置);
定数値をいじってみるも、問題が解決しないので、まさかと思い光センサーの値を確認。
この場所だけ、白の部分が50という値を観測。他が55なので、明らかにおかしい。
探ってみたところ、どうやら自身の影が光センサーを邪魔していたらしい。
光センサーは、色ではなく、跳ね返ってくる光の強弱でラインを判定しているため、光射状況に弱い。
実際、日光のあたる場所に連れて行くと、白い部分で最大89を観測した。
直射日光、蛍光灯や本体自身の影等、あらゆる条件によって読み取り値が変化するため、
光センサーの扱いは注意したほうが良いということを実感した体験だった。
発表間近でこれを経験すると意外と焦るので気をつける。
以上で、ライントレスは完成。スタートからゴールまでの道は確保できた。
**ボールを持ち運んで、シュートする。[#carrying_and_shooting]
***まずは、ボールをどうやって持つか。[#how_to_chatch_the_ball]
正直、これを考えるのが一番楽しかったかもしれない。
腕で捕まえる、上の方に持ちあげる等、いろいろとアイディアが出たが、最終的に選んだ方法は、縦方向に挟み込む案。
#ref(2012b/Member/ddnp/Mission1/arm_1.jpg, 100%, ピンポン玉取得アーム)
#ref(2012b/Member/ddnp/Mission1/arm_catch.jpg, 100%, ピンポン玉のとり方)
ワームが回転し、光センサーとワームを回すシャフト自身がピンポン玉のつっかえ棒となり、挟みこむことが出来る。
しかも、ワームを逆回転させればシュートも出来るという優れもの。
つかむ時、写真手前側にボールが逃げないよう、5つ穴のブロックでピンポン玉が逃げないようにしてある。
光センサーの裏を使って挟み込むことは、省スペースにも繋がり、この後コースを走らせてみても障害物に当たることはなかった。
本当は、ワームの上にただボールを載せておくだけで、シュートする時はループシュートという事をやりたかったけど、
ボールを抱え込んでワーム上に設置するのが難しかったのでこれだけにしておいた。何故やりたかったというと、ただの好み。
***ボールを持って、シュートさせる[#how_to_shoot]
パーツは出来たので、後はプログラム。
しかし実際は、モーターを前後に動かすだけでキャッチもシュートも出来るため、プログラム自体は非常にシンプルなものに。
つかむプログラム
OnRev(OUT_B); // 腕内側回転
Wait(PICKUPTIME); // 少しの間回転
Off(OUT_B); // 停止
シュートするプログラム
SetPower(OUT_B, 5); // パワー強め
OnFwd(OUT_B); // シュート
Wait(100);
Off(OUT_ABC); // シュート停止とともに全動作を停止
パワーを強めにしてみたはいいものの、あまり実感は出来なかった。
以上で、ボールキャッチからシュートまでの処理は完成。
*まとめ[#conclusion]
**今回使った機体[#used_robot]
#ref(2012b/Member/ddnp/Mission1/robot.jpg, 100%, 使ったロボットの全影)
そういえば名前とか付けてない…。
全体的に強度が足りない時期があったので、細かいブロックなどでガチガチに固めてある。
その結果、ロゴでよくある「あのパーツが足りない」現象が起こったのでつぎはぎだらけ。
薄い2*2ブロックとか1*1ブロックとか高さ調整ですごい使った。L字型も導入する始末。
そしてその小型ブロックのせいで、分解時に爪を痛めたのもいい想い出。
**実際に使用したプログラム[#used_program_code]
//------------------------------------------------//
//
// Title : Carrying-ball
//
// Use : ボールを拾い上げた後、
// 進行方向に対して左側の境界線を
// ライントレス
// 障害を乗り越えた後、
// ボールをゴールにシュート
//
//------------------------------------------------//
/*
############################
定数定義
############################
*/
// トレス判定系
#define DEEPBLACK 40 // 濃い黒のしきい値
#define BLACK 45 // 黒のしきい値
#define WHITE 50 // 白のしきい値
#define DEEPWHITE 53 // 濃い白のしきい値
#define CHANGE_BLACK 1150 // 境界→黒トレス切り替え時間
#define CHANGE_OUTLINE 400 // 黒→境界トレス切り替え時間
#define CORNER_WAIT 1000 // コーナー判定を無視する時間
// 境界トレス用
#define CORNER_TIME 38 // 直角判定する時間
// 黒トレス用
#define SEESIGHT 120 // 黒トレス時のライン捜索時間
// ピンポン玉処理系
#define PICKUPTIME 30 // ピンポン玉取得のための腕回転時間
#define SHOOT 600 // ピンポン玉発射時の腕回転時間
// 音符
#define C4 523
#define E4 659
#define F4 698
#define G4 784
#define A5 880
#define B5 988
#define C5 1046
#define D5 1175
#define E5 1318
#define F5 1397
#define G5 1568
#define C6 2093
/*
#############################
# マクロ
#############################
*/
#define RotateRight OnFwd(OUT_A); OnRev(OUT_C); // 時計回り
#define RotateLeft OnFwd(OUT_C); OnRev(OUT_A); // 反時計回り
/*
#############################
# 右旋回ルーチン
#############################
*/
sub rotateright()
{
OnFwd(OUT_A);
OnRev(OUT_C);
}
/*
#############################
# 左旋回ルーチン
#############################
*/
sub rotateleft()
{
OnFwd(OUT_C);
OnRev(OUT_A);
}
/*
###############################
# 歓喜の舞
###############################
*/
sub kirby_clear ()
{
// 超舞う
RotateLeft;
PlayTone(F4, 10); Wait(13);
PlayTone(G4, 10); Wait(13);
PlayTone(A5, 10); Wait(13); RotateRight;
PlayTone(B5, 10); Wait(13);
PlayTone(A5, 10); Wait(13);
PlayTone(B5, 10); Wait(13); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(C5, 10); Wait(25);
PlayTone(E4, 10); Wait(13);
PlayTone(E4, 10);
Off(OUT_B);Wait(13); Wait(25);
RotateRight;
PlayTone(F4, 10); Wait(13);
PlayTone(G4, 10); Wait(13);
PlayTone(A5, 10); Wait(13); RotateLeft;
PlayTone(B5, 10); Wait(13);
PlayTone(A5, 10); Wait(13);
PlayTone(B5, 10); Wait(13); Off(OUT_AC); OnRev(OUT_B);
PlayTone(C5, 10); Wait(25);
PlayTone(E4, 10); Wait(13);
PlayTone(C4, 10);
Off(OUT_B); Wait(13); Wait(25);
RotateLeft;
PlayTone(F4, 10); Wait(13); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(G4, 10); Wait(13); Off(OUT_B); RotateRight;
PlayTone(A5, 10); Wait(13); Off(OUT_AC); OnRev(OUT_B);
PlayTone(B5, 10); Wait(13); Off(OUT_B); RotateRight;
PlayTone(A5, 10); Wait(13); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(B5, 10); Wait(13); Off(OUT_B); RotateLeft;
PlayTone(C5, 22); Wait(25); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(G4, 10); Wait(13); RotateRight;
PlayTone(E4, 22); Wait(25);
PlayTone(G5, 10); Wait(13);
PlayTone(F5, 22); Wait(25);
PlayTone(E5, 10); Wait(13);
PlayTone(D5, 22); Wait(25);
PlayTone(E5, 10); Wait(13); Off(OUT_AC);
PlayTone(C5, 35); Wait(38);Off(OUT_B);
PlayTone(C6, 10); Wait(13);
}
/*
################################
#
# メイン関数
#
################################
*/
task main()
{
//-------------------------//
// 初期化設定
//-------------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT); // 光センサーを設定
SetPower(OUT_ABC, 2); // モーター動力を少し低く指定
ClearTimer(0); // プログラムを走らせだしてからの時間を測定
//-------------------------//
// 変数宣言
//-------------------------//
int clockwise = 3; // 黒トレス時の回転方向を記憶 奇数は時計回り
int rad_increase = 1; // 黒トレス時の反転回数 倍角変数にもなる
int corner_times = 0; // コーナーを曲がった回数
int backturn = 0; // シュート時の角度補正用
int corner_wait = -800; // コーナーを曲がった時の時間
//-------------------------//
// 玉取得
//-------------------------//
OnRev(OUT_B); // 腕内側回転
Wait(PICKUPTIME); // 少しの間回転
Off(OUT_B); // 停止
//-------------------------//
// ライントレス
//-------------------------//
// コーナーを3回以上回るまでライントレス処理を実行
while(corner_times < 3)
{
// 指定時間外かつライン左側で輪郭線ライントレス
if((FastTimer(0) <= CHANGE_BLACK) || (FastTimer(0) >= CHANGE_BLACK+CHANGE_OUTLINE && clockwise % 2))
{
//---------------------------//
// 輪郭線ライントレス
//---------------------------//
// 白側→黒側
if(SENSOR_2 > DEEPWHITE)
{
// 薄くなるまで回転
while(SENSOR_2 > WHITE)
{
rotateright(); // 右小回転
}
}
// 黒側→白側
else if(SENSOR_2 < DEEPBLACK)
{
ClearTimer(2); // 旋回時間を測定
// 薄くなるまで回転
while(SENSOR_2 < BLACK)
{
rotateleft(); // 左小回転
}
// コーナーを判定且つ以前コーナーに来てからしばらく経過している場合
if(FastTimer(2) >= CORNER_TIME && FastTimer(0) >= corner_wait+CORNER_WAIT)
{
backturn = FastTimer(2)+5; // 曲がるのにかかった時間を記憶
corner_times++; // コーナーの数を数える
PlayTone(440, 30); // コーナーの数を数えたことを知らせる
corner_wait = FastTimer(0); // コーナーを曲がったときの時間を記憶
}
}
else
{
OnFwd(OUT_AC); // 常に前進
}
}
// 指定時間内かつライン右側で黒ライントレス
else if(FastTimer(0) > CHANGE_BLACK || FastTimer(0) < CHANGE_BLACK+CHANGE_OUTLINE && !(clockwise % 2))
{
//----------------------------//
// 黒ライントレス
//----------------------------//
// 完全に白い場所を感知した時、方向修正
if(SENSOR_2 >= DEEPWHITE)
{
rad_increase = 1; // 倍角変数を初期化
// 黒ラインに入るまで処理。黒ラインが見当たらないとき一度処理を抜ける
while(SENSOR_2 >= DEEPWHITE && rad_increase <= 3)
{
ClearTimer(1); // タイマーリセット
// 黒ラインに入るまで、且つ旋回時間が過ぎるまで旋回
while((SENSOR_2 >= BLACK) && (FastTimer(1) <= SEESIGHT * rad_increase))
{
// 時計回り
if(clockwise % 2)
{
rotateright();
}
// 反時計回り
else if(!(clockwise % 2))
{
rotateleft();
}
}
// 白い場所から脱出できなかった場合
if(SENSOR_2 >= BLACK)
{
clockwise ++; // 回転方向を変更
rad_increase += 1; // 回転時間を増加
}
}
}
else
{
OnFwd(OUT_AC); // 常に前進
}
}
}
// 止まる
Off(OUT_AC);
//-----------------------------//
// シュート処理
//-----------------------------//
// 曲がった分バック
rotateright();
Wait(backturn);
Off(OUT_AC);
// 腕を回転
SetPower(OUT_B, 5);
OnFwd(OUT_B);
Wait(100);
// 動作を停止
Off(OUT_ABC);
// 準備体操
SetPower(OUT_B, 2);
// 歓喜の舞
Wait(300);
kirby_clear();
}
上で記述しなかった改善点がいくつか存在する。
-境界→黒トレスまでの秒数を調整。1150が調子良かった様子。
-コーナー判定の秒数を短くした
--可能な限り確実にコーナーを判定できるよう、40〜35を試した結果、37、38に落ち着く
-右回転、左回転をサブルーチン化
--授業資料に、サブルーチンを使用すると、何回か出てくる処理が効率的に実装される的なことが書いてあったため
-黒トレス時の回転方向を指定する変数の初期値を変更
--そういえば、int型では小数切り捨てだったことを思い出した。1/2=0.5だが、0.5=0となっては処理に以上が出る
-全体的にコメントの書き方を一新した
--自分にとって、毎度のことながらコメントで汚してる感が否めない気がする。
-変数や定数の名前が一部変更
--前述のUSBメモリ紛失事件や、プログラムの見直しなど様々な理由。分かりやすい変数名にしたつもり
-歓喜の舞を実装
--シュート後に音楽を鳴らしながら踊り狂う命令を追加。趣味で作ったもの。
--やってみる人はご自由に。舞は再現できなくても音はまんまのハズ。それ以前に何やったか関数名から予測つくねうん
**感想 [#k14bc134]
楽しみながらスムーズに完成させられたように思う。
プログラミングについては、趣味で多少やっているので、困ったことはあまりなかった。しいて言うなら数学でもよくやらかす>と<の間違い。
個人的な狙いとして、如何にして綺麗にコードを書くかというテーマを決めて取り組んだため、
そこができていれば今回の課題はソフトウェア的にはOKだと感じている。
ハードウェア面は、不慣れなLEGOに悪戦苦闘した割りには、よくやれたように感じる。
それ以前に、LEGOの楽しさに触れられたことの方に充実感を感じているように思う。
どうやって目的の動作をするパーツを作るかという醍醐味に引きこまれた模様。
次は、これ以上にアイディアのつまったロボットを組み立てられるように頑張りたい。
全体的に見ればまだまだ突き詰められる場所があるように感じるが、今回はこれにて満足。:)
***余談 [#g417b242]
歓喜の舞に、Pythonで音階の周波数を計算してA2〜A7までつらつら出力させようとしたら、命令文に「;」書いて怒られる。
忘れすぎて嫌になるはずの「;」が、Pythonだと逆に無くて落ち着かないこれ正にジレンマ。
だいぶNQCに慣れてきたようなので、これから後の課題もがんばろう。
分解している最中に、こんな表情を見せた。
名前はスマイリーとかでいいかもしれない。:)
#ref(2012b/Member/ddnp/Mission1/smile.jpg, 100%, スマイル)
終了行:
#contents
*ライントレス[#linetrace]
**まずはラインに沿って動かしてみる[#tracing]
とりあえず、ラインに沿って動くことが出来なければボールを運ぶも何も無いので、ラインをトレースするプログラムを描いてみる。
そこで、練習用として、次のようなコースを用意した。
この時使った機体は、マニュアルに設計図の乗っているデフォ子(量産型)。
&ref(2012b/Member/ddnp/Mission1/practice_course.jpg, 100%, 練習用コース);&ref(2012b/Member/ddnp/Mission1/default_bot.JPG, 100%, 練習用ロボ、デフォ子);
***黒ライントレス[#black_trace]
光センサーは、光の反射量を読み取るため、黒い部分の値は低く、白い部分の値は高い数値が出る。値は0〜100。
そこで、光センサーが白い部分を読み取ったら、つまり光センサーの値がある一定値を超えたら、旋回するようにしてみる。
コースの黒いところから、白いところまでの値を事前に読み取り、以下のプログラムを作成。
黒いところと白いところで、値の違いは大体38〜51。よって、40〜50あたりで判定を行うよう、しきい値を設定する。
//---------------------//
// 定数定義
//---------------------//
#define WHITE 50 // 白い部分のしきい値
task main()
{
//-----------------------//
// 初期化
//-----------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT);
while(true)
{
OnFwd(OUT_AC); // 常に前進
// 白い場所を読み取っている間旋回
while(SENSOR_2 >= WHITE)
{
// 反時計回りに旋回
OnFwd(OUT_A);
OnRev(OUT_C);
}
}
}
ちなみに、上記のプログラムでは、「黒い部分に入るまで旋回する」というような処理を行なっているが、
「白い部分に入ったら、黒が濃い部分を読み取るまで旋回する」とした方が、ライン中心に沿ってトレスする様になり、スムーズになるようだ。
また、白いラインに入った際、少し時間を空けてから黒い部分を探しだすようにすると、直線では少々スピードが上がる。
ただし、開けすぎて白い部分で旋回し続けるようにならないよう注意。特に急カーブ等。
つまり、定数定義に以下の二文を追加し、
#define BLACK 40 // 黒い部分のしきい値
#define STRAIGHT 10 // 白い部分読み取りの際の遊び
「白い部分を読み取っている間旋回」するプログラムを、以下の様に書き換える
// 白い部分を読み取った時
if(SENSOR_2 >= WHITE)
{
Wait(STRAIGHT); // 少しだけ直進
// 黒いラインの中心付近に来るまで旋回
while(SENSOR_2 <= BLACK)
{
// 反時計回りに旋回
OnFwd(OUT_A);
OnRev(OUT_C);
}
}
これで、多少スムーズにライントレスするようになるだろう。
ちなみに、BLACKの値をあまり低く設定すると、中心からずれやすい。黒い場所を読み取ってから次の動作に移るまで、ごく微小の遅延があるようだ。
よって、BLACKの値は多少高めでも問題無いだろう。
ところで、このプログラムには大きな問題がある。左カーブ時はいいが、右カーブ時に半回転し、逆走する事態が発生する。
そこで、ある程度探しても黒線が見つからなかった場合、逆回りして黒線を探すように改造してみる。
そこで書いたのが、以下のプログラム。
//---------------------//
// 変数定義
//---------------------//
#define BLACK 42 // 黒のしきい値
#define WHITE 50 // 白のしきい値
#define RADIO_SIGHT 60 // 黒い部分を探す際の視野
#define STRAIGHT 10 // 黒い部分を探す処理に移る前の遊び
//--------------------------//
//
// その場で旋回する関数
//
// cw 旋回方向を指定する変数 奇数なら右回り(clock wise)
//-------------------------//
void stay_turn (int cw)
{
// 時計回り
if(cw % 2)
{
OnFwd(OUT_A);
OnRev(OUT_C);
}
// 反時計回り
else
{
OnFwd(OUT_C);
OnRev(OUT_A);
}
}
//-------------------------//
//
// メイン関数
//
//-------------------------//
task main ()
{
//----------------------//
// 初期化
//----------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT); // 光センサーを設定
//----------------------//
// 変数定義
//----------------------//
int rotate = 1; // 回転方向を指定 奇数は右回り
int times = 0; // 旋回回数を記憶 かつ視野を増加させる
int switch = 1; // 処理を抜けるための変数
int timer = 0; // タイマーの時刻を記憶
//-----------------------//
// 黒ライントレス
//-----------------------//
while(switch)
{
// 黒い間は直進
while(SENSOR_2 <= WHITE)
{
OnFwd(OUT_AC);
}
Wait(STRAIGHT);
// 白い部分に来た時
if(SENSOR_2 > WHITE)
{
times = 0; // 回転回数初期化
// 黒い部分を探しながら、カーブの方向を決定する
while( (SENSOR_2 > BLACK) && (times <= 3) )
{
ClearTimer(0); // 旋回時間測定用のタイマーをセット(タイマー0)
// 視野の分だけ黒い場所を探す
while ( (SENSOR_2 > BLACK) && (FastTimer(0) <= RADIO_SIGHT * times) )
{
stay_turn(rotate); // 旋回
}
// 黒い部分が見つからなかった場合
if(SENSOR_2 > BLACK)
{
rotate ++; // 逆回転を指定
times ++; // 回転回数と視野を増加
}
}
// 黒い線が見つからなかった時(一回転以上した時)
if(times > 3)
{
// 完全に白い場所に出たということで、処理を中止。あきらめろ。お前はよくやった。もう休むんだ。
switch = 0;
}
}
}
// 停止
Off(OUT_AC);
}
黒線を探す際の視野というのは、どの程度回転させるかを示している。
急カーブでコースアウトした際に、回転する方向は合っていても、視野が狭ければ逆側を探索しだすため、効率が落ちてしまう。
最悪、逆探索をした際に近くにある別の線を読みとる場合も考えられる。
なので、視野の値は、急カーブでも一回目の旋回で曲がりきれるくらいの値にするのが好ましい。
このプログラムでは、大体120度くらい旋回するようにしてある。
何はともあれ、これで黒線をトレースすることができた。めでたしめでたし。
と、思うじゃん?
先生「白と黒の境界をトレスする方が効率が良いです」
( ゚д゚) ...
( ゚д゚) ...&ruby(どういうことなの…?){What do you mean...?};
***境界ライントレス[#border_trace]
黒ライントレスの方法では、ラインから外れた場合、ただがむしゃらに回転してラインを探すほか無かった。
要するに、カーブが右カーブなのか左カーブなのかを判別する能力がないわけだ。
しかし、黒線と白い部分の間を進む、つまり境界線をトレースするようにすると、面白いくらいにロボットが賢くなる。
#ref(2012b/Member/ddnp/Mission1/border_trace.gif, 100%, 境界トレスの仕組み)
例えば、ロボットに「特に白い部分を感知したら右に、特に黒い部分を感知したら左に回って、境界線を探すようにする」
とプログラムしたとすると、ロボットは黒いラインの左側をトレースするようになる。
これによって最も嬉しいことは、ロボット自身で「境界線のどちら側にずれているのか」を判別できるようになることだ。
これは、ロボットが右カーブと左カーブを判別することが出来るようになることにもつながり、非常にスマートな方法だと言える。
早速、プログラムを書いて動かしてみよう。
//----------------------------//
//
// 定数定義
//
//----------------------------//
#define DEEPBLACK 40 // 濃い黒のしきい値
#define BLACK 43 // 黒のしきい値
#define WHITE 47 // 白のしきい値
#define DEEPWHITE 50 // 濃い白のしきい値
//----------------------------//
//
// メイン処理
//
//----------------------------//
task main()
{
//----------------------------//
// 初期化
//----------------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT);
//----------------------------//
// 境界ライントレス
//----------------------------//
while(true)
{
// 白側に出た時
if(SENSOR_2 > DEEPWHITE)
{
// 境界に近づくまで回転
while(SENSOR_2 <= WHITE)
{
// 時計回り
OnFwd(OUT_A);
OnRev(OUT_C);
}
}
// 黒側に出た時
else if(SENSOR_2 < DEEPBLACK)
{
// 境界に近づくまで回転
while(SENSOR_2 >= BLACK)
{
// 反時計回り
OnFwd(OUT_C);
OnRev(OUT_A);
}
}
// 境界線上は直進
else
{
OnFwd(OUT_AC);
}
}
}
黒トレスと比べて、すっきりしたプログラムになった。
境界ライントレスについては、これで問題なく動作したので、しばらくこのプログラムを使うことにしよう。
*実際のコースで動作させる [#c883296f]
**スタートからゴールまでのライントレス[#from_start_to_goal]
***課題の題材となったコースと注意するポイント[#checkpoint]
下図を参照。
このコースの端からスタートして、もう片方の端にあるゴールにボールをシュートすることとなる。
#ref(2012b/Member/ddnp/Mission1/course_1.jpg, 100%, 課題コース)
S字カーブ、直角カーブ、急カーブ、交差点と、入り組んだコースとなっている上、
S字カーブの真ん中に障害物を設置し、それに触れないように進まなければならない。
まずは、シュートの前段階として、スタート地点からゴール地点まで行き着くようなロボットを完成させることとする。
そのためにクリアしなければいけない課題として、以下のことが想定される。
-S字の障害物を避けるため、本体を小さくする。
-急カーブを曲がれるよう工夫する。
-交差点と直角カーブの区別を明確にする。
-ゴールを判別し、ゴールに標準を合わせる。
とりあえず、次の項では本体を小さくすることに焦点を当ててみる。
***その前にデフォ子の追悼式[#see_you_default_bot]
この日、授業の初めの方から付き合って下さったデフォ子様の解体の日となりました。
お集まりの皆様方には、デフォ子様との想い出の日々が以下都合により割愛
***本体を小さくする[#make_the_robot_small]
とりあえず、実際に本体を作らず、紙面上で実験。
ブロックやシャフトを使い、縦長、横長など様々な大きさの枠を作ってはラインにそって手で動かしてみると、以下の事がわかった。
-デフォ子に使われているタイヤは太すぎて、使うには難しい。
-左右のモーターが多少大きいので、付ける位置を近づけるなどして、横幅を小さくした方がいい。
-旋回する時中心となる点から、機体が前や後ろに長すぎると、内輪差などの影響で障害物にぶつかる。
まぁぶっちゃけ全体的にコンパクトになってればなってるほど後先の事を考えなくていいと感じたので、可能な限り小さくすることを考えてみる。
結果的に、以下の三点を実施。
-二輪に変更
--実際にはバランスを取るために三輪。ただ三番目のタイヤは、どちらかというと支え棒といったほうが正しい。
-モーター同士をできる限りくっつけてみた。
--しかし不安定性が増したので、上下に固定用のブロックを足して、強化している。
--同時に、車軸を固定するパーツ&ref(2012b/Member/ddnp/Mission1/parts.gif, 100%, シャフト固定パーツ);が使えなくなった。代用として車軸を左右から押さえつけてはめ込むようにしたが、問題ない様子
-RCX本体が縦長なので、デフォ子のように寝かせるのでなく、立ててみることにした
--中心でなく後ろ側に寄せて立てることで、前側に光センサーやボールを掴むパーツを配置できるくらいの余裕を作る。
--嬉しいことに、寝かせるより立てたほうが、本体ががっちり固定されて扱いやすいことが判明。嬉しい誤算。
--センサーが離れてしまったのでコードを延長したのだが、延長部分がRCXの裏側に張り付くのでまとめやすい。これまた嬉しい誤算。
--ただし、これによって重心が後ろに傾いた。何も影響がなければいいが…。
--欠点としては、スイッチ類が押しにくかったり、View値が読みづらかったりする。
これを考慮して出来た機体がこれ。
#ref(2012b/Member/ddnp/Mission1/half_of_robot.jpg, 100%, 半完成ロボット)
さらに、光センサーの位置についても考慮してある。
出来るだけ正確に読み取れるように、地面に近づけ、本体の中心に近い位置に設置してある。
このことにより、光センサーが本体の中心からより離れている場合と比べて、多少回転が早くても誤作動が少なくなる。
つまり、
#ref(2012b/Member/ddnp/Mission1/degree_difference.gif, 100%, 回転の軸からより近い場所にある光センサーの読み取り距離の差)
上図のように、光センサーの位置によって、回転した時間が同じでも、光センサーが移動する距離に差が出てくる。
これはつまり、回転の中心に光センサーを寄せるほど、ゆっくりと読み取りが出来るということである。
よって、多少早い動作をしても、光センサーの読み取りミスが生じることは無くなるはずである。
ただし、欠点もある。近くに寄せ過ぎると、光センサーの読み取りが一点に集中し過ぎて、
ラインがほぼ真下にあるのに読み込めないという事態が発生しうる。よって、調度良い距離を見つけることが必要となる。
今回の機体では、大体写真の位置で読み取るよう設定した。
&ref(2012b/Member/ddnp/Mission1/read_distance.jpg, 100%, 光センサーの地面からの近さ);&ref(2012b/Member/ddnp/Mission1/read_area.jpg, 100%, 光センサーの中心の軸からの距離);
これにて基盤は完成。
***ゴールを目指す[#aim_the_goal]
機体は出来た。ということで、テストも兼ねてラインをトレースしてみることにする。
試しに、先ほど作った境界トレスのプログラムを動作させてみたところ、トレース自体に問題はなかった。
唯一変更した点は、光センサーのしきい値くらいである。
それにしても、再利用できてよかった。
さて、ラインはトレースできても、ゴールまで行きつけなければ仕方がない。
この時点で、自分がクリアすべき課題は
+コース途中の交差点をどう通過するか
+ゴールを判定するにはどうするか
の2つであった。
#ref(2012b/Member/ddnp/Mission1/course_2.jpg, 100%, 通過すべき交差点の位置)
まずは、途中の通過すべき交差点の処理を考える。
普通なら、交差点であることを判別して、黒い線を跨ぐような処理をすればいいのだが、
ちょっとした思いつきで、交差点の判別をしないで超えてみることにした。
使用するのは、前に作った黒トレスのプログラム。
交差点に突入する前に、黒ライントレスを開始し、
通り過ぎた頃に黒ライントレスから境界ライントレスに移行するよう、タイマー機能を使用して実装してみる。
ただし、黒ライントレスから境界ライントレスに移行する時に、ライン左側にいなければ逆走を開始してしまうため、
本来は黒ライントレス時の回転方向を決定する変数を利用して、ラインどちら側にいるかを判定する。
//--------------------------//
//
// 定数定義
//
//--------------------------//
#define DEEPBLACK 40 // 濃い黒のしきい値
#define BLACK 45 // 黒のしきい値
#define WHITE 50 // 白のしきい値
#define DEEPWHITE 53 // 濃い白のしきい値
#define CHANGE_BLACK 1100 // 境界→黒トレス切り替え時間
#define CHANGE_OUTLINE 400 // 黒→境界トレス切り替え時間
#define SEESIGHT 120 // 黒トレス時のライン捜索時間
//---------------------------//
//
// メイン処理
//
//---------------------------//
task main()
{
//-------------------------//
// 初期化
//-------------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT); // 光センサーを設定
ClearTimer(0); // 境界・黒ライントレスの入れ替わり時間を測定
//----------------------//
// 変数定義
//----------------------//
int rotate = 1; // 回転方向を指定 奇数は右回り
int times = 0; // 旋回回数を記憶 かつ視野を増加させる
//-------------------------//
// ライントレス
//-------------------------//
while(true)
{
// 指定時間外かつライン左側で輪郭線ライントレス
if((FastTimer(0) <= CHANGE_BLACK) || (FastTimer(0) >= CHANGE_BLACK+CHANGE_OUTLINE && clockwise % 2))
{
//---------------------------//
// 輪郭線ライントレス
//---------------------------//
・・・
・・
・
}
// 指定時間内かつライン右側で黒ライントレス
else if(FastTimer(0) > CHANGE_BLACK || FastTimer(0) < CHANGE_BLACK+CHANGE_OUTLINE && !(clockwise % 2))
{
//----------------------------//
// 黒ライントレス
//----------------------------//
・・・
・・
・
}
}
}
ライントレス処理は上記とほぼ同じなので省略。
途中、プログラムの入ったUSBメモリをなくして作りなおしたので、変数名が変わっているくらい。
USBメモリの管理には気をつけましょう。
これにて、目論見どおり交差点を渡ることが出来た。
#ref(2012b/Member/ddnp/Mission1/course_3.jpg, 100%, シュートの位置)
次は、ゴール、つまりシュートする位置を判別出来るようにしなければならない。
といっても、シュートする位置を示す特徴が、交差点くらいしかないので、結局交差点を判別するプログラムを考えることに。
そこで、境界ライントレス時に、旋回する時間が長い場合は、直角カーブまたは交差点と判別するプログラムを書くことにした。
#ref(2012b/Member/ddnp/Mission1/course_4.jpg, 100%, コーナーの数)
地形的な特徴と、黒ライントレスの処理があることから、黒側→白側に向かって旋回している時にこの処理を行えば、
ゴールまで計3つのコーナーを通過することになるので、3つ目の直角でシュートを行えるようにプログラムを改造してみる。
定数と変数を付け加え、
#define CORNER_TIME 40 // コーナーと判別する時間
int backturn = 0; // コーナーを曲がるのにかかった時間を記憶
int corner_times = 0; // 曲がったコーナーの数
境界ライントレスの、反時計回り旋回時のプログラムをこのように書き換えた。
// 黒側→白側
else if(SENSOR_2 < DEEPBLACK)
{
ClearTimer(2); // コーナーを曲がっている時間を測定
// 白側へ旋回
while(SENSOR_2 < BLACK)
{
// 左旋回
OnFwd(OUT_C);
OnRev(OUT_A);
}
// 曲がるのに時間がかかった場合、直角だと判断
if(FastTimer(2) >= CORNER_TIME)
{
backturn = FastTimer(2)+5; // 曲がるのにかかった時間を記憶
corner_times++; // コーナーの数を数える
PlayTone(440, 30); // コーナーの数を数えたことを知らせる
}
}
これで直角を判別してくれるようになった。
直角を判定した時には、機体はコーナーを曲がりきってしまっているので、
シュート処理するときにはゴールの方向へ機体を向ける必要がある。
そこで、曲がるまでにかかった時間を記憶して、シュート処理の時に、その時間だけ逆旋回させればよい。
しかし、処理の遅延なのか、実際には多少ズレが生じるので、そのズレの解消のために、タイマーの時間にさらに5を足している。
さらに、シュート処理に移るときは、ライントレスをする必要はないので、
コーナーを3回以上曲がった場合に処理を抜けるように、ループ処理を変更してみる。
つまり、
while(true)
の一文を、
while(corner_times < 3)
に変更する。
ここで、再び問題が発生。
コーナーで、「2回以上コーナーを曲がったことにしてしまう処理が時たま発生する。」というもの。これではゴール前でシュート処理に移行してしまう。
どうやら、トレスの方法に、「色が濃い場所までいってから、薄いところまで軌道を修正する方法」を用いているため、
コーナーに入る角度によって、コーナーのトレスに2,3回軌道修正をしなければいけない状況があるようだ。
コーナーと判定する時間を45くらいまで増やしてみたところ、今度はコーナー自体を判別しなくなる場合が出てきてしまったので、
定数でどうにかするには心もとない。
ということで、一度コーナーを判定したら、しばらくコーナー判定を無視するようにプログラムを改善してみた。
定数と変数をさらに追加。
#define CORNER_WAIT 1000 // コーナー判定を無視する時間
int corner_wait = -800; // コーナーを曲がった時の時間
コーナー判定プログラムは次のようにした。
// コーナーを判定且つ以前コーナーに来てからしばらく経過している場合
if(FastTimer(2) >= CORNER_TIME && FastTimer(0) >= corner_wait+CORNER_WAIT)
{
backturn = FastTimer(2)+5; // 曲がるのにかかった時間を記憶
corner_times++; // コーナーの数を数える
PlayTone(440, 30); // コーナーの数を数えたことを知らせる
corner_wait = FastTimer(0); // コーナーを曲がったときの時間を記憶
}
コーナーを曲がるときの条件式に合わせて、コーナーを曲がった時の時間の初期値を-800にしている。
コレがないと一個目のコーナーを判別できない可能性が高い。
改善前もコーナー以外で誤動作することはなかったが、こうすることでさらに確実に、コーナーのみを選び出すことが出来るだろう。
ところがどっこい今度も謎の問題が発生
図の部分だけ、曲がりきることが出来ない問題が発生した。
#ref(2012b/Member/ddnp/Mission1/course_5.jpg, 100%, 謎の現象 発生位置);
定数値をいじってみるも、問題が解決しないので、まさかと思い光センサーの値を確認。
この場所だけ、白の部分が50という値を観測。他が55なので、明らかにおかしい。
探ってみたところ、どうやら自身の影が光センサーを邪魔していたらしい。
光センサーは、色ではなく、跳ね返ってくる光の強弱でラインを判定しているため、光射状況に弱い。
実際、日光のあたる場所に連れて行くと、白い部分で最大89を観測した。
直射日光、蛍光灯や本体自身の影等、あらゆる条件によって読み取り値が変化するため、
光センサーの扱いは注意したほうが良いということを実感した体験だった。
発表間近でこれを経験すると意外と焦るので気をつける。
以上で、ライントレスは完成。スタートからゴールまでの道は確保できた。
**ボールを持ち運んで、シュートする。[#carrying_and_shooting]
***まずは、ボールをどうやって持つか。[#how_to_chatch_the_ball]
正直、これを考えるのが一番楽しかったかもしれない。
腕で捕まえる、上の方に持ちあげる等、いろいろとアイディアが出たが、最終的に選んだ方法は、縦方向に挟み込む案。
#ref(2012b/Member/ddnp/Mission1/arm_1.jpg, 100%, ピンポン玉取得アーム)
#ref(2012b/Member/ddnp/Mission1/arm_catch.jpg, 100%, ピンポン玉のとり方)
ワームが回転し、光センサーとワームを回すシャフト自身がピンポン玉のつっかえ棒となり、挟みこむことが出来る。
しかも、ワームを逆回転させればシュートも出来るという優れもの。
つかむ時、写真手前側にボールが逃げないよう、5つ穴のブロックでピンポン玉が逃げないようにしてある。
光センサーの裏を使って挟み込むことは、省スペースにも繋がり、この後コースを走らせてみても障害物に当たることはなかった。
本当は、ワームの上にただボールを載せておくだけで、シュートする時はループシュートという事をやりたかったけど、
ボールを抱え込んでワーム上に設置するのが難しかったのでこれだけにしておいた。何故やりたかったというと、ただの好み。
***ボールを持って、シュートさせる[#how_to_shoot]
パーツは出来たので、後はプログラム。
しかし実際は、モーターを前後に動かすだけでキャッチもシュートも出来るため、プログラム自体は非常にシンプルなものに。
つかむプログラム
OnRev(OUT_B); // 腕内側回転
Wait(PICKUPTIME); // 少しの間回転
Off(OUT_B); // 停止
シュートするプログラム
SetPower(OUT_B, 5); // パワー強め
OnFwd(OUT_B); // シュート
Wait(100);
Off(OUT_ABC); // シュート停止とともに全動作を停止
パワーを強めにしてみたはいいものの、あまり実感は出来なかった。
以上で、ボールキャッチからシュートまでの処理は完成。
*まとめ[#conclusion]
**今回使った機体[#used_robot]
#ref(2012b/Member/ddnp/Mission1/robot.jpg, 100%, 使ったロボットの全影)
そういえば名前とか付けてない…。
全体的に強度が足りない時期があったので、細かいブロックなどでガチガチに固めてある。
その結果、ロゴでよくある「あのパーツが足りない」現象が起こったのでつぎはぎだらけ。
薄い2*2ブロックとか1*1ブロックとか高さ調整ですごい使った。L字型も導入する始末。
そしてその小型ブロックのせいで、分解時に爪を痛めたのもいい想い出。
**実際に使用したプログラム[#used_program_code]
//------------------------------------------------//
//
// Title : Carrying-ball
//
// Use : ボールを拾い上げた後、
// 進行方向に対して左側の境界線を
// ライントレス
// 障害を乗り越えた後、
// ボールをゴールにシュート
//
//------------------------------------------------//
/*
############################
定数定義
############################
*/
// トレス判定系
#define DEEPBLACK 40 // 濃い黒のしきい値
#define BLACK 45 // 黒のしきい値
#define WHITE 50 // 白のしきい値
#define DEEPWHITE 53 // 濃い白のしきい値
#define CHANGE_BLACK 1150 // 境界→黒トレス切り替え時間
#define CHANGE_OUTLINE 400 // 黒→境界トレス切り替え時間
#define CORNER_WAIT 1000 // コーナー判定を無視する時間
// 境界トレス用
#define CORNER_TIME 38 // 直角判定する時間
// 黒トレス用
#define SEESIGHT 120 // 黒トレス時のライン捜索時間
// ピンポン玉処理系
#define PICKUPTIME 30 // ピンポン玉取得のための腕回転時間
#define SHOOT 600 // ピンポン玉発射時の腕回転時間
// 音符
#define C4 523
#define E4 659
#define F4 698
#define G4 784
#define A5 880
#define B5 988
#define C5 1046
#define D5 1175
#define E5 1318
#define F5 1397
#define G5 1568
#define C6 2093
/*
#############################
# マクロ
#############################
*/
#define RotateRight OnFwd(OUT_A); OnRev(OUT_C); // 時計回り
#define RotateLeft OnFwd(OUT_C); OnRev(OUT_A); // 反時計回り
/*
#############################
# 右旋回ルーチン
#############################
*/
sub rotateright()
{
OnFwd(OUT_A);
OnRev(OUT_C);
}
/*
#############################
# 左旋回ルーチン
#############################
*/
sub rotateleft()
{
OnFwd(OUT_C);
OnRev(OUT_A);
}
/*
###############################
# 歓喜の舞
###############################
*/
sub kirby_clear ()
{
// 超舞う
RotateLeft;
PlayTone(F4, 10); Wait(13);
PlayTone(G4, 10); Wait(13);
PlayTone(A5, 10); Wait(13); RotateRight;
PlayTone(B5, 10); Wait(13);
PlayTone(A5, 10); Wait(13);
PlayTone(B5, 10); Wait(13); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(C5, 10); Wait(25);
PlayTone(E4, 10); Wait(13);
PlayTone(E4, 10);
Off(OUT_B);Wait(13); Wait(25);
RotateRight;
PlayTone(F4, 10); Wait(13);
PlayTone(G4, 10); Wait(13);
PlayTone(A5, 10); Wait(13); RotateLeft;
PlayTone(B5, 10); Wait(13);
PlayTone(A5, 10); Wait(13);
PlayTone(B5, 10); Wait(13); Off(OUT_AC); OnRev(OUT_B);
PlayTone(C5, 10); Wait(25);
PlayTone(E4, 10); Wait(13);
PlayTone(C4, 10);
Off(OUT_B); Wait(13); Wait(25);
RotateLeft;
PlayTone(F4, 10); Wait(13); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(G4, 10); Wait(13); Off(OUT_B); RotateRight;
PlayTone(A5, 10); Wait(13); Off(OUT_AC); OnRev(OUT_B);
PlayTone(B5, 10); Wait(13); Off(OUT_B); RotateRight;
PlayTone(A5, 10); Wait(13); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(B5, 10); Wait(13); Off(OUT_B); RotateLeft;
PlayTone(C5, 22); Wait(25); Off(OUT_AC); OnFwd(OUT_B);
PlayTone(G4, 10); Wait(13); RotateRight;
PlayTone(E4, 22); Wait(25);
PlayTone(G5, 10); Wait(13);
PlayTone(F5, 22); Wait(25);
PlayTone(E5, 10); Wait(13);
PlayTone(D5, 22); Wait(25);
PlayTone(E5, 10); Wait(13); Off(OUT_AC);
PlayTone(C5, 35); Wait(38);Off(OUT_B);
PlayTone(C6, 10); Wait(13);
}
/*
################################
#
# メイン関数
#
################################
*/
task main()
{
//-------------------------//
// 初期化設定
//-------------------------//
SetSensor(SENSOR_2, SENSOR_LIGHT); // 光センサーを設定
SetPower(OUT_ABC, 2); // モーター動力を少し低く指定
ClearTimer(0); // プログラムを走らせだしてからの時間を測定
//-------------------------//
// 変数宣言
//-------------------------//
int clockwise = 3; // 黒トレス時の回転方向を記憶 奇数は時計回り
int rad_increase = 1; // 黒トレス時の反転回数 倍角変数にもなる
int corner_times = 0; // コーナーを曲がった回数
int backturn = 0; // シュート時の角度補正用
int corner_wait = -800; // コーナーを曲がった時の時間
//-------------------------//
// 玉取得
//-------------------------//
OnRev(OUT_B); // 腕内側回転
Wait(PICKUPTIME); // 少しの間回転
Off(OUT_B); // 停止
//-------------------------//
// ライントレス
//-------------------------//
// コーナーを3回以上回るまでライントレス処理を実行
while(corner_times < 3)
{
// 指定時間外かつライン左側で輪郭線ライントレス
if((FastTimer(0) <= CHANGE_BLACK) || (FastTimer(0) >= CHANGE_BLACK+CHANGE_OUTLINE && clockwise % 2))
{
//---------------------------//
// 輪郭線ライントレス
//---------------------------//
// 白側→黒側
if(SENSOR_2 > DEEPWHITE)
{
// 薄くなるまで回転
while(SENSOR_2 > WHITE)
{
rotateright(); // 右小回転
}
}
// 黒側→白側
else if(SENSOR_2 < DEEPBLACK)
{
ClearTimer(2); // 旋回時間を測定
// 薄くなるまで回転
while(SENSOR_2 < BLACK)
{
rotateleft(); // 左小回転
}
// コーナーを判定且つ以前コーナーに来てからしばらく経過している場合
if(FastTimer(2) >= CORNER_TIME && FastTimer(0) >= corner_wait+CORNER_WAIT)
{
backturn = FastTimer(2)+5; // 曲がるのにかかった時間を記憶
corner_times++; // コーナーの数を数える
PlayTone(440, 30); // コーナーの数を数えたことを知らせる
corner_wait = FastTimer(0); // コーナーを曲がったときの時間を記憶
}
}
else
{
OnFwd(OUT_AC); // 常に前進
}
}
// 指定時間内かつライン右側で黒ライントレス
else if(FastTimer(0) > CHANGE_BLACK || FastTimer(0) < CHANGE_BLACK+CHANGE_OUTLINE && !(clockwise % 2))
{
//----------------------------//
// 黒ライントレス
//----------------------------//
// 完全に白い場所を感知した時、方向修正
if(SENSOR_2 >= DEEPWHITE)
{
rad_increase = 1; // 倍角変数を初期化
// 黒ラインに入るまで処理。黒ラインが見当たらないとき一度処理を抜ける
while(SENSOR_2 >= DEEPWHITE && rad_increase <= 3)
{
ClearTimer(1); // タイマーリセット
// 黒ラインに入るまで、且つ旋回時間が過ぎるまで旋回
while((SENSOR_2 >= BLACK) && (FastTimer(1) <= SEESIGHT * rad_increase))
{
// 時計回り
if(clockwise % 2)
{
rotateright();
}
// 反時計回り
else if(!(clockwise % 2))
{
rotateleft();
}
}
// 白い場所から脱出できなかった場合
if(SENSOR_2 >= BLACK)
{
clockwise ++; // 回転方向を変更
rad_increase += 1; // 回転時間を増加
}
}
}
else
{
OnFwd(OUT_AC); // 常に前進
}
}
}
// 止まる
Off(OUT_AC);
//-----------------------------//
// シュート処理
//-----------------------------//
// 曲がった分バック
rotateright();
Wait(backturn);
Off(OUT_AC);
// 腕を回転
SetPower(OUT_B, 5);
OnFwd(OUT_B);
Wait(100);
// 動作を停止
Off(OUT_ABC);
// 準備体操
SetPower(OUT_B, 2);
// 歓喜の舞
Wait(300);
kirby_clear();
}
上で記述しなかった改善点がいくつか存在する。
-境界→黒トレスまでの秒数を調整。1150が調子良かった様子。
-コーナー判定の秒数を短くした
--可能な限り確実にコーナーを判定できるよう、40〜35を試した結果、37、38に落ち着く
-右回転、左回転をサブルーチン化
--授業資料に、サブルーチンを使用すると、何回か出てくる処理が効率的に実装される的なことが書いてあったため
-黒トレス時の回転方向を指定する変数の初期値を変更
--そういえば、int型では小数切り捨てだったことを思い出した。1/2=0.5だが、0.5=0となっては処理に以上が出る
-全体的にコメントの書き方を一新した
--自分にとって、毎度のことながらコメントで汚してる感が否めない気がする。
-変数や定数の名前が一部変更
--前述のUSBメモリ紛失事件や、プログラムの見直しなど様々な理由。分かりやすい変数名にしたつもり
-歓喜の舞を実装
--シュート後に音楽を鳴らしながら踊り狂う命令を追加。趣味で作ったもの。
--やってみる人はご自由に。舞は再現できなくても音はまんまのハズ。それ以前に何やったか関数名から予測つくねうん
**感想 [#k14bc134]
楽しみながらスムーズに完成させられたように思う。
プログラミングについては、趣味で多少やっているので、困ったことはあまりなかった。しいて言うなら数学でもよくやらかす>と<の間違い。
個人的な狙いとして、如何にして綺麗にコードを書くかというテーマを決めて取り組んだため、
そこができていれば今回の課題はソフトウェア的にはOKだと感じている。
ハードウェア面は、不慣れなLEGOに悪戦苦闘した割りには、よくやれたように感じる。
それ以前に、LEGOの楽しさに触れられたことの方に充実感を感じているように思う。
どうやって目的の動作をするパーツを作るかという醍醐味に引きこまれた模様。
次は、これ以上にアイディアのつまったロボットを組み立てられるように頑張りたい。
全体的に見ればまだまだ突き詰められる場所があるように感じるが、今回はこれにて満足。:)
***余談 [#g417b242]
歓喜の舞に、Pythonで音階の周波数を計算してA2〜A7までつらつら出力させようとしたら、命令文に「;」書いて怒られる。
忘れすぎて嫌になるはずの「;」が、Pythonだと逆に無くて落ち着かないこれ正にジレンマ。
だいぶNQCに慣れてきたようなので、これから後の課題もがんばろう。
分解している最中に、こんな表情を見せた。
名前はスマイリーとかでいいかもしれない。:)
#ref(2012b/Member/ddnp/Mission1/smile.jpg, 100%, スマイル)
ページ名: