Monitorの製作 (01)

ESP32CAMが技適対応となったのでこれを使って訪問者を監視するMonitorを作ってみたいと思います。

作りたいモニター

  • 玄関に設置して訪問者の画像を家の中からタブレットで確認。
  • カメラ付きインターフォンを目指す(音声は入らないが)
  • 訪問者が来たら人感センサーで感知しメールを送る。
  • ソーラパネルを使って電源を確保する。

ESP32CAMからの画像はブラウザで

ESP32CAM用のサンプルコードがArduinoには用意されています。

 スケッチの例ー>ESP32ー>Cameraー>CameraWebServerでサンプルコードを読み込む事が出来ます。ただこのコードはかなり難しく、これを元に自分なりに変更したのがもう一度サンプルスケッチ(04)に有ります。

もう一度サンプルスケッチ(04)では以下の様なサーバーが上がります。

今回はモニターなので画面の大きさを変える機能以外の設定機能は全て削除します。そして

  • Streamingの開始、停止ボタン
  • 静止画(写真の)の撮影ボタン
  • 撮影した画像のダウンロード

を付け足して以下の様な画面としました。

SDカードは諦める

SDカードが付いているのは有り難いのですが、そのために自由に使えるGPIOピンがほとんど有りません。今後人感センサーを接続する事を踏まえて思い切ってSDカードを使わない事にしました。これで使用出来るGPIOポートが増えました。

写真とHPのHTMLコードは何処に保存する?

SPIFFSを使用する事にします。SPIFFSの容量はSDカードに比べ小さいですが、

  • サーバー用のHTMLコードはサイズが小さい。
  • モニターなので写真も何枚も取る必要は無い。
  • 撮った写真はメールで送信

とすれはSDカードが無くても問題無いと思います。

SPIFFSが使えない?

Arduino IDEでESP32CAM用のボードは ”AI Thinker ESP32-CAM”を指定します。これを設定した所、SPIFFS関係の設定項目、”Partition Scheme…..”がメニューに表示されません。下記 左が ”AI Thinker ESP32-CAM”を選んだ場合、右は ”ESP32 Wrover Module”を選んだ場合です。”ESP32 Wrover Module” にはSPIFFS設定項目が有ります。ちなみに”ESP32 Wrover Module”はもう一度サンプルスケッチ(04)で使用しているESP32CAMとは若干違うカメラ操作の出来るESP32です。

”AI Thinker ESP32-CAM”ではSPIFFSは使えないと思ったのですが、どちらも同じESP32なのだから

  • ソースでカメラ用に設定するピンは ”AI Thinker ESP32-CAM” を使用。
  • IDEのボード設定は ”ESP32 Wrover Module” を使用。

とすれば動くのではと思いやってみたら問題無く動きました。問題は無いと思うのですが。

プログラム

今回書いたソースは下記の3つです。

  • monitor.ino:  メインプログラム
  • monitor.html: サーバーのHTMLファイル
  • monitor.css:  サーバーのCSSファイル

monitor.ino

monitor.ino

#include "esp_camera.h"
#include "Arduino.h"
#include <SPIFFS.h>

#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

// Pin definition for CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM       32
#define RESET_GPIO_NUM      -1
#define XCLK_GPIO_NUM        0
#define SIOD_GPIO_NUM       26
#define SIOC_GPIO_NUM       27

#define Y9_GPIO_NUM         35
#define Y8_GPIO_NUM         34
#define Y7_GPIO_NUM         39
#define Y6_GPIO_NUM         36
#define Y5_GPIO_NUM         21
#define Y4_GPIO_NUM         19
#define Y3_GPIO_NUM         18
#define Y2_GPIO_NUM          5
#define VSYNC_GPIO_NUM      25
#define HREF_GPIO_NUM       23
#define PCLK_GPIO_NUM       22

#define Ready_LED           33

WebServer server(80);
WebServer st_server(81);

