EV3を動かしてみる (5) 〜 本体のボタンとディスプレイを使う 〜

ロボティクス入門ゼミ 教材 2017-12-08

本体ボタンを使う

シェルからボタンの状態を見てみる

EV3の本体には、 中央にあるENTERボタンとその周りにあるUP、DOWN、LEFT、 RIGHTの4つのボタン、 そして左上のBACKSPACEの合計6つのボタンがある。

これらのボタンが押されたり戻されたこと、つまり何らかのイベント(出来事)は、 Linuxでは「イベント・デバイス・ファイル」の情報として扱われる。 実際に、EV3のボタンは、/dev/input/event0 というファイルで表されている。

/dev/input/event0 の中身を表示させてみる。

robot@ev3dev:~$ cat /dev/input/event0

このコマンドを入力した後、LEFTボタンを押してみると、文字化けしたようなものが表示される。 Control-C (コントロールキーを押しながらcを押す)でプロンプトに戻る。 ファイルの中身がバイナリデータになっているので、そのままだとターミナルでは読めない。 そのデータを通常のテキストとして読めるように、 cat の出力を od という変換プログラムに渡すことで通常のテキストとして読むことができる。

robot@ev3dev:~$ cat /dev/input/event0 | od
0000000 176654 055024 034754 000003 000001 000151 000001 000000
0000020 176654 055024 034754 000003 000000 000000 000000 000000
0000040 176654 055024 140057 000004 000001 000151 000000 000000
0000060 176654 055024 140057 000004 000000 000000 000000 000000
^C

終了するのは上と同じく Control-C。 この例では catコマンドの標準出力を「|」を使ってodコマンドの標準入力にした。 シェルでこのように複数のコマンドをつなげる操作のことをパイプと言う。

odはデフォルトでは結果を8進数表示するが、-h や -d オプションをつけることで16進数表示や10進数表示もできる。

robot@ev3dev:~$ cat /dev/input/event0 | od -h
0000000 fe7b 5a14 24a4 000d 0001 0069 0001 0000
0000020 fe7b 5a14 24a4 000d 0000 0000 0000 0000
0000040 fe7b 5a14 d391 000e 0001 0069 0000 0000
0000060 fe7b 5a14 d391 000e 0000 0000 0000 0000
^C

ここで、行番号に続く4つの4桁16進数 (上の例では「fe7b 5a14 24a4 000d」)はイベントの起きた時間(タイムスタンプという)を表している。 前半の32bitが秒、後半の32bitがマイクロ秒である。 ただし、このデータはいわゆる「リトル・エンディアン」と呼ばれるデータの並びになっているので、 32bitで秒を示す「7efb」と「5a14」は、「7efb」の方が下位の16bitで「5a14」が上位の16bitである。 実際に、一秒ごとにLEFTをクリックすると最初の16bitの最後の数字が1づつ増えるのがわかる (後ろの16bitはすぐには変化しない)。

タイムスタンプに続く次の16bitは、イベントの「タイプ」を示す。キーボードのキーやボタンが変化したときは 1 になる。 マウスなどが相対的に位置を変えた時は 2 となる。

さらにその次の16bit(上の例では0069)は、ボタンのコードを示し、 16進数の69(10進数では105)はLEFTボタンに対応する。 ちなみにコードの一覧やデータの構造などは、/usr/include/linux/input.h というファイルを見ればわかる。

最後の32bitはイベントの値を示し、ボタンを押しときは 1 、離した時は 0 となっている (リトルエンディアンなので 最初の16bitが下位)。

ENTERボタンとBACKSPACEボタンも、他のボタンと同様に扱うことができるが、 もともとEV3にはメニューシステムのbrickmanが動作しており、 意図しない操作を避けるために、以下の例では、UP、DOWN、LEFT、RIGHT の4つだけを使う。

Buttonクラスを使う

