サンプルスケッチを参考に製作中のHPのカメラ設定項目を追加して行きます。
サンプルスケッチは複雑なのですが、
サンプルスケッチは複雑なのですが今回必要な部分は、
- カメラ設定用パラメータの読み書き。
- 画像の撮影。
- 動画の配信。
です。これだけならかなり簡単にプログラム出来ます。
先ずはHPのHTMLの入手
サンプルスケッチはHPのHTMLをバイナリー形式でスケッチ内で定義しています。これではHPの修正が困難です。そこで、先ずHPのHTMLを下記の要領で入手します。
- サンプルスケッチをコンパイル実行。
- ブラウザでHPにアクセス。
- ブラウザの機能、”ページのソース”を使って、HPのHTMLのソースを入手。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32 OV2460</title>
<style>
body {
font-family: Arial,Helvetica,sans-serif;
background: #181818;
color: #EFEFEF;
font-size: 16px
}
h2 {
font-size: 18px
}
section.main {
display: flex
}
#menu,section.main {
flex-direction: column
}
#menu {
display: none;
flex-wrap: nowrap;
min-width: 340px;
background: #363636;
padding: 8px;
border-radius: 4px;
margin-top: -10px;
margin-right: 10px;
}
#content {
display: flex;
flex-wrap: wrap;
align-items: stretch
}
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;
}
@media (min-width: 800px) and (orientation:landscape) {
#content {
display:flex;
flex-wrap: nowrap;
align-items: stretch
}
figure img {
display: block;
max-width: 100%;
max-height: calc(100vh - 40px);
width: auto;
height: auto
}
figure {
padding: 0 0 0 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
}
}
section#buttons {
display: flex;
flex-wrap: nowrap;
justify-content: space-between
}
#nav-toggle {
cursor: pointer;
display: block
}
#nav-toggle-cb {
outline: 0;
opacity: 0;
width: 0;
height: 0
}
#nav-toggle-cb:checked+#menu {
display: flex
}
.input-group {
display: flex;
flex-wrap: nowrap;
line-height: 22px;
margin: 5px 0
}
.input-group>label {
display: inline-block;
padding-right: 10px;
min-width: 47%
}
.input-group input,.input-group select {
flex-grow: 1
}
.range-max,.range-min {
display: inline-block;
padding: 0 5px
}
button {
display: block;
margin: 5px;
padding: 0 12px;
border: 0;
line-height: 28px;
cursor: pointer;
color: #fff;
background: #ff3034;
border-radius: 5px;
font-size: 16px;
outline: 0
}
button:hover {
background: #ff494d
}
button:active {
background: #f21c21
}
button.disabled {
cursor: default;
background: #a0a0a0
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
height: 22px;
background: #363636;
cursor: pointer;
margin: 0
}
input[type=range]:focus {
outline: 0
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #EFEFEF;
border-radius: 0;
border: 0 solid #EFEFEF
}
input[type=range]::-webkit-slider-thumb {
border: 1px solid rgba(0,0,30,0);
height: 22px;
width: 22px;
border-radius: 50px;
background: #ff3034;
cursor: pointer;
-webkit-appearance: none;
margin-top: -11.5px
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #EFEFEF
}
input[type=range]::-moz-range-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #EFEFEF;
border-radius: 0;
border: 0 solid #EFEFEF
}
input[type=range]::-moz-range-thumb {
border: 1px solid rgba(0,0,30,0);
height: 22px;
width: 22px;
border-radius: 50px;
background: #ff3034;
cursor: pointer
}
input[type=range]::-ms-track {
width: 100%;
height: 2px;
cursor: pointer;
background: 0 0;
border-color: transparent;
color: transparent
}
input[type=range]::-ms-fill-lower {
background: #EFEFEF;
border: 0 solid #EFEFEF;
border-radius: 0
}
input[type=range]::-ms-fill-upper {
background: #EFEFEF;
border: 0 solid #EFEFEF;
border-radius: 0
}
input[type=range]::-ms-thumb {
border: 1px solid rgba(0,0,30,0);
height: 22px;
width: 22px;
border-radius: 50px;
background: #ff3034;
cursor: pointer;
height: 2px
}
input[type=range]:focus::-ms-fill-lower {
background: #EFEFEF
}
input[type=range]:focus::-ms-fill-upper {
background: #363636
}
.switch {
display: block;
position: relative;
line-height: 22px;
font-size: 16px;
height: 22px
}
.switch input {
outline: 0;
opacity: 0;
width: 0;
height: 0
}
.slider {
width: 50px;
height: 22px;
border-radius: 22px;
cursor: pointer;
background-color: grey
}
.slider,.slider:before {
display: inline-block;
transition: .4s
}
.slider:before {
position: relative;
content: "";
border-radius: 50%;
height: 16px;
width: 16px;
left: 4px;
top: 3px;
background-color: #fff
}
input:checked+.slider {
background-color: #ff3034
}
input:checked+.slider:before {
-webkit-transform: translateX(26px);
transform: translateX(26px)
}
select {
border: 1px solid #363636;
font-size: 14px;
height: 22px;
outline: 0;
border-radius: 5px
}
.image-container {
position: relative;
min-width: 160px
}
.close {
position: absolute;
right: 5px;
top: 5px;
background: #ff3034;
width: 16px;
height: 16px;
border-radius: 100px;
color: #fff;
text-align: center;
line-height: 18px;
cursor: pointer
}
.hidden {
display: none
}
</style>
</head>
<body>
<section class="main">
<div id="logo">
<label for="nav-toggle-cb" id="nav-toggle">☰ Toggle OV2640 settings</label>
</div>
<div id="content">
<div id="sidebar">
<input type="checkbox" id="nav-toggle-cb" checked="checked">
<nav id="menu">
<div class="input-group" id="framesize-group">
<label for="framesize">Resolution</label>
<select id="framesize" class="default-action">
<option value="10">UXGA(1600x1200)</option>
<option value="9">SXGA(1280x1024)</option>
<option value="8">XGA(1024x768)</option>
<option value="7">SVGA(800x600)</option>
<option value="6">VGA(640x480)</option>
<option value="5" selected="selected">CIF(400x296)</option>
<option value="4">QVGA(320x240)</option>
<option value="3">HQVGA(240x176)</option>
<option value="0">QQVGA(160x120)</option>
</select>
</div>
<div class="input-group" id="quality-group">
<label for="quality">Quality</label>
<div class="range-min">10</div>
<input type="range" id="quality" min="10" max="63" value="10" class="default-action">
<div class="range-max">63</div>
</div>
<div class="input-group" id="brightness-group">
<label for="brightness">Brightness</label>
<div class="range-min">-2</div>
<input type="range" id="brightness" min="-2" max="2" value="0" class="default-action">
<div class="range-max">2</div>
</div>
<div class="input-group" id="contrast-group">
<label for="contrast">Contrast</label>
<div class="range-min">-2</div>
<input type="range" id="contrast" min="-2" max="2" value="0" class="default-action">
<div class="range-max">2</div>
</div>
<div class="input-group" id="saturation-group">
<label for="saturation">Saturation</label>
<div class="range-min">-2</div>
<input type="range" id="saturation" min="-2" max="2" value="0" class="default-action">
<div class="range-max">2</div>
</div>
<div class="input-group" id="special_effect-group">
<label for="special_effect">Special Effect</label>
<select id="special_effect" class="default-action">
<option value="0" selected="selected">No Effect</option>
<option value="1">Negative</option>
<option value="2">Grayscale</option>
<option value="3">Red Tint</option>
<option value="4">Green Tint</option>
<option value="5">Blue Tint</option>
<option value="6">Sepia</option>
</select>
</div>
<div class="input-group" id="awb-group">
<label for="awb">AWB</label>
<div class="switch">
<input id="awb" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="awb"></label>
</div>
</div>
<div class="input-group" id="awb_gain-group">
<label for="awb_gain">AWB Gain</label>
<div class="switch">
<input id="awb_gain" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="awb_gain"></label>
</div>
</div>
<div class="input-group" id="wb_mode-group">
<label for="wb_mode">WB Mode</label>
<select id="wb_mode" class="default-action">
<option value="0" selected="selected">Auto</option>
<option value="1">Sunny</option>
<option value="2">Cloudy</option>
<option value="3">Office</option>
<option value="4">Home</option>
</select>
</div>
<div class="input-group" id="aec-group">
<label for="aec">AEC SENSOR</label>
<div class="switch">
<input id="aec" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="aec"></label>
</div>
</div>
<div class="input-group" id="aec2-group">
<label for="aec2">AEC DSP</label>
<div class="switch">
<input id="aec2" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="aec2"></label>
</div>
</div>
<div class="input-group" id="ae_level-group">
<label for="ae_level">AE Level</label>
<div class="range-min">-2</div>
<input type="range" id="ae_level" min="-2" max="2" value="0" class="default-action">
<div class="range-max">2</div>
</div>
<div class="input-group" id="aec_value-group">
<label for="aec_value">Exposure</label>
<div class="range-min">0</div>
<input type="range" id="aec_value" min="0" max="1200" value="204" class="default-action">
<div class="range-max">1200</div>
</div>
<div class="input-group" id="agc-group">
<label for="agc">AGC</label>
<div class="switch">
<input id="agc" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="agc"></label>
</div>
</div>
<div class="input-group hidden" id="agc_gain-group">
<label for="agc_gain">Gain</label>
<div class="range-min">1x</div>
<input type="range" id="agc_gain" min="0" max="30" value="5" class="default-action">
<div class="range-max">31x</div>
</div>
<div class="input-group" id="gainceiling-group">
<label for="gainceiling">Gain Ceiling</label>
<div class="range-min">2x</div>
<input type="range" id="gainceiling" min="0" max="6" value="0" class="default-action">
<div class="range-max">128x</div>
</div>
<div class="input-group" id="bpc-group">
<label for="bpc">BPC</label>
<div class="switch">
<input id="bpc" type="checkbox" class="default-action">
<label class="slider" for="bpc"></label>
</div>
</div>
<div class="input-group" id="wpc-group">
<label for="wpc">WPC</label>
<div class="switch">
<input id="wpc" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="wpc"></label>
</div>
</div>
<div class="input-group" id="raw_gma-group">
<label for="raw_gma">Raw GMA</label>
<div class="switch">
<input id="raw_gma" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="raw_gma"></label>
</div>
</div>
<div class="input-group" id="lenc-group">
<label for="lenc">Lens Correction</label>
<div class="switch">
<input id="lenc" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="lenc"></label>
</div>
</div>
<div class="input-group" id="hmirror-group">
<label for="hmirror">H-Mirror</label>
<div class="switch">
<input id="hmirror" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="hmirror"></label>
</div>
</div>
<div class="input-group" id="vflip-group">
<label for="vflip">V-Flip</label>
<div class="switch">
<input id="vflip" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="vflip"></label>
</div>
</div>
<div class="input-group" id="dcw-group">
<label for="dcw">DCW (Downsize EN)</label>
<div class="switch">
<input id="dcw" type="checkbox" class="default-action" checked="checked">
<label class="slider" for="dcw"></label>
</div>
</div>
<div class="input-group" id="colorbar-group">
<label for="colorbar">Color Bar</label>
<div class="switch">
<input id="colorbar" type="checkbox" class="default-action">
<label class="slider" for="colorbar"></label>
</div>
</div>
<div class="input-group" id="face_detect-group">
<label for="face_detect">Face Detection</label>
<div class="switch">
<input id="face_detect" type="checkbox" class="default-action">
<label class="slider" for="face_detect"></label>
</div>
</div>
<div class="input-group" id="face_recognize-group">
<label for="face_recognize">Face Recognition</label>
<div class="switch">
<input id="face_recognize" type="checkbox" class="default-action">
<label class="slider" for="face_recognize"></label>
</div>
</div>
<section id="buttons">
<button id="get-still">Get Still</button>
<button id="toggle-stream">Start Stream</button>
<button id="face_enroll" class="disabled" disabled="disabled">Enroll Face</button>
</section>
</nav>
</div>
<figure>
<div id="stream-container" class="image-container hidden">
<div class="close" id="close-stream">×</div>
<img id="stream" src="">
</div>
</figure>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function (event) {
var baseHost = document.location.origin
var streamUrl = baseHost + ':81'
const hide = el => {
el.classList.add('hidden')
}
const show = el => {
el.classList.remove('hidden')
}
const disable = el => {
el.classList.add('disabled')
el.disabled = true
}
const enable = el => {
el.classList.remove('disabled')
el.disabled = false
}
const updateValue = (el, value, updateRemote) => {
updateRemote = updateRemote == null ? true : updateRemote
let initialValue
if (el.type === 'checkbox') {
initialValue = el.checked
value = !!value
el.checked = value
} else {
initialValue = el.value
el.value = value
}
if (updateRemote && initialValue !== value) {
updateConfig(el);
} else if(!updateRemote){
if(el.id === "aec"){
value ? hide(exposure) : show(exposure)
} else if(el.id === "agc"){
if (value) {
show(gainCeiling)
hide(agcGain)
} else {
hide(gainCeiling)
show(agcGain)
}
} else if(el.id === "awb_gain"){
value ? show(wb) : hide(wb)
} else if(el.id === "face_recognize"){
value ? enable(enrollButton) : disable(enrollButton)
}
}
}
function updateConfig (el) {
let value
switch (el.type) {
case 'checkbox':
value = el.checked ? 1 : 0
break
case 'range':
case 'select-one':
value = el.value
break
case 'button':
case 'submit':
value = '1'
break
default:
return
}
const query = `${baseHost}/control?var=${el.id}&val=${value}`
fetch(query)
.then(response => {
console.log(`request to ${query} finished, status: ${response.status}`)
})
}
document
.querySelectorAll('.close')
.forEach(el => {
el.onclick = () => {
hide(el.parentNode)
}
})
// read initial values
fetch(`${baseHost}/status`)
.then(function (response) {
return response.json()
})
.then(function (state) {
document
.querySelectorAll('.default-action')
.forEach(el => {
updateValue(el, state[el.id], false)
})
})
const view = document.getElementById('stream')
const viewContainer = document.getElementById('stream-container')
const stillButton = document.getElementById('get-still')
const streamButton = document.getElementById('toggle-stream')
const enrollButton = document.getElementById('face_enroll')
const closeButton = document.getElementById('close-stream')
const stopStream = () => {
window.stop();
streamButton.innerHTML = 'Start Stream'
}
const startStream = () => {
view.src = `${streamUrl}/stream`
show(viewContainer)
streamButton.innerHTML = 'Stop Stream'
}
// Attach actions to buttons
stillButton.onclick = () => {
stopStream()
view.src = `${baseHost}/capture?_cb=${Date.now()}`
show(viewContainer)
}
closeButton.onclick = () => {
stopStream()
hide(viewContainer)
}
streamButton.onclick = () => {
const streamEnabled = streamButton.innerHTML === 'Stop Stream'
if (streamEnabled) {
stopStream()
} else {
startStream()
}
}
enrollButton.onclick = () => {
updateConfig(enrollButton)
}
// Attach default on change action
document
.querySelectorAll('.default-action')
.forEach(el => {
el.onchange = () => updateConfig(el)
})
// Custom actions
// Gain
const agc = document.getElementById('agc')
const agcGain = document.getElementById('agc_gain-group')
const gainCeiling = document.getElementById('gainceiling-group')
agc.onchange = () => {
updateConfig(agc)
if (agc.checked) {
show(gainCeiling)
hide(agcGain)
} else {
hide(gainCeiling)
show(agcGain)
}
}
// Exposure
const aec = document.getElementById('aec')
const exposure = document.getElementById('aec_value-group')
aec.onchange = () => {
updateConfig(aec)
aec.checked ? hide(exposure) : show(exposure)
}
// AWB
const awb = document.getElementById('awb_gain')
const wb = document.getElementById('wb_mode-group')
awb.onchange = () => {
updateConfig(awb)
awb.checked ? show(wb) : hide(wb)
}
// Detection and framesize
const detect = document.getElementById('face_detect')
const recognize = document.getElementById('face_recognize')
const framesize = document.getElementById('framesize')
framesize.onchange = () => {
updateConfig(framesize)
if (framesize.value > 5) {
updateValue(detect, false)
updateValue(recognize, false)
}
}
detect.onchange = () => {
if (framesize.value > 5) {
alert("Please select CIF or lower resolution before enabling this feature!");
updateValue(detect, false)
return;
}
updateConfig(detect)
if (!detect.checked) {
disable(enrollButton)
updateValue(recognize, false)
}
}
recognize.onchange = () => {
if (framesize.value > 5) {
alert("Please select CIF or lower resolution before enabling this feature!");
updateValue(recognize, false)
return;
}
updateConfig(recognize)
if (recognize.checked) {
enable(enrollButton)
updateValue(detect, true)
} else {
disable(enrollButton)
}
}
})
</script>
</body>
</html>
このHTMLには、下記が含まれていました。
- 本来のコード
- CSSファイル
- Java Scriptファイル
今回は、これらの部分を、menu.html. menu.css menu.js と3つのファイルに分割しSDカードに保存する事にしました。
スケッチの修正
- カメラ設定パラメータの読込。
- パラメータの読込には、esp_camera_sensor_get() 関数を使います。
- ポインターを取得し、各パラメータを指定しながら値を読込ます。
- 設定読込用関数 void read_State() を作製しています。
void read_State()
{
sensor_t * s = esp_camera_sensor_get();
cam_state[0]= s->status.framesize;
cam_state[1]= s->status.quality;
cam_state[2]= s->status.brightness;
cam_state[3]= s->status.contrast;
cam_state[4]= s->status.saturation;
cam_state[5]= s->status.sharpness;
cam_state[6]= s->status.special_effect;
cam_state[7]= s->status.awb;
cam_state[8]= s->status.awb_gain;
cam_state[9]= s->status.wb_mode;
cam_state[10]= s->status.aec;
cam_state[11]= s->status.aec2;
cam_state[12]= s->status.ae_level;
cam_state[13]= s->status.agc;
cam_state[14]= s->status.agc_gain;
cam_state[15]= s->status.gainceiling;
cam_state[16]= s->status.bpc;
cam_state[17]= s->status.wpc;
cam_state[18]= s->status.raw_gma;
cam_state[19]= s->status.lenc;
cam_state[20]= s->status.hmirror;
cam_state[21]= s->status.vflip;
cam_state[22]= s->status.dcw;
cam_state[23]= s->status.colorbar;
}
- カメラ設定パラメータの書込
- パラメータの書込には読込同様、esp_camera_sensor_get() 関数を使います。
- ポインターを取得し、各パラメータに値を書込ます。
- プログラムでは下記の様に書き込んでいます。
s = esp_camera_sensor_get();
switch(a)
{
case 0: s->set_framesize(s,(framesize_t)cam_state[0]); break; //framesize
case 1: s->set_quality(s,cam_state[1]); break; //quality
case 2: s->set_brightness(s,cam_state[2]); break; //brightness
case 3: s->set_contrast(s,cam_state[3]); break; //contrast
case 4: s->set_saturation(s,cam_state[4]); break; //saturation
case 5: s->set_sharpness(s,cam_state[5]); break; //sharpness
case 6: s->set_special_effect(s,cam_state[6]); break; //special_effect
case 7: s->set_whitebal(s,cam_state[7]); break; //awb
case 8: s->set_awb_gain(s,cam_state[8]); break; //awb_gain
case 9: s->set_wb_mode(s,cam_state[9]); break; //wb_mode
case 10: s->set_aec2(s,cam_state[10]); break; //aec
case 11: s->set_exposure_ctrl(s,cam_state[11]); break; //aec2
case 12: s->set_ae_level(s,cam_state[12]); break; //ae_level
case 13: s->set_gain_ctrl(s,cam_state[13]); break; //agc
case 14: s->set_agc_gain(s,cam_state[14]); break; //agc_gain
case 15: s->set_gainceiling(s,(gainceiling_t)cam_state[15]); break; //gainceiling
case 16: s->set_bpc(s,cam_state[16]); break; //bpc
case 17: s->set_wpc(s,cam_state[17]); break; //wpc
case 18: s->set_raw_gma(s,cam_state[18]); break; //raw_gma
case 19: s->set_lenc(s,cam_state[19]); break; //lenc
case 20: s->set_hmirror(s,cam_state[20]); break; //hmirror
case 21: s->set_vflip(s,cam_state[21]); break; //vflip
case 22: s->set_dcw(s,cam_state[22]); break; //dcw
case 23: s->set_colorbar(s,cam_state[23]); break; //colorba
}
- 写真の撮影
- 写真の撮影は前回と同じ。fb = esp_camera_fb_get();
- 撮影と撮影後のSDカードへの保存は以下の様に行っています。
case 1: // Get Still
fb = esp_camera_fb_get();
dataFile = SD.open("/data.jpg", FILE_WRITE);
dataFile.write(fb->buf, fb->len); // payload (image), payload length
dataFile.close();
esp_camera_fb_return(fb);
Serial.printf("Take a photo.\n");
cam_state[ope_stat] = 2;
break;
- 動画の配信
- 実は前回のHPは動画配信中はHPの他の機能を使う事が出来ませんでした。
- サンプルスケッチを見ると、カメラ操作用と動画配信用にポートを分けている事が分かりました。
- そこで、カメラ操作用にポートの80。動画配信用にポートの81を開放し各々に対してWebサーバーを立ち上げました。
プログラム
今回書いたプログラムは以下の4つ。
- hp_samp.ino : メインのスケッチ
- menu.html : HPのHTML部
- menu.css : HPのCSSファイル
- menu.js : HPのJava Scriptファイル
#include "esp_camera.h"
#include "Arduino.h"
#include "SD.h"
#include "FS.h"
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <ESP32_MailClient.h>
// Pin definition for CAMERA_MODEL_WROVER_KIT
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 21
#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 19
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 5
#define Y2_GPIO_NUM 4
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
// Pin definition for SD Card
#define sd_sck 13
#define sd_mosi 15
#define sd_ss 12
#define sd_miso 14
WebServer server(80);
WebServer st_server(81);
const char *SSID = "aaaaaaaaaa";
const char *PASSWORD = "bbbbbbbbbbbb";
int cam_state[25]={0};
#define ope_stat 24
#define SMTP_HOST "smtp.gmail.com"
#define SMTP_PORT 465
/* The sign in credentials */
#define AUTHOR_EMAIL "cccccccc@gmail.com"
#define AUTHOR_PASSWORD "xxxxxxxxxxxxxxxx"
/* Recipient's email*/
#define RECIPIENT_EMAIL "yyyyyyyyyy"
/* The SMTP Session object used for Email sending */
SMTPData smtpData;
void setup()
{
Serial.begin(115200);
delay(100);
Serial.println("Connecting to WiFi");
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
delay(500);
WiFi.mode(WIFI_STA);
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());
if (MDNS.begin("esp32cam"))
{
Serial.println("MDNS responder started");
}
init_cam();
SPI.begin(sd_sck, sd_miso, sd_mosi, sd_ss);
SD.begin(sd_ss);
server.begin();
server.on("/", handleRoot);
server.onNotFound(handleWebRequests);
Serial.println("HTTP server started");
st_server.begin();
st_server.on("/", st_handleRoot);
read_State();
cam_state[ope_stat] = 0;
}
void loop()
{
server.handleClient();
st_server.handleClient();
}
void send_mail()
{
//send email
Serial.println("Sending email...");
//Set the Email host, port, account and password
smtpData.setLogin("smtp.gmail.com", 465, AUTHOR_EMAIL, AUTHOR_PASSWORD);
//Set the sender name and Email
smtpData.setSender("ESP32-CAM", AUTHOR_EMAIL);
//Set Email priority or importance High, Normal, Low or 1 to 5 (1 is highest)
smtpData.setPriority("Normal");
//Set the subject
smtpData.setSubject("Motion Detected - ESP32-CAM");
//Set the message - normal text or html format
// smtpData.setMessage("<div style=\"color:#003366;font-size:20px;\">Image captured and attached.</div>", true);
smtpData.setMessage("Image captured and attached.", false);
//Add recipients, can add more than one recipient
smtpData.addRecipient(RECIPIENT_EMAIL);
//Add attach files from SD card
smtpData.addAttachFile("/data.jpg");
//Set the storage types to read the attach files (SD is default)
smtpData.setFileStorageType(MailClientStorageType::SD);
smtpData.setSendCallback(sendCallback);
//Start sending Email, can be set callback function to track the status
if (!MailClient.sendMail(smtpData))
Serial.println("Error sending Email, " + MailClient.smtpErrorReason());
//Clear all data from Email object to free memory
smtpData.empty();
}
/* Callback function to get the Email sending status */
void sendCallback(SendStatus msg)
{
//Print the current status
Serial.println(msg.info());
//Do something when complete
if (msg.success())
{
Serial.println("----------------");
}
}
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 read_State()
{
sensor_t * s = esp_camera_sensor_get();
cam_state[0]= s->status.framesize;
cam_state[1]= s->status.quality;
cam_state[2]= s->status.brightness;
cam_state[3]= s->status.contrast;
cam_state[4]= s->status.saturation;
cam_state[5]= s->status.sharpness;
cam_state[6]= s->status.special_effect;
cam_state[7]= s->status.awb;
cam_state[8]= s->status.awb_gain;
cam_state[9]= s->status.wb_mode;
cam_state[10]= s->status.aec;
cam_state[11]= s->status.aec2;
cam_state[12]= s->status.ae_level;
cam_state[13]= s->status.agc;
cam_state[14]= s->status.agc_gain;
cam_state[15]= s->status.gainceiling;
cam_state[16]= s->status.bpc;
cam_state[17]= s->status.wpc;
cam_state[18]= s->status.raw_gma;
cam_state[19]= s->status.lenc;
cam_state[20]= s->status.hmirror;
cam_state[21]= s->status.vflip;
cam_state[22]= s->status.dcw;
cam_state[23]= s->status.colorbar;
}
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 handleRoot()
{
String buf,cmd;
int a,b,fl,c_state[24];
File dataFile;
camera_fb_t * fb = NULL;
sensor_t * s;
fl=1;
cmd=server.argName(0);
switch(cmd.toInt())
{
case 1: // Get Still
fb = esp_camera_fb_get();
dataFile = SD.open("/data.jpg", FILE_WRITE);
dataFile.write(fb->buf, fb->len); // payload (image), payload length
dataFile.close();
esp_camera_fb_return(fb);
Serial.printf("Take a photo.\n");
cam_state[ope_stat] = 2;
break;
case 2: //Set Camera Parameter
buf=server.arg("2");
a=buf.toInt();
if(a < 0)
{
a = -a; a -= 10;
cam_state[a] ^= 1; cam_state[a] &= 1;
}
else
{
a -= 10;
b=0; while(buf[b] != ',') b ++;
b ++;
cmd=buf.substring(b);
cam_state[a]=cmd.toInt();
}
s = esp_camera_sensor_get();
switch(a)
{
case 0: s->set_framesize(s,(framesize_t)cam_state[0]); break; //framesize
case 1: s->set_quality(s,cam_state[1]); break; //quality
case 2: s->set_brightness(s,cam_state[2]); break; //brightness
case 3: s->set_contrast(s,cam_state[3]); break; //contrast
case 4: s->set_saturation(s,cam_state[4]); break; //saturation
case 5: s->set_sharpness(s,cam_state[5]); break; //sharpness
case 6: s->set_special_effect(s,cam_state[6]); break; //special_effect
case 7: s->set_whitebal(s,cam_state[7]); break; //awb
case 8: s->set_awb_gain(s,cam_state[8]); break; //awb_gain
case 9: s->set_wb_mode(s,cam_state[9]); break; //wb_mode
case 10: s->set_aec2(s,cam_state[10]); break; //aec
case 11: s->set_exposure_ctrl(s,cam_state[11]); break; //aec2
case 12: s->set_ae_level(s,cam_state[12]); break; //ae_level
case 13: s->set_gain_ctrl(s,cam_state[13]); break; //agc
case 14: s->set_agc_gain(s,cam_state[14]); break; //agc_gain
case 15: s->set_gainceiling(s,(gainceiling_t)cam_state[15]); break; //gainceiling
case 16: s->set_bpc(s,cam_state[16]); break; //bpc
case 17: s->set_wpc(s,cam_state[17]); break; //wpc
case 18: s->set_raw_gma(s,cam_state[18]); break; //raw_gma
case 19: s->set_lenc(s,cam_state[19]); break; //lenc
case 20: s->set_hmirror(s,cam_state[20]); break; //hmirror
case 21: s->set_vflip(s,cam_state[21]); break; //vflip
case 22: s->set_dcw(s,cam_state[22]); break; //dcw
case 23: s->set_colorbar(s,cam_state[23]); break; //colorba
}
break;
case 3: // Send camera parameter
cmd="";
for(a=0; a<25; a++) cmd += (String(cam_state[a]) + ',');
server.send(200, "text/plain", cmd);
fl=0;
break;
case 4: // Stream Start Stop
if(cam_state[ope_stat] == 1) cam_state[ope_stat] = 0;
else cam_state[ope_stat] = 1;
break;
case 5: // Send mail
send_mail();
break;
}
if(fl)
{
dataFile = SD.open("/menu.html", FILE_READ);
server.streamFile(dataFile,"text/html");
dataFile.close();
}
}
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/jpg";
else if(path.endsWith(".css")) dataType = "text/css";
else if(path.endsWith(".js")) dataType = "application/javascript";
else if(path.endsWith(".png")) dataType = "image/png";
else if(path.endsWith(".html")) dataType = "text/html";
dataFile = SD.open(path.c_str(), "r");
server.streamFile(dataFile, dataType);
dataFile.close();
delay(5);
}
menu.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type='text/javascript' src='menu.js'></script>
<link rel='stylesheet' type='text/css' href='menu.css' >
<title>ESP32CAM</title>
</head>
<body>
<section class="main">
<div style='font-size:40px'><b><i><u>esp32cam</u></i></b><br></div>
<div id="content">
<div id="sidebar">
<nav id="menu">
<div class="input-group">
<label>Resolution</label>
<select id="10" >
<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' selected >VGA(640x480)</option>
<option value='7'>SVGA(800x600)</option>
<option value='8'>XGA(1024x768)</option>
<option value='9'>SXGA(1280x1024)</option>
<option value='10'>UXGA(1600x1200)</option>
</select>
</div>
<div class="input-group">
<label>Quality</label>
<div class="range-min">10</div>
<input type="range" id="11" min="10" max="63" value="10" >
<div class="range-max">63</div>
</div>
<div class="input-group">
<label>Brightness</label>
<div class="range-min">-2</div>
<input type="range" id="12" min="-2" max="2" value="0">
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Contrast</label>
<div class="range-min">-2</div>
<input type="range" id="13" min="-2" max="2" value="0">
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Saturation</label>
<div class="range-min">-2</div>
<input type="range" id="14" min="-2" max="2" value="0" >
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Sharpness</label>
<div class="range-min">-2</div>
<input type="range" id="15" min="-2" max="2" value="0" >
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Special Effect</label>
<select id="16" >
<option value="0" selected="selected">No Effect</option>
<option value="1">Negative</option>
<option value="2">Grayscale</option>
<option value="3">Red Tint</option>
<option value="4">Green Tint</option>
<option value="5">Blue Tint</option>
<option value="6">Sepia</option>
</select>
</div>
<div class="input-group">
<label for="-17">AWB</label>
<div class="switch">
<input id="-17" type="checkbox">
<label class="slider" for="-17"></label>
</div>
</div>
<div class="input-group">
<label for="-18">AWB Gain</label>
<div class="switch">
<input id="-18" type="checkbox">
<label class="slider" for="-18"></label>
</div>
</div>
<div class="input-group">
<label>WB Mode</label>
<select id="19" >
<option value="0" selected="selected">Auto</option>
<option value="1">Sunny</option>
<option value="2">Cloudy</option>
<option value="3">Office</option>
<option value="4">Home</option>
</select>
</div>
<div class="input-group">
<label for="-20">AEC SENSOR</label>
<div class="switch">
<input id="-20" type="checkbox">
<label class="slider" for="-20"></label>
</div>
</div>
<div class="input-group">
<label for="-21">AEC DSP</label>
<div class="switch">
<input id="-21" type="checkbox" >
<label class="slider" for="-21"></label>
</div>
</div>
<div class="input-group">
<label>AE Level</label>
<div class="range-min">-2</div>
<input type="range" id="22" min="-2" max="2" value="0">
<div class="range-max">2</div>
</div>
<div class="input-group">
<label for="-23">AGC</label>
<div class="switch">
<input id="-23" type="checkbox" >
<label class="slider" for="-23"></label>
</div>
</div>
<div class="input-group">
<label>Agc_gain</label>
<div class="range-min">0</div>
<input type="range" id="24" min="0" max="30" value="0">
<div class="range-max">30</div>
</div>
<div class="input-group">
<label>Gain Ceiling</label>
<div class="range-min">2x</div>
<input type="range" id="25" min="0" max="6" value="0">
<div class="range-max">128x</div>
</div>
<div class="input-group">
<label for="-26">BPC</label>
<div class="switch">
<input id="-26" type="checkbox" >
<label class="slider" for="-26"></label>
</div>
</div>
<div class="input-group">
<label for="-27">WPC</label>
<div class="switch">
<input id="-27" type="checkbox">
<label class="slider" for="-27"></label>
</div>
</div>
<div class="input-group">
<label for="-28">Raw GMA</label>
<div class="switch">
<input id="-28" type="checkbox">
<label class="slider" for="-28"></label>
</div>
</div>
<div class="input-group">
<label for="-29">Lens Correction</label>
<div class="switch">
<input id="-29" type="checkbox">
<label class="slider" for="-29"></label>
</div>
</div>
<div class="input-group">
<label for="-30">H-Mirror</label>
<div class="switch">
<input id="-30" type="checkbox">
<label class="slider" for="-30"></label>
</div>
</div>
<div class="input-group">
<label for="-31">V-Flip</label>
<div class="switch">
<input id="-31" type="checkbox">
<label class="slider" for="-31"></label>
</div>
</div>
<div class="input-group">
<label for="-32">DCW (Downsize EN)</label>
<div class="switch">
<input id="-32" type="checkbox">
<label class="slider" for="-32"></label>
</div>
</div>
<div class="input-group">
<label for="-33">Color Bar</label>
<div class="switch">
<input id="-33" type="checkbox" >
<label class="slider" for="-33"></label>
</div>
</div>
<form method='get' id='abc'>
<input name="2" id='123' style="display:none">
</form>
<form method='get'>
<sp>
<button type='submit' name='1' id="get-still" style='float:left' onClick="onBtnStearm()" >Get Still</button>
<button type='submit' name='4' id="st_start" style='float:left' onClick="onBtnStearm()" >Start Stream</button>
<button type='submit' name='5' style='float:right' >Mail</button>
</sp>
</form>
</nav>
</div>
<figure>
<img id="stream" src="">
</figure>
</div>
</section>
</body>
</html>
menu.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: 340px;
background: #363636;
padding: 8px;
border-radius: 4px;
margin-top: -10px;
margin-right: 10px;
}
#content {
display: flex;
flex-wrap: wrap;
align-items: stretch
}
.input-group {
display: flex;
flex-wrap: nowrap;
line-height: 22px;
margin: 5px 0
}
.input-group>label {
display: inline-block;
padding-right: 10px;
min-width: 47%
}
.input-group input,.input-group select {
flex-grow: 1
}
.range-max,.range-min {
display: inline-block;
padding: 0 5px
}
button {
display: block;
margin: 5px;
padding: 0 12px;
border: 0;
line-height: 28px;
cursor: pointer;
color: #fff;
background: #ff3034;
border-radius: 5px;
font-size: 16px;
outline: 0
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
height: 22px;
background: #363636;
cursor: pointer;
margin: 0
}
input[type=range]::-moz-range-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #EFEFEF;
border-radius: 0;
border: 0 solid #EFEFEF
}
input[type=range]::-moz-range-thumb {
border: 1px solid rgba(0,0,30,0);
height: 22px;
width: 22px;
border-radius: 50px;
background: #ff3034;
cursor: pointer
}
input:checked+.slider {
background-color: #ff3034
}
input:checked+.slider:before {
-webkit-transform: translateX(26px);
transform: translateX(26px)
}
.switch {
display: block;
position: relative;
line-height: 22px;
font-size: 16px;
height: 22px
}
.switch input {
outline: 0;
opacity: 0;
width: 0;
height: 0
}
.slider {
width: 50px;
height: 22px;
border-radius: 22px;
cursor: pointer;
background-color: grey
}
.slider,.slider:before {
display: inline-block;
transition: .4s
}
.slider:before {
position: relative;
content: "";
border-radius: 50%;
height: 16px;
width: 16px;
left: 4px;
top: 3px;
background-color: #fff
}
select {
border: 1px solid #363636;
font-size: 14px;
height: 22px;
outline: 0;
border-radius: 5px
}
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;
}
.image-container {
position: relative;
min-width: 160px
}
.hidden {
display: none
}
.close {
position: absolute;
right: 5px;
top: 5px;
background: #ff3034;
width: 16px;
height: 16px;
border-radius: 100px;
color: #fff;
text-align: center;
line-height: 18px;
cursor: pointer
}
input.ip1 {width:30px;}
input.ip2 {width:20px;}
menu.js
var para = Array(25);
document.addEventListener('change', function (event) {
var targetElement = event.target || event.srcElement;
document.getElementById('123').value=targetElement.id + ',' + targetElement.value;
document.getElementById('abc').submit();
}
,false);
document.addEventListener('DOMContentLoaded', function (event) {
var url = "http://esp32cam.local?3=";
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<25; a++)
{
para[a]='';
while( xhr.responseText[b] != ',')
{
para[a] += xhr.responseText[b];
b ++;
}
b ++;
}
for(a=0; a<24; a++)
{
switch(a)
{
case 0:
case 6:
case 9: document.getElementById(String(a + 10)).selectedIndex=Number(para[a]);
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 12:
case 14:
case 15: document.getElementById(String(a + 10)).value = para[a];
break;
default: str=true;
if(para[a] == "0") str=false;
document.getElementById(String(-a - 10)).checked = str;
break;
}
}
switch(Number(para[24]))
{
case 0:
document.getElementById("stream").src = ``;
document.getElementById('st_start').innerHTML = "Start Stream";
break;
case 1:
document.getElementById("stream").src = `http://esp32cam.local:81/`;
document.getElementById('st_start').innerHTML = "Stop Stream";
break;
case 2:
document.getElementById("stream").src = `./data.jpg`;
break;
}
}
}
});
function onBtnStearm() {
if(Number(para[24]) != 0)
{
window.stop();
document.getElementById('st_start').innerHTML = "Start Stream";
}
}
history.pushState(null,null,'/');
ハードウエア
今回使用したハードは前回と同じです。
実行
- menu.html / menu.css / menu.js の3つのファイルをSDカードのルートに保存して下さい。
- Arduino IDEを上げて、hp_samp.inoを作製して下さい。
- シリアルモニタを上げて下さい。
- プログラムをコンパイル後、実行。
- PC側でブラウザを上げて、ブラウザのURL欄に、”esp32cam.local”と入力して下さい。
- こんな感じのHPが表示されます。
今回は、サンプルスケッチの機能の他に、メールを送る機能を追加しています。この様にプログラムを簡素化すると追加機能も付けやすくなります。メール機能に関しては、ー>ここを参照下さい。
今回使用したファイルをここに保存します。
次回は
このESP32cam(WROVER_KIT)でも元祖ESP32camと同じ様な事が出来る事が分かりました。次はこのESP32cam(WROVER_KIT)でもTimelapsをやってみたいと思います。