const char *SSID = "your SSID";
const char *PASSWORD = "SSID password";

int cam_state[5]={0};
#define ope_stat            1

IPAddress ip(192, 168, 3, 200);           // IP Address
IPAddress gateway(192,168, 3, 1);         // Gateway Address
IPAddress subnet(255, 255, 255, 0);       // Subnet Address

void setup() 
{
    sensor_t * cam_s;
    
    Serial.begin(115200);
    delay(100);

    Serial.println("Connecting to WiFi");
    WiFi.disconnect(true);
    WiFi.softAPdisconnect(true);
    delay(500);

    WiFi.mode(WIFI_STA);
    WiFi.config(ip, gateway, subnet);
    WiFi.begin(SSID, PASSWORD);
    delay(1000);

    // Try forever
    while (WiFi.status() != WL_CONNECTED) 
    {
        Serial.println("...Connecting to WiFi");
        delay(1000);
    }
    Serial.println("Connected");
    Serial.println(SSID);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    server.begin();
    server.on("/", handleRoot);
    server.onNotFound(handleWebRequests);
    Serial.println("HTTP server started");

    st_server.begin();
    st_server.on("/", st_handleRoot);

    pinMode(Ready_LED, OUTPUT);
    digitalWrite (Ready_LED, LOW) ;

    init_cam();
    cam_s = esp_camera_sensor_get();
    cam_state[ope_stat] = 0;
    cam_state[0] = cam_s->status.framesize;

    SPIFFS.begin();  
}

void loop() 
{
    server.handleClient();
    st_server.handleClient();
}

void handleRoot() 
{
    String cmd;
    int a,fl;
    File dataFile;
    camera_fb_t * fb = NULL;
    sensor_t * cam_s;

    fl=1;
    cmd=server.argName(0);
    switch(cmd.toInt())
    {
      case 1: // Get Still
                fb = esp_camera_fb_get();
                dataFile = SPIFFS.open("/data.jpg", FILE_WRITE);
                dataFile.write(fb->buf, fb->len); // payload (image), payload length
                dataFile.close();
                esp_camera_fb_return(fb);
                Serial.println("Take a photo.");
                cam_state[ope_stat] = 2;
                break;

      case 2: //Set Camera Parameter
                Serial.println("Set Camera Parameter");
                cmd=server.arg("2");
                cam_state[0] = cmd.toInt();
                cam_s = esp_camera_sensor_get();
                cam_s->set_framesize(cam_s,(framesize_t)cam_state[0]);        //framesize
                break;

      case 3: //  Stream Start 
                cam_state[ope_stat] = 1;
                break;
                
      case 4: //  Stream Stop 
                Serial.println("Stream Stop.");
                cam_state[ope_stat] = 0;
                break;

      case 80: // Send camera parameter
                cmd="";
                for(a=0; a<2; a++) cmd += (String(cam_state[a]) + ',');
                server.send(200, "text/plain", cmd);
                fl=0;
                break;
    }

    if(fl)
    {
        dataFile = SPIFFS.open("/monitor.html", FILE_READ);
        server.streamFile(dataFile,"text/html");
        dataFile.close();
    }
}

void init_cam()
{
    camera_config_t config;

    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM;
    config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM;
    config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM;
    config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM;
    config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk = XCLK_GPIO_NUM;
    config.pin_pclk = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href = HREF_GPIO_NUM;
    config.pin_sscb_sda = SIOD_GPIO_NUM;
    config.pin_sscb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 20000000;
    config.pixel_format = PIXFORMAT_JPEG;
    config.frame_size = FRAMESIZE_VGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
    config.jpeg_quality = 10;
    config.fb_count = 2;

    // Init Camera
    esp_camera_init(&config);
}

