Raspberry Piを使ってVPN接続 で外部から自宅のLANへの接続が簡単に出来る様になったので、Monitorの製作 (01):画像を受信する。をちょっと発展させてペットモニタカメラを作って見ました。
主な部品
No. | ITEM | 詳細 |
---|---|---|
1 | カメラモジュール付きシンカー,ESP32-CAM〜160度,850nm,デュアルアンテナ, wifi,Bluetooth,ESP32-S =>(技適マークの刻印されたモデルでしたが既に完売の様でリンクが切れていました。) | |
2 | 新しいOV2640カメラモジュールためESP32カムカメラモジュール 2MP 222 200 180 66 120 160度650nm 850nmナイトビジョンdvp 24PIN 0.5ミリメートル | |
3 | 1セットfpvサーボ2軸ジンバルマウントブラケットカメラプラットフォーム防振サポート 2個SG90 9グラムサーボrcモデル飛行機 | |
4 | マイクロUSBシリアルポートESP32-CAM,Wi-Fi,ESP32-CAM-MB mm (CH340/340g), 5V,Bluetoothおよびov2640カードスロット付き | |
5 | ビニールのパック | |
6 | 中部電磁器工業 CEC ブロンズ粘土 [工作用ねんど] | |
7 | Kaito Denshi(海渡電子) ACアダプター 5V 1.5A 7.5W センタープラス スイッチング 5.5mm 2.1 mm DCプラグ PSE RoHS |
回路図
- 電源は5Vを使用しています。2つのサーボモータとESP32-CAMの5V端子に入力しています。
- サーボモータのコントロールはGPIO13,14の2端子で行っています。
- PC本体とTXD,RXD端子で接続。プログラムを書き込み用にリセットとGPIO0端子に回路を追加
現物
- サーボモータの反動が大きく。反動を抑える為に土台にそれなりの重さが必要でした。
- そこでフードパックの中に粘土を入れて土台に使っています。
- レンズは付属のものでは無く160度魚眼レンズを使用しています。映る範囲が広がりました。
- 写真ではアンテナを使用しています。アンテナを使用する場合はESP322-CAMのアンテナ端子の近くに有るチップを変更する必要が有ります。
- 実際の使用時は、PCとの接続用コード、リセットコードはなくなりますが、それでもケーブルが若干目立ちます。
プログラム
使用してプログラムは monitor.ino: 本体 / monitor.html: サーバーHP用HTML / monitor.css: サーバーHP用cssの3つです。
- monitor.ino: 本体
- 基本的に Monitorの製作 (01) と同じです。
- 今回はサーボモータを使用しているのでその部分が追加されています。
- ESP32でサーボモータ(SG90)を使用するには、ESP32Servo.hが必要です。9行でIncludeしています。
- サーボモータの制御は非常に簡単でした。
- 56,57行: Servo型のインスタンスを各モータ用に2つ宣言
- 76,77行: xxx.attach(PIN_No.)で制御するGPIOで指定。
- 78,80行: xxx.write(angle) で回転角度を指定します。回転角度は0から180度の様です。
- サーボモータの情報を読み取る方法が有りません。モーターの位置はホスト側で保存する必要が有ります。
- 意外と回転速度が速いです。速度を落とそうとしましたが良い方法が見つかりませんでした。
#include "esp_camera.h"
#include "Arduino.h"
#include <SPIFFS.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <ESP32Servo.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 flash_port 4
#define r_l_pin 13
#define u_d_pin 14
WebServer server(80);
WebServer st_server(81);
const char *SSID = "xxxxxxxxxx";
const char *PASSWORD = "yyyyyyyyyy";
int cam_state[7];
#define ope_stat 0
#define g_mode 1
#define ud_num 2
#define lr_num 3
#define flash_flg 4
#define motor_ud 0
#define motor_lr 1
#define up_max -70
#define down_max 70
#define right_max 70
#define left_max -70
#define center_p 90
Servo r_l;
Servo u_d;
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;
pinMode(flash_port, OUTPUT);
digitalWrite (flash_port, LOW) ;
Serial.begin(115200);
cam_state[ud_num] = 0; // Up/Down Home position
cam_state[lr_num] = 0; // Left/Right Home position
cam_state[flash_flg] = 0; // LED OFF
r_l.attach(r_l_pin);
u_d.attach(u_d_pin);
r_l.write(center_p);
delay(1000);
u_d.write(center_p);
delay(1000);
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);
init_cam();
cam_s = esp_camera_sensor_get();
cam_state[ope_stat] = 0; // Stream Stop
cam_state[g_mode] = cam_s->status.framesize;
SPIFFS.begin();
digitalWrite (flash_port, HIGH);
delay(100);
digitalWrite (flash_port, LOW);
}
void loop()
{
server.handleClient();
st_server.handleClient();
}
void handleRoot()
{
String cmd,str;
int a,b,c,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");
str = cmd.substring(2);
a = str.toInt();
switch(cmd.charAt(0))
{
case '1': //Change Graphick mode
cam_state[g_mode] = a;
cam_s = esp_camera_sensor_get();
cam_s -> set_framesize(cam_s,(framesize_t)cam_state[g_mode]); //framesize
break;
case '2': //Move
b = cam_state[ud_num];
if(a > 2) b = cam_state[lr_num];
c = 10;
if(a & 1) c = -10;
b += c;
switch(a)
{
case 1: if(b < up_max) b = up_max;
break;
case 2: if(b > down_max) b = down_max;
break;
case 3: if(b < left_max) b = left_max;
break;
case 4: if(b > right_max) b = right_max;
}
if(a > 2)
{
cam_state[lr_num] = b;
r_l.write(b + center_p);
}
else
{
cam_state[ud_num] = b;
u_d.write(b + center_p);
}
break;
case '3': //Flash On, Off
cam_state[flash_flg] = ! cam_state[flash_flg];
digitalWrite (flash_port, cam_state[flash_flg]);
break;
}
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 < 5; a ++) cmd += (String(cam_state[a]) + ',');
Serial.println("data: " + cmd);
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);
}
- monitor.html: サーバーHP用HTML
- Monitorの製作 (01) にサーボモータコントロール用のボタンを追加したのみです。
<!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>
<div style='font-size:35px'><b><i><u>Monitor</u></i></b></div>
<div> .</div>
<div id="content">
<nav id="menu">
<form method='get'>
<button type='submit' name='3'>Start</button>
<button type='submit' name='4'>Stop</button>
<button type='submit' name='1'>Take</button>
<div style = "margin-left: 25%;">
<a style="color:#ffff00;cursor: pointer;font-weight: bold" href='./data.jpg' download='data.jpg'>Download</a>
</div><br>
<div style ="display:flex;margin-left: 25%;">
<button class="btn" type='button' onclick="set_data(1)" >Up</button>
</div>
<div style ="display:flex;">
<button class="btn" type='button' onclick="set_data(3)" >Left</button>
<button class="btn" type='button' onclick="set_data(4)" >Right</button>
</div>
<div style ="display:flex;margin-left: 25%;">
<button class="btn" type='button' onclick="set_data(2)" >Down</button>
</div>
<div style="font-size: 14px;font-weight: bold">
U/D:<input type="text" id="_ud" size="2" readonly>
L/R:<input type="text" id="_lr" size="2" readonly>
<br><br>Light : <input type="radio" id="_flash" style ="margin-left:20%;" onclick="set_data(5)"/>
</div>
<div class="input-group">
<br>
<select id="1">
<option value="0" style="display:none">96x96</option>
<option value="1">QQVGA(160x120)</option>
<option value="2">QCIF(176x144)</option>
<option value="3">HQVGA(240x176)</option>
<option value="4">240x240</option>
<option value="5">QVGA(320x240)</option>
<option value="6">CIF(400x296)</option>
<option value="7">HVGA(480x320)</option>
<option value="8">VGA(640x480)</option>
<option value="9" selected >SVGA(800x600)</option>
<option value="10">XGA(1024x768)</option>
<option value="11">HD(1280x720)</option>
<option value="12">SXGA(1280x1024)</option>
<option value="13">UXGA(1600x1200)</option>
</select>
</div>
</form>
<form method='get' id='abc'>
<input name="2" id='123' style="display:none">
</form>
</nav>
<figure> <img id="stream" src=""> </figure>
</div>
<script>
var para = Array(6);
// 0: state, 1: gmode, 2: ud_num, 3: lr_num, 4: Flash
var cnt_ud;
var cnt_lr;
document.getElementById("1").addEventListener('change', function (event)
{
var targetElement = event.target || event.srcElement;
document.getElementById('123').value = "1," + 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 < 5; a++)
{
para[a] = '';
while( xhr.responseText[b] != ',')
{
para[a] += xhr.responseText[b];
b ++;
}
b ++;
}
switch(Number(para[0]))
{
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;
}
document.getElementById("1").selectedIndex = Number(para[1]);
document.getElementById("_ud").value = para[2];
cnt_ud = Number(para[2]);
document.getElementById("_lr").value = para[3];
cnt_lr = Number(para[3]);
if(para[4] == "1") document.getElementById("_flash").checked = true ;
else document.getElementById("_flash").checked = false ;
}
}
});
function set_data(num)
{
if(num < 5) document.getElementById('123').value = "2," + String(num);
else document.getElementById('123').value = "3,0";
document.getElementById('abc').submit();
}
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: サーバーHP用css
@charset "UTF-8";
body {
font-family: Arial,Helvetica,sans-serif;
background: #181818;
color: #EFEFEF;
font-size: 16px
}
#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: #ff7f50 ;
background: #afeeee ;
border-radius: 10px;
font-size: 16px;
width: 90%;
height: 28px;
font-weight: bold;
border:inset 3px silver;
}
button:active{
background: #ff3034;
}
.btn {
display: block;
margin: 4px;
padding: 0 5px;
border: 0;
cursor: pointer;
color: #ff7f50 ;
background: #afeeee ;
border-radius: 10px;
font-size: 16px;
width: 50%;
height: 24px;
touch-action: none;
user-select: none;
font-weight: bold;
border:inset 2px silver;
}
.btn:active{
background: #ff3034;
}
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を使用している為、moitorフォルダーの下にmonitor.inoとdataフォルダー保存。dataフォルダーの下にmonitor.htmlとmonitor.cssを保存して下さい。後は本体をコンパイル後ESP32に焼く。monitor.htmlとmonitor.cssは ESP32 Sketch Data Upload を使ってESP32に焼きます。
実行
- 電源を入れると機器の初期設定が開始されます。
- 機器がReadyになるとESP32-CAMについているLightが一瞬光ります。
- 光を確認したらブラウザー(FireFox推奨)のURL欄に192.168.3.200と入力します。
- スマホからサーバにアクセスする場合DNSが使えない為サーバのIPアドレスを固定しています。
- HP画面メニュー列にUp / Down / Left / Right のボタンを追加しています。このボタンを押すとカメラの角度が変わります。
- その下のLightボタンがクリックするとESP32-CAMについているLightが点灯します。
- もちろん、Raspberry Piを使ってVPN接続 を使えば外出先(LAN外)からも繋がります。
最後に
出先で自宅のペットを監視したいとはそんなに思っていないのですが、外出先で自宅が見え、カメラが動いたりするとなんだか楽しい気持ちになりました。