IoT何をいまさら(81) PiZeroで撮った写真をNode-REDでWeb表示

Joseph Halfmoon

第78回でPIRセンサでPiCameraのシャッタを押させて写真をとるところまでやっておきながら、その後、Webページとしてブラウザで見れるようにするステップがペンディングになったままでした。今回はサーバー機に送った写真ファイルをNode-REDにhtml出力してもらってブラウザで見るところまでやりたいと思います。

今回の道具立てを概略説明いたしますと、以下のような感じです。

  • Raspberry Zero、人感センサとPiCamera接続、WiFi。
  • Raspberry 3 model B+、Node-RED(とMosquitto MQTT)実行中のサーバ
  • PC、Network経由でNode-REDのエディタやダッシュボードにアクセス

ラズパイZeroは、人感センサ(PIRセンサ)が反応するとPiCameraを使って写真をとり、WiFi経由で、NFSでマウントしているラズパイ3B+のHDDに撮影した写真ファイルを送ります。また、ラズパイZero上の制御ソフト(Pythonスクリプト)は、ラズパイ3B+上のMQTTブローカと「握っていて」自分の動作状況を報告するとともに、指示を受け取れるようになっています(現状、受け取るだけで何もしませんが。)

一方PC上のブラウザからラズパイ3B+上のNode-REDダッシュボードにアクセスすれば、ラズパイZero上の制御ソフトのRunning状況が確認でき、また、ボタンを押して何か指示をラズパイZeroに送ることができます(今はただ受け取るだけで何もしませんが。)また、PC上のブラウザからは所定のNode-RED配下の所定のURLにアクセスすることで、先ほどラズパイZeroが撮影した写真を閲覧することもできる、と。

まずは先っぽのラズパイZero機の様子を上のアイキャッチ画像に掲げました。中央やや右付近の白いケースの中にラズパイZeroが居ます。ケースとカメラとの接続の1件はこちらに。その右側にあるボード上に赤、緑、青のLEDとタクトスイッチ、そしてPIRセンサ接続のためのターミナルが載っています。元々は別なラズパイ1Bに接続していた、のをZeroに移設(スポッと抜いて指しなおすだけ)したものです。右下隅にちょっと切れて写っているのがニッセラ製のPIR(人感)センサです。

制御に使ったPythonのスクリプトは末尾の以下の見出しの下に全文を掲げました。

3色LEDを点滅させつつ人感センサに反応して写真をとるPythonスクリプト

歴史的経緯?と後々いろいろやらせたいこともあり、写真撮影とは関係なく3色Lチカなども制御しており、無駄に長くなっています。写真ファイルの転送そのものはNFSで直接ファイルをサーバ側のHDDに書きこんでいるだけです。しかし、Node-RED側とやりとりするために、MQTTブローカと通信もしています。とりあえず現状は走っているときは Running、止まる時は Stop と言う(Publish)、というのが唯一意味のある「情報交換」です。今回から、MQTTサブスクライブ機能も加えましたが、機能的にはまだ遊んでいます。とりあえず何か指令が来た、ことが分かるだけ。

さてラズパイ3B+上で走っているNode-REDエディタのラズパイZero対応のタブは以下のような感じです。簡単なフローが3つ。

  • 一番上のフロー、トピック PiZero/StatusでMQTTパブリッシュのメッセージを受け取り、ダッシュボードのStatus表示に送る
  • 真ん中のフロー、ダッシュボード上のButtonが押されたら、トピックPiZero/Buttonに押されたことをパブリッシュする(当然、ラズパイZero側ではこれをサブスクライブしているので、伝わる)
  • 一番下のフロー、Node-RED配下の/pizero/photoというurlにアクセスされたら、テンプレートで定義されるhtmlを生成して投げ返す(テンプレート内でラズパイZeroから転送されたファイルをイメージとして読み出すようになっているので、アクセスしたブラウザ上で表示される筈)

という段取り。

NodeRed_PiZero_tab上のフローのうち、上2つは、以前の投稿で何度かやってきているので、新規なのは一番下のみであります。httpの入力ノードはアクセスされるurlの設定のみ。ここでは/Pizero/Photoと設定しました。すると、以下のアドレスにブラウザでアクセスすると画像が見られることになります。

http://サーバのIPアドレス:1880/PiZero/Photo/

templateはそのとき表示するhtmlの雛形を書き入れますが、今回 Payload(GETしたパラメータなど与えられるとこれに載ってくる筈)は使ってないので、ただ写真を表示するためのイメージTAGを置くためのもの。一番の問題は、

ファイルシステム上のどこに置いたらNode-REDのhttpサーバから見えるの?

という問題であります。そんなことはNode-RED日本ユーザ会の方々はオミトオシであります(ちと「を」が気になるのですが。)

設定ファイルをどこにありますか?

設定ファイル settings.js を調べると、中に

httpStatic

という項目があり、ここにパスを記せば、その下のディレクトリ(今回のケースでは images )が見えるようになるのでした。テンプレート・ノードの中身はこちら。

