フィールドの説明
フィールドは課題2で使用した紙を使用する。
空き缶を6本用意し、そのうち3本をC,E,Fの交差点に描いた半径4cmの円内に置き、 その上にボールを載せておく。
同様に残りの空き缶をC', E', F' 地点に置く。
競技時間は審判が続行不能と判断するまで、あるいはリタイアするまで。
図のA地点または(および)A'地点からスタートする。ただし接地している部分はそれぞれの領域内 に収まるものとする(線上はOK)。上空部分は領域からはみ出していてもよい。
開始の合図から5秒以内にスタートボタンを押す作業を完了すること。
競技が終了するまで、ロボットに触ったり人間が遠隔で操作してはならない。
途中でうまく動かなくなった場合、1回限り再スタートすることができる(再スタートの際に別プログラムで起動してよい)。
競技終了後、ロボットが、空き缶に触れていてはいけない。
ボールを1個運べば8点。3つとも運べば24点。
競技終了後、ゴールの空き缶が半分以上円からはみ出しまった場合、3点減点。
競技終了後、ゴールの空き缶が完全に円から出しまった場合、4点減点。
競技終了後、空き缶にロボットが触れていれば、もとの基本点の3減点。
以下の動作の精度・スピード・確実性などを含めた技術的な工夫や芸術性について他の全てのチーム(5チーム)が20点満点で採点し、その平均点を求める。 得点の目安:
・ボールを探し取りにいくまでの動作 (3点)
・ボールを掴む動作 (3点)
・ボールを運ぶ動作 (2点)
・ボールを容器の中に入れる動作 (2点)
・2台のNXT、EV3の連携の良さ(2点)
・自立型のロボットとしての形や動作の美しさ、斬新さ(2点)
・その他 (3点)
今回のロボットは「できるだけ小さく」を目標に設計した。その点においてはこれ以上ないくらいのできばえになったと思う。小さい分単純な動きしかできず共同作業には向かないため、同じものを2台作って別々に動作させるようにした。
前述したように簡単な動作のみを行う。ライントレースし、ボールの置いてある缶を超音波センサで感知したらボールを掴み、ボールが置かれていない缶を感知したらその場で離すというものだ。ボールをきちんと掴めるように、横から押さえるだけでなく、アームの下部にすくい上げるようなパーツをつけた。この構造のおかげで安定してボールを掴むことができた。
このロボットを小さくするにあたって最も効果があったのは、MediumMotorを完全に本体の下に収納したことである。課題1、2で製作したロボットはいずれもロボットの正面にMediumMotorをつけていたため、アームはさらに外側につけなくてはならなかった。今回のロボットはその構造上、本体のすぐそばにアームをつけることができたため、大幅なサイズダウンに成功した。3つの缶に通じる狭い交差点の中でも回転する余裕のあるサイズとなったため、小回りがききライントレースを失敗してしまうこともほぼなくなり、缶にぶつかって減点をうけることもなくなった。しかし、この構造にも1つ欠点がある。モーターとアームとの間の距離が長くなってしまったため、回転を伝える部分が横揺れを起こす現象が起きた。そのためアームに若干のゆとりができてしまい、初期位置と回転角をきちんと調整しないとボールをうまく掴むことができなくなってしまった。ただ、正しい調整が行われればほぼ100%ボールを掴むことができるので課題をこなす上では大きな問題ではなかった。
ミスの少ないライントレースを動かすには光センサのブレを少なくすることが有効だが、今回のロボットは光センサと重心の位置が大きく離れていないため、光センサのブレは起こらなかった。超音波センサは横から少し斜めに向けて設置されているため、ロボットの正面にある缶を正確に捉えることができる。センサーの取りつけはプログラムの正常な動作に大きく関わってくるため特に注意した。
#!/usr/bin/env python3 from ev3dev2.motor import * from ev3dev2.sensor import * from ev3dev2.sensor.lego import * from ev3dev2.sound import * from time import *
Wmax,Bmax,spmax,spmin,brockch,tst,balltime=75,5,8,4,6,2.3,1#それぞれ、白の基準、黒の基準、車輪の最大速度、最低速度、交差点判断のためのカウント数、を表している。 tw = MoveTank(OUTPUT_A, OUTPUT_D) Mm= MediumMotor(OUTPUT_B) cs,ts,us = ColorSensor(),TouchSensor(),UltrasonicSensor() us.mode = 'US-DIST-CM' Mv=(Wmax+Bmax)/2#カラーセンサにて灰色判断をするための数 Lp=(Wmax-Mv)/4#上記に同じ llp=2*Lp spab=(spmax+spmin)/2#ライントレース時、黒灰色や白灰色を検知したとき片輪のみを動かさないようにするための定数 Clev=[Wmax,Wmax-Lp,Mv+Lp,Mv-Lp,Bmax+Lp,Bmax]#白、白灰、灰、黒灰、黒をそれぞれ判定するためのリスト ballst=[1,1,1,0,0,0] snd=Sound()#音を鳴らすための関数
class Linetorace: def __init__(self,a): self.cv=a#aには実行時、カラーセンサで読み取った値が入る。 self.colortyp,self.dcv,self.lesp,self.risp=0,0,0,0#色の種類、速度調整、左輪の速度、右輪の速度をそれぞれ0として定義しておく。 self.typcheck()#光センサで読み取った値の色分けを行う。下記に詳しく self.movesp()#上記の色分けから左輪右輪の速度を定める。下記に詳しく def typcheck(self): for i in range(5): if Clev[i+1]<=self.cv<Clev[i]:#カラーセンサで読み取った値が黒よりなのか白よりなのか判断する。 self.colortyp=i#色の種類を定める。 self.dcv=Clev[i]-self.cv#基準となる値との差から速度を調整するための値を定める。 def movesp(self): zuw=round(self.dcv/llp,1)#上記のdcvを使って速度調整用の変数を定義する。 if self.colortyp==0:#白 self.lesp=spab-(spab-spmin)*zuw self.risp=-spmin elif self.colortyp==1:#白灰 self.lesp=spmax self.risp=spab+(spmax-spab)*zuw elif self.colortyp==2:#灰 self.lesp=spmax self.risp=spmax elif self.colortyp==3:#黒灰 self.lesp=spab+(spmax-spab)*zuw self.risp=spmax else:#黒 self.lesp=-spmin self.risp=spab-(spab-spmin)*zuw
はじめに定義されたClevというリストを用いてカラーセンサの値を分析し、色の種類を決定する。その後、色の基準となるWmaxやBmaxなどからどれほど離れているかによって右左輪の速度を調整する。近ければ近いほど速度は小さくなるようになっている。
def action(line,stti,enti):#括弧内の変数にはそれぞれ、左右のどちらをライントレースするかを決定する0か1の数、交差点判断を始める時間、交差点判断を終わる時間を入力する。 sttime1,notime1 = time(),time()#ライントレースを開始した時間を記憶する bcon=0#黒判定した回数をカウントするための変数を0として定義しておく。 while not (ts.is_pressed) and not ((notime1-sttime1)>enti): cvp = cs.reflected_light_intensity MLT=Linetorace(cvp) if MLT.colortyp==4:#黒判定したとき bcon=bcon+1#連続して黒判定した数をカウント else: bcon=0#黒判定しなかったときカウントリセット notime1=time()#引き算でライントレースの経過時間を求められるようにする if (stti <= (notime1-sttime1) <=enti) and (bcon== brockch):#交差点判断時間ないで黒判定カウントが規定の数に達したときwhile文から抜け出す。 break if line==0:#右か左のどちらかをラインとレースしているかを判断し、それにあったモーターの速度を代入する。 tw.on(MLT.lesp,MLT.risp) elif line==1: tw.on(MLT.risp,MLT.lesp) else: tw.on(-MLT.lesp,-MLT.risp) sleep(0.1) tw.off()
黒カウントの規定数とははじめに定義したbrockchによって制御される。
def closs(pp,line):#ppで曲がりたい方向を指定し、lineで右左を指定する cvp= cs.reflected_light_intensity MLT = Linetorace(cvp) if line==0:#交差点判断したときに生じる傾きを修正するための挙動 tw.on(-MLT.lesp,-MLT.risp) else: tw.on(-MLT.risp,-MLT.lesp) sleep(0.1*brockch) tw.off() if pp==0:#右側に曲がる action(0,tst,tst+0.1) else: if pp==1:#右側に曲がる action(0,tst,tst+0.1) elif pp==2:#直進する tw.on_for_seconds(spmax,spmax,1) else:#左側に曲がる action(1,tst,tst+0.1) tw.off()
右側ライントレースをすれば右折し、左側ライントレースをすれば左折する。そして直進するときはまっすぐ進ませる。
class accomand: def __init__(self,line,cltyp,sttime,endtime): self.line,self.cltyp,self.sttime,self.endtime=line,cltyp,sttime,endtime def sendcomand(self): action(self.line,self.sttime,self.endtime) closs(self.cltyp,self.line)
ここで値を定義していれば、わざわざ関数ごとに値を代入しておく必要がなくなる。
class Vector2: def __init__(self,x,y):#代入したx、yを複素数平面に変換する self.x,self.y=x,y self.comp=complex(x,y) def disp(self): print(self.x,",",self.y) def fukusosuu(self): print(self.comp) class houkou: def __init__(self,hou):#houの値によって上記のx、yの値を決める。 self.hou=hou self.x,self.y=0,0 self.kakutei() def kakutei(self): if self.hou==1: self.x=1 elif self.hou==2: self.y=1 elif self.hou==3: self.x=-1 elif self.hou==4: self.y=-1 self.ch=complex(self.x,self.y)#複素数平面に変換する def linekizyun(linehou,pointhou):#2つの値によって右側、左側ライントレースの判断する。 a=houkou(linehou) b=houkou(pointhou) che=Vector2(0,1)#虚軸に1 if a.ch==(b.ch*che.comp): print("left") ty=0#ライントレース時のlineの値となる else: print("ri") ty=1#ライントレース時のlineの値となる return ty
フィールドを複素数平面と捉えて、ライントレースしたい向きの値をlinehouに入れ、進みたい向きをpointhouに入れる。そうすると、任意のライントレースが実行される。
def ballaction(tp):#ボールを置くとき1と取るとき0でtpの値を変える face=0 while not (ts.is_pressed):#繰り返す cvp= cs.reflected_light_intensity MLT= Linetorace(cvp) tw.on(MLT.risp,MLT.lesp)#ライントレース txle= us.distance_centimeters#缶との距離を見る if txle<=4: tw.on_for_seconds(4,4,0.9)#缶に近づく face=1 break#繰り返しから抜け出す sleep(0.1) tw.off() if face==1: print("check") if tp==0: Mm.on_for_rotations(20,0.3)#ボールを掴む tw.on_for_seconds(-15,-15,1.1)#缶から離れる tw.on_for_degrees(-4,4,360)#方向転換 else: Mm.on_for_rotations(-10,0.3)#ボールを置く tw.on_for_seconds(-15,-15,1.1) tw.on_for_degrees(-4,4,360)
ボールがある時ない時で値を変えることで制御できる。
class st: def __init__(self): self.stt="xa"#ゴールの地点を決める self.line,self.point=0,0 def go(self): print("0") def point_a(stas): print("a") root=accomand(linekizyun(1,4),1,0,30)#ライントレースの内容を決定する root.sendcomand()#ライントレースをする stas.stt="yl" if not (ts.is_pressed): point_b(stas)#bの関数に移動する
以降の関数も同様の内容となっている。
def point_b(stas): print("b") if stas.stt in "xa": ballaction(0) if not (ts.is_pressed): point_a(stas) elif stas.stt in "y" or "xg" or "xh": root=accomand(linekizyun(2,1),0,0,30) root.sendcomand() if not (ts.is_pressed): point_c(stas) def point_c(stas): sleep(15) print("c") if stas.stt in "x": root=accomand(linekizyun(2,3),3,0,30) root.sendcomand() if not (ts.is_pressed): point_b(stas) elif stas.stt in "y" or "yl": root=accomand(linekizyun(2,1),3,0,30) root.sendcomand() if not (ts.is_pressed): point_d(stas) def point_d(stas): print("d") if stas.stt in "xa": root=accomand(linekizyun(2,3),3,0,30) root.sendcomand() if not (ts.is_pressed): point_c(stas) elif stas.stt in "yl": root=accomand(linekizyun(4,2),2,0,30) root.sendcomand() if not (ts.is_pressed): point_i(stas) elif stas.stt in "yn": root=accomand(linekizyun(4,2),3,2,30) root.sendcomand() if not (ts.is_pressed): point_i(stas) elif stas.stt in "yp": root=accomand(linekizyun(4,2),3,2,30) root.sendcomand() if not (ts.is_pressed): point_i(stas) elif stas.stt in "xg": root=accomand(linekizyun(3,2),2,2,30) root.sendcomand() if not (ts.is_pressed): point_e(stas) elif stas.stt in "xh": root=accomand(linekizyun(3,2),3,2,30) root.sendcomand() if not (ts.is_pressed): point_e(stas) def point_e(stas): print("e") if stas.stt in "xg": root=accomand(linekizyun(3,2),3,0,30) root.sendcomand() if not (ts.is_pressed): point_f(stas) if stas.stt in "xh": ballaction(0) if not (ts.is_pressed): point_h(stas) elif stas.stt in "yp" or "yn": root=accomand(linekizyun(3,4),1,0,30) root.sendcomand() if not (ts.is_pressed): point_d(stas) def point_f(stas): print("f") if stas.stt in "xg": ballaction(0) if not (ts.is_pressed): point_g(stas) elif stas.stt in "yp": root=accomand(linekizyun(3,4),2,0,30) root.sendcomand() if not (ts.is_pressed): point_e(stas) def point_g(stas): print("g") stas.stt="yp" root=accomand(linekizyun(3,4),2,0,30) root.sendcomand() if not (ts.is_pressed): point_e(stas) def point_h(stas): print("h") stas.stt="yn" root=accomand(linekizyun(3,4),1,0,30) root.sendcomand() if not (ts.is_pressed): point_d(stas) def point_i(stas): print("i") if stas.stt in "xa": root=accomand(linekizyun(2,3),2,2,30) root.sendcomand() if not (ts.is_pressed): point_d(stas) elif stas.stt in "yl": root=accomand(linekizyun(4,1),3,0,30) root.sendcomand() if not (ts.is_pressed): point_j(stas) elif stas.stt in "yn": root=accomand(linekizyun(1,4),3,2,30) root.sendcomand() if not (ts.is_pressed): point_m(stas) elif stas.stt in "yp": root=accomand(linekizyun(1,4),2,2,30) root.sendcomand() if not (ts.is_pressed): point_m(stas) elif stas.stt in "xg": root=accomand(1,3,2,30) root.sendcomand() if not (ts.is_pressed): point_d(stas) elif stas.stt in "xh": root=accomand(linekizyun(2,3),3,2,30) root.sendcomand() if not (ts.is_pressed): point_d(stas) def point_j(stas): print("j") if stas.stt in "xg": root=accomand(linekizyun(4,3),3,0,30) root.sendcomand() if not (ts.is_pressed): point_i(stas) elif stas.stt in "y" or "yl": root=accomand(linekizyun(4,1),3,0,30) root.sendcomand() if not (ts.is_pressed): point_k(stas) def point_k(stas): print("k") if stas.stt in "yl": ballaction(1) if not (ts.is_pressed): point_l(stas) elif stas.stt in "x" or "xg": root=accomand(linekizyun(4,3),0,0,30) root.sendcomand() if not (ts.is_pressed): point_j(stas) def point_l(stas): print("l") stas.stt="ee" root=accomand(linekizyun(3,2),1,0,30) root.sendcomand() if not (ts.is_pressed): endd() def point_m(stas): print("m") if stas.stt in "yp": root=accomand(linekizyun(1,4),3,0,30) root.sendcomand() if not (ts.is_pressed): point_o(stas) elif stas.stt in "yn": ballaction(1) if not (ts.is_pressed): endd() stas.stt="ee" elif stas.stt in "xa" or "xh": root=accomand(linekizyun(1,2),1,2,30) root.sendcomand() if not (ts.is_pressed): point_i(stas) def point_n(stas): print("n") stas.stt="xa" root=accomand(linekizyun(1,2),1,0,30) root.sendcomand() if not (ts.is_pressed): point_i(stas) def point_o(stas): print("o") if stas.stt in "yp": ballaction(1) if not (ts.is_pressed): point_p(stas) elif stas.stt in "xa": root=accomand(linekizyun(1,2),2,0,30) root.sendcomand() if not (ts.is_pressed): point_m(stas) def point_p(stas): print("p") stas.stt="xh" root=accomand(linekizyun(1,2),2,0,30) root.sendcomand() if not (ts.is_pressed): point_m(stas) def endd(): snd.speak("finish")
フィールドの交差点を上記のa,b,c...に置き換え、その地点にいるとき動かしたいように動かすための関数を定義していく。そして、目的地に合わせて次の関数に移行していき目的を達成する。最後にはendd()関数に入るようになっている。2台のロボットを使用しているが、プログラムの違いはここのみで、お互いをよけるためのスリープ場所や時間が異なる。
def main(): snd.speak("start")#スタートとしゃべらせる lobo=st()#目的地を決定する point_b(lobo)#b地点から開始する。もう1台の方はjから始める。 snd.beep()#終了後に音を鳴らす
point関数に入って一通り仕事をこなした後止まる。
if __name__ == '__main__': main()
今回はロボットを私が作り、プログラムは相方が書きました。プログラムの内容はできる限り理解して書いたつもりですが、相方のものに比べると説明不足であると思います。すいません。
本番ではボールをすべて運ぶことができた。しかし、途中で超音波センサがうまく作動できず缶を円の中から押し出してしまったのは残念だ。リハーサルではそんなことは起こらなかったので、もう少しロボットの正確性や缶の位置にまで気を遣えばよかったと思っている。