void st_handleRoot() 
{
    WiFiClient client;
    camera_fb_t * fb = NULL;

    client = st_server.client();
    String response = "HTTP/1.1 200 OK\r\n";
    response += "Content-Type: multipart/x-mixed-replace; boundary=--frame\r\n\r\n";
    st_server.sendContent(response);
    while (1)
    {
      fb = esp_camera_fb_get();
      if (!client.connected())
      {
        esp_camera_fb_return(fb);
        break;
      }

      response = "--frame\r\n";
      response += "Content-Type: image/jpeg\r\n\r\n";
      st_server.sendContent(response);

      client.write(fb->buf, fb->len);
      st_server.sendContent("\r\n");
      esp_camera_fb_return(fb);

      if (!client.connected()) break;
    }
}

void handleWebRequests()
{
    String dataType = "text/plain";
    String path;
    File dataFile;
    camera_fb_t * fb;

    path = server.uri();
    if(path.endsWith(".txt")) dataType = "text/plain";
    else if(path.endsWith(".jpg")) dataType = "image/jpeg";
    else if(path.endsWith(".css")) dataType = "text/css";
    else if(path.endsWith(".js")) dataType = "application/javascript";
    else if(path.endsWith(".html")) dataType = "text/html";

    dataFile = SPIFFS.open(path.c_str(), "r");
    server.streamFile(dataFile, dataType);
    dataFile.close();
    delay(5);
}
  • 10から27行: GPIOの定義。”AI Thinker ESP32-CAM”を使用しています。
  • 34,35行: 自分のSSID Passwordを設定して下さい。
  • 40から42行: タブレットで画像を受ける予定ですが、タブレットではDNA機能が使えない為、IPを固定しています。
  • 57行: WiFi.config(ip, gateway, subnet); ー> ここで IPを固定。
  • 83行: カメラの初期設定
  • 88行: SPIFFSの開始
  • 97行: サーバー側の処理
    • 109行 case 1: 写真の撮影
    • 119行 case 2: 解像度の設定
    • 127行 case 3: Streaming開始
    • 131行 case 4: Streaming停止
    • 136行 case 80: クライアントへデータの送信

monitor.html

monito.html

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link rel='stylesheet' type='text/css' href='monitor.css' >
        <title>ESP32 Monitor</title>
    </head>
    <body>
        <section class="main">
            <div style='font-size:40px'><b><i><u>Monitor</u></i></b><br></div>
            <div id="content">
                    <nav id="menu">
                    	<form method='get'>
                    		<sp>
     	                    <button type='submit' name='3'>Start</button><br>
    	                    <button type='submit' name='4' id="st_stop">Stop</button><br>
    	                    <button type='submit' name='1'>Take</button><br>
    	                    <div style = "margin-left: 25%;">
		            		<a style="color:#ffff00;cursor: pointer;" href='./data.jpg' download='data.jpg'>Download</a>
		            		</div>
							</sp>
    	                </form>
                        <div class="input-group">
                        	<br><br>
                            <select id="1">
                                <option value='0' style="display:none">QQVGA(160x120)</option>
                                <option value='1'>QQVGA(160x120)</option>
                                <option value='2'>QCIF(176x144)</option>
                                <option value='3'>HQVGA(240x176)</option>
                                <option value='4'>QVGA(320x240)</option>
                                <option value='5'>CIF(400x296)</option>
                                <option value='6'>VGA(640x480)</option>
                                <option value='7'>SVGA(800x600)</option>
                                <option value='8' selected >XGA(1024x768)</option>
                                <option value='9'>SXGA(1280x1024)</option>
                                <option value='10'>UXGA(1600x1200)</option>
                            </select>
                        </div>
                    	<form method='get' id='abc'>
                        	<input name="2" id='123' style="display:none">
                    	</form>
                  </nav>
                <figure> <img id="stream" src=""> </figure>
            </div>
        </section>
      <script>
      	var para = Array(4);
		document.addEventListener('change', function (event) 
		{
    		var targetElement = event.target || event.srcElement;
			document.getElementById('123').value = targetElement.value;
			document.getElementById('abc').submit();
		}
    	,false);

		document.addEventListener('DOMContentLoaded', function (event) 
		{
    		var url = "http://192.168.3.200?80=";
    		var xhr = new XMLHttpRequest();
		var a,b,str;
    		xhr.open('GET', url);
    		xhr.send();
    		xhr.onreadystatechange = function()
    		{
      			if(xhr.readyState === 4 && xhr.status === 200)
      			{
					console.log( xhr.responseText );
			        b=0;
        			for(a = 0; a < 2; a++)
        			{
			            para[a] = '';
        			    while( xhr.responseText[b] != ',')
        			    {
        			        para[a] += xhr.responseText[b];
        			        b ++;
        			    }
        			    b ++;
        			}
        			
					document.getElementById("1").selectedIndex=Number(para[0]);
					
					switch(Number(para[1]))
					{
						case 0:	document.getElementById("stream").src = ``;
								break;
						case 1:	document.getElementById("stream").src = `http://192.168.3.200:81/`;
								setTimeout(stream_stop, 240000);
								break;
						case 2:	document.getElementById("stream").src = `./data.jpg`;
								break;
					}
    			}	
  			}
		});

		function stream_stop() 
		{
			window.stop();
    		var url = "http://192.168.3.200?4=";
    		var xhr = new XMLHttpRequest();
    		xhr.open('GET', url);
    		xhr.send();
		}

		//history.pushState(null,null,'/');
		
      </script>
    </body>