<html>
    <head>
        <title>PiZero Photo</title>
    </head>
    <body>
    <img src="/images/test000.jpg" ALT="m(^^)m">
    </body>
</html>

なお、今回はあまり活躍しませんが、PiZero用に作ったNode-REDダッシュボードの様子がこちら。Zeroの上でスクリプトが走っているときはStopでなく、Runningが表示されます。BUTTONを押しても、「押された」というメッセージがラズパイZeroの標準出力に出力されるだけ。サブスクライブのテストのみ。

PiZeroDashboardテスト自体は、ラズパイZeroにSSHでログインして実施しているので、以下のキャプチャのような感じです。スクリプト自体は、PCの上でVS Code使ってラズパイ3B+上でリモート編集しており、ラズパイZeroはそのファイルを読み出して実行しています。これは前にも書きましたが、残念なことに VS CodeはArm V6のラズパイ1やZeroのリモート接続をサポートしていないためです。

写真をとるトリガが無ければ、無意味に3色のLEDが点滅を繰り返します。ラズパイZeroへ近づくとPIRセンサに捕捉され、写真が撮られます。今回は1枚撮ったら後始末して終了。

PiZero_stdout

なおカメラとPIRセンサは別な方角を向いているので写真は以下のようなしょうもないものです。電波目覚まし時計を写しているので、ちゃんと撮れたということ(撮影時刻)だけは確認できる、と。

PiZero_Photo_Pageまあ一応、一通り動いたけれども、写真の色は悪いし(調整できるのか)、過去の履歴は残らないし、まだいろいろやることはありそうだな。ズルズル行くのはマズイ気もするのですが。

IoT何をいまさら(80) M5Stack、BB風拡張PCA9306搭載 へ戻る

IoT何をいまさら(82) M5Stack、M5ez、簡易ファイラー? へ進む

3色LEDを点滅させつつ人感センサに反応して写真をとるPythonスクリプト
import RPi.GPIO as GPIO
import picamera
import time
import datetime
import paho.mqtt.client as mqtt
import threading

runFlag = True

class Hard:
    photoDir = "Your image directory"
    greenLed = 18
    blueLed = 27
    redLed = 22
    swPin = 23
    pirPin = 24
    ledStat = [False, False, False] # [R, G, B]
    ledNamDic = {redLed:0, greenLed:1, blueLed:2}
    ledIdxDic = {0:redLed, 1:greenLed, 2:blueLed}

def initIO():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(Hard.greenLed, GPIO.OUT)
    GPIO.setup(Hard.blueLed, GPIO.OUT)
    GPIO.setup(Hard.redLed, GPIO.OUT)
    GPIO.setup(Hard.swPin, GPIO.IN)
    GPIO.setup(Hard.pirPin, GPIO.IN)
    ledOFF()

def ledOFF():
    for idx in range(3):
        GPIO.output(Hard.ledIdxDic[idx], Hard.ledStat[idx])
    ledStat = [False, False, False] # [R, G, B]

def toggleLed(ledidx):
    Hard.ledStat[ledidx] = not (Hard.ledStat[ledidx])
    GPIO.output(Hard.ledIdxDic[ledidx], Hard.ledStat[ledidx])

def toggleLedByPinNum(pinN):
    toggleLed(Hard.ledNamDic[pinN])

def blink(pinN, waitS):
    global runFlag
    while runFlag:
        toggleLedByPinNum(pinN)
        time.sleep(waitS)

def takeAphoto():
    global runFlag
    with picamera.PiCamera() as picam:
        picam.rotation = 0
        picam.resolution = (1024, 768)
        picam.start_preview()
        time.sleep(2)
        takeApicture = False
        while not takeApicture:
            if GPIO.input(Hard.swPin) == 0:
                takeApicture = True
            elif GPIO.input(Hard.pirPin) == 0:
                takeApicture = True
            else:
                time.sleep(0.1)
        fnam = "{0}{1}.jpg".format(Hard.photoDir, 'test000')
        picam.capture(fnam)
        picam.stop_preview()
    runFlag = False
    return fnam

def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    client.subscribe("PiZero/Button")

def on_message(client, userdata, msg):
    print(msg.topic + " " + str(msg.payload))

def main():
    global runFlag
    print("Camera & MQTT test on Pi Zero")
    print("Initialize IOs")
    initIO()

    print("Start Threads")
    t1 = threading.Thread(target = blink, args=(Hard.greenLed, 1.0) )
    t2 = threading.Thread(target = blink, args=(Hard.blueLed, 2.0) )
    t3 = threading.Thread(target = blink, args=(Hard.redLed, 3.0) )
    t1.start()
    t2.start()
    t3.start()

    print("Connect MQTT")
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect("YOUR MQTT ADDRESS", 1883, 60)
    client.loop_start()
    client.publish("PiZero/Status", "Running")

    fnam = takeAphoto()
    print("PHOTO: {0}".format(fnam))

    client.publish("PiZero/Status", "Stop")
    client.loop_stop()
    t1.join()
    t2.join()
    t3.join()
    ledOFF()
    GPIO.cleanup()

if __name__ == '__main__':
    main()