raspberry pi 3b+ に標準で載っているpython3を使って、簡単なシステムを作ってみます。
先日の記事で、ラズパイの環境構築までを完了させました。
今回はpython3のpygameとurllibのライブラリを使って簡単なシステムを作ってみます。
raspbeanにはPythonをコーディングするためのIDEが標準搭載されていたので、それを使って作りました。

『研究室にあるポットとコーヒーの水入れ替えの時間を管理するシステム』で、下のRESETボタンを押すと時刻がリセットされるというものです。コード自体に難しいところはないと思います。
長いけどとりあえず下まで読んで!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
import pygame as pg from pygame.locals import * import sys import datetime import urllib.request import urllib.parse import json import redis # -CONST START screen = None font = None fontLarge = None potResetTime = None coffeeResetTime = None errorCode = None redisComponent = None CONST = { 'APP_NAME' : "Time Checker", 'URLLOG' : "http://##RESETボタン押下時の時刻を送信してDBにInsertするphpファイルのURL##", 'URLGET' : "http://##最新のRESETボタン押下時の時刻を取得するphpファイルのURL##", 'FONT_SIZE' : 40, 'fontLarge_SIZE' : 65, 'TEXT_POT' : None, 'TEXT_COFFEE' : None, 'IMG_WIFI' : None, 'POT' : { 'X' : 55, # BaseLine of Left(Pot) 'TEXT' : { 'COLOR' : (255, 255, 255), 'X' : 85, 'Y' : 15, } }, 'RECT_POT' : { 'COLOR' : (0, 80, 0), 'X' : 20, 'Y' : 10, 'W' : 200, 'H': 40, }, 'COFFEE' : { 'X' : 295, # BaseLine of Left(Coffee) 'TEXT' : { 'COLOR' : (255, 255, 255), 'X' : 305, 'Y' : 15, } }, 'RECT_COFFEE' : { 'COLOR' : (0, 80, 0), 'X' : 260, 'Y' : 10, 'W' : 200, 'H': 40, }, 'DAYS' : { 'X_OFFSET' : 15, 'Y' : 90, 'COLOR' : (255, 255, 255), }, 'TIME' : { 'X_OFFSET' : -15, 'Y' : 140, 'COLOR' : (255, 255, 255), }, 'AGO' : { 'X_OFFSET' : 35, 'Y' : 190, 'COLOR' : (255, 255, 255), }, 'RESET' : { 'COLOR' : (40, 120, 40), 'X_OFFSET' : 25, 'Y_OFFSET' : 16 }, 'RECT_RESET' : { 'COLOR' : (255,255,51), 'X_OFFSET' : -10, 'Y' : 250, 'W' : 140, 'H': 60, }, } # -CONST END def init(): global CONST global screen global font global fontLarge global potResetTime global coffeeResetTime global errorCode global redisComponent #screen = pg.display.set_mode((480, 320)) screen = pg.display.set_mode((480, 320), FULLSCREEN) thickarrow_strings = ( " ", " ", " ", " ", " ", " ", " ", " ",) cursor = pg.cursors.compile(thickarrow_strings) pg.mouse.set_cursor((8,8),(0,0),cursor[0],cursor[1]) pg.init() pg.display.set_caption(CONST['APP_NAME']) font = pg.font.Font(None, CONST['FONT_SIZE']) fontLarge = pg.font.Font(None, CONST['fontLarge_SIZE']); CONST['TEXT_POT'] = font.render("POT", True, CONST['POT']['TEXT']['COLOR']); CONST['TEXT_COFFEE'] = font.render("COFFEE", True, CONST['COFFEE']['TEXT']['COLOR']); CONST['IMG_WIFI'] = pg.image.load('wifi.jpg') #TestData #potResetTime = datetime.datetime(2019, 3, 26, 15, 00, 00) #coffeeResetTime = datetime.datetime(2019, 3, 23, 18, 00, 00) errorCode = font.render("", False, (0, 0, 0)) url = CONST['URLGET'] try : request = urllib.request.Request(url) response = urllib.request.urlopen(request) result = response.read().decode('utf-8') data = json.loads(result) potResetTime = datetime.datetime.strptime(data['POT'], '%Y-%m-%d %H:%M:%S') coffeeResetTime = datetime.datetime.strptime(data['COFFEE'], '%Y-%m-%d %H:%M:%S') if 'ERROR' in data: errorCode = font.render(data['ERROR'], True, (255, 50, 50)) except urllib.error.HTTPError as e: errorCode = font.render("E:"+str(e.code), True, (255, 50, 50)) print(e) redisComponent = redis.StrictRedis(host='localhost', port=6379) def drawUI(): global CONST _rPot = CONST['RECT_POT'] _rCoffee = CONST['RECT_COFFEE'] pg.draw.rect(screen, _rPot['COLOR'], Rect(_rPot['X'], _rPot['Y'], _rPot['W'], _rPot['H'])) pg.draw.rect(screen, _rCoffee['COLOR'], Rect(_rCoffee['X'], _rCoffee['Y'], _rCoffee['W'], _rCoffee['H'])) _tPot = CONST['POT']['TEXT'] _tCoffee = CONST['COFFEE']['TEXT'] screen.blit(CONST['TEXT_POT'], [_tPot['X'], _tPot['Y']]) screen.blit(CONST['TEXT_COFFEE'], [_tCoffee['X'], _tCoffee['Y']]) def drawElements(days, subStr, type) : global CONST tDays = font.render(days+" days", True, CONST['DAYS']['COLOR']) tTime = fontLarge.render(subStr, True, CONST['TIME']['COLOR']) tAgo = font.render("ago", True, CONST['AGO']['COLOR']) tReset = font.render("RESET", True, CONST['RESET']['COLOR']); _type = CONST[type] _days = CONST['DAYS'] _time = CONST['TIME'] _ago = CONST['AGO'] screen.blit(tDays, [_type['X'] + _days['X_OFFSET'], _days['Y']]) screen.blit(tTime, [_type['X'] + _time['X_OFFSET'], _time['Y']]) screen.blit(tAgo, [_type['X'] + _ago['X_OFFSET'], _ago['Y']]) _rect = CONST['RECT_RESET'] _reset = CONST['RESET'] pg.draw.rect(screen, _rect['COLOR'], Rect(CONST[type]['X'] + _rect['X_OFFSET'], _rect['Y'], _rect['W'], _rect['H'])) screen.blit(tReset, [CONST[type]['X'] + _rect['X_OFFSET'] + _reset['X_OFFSET'], _rect['Y'] + _reset['Y_OFFSET']]) global errorCode screen.blit(errorCode, [10, 70]) return None def calcTime(resetTime): now = datetime.datetime.now() sub = now - resetTime subSec = int(sub.seconds) days = str(sub.days) hours = str(int(subSec / 3600)) minutes = str(int((subSec % 3600) / 60)) seconds = str(subSec % 3600 % 60) subStr = str(hours) + ":" + minutes.zfill(2) + ":" + seconds.zfill(2) return days, subStr def checkInnerTap(x, y, type): global CONST _rect = CONST['RECT_RESET'] x1, y1 = (CONST[type]['X'] + _rect['X_OFFSET'], _rect['Y']) x2, y2 = (x1 + _rect['W'], y1 + _rect['H']) if ((x1 < x and x < x2) and (y1 < y and y < y2)) : return True return False def tapAction(x, y): global potResetTime global coffeeResetTime if (checkInnerTap(x, y, 'POT')) : now = datetime.datetime.now() if(postLog(now, 'POT')) : potResetTime = now if (checkInnerTap(x, y, 'COFFEE')) : now = datetime.datetime.now() if(postLog(now, 'COFFEE')) : coffeeResetTime = now def postLog(resetTime, type): global CONST global screen global errorCode global redisComponent errorCode = font.render("", False, (0, 0, 0)); screen.blit(CONST['IMG_WIFI'], CONST['IMG_WIFI'].get_rect()) pg.display.update() url = CONST['URLLOG'] data = urllib.parse.urlencode({ 'date' : resetTime.strftime("%Y-%m-%d %H:%M:%S"), 'type': type, }).encode('utf-8') try : request = urllib.request.Request(url, data) response = urllib.request.urlopen(request) result = response.read() errorCode = font.render("", False, (0, 0, 0)) print("publish") redisComponent.publish(type, resetTime.strftime("%Y-%m-%d %H:%M:%S")) return True except urllib.error.HTTPError as e: errorCode = font.render("E:"+str(e.code), True, (255, 50, 50)) print(e) return False def main(): global CONST init() while(True): screen.fill((0, 0, 0)) drawUI(); pDays, pSubStr = calcTime(potResetTime) cDays, cSubStr = calcTime(coffeeResetTime) drawElements(pDays, pSubStr, 'POT') drawElements(cDays, cSubStr, 'COFFEE') for event in pg.event.get(): if event.type == QUIT: pg.quit() sys.exit() if event.type == KEYDOWN: # キーを押したとき if event.key == K_ESCAPE: # Escキーが押されたとき pg.quit() sys.exit() if event.type == MOUSEBUTTONDOWN: x, y = event.pos tapAction(x, y) pg.display.update() if __name__ == "__main__": main() |
長っ!!
と思われるかもしれません。ちょっとずつ説明していきます。
全体の関数の流れはこんな感じです。
プログラムの説明
まずはライブラリをインポートします。(redisが入ってますが、これは番外編で使います。)
実行前にはターミナルで、$ pip3 install (パッケージ名) でインストールしておきましょう。
1 2 3 4 5 6 7 8 |
import pygame as pg from pygame.locals import * import sys import datetime import urllib.request import urllib.parse import json import redis |
プログラムをターミナルから実行すると、一番下の
1 2 |
if __name__ == "__main__": main() |
に飛びます。
main()
main() は、”Escキー”または”閉じるボタン”を押すまで、ボタンの描画などの処理を無限ループで実行します。
まず様々な最初の設定(初期化)を行う init() を呼び出します。
その後、変わることがないUI部の描画を drawUI() で行います。
そして、最後に水を変えた時間から何分経ったのかを計算するために、calcTime() を実行します。
計算後、計算された値を画面に表示します。( drawElements() 部)
次にマウスクリックやキーボード入力などのイベントがあれば、それに応じた処理をします。
バツボタン(閉じるボタン?右上のXです)を押した時、Escキーを押したときにシステムを終了。
画面内をクリックしたときは、tapAction()へ行きます。
は”Escキー”または”閉じるボタン”を押すまで、ボタンの描画などの処理を無限ループで実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def main(): global CONST init() while(True): screen.fill((0, 0, 0)) drawUI(); pDays, pSubStr = calcTime(potResetTime) cDays, cSubStr = calcTime(coffeeResetTime) drawElements(pDays, pSubStr, 'POT') drawElements(cDays, cSubStr, 'COFFEE') for event in pg.event.get(): if event.type == QUIT: #バツボタン押下時 pg.quit() sys.exit() if event.type == KEYDOWN: # キー押下時 if event.key == K_ESCAPE: # Escキー押下時 pg.quit() sys.exit() if event.type == MOUSEBUTTONDOWN: #クリック時 x, y = event.pos tapAction(x, y) pg.display.update() |
calcTime()
calcTime() では、現在時刻との差分を計算しています。
datetime 同士の引き算は、timedelta型 の返り値を持つため、特殊な実装方法です。
一度、差を秒に変換してから、日数、時分秒に変えています。もっと賢くコーディングしたい。
1 2 3 4 5 6 7 8 9 10 11 |
def calcTime(resetTime): now = datetime.datetime.now() sub = now - resetTime subSec = int(sub.seconds) days = str(sub.days) hours = str(int(subSec / 3600)) minutes = str(int((subSec % 3600) / 60)) seconds = str(subSec % 3600 % 60) subStr = str(hours) + ":" + minutes.zfill(2) + ":" + seconds.zfill(2) return days, subStr |
drawElements()
drawElements() では、時刻テキストの表示、リセットボタンの表示を行っています。
font.render と文字列を一度画像にレンダリングしているのは、画像でないと読み込めないというpygameの仕様みたいです。このせいでかなりコードが膨らみますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def drawElements(days, subStr, type) : global CONST tDays = font.render(days+" days", True, CONST['DAYS']['COLOR']) tTime = fontLarge.render(subStr, True, CONST['TIME']['COLOR']) tAgo = font.render("ago", True, CONST['AGO']['COLOR']) tReset = font.render("RESET", True, CONST['RESET']['COLOR']); _type = CONST[type] _days = CONST['DAYS'] _time = CONST['TIME'] _ago = CONST['AGO'] screen.blit(tDays, [_type['X'] + _days['X_OFFSET'], _days['Y']]) screen.blit(tTime, [_type['X'] + _time['X_OFFSET'], _time['Y']]) screen.blit(tAgo, [_type['X'] + _ago['X_OFFSET'], _ago['Y']]) _rect = CONST['RECT_RESET'] _reset = CONST['RESET'] pg.draw.rect(screen, _rect['COLOR'], Rect(CONST[type]['X'] + _rect['X_OFFSET'], _rect['Y'], _rect['W'], _rect['H'])) screen.blit(tReset, [CONST[type]['X'] + _rect['X_OFFSET'] + _reset['X_OFFSET'], _rect['Y'] + _reset['Y_OFFSET']]) global errorCode screen.blit(errorCode, [10, 70]) return None |
init()
init() では、初期化を行います。
まず、ディスプレイの設定を行います。本番環境はFULLSCREENなのですが、開発環境はそうだと面倒なのでコメントアウトしています。
次に、マウスポインターの設定をしています。今回はマウスポインターを非表示(透明)にするためにこのような処理を行っています。マウスポインターを非表示にする組み込み関数はあったのですが、マウスクリックが出来ない状態になってしまったので、このような実装方法を取りました。
その後は、アプリ名の指定、フォントの指定を行います。
try文からは、DBに最新のRESETボタン押下時刻を取りに行っています。接続できなかった場合はエラーコードが画面に出るので、アプリを閉じて再起動してもらうことになります。
最後の redisComponent は今は必要ないです。後の記事で必要になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
def init(): global CONST global screen global font global fontLarge global potResetTime global coffeeResetTime global errorCode global redisComponent #screen = pg.display.set_mode((480, 320)) screen = pg.display.set_mode((480, 320), FULLSCREEN) thickarrow_strings = ( " ", " ", " ", " ", " ", " ", " ", " ",) cursor = pg.cursors.compile(thickarrow_strings) pg.mouse.set_cursor((8,8),(0,0),cursor[0],cursor[1]) pg.init() pg.display.set_caption(CONST['APP_NAME']) font = pg.font.Font(None, CONST['FONT_SIZE']) fontLarge = pg.font.Font(None, CONST['fontLarge_SIZE']); CONST['TEXT_POT'] = font.render("POT", True, CONST['POT']['TEXT']['COLOR']); CONST['TEXT_COFFEE'] = font.render("COFFEE", True, CONST['COFFEE']['TEXT']['COLOR']); CONST['IMG_WIFI'] = pg.image.load('wifi.jpg') #TestData #potResetTime = datetime.datetime(2019, 3, 26, 15, 00, 00) #coffeeResetTime = datetime.datetime(2019, 3, 23, 18, 00, 00) errorCode = font.render("", False, (0, 0, 0)) url = CONST['URLGET'] try : request = urllib.request.Request(url) response = urllib.request.urlopen(request) result = response.read().decode('utf-8') data = json.loads(result) potResetTime = datetime.datetime.strptime(data['POT'], '%Y-%m-%d %H:%M:%S') coffeeResetTime = datetime.datetime.strptime(data['COFFEE'], '%Y-%m-%d %H:%M:%S') if 'ERROR' in data: errorCode = font.render(data['ERROR'], True, (255, 50, 50)) except urllib.error.HTTPError as e: errorCode = font.render("E:"+str(e.code), True, (255, 50, 50)) print(e) redisComponent = redis.StrictRedis(host='localhost', port=6379) |
drawUI()
画面上部のPOT と COFFEE を描画しています。
1 2 3 4 5 6 7 8 9 10 |
def drawUI(): global CONST _rPot = CONST['RECT_POT'] _rCoffee = CONST['RECT_COFFEE'] pg.draw.rect(screen, _rPot['COLOR'], Rect(_rPot['X'], _rPot['Y'], _rPot['W'], _rPot['H'])) pg.draw.rect(screen, _rCoffee['COLOR'], Rect(_rCoffee['X'], _rCoffee['Y'], _rCoffee['W'], _rCoffee['H'])) _tPot = CONST['POT']['TEXT'] _tCoffee = CONST['COFFEE']['TEXT'] screen.blit(CONST['TEXT_POT'], [_tPot['X'], _tPot['Y']]) screen.blit(CONST['TEXT_COFFEE'], [_tCoffee['X'], _tCoffee['Y']]) |
tapAction(), checkInnerTap()
tapAction()では、ボタンが押された位置によって処理を分岐させます。
ifの条件内の checkInnerTap(x, y, ‘POT’) では、グローバル変数CONST の中の POT のx座標とy座標を参照して、タップ位置が内側か否か(True or False) を返します。
もし内側であれば、次にDBに現在時刻などの情報を送ります。この処理が問題なく完了すれば、pythonを動かしている端末上の時刻の値を書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def tapAction(x, y): global potResetTime global coffeeResetTime if (checkInnerTap(x, y, 'POT')) : now = datetime.datetime.now() if(postLog(now, 'POT')) : potResetTime = now if (checkInnerTap(x, y, 'COFFEE')) : now = datetime.datetime.now() if(postLog(now, 'COFFEE')) : coffeeResetTime = now def checkInnerTap(x, y, type): global CONST _rect = CONST['RECT_RESET'] x1, y1 = (CONST[type]['X'] + _rect['X_OFFSET'], _rect['Y']) x2, y2 = (x1 + _rect['W'], y1 + _rect['H']) if ((x1 < x and x < x2) and (y1 < y and y < y2)) : return True return False |
postLog()
postLog() は、指定したURLにボタンをクリックしたときの時刻を送信します。
まず、この関数に入ってくると、画面左上に IMG_WIFI という画像を出力します。通信中という表示です。

それからPOSTデータをエンコードして、リクエストを投げます。レスポンスでエラーがなければTrueを返します。(redisComponentと書かれているところは無視してください)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def postLog(resetTime, type): global CONST global screen global errorCode global redisComponent errorCode = font.render("", False, (0, 0, 0)); screen.blit(CONST['IMG_WIFI'], CONST['IMG_WIFI'].get_rect()) pg.display.update() url = CONST['URLLOG'] data = urllib.parse.urlencode({ 'date' : resetTime.strftime("%Y-%m-%d %H:%M:%S"), 'type': type, }).encode('utf-8') try : request = urllib.request.Request(url, data) response = urllib.request.urlopen(request) result = response.read() errorCode = font.render("", False, (0, 0, 0)) print("publish") redisComponent.publish(type, resetTime.strftime("%Y-%m-%d %H:%M:%S")) return True except urllib.error.HTTPError as e: errorCode = font.render("E:"+str(e.code), True, (255, 50, 50)) print(e) return False |
プログラムの処理は以上になります。
Pythonのコードは以上になりますが、DB処理を行う際のPHPファイルがないのでまだ完成ではありません。
ただ少し長くなってしまったのでPHPファイルに関する説明は別記事に行います。