はじめに
こちらは
Arduino と Raspberry Pi の違い
の4つ目の個別記事で、Raspberry Pi で GUI を作る。
目的
Raspeberry Pi3 には HDMI出力がついている。私が利用する環境では、HDMI 入力があるディスプレイがあるので、これまで小型の OLED を使って表示していたステータスやメニューをHDMI出力のディスプレイへの表示に切り替えて実装することにする。最終的には、比較的小型のタッチパネル付きの液晶ディスプレイに変更するかもしれないが、まずは Mouse操作を前提として機能を移植する。
念のために書いておくと、Raspberry Pi の Linux としてのデスクトップ環境の Window Manager の GUI のことではなくて、python でプログラムを書いて、それを利用するための GUI の話である。
最低限のGUI
まず最初にArduinoで実現していた機能を整理すると以下のようになる。
Arduino | Raspberry Pi3 | |
---|---|---|
トリガ | Push switch | GUIのボタンで特定の関数を実行する (完了) |
Parameter調整 | OLEDに表示 | Comboboxを使って変数を選択する(検証中) |
進捗表示 | OLEDに時間表示 | どこまで進捗したか、時間の経過を表示していた。これをGUI上に表示する。(検証中) |
データ表示 | OLEDにパラメータ表示 | パラメータの値をリアルタイムにでGUIに表示する。(未着手) |
Arduinoではたくさん push switchやRotary Encoderを複数接続していたが、Raspberry Pi3では、もう全部GUIでやることにする。つまり、たくさんのスイッチもRotary encoderも接続しなくてよいため、配線は相当シンプルになる。
1. 最初の一歩
- ボタンを押したら、動かしたい関数を実行する
必要なcode
まずはこれ。tkinterというGUI toolkitをを使う。
import tkinter as tk
- それぞれなんとなく分かるが、指定サイズの windowを開く。
- ボタンを作り、押されたら実行したい関数名を記載。packでならべてまとめて描画する
push2, push3の関数は省略している。 - 終了してしまわないように root.mainloop()を入れておく。
こんな感じでお手軽にボタンを配置して、対応した関数を割り当てて、マウスで実行できるようになる。なんて簡単なんだ。
root= tk.Tk()
root.geometry('130x200')
button10 = tk.Button(root, text='Profile #1',command=push).pack()
button11 = tk.Button(root, text='Profile #2',command=push2).pack()
button12 = tk.Button(root, text='Profile #3',command=push3).pack()
sleep(0.01)
root.mainloop()
実装
import tkinter as tk
import RPi.GPIO as GPIO
def push():
GPIO.setmode(GPIO.BCM)
GPIO.setup(RELAY_GPIO, GPIO.OUT)
try:
GPIO.output(RELAY_GPIO,GPIO.HIGH)
sleep(2)
GPIO.output(RELAY_GPIO, GPIO.LOW)
except KeyboardInterrupt:
clean()
### MAIN ------------------------------------------------------
print('MAIN-----------------')
print('setup - 01 DEFINITION')
RELAY_GPIO = 25 # GPIO number for relay (motor)
PC_GPIO = 26 # GPIO number for photo caplur
RELAY2_GPIO = 21 # GPIO number for 2nd relay (push switch)
print('setup - 03 PIN mode.')
GPIO.setmode(GPIO.BCM)
GPIO.setup(RELAY_GPIO, GPIO.OUT)
GPIO.setup(RELAY2_GPIO, GPIO.OUT)
GPIO.setup(PC_GPIO, GPIO.OUT)
print('setup - 04 set PIN initial values.')
GPIO.output(RELAY_GPIO, GPIO.LOW)
GPIO.output(RELAY2_GPIO, GPIO.LOW)
GPIO.output(PC_GPIO, GPIO.LOW)
print('setup - 05 GUI.')
root= tk.Tk()
root.geometry('130x200')
button10 = tk.Button(root, text='Profile #1',command=push).pack()
button11 = tk.Button(root, text='Profile #2',command=push).pack()
button12 = tk.Button(root, text='Profile #3',command=push).pack()
print('END OF GUI.---------------------------')
sleep(0.01)
root.mainloop()
print('END OF MAIN---------------------------')
### END OF MAIN
2. 進捗表示
Arduino では、graphicalにProgress Barを表示していたが、まずは数字を表示すれば十分かと思い、少しレベルを下げて数字のみ表示することににする。
さらに目標をさげて、console表示でまずは対応した。
その前にMenuをつくる。File/Edit/Optionの三つだ。昔のMacintoshの最低限のApplicationのMenuである。
まずは、File のみ。Quitを選んだら終了する。
次は、File の横にEditを加える。
最後にOptionを加えて、中身も作る。
3. 最後は、データ表示
こっちもconsoleにしちゃった。
とりあえず。
ステップ2
ボタンラベルのトグル表示
ボタンを押して、特定の関数を実行することができるようになったが、実行後、停止したいときのためにボタンのラベルを変更して、停止に対応できるようにする。
具体的には、最初 Start だったラベルを Stop に変更する。もうちょっとよいやり方があるはずだが、とりあえずlabelの切り替えはできるようになった。
#!/usr/bin/python
from tkinter import *
from tkinter import ttk
btnLabel = "Start"
def button_start_clicked():
global btnLabel
if btnLabel == "Start":
button2["text"] = "Stop"
btnLabel = "Stop"
elif btnLabel == "Stop":
button2["text"] = "Start"
btnLabel = "Start"
# GUI
root = Tk()
root.title('test')
# FRAME3
frame3 = ttk.Frame(root)
frame3['height'] = 300
frame3['width'] = 300
frame3['borderwidth'] = 5
frame3.grid(row=0,column=0)
## butonn for frame3
button2 = ttk.Button(frame3, text='Start', command=button_start_clicked)
button2.grid(row=0, column=0)
root.mainloop()
Multi Thread
ボタンを押して、ある関数を呼ぶのはできたが、それだけだと処理が完了するまで何もできなくなってしまう。これは別のThreadでその関数を並列して実行するしかないだろう。
ある関数をthreadとして実行するには、最後の2行のようにする。
import threading, time
def myprint(thread_name, sleep_time):
for i in range(5):
print(thread_name + ': ' + str(i))
time.sleep(sleep_time)
# MAIN
if __name__ == '__main__':
thread1 = threading.Thread(target=myprint, args=('new thread1', 1,))
thread1.start()
$ python 077-thread.py
new thread1: 0
new thread1: 1
new thread1: 2
new thread1: 3
new thread1: 4
ボタンからthread実行
ボタンからthreadを実行する。
#!/usr/bin/python
from tkinter import *
from tkinter import ttk
import time
import threading
btnLabel = "Start"
def button_start_clicked():
global btnLabel
global thread1
if btnLabel == "Start":
button2["text"] = "Stop"
btnLabel = "Stop"
print("doing someting...wait")
#mytimer()
thread1 = threading.Thread(target=mytimer,args=('A',10))
thread1.start()
print("thread started")
elif btnLabel == "Stop":
button2["text"] = "Start"
btnLabel = "Start"
print("stopped...")
def mytimer(name, sleep_time):
print("my timer started...")
time_before = time.time()
time.sleep(sleep_time)
time_after = time.time()
time_elapsed = time_after - time_before
print(time_elapsed)
print("my timer finished....")
# MAIN
if __name__ == '__main__':
root = Tk()
root.title('test')
# FRAME3
frame3 = ttk.Frame(root)
frame3['height'] = 300
frame3['width'] = 300
frame3['borderwidth'] = 5
frame3.grid(row=0,column=0)
## butonn for frame3
button2 = ttk.Button(frame3, text='Start', command=button_start_clicked)
button2.grid(row=0, column=0)
root.mainloop()
ステップ3 - GUIデザイン
自分の用途には、pack()とか紹介する必要もないくらい使えないことが分かった。
grid()を使うと自由度が高い。
複数のフレームを作り、それらをgridで配置しておければ、それらのlayoutを自由に変えられるため、自由度が高い。
例えば、画面を6分割して、6個のフレームを使う。[11]などの数字は、gridで配置する歳の row, columnの数字である。
Frame1[11] Frame2[12]
Frame3[21] Frame4[22]
Frame5[31] Frame6[32]
具体的には、一番上の二つであれば以下のように指定すればよい。
frame1 = ttk.Frame(root)
frame1['height'] = 150
frame1['width'] = 250
frame1['borderwidth'] = 5
frame1.grid(row=1, column=1)
frame2 = ttk.Frame(root)
frame2['height'] = 250
frame2['width'] = 250
frame2['borderwidth'] = 5
frame2.grid(row=1,column=2)
そして、それぞれのフレーム内でボタンなどwidgetを配置していけばよい。
実行
python application実行は、どちらのversionのpythonを使っているか意識が必要なことがある。
defaultではpythonは2.x系であり、python3を使う場合は、明示的にcommand lineでpython3として起動しないといけない。
logger を使って debug する
目的
-
ここでは、開発中に print()で変数やメッセージを出していたが、開発中に不要なprintがでてきたものの、消すには惜しい、また使うかもというような場合に有効である。
-
Java では Log4J なんてものがあったが、python ではどうするのか?
import logging.config
def foo():
print("test test test.")
logger.log(20, 'foo.info 20')
if __name__ == '__main__':
logging.config.fileConfig('logging.conf')
logger = logging.getLogger()
logger.setLevel(10)
logger.log(20, 'info is 20')
logger.log(30, 'warning is 30')
logger.log(100, 'test is 100')
logger.info('info: test')
logger.warning('warning: test')
foo()
設定ファイルはこちら。
[loggers]
keys=root
[handlers]
keys=consoleHandler, fileHandler
[formatters]
keys=logFormatter
[logger_root]
handlers=consoleHandler, fileHandler
[handler_consoleHandler]
class=logging.StreamHandler
level=DEBUG
formatter=logFormatter
args=(sys.stdout, )
[handler_fileHandler]
class=logging.FileHandler
level=DEBUG
formatter=logFormatter
args=('test.log', )
[formatter_logFormatter]
class=logging.Formatter
format=%(asctime)s:%(lineno)d:%(levelname)s:%(message)s
この例では、
logger.setLevel(10)
の10のところを30にすると、info レベルは表示されなくなる。
これまでの print の代わりに logger.infoから使い始めて、不要なメッセージだと思えてくれば、levelを分けていくのがよいかと思う。
Logging HOWTO より、Levelとその使い方は以下の通り。
Name | Level | 用途 |
---|---|---|
DEBUG | 10 | 診断時に利用するための詳細情報 |
INFO | 20 | 期待通りに動作していることを確認するレベル |
WARNING | 30 | disk容量の不足など、近い将来に起きそうなことの警告。まだ期待通りに動作する状態 |
ERROR | 40 | 深刻な問題が発生し、部分的に期待通りに動作しない状態 |
CRITICAL | 50 | さらに深刻なエラーで、正常に継続できない状況 |
Levelのdefaultは、WARNINGになっているそうで、開発中は DEBUG, INFOが妥当かと考える。問題がおきなたら、DEBUGかINFOに変更して様子を見る感じか。