ESP32-CAM模組板硬體說明
ESP32-CAM模組板使用者網頁程式設計
Arduino/ESP32-CAM程式設計與分析
實機測試
在上一篇文章中示範了如何把ESP32-CAM設計成一網路IP監視器,而該系統最大的特色,就是可以在本地區域的無線WiFi分享器範圍內使用,也可以在遠端透過網際網路(internet)連線使用,而且不需要透過任何中介的伺服器,只要直接打開手機或電腦的瀏覽器,並輸入對應的IP位址就可以看到ESP32-CAM的畫面了!除此之外,WiFi分享器所連接的網際網路本身也不一定要有固定IP位址,只要可以連上網際網路就可以了。不過唯一的要求,就是所使用的WiFi分享器(Router)必須具有所謂埠轉發(Port Forwarding),或是虛擬伺服器(Virtual Server)的功能。
該系統可說是簡單易用,不過實際使用時有個缺點,就是在晚上或是光線不足的地方畫面就無法看得很清楚;雖然說ESP32-CAM模組板上有內建一顆高亮度的LED,但是在ESP32-CAM官方範例程式「CameraWebServer.ino」中也沒有設計開關這顆高亮度LED的動作,為了能提高ESP32-CAM這塊模組板的效能,決定把「CameraWebServer.ino」這個範例程式改寫一下,讓它也能在操作網頁中去控制這顆LED的亮滅。
圖一、統架構圖
在改寫程式完成後發現,由於這個原廠所提供的範例程式同時使用了兩個內部的通訊埠(分別是網頁用的80和影像串流傳輸用的81),可是一般的WiFi分享器(Router)在使用埠轉發(Port Forwarding),或是虛擬伺服器(Virtual Server)的功能時,只能對應到一個內部的通訊埠,也就是說這個電路就無法直接拿到網際網路上使用了!以上面的【圖一】來說,手機或其他行動通訊裝置只能經由路徑1去連接這塊ESP32-CAM模組板。不過由於一般的WiFi分享器(Router)可以涵蓋範圍也算滿寬廣,這樣的系統在一些家庭或是公司、學校類的場域來說也是可以延伸出一些應用,例如裝在家庭各處具遙控開關的監視器、具影像功能的自走車等。
在本範例中將著重在使用者介面網頁的設計(或指該說改寫),及ESP32-CAM模組板輸出控制等功能方面的介紹,並限制使用在單一個WiFi分享器(Router)可以涵蓋範圍內。
一、ESP32-CAM模組板硬體說明
【圖一.1】是本文所使用的ESP32-CAM開發板硬體接腳外觀圖,由於這塊ESP32-CAM開發板不具備USB🡪RS232的轉接電路,因此必須使用外部的轉接板;接著的【圖一.2】是使用一般常見的FTDI /USB🡪RS232轉接板與ESP32-CAM開發板的接線圖,而【圖一.3】則是兩者之間接腳名稱的對應表。要注意到FTDI /USB🡪RS232轉接板的TXO腳是接到ESP32-CAM開發板的GPIO 3,也就是U0RXD,而RXI則是接到GPIO 1/U0TXD;此外在燒錄程式時還必須先把ESP32-CAM開發板GPIO 0接到地(GND),否則會無法進入燒錄模式。
在【圖一.1】中有標示這顆高亮度內建LED的位置,和控制這顆LED的I/O腳位(即GPIO 4),這在後面的程式中會用到,還請讀者注意!
圖一.1 ESP32-CAM開發模組板接腳圖
圖一.2 ESP32-CAM開發模組板與FTDI燒錄轉接板連接圖
圖一.3 ESP32-CAM開發模組板與FTDI燒錄轉接板連接腳對應表
二、ESP32-CAM模組板使用者網頁程式設計
為了加速設計的過程,因此本範例主要是把「CameraWebServer.ino」這個由樂鑫公司官方所提供的範例程式拿來修改而成;首先就照下面的【圖二.1】開啟ESP32-CAM範例程式「CameraWebServer.ino」流程打開這個程式,這時我們可以在【圖二.2】看到這個程式的外觀,它主要是由4個部分所組成,除了主程式「CameraWebServer」之外,還分成了3個子程式頁面,分別是「app_httpd.cpp」、「camera_index.h 」和「camera_pins.h」。在本範例中除最後一個用來定義ESP32-CAM模組板接腳的引入檔「camera_pins.h」不需要更動之外,另外三個程式都會需要做部分的修改。在這個小節中我們將討論如何修改「camera_index.h 」這個使用者網頁介面程式,以便可以符合我們的功能與動作需求。
圖二.1 開啟ESP32-CAM範例程式「CameraWebServer.ino」流程
圖二.2 ESP32-CAM範例程式「CameraWebServer.ino」主程式及外觀
當我們把「CameraWebServer.ino」這個由樂鑫公司官方所提供的範例程式燒錄進ESP32-CAM模組板並啟動連線之後,在用手機連線上WiFi分享器(Router)和登入ESP32-CAM模組板,便可看見下方【圖二.3】的使用者網頁介面,其實從頁面最上方的標題『Toggle OV2640 settings』便可以知道,這個網頁頁面主要是用來設定OV2640這個鏡頭模組,而不是整個ESP32-CAM模組板,所以找不到開關內建高亮度LED的選項,否則拿這個範例程式來使用是最快的方式。
圖二.3 「camera_index.h」ESP32-CAM/OV2640設定網頁外觀
圖二.4 新舊網頁外觀比較畫面
接著我們來看一下上面的【圖二.4】,畫面右邊是本範例打算使用的網頁介面,它是由左邊那個原始頁面修改而成的,由於原來的頁面有許多設定是針對OV2640這個鏡頭模組而來,在實際使用時並沒有用到,因此只留下了一些筆者認為以後比較有可能用到的設定功能。此外還修改了一些功能及內容,在圖片中標記1~3用藍色方框圈起來的部分,便是修改的部分,其中標記1單純只是修改了這個頁面的抬頭標題而已,而標記2則是原來的「Start Stream」按鍵,只是把按鍵名稱中文化一下,其中的標記3就是本範例想增加的內建高亮度的LED開關,至於詳細的內容與用途會在後面說明。
圖二.5 「camera_index.h 」使用者網頁介面程式內容畫面
接下來就讓我們看一下「CameraWebServer.ino」這個範例程式中的使用者網頁介面程式「camera_index.h 」,當我們點選【圖二.5】中的標記1時會倏然發現,這個網頁程式(即”index_ov2640_html_gz”這個檔案)居然是一整片的16進制碼,而不是一般習見的html網路超連結程式!這是因為它是個一串把原來html程式經過 Gzip 壓縮的十六進制碼,為了要一窺原來的程式,可以利用這個名為[網路大廚](CyberChef) 的應用程式進行反解碼,後面是這個[網路大廚](CyberChef) 的網站網址,就讓我們連線進去看看這個網路程式:
https://gchq.github.io/CyberChef/
下面的【圖二.6】就是這個[網路大廚](CyberChef) 網站的首頁畫面,在這個頁面中主要分成4個部分,分別是:
1、原始的html檔/壓縮編碼檔輸入(Input)區(即【圖二.6】中標記2的區域)
2、壓縮後或反解碼後的壓縮編碼檔/html檔輸出(Output)區(即【圖二.6】
中標記3的區域)
3、所有壓縮/解壓縮動作彙整菜單(Recipe)區(即【圖二.6】中標記1的區域)
4、動作(Operation)選擇區(即【圖二.6】中標記4的區域)
圖二.6 「CyberChef」網頁首頁畫面
這個[網路大廚](CyberChef)程式可以把原來的html網路程式壓縮成16進制碼,也可以把16進制碼反解碼成標準的html程式,因此我們就先把【圖二.5】中標記1的網頁程式”index_ov2640_html_gz[]”這個檔案內由大括弧{------ }所圈起來的所有16進制碼複製後,貼到【圖二.7】中標記1的「input」區域內,這時可發現輸出區(Output)的內容和輸入區是一樣的,然後把游標移到標記2的動作(Operations)區的搜尋(Seach)欄位上。
圖二.7 「CyberChef」網頁首頁畫面
接著如【圖二.8】中標記1所示,在動作(Operations)區搜尋欄位中輸入「Find」這個特徵字串,便可在下方的動作選擇區中看到標記2的『Find/Replace』選項;這時再用滑鼠把這個動作單元往右拉到菜單(Recipe)區內;並在這個動作選項的搜尋欄位(標記3)中輸入要取代的字元即逗點”,”,而要取代的內容就讓它空白著,最後要記得點選標記4的「Global match」這個選項,由於我們預設是自動烘培(即點選了標記5的”Auto Bake”),因此會隨選隨即動作,此時就可以發現輸出區(Output)內原來的16進制碼中所有的逗點”,”都不見了!
圖二.8 加入『Find/Replace』動作選項畫面
接下來照【圖二.9】中標記1所示,在動作(Operations)搜尋(Seach)欄位中輸入「remove」這個特徵字串,這樣便可在下方的動作選擇區中看到標記2的『Remove whitespace』選項;同樣的再用滑鼠把這個動作單元往右拉到菜單(Recipe)區內,並且把「Spaces」「Carriage return(\r)」「Line feeds(\n)」這三個選項點選起來,這時右下方標記4輸出區(Output)內原來的16進制碼中所有的空白、換行(\n)及回頭(\r)字碼都會被移除!
圖二.9 加入『Remove whitespace』動作選項畫面
第三個步驟就是把輸出取的16進制碼中每一個位元組前面單純用來代表16進制用的前置字碼「0x」去除掉,同樣的先在【圖二.10】左邊動作區找到『From Hex』動作選項(即標記1、2處)後,再拉到菜單(Recipe)區內,並在「Delimiter」欄中輸入”0x”字碼,就可以把右下方輸出區(Output)內原來的16進制碼中所有的”0x”字碼給移除。
圖二.10 加入『From Hex』動作選項畫面
接著也就是最後一個動作,就是開始執行反壓縮的動作,一樣的先在【圖二.11】左邊動作區內找到『Gunzip』這個動作選項(即標記1、2處)後,拉到菜單(Recipe)區內,此時馬上就可以在右下方輸出區(Output)中看原始的html網頁程式碼(標記4)了!在看到原始的html程式碼之後,可點選標記5的複製選項,把整個程式給取出來,看使用者喜歡使用那一種的html網頁程式編輯器都可以,只要執行「貼上」的動作就可以了。
圖二.11 加入『Gunzip』動作選項畫面
一般來說只要具備文字編輯能力的軟體,都可以用來編寫網頁的html程式,例如Windows內建的「筆記本」;不過如果想要有好一點的人機介面功能的話,建議可以用「Notepad++」這個軟體程式,下面的【圖二.12】便是以「Notepad++」去開啟前面解壓縮所得到的html程式的畫面。由於原廠的程式長達790行,在此就不表列出來,請讀者指自行反解壓縮後閱覽。
圖二.12 以「Notepad++」軟體開啟網頁介面程式「camera_index.h 」畫面
圖二.13 修改後的使用者設定網頁介面畫面
【圖二.13】是式修改後的設定網頁畫面,在此只留下一些可能會用到功能,而其中最重要的就是標記3這個用來控制ESP32-CAM模組板上內建高亮度LED的開關,後面會對其詳細內容加以說明。
完整的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">☰ ESP32-CAM/OV2640 設定</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" Selected="selected">XGA(1024x768)</option>
- <option value="7">SVGA(800x600)</option>
- <option value="6">VGA(640x480)</option>
- <option value="5">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="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="hmirror-group">
- <label for="hmirror">H-Mirror</label>
- <div class="switch">
- <input id="hmirror" type="checkbox" class="default-action">
- <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">
- <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">LED燈光</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">啟動監視器</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 = '啟動監視器'
- }
- const startStream = () => {
- view.src = `${streamUrl}/stream`
- show(viewContainer)
- streamButton.innerHTML = '停止監視器'
- }
- // 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 === '停止監視器'
- 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)
- }
- }
- })
- document.addEventListener ('keyup', function (el) {
- var baseHost = document.location.origin // establish base host and stream socket
- var streamUrl = baseHost + ':81'
- const query = `${baseHost}/control?var=key&val=${el.keyCode}` // put 'key' and the ASCII value in the query
- fetch(query)// send the query
- .then(response => {
- console.log(`request to ${query} finished, status: ${response.status}`)
- })
- }, false);
- </script>
- </body>
- </html>
修改後的設定網頁介面程式「camera_index.h 」
【圖二.13】中標記1 的[ESP32-CAM/OV2640 設定]標題文字是由下面幾行程式所產生:
334. <div id="logo">
335. <label for="nav-toggle-cb"
id="nav-toggle">☰ ESP32-CAM/OV2640 設定</label>
336. </div>
而標記2的監視器啟動按鈕與標示[啟動監視器]則是下面幾行程式負責:
442. <section id="buttons">
443. <button id="get-still">Get Still</button>
444. <button id="toggle-stream">啟動監視器</button>
445. <button id="face_enroll" class="disabled"
446. disabled="disabled">Enroll Face</button>
447. </section>
至於標記3的[ LED燈光]開關同樣也是改寫原來範例而成的,在此是借用原來名為「colorbar」的這個開關的部分;在下面這段程式中先把它的顯示標籤改名為[LED燈光],然後再配合修改它所對應的ESP32-CAM在Arduino部分程式:
407. <div class="input-group" id="colorbar-group">
408. <label for="colorbar">LED燈光</label>
409. <div class="switch">
410. <input id="colorbar" type="checkbox" class="default-action">
411. <label class="slider" for="colorbar"></label>
412. </div>
413. </div>
在新的html網頁程式完成之後,接下來的工作就是重新再把它壓縮成Gzip 的十六進制碼,要完成這個工作,當然首先還是要開啟前面【圖二.6】的[網路大廚](CyberChef) 網站的首頁畫面,然後如【圖二.14】所示,把修改完後的網頁介面程式「camera_index.h 」複製到標記1的來源程式輸入區中,接著在動作區內也就是左邊標記2、3處搜尋並取出「Gzip」這個動作單元,拖曳到標記4的菜單(Recipe)區後,如果沒有意外,應該就馬上會在標記5的壓縮碼輸出區看到一堆奇怪的字碼出現。
圖二.14 在「CyberChef」網頁中取出「Gzip」動作畫面
為了把原來壓縮出來的16進制碼轉成可用的16進制文字碼,接下來如【圖二.15】所示,在動作區內也就是左邊標記1處搜尋並取出「To Hex」這個動作單元,拖曳到菜單(Recipe)區(標記2)後,然後在其中的[Delimiter]欄位中輸入字碼”0x”,接著搜尋並取出「split」(標記3)這個動作單元,一樣拖曳到菜單區(標記4),並分別在[Delimiter]欄位中輸入字碼”0x”,及 [Join delimiter]欄位中輸入字碼”,0x”,如果一切順利,應該就會在標記5的壓縮碼輸出區看到已經轉換完成的文字型的16進制碼。這時可點選標記6,把完整的輸出結果複製起來,再貼回【圖二.5】中標記1的網頁程式”index_ov2640_html_gz[]”這個檔案內大括弧{------ }所圈起來的空間,就差不多快大功告成了!不過要注意一點就是記得把第一個位元組”,0x1f”最前面的逗號”,”拿掉,否則在編譯時會出現錯誤的訊息。
圖二.15 「CyberChef」網頁中取出「To Hex」與「Split」動作畫面
在Arduino IDE中的網頁程式”index_ov2640_html_gz[]”這個檔案內所看到的壓縮16進制碼,嚴格來說應該是文字碼,也就說一個位元組加上前面的逗號例如第一個位元組的”,0x1f”,其實是佔了5個ASCII碼,這也是為何上面【圖二.15】中標記7的壓縮碼長度(length)值為20055,遠遠大於原始程式中的4316的原因!如果要取得原始壓縮碼的長度值,必須依【圖二.16】中標記1所示,把「To Hex」及[Delimiter]這兩個動作單元右上方的致能(Enable)選擇改為Disable,這時便可看到這個html網頁程式的壓縮碼長度(length)值改為4011,也就是之前看到的1/5大小。最後再把這個值填進”index_ov2640_html_gz[]”檔案內的長度常數變數,也就是下面這一行程式的最後面,這樣整個html網頁程式的Gzip壓縮碼產生過程便大功告成:
#define index_ov2640_html_gz_len 4011
圖二.16 在「CyberChef」網頁中取得Gzip壓縮後真正的程式碼長度畫面
轉換後完整的html網頁程式16進制碼列表如下,在此為了節省篇幅,因此只表列出”index_ov2640_html_gz[]”這個陣列的內容。
//File: index_ov2640.html.gz, Size: 4011
#define index_ov2640_html_gz_len 4011
const uint8_t index_ov2640_html_gz[] = {0x1f,0x8b,0x08,0x00,0x39,0x2d,0x56,0x60,0x00,0xff,0xe5,0x5c,0x5b,0x8f,0xdb,
0xc6,0xf5,0x7f,0x6e,0x3e,0x05,0x97,0x76,0xbc,0x32,0xb2,0xd2,0x4a,0x5a,0x65,
0xbd,0x56,0x56,0x72,0xd7,0xeb,0x75,0x6c,0x20,0xce,0xcd,0xcd,0x05,0x08,0x02,
0x9b,0x22,0x47,0x12,0x63,0x8a,0x54,0x78,0x59,0xed,0xc6,0xd8,0x87,0x3e,0x14,0x6d,0xd1,0xe7,0xb4,0xf8,0xb7,0x7f,0xe0,0x0f,0xb4,0x40,0x91,0x97,0xf4,0x4b,
0xf4,0xb3,0xf8,0x9f,0x7c,0x8c,0xfc,0xe6,0x42,0x72,0x48,0x0e,0x2f,0xd2,0x36,
0xbb,0x69,0x22,0x01,0x12,0xc5,0x99,0x73,0xe6,0xdc,0xe7,0xcc,0x99,0xa1,0x0e,
0xb7,0x2c,0xcf,0x0c,0xcf,0x97,0x44,0x9b,0x87,0x0b,0x67,0xfc,0xda,0x21,0xff,
0xd2,0xf0,0x3a,0x9c,0x13,0xc3,0xe2,0x97,0xec,0xe7,0x82,0x84,0x86,0x66,0xce,
0x0d,0x3f,0x20,0xe1,0x48,0x8f,0xc2,0x69,0xfb,0x40,0xcf,0x37,0xbb,0xc6,0x82,
0x8c,0xf4,0x53,0x9b,0xac,0x96,0x9e,0x1f,0xea,0x9a,0xe9,0xb9,0x21,0x71,0xd1,
0x7d,0x65,0x5b,0xe1,0x7c,0x64,0x91,0x53,0xdb,0x24,0x6d,0xf6,0x63,0xc7,0x76,
0xed,0xd0,0x36,0x9c,0x76,0x60,0x1a,0x0e,0x19,0xf5,0x64,0x5c,0xa1,0x1d,0x3a,
0x64,0x7c,0xf2,0xf4,0xfd,0xbd,0xbe,0xf6,0xde,0xc7,0xfd,0xc1,0x7e,0xf7,0x70,
0x97,0xdf,0x4b,0xfb,0x04,0xe1,0xb9,0xfc,0x9b,0xbe,0x26,0x9e,0x75,0xae,0xbd,
0xcc,0xdc,0xa2,0xaf,0x29,0x88,0x68,0x4f,0x8d,0x85,0xed,0x9c,0x0f,0xb5,0x23,
0x1f,0x63,0xee,0x3c,0x22,0xce,0x29,0x09,0x6d,0xd3,0xd8,0x09,0x0c,0x37,0x68,
0x07,0xc4,0xb7,0xa7,0x6f,0x15,0x00,0x27,0x86,0xf9,0x62,0xe6,0x7b,0x91,0x6b,
0x0d,0xb5,0x1b,0xbd,0x03,0xfa,0x2e,0x76,0x32,0x3d,0xc7,0xf3,0xd1,0x7e,0xf2,
0x90,0xbe,0x8b,0xed,0x6c,0xf4,0xc0,0xfe,0x8a,0x0c,0xb5,0xde,0xfe,0xf2,0x2c,
0xd3,0x7e,0xf1,0x5a,0xe6,0xe7,0xbc,0x5f,0x46,0xbd,0x80,0x3f,0xa8,0x86,0x0f,
0x88,0x19,0xda,0x9e,0xdb,0x59,0x18,0xb6,0xab,0xc0,0x64,0xd9,0xc1,0xd2,0x31,
0x20,0x83,0xa9,0x43,0x2a,0xf1,0xdc,0x58,0x10,0x37,0xda,0xa9,0xc1,0x46,0x91,
0xb4,0x2d,0xdb,0xe7,0xbd,0x86,0x54,0x0e,0xd1,0xc2,0xad,0x45,0x5b,0x45,0x97,0xeb,0xb9,0x44,0x21,0x40,0x3a,0xd0,0xca,0x37,0x96,0xb4,0x03,0xfd,0x2e,0x76,
0x59,0xd8,0x2e,0x37,0xaa,0xa1,0xb6,0x37,0xe8,0x2e,0xcf,0x6a,0x54,0xb9,0xb7,
0x4f,0xdf,0xc5,0x4e,0x4b,0xc3,0xb2,0x6c,0x77,0x36,0xd4,0x20,0x67,0x05,0x0a,
0xcf,0xb7,0x88,0xdf,0xf6,0x0d,0xcb,0x8e,0x82,0xa1,0x36,0x50,0xf5,0x59,0x18,
0xfe,0x0c,0xb4,0x84,0x1e,0x88,0x6d,0xf7,0x94,0x94,0x88,0x2e,0xbe,0x3d,0x9b,
0x87,0x50,0x69,0xa1,0x4f,0x5e,0x68,0xc2,0x85,0xea,0xf4,0x59,0x29,0x37,0xb5,
0xd4,0x0c,0xc7,0x9e,0xb9,0x6d,0x3b,0x24,0x0b,0xb0,0x13,0x84,0x3e,0x09,0xcd,
0x79,0x15,0x29,0x53,0x7b,0x16,0xf9,0x44,0x41,0x48,0x22,0xb7,0x0a,0x86,0xd1,0x58,0x6c,0x6a,0xaf,0xc8,0xe4,0x85,0x1d,0xb6,0x85,0x4c,0x26,0x64,0xea,0xf9,
0xb0,0x73,0x45,0xcf,0xb8,0x87,0xe3,0x99,0x2f,0xda,0x41,0x68,0xf8,0x90,0x5d,
0x3d,0x42,0x63,0x1a,0x12,0xf8,0x66,0x1d,0x3e,0x42,0xad,0xa2,0x1e,0x5b,0xf9,
0xb0,0xa2,0x83,0xed,0x3a,0xb6,0x4b,0x9a,0x93,0x57,0x36,0x6e,0x16,0x1d,0xef,
0xd5,0x40,0x31,0xf6,0x62,0x56,0x65,0x25,0x8c,0xd7,0xe2,0x60,0xc2,0x6f,0x7a,
0xdd,0xee,0xeb,0xc5,0xc6,0x39,0xe1,0x66,0x6a,0x44,0xa1,0x77,0x79,0x8f,0x28,
0xb8,0x55,0x8e,0x8f,0x5f,0x2f,0x88,0x65,0x1b,0x5a,0x4b,0x72,0xe7,0x83,0x2e,
0x6c,0xea,0xb6,0x66,0xb8,0x96,0xd6,0xf2,0x7c,0x1b,0x8e,0x60,0xb0,0x70,0xe3,
0xe0,0x0e,0x26,0x8e,0x25,0xb9,0xad,0x60,0xb9,0xc2,0x67,0x64,0x89,0xa8,0xdd,
0xa6,0x61,0xc8,0x69,0xe4,0x40,0x0a,0x1e,0x6b,0xf5,0xd5,0x44,0x67,0x5c,0xb0,
0x20,0xb1,0x4a,0x77,0x71,0xa7,0x58,0x87,0x98,0x66,0xcd,0x16,0xba,0x9e,0xce,
0xb5,0xb6,0x46,0xa3,0xe4,0x6d,0x35,0x8c,0x40,0xaa,0x56,0x79,0xde,0x28,0xd6,
0x60,0x57,0xcd,0x6a,0x1a,0x3b,0xf8,0x5b,0x65,0x43,0x35,0x51,0x64,0xbd,0x48,
0xb2,0x46,0x34,0x59,0x2b,0xa2,0x34,0x8e,0x2a,0x6b,0x45,0x96,0x75,0xa2,0xcb,
0x1a,0x11,0xa6,0x51,0x94,0xe1,0xea,0xac,0xcf,0x37,0x6e,0x4c,0xa2,0x30,0xf4,
0xdc,0xe0,0x52,0x53,0x54,0x99,0x9f,0x7d,0x11,0x05,0xa1,0x3d,0x3d,0x6f,0x0b,
0x97,0x86,0x9f,0x2d,0x0d,0xa4,0x90,0x13,0x12,0xae,0x08,0xa9,0x4e,0x37,0x5c,
0xe3,0x14,0x71,0x67,0x36,0x73,0x54,0xb6,0x67,0x46,0x7e,0x40,0xf3,0xb6,0xa5,
0x67,0x03,0xb1,0x5f,0x1c,0x38,0xeb,0x83,0x0d,0x07,0x6a,0x9b,0x13,0xc5,0x58,
0x5e,0x14,0x52,0x19,0x2b,0x35,0xe1,0x81,0x1d,0x3b,0xc4,0x30,0x8a,0x36,0xe1,
0x89,0x8a,0x96,0xd8,0x05,0x2b,0xa7,0x85,0x2c,0x5d,0x43,0x73,0x4e,0xcc,0x17,
0xc4,0x7a,0xa3,0x36,0x0d,0xab,0x4b,0x0f,0x3b,0xb6,0xbb,0x8c,0xc2,0x36,0x4d,
0xa7,0x96,0x3f,0x8a,0xce,0x99,0x41,0xc6,0x2c,0xf6,0xfb,0x55,0x49,0xc5,0x9b,
0xcb,0xb3,0x6a,0x21,0xc8,0xc4,0x8e,0x1d,0x63,0x42,0x9c,0x2a,0x92,0x85,0x33,
0x94,0x84,0x5d,0x11,0xab,0xca,0x73,0xb7,0x5c,0x2e,0x3a,0xb8,0xf3,0x7a,0x63,
0x39,0xb2,0xeb,0x9d,0xcc,0xad,0x80,0x38,0x70,0xb0,0xb2,0xd4,0x1b,0x7d,0x56,0xa0,0xa1,0x72,0x00,0xdf,0x70,0x67,0x04,0xb1,0xe0,0x6c,0x27,0xbe,0xac,0x5e,
0x18,0x34,0x62,0x9f,0x86,0x6a,0x88,0xbd,0x6a,0x60,0x1e,0x10,0x36,0x48,0x46,
0x24,0xb5,0x56,0x8e,0xdf,0x53,0x1a,0x05,0xcf,0x47,0x94,0x0e,0x93,0x35,0x29,
0x65,0x7e,0x5f,0x1b,0x11,0xe2,0x95,0xde,0x74,0x5a,0xb7,0x56,0x9c,0x4e,0xf7,
0xba,0x7b,0x83,0xda,0x84,0x49,0xc9,0x65,0x6e,0xbd,0xa8,0x88,0x18,0x49,0x34,
0xa9,0x57,0xc1,0x70,0xee,0x9d,0x12,0x5f,0xa1,0x88,0x1c,0xb9,0x83,0xbb,0x03,
0xab,0x01,0x36,0x03,0xf1,0xfe,0x54,0x15,0x4d,0xb3,0xe8,0xfa,0x3d,0xb3,0x5f,
0x69,0x98,0x1c,0x5d,0x07,0xd6,0x60,0x4c,0x1c,0x62,0x55,0x84,0x67,0x8b,0x4c,0x8d,0xc8,0x09,0x6b,0xe4,0x6d,0x74,0xe9,0xbb,0x6a,0x44,0xe6,0x57,0x9f,0xd1,
0x42,0xc7,0x88,0x79,0xc2,0xe7,0x8a,0x31,0xe3,0xb9,0xd3,0x58,0x2e,0x89,0x81,
0x5e,0x26,0x64,0xac,0x5e,0x92,0x36,0xca,0x99,0xd5,0x81,0xab,0xd1,0x42,0xb4,
0xd6,0x14,0x93,0x6c,0x68,0x2d,0x9e,0x87,0x53,0xcf,0x8c,0x54,0xd3,0x74,0x33,
0x93,0x2a,0xe2,0x1b,0xc6,0x22,0x0b,0x1c,0x9b,0x19,0x76,0xe4,0xba,0x54,0xa3,
0xed,0xd0,0x07,0x9b,0x8a,0x81,0x9a,0x09,0x6e,0x23,0xef,0xcc,0x08,0xb6,0xac,
0x18,0x93,0x73,0x40,0x45,0xa0,0x48,0x62,0x88,0x16,0x78,0x60,0x2a,0x46,0x75,0x39,0xb9,0x84,0xf3,0x68,0xa1,0x4a,0x0c,0xe2,0xc1,0x7a,0x98,0xc5,0xf8,0x70,
0xfe,0x6c,0x62,0xb4,0xba,0x3b,0xdd,0x9d,0x3d,0x7c,0x28,0x12,0xf4,0x6a,0xe3,
0x12,0xe2,0x2d,0xb1,0xbc,0x5c,0xf0,0xa9,0xaf,0x93,0x94,0x85,0xb1,0x5a,0x5d,
0x34,0xf7,0xa4,0x6c,0xc1,0xa4,0xd7,0xa9,0x99,0x58,0x4a,0x4c,0x7a,0x7d,0x43,
0x54,0x58,0xcb,0xba,0x2a,0x5e,0x78,0x5f,0x41,0x98,0x74,0x56,0xfd,0xc5,0x5b,
0xbb,0x24,0x8a,0x9f,0xb5,0xa5,0xaf,0x2d,0x97,0xe0,0xba,0x6d,0x03,0x2b,0xea,
0x52,0xf9,0x88,0x7c,0x06,0x14,0xba,0x58,0x54,0xf9,0x58,0x5d,0x95,0xe6,0x3c,
0x52,0x9f,0x0d,0x64,0x30,0xb5,0x1d,0xa7,0xed,0x78,0xab,0xfa,0x4c,0xa4,0xda,
0x92,0x0b,0x76,0x5a,0x6f,0xf2,0x9b,0x52,0x1b,0x21,0x72,0xfd,0x57,0x50,0xfb,
0x4b,0x9b,0x5a,0x24,0xd7,0xd8,0x6c,0xa2,0xd8,0xc0,0x1e,0x2f,0x37,0x50,0x23,
0x53,0xe2,0x99,0x60,0xe5,0x62,0x2e,0x58,0xd9,0xa8,0x2f,0x6e,0xb0,0xa8,0x5a,
0x7a,0x01,0x76,0xda,0xe8,0x1e,0x8d,0x4f,0x1c,0x94,0x4f,0x4f,0x15,0xf3,0x70,
0x83,0x25,0x77,0xed,0xc2,0x44,0x06,0x6f,0xc2,0x09,0x13,0xdd,0x4f,0xa7,0x5c,
0xd2,0xe1,0xb9,0x43,0x79,0xac,0x56,0x9b,0x75,0x4d,0xba,0x9f,0xf5,0x0c,0x75,
0xa7,0x35,0x22,0x7a,0x1c,0xb4,0x67,0x3e,0x39,0x6f,0xc0,0xcc,0x8e,0xf8,0x1e,
0xf2,0x82,0xe8,0xe6,0x6b,0x7f,0x36,0x01,0x08,0x2b,0xea,0x0c,0x82,0x06,0x43,
0x97,0x0f,0xd9,0xc4,0x1e,0x93,0x72,0x9f,0xae,0x37,0x08,0x37,0x15,0x53,0xa8,
0xda,0x54,0xe3,0xd9,0x57,0xd9,0xe8,0x90,0x29,0x00,0x95,0xbb,0x19,0x2c,0x4f,
0xdd,0xab,0x8e,0x6e,0xb1,0x8a,0x68,0x9d,0xa0,0x36,0x72,0x24,0x55,0xb9,0x72,
0xeb,0x53,0x62,0xa6,0xd1,0x73,0x6d,0xe4,0xe5,0x2a,0x89,0xd3,0x67,0xa6,0x66,
0xf4,0x59,0x88,0x29,0x1f,0xea,0x21,0x9f,0xb6,0xfa,0x10,0x93,0x62,0xc2,0xa8,
0xe8,0x5c,0x5d,0x35,0x2e,0x29,0x6b,0x15,0xa7,0xac,0xd2,0x05,0xb2,0x1c,0x8b,
0x94,0x8a,0xaa,0xf6,0xca,0xaa,0x08,0x53,0xac,0xd1,0x54,0xd7,0xf0,0x16,0x06,
0xd2,0x5e,0x6a,0xae,0xd8,0x26,0x57,0xea,0xaf,0x89,0xb9,0x4b,0x45,0xc3,0xde,
0x3e,0xc2,0x4c,0xe5,0x90,0xa6,0xe3,0x05,0xd5,0x7e,0x65,0x4c,0x20,0xbf,0x28,
0x54,0x0c,0x24,0x4a,0x97,0xca,0xca,0x13,0x33,0x6e,0x65,0x4b,0xa3,0xa9,0xbb,
0xd2,0xa7,0xaa,0xdd,0x31,0x27,0x73,0x24,0xc5,0xca,0x30,0x59,0x55,0x7f,0x0b,
0xc9,0x19,0xd6,0x9b,0x74,0x43,0x0e,0x3b,0x5d,0x88,0x1c,0xaa,0x30,0x9a,0x99,
0xe4,0xe8,0x61,0x8a,0x4b,0x25,0xfc,0x9d,0xb9,0x6d,0x59,0xa4,0xb2,0xca,0x49,
0xd7,0xbc,0x39,0x14,0xc9,0xf9,0x95,0x5d,0xe9,0x00,0xcb,0xe1,0x6e,0x7a,0xd6,
0xe6,0x90,0x9e,0x62,0x91,0xcf,0xb9,0xf0,0x4d,0x16,0xcd,0x74,0x8c,0x20,0x18,
0xe9,0xf4,0x34,0x86,0x74,0x54,0x86,0x75,0xb1,0xec,0x53,0xcd,0xb6,0x46,0xba,
0xe3,0xcd,0xbc,0x5c,0x1b,0x6b,0xe7,0x65,0x6f,0x78,0xea,0x48,0xcf,0x6c,0x09,
0xe8,0x0c,0x2a,0xbd,0xa5,0x8f,0x6f,0xdd,0xb8,0x7b,0xe7,0xce,0xfe,0x5b,0xb7,
0xdc,0x49,0xb0,0x14,0x9f,0xec,0x14,0x4e,0xfb,0xf8,0xe8,0xc9,0x2e,0x4e,0xe2,
0xec,0x0f,0xba,0xda,0xf7,0xdf,0x7c,0xfb,0xea,0x5f,0x7f,0x3d,0xdc,0x65,0x58,
0x73,0x94,0xec,0x82,0x94,0x12,0xe2,0x44,0x44,0x57,0xd1,0x17,0x77,0x09,0x10,0xa4,0x26,0x86,0xaf,0xe8,0xc2,0xba,0xf1,0x7c,0x81,0xa5,0x5a,0x3a,0x8b,0x6c,
0x13,0xef,0x2c,0xcf,0x02,0xe3,0x4a,0x84,0x3d,0xd1,0x8b,0x58,0x65,0x08,0x01,
0xc6,0xc0,0xe9,0x86,0x48,0x49,0x9f,0x84,0x3e,0x21,0x7e,0xa9,0x3e,0xcf,0x87,
0x9e,0xfa,0x38,0xf2,0x44,0x23,0x91,0xb8,0x59,0x8e,0x26,0xaf,0x8a,0x04,0x52,
0x1f,0x7f,0x48,0x98,0xbf,0x42,0xcd,0x4a,0xb1,0x16,0xb0,0x88,0x10,0x9a,0x19,
0x1f,0x6c,0x73,0x12,0x45,0xc9,0xb4,0x4d,0x8b,0xb5,0x5e,0xde,0x56,0x94,0xe8,
0xbc,0x25,0xb3,0xb0,0x53,0xc3,0x89,0x20,0xda,0x5e,0x57,0x1f,0x7f,0xf4,0xe9,
0xdb,0x47,0x2d,0x44,0xa2,0xee,0x59,0xaf,0xdf,0xed,0xde,0x3e,0xdc,0xe5,0x5d,
0xd6,0xc6,0x75,0x57,0x1f,0x3f,0x65,0xa8,0xfa,0x07,0x40,0xd5,0xed,0x0f,0x36,
0x47,0x75,0xa0,0x6b,0x4f,0x19,0xdb,0x54,0xb1,0x5c,0x00,0x54,0xb3,0x0c,0x3b,
0x10,0x9f,0xdd,0xd9,0x3f,0xd8,0x1c,0xf9,0x1d,0xd0,0xf9,0x31,0x30,0xe1,0xb8,
0xc1,0x19,0xb8,0xde,0x1c,0xd1,0xbe,0x3e,0xa6,0x78,0xe0,0x27,0x67,0x83,0x83,
0x4b,0xe0,0x79,0x53,0x1f,0x1f,0x3f,0x7e,0xd8,0x1a,0x80,0x9e,0xfe,0xdd,0xfd,
0xcd,0xf1,0x0c,0xf4,0xf1,0x07,0x94,0xa0,0xbd,0x3e,0x10,0x0d,0x2e,0x41,0xd0,
0x9e,0x3e,0x7e,0xc4,0x30,0x01,0xcb,0x59,0xef,0xce,0x25,0x48,0x82,0x79,0x7d,
0xc0,0x30,0xc1,0xbe,0xa8,0x79,0x35,0xc4,0x84,0x80,0xc9,0x94,0x5e,0xe1,0xa7,
0xc5,0xe8,0xd3,0xd8,0x8d,0xbf,0x8c,0x30,0x77,0x84,0xe7,0x6b,0x3b,0xb1,0x80,
0x03,0x4b,0xfc,0xa2,0x99,0xff,0x4a,0x94,0x24,0xfb,0x72,0xfa,0xb8,0x87,0x53,
0x8e,0x95,0x1c,0x14,0xa2,0x20,0x03,0xce,0x30,0xa0,0xd3,0x54,0x82,0xf9,0x30,
0x3d,0xfb,0x01,0x7b,0xdc,0xd3,0x25,0xbf,0xde,0x28,0x44,0x28,0xa8,0x35,0xce,
0xf4,0xf1,0xfe,0x5e,0x9d,0xbc,0x2f,0xa1,0x8e,0x09,0xcb,0x53,0x5c,0x12,0x04,
0x6b,0x6b,0x24,0x05,0xd5,0xc7,0xf7,0x93,0xeb,0xcb,0xe8,0xa5,0xdd,0xbf,0x84,
0x5e,0x24,0x72,0xb8,0x6a,0xda,0x7d,0xa1,0x1a,0x7c,0x27,0x1e,0xf1,0x9f,0x54,
0x4c,0x1d,0xb5,0x97,0xd1,0x0b,0x9d,0xc4,0x7d,0x23,0x88,0xef,0x35,0xd7,0x4a,
0x0c,0x88,0xb0,0x26,0xae,0xae,0x4d,0x23,0x09,0x29,0x3f,0x03,0x7d,0x04,0x46,
0x18,0xf9,0xec,0x44,0xdc,0xda,0x1a,0x49,0x41,0x31,0xf7,0x25,0xd7,0xd7,0xa6,
0x15,0x89,0x9c,0x9f,0x81,0x5e,0x8c,0xd5,0x64,0x6d,0x85,0x00,0x46,0x1f,0x1f,
0x7d,0x72,0x7f,0x6d,0x15,0xf0,0x72,0x5a,0x93,0x44,0x8f,0xcb,0x5e,0x10,0xa8,
0x17,0x52,0x69,0xb5,0x70,0x9b,0xa6,0xd3,0x0a,0xbe,0x62,0x02,0x59,0xf5,0x41,
0x97,0xd8,0x6c,0xc6,0xe3,0x8f,0xa7,0x9f,0xf9,0xc2,0xf6,0x7d,0xcf,0x5f,0x5b,
0x47,0x02,0x0e,0xb9,0x50,0xfb,0x09,0xbb,0xba,0x12,0x65,0xc5,0xa3,0x36,0x54,
0xd8,0xa5,0xd4,0x93,0x70,0x78,0xdd,0x2a,0x3a,0x9d,0x3a,0xf6,0x72,0x6d,0x05,
0x31,0x28,0xe4,0xe0,0xed,0x87,0xf8,0xbe,0x12,0xe5,0xf0,0x11,0xaf,0x42,0x35,
0x82,0xb7,0xeb,0x56,0x8c,0x65,0xae,0xd6,0x56,0x0b,0x60,0xf4,0xf1,0x83,0xe3,
0x4f,0xb4,0xd6,0x03,0x6f,0x85,0x42,0xf2,0x57,0x44,0x3b,0x79,0x17,0x89,0xff,
0x15,0xe8,0x87,0x0e,0x7d,0x3d,0x91,0x8e,0x31,0x7d,0xdd,0xda,0x62,0x45,0x33,0x14,0x55,0x36,0xc8,0xd8,0x38,0xa0,0x3e,0x7e,0xe7,0xe4,0xc1,0x77,0xbf,0xff,
0xc3,0xab,0xdf,0xfd,0xf1,0x4a,0xf4,0x95,0x8c,0x7b,0x15,0x2e,0x95,0x32,0x79,
0xdd,0x7a,0x9a,0xe2,0x54,0xf3,0x33,0x8b,0x84,0x58,0xe1,0xae,0x5f,0x49,0x4a,
0x61,0xf5,0xf1,0x43,0xfc,0xd0,0x1e,0xb0,0x1f,0x9b,0xe4,0x73,0x9b,0xa8,0x4c,
0x1e,0xff,0x2a,0xb4,0x96,0xe1,0xf7,0x27,0xa1,0x38,0x3c,0x05,0xe7,0xcd,0xdc,
0x8d,0xaa,0x80,0x19,0x70,0xa1,0xbe,0x0f,0xf9,0xef,0xab,0x55,0x60,0x4a,0xc4,
0x95,0xe9,0x50,0xe2,0xfb,0x2a,0xd4,0x18,0x97,0xd2,0xd9,0xba,0x9c,0x3f,0xb3,
0x50,0xa7,0x29,0x71,0x92,0x99,0x42,0xcc,0x08,0xb6,0x9c,0x42,0xec,0xe8,0xeb,
0xe3,0xb7,0x49,0xa8,0x3d,0xa5,0x97,0x87,0xbb,0xbc,0x43,0x73,0x2c,0xa2,0x4c,
0x4d,0x9f,0x16,0x32,0x16,0xfa,0xf8,0xd5,0xd7,0xff,0xf7,0xea,0x4f,0x5f,0x7f,0xf7,0xb7,0x7f,0x7c,0xff,0xcf,0x3f,0xbf,0xfa,0x9f,0x6f,0xd6,0xc7,0xc7,0xe4,0x48,
0x5c,0xdf,0x03,0x5d,0x89,0x9e,0xc4,0xb9,0x5a,0x9d,0xee,0x44,0xb0,0x2b,0xe9,
0xde,0xf8,0x84,0x75,0xd6,0xa8,0xa1,0xd5,0x0f,0x47,0xab,0x6e,0x4c,0x68,0x25,
0x25,0xf4,0x5d,0xd4,0xd0,0x15,0x15,0x7d,0xb5,0x22,0x0e,0xf9,0x93,0x40,0x25,
0xa8,0x92,0x5d,0x00,0x26,0x9a,0x74,0x4b,0x2d,0x61,0x2b,0xbf,0xd5,0xc6,0xf7,
0x5f,0x1a,0x16,0xee,0xd9,0xa6,0x99,0x98,0x12,0xe9,0x65,0xa2,0x81,0x7f,0xff,
0xa5,0xce,0x6c,0xe8,0x73,0x5a,0x29,0x61,0xba,0x16,0xf8,0xe6,0x48,0x2f,0xdb,
0x53,0x28,0xe1,0x7c,0x57,0xc5,0x7a,0xae,0xb3,0x42,0xd6,0x87,0x81,0xe9,0xdb,
0x4b,0xd4,0x3c,0xf1,0x94,0x76,0x84,0x6d,0x8a,0xb0,0x83,0x33,0xf0,0x27,0xa7,
0xb8,0x78,0xc7,0x0e,0xb0,0x9d,0x42,0xfc,0xd6,0xf6,0x83,0xf7,0x9e,0xd0,0x9a,
0x0a,0xbd,0xe7,0x19,0x16,0xb1,0xb6,0x77,0xb4,0x69,0xe4,0x72,0x4b,0x6f,0x11,
0xda,0x97,0x3f,0x23,0x77,0x6a,0xf8,0xd8,0xc8,0x0b,0xc8,0x23,0x2f,0x08,0xb5,
0x91,0x96,0x60,0xc4,0x86,0x3f,0x5b,0x77,0x77,0xf0,0x7c,0x1d,0xce,0x62,0x8a,
0x9e,0x9c,0xd9,0x8f,0x7c,0x07,0x5d,0x13,0xa8,0x37,0xb4,0xed,0xe1,0x41,0x6f,
0x9b,0xee,0x83,0x41,0x0d,0xb8,0x01,0x15,0x10,0x74,0x80,0x8f,0x8f,0xc6,0x62,
0x23,0x8c,0x38,0xd8,0xa0,0x84,0xc8,0x29,0x81,0x94,0xda,0xd6,0x36,0xd7,0xd3,0x36,0xdd,0x14,0xa6,0xfb,0x5f,0x1c,0x32,0x98,0x7b,0xab,0x2a,0x48,0x9f,0x2c,
0x70,0x74,0x3d,0x07,0x9c,0x40,0x0b,0x6b,0xae,0x1d,0x3a,0xb6,0x7a,0x06,0xcf,
0x3a,0x24,0x67,0xce,0x47,0xd8,0xb4,0x8e,0xe8,0xee,0x9c,0x84,0x96,0xb0,0x43,
0xa3,0x4d,0xc8,0xaa,0x44,0x3c,0x35,0x9c,0x20,0x87,0x39,0x5a,0x5a,0xd8,0x1d,
0xff,0x98,0xd6,0x31,0xd0,0xa1,0x45,0x9c,0x1d,0x5e,0xd4,0xd8,0x11,0x2d,0x1f,
0x02,0x6f,0x88,0x87,0x19,0x93,0x51,0xe5,0xdb,0x80,0xc8,0xfe,0x1c,0x69,0x6e,
0x04,0x17,0xbe,0xc7,0x58,0xd0,0x86,0x99,0x56,0x06,0xed,0x20,0x40,0x89,0xe7,0xeb,0xd9,0x98,0xec,0xa6,0x3d,0xa5,0x03,0x77,0xd8,0xd3,0xfe,0x23,0xe0,0xd8,
0x8e,0x03,0xfc,0x76,0xfa,0x10,0xa5,0x0c,0xc4,0xe4,0xd0,0x11,0xa9,0xb0,0x68,
0x67,0x44,0xa3,0x61,0x6b,0x8b,0x5d,0x89,0xbb,0x69,0x37,0x34,0xa5,0x0d,0x17,0x68,0x90,0x76,0xaa,0x8b,0xb8,0x73,0x38,0x62,0xe4,0x12,0x06,0xbe,0xe5,0x4a,
0x29,0xcf,0x48,0xe0,0xd6,0xad,0x2c,0xb6,0x2d,0xb0,0xc3,0xa0,0x52,0x4e,0x78,
0x7f,0x78,0x06,0x3c,0x0f,0x6c,0x8b,0xf3,0x0b,0x82,0x24,0x7b,0xda,0xda,0xca,
0x08,0x3e,0xa1,0x71,0x4a,0x45,0x84,0xa3,0x07,0x54,0x40,0xba,0x41,0x4c,0x3d,
0x69,0x8a,0x99,0xbf,0xc7,0xac,0xbe,0x45,0xce,0xb0,0xdf,0x0e,0x97,0xbe,0x0d,
0xf9,0x53,0x63,0x4e,0x6f,0x88,0xfe,0xe9,0x50,0x32,0xc6,0x59,0x06,0x23,0x65,
0x2c,0x47,0x37,0x3b,0x22,0x41,0xf1,0xcd,0x10,0xe5,0x8e,0x89,0x8d,0x4d,0xeb,
0x99,0x7c,0x9e,0x82,0x0d,0x0e,0x34,0x6f,0xa3,0x39,0xbd,0x9f,0x13,0x75,0xd2,
0xb1,0x04,0x09,0x1b,0xa0,0x88,0xa4,0x92,0xf2,0xd5,0xe4,0x19,0xc5,0xa6,0x12,
0x08,0x43,0xb7,0x9a,0x50,0x51,0xb0,0x51,0x71,0x59,0x85,0x2a,0x37,0xf7,0x2b,
0x10,0x72,0x47,0x6c,0xf1,0x79,0xed,0x3e,0x9b,0xa3,0x28,0x72,0xe1,0x63,0xd9,
0xfb,0xf1,0x48,0xc2,0x60,0x84,0xd1,0x24,0x21,0x50,0xb6,0x03,0x6a,0xff,0xb1,
0xa4,0xa9,0x8b,0xa4,0x86,0x26,0x8e,0xa4,0xc5,0xfe,0x91,0xaa,0xc3,0x44,0xec,
0x93,0x3c,0x65,0x98,0x23,0x55,0x76,0x11,0xd0,0xdd,0x03,0x91,0xe9,0x21,0xb3,0x09,0x42,0x68,0xfc,0xe4,0x20,0xc7,0xc3,0xca,0x8f,0x09,0x12,0x7e,0x8f,0x6f,
0x6b,0xb5,0x71,0x54,0x40,0x8d,0x5d,0x76,0x12,0x15,0x4e,0x3e,0x85,0xe7,0x91,0x46,0x93,0x85,0x1d,0x2a,0x10,0x6e,0x23,0x7c,0xab,0x70,0x89,0xdc,0x2e,0x05,
0xc0,0xd3,0xcc,0x91,0xcf,0x1f,0xb0,0x14,0x5e,0xc8,0x23,0xd9,0x97,0x11,0xf1,
0xcf,0x81,0xe8,0xf9,0xcd,0x97,0xf1,0xbc,0x70,0xb1,0xcb,0x6a,0xea,0x9e,0x73,
0x0f,0x33,0xc7,0xe8,0xe6,0x4b,0xa6,0xea,0x8b,0x5b,0x18,0x12,0x3f,0xd8,0xc0,
0x17,0xcf,0x39,0x8a,0x29,0x7d,0x44,0xba,0xc5,0x50,0xc4,0x7a,0xeb,0x84,0x73,
0xe2,0xb6,0x7c,0x12,0x2c,0x81,0x1e,0x14,0xc6,0x01,0x30,0x1e,0xd1,0x73,0x08,
0xa6,0xa8,0x59,0xeb,0xb9,0x4f,0x00,0x07,0x02,0x42,0x4f,0xbb,0xf9,0x92,0xa1,
0xb8,0xc0,0xa3,0xc5,0xae,0x1d,0xcc,0x89,0xb5,0x83,0xf9,0x0a,0x05,0x64,0x9c,
0x1d,0xb9,0xf9,0x32,0x46,0xd5,0xe1,0xb7,0x2e,0x9e,0x27,0x16,0x92,0x4c,0x22,
0xf1,0xdc,0xc7,0x1a,0x3a,0x0c,0x17,0xdf,0x5b,0xf6,0xfc,0x23,0xc7,0x69,0x6d,
0xf3,0x23,0x36,0x22,0xb6,0x77,0x90,0xaf,0x9e,0x18,0x20,0x5b,0x9e,0x14,0x58,
0xbc,0xf2,0x5c,0xd3,0xb1,0x71,0xba,0x1b,0x01,0x5d,0x8a,0xdc,0x89,0xef,0xa1,
0x07,0x3f,0x32,0xfd,0xae,0x67,0xa5,0x51,0x81,0x4b,0xf4,0x36,0x25,0x63,0x77,
0x17,0x52,0x36,0xac,0x38,0x94,0x71,0x1d,0xd1,0xb3,0x75,0x5c,0x4c,0x19,0x09,0x73,0x66,0x04,0x2f,0x5c,0x66,0xe9,0x2c,0x1f,0xb3,0x9c,0x9a,0x2d,0xd7,0x1e,
0xbe,0x84,0x2c,0xbe,0x08,0x3c,0xb7,0xc5,0x81,0x99,0x18,0x8a,0x38,0xe8,0x00,
0x12,0x82,0x8c,0x88,0xca,0xc4,0x94,0x5d,0x10,0x08,0x79,0x55,0xc8,0x2c,0x8d,
0xcb,0x2c,0x6c,0xb3,0x69,0x90,0x8d,0xfb,0x19,0x33,0x99,0xcf,0x91,0xb9,0xd0,
0xa9,0x53,0x8a,0x48,0x09,0xc9,0xc9,0x4c,0x4a,0xff,0x5c,0x46,0x4e,0x5f,0x90,
0x96,0x9f,0x38,0x84,0x5e,0xde,0x3f,0x7f,0x8c,0x29,0x9f,0x27,0x2e,0x8c,0x96,
0x14,0x80,0x26,0x48,0x3c,0x69,0xac,0x85,0x4c,0x13,0x4c,0x09,0x07,0xcb,0xfb,
0x79,0xbc,0xa9,0xc2,0x90,0x2c,0x11,0x32,0xa0,0x14,0x6b,0x3d,0x6c,0x66,0x61,
0x20,0xc1,0xcb,0xb1,0xae,0x0a,0x5e,0x5a,0x08,0x48,0xd0,0xcc,0x90,0xeb,0x81,
0xe5,0x94,0x18,0xd0,0x12,0xf1,0xde,0xf2,0x29,0xbb,0x9b,0x33,0xf2,0x95,0xed,
0x5a,0xde,0x0a,0x2e,0xe6,0x2d,0x5b,0x62,0x6a,0x95,0x19,0xc5,0x73,0xac,0x10,
0xe0,0xa3,0xdf,0x3c,0x79,0x87,0x86,0x9c,0xdc,0x22,0x87,0x06,0x20,0x29,0x35,
0x62,0x0f,0xb4,0x2b,0x07,0xa1,0x9a,0xeb,0x20,0xdb,0xe6,0xd1,0x26,0xc9,0x48,
0xa9,0x33,0xd0,0xcb,0xe7,0x7c,0x58,0x3a,0xf7,0x64,0x74,0xcc,0x6d,0xa6,0x82,
0x9c,0xdf,0xfe,0xef,0xff,0x7f,0xfb,0xf7,0x22,0x39,0x70,0xc7,0xa3,0x30,0x84,
0xd5,0x6a,0xdc,0x9e,0x03,0x1a,0x6a,0xc4,0x3a,0x11,0xcd,0x92,0x0d,0x94,0x78,
0x7e,0x2a,0x2d,0xe1,0x6a,0x59,0x06,0xa4,0x70,0x69,0x2c,0xe1,0x9e,0xe4,0xde,
0x33,0x73,0x82,0x08,0xf9,0x00,0x0e,0xd0,0xc1,0xb3,0xd0,0xad,0xdb,0x08,0x93,
0xe5,0x2c,0x71,0x91,0xa5,0xfa,0x6c,0x4a,0x04,0x8b,0x45,0x6a,0x6c,0x19,0x19,
0xa9,0xd1,0xc9,0x46,0x7c,0xc2,0x66,0x65,0x9a,0xe3,0x95,0x09,0x97,0x66,0x95,
0x0a,0xf1,0xf2,0x44,0x27,0x83,0x24,0x8d,0x34,0x05,0x82,0x73,0xb9,0x8c,0x64,
0x1f,0x71,0x87,0x98,0x7e,0xd9,0x37,0x4a,0xe8,0xcf,0xa6,0x81,0xb9,0xbc,0x21,
0xa7,0x76,0x11,0xce,0x34,0x7a,0xda,0x6e,0x4e,0x67,0x6a,0x61,0x08,0x4d,0x66,
0x0c,0x65,0x28,0xac,0x9c,0x3a,0xf8,0x08,0x31,0xb5,0xf9,0x74,0x35,0x3b,0x4d,
0x1c,0xe3,0x5f,0x10,0xbc,0x45,0x6c,0x97,0xfc,0x1e,0xcd,0xdd,0x12,0x27,0x42,
0x2e,0x57,0xe5,0xdf,0x68,0x96,0x82,0x82,0x48,0xfc,0x6a,0x00,0x58,0xb6,0xc7,
0xeb,0x59,0x12,0xac,0x94,0x50,0x56,0x46,0x42,0x74,0x33,0x79,0x37,0x09,0x05,0xb0,0x16,0x39,0x57,0xe8,0x09,0xfd,0xb8,0x00,0xa8,0xd9,0x50,0x20,0x91,0x61,
0x49,0x46,0x53,0x92,0x21,0x17,0xb3,0xe3,0x9c,0x35,0x95,0x65,0xc5,0xc5,0x8c,
0x38,0xb1,0x32,0xc8,0xfa,0x44,0xe4,0xf6,0xa9,0x08,0x49,0xb5,0xbc,0x89,0x2c,
0xef,0x78,0x65,0x50,0x03,0xf1,0x8c,0xcd,0xff,0xb2,0xb8,0x48,0x43,0x71,0x11,
0x21,0x2e,0x0a,0x90,0x26,0xa3,0xf5,0xcb,0x94,0xc4,0xfe,0x3f,0xb9,0x9f,0x72,
0xb6,0x9a,0x54,0xd2,0x29,0x96,0x01,0x12,0x7b,0xd5,0x00,0xe8,0xbf,0x40,0xfe,
0x23,0xb3,0xb5,0x9a,0x34,0x63,0x2b,0x5e,0x46,0x50,0x80,0x94,0x2d,0xf5,0x62,
0x23,0x66,0x25,0xa9,0x7c,0xb3,0xff,0x0c,0x4a,0x4e,0x47,0xa6,0x95,0x03,0xd6,
0x5e,0x3b,0x91,0xf2,0x6e,0x12,0x93,0xc9,0x72,0xa5,0x16,0x34,0xe9,0x29,0x41,
0x27,0x74,0x54,0x42,0xc7,0x9d,0xf8,0x0c,0x9c,0xfc,0x6c,0x24,0xac,0xa4,0x77,
0xea,0x38,0x29,0x02,0x9e,0xfc,0x8f,0xb5,0x37,0xf3,0xcb,0x63,0x9e,0x86,0x71,
0x66,0x73,0xc9,0x97,0xdc,0x21,0x61,0x29,0xd3,0x27,0x71,0x10,0x0e,0x5f,0x46,
0x66,0x2d,0x29,0xf8,0xfb,0x3f,0x3f,0x6c,0xe9,0xef,0x3b,0x84,0x2e,0x5d,0xc4,
0xd9,0x56,0x9c,0x7e,0xd4,0x3c,0x5f,0xe3,0x4f,0x69,0x21,0x8d,0x15,0xe7,0x63,
0x35,0xf1,0x08,0x03,0x5b,0x20,0xd2,0x18,0x14,0xce,0xed,0x00,0xf9,0x32,0x3d,
0x3d,0x42,0xb6,0xf4,0xe4,0x29,0x85,0x5a,0xf6,0x78,0x82,0x2c,0x8a,0x02,0x45,
0x71,0x72,0x98,0x54,0x96,0x5b,0x82,0xc7,0x42,0x20,0xaa,0x5a,0x92,0xae,0x21,
0xc2,0xa4,0xf9,0x27,0x2b,0x45,0x35,0x03,0xb5,0x82,0x4c,0xc0,0x52,0x59,0xa6,
0xbc,0x16,0xa4,0xa9,0x5a,0xf7,0x57,0x68,0x94,0x96,0xbf,0x94,0x51,0xbe,0x5c,
0x2b,0x5c,0xe2,0x74,0x62,0x2d,0x2d,0xa9,0x6a,0xad,0xed,0x17,0xe4,0x1c,0xc1,
0x2a,0x53,0x48,0x15,0x85,0x83,0x86,0x55,0x54,0x1a,0x8a,0xb0,0x58,0xa5,0xe2,
0x0d,0xe6,0x0c,0x40,0x9b,0x53,0x08,0x1a,0x94,0x78,0x1e,0x84,0x47,0x5c,0xc0,
0x3b,0x4d,0x28,0x6a,0xcb,0xad,0xbf,0x6a,0xba,0xf4,0x06,0xd9,0x62,0xd5,0x8d,
0x14,0x03,0x3f,0x8e,0x11,0x78,0x2f,0x9e,0xb3,0xb8,0x48,0xb7,0x7a,0x28,0x5b,
0xdb,0x8c,0x04,0xac,0xfd,0xb4,0xa3,0xa7,0xc7,0x8f,0x1f,0x8b,0xba,0x00,0x08,
0xa6,0xb7,0xd8,0x00,0xaf,0x65,0x56,0xe9,0x00,0x0d,0xf0,0xdf,0x50,0x72,0xf3,
0xd5,0x2f,0xdb,0x2f,0x62,0x9b,0x7b,0x2b,0x7d,0x04,0x03,0xd5,0x72,0x51,0x1b,
0xe7,0xbf,0xf8,0x63,0x13,0x78,0x92,0x82,0xfd,0x81,0xe9,0x0f,0xf3,0xbe,0x46,
0x46,0xd8,0x54,0x00,0x00
};
修改後的Camera_index.h / index_ov2640_html_gz[] 部分程式碼
三、Arduino/ESP32-CAM程式設計與分析
修改完Arduino IDE中官方範例程式「CameraWebServer.ino」內的網頁介面程式「camera_index.h 」之後,接下來就是要修改主頁面程式「CameraWebServer」及與網頁部分程式對應的「app_httpd.cpp」,至於「camera_pins.h」主要是定義ESP32-CAM模組板的硬體接腳,在本範例中並不需要更動。
圖三.1 ESP32-CAM範例程式「CameraWebServer.ino」外觀
下面的程式是修改後的主頁面程式「CameraWebServer」列表:
- #include "esp_camera.h"
- #include <WiFi.h>
- #define CAMERA_MODEL_AI_THINKER
- #include "camera_pins.h"
- //用你自己的WiFi分享器資料取代下面的兩個變數:
- const char* ssid = "你的WiFi分享器SSID";
- const char* password = "你的WiFi分享器密碼";
- IPAddress staticIP(192,168,0,166); // 固定式本地IP位址
- IPAddress gateway(192,168,0,1);
- IPAddress subnet(255,255,255,0);
- void startCameraServer();
- char LedSw; // LED開關狀態字元
- int getLedSw(); // 取得LED開關狀態
- int LedLight = 4;// the on-board LED flash
- void setup() {
- Serial.begin(115200);
- Serial.setDebugOutput(true);
- Serial.println();
- pinMode(LedLight, OUTPUT);
- digitalWrite(LedLight, LOW);
- pinMode(33,OUTPUT);
- digitalWrite(33,1);
- 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;
- //init with high specs to pre-allocate larger buffers
- if(psramFound()){
- config.frame_size = FRAMESIZE_UXGA;
- config.jpeg_quality = 10;
- config.fb_count = 2;
- } else {
- config.frame_size = FRAMESIZE_SVGA;
- config.jpeg_quality = 12;
- config.fb_count = 1;
- }
- #if defined(CAMERA_MODEL_ESP_EYE)
- pinMode(13, INPUT_PULLUP);
- pinMode(14, INPUT_PULLUP);
- #endif
- // camera init
- esp_err_t err = esp_camera_init(&config);
- if (err != ESP_OK) {
- Serial.printf("Camera init failed with error 0x%x", err);
- return;
- }
- sensor_t * s = esp_camera_sensor_get();
- //initial sensors are flipped vertically and colors are a bit saturated
- if (s->id.PID == OV3660_PID) {
- s->set_vflip(s, 1);//flip it back
- s->set_brightness(s, 1);//up the blightness just a bit
- s->set_saturation(s, -2);//lower the saturation
- }
- //drop down frame size for higher initial frame rate
- s->set_framesize(s, FRAMESIZE_QVGA);
- #if defined(CAMERA_MODEL_M5STACK_WIDE)
- s->set_vflip(s, 1);
- s->set_hmirror(s, 1);
- #endif
- // Wi-Fi connection
- Serial.print("連接到SSID : ");
- Serial.println(ssid);
- WiFi.config(staticIP,gateway,subnet);
- delay(10);
- WiFi.begin(ssid, password);
- while (WiFi.status() != WL_CONNECTED) {
- delay(500);
- Serial.print(".");
- }
- Serial.println("");
- Serial.println("WiFi connected");
- digitalWrite(33,0);
- Serial.print("Camera Ready! Use 'http://");
- Serial.print(WiFi.localIP());
- Serial.println("' to connect");
- startCameraServer();
- }
- void loop() {
- LedSw=getLedSw();
- if(LedSw == 3)
- digitalWrite(LedLight,HIGH);
- else
- digitalWrite(LedLight,LOW);
- delay(50);
- }
修改後的CameraWebServer主程式部分程式碼
在主頁面程式「CameraWebServer」中共有兩地方需要修改,或者是說新增;首先是下面18~20行的部分,其中的”getLedSw()”是對在「app_httpd.cpp」中實作的副程式預先宣告,而第三個” LedLight = 4”則是定義ESP32-CAM模組板上那顆高亮度LED實際連接的IO腳位號碼(即GPIO4)。
18. char LedSw; // LED開關狀態字元
19. int getLedSw(); // 取得LED開關狀態
20. int LedLight = 4;// the on-board LED flash
在定義好這三個變數之後,接下來就是在原本只有一行的主迴圈(loop())中加上下面這一段程式;一開始利用在「app_httpd.cpp」中實作的副程式”getLedSw()”取得[圖二.13]的使用者設定網頁介面被觸動的元件值,以我們在前面修改的結果來說,也就是原來名為「colorbar」的這個開關的部分,如果取得的結果為”3” (即LedSw == 3),代表開關是撥到”開”的狀態,便會令高亮度LED的腳位輸出高態以點亮LED,反之則熄滅LED(輸出”LOW”),這樣使用者便可以透過使用者設定網頁頁面去開關這顆內建在ESP32-CAM的高亮度LED了。
114. LedSw=getLedSw();
115. if(LedSw == 3)
116. digitalWrite(LedLight,HIGH);
117. else
118. digitalWrite(LedLight,LOW);
119. delay(50);
接著的程式是修改後的「app_httpd.cpp」程式列表:
- #include "esp_http_server.h"
- #include "esp_timer.h"
- #include "esp_camera.h"
- #include "img_converters.h"
- #include "camera_index.h"
- #include "Arduino.h"
- #include "fb_gfx.h"
- #include "fd_forward.h"
- #include "fr_forward.h"
- int LedSwVal; // LED開關狀態值
- #define ENROLL_CONFIRM_TIMES 5
- #define FACE_ID_SAVE_NUMBER 7
- #define FACE_COLOR_WHITE 0x00FFFFFF
- #define FACE_COLOR_BLACK 0x00000000
- #define FACE_COLOR_RED 0x000000FF
- #define FACE_COLOR_GREEN 0x0000FF00
- #define FACE_COLOR_BLUE 0x00FF0000
- #define FACE_COLOR_YELLOW (FACE_COLOR_RED | FACE_COLOR_GREEN)
- #define FACE_COLOR_CYAN (FACE_COLOR_BLUE | FACE_COLOR_GREEN)
- #define FACE_COLOR_PURPLE (FACE_COLOR_BLUE | FACE_COLOR_RED)
- typedef struct {
- size_t size; //number of values used for filtering
- size_t index; //current value index
- size_t count; //value count
- int sum;
- int * values; //array to be filled with values
- } ra_filter_t;
- typedef struct {
- httpd_req_t *req;
- size_t len;
- } jpg_chunking_t;
- #define PART_BOUNDARY "123456789000000000000987654321"
- static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
- static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
- static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
- static ra_filter_t ra_filter;
- httpd_handle_t stream_httpd = NULL;
- httpd_handle_t camera_httpd = NULL;
- static mtmn_config_t mtmn_config = {0};
- static int8_t detection_enabled = 0;
- static int8_t recognition_enabled = 0;
- static int8_t is_enrolling = 0;
- static face_id_list id_list = {0};
- static ra_filter_t * ra_filter_init(ra_filter_t * filter, size_t sample_size){
- memset(filter, 0, sizeof(ra_filter_t));
- filter->values = (int *)malloc(sample_size * sizeof(int));
- if(!filter->values){
- return NULL;
- }
- memset(filter->values, 0, sample_size * sizeof(int));
- filter->size = sample_size;
- return filter;
- }
- static int ra_filter_run(ra_filter_t * filter, int value){
- if(!filter->values){
- return value;
- }
- filter->sum -= filter->values[filter->index];
- filter->values[filter->index] = value;
- filter->sum += filter->values[filter->index];
- filter->index++;
- filter->index = filter->index % filter->size;
- if (filter->count < filter->size) {
- filter->count++;
- }
- return filter->sum / filter->count;
- }
- static void rgb_print(dl_matrix3du_t *image_matrix, uint32_t color, const char * str){
- fb_data_t fb;
- fb.width = image_matrix->w;
- fb.height = image_matrix->h;
- fb.data = image_matrix->item;
- fb.bytes_per_pixel = 3;
- fb.format = FB_BGR888;
- fb_gfx_print(&fb, (fb.width - (strlen(str) * 14)) / 2, 10, color, str);
- }
- static int rgb_printf(dl_matrix3du_t *image_matrix, uint32_t color, const char *format, ...){
- char loc_buf[64];
- char * temp = loc_buf;
- int len;
- va_list arg;
- va_list copy;
- va_start(arg, format);
- va_copy(copy, arg);
- len = vsnprintf(loc_buf, sizeof(loc_buf), format, arg);
- va_end(copy);
- if(len >= sizeof(loc_buf)){
- temp = (char*)malloc(len+1);
- if(temp == NULL) {
- return 0;
- }
- }
- vsnprintf(temp, len+1, format, arg);
- va_end(arg);
- rgb_print(image_matrix, color, temp);
- if(len > 64){
- free(temp);
- }
- return len;
- }
- static void draw_face_boxes(dl_matrix3du_t *image_matrix, box_array_t *boxes, int face_id){
- int x, y, w, h, i;
- uint32_t color = FACE_COLOR_YELLOW;
- if(face_id < 0){
- color = FACE_COLOR_RED;
- } else if(face_id > 0){
- color = FACE_COLOR_GREEN;
- }
- fb_data_t fb;
- fb.width = image_matrix->w;
- fb.height = image_matrix->h;
- fb.data = image_matrix->item;
- fb.bytes_per_pixel = 3;
- fb.format = FB_BGR888;
- for (i = 0; i < boxes->len; i++){
- // rectangle box
- x = (int)boxes->box[i].box_p[0];
- y = (int)boxes->box[i].box_p[1];
- w = (int)boxes->box[i].box_p[2] - x + 1;
- h = (int)boxes->box[i].box_p[3] - y + 1;
- fb_gfx_drawFastHLine(&fb, x, y, w, color);
- fb_gfx_drawFastHLine(&fb, x, y+h-1, w, color);
- fb_gfx_drawFastVLine(&fb, x, y, h, color);
- fb_gfx_drawFastVLine(&fb, x+w-1, y, h, color);
- #if 0
- // landmark
- int x0, y0, j;
- for (j = 0; j < 10; j+=2) {
- x0 = (int)boxes->landmark[i].landmark_p[j];
- y0 = (int)boxes->landmark[i].landmark_p[j+1];
- fb_gfx_fillRect(&fb, x0, y0, 3, 3, color);
- }
- #endif
- }
- }
- static int run_face_recognition(dl_matrix3du_t *image_matrix, box_array_t *net_boxes){
- dl_matrix3du_t *aligned_face = NULL;
- int matched_id = 0;
- aligned_face = dl_matrix3du_alloc(1, FACE_WIDTH, FACE_HEIGHT, 3);
- if(!aligned_face){
- Serial.println("Could not allocate face recognition buffer");
- return matched_id;
- }
- if (align_face(net_boxes, image_matrix, aligned_face) == ESP_OK){
- if (is_enrolling == 1){
- int8_t left_sample_face = enroll_face(&id_list, aligned_face);
- if(left_sample_face == (ENROLL_CONFIRM_TIMES - 1)){
- Serial.printf("Enrolling Face ID: %d\n", id_list.tail);
- }
- Serial.printf("Enrolling Face ID: %d sample %d\n", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
- rgb_printf(image_matrix, FACE_COLOR_CYAN, "ID[%u] Sample[%u]", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
- if (left_sample_face == 0){
- is_enrolling = 0;
- Serial.printf("Enrolled Face ID: %d\n", id_list.tail);
- }
- } else {
- matched_id = recognize_face(&id_list, aligned_face);
- if (matched_id >= 0) {
- Serial.printf("Match Face ID: %u\n", matched_id);
- rgb_printf(image_matrix, FACE_COLOR_GREEN, "Hello Subject %u", matched_id);
- } else {
- Serial.println("No Match Found");
- rgb_print(image_matrix, FACE_COLOR_RED, "Intruder Alert!");
- matched_id = -1;
- }
- }
- } else {
- Serial.println("Face Not Aligned");
- //rgb_print(image_matrix, FACE_COLOR_YELLOW, "Human Detected");
- }
- dl_matrix3du_free(aligned_face);
- return matched_id;
- }
- static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len){
- jpg_chunking_t *j = (jpg_chunking_t *)arg;
- if(!index){
- j->len = 0;
- }
- if(httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK){
- return 0;
- }
- j->len += len;
- return len;
- }
- static esp_err_t capture_handler(httpd_req_t *req){
- camera_fb_t * fb = NULL;
- esp_err_t res = ESP_OK;
- int64_t fr_start = esp_timer_get_time();
- fb = esp_camera_fb_get();
- if (!fb) {
- Serial.println("Camera capture failed");
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- httpd_resp_set_type(req, "image/jpeg");
- httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
- httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
- size_t out_len, out_width, out_height;
- uint8_t * out_buf;
- bool s;
- bool detected = false;
- int face_id = 0;
- if(!detection_enabled || fb->width > 400){
- size_t fb_len = 0;
- if(fb->format == PIXFORMAT_JPEG){
- fb_len = fb->len;
- res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
- } else {
- jpg_chunking_t jchunk = {req, 0};
- res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL;
- httpd_resp_send_chunk(req, NULL, 0);
- fb_len = jchunk.len;
- }
- esp_camera_fb_return(fb);
- int64_t fr_end = esp_timer_get_time();
- Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000));
- return res;
- }
- dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
- if (!image_matrix) {
- esp_camera_fb_return(fb);
- Serial.println("dl_matrix3du_alloc failed");
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- out_buf = image_matrix->item;
- out_len = fb->width * fb->height * 3;
- out_width = fb->width;
- out_height = fb->height;
- s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf);
- esp_camera_fb_return(fb);
- if(!s){
- dl_matrix3du_free(image_matrix);
- Serial.println("to rgb888 failed");
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config);
- if (net_boxes){
- detected = true;
- if(recognition_enabled){
- face_id = run_face_recognition(image_matrix, net_boxes);
- }
- draw_face_boxes(image_matrix, net_boxes, face_id);
- free(net_boxes->score);
- free(net_boxes->box);
- free(net_boxes->landmark);
- free(net_boxes);
- }
- jpg_chunking_t jchunk = {req, 0};
- s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk);
- dl_matrix3du_free(image_matrix);
- if(!s){
- Serial.println("JPEG compression failed");
- return ESP_FAIL;
- }
- int64_t fr_end = esp_timer_get_time();
- Serial.printf("FACE: %uB %ums %s%d\n", (uint32_t)(jchunk.len), (uint32_t)((fr_end - fr_start)/1000), detected?"DETECTED ":"", face_id);
- return res;
- }
- static esp_err_t stream_handler(httpd_req_t *req){
- camera_fb_t * fb = NULL;
- esp_err_t res = ESP_OK;
- size_t _jpg_buf_len = 0;
- uint8_t * _jpg_buf = NULL;
- char * part_buf[64];
- dl_matrix3du_t *image_matrix = NULL;
- bool detected = false;
- int face_id = 0;
- int64_t fr_start = 0;
- int64_t fr_ready = 0;
- int64_t fr_face = 0;
- int64_t fr_recognize = 0;
- int64_t fr_encode = 0;
- static int64_t last_frame = 0;
- if(!last_frame) {
- last_frame = esp_timer_get_time();
- }
- res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
- if(res != ESP_OK){
- return res;
- }
- httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
- while(true){
- detected = false;
- face_id = 0;
- fb = esp_camera_fb_get();
- if (!fb) {
- Serial.println("Camera capture failed");
- res = ESP_FAIL;
- } else {
- fr_start = esp_timer_get_time();
- fr_ready = fr_start;
- fr_face = fr_start;
- fr_encode = fr_start;
- fr_recognize = fr_start;
- if(!detection_enabled || fb->width > 400){
- if(fb->format != PIXFORMAT_JPEG){
- bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
- esp_camera_fb_return(fb);
- fb = NULL;
- if(!jpeg_converted){
- Serial.println("JPEG compression failed");
- res = ESP_FAIL;
- }
- } else {
- _jpg_buf_len = fb->len;
- _jpg_buf = fb->buf;
- }
- } else {
- image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
- if (!image_matrix) {
- Serial.println("dl_matrix3du_alloc failed");
- res = ESP_FAIL;
- } else {
- if(!fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item)){
- Serial.println("fmt2rgb888 failed");
- res = ESP_FAIL;
- } else {
- fr_ready = esp_timer_get_time();
- box_array_t *net_boxes = NULL;
- if(detection_enabled){
- net_boxes = face_detect(image_matrix, &mtmn_config);
- }
- fr_face = esp_timer_get_time();
- fr_recognize = fr_face;
- if (net_boxes || fb->format != PIXFORMAT_JPEG){
- if(net_boxes){
- detected = true;
- if(recognition_enabled){
- face_id = run_face_recognition(image_matrix, net_boxes);
- }
- fr_recognize = esp_timer_get_time();
- draw_face_boxes(image_matrix, net_boxes, face_id);
- free(net_boxes->score);
- free(net_boxes->box);
- free(net_boxes->landmark);
- free(net_boxes);
- }
- if(!fmt2jpg(image_matrix->item, fb->width*fb->height*3, fb->width, fb->height, PIXFORMAT_RGB888, 90, &_jpg_buf, &_jpg_buf_len)){
- Serial.println("fmt2jpg failed");
- res = ESP_FAIL;
- }
- esp_camera_fb_return(fb);
- fb = NULL;
- } else {
- _jpg_buf = fb->buf;
- _jpg_buf_len = fb->len;
- }
- fr_encode = esp_timer_get_time();
- }
- dl_matrix3du_free(image_matrix);
- }
- }
- }
- if(res == ESP_OK){
- size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
- res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
- }
- if(res == ESP_OK){
- res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
- }
- if(res == ESP_OK){
- res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
- }
- if(fb){
- esp_camera_fb_return(fb);
- fb = NULL;
- _jpg_buf = NULL;
- } else if(_jpg_buf){
- free(_jpg_buf);
- _jpg_buf = NULL;
- }
- if(res != ESP_OK){
- break;
- }
- int64_t fr_end = esp_timer_get_time();
- int64_t ready_time = (fr_ready - fr_start)/1000;
- int64_t face_time = (fr_face - fr_ready)/1000;
- int64_t recognize_time = (fr_recognize - fr_face)/1000;
- int64_t encode_time = (fr_encode - fr_recognize)/1000;
- int64_t process_time = (fr_encode - fr_start)/1000;
- int64_t frame_time = fr_end - last_frame;
- last_frame = fr_end;
- frame_time /= 1000;
- uint32_t avg_frame_time = ra_filter_run(&ra_filter, frame_time);
- Serial.printf("MJPG: %uB %ums (%.1ffps), AVG: %ums (%.1ffps), %u+%u+%u+%u=%u %s%d\n",
- (uint32_t)(_jpg_buf_len),
- (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time,
- avg_frame_time, 1000.0 / avg_frame_time,
- (uint32_t)ready_time, (uint32_t)face_time, (uint32_t)recognize_time, (uint32_t)encode_time, (uint32_t)process_time,
- (detected)?"DETECTED ":"", face_id
- );
- }
- last_frame = 0;
- return res;
- }
- static esp_err_t cmd_handler(httpd_req_t *req){
- char* buf;
- size_t buf_len;
- char variable[32] = {0,};
- char value[32] = {0,};
- buf_len = httpd_req_get_url_query_len(req) + 1;
- if (buf_len > 1) {
- buf = (char*)malloc(buf_len);
- if(!buf){
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
- if (httpd_query_key_value(buf, "var", variable, sizeof(variable)) == ESP_OK &&
- httpd_query_key_value(buf, "val", value, sizeof(value)) == ESP_OK) {
- } else {
- free(buf);
- httpd_resp_send_404(req);
- return ESP_FAIL;
- }
- } else {
- free(buf);
- httpd_resp_send_404(req);
- return ESP_FAIL;
- }
- free(buf);
- } else {
- httpd_resp_send_404(req);
- return ESP_FAIL;
- }
- int val = atoi(value);
- sensor_t * s = esp_camera_sensor_get();
- int res = 0;
- if(!strcmp(variable, "framesize")) {
- if(s->pixformat == PIXFORMAT_JPEG) res = s->set_framesize(s, (framesize_t)val);
- }
- else if(!strcmp(variable, "quality")) res = s->set_quality(s, val);
- else if(!strcmp(variable, "contrast")) res = s->set_contrast(s, val);
- else if(!strcmp(variable, "brightness")) res = s->set_brightness(s, val);
- else if(!strcmp(variable, "saturation")) res = s->set_saturation(s, val);
- else if(!strcmp(variable, "gainceiling")) res = s->set_gainceiling(s, (gainceiling_t)val);
- else if(!strcmp(variable, "awb")) res = s->set_whitebal(s, val);
- else if(!strcmp(variable, "hmirror")) res = s->set_hmirror(s, val);
- else if(!strcmp(variable, "vflip")) res = s->set_vflip(s, val);
- else if(!strcmp(variable, "dcw")) res = s->set_dcw(s, val);
- else if(!strcmp(variable, "colorbar"))
- if(val == 1)
- LedSwVal=3;
- else
- LedSwVal=4;
- else if(!strcmp(variable, "face_detect")) {
- detection_enabled = val;
- if(!detection_enabled) {
- recognition_enabled = 0;
- }
- }
- else if(!strcmp(variable, "face_enroll")) is_enrolling = val;
- else if(!strcmp(variable, "face_recognize")) {
- recognition_enabled = val;
- if(recognition_enabled){
- detection_enabled = val;
- }
- }
- else {
- res = -1;
- }
- if(res){
- return httpd_resp_send_500(req);
- }
- httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
- return httpd_resp_send(req, NULL, 0);
- }
- static esp_err_t status_handler(httpd_req_t *req){
- static char json_response[1024];
- sensor_t * s = esp_camera_sensor_get();
- char * p = json_response;
- *p++ = '{';
- p+=sprintf(p, "\"framesize\":%u,", s->status.framesize);
- p+=sprintf(p, "\"quality\":%u,", s->status.quality);
- p+=sprintf(p, "\"brightness\":%d,", s->status.brightness);
- p+=sprintf(p, "\"contrast\":%d,", s->status.contrast);
- p+=sprintf(p, "\"saturation\":%d,", s->status.saturation);
- p+=sprintf(p, "\"sharpness\":%d,", s->status.sharpness);
- p+=sprintf(p, "\"special_effect\":%u,", s->status.special_effect);
- p+=sprintf(p, "\"wb_mode\":%u,", s->status.wb_mode);
- p+=sprintf(p, "\"awb\":%u,", s->status.awb);
- p+=sprintf(p, "\"awb_gain\":%u,", s->status.awb_gain);
- p+=sprintf(p, "\"aec\":%u,", s->status.aec);
- p+=sprintf(p, "\"aec2\":%u,", s->status.aec2);
- p+=sprintf(p, "\"ae_level\":%d,", s->status.ae_level);
- p+=sprintf(p, "\"aec_value\":%u,", s->status.aec_value);
- p+=sprintf(p, "\"agc\":%u,", s->status.agc);
- p+=sprintf(p, "\"agc_gain\":%u,", s->status.agc_gain);
- p+=sprintf(p, "\"gainceiling\":%u,", s->status.gainceiling);
- p+=sprintf(p, "\"bpc\":%u,", s->status.bpc);
- p+=sprintf(p, "\"wpc\":%u,", s->status.wpc);
- p+=sprintf(p, "\"raw_gma\":%u,", s->status.raw_gma);
- p+=sprintf(p, "\"lenc\":%u,", s->status.lenc);
- p+=sprintf(p, "\"vflip\":%u,", s->status.vflip);
- p+=sprintf(p, "\"hmirror\":%u,", s->status.hmirror);
- p+=sprintf(p, "\"dcw\":%u,", s->status.dcw);
- p+=sprintf(p, "\"colorbar\":%u,", s->status.colorbar);
- p+=sprintf(p, "\"face_detect\":%u,", detection_enabled);
- p+=sprintf(p, "\"face_enroll\":%u,", is_enrolling);
- p+=sprintf(p, "\"face_recognize\":%u", recognition_enabled);
- *p++ = '}';
- *p++ = 0;
- httpd_resp_set_type(req, "application/json");
- httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
- return httpd_resp_send(req, json_response, strlen(json_response));
- }
- static esp_err_t index_handler(httpd_req_t *req){
- httpd_resp_set_type(req, "text/html");
- httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
- sensor_t * s = esp_camera_sensor_get();
- if (s->id.PID == OV3660_PID) {
- return httpd_resp_send(req, (const char *)index_ov3660_html_gz, index_ov3660_html_gz_len);
- }
- return httpd_resp_send(req, (const char *)index_ov2640_html_gz, index_ov2640_html_gz_len);
- }
- void startCameraServer(){
- httpd_config_t config = HTTPD_DEFAULT_CONFIG();
- httpd_uri_t index_uri = {
- .uri = "/",
- .method = HTTP_GET,
- .handler = index_handler,
- .user_ctx = NULL
- };
- httpd_uri_t status_uri = {
- .uri = "/status",
- .method = HTTP_GET,
- .handler = status_handler,
- .user_ctx = NULL
- };
- httpd_uri_t cmd_uri = {
- .uri = "/control",
- .method = HTTP_GET,
- .handler = cmd_handler,
- .user_ctx = NULL
- };
- httpd_uri_t capture_uri = {
- .uri = "/capture",
- .method = HTTP_GET,
- .handler = capture_handler,
- .user_ctx = NULL
- };
- httpd_uri_t stream_uri = {
- .uri = "/stream",
- .method = HTTP_GET,
- .handler = stream_handler,
- .user_ctx = NULL
- };
- ra_filter_init(&ra_filter, 20);
- mtmn_config.type = FAST;
- mtmn_config.min_face = 80;
- mtmn_config.pyramid = 0.707;
- mtmn_config.pyramid_times = 4;
- mtmn_config.p_threshold.score = 0.6;
- mtmn_config.p_threshold.nms = 0.7;
- mtmn_config.p_threshold.candidate_number = 20;
- mtmn_config.r_threshold.score = 0.7;
- mtmn_config.r_threshold.nms = 0.7;
- mtmn_config.r_threshold.candidate_number = 10;
- mtmn_config.o_threshold.score = 0.7;
- mtmn_config.o_threshold.nms = 0.7;
- mtmn_config.o_threshold.candidate_number = 1;
- face_id_init(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
- Serial.printf("Starting web server on port: '%d'\n", config.server_port);
- if (httpd_start(&camera_httpd, &config) == ESP_OK) {
- httpd_register_uri_handler(camera_httpd, &index_uri);
- httpd_register_uri_handler(camera_httpd, &cmd_uri);
- httpd_register_uri_handler(camera_httpd, &status_uri);
- httpd_register_uri_handler(camera_httpd, &capture_uri);
- }
- config.server_port += 1;
- config.ctrl_port += 1;
- Serial.printf("Starting stream server on port: '%d'\n", config.server_port);
- if (httpd_start(&stream_httpd, &config) == ESP_OK) {
- httpd_register_uri_handler(stream_httpd, &stream_uri);
- }
- }
- int getLedSw() { // send the value of 3 for ON or 4 for OFF to turn the floodlight on/off
- return LedSwVal;
- }
修改後的app_httpd.cpp 部分程式碼
「app_httpd.cpp」這個部分的程式其實修改的不多,因為我們並沒有新增什麼控制項,反而是刪除了許多之後不會用到的項目,為了避免在刪除的過程中誤刪了一些東西,所以只要不影響我們的動作,在此基本上就儘量不去動它,只針對我們想修改的部分去處理。
下面這行程式是原來就有的,在此提出來是想讓讀者了解後面程式中”val”這個變數的由來。
507. int val = atoi(value);
在使用者設定的網頁頁面中,我們是借用了"colorbar"這個控制開關作為切換那顆內建在ESP32-CAM的高亮度LED之用,當這個開關被使用者切換到”on”時,”val”這個整數變數會從”value”這個字元變數得到”1”的結果,反之則為”0”;因此我們依照”val”這個變數的結果,再讓主程式中的整體變數”LedSwVal”=3(on)或是=4(off)。
489. else if(!strcmp(variable, "colorbar"))
490. if(val == 1)
491. LedSwVal=3;
492. else
493. LedSwVal=4;
最後由下面這個副程式”getLedSw()”把變數”LedSwVal”回傳給迴圈主程式(loop())中呼叫的程式列,這樣我們便可以依照使用者在網頁上操作的結果,開關那顆內建在ESP32-CAM的高亮度LED了。
644. int getLedSw() { // send the value of 3 for ON or 4 for OFF to turn the floodlight 684. on/off
645. return LedSwVal;
646. }
四、實機測試
在本文一開始就說明過由於受限於一般WiFi分享器(Router)在使用埠轉發(Port Forwarding),或是虛擬伺服器(Virtual Server)的功能時,只能對應到一個內部的通訊埠,也就是說這個電路就無法直接拿到網際網路上使用將會限制使用在單一個WiFi分享器(Router)可以涵蓋範圍內!以前面的【圖一】來說,手機或其他行動通訊裝置只能經由路徑1去連接這塊ESP32-CAM模組板。不過由於一般的WiFi分享器(Router)可以涵蓋範圍也算滿寬廣,這樣的系統在一些家庭或是公司、學校類的場域來說也是可以延伸出一些應用,例如裝在家庭各處具遙控開關的監視器、具影像功能的自走車等。
為了讓使用者不必猜測所使用的ESP32-CAM模組板在連上WiFi分享器(Router)時會被分配到那一個內部的IP位址,在此我們跟前一個範例一樣是使用固定IP的方式去設定連線,也就是說還是使用『192.168.0.166』這個內部IP網址。使用者在連上自己的WiFi分享器(Router)後,開啟瀏覽器並輸入上面的IP位址,便可以連線到你的ESP32-CAM並看到鏡頭所拍到的畫面了!
下面的兩張圖片分別是熄滅與點亮該高亮度LED後實際拍攝到的畫面,大家可以比較一下兩這的差異,由於這個高亮度LED的功率並不是太大,所以距離如果太遠還是無法看得太清楚。
圖四.1 ESP32-CAM模組上的LED熄滅時所實際拍攝畫面
圖四.2 ESP32-CAM模組上的LED點亮時所實際拍攝畫面