EV3に限らず、キーボードやマウスなどの入力機器を扱うための python-evdev というpythonライブラリがある (http://python-evdev.readthedocs.io/)。 もちろんEV3でもこのライブラリを使うことができるが、 ここでは簡単のため、 EV3 python チュートリアル (外部サイト https://sites.google.com/site/ev3python/learn_ev3_python/buttons) を参考に、 python-ev3で用意されている Button というクラスを使ってみる。

Buttonクラスを使った最初の例は、 LEFTボタンとRIGHTボタンの値を同時に表示するプログラムである。

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep

btn = Button()      # ボタンのインスタンスを作る

def main():
    while True:     # 無限ループ
        print(btn.left, btn.right)  # LEFTボタンとRIGHTボタンの値を表示
        sleep(1)                    # 1秒間隔

if __name__ == '__main__':
    main()

このスクリプトを実行するとLEFTとRIGHTのボタンの状態が1秒おきに表示される。 押されていない場合にFalse、押されている場合はTrueになる。 実際、LEFTとRIGHTの適当にボタンを押してみると、値が変わるのがわかる。

False False
True False
False False
False True
False False
True True
False False
...

while Trueのループがあるのでいつまでも値を表示し続けるが、 Control-C で強制的に終了することができる。

毎回上の例のように Control-C で強制的に終了させるのは少し気持ちが悪いので、 次のように変更して、DOWNボタンを押せばスクリプトが終了するようにする。

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep

btn = Button()

def main():
    while not btn.down:    #  ここは while btn.down == False: と書いてもよい
        print(btn.left, btn.right)
        sleep(1)

    Sound.beep().wait()    #  whileを抜けたらビープ音を鳴らして終了

if __name__ == '__main__':
    main()

ここではSound機能を使った。実際に実行してDOWNボタンでスクリプトが終了するか確認する。

【練習問題】 他のボタンの状態も同時に表示するように、上の例を改良しなさい。 ただし、BACKSPACE、ENTER、UPの状態は、Buttonクラスの backspace、enter、up というプロパティで得られる。

複数のボタンが同時に押されているか判定する

複数のボタンが同時に押されているかどうかを判定する場合は、 check_buttons()というメソッドを使うと便利である。

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep

btn = Button()

def main():

    #  'left'と'right'ボタンが同時に押されない限り、繰り返す
    while not btn.check_buttons(buttons=['left','right']):
        print(btn.left, btn.right)
        sleep(1)

    Sound.beep().wait()

if __name__ == '__main__':
    main()

ここで、check_buttons()はリスト内のボタンがすべて押されていればTrue、 そうでなければFalseを返す。

ボタンの状態変化に応じて何かさせる

process()というメソッドを使えば、 常時ボタンを監視して、変化があった時に(「イベント」が起きた時に) その変化に応じて何らかの動作をさせることもできる。

次の例は、LEFTボタンを押した時に 'LFFT button pressed!' と表示し、 LEFTボタンを離した時に 'LEFT button released!'と表示するプログラムである。

この例では、LEFTボタンの状態が変化した時に実行する関数を control_left() という名前で定義し(名前は何でもよい)、 それをButtonクラスに予め用意されている on_left()という関数の実体として使っている。 このように何かイベントが起きた時に実行される関数を「イベントハンドラ」と呼ぶ。

#!/usr/bin/env python3
from ev3dev.ev3 import *                                                        
from time import sleep                                                          
                                                                                
btn = Button()
  
def control_left(state):    # 引数の state にはLEFTの状態が渡される
    if state:
        print('LEFT button pressed!')
    else:
        print('LEFT button released!')

btn.on_left = control_left  # イベントハンドラとしてcontrol_leftを登録

def main():
    while btn.down == False:
        btn.process()       # ボタンの状態の変化をチェックし、イベントハンドラを呼び出す
        sleep(0.1)

    Sound.beep().wait()

if __name__ == '__main__':
    main()

この例を応用すれすれば、LEFTとRIGHTのボタンで左右のモータを動かすことも 簡単にできる。 次の例は左右のモータをそれぞれLEFTボタン、RIGHTボタンが押されている時だけ 回転させるプログラムである。

#!/usr/bin/env python3
from ev3dev.ev3 import *                                                        
from time import sleep                                                          
                                                                                
mL = LargeMotor('outA')
mR = LargeMotor('outB')
btn = Button()
  
def control_left(state):
    if state:
        mL.run_forever(speed_sp='500')
    else:
        mL.stop(stop_action='brake')

def control_right(state):
    if state:
        mR.run_forever(speed_sp='500')
    else:
        mR.stop(stop_action='brake')

btn.on_left = control_left
btn.on_right = control_right

def main():
    while btn.down == False:
        btn.process()
        sleep(0.1)
    Sound.beep().wait()

if __name__ == '__main__':
    main()

プログラムを本体メニューから選択して実行する場合は、 ENTERボタンでスタート、BACKSPACEボタンで停止、 のような割り当てをしても問題ない。

本体ディスプレイを使う

EV3本体には178x128ピクセルのモノクロ液晶ディスプレイがついている。 テキストや図形を表示する場合には座標を指定する必要がある。 座標は、左上のピクセルが (0,0)、 右下が (177,127)となっている (通常の x-y 座標と違って y軸は下向きが正の方向になっている)。

ev3devのpythonライブラリでは、PIL (Python Imaging Library) から派生したPillow (http://pillow.readthedocs.io/)という画像処理の汎用ライブラリを使っている。 パソコンのディスプレイと違ってEV3の表示機能はそれほど高機能ではないが、 EV3用に特化したものではないのでPillowの機能が基本的にはそのまま使用できる。 デフォルトではPillowのImageモジュールとImageDrawモジュールがインポートされているので、 それらに含まれるメソッドはそのまま使用できる。詳細は以下のリファレンスを参照のこと。

それ以外のモジュールを使う場合は、個々にインポートする必要がある。

ところで、ディスプレイにはbrickmanのメニューがもともと表示されているが、 ディスプレイに何か表示させたい場合は、brickmanの File Browser からプログラムを起動するのが無難。 パソコンのターミナルから起動する場合は、 https://sites.google.com/site/ev3python/learn_ev3_python/screen の最後の方に書かれている注意を参考のこと。

テキストの表示

簡単な例から始める。 基本的な手順は、まずディスプレイのインスタンスを作り、 clearメソッドを実行して画面をクリア、 draw.textメソッドで座標を指定してテキストを画面に置く、 updateメソッドで実際に表示、 というステップになる。 以下の例は、(10,50)の座標に'Hello, EV3!'というテキストを表示するプログラムである。

#!/usr/bin/env python3
from ev3dev.ev3 import *                                                        
from time import sleep

lcd = Screen()    # Screenのインスタンスを作る

def main():
    lcd.clear()                           # 画面をクリア (updateするまで反映されない)
    lcd.draw.text((10,50),'Hello, EV3!')  # (10,50)にテキストを表示
    lcd.update()                          # 実際に画面に表示
    sleep(6)

if __name__ == '__main__':
    main()

プログラムを実行しても、画面に'Hello, EV3!'が表示されるまで8秒くらいかかる。

大きなフォントを使いたい場合は、ev3dev.fonts モジュールをインポートすればよい。 使用できるフォントの一覧は http://python-ev3dev.readthedocs.io/en/latest/other.html#bitmap-fonts を参照。 また、ev3dev.fontsで提供されている available() 関数を使って 次のような短いスクリプトを作成・実行させれば、フォントの一覧が表示される。

#!/usr/bin/env python3
import ev3dev.fonts as fonts
print(fonts.available())

実際のスクリプトでは、フォントの読み込みはfonts.load()関数で行い、 フォントの指定はdraw.text()のオプションで行う。 以下の例は、luBS18 というフォントを使用する場合。 フォントの読み込みと指定を同時に行っている。

#!/usr/bin/env python3
from ev3dev.ev3 import *                                                        
from time import sleep
import ev3dev.fonts as fonts   # ev3dev.fonts をインポート

lcd = Screen()

def main():
    lcd.clear()
    lcd.draw.text((10,50),'Hello, EV3!',font=fonts.load('luBS18')) # フォントの読み込みと指定
    lcd.update()
    sleep(6)

if __name__ == '__main__':
    main()

次の例は、タッチセンサとカラーセンサの値をディスプレイに表示するプログラムの例である。

#!/usr/bin/env python3                                                      
from ev3dev.ev3 import *
from time import sleep
import ev3dev.fonts as fonts

ts = TouchSensor('in3')
cs = ColorSensor('in2')
cs.mode = 'COL-REFLECT'
btn = Button()
lcd = Screen()

def main():
    myfont = fonts.load('luBS14')    # フォントをロードしておく

    while not btn.backspace:
        lcd.clear()
        lcd.draw.text(( 10,20), 'Touch Sensor: '+str(ts.value()), font=myfont)
        lcd.draw.text(( 10,40), 'Color Sensor: '+str(cs.value()), font=myfont)
        lcd.update()
        sleep(0.1)

    lcd.clear()
    lcd.update()
    sleep(1)

if __name__ == '__main__':
    main()

ts.value()で得られた数値をstr()関数を使ってテキストに変換して表示する、ということに注意。

【練習問題】 超音波センサとジャイロセンサの値も同時に表示するように、上の例を改良しなさい。

図形の描画

PILでは、図形を描画するためのメソッドもたくさん用意されている。 直線は始点と終点の座標を、長方形(rectangle)、楕円(ellipse)、arc(弧)は それらが入るボックス(長方形)の左上と右下の頂点座標を指定する。 座標の指定は、((x0,y0),(x1,y1),...) または (x0,y0,x1,y1,....) のどちらの記法でもよい。

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep

lcd = Screen()   # ディスプレイのインスタンスを作る
btn = Button()

def main():
    lcd.draw.rectangle(((50,40),(129,87)), fill='black')   # 長方形の左上と右下の頂点座標を指定
    lcd.update()          # 実際に描画する
    sleep(8)

if __name__ == '__main__':
    main()

次の例は上下のボタンで眉が変化する顔のイラスト。 それぞれの描画メソッドの詳細は上で紹介した文献を参照。

#!/usr/bin/env python3
from ev3dev.ev3 import *
from time import sleep

lcd = Screen()
btn = Button()

def main():
    while not btn.backspace:
        lcd.clear()

        r1,r2 = 20,30    # 目(ellipse)と短径・長径 (r1=20 と r2=30 をまとめて代入)

        for x,y in [(40,50),(137,50)]:   # 左右の目の中心の座標でループさせる
            lcd.draw.ellipse(((x-r1,y-r1),(x+r1,y+r1)),fill='black')  # 目(内)
            lcd.draw.ellipse(((x-r1,y-r2),(x+r1,y+r2)),fill=None)     # 目(外)

        dy = 7 if btn.up else -7 if btn.down else 0   # 眉の変化量dy: UPを押すと7、DOWNを押すと-7、
                                                      # その他は 0 (ifの入れ子構造に注意)
        lcd.draw.line((( 20,10+dy),( 60,10-dy)), width=3)   # 左の眉
        lcd.draw.line(((117,10-dy),(157,10+dy)), width=3)   # 右の眉

        lcd.draw.arc(((85,50),(105,90)), 120,240)           # 鼻(3時を基準に時計周りで120〜240度の弧)
        lcd.draw.rectangle(((50,95),(127,125)), fill=None)  # 口
        lcd.draw.polygon(((60,105),(89,120),(117,100)), fill=None, outline=None)  # 多角形

        lcd.update()
        sleep(0.1)

    lcd.clear()     # 表示を消して終了
    lcd.update()
    sleep(2)

if __name__ == '__main__':
    main()

【練習問題】 上のプログラムを変更して、UP,DOWN,LEFT,RIGHTボタンを押せば目が上下左右に動くようにしなさい。