</html>

monitor.css

monitor.css

@charset "UTF-8";

body {
        font-family: Arial,Helvetica,sans-serif;
        background: #181818;
        color: #EFEFEF;
        font-size: 16px
}

section.main {
        display: flex
}

#menu,section.main {
        flex-direction: column
}

#menu {
        display: flex;
        flex-wrap: nowrap;
        min-width: 142px;
        background: #363636;
        padding: 8px;
        border-radius: 4px;
        margin-top: -10px;
        margin-right: 10px;
}

#content {
        display: flex;
        flex-wrap: wrap;
        align-items: stretch
}

button {
        display: block;
        margin: 4px;
        padding: 0 5px;
        border: 0;
        cursor: pointer;
        color: #fff;
        background: #ff3034;
        border-radius: 10px;
        font-size: 16px;
        width: 90%;
        height: 35px;
}

figure {
        padding: 0px;
        margin: 0;
        -webkit-margin-before: 0;
        margin-block-start: 0;
        -webkit-margin-after: 0;
        margin-block-end: 0;
        -webkit-margin-start: 0;
        margin-inline-start: 0;
        -webkit-margin-end: 0;
        margin-inline-end: 0
}

figure img {
        display: block;
        width: 100%;
        height: auto;
        border-radius: 4px;
        margin-top: 8px;
}

コンパイルする前に

今回はSPIFFSを使用しているので各ファイルを以下の様に配置します。

  • monitor01というプロジェクト(フォルダー)の中にmonitor.inoとdataフォルダーを保存
  • dataフォルダーの中に、ESP32に書き込むファイルを保存
    • 今回は、monitor.html とmonitor.css

IDEの設定は

  • ボードに”ESP32 Wrover Module”を指定
  • Partition Schemeに”Default 4MB with spiffs(1.2MB APP/1.5MB SPIFSS)”を指定

この設定で、コンパイル、データのUploadを行って下さい。

実行

ブラウザを上げて、IPアドレス欄に 192.168.3.200 と入力して下さい。最初は画像の無い画面が上がりますが、Startボタンを押すとStreamingが始まります。StopボタンはStreaming停止。Takeボタンは写真の撮影。Downloadは写真をダウンロードします。一番下のプルダウンウインドウで画面の解像度を設定出来ます。

次回は

次回は人感センサーとメールの送信機能を追加して行く予定です。今回のプログラムを以下に添付します。

<<Before