プラレール に Raspberry Pi Zero を搭載してスマート化する企画、第2回は前回に続いて、Raspberry Pi Zero 上にカメラストリーミングやモータ制御のためのUIを FLASK を使って構築します。
Python用のWebフレームワークFLASKで動かせるカメラ用ライブラリをいくつか試してみましたが、FLASK初心者の自分が導入出来て、レスポンスが実用レベルだったのはこちらでした。
但し、それでも発生する遅延から、camera_pi.py内を少しいじり、フレームレートを初期値15から5へと落とし、映像反転処理も加えています。
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 |
#!/usr/bin/env python # -*- coding: utf-8 -*- import time import io import threading import picamera class Camera(object): thread = None # background thread that reads frames from camera frame = None # current frame is stored here by background thread last_access = 0 # time of last client access to the camera def initialize(self): if Camera.thread is None: # start background frame thread Camera.thread = threading.Thread(target=self._thread) Camera.thread.start() # wait until frames start to be available while self.frame is None: time.sleep(0) def get_frame(self): Camera.last_access = time.time() self.initialize() return self.frame @classmethod def _thread(cls): with picamera.PiCamera() as camera: # camera setup camera.resolution = (320, 240) # camera.framerate = 15 camera.framerate = 5 camera.hflip = True camera.vflip = True # let camera warm up camera.start_preview() time.sleep(2) stream = io.BytesIO() for foo in camera.capture_continuous(stream, 'jpeg', use_video_port=True): # store frame stream.seek(0) cls.frame = stream.read() # reset stream for next frame stream.seek(0) stream.truncate() # if there hasn't been any clients asking for frames in # the last 10 seconds stop the thread if time.time() - cls.last_access > 10: break cls.thread = None |
続いてFLASKによるモータの制御です。以下の記事を参考に組みました、感謝。
そしてこれら2つの機能を合わせたFLASKのapp.pyは、以下のようになります。
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 |
# -*- coding: utf-8 -*- from flask import Flask, render_template, request, Response from camera_pi import Camera import RPi.GPIO as GPIO import signal import sys # VARIABLES # Swap 24:23pin for Rev. Wiring Conn. pinFwd = 24 pinRev = 23 pinLed = 4 pwmFrq = 200 stsLed = 0 # FLASK INSTANSE app = Flask(__name__) # GPIO SETUP GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(pinFwd, GPIO.OUT) GPIO.setup(pinRev, GPIO.OUT) GPIO.setup(pinLed, GPIO.OUT) GPIO.output(pinLed, GPIO.LOW) p = GPIO.PWM(pinFwd, pwmFrq) q = GPIO.PWM(pinRev, pwmFrq) # ACTION FOR / @app.route("/") def index(): # READ GPIO STATUS stsLed = GPIO.input(pinLed) templateData = { 'led' : stsLed, } return render_template("index.html", **templateData) # ACTION FOR /CHANGEPWM @app.route("/changepwm", methods=["POST"]) def changepwm(): app.logger.debug(request.method) if "POST" == request.method: dire = request.form["dire"] duty = request.form["duty"] app.logger.debug("dire = " + dire) app.logger.debug("dire type = " + str(type(dire))) app.logger.debug("duty = " + duty) app.logger.debug("duty type = " + str(type(duty))) duty = int(duty) if dire == "n": # STOP p.stop() q.stop() elif dire == "f": # FORWARD q.stop() p.start(0) p.ChangeDutyCycle(duty) elif dire == "r": # REVERSE p.stop() q.start(0) q.ChangeDutyCycle(duty) return "" # ACTION FOR /CHANGELED @app.route("/changeled", methods=["POST"]) def changeled(): app.logger.debug(request.method) if "POST" == request.method: led = request.form["led"] app.logger.debug("led = " + led) app.logger.debug("led type = " + str(type(led))) if led == '1': GPIO.output(pinLed, True) if led == '0': GPIO.output(pinLed, False) # READ GPIO STATUS stsLed = GPIO.input(pinLed) templateData = { 'led' : stsLed, } return render_template("index.html", **templateData) # ACTION FOR /VIDEO_FEED def gen(camera): while True: frame = camera.get_frame() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') @app.route('/video_feed') def video_feed(): return Response(gen(Camera()), mimetype='multipart/x-mixed-replace; boundary=frame') # CTRL+C INTERRUPTION def sigint_handler(signal, frame): app.logger.debug("Closing") p.stop() q.stop() GPIO.cleanup() app.logger.debug("Closed") sys.exit(0) ## MAIN ROUTINE ## if __name__ == "__main__": # SIGINT HANDLER signal.signal(signal.SIGINT, sigint_handler) # CALL FLASK INSTANSE app.run("0.0.0.0", debug=True) #app.run("0.0.0.0") |
そしてUIのフロントエンドとなるhtml部分は、勝手知ったるJavaScriptを駆使して組んでみました。
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 |
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Thomas Pi Controller</title> <script type="text/javascript"> function changePWM(dir,vel) { // FORM ELEM var form = document.createElement("form"); form.setAttribute("action","/changepwm"); form.setAttribute("method","post"); form.setAttribute("target","hiddeniframe"); form.style.display = "none"; document.body.appendChild(form); // SET PARAM var input; // DIRECTION input = document.createElement("input"); input.setAttribute("type","hidden"); input.setAttribute("name","dire"); input.setAttribute("value",dir); form.appendChild(input); // DUTY RATIO = VELOCITY input = document.createElement("input"); input.setAttribute("type","hidden"); input.setAttribute("name","duty"); input.setAttribute("value",vel); form.appendChild(input); // SUBMIT //alert("Submit: "+dir+vel); form.submit(); } function changeLED(sts) { // FORM ELEM var form = document.createElement("form"); form.setAttribute("action","/changeled"); form.setAttribute("method","post"); form.setAttribute("target","hiddeniframe"); form.style.display = "none"; document.body.appendChild(form); // SET PARAM var input; input = document.createElement("input"); input.setAttribute("type","hidden"); input.setAttribute("name","led"); input.setAttribute("value",sts); form.appendChild(input); // SUBMIT form.submit(); } function onChangePWM(d,v) { var elID = d + v; // SET DUTYCYCLE if (v > 0) { switch(v) { case 1: vel = 10; break; case 2: vel = 15; break; case 3: vel = 25; break; case 4: vel = 30; break; default: vel = 0; break; } } else {vel = 0;} // SET DIR switch(d) { case 'n': dir = d; break; case 'r': if (currID.substr(0,1) != 'f') { dir = d; } else { //SAFETY dir = 'n'; vel = 0; v = 0; forceSetNeutral(); } break; case 'f': if (currID.substr(0,1) != 'r') { dir = d; } else { //SAFETY dir = 'n'; vel = 0; v = 0; forceSetNeutral(); } break; default: dir = d; break; } //CALL FORM SUBMIT currID = dir + v; changePWM(dir,vel); } function onChangeLED(sts) { //CALL FORM SUBMIT changeLED(sts); } function forceSetNeutral() { // https://www.nishishi.com/javascript-tips/radiobutton-alloff.html var masconList = document.getElementsByName("mascon"); for(var i=0; i<masconList.length; i++){ masconList[i].checked = false; } masconList[4].checked = true; } //function clearRadioBtn(nam) { // https://www.nishishi.com/javascript-tips/radiobutton-alloff.html // var radioList = document.getElementsByName(nam); // for(var i=0; i<radioList.length; i++){ // radioList[i].checked = false; // } //} var currID; window.onload = function() { // GET CURRENT MASCON STATUS // http://www.openspc2.org/reibun/JavaScript_technique/sample/03_form/019/index.html var masconList = document.getElementsByName("mascon"); for(var i=0; i<masconList.length; i++){ if (masconList[i].checked) { currID = masconList[i].id; break; } } } </script> <style> body { font-family: Arial, Helvetica, sans-serif; background: #CCC; height: 100%; width: 365px; text-align: center; } div#blk_mascon, div#blk_led, div#blk_cam { background: #EEE; border: 1px solid #666; padding: 10px; text-align: center; margin: 10px; } div#blk_cam { height: 240px; } img#camImg { border: 1px solid #666; } </style> </head> <body> <div id="blk_cam"><img src="{{ url_for('video_feed') }}" id="camImg" /> </div> <form name="controller"> <div id="blk_mascon"> <input type="radio" name="mascon" onclick="onChangePWM('r',4);" id="r4" /><label for="r4">R4</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('r',3);" id="r3" /><label for="r3">R3</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('r',2);" id="r2" /><label for="r2">R2</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('r',1);" id="r1" /><label for="r1">R1</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('n',0);" id="n0" checked/><label for="n0">N</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('f',1);" id="f1" /><label for="p1">P1</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('f',2);" id="f2" /><label for="p2">P2</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('f',3);" id="f3" /><label for="p3">P3</label><br /> <input type="radio" name="mascon" onclick="onChangePWM('f',4);" id="f4" /><label for="p4">P4</label><br /> </div><div id="blk_led" class="btn-group" data-toggle="buttons"> <span>LIGHT: </span> <label><input type="radio" name="led" autocomplete="off" onclick="onChangeLED(0);" id="led_off" {%if led == 1%}checked{%endif%} />OFF</label> <label><input type="radio" name="led" autocomplete="off" onclick="onChangeLED(1);" id="led_on" {%if led == 1%}checked{%endif%} />ON</label> </div></form> <iframe hidden name="hiddeniframe"></iframe> </body> </html> |
これらFLASKアプリケーションのファイル構成は、フレームワーク独特な次のような配置になります。
1 2 3 4 5 6 7 8 9 10 11 |
pi ├── flask │ └── controller │ ├── app.py #アプリ │ ├── camera_pi.py #カメラライブラリ │ ├── static #html静的ファイル置き場 │ │ ├── jnr20.png │ │ ├── light30.png │ │ └── whitenoise.gif │ └── templates │ ├── index.html #htmlファイル |
これを実際にブラウザから呼び出してみると、こうなります。
カメラ映像下に前後進各4段のマスコン、そしてLEDライトのスイッチも配置してみました。html的な装飾はシステムの実機搭載後にやっていこうと思います。