2021年4月2日 星期五

以ESP32-CAM設計一網路IP監視器 之二(內網篇) – 點光明燈



  1. ESP32-CAM模組板硬體說明

  2. ESP32-CAM模組板使用者網頁程式設計

  3. Arduino/ESP32-CAM程式設計與分析

  4. 實機測試





在上一篇文章中示範了如何把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網頁程式列表如下

  1. <!doctype html>
  2. <html>
  3.     <head>
  4.         <meta charset="utf-8">
  5.         <meta name="viewport" content="width=device-width,initial-scale=1">
  6.         <title>ESP32 OV2460</title>
  7.         <style>
  8.             body {
  9.                 font-family: Arial,Helvetica,sans-serif;
  10.                 background: #181818;
  11.                 color: #EFEFEF;
  12.                 font-size: 16px
  13.             }
  14.             h2 {
  15.                 font-size: 18px
  16.             }
  17.             section.main {
  18.                 display: flex
  19.             }
  20.             #menu,section.main {
  21.                 flex-direction: column
  22.             }
  23.             #menu {
  24.                 display: none;
  25.                 flex-wrap: nowrap;
  26.                 min-width: 340px;
  27.                 background: #363636;
  28.                 padding: 8px;
  29.                 border-radius: 4px;
  30.                 margin-top: -10px;
  31.                 margin-right: 10px;
  32.             }
  33.             #content {
  34.                 display: flex;
  35.                 flex-wrap: wrap;
  36.                 align-items: stretch
  37.             }
  38.  
  39.             figure {
  40.                 padding: 0px;
  41.                 margin: 0;
  42.                 -webkit-margin-before: 0;
  43.                 margin-block-start: 0;
  44.                 -webkit-margin-after: 0;
  45.                 margin-block-end: 0;
  46.                 -webkit-margin-start: 0;
  47.                 margin-inline-start: 0;
  48.                 -webkit-margin-end: 0;
  49.                 margin-inline-end: 0
  50.             }
  51.             figure img {
  52.                 display: block;
  53.                 width: 100%;
  54.                 height: auto;
  55.                 border-radius: 4px;
  56.                 margin-top: 8px;
  57.             }
  58.             @media (min-width: 800px) and (orientation:landscape) {
  59.                 #content {
  60.                     display:flex;
  61.                     flex-wrap: nowrap;
  62.                     align-items: stretch
  63.                 }
  64.  
  65.                 figure img {
  66.                     display: block;
  67.                     max-width: 100%;
  68.                     max-height: calc(100vh - 40px);
  69.                     width: auto;
  70.                     height: auto
  71.                 }
  72.  
  73.                 figure {
  74.                     padding: 0 0 0 0px;
  75.                     margin: 0;
  76.                     -webkit-margin-before: 0;
  77.                     margin-block-start: 0;
  78.                     -webkit-margin-after: 0;
  79.                     margin-block-end: 0;
  80.                     -webkit-margin-start: 0;
  81.                     margin-inline-start: 0;
  82.                     -webkit-margin-end: 0;
  83.                     margin-inline-end: 0
  84.                 }
  85.             }
  86.  
  87.             section#buttons {
  88.                 display: flex;
  89.                 flex-wrap: nowrap;
  90.                 justify-content: space-between
  91.             }
  92.  
  93.             #nav-toggle {
  94.                 cursor: pointer;
  95.                 display: block
  96.             }
  97.  
  98.             #nav-toggle-cb {
  99.                 outline: 0;
  100.                 opacity: 0;
  101.                 width: 0;
  102.                 height: 0
  103.             }
  104.  
  105.             #nav-toggle-cb:checked+#menu {
  106.                 display: flex
  107.             }
  108.  
  109.             .input-group {
  110.                 display: flex;
  111.                 flex-wrap: nowrap;
  112.                 line-height: 22px;
  113.                 margin: 5px 0
  114.             }
  115.  
  116.             .input-group>label {
  117.                 display: inline-block;
  118.                 padding-right: 10px;
  119.                 min-width: 47%
  120.             }
  121.  
  122.             .input-group input,.input-group select {
  123.                 flex-grow: 1
  124.             }
  125.  
  126.             .range-max,.range-min {
  127.                 display: inline-block;
  128.                 padding: 0 5px
  129.             }
  130.  
  131.             button {
  132.                 display: block;
  133.                 margin: 5px;
  134.                 padding: 0 12px;
  135.                 border: 0;
  136.                 line-height: 28px;
  137.                 cursor: pointer;
  138.                 color: #fff;
  139.                 background: #ff3034;
  140.                 border-radius: 5px;
  141.                 font-size: 16px;
  142.                 outline: 0
  143.             }
  144.  
  145.             button:hover {
  146.                 background: #ff494d
  147.             }
  148.  
  149.             button:active {
  150.                 background: #f21c21
  151.             }
  152.  
  153.             button.disabled {
  154.                 cursor: default;
  155.                 background: #a0a0a0
  156.             }
  157.  
  158.             input[type=range] {
  159.                 -webkit-appearance: none;
  160.                 width: 100%;
  161.                 height: 22px;
  162.                 background: #363636;
  163.                 cursor: pointer;
  164.                 margin: 0
  165.             }
  166.  
  167.             input[type=range]:focus {
  168.                 outline: 0
  169.             }
  170.  
  171.             input[type=range]::-webkit-slider-runnable-track {
  172.                 width: 100%;
  173.                 height: 2px;
  174.                 cursor: pointer;
  175.                 background: #EFEFEF;
  176.                 border-radius: 0;
  177.                 border: 0 solid #EFEFEF
  178.             }
  179.  
  180.             input[type=range]::-webkit-slider-thumb {
  181.                 border: 1px solid rgba(0,0,30,0);
  182.                 height: 22px;
  183.                 width: 22px;
  184.                 border-radius: 50px;
  185.                 background: #ff3034;
  186.                 cursor: pointer;
  187.                 -webkit-appearance: none;
  188.                 margin-top: -11.5px
  189.             }
  190.  
  191.             input[type=range]:focus::-webkit-slider-runnable-track {
  192.                 background: #EFEFEF
  193.             }
  194.  
  195.             input[type=range]::-moz-range-track {
  196.                 width: 100%;
  197.                 height: 2px;
  198.                 cursor: pointer;
  199.                 background: #EFEFEF;
  200.                 border-radius: 0;
  201.                 border: 0 solid #EFEFEF
  202.             }
  203.  
  204.             input[type=range]::-moz-range-thumb {
  205.                 border: 1px solid rgba(0,0,30,0);
  206.                 height: 22px;
  207.                 width: 22px;
  208.                 border-radius: 50px;
  209.                 background: #ff3034;
  210.                 cursor: pointer
  211.             }
  212.  
  213.             input[type=range]::-ms-track {
  214.                 width: 100%;
  215.                 height: 2px;
  216.                 cursor: pointer;
  217.                 background: 0 0;
  218.                 border-color: transparent;
  219.                 color: transparent
  220.             }
  221.  
  222.             input[type=range]::-ms-fill-lower {
  223.                 background: #EFEFEF;
  224.                 border: 0 solid #EFEFEF;
  225.                 border-radius: 0
  226.             }
  227.  
  228.             input[type=range]::-ms-fill-upper {
  229.                 background: #EFEFEF;
  230.                 border: 0 solid #EFEFEF;
  231.                 border-radius: 0
  232.             }
  233.  
  234.             input[type=range]::-ms-thumb {
  235.                 border: 1px solid rgba(0,0,30,0);
  236.                 height: 22px;
  237.                 width: 22px;
  238.                 border-radius: 50px;
  239.                 background: #ff3034;
  240.                 cursor: pointer;
  241.                 height: 2px
  242.             }
  243.  
  244.             input[type=range]:focus::-ms-fill-lower {
  245.                 background: #EFEFEF
  246.             }
  247.  
  248.             input[type=range]:focus::-ms-fill-upper {
  249.                 background: #363636
  250.             }
  251.  
  252.             .switch {
  253.                 display: block;
  254.                 position: relative;
  255.                 line-height: 22px;
  256.                 font-size: 16px;
  257.                 height: 22px
  258.             }
  259.  
  260.             .switch input {
  261.                 outline: 0;
  262.                 opacity: 0;
  263.                 width: 0;
  264.                 height: 0
  265.             }
  266.  
  267.             .slider {
  268.                 width: 50px;
  269.                 height: 22px;
  270.                 border-radius: 22px;
  271.                 cursor: pointer;
  272.                 background-color: grey
  273.             }
  274.  
  275.             .slider,.slider:before {
  276.                 display: inline-block;
  277.                 transition: .4s
  278.             }
  279.  
  280.             .slider:before {
  281.                 position: relative;
  282.                 content: "";
  283.                 border-radius: 50%;
  284.                 height: 16px;
  285.                 width: 16px;
  286.                 left: 4px;
  287.                 top: 3px;
  288.                 background-color: #fff
  289.             }
  290.  
  291.             input:checked+.slider {
  292.                 background-color: #ff3034
  293.             }
  294.  
  295.             input:checked+.slider:before {
  296.                 -webkit-transform: translateX(26px);
  297.                 transform: translateX(26px)
  298.             }
  299.  
  300.             select {
  301.                 border: 1px solid #363636;
  302.                 font-size: 14px;
  303.                 height: 22px;
  304.                 outline: 0;
  305.                 border-radius: 5px
  306.             }
  307.  
  308.             .image-container {
  309.                 position: relative;
  310.                 min-width: 160px
  311.             }
  312.  
  313.             .close {
  314.                 position: absolute;
  315.                 right: 5px;
  316.                 top: 5px;
  317.                 background: #ff3034;
  318.                 width: 16px;
  319.                 height: 16px;
  320.                 border-radius: 100px;
  321.                 color: #fff;
  322.                 text-align: center;
  323.                 line-height: 18px;
  324.                 cursor: pointer
  325.             }
  326.  
  327.             .hidden {
  328.                 display: none
  329.             }
  330.         </style>
  331.     </head>
  332.     <body>
  333.         <section class="main">
  334.             <div id="logo">
  335.                 <label for="nav-toggle-cb" id="nav-toggle">&#9776;&nbsp;&nbsp;ESP32-CAM/OV2640 設定</label>
  336.             </div>
  337.             <div id="content">
  338.                 <div id="sidebar">
  339.                <input type="checkbox" id="nav-toggle-cb" checked="checked">
  340.                     <nav id="menu">
  341.                         <div class="input-group" id="framesize-group">
  342.                             <label for="framesize">Resolution</label>
  343.                             <select id="framesize" class="default-action">
  344.               <option value="10">UXGA(1600x1200)</option>
  345.               <option value="9">SXGA(1280x1024)</option>
  346.               <option value="8" Selected="selected">XGA(1024x768)</option>
  347.               <option value="7">SVGA(800x600)</option>
  348.               <option value="6">VGA(640x480)</option>
  349.               <option value="5">CIF(400x296)</option>
  350.               <option value="4">QVGA(320x240)</option>
  351.               <option value="3">HQVGA(240x176)</option>
  352.               <option value="0">QQVGA(160x120)</option>
  353.                             </select>
  354.                         </div>
  355.                         <div class="input-group" id="quality-group">
  356.                             <label for="quality">Quality</label>
  357.                             <div class="range-min">10</div>
  358.                             <input type="range" id="quality" min="10" max="63" value="10" class="default-action">
  359.                             <div class="range-max">63</div>
  360.                         </div>
  361.                         <div class="input-group" id="brightness-group">
  362.                             <label for="brightness">Brightness</label>
  363.                             <div class="range-min">-2</div>
  364.                             <input type="range" id="brightness" min="-2" max="2" value="0" class="default-action">
  365.                             <div class="range-max">2</div>
  366.                         </div>
  367.                         <div class="input-group" id="contrast-group">
  368.                             <label for="contrast">Contrast</label>
  369.                             <div class="range-min">-2</div>
  370.                             <input type="range" id="contrast" min="-2" max="2" value="0" class="default-action">
  371.                             <div class="range-max">2</div>
  372.                         </div>
  373.                         <div class="input-group" id="saturation-group">
  374.                             <label for="saturation">Saturation</label>
  375.                             <div class="range-min">-2</div>
  376.                             <input type="range" id="saturation" min="-2" max="2" value="0" class="default-action">
  377.                             <div class="range-max">2</div>
  378.                         </div>
  379.                         <div class="input-group" id="awb-group">
  380.                             <label for="awb">AWB</label>
  381.                             <div class="switch">
  382.                                 <input id="awb" type="checkbox" class="default-action" checked="checked">
  383.                                 <label class="slider" for="awb"></label>
  384.                             </div>
  385.                         </div>
  386.                         <div class="input-group" id="hmirror-group">
  387.                             <label for="hmirror">H-Mirror</label>
  388.                             <div class="switch">
  389.                                 <input id="hmirror" type="checkbox" class="default-action">
  390.                                 <label class="slider" for="hmirror"></label>
  391.                             </div>
  392.                         </div>
  393.                         <div class="input-group" id="vflip-group">
  394.                             <label for="vflip">V-Flip</label>
  395.                             <div class="switch">
  396.                                 <input id="vflip" type="checkbox" class="default-action">
  397.                                 <label class="slider" for="vflip"></label>
  398.                             </div>
  399.                         </div>
  400.                         <div class="input-group" id="dcw-group">
  401.                             <label for="dcw">DCW (Downsize EN)</label>
  402.                             <div class="switch">
  403.                                 <input id="dcw" type="checkbox" class="default-action" checked="checked">
  404.                                 <label class="slider" for="dcw"></label>
  405.                             </div>
  406.                         </div>
  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>
  414.                         <div class="input-group" id="face_detect-group">
  415.                             <label for="face_detect">Face Detection</label>
  416.                             <div class="switch">
  417.                                 <input id="face_detect" type="checkbox" class="default-action">
  418.                                 <label class="slider" for="face_detect"></label>
  419.                             </div>
  420.                         </div>
  421.                         <div class="input-group" id="face_recognize-group">
  422.                             <label for="face_recognize">Face Recognition</label>
  423.                             <div class="switch">
  424.                                 <input id="face_recognize" type="checkbox" class="default-action">
  425.                                 <label class="slider" for="face_recognize"></label>
  426.                             </div>
  427.                         </div>
  428.                   <section id="buttons">
  429.                       <button id="get-still">Get Still</button>
  430.                       <button id="toggle-stream">啟動監視器</button>
  431.                       <button id="face_enroll" class="disabled" disabled="disabled">Enroll Face</button>
  432.                    </section>
  433.                     </nav>
  434.                 </div>
  435.                 <figure>
  436.                <div id="stream-container" class="image-container hidden">
  437.                         <div class="close" id="close-stream">×</div>
  438.                         <img id="stream" src="">
  439.                     </div>
  440.                 </figure>
  441.             </div>
  442.         </section>
  443.         <script>
  444. document.addEventListener('DOMContentLoaded', function (event) {
  445.   var baseHost = document.location.origin
  446.   var streamUrl = baseHost + ':81'
  447.  
  448.   const hide = el => {
  449.     el.classList.add('hidden')
  450.   }
  451.   const show = el => {
  452.     el.classList.remove('hidden')
  453.   }
  454.  
  455.   const disable = el => {
  456.     el.classList.add('disabled')
  457.     el.disabled = true
  458.   }
  459.  
  460.   const enable = el => {
  461.     el.classList.remove('disabled')
  462.     el.disabled = false
  463.   }
  464.  
  465.   const updateValue = (el, value, updateRemote) => {
  466.     updateRemote = updateRemote == null ? true : updateRemote
  467.     let initialValue
  468.     if (el.type === 'checkbox') {
  469.       initialValue = el.checked
  470.       value = !!value
  471.       el.checked = value
  472.     } else {
  473.       initialValue = el.value
  474.       el.value = value
  475.     }
  476.  
  477.     if (updateRemote && initialValue !== value) {
  478.       updateConfig(el);
  479.     } else if(!updateRemote){
  480.       if(el.id === "aec"){
  481.         value ? hide(exposure) : show(exposure)
  482.       } else if(el.id === "agc"){
  483.         if (value) {
  484.           show(gainCeiling)
  485.           hide(agcGain)
  486.         } else {
  487.           hide(gainCeiling)
  488.           show(agcGain)
  489.         }
  490.       } else if(el.id === "awb_gain"){
  491.         value ? show(wb) : hide(wb)
  492.       } else if(el.id === "face_recognize"){
  493.         value ? enable(enrollButton) : disable(enrollButton)
  494.       }
  495.     }
  496.   }
  497.  
  498.   function updateConfig (el) {
  499.     let value
  500.     switch (el.type) {
  501.       case 'checkbox':
  502.         value = el.checked ? 1 : 0
  503.         break
  504.       case 'range':
  505.       case 'select-one':
  506.         value = el.value
  507.         break
  508.       case 'button':
  509.       case 'submit':
  510.         value = '1'
  511.         break
  512.       default:
  513.         return
  514.     }
  515.  
  516.     const query = `${baseHost}/control?var=${el.id}&val=${value}`
  517.  
  518.     fetch(query)
  519.       .then(response => {
  520.         console.log(`request to ${query} finished, status: ${response.status}`)
  521.       })
  522.   }
  523.  
  524.   document
  525.     .querySelectorAll('.close')
  526.     .forEach(el => {
  527.       el.onclick = () => {
  528.         hide(el.parentNode)
  529.       }
  530.     })
  531.  
  532.   // read initial values
  533.   fetch(`${baseHost}/status`)
  534.     .then(function (response) {
  535.       return response.json()
  536.     })
  537.     .then(function (state) {
  538.       document
  539.         .querySelectorAll('.default-action')
  540.         .forEach(el => {
  541.           updateValue(el, state[el.id], false)
  542.         })
  543.     })
  544.  
  545.   const view = document.getElementById('stream')
  546.   const viewContainer = document.getElementById('stream-container')
  547.   const stillButton = document.getElementById('get-still')
  548.   const streamButton = document.getElementById('toggle-stream')
  549.   const enrollButton = document.getElementById('face_enroll')
  550.   const closeButton = document.getElementById('close-stream')
  551.  
  552.   const stopStream = () => {
  553.     window.stop();
  554.     streamButton.innerHTML = '啟動監視器'
  555.   }
  556.  
  557.   const startStream = () => {
  558.     view.src = `${streamUrl}/stream`
  559.     show(viewContainer)
  560.     streamButton.innerHTML = '停止監視器'
  561.   }
  562.  
  563.   // Attach actions to buttons
  564.   stillButton.onclick = () => {
  565.     stopStream()
  566.     view.src = `${baseHost}/capture?_cb=${Date.now()}`
  567.     show(viewContainer)
  568.   }
  569.  
  570.   closeButton.onclick = () => {
  571.     stopStream()
  572.     hide(viewContainer)
  573.   }
  574.  
  575.   streamButton.onclick = () => {
  576.     const streamEnabled = streamButton.innerHTML === '停止監視器'
  577.     if (streamEnabled) {
  578.       stopStream()
  579.     } else {
  580.       startStream()
  581.     }
  582.   }
  583.  
  584.   enrollButton.onclick = () => {
  585.     updateConfig(enrollButton)
  586.   }
  587.  
  588.   // Attach default on change action
  589.   document
  590.     .querySelectorAll('.default-action')
  591.     .forEach(el => {
  592.       el.onchange = () => updateConfig(el)
  593.     })
  594.  
  595.   // Custom actions
  596.   // Gain
  597.   const agc = document.getElementById('agc')
  598.   const agcGain = document.getElementById('agc_gain-group')
  599.   const gainCeiling = document.getElementById('gainceiling-group')
  600.   agc.onchange = () => {
  601.     updateConfig(agc)
  602.     if (agc.checked) {
  603.       show(gainCeiling)
  604.       hide(agcGain)
  605.     } else {
  606.       hide(gainCeiling)
  607.       show(agcGain)
  608.     }
  609.   }
  610.  
  611.   // Exposure
  612.   const aec = document.getElementById('aec')
  613.   const exposure = document.getElementById('aec_value-group')
  614.   aec.onchange = () => {
  615.     updateConfig(aec)
  616.     aec.checked ? hide(exposure) : show(exposure)
  617.   }
  618.  
  619.   // AWB
  620.   const awb = document.getElementById('awb_gain')
  621.   const wb = document.getElementById('wb_mode-group')
  622.   awb.onchange = () => {
  623.     updateConfig(awb)
  624.     awb.checked ? show(wb) : hide(wb)
  625.   }
  626.  
  627.   // Detection and framesize
  628.   const detect = document.getElementById('face_detect')
  629.   const recognize = document.getElementById('face_recognize')
  630.   const framesize = document.getElementById('framesize')
  631.  
  632.   framesize.onchange = () => {
  633.     updateConfig(framesize)
  634.     if (framesize.value > 5) {
  635.       updateValue(detect, false)
  636.       updateValue(recognize, false)
  637.     }
  638.   }
  639.  
  640.   detect.onchange = () => {
  641.     if (framesize.value > 5) {
  642.       alert("Please select CIF or lower resolution before enabling this feature!");
  643.       updateValue(detect, false)
  644.       return;
  645.     }
  646.     updateConfig(detect)
  647.     if (!detect.checked) {
  648.       disable(enrollButton)
  649.       updateValue(recognize, false)
  650.     }
  651.   }
  652.  
  653.   recognize.onchange = () => {
  654.     if (framesize.value > 5) {
  655.       alert("Please select CIF or lower resolution before enabling this feature!");
  656.       updateValue(recognize, false)
  657.       return;
  658.     }
  659.     updateConfig(recognize)
  660.     if (recognize.checked) {
  661.       enable(enrollButton)
  662.       updateValue(detect, true)
  663.     } else {
  664.       disable(enrollButton)
  665.     }
  666.   }
  667. })
  668.  
  669. document.addEventListener ('keyup', function (el) {
  670.   var baseHost = document.location.origin // establish base host and stream socket
  671.   var streamUrl = baseHost + ':81'
  672. const query = `${baseHost}/control?var=key&val=${el.keyCode}`  // put 'key' and the ASCII value in the query
  673.  fetch(query)// send the query
  674.       .then(response => {
  675.         console.log(`request to ${query} finished, status: ${response.status}`)
  676.       })
  677. }, false);
  678.  
  679.         </script>
  680.     </body>
  681. </html>

修改後的設定網頁介面程式「camera_index.h 」


【圖二.13】中標記1[ESP32-CAM/OV2640 設定]標題文字是由下面幾行程式所產生:


334.            <div id="logo">

335.                <label for="nav-toggle-cb"

                            id="nav-toggle">&#9776;&nbsp;&nbsp;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的來源程式輸入區中接著在動作區內也就是左邊標記23處搜尋並取出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 HexSplit動作畫面


在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」列表



  1. #include "esp_camera.h"
  2. #include <WiFi.h>
  3.  
  4. #define CAMERA_MODEL_AI_THINKER
  5.  
  6. #include "camera_pins.h"
  7.  
  8. //用你自己的WiFi分享器資料取代下面的兩個變數:
  9. const char* ssid = "你的WiFi分享器SSID";
  10. const char* password = "你的WiFi分享器密碼";
  11.  
  12. IPAddress staticIP(192,168,0,166);    // 固定式本地IP位址
  13. IPAddress gateway(192,168,0,1);
  14. IPAddress subnet(255,255,255,0);
  15.  
  16. void startCameraServer();
  17.  
  18. char  LedSw;      // LED開關狀態字元
  19. int getLedSw();   // 取得LED開關狀態
  20. int LedLight = 4;// the on-board LED flash
  21.  
  22. void setup() {
  23.   Serial.begin(115200);
  24.   Serial.setDebugOutput(true);
  25.   Serial.println();
  26.   pinMode(LedLight, OUTPUT);
  27.   digitalWrite(LedLight, LOW);
  28.   pinMode(33,OUTPUT);
  29.   digitalWrite(33,1);
  30.  
  31.   camera_config_t config;
  32.   config.ledc_channel = LEDC_CHANNEL_0;
  33.   config.ledc_timer = LEDC_TIMER_0;
  34.   config.pin_d0 = Y2_GPIO_NUM;
  35.   config.pin_d1 = Y3_GPIO_NUM;
  36.   config.pin_d2 = Y4_GPIO_NUM;
  37.   config.pin_d3 = Y5_GPIO_NUM;
  38.   config.pin_d4 = Y6_GPIO_NUM;
  39.   config.pin_d5 = Y7_GPIO_NUM;
  40.   config.pin_d6 = Y8_GPIO_NUM;
  41.   config.pin_d7 = Y9_GPIO_NUM;
  42.   config.pin_xclk = XCLK_GPIO_NUM;
  43.   config.pin_pclk = PCLK_GPIO_NUM;
  44.   config.pin_vsync = VSYNC_GPIO_NUM;
  45.   config.pin_href = HREF_GPIO_NUM;
  46.   config.pin_sscb_sda = SIOD_GPIO_NUM;
  47.   config.pin_sscb_scl = SIOC_GPIO_NUM;
  48.   config.pin_pwdn = PWDN_GPIO_NUM;
  49.   config.pin_reset = RESET_GPIO_NUM;
  50.   config.xclk_freq_hz = 20000000;
  51.   config.pixel_format = PIXFORMAT_JPEG;
  52.   //init with high specs to pre-allocate larger buffers
  53.   if(psramFound()){
  54.     config.frame_size = FRAMESIZE_UXGA;
  55.     config.jpeg_quality = 10;
  56.     config.fb_count = 2;
  57.   } else {
  58.     config.frame_size = FRAMESIZE_SVGA;
  59.     config.jpeg_quality = 12;
  60.     config.fb_count = 1;
  61.   }
  62.  
  63. #if defined(CAMERA_MODEL_ESP_EYE)
  64.   pinMode(13, INPUT_PULLUP);
  65.   pinMode(14, INPUT_PULLUP);
  66. #endif
  67.  
  68.   // camera init
  69.   esp_err_t err = esp_camera_init(&config);
  70.   if (err != ESP_OK) {
  71.     Serial.printf("Camera init failed with error 0x%x", err);
  72.     return;
  73.   }
  74.  
  75.   sensor_t * s = esp_camera_sensor_get();
  76.   //initial sensors are flipped vertically and colors are a bit saturated
  77.   if (s->id.PID == OV3660_PID) {
  78.     s->set_vflip(s, 1);//flip it back
  79.     s->set_brightness(s, 1);//up the blightness just a bit
  80.     s->set_saturation(s, -2);//lower the saturation
  81.   }
  82.   //drop down frame size for higher initial frame rate
  83.   s->set_framesize(s, FRAMESIZE_QVGA);
  84.  
  85. #if defined(CAMERA_MODEL_M5STACK_WIDE)
  86.   s->set_vflip(s, 1);
  87.   s->set_hmirror(s, 1);
  88. #endif
  89.  
  90.   // Wi-Fi connection
  91.   Serial.print("連接到SSID : ");
  92.   Serial.println(ssid);
  93.     WiFi.config(staticIP,gateway,subnet);
  94.     delay(10);
  95.   WiFi.begin(ssid, password);
  96.   while (WiFi.status() != WL_CONNECTED) {
  97.     delay(500);
  98.     Serial.print(".");
  99.   }
  100.   Serial.println("");
  101.   Serial.println("WiFi connected");
  102.   digitalWrite(33,0);
  103.   
  104.   Serial.print("Camera Ready! Use 'http://");
  105.   Serial.print(WiFi.localIP());
  106.   Serial.println("' to connect");
  107.  
  108.   startCameraServer();
  109.  
  110. }
  111.  
  112. void loop() {
  113.  
  114.    LedSw=getLedSw();
  115.    if(LedSw == 3)
  116.     digitalWrite(LedLight,HIGH);
  117.    else
  118.     digitalWrite(LedLight,LOW);
  119.    delay(50);
  120. }

修改後的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」程式列表




  1. #include "esp_http_server.h"
  2. #include "esp_timer.h"
  3. #include "esp_camera.h"
  4. #include "img_converters.h"
  5. #include "camera_index.h"
  6. #include "Arduino.h"
  7.  
  8. #include "fb_gfx.h"
  9. #include "fd_forward.h"
  10. #include "fr_forward.h"
  11.  
  12. int LedSwVal; // LED開關狀態值
  13.  
  14. #define ENROLL_CONFIRM_TIMES 5
  15. #define FACE_ID_SAVE_NUMBER 7
  16.  
  17. #define FACE_COLOR_WHITE  0x00FFFFFF
  18. #define FACE_COLOR_BLACK  0x00000000
  19. #define FACE_COLOR_RED    0x000000FF
  20. #define FACE_COLOR_GREEN  0x0000FF00
  21. #define FACE_COLOR_BLUE   0x00FF0000
  22. #define FACE_COLOR_YELLOW (FACE_COLOR_RED | FACE_COLOR_GREEN)
  23. #define FACE_COLOR_CYAN   (FACE_COLOR_BLUE | FACE_COLOR_GREEN)
  24. #define FACE_COLOR_PURPLE (FACE_COLOR_BLUE | FACE_COLOR_RED)
  25.  
  26. typedef struct {
  27.         size_t size; //number of values used for filtering
  28.         size_t index; //current value index
  29.         size_t count; //value count
  30.         int sum;
  31.         int * values; //array to be filled with values
  32. } ra_filter_t;
  33.  
  34. typedef struct {
  35.         httpd_req_t *req;
  36.         size_t len;
  37. } jpg_chunking_t;
  38.  
  39. #define PART_BOUNDARY "123456789000000000000987654321"
  40. static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
  41. static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
  42. static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
  43.  
  44. static ra_filter_t ra_filter;
  45. httpd_handle_t stream_httpd = NULL;
  46. httpd_handle_t camera_httpd = NULL;
  47.  
  48. static mtmn_config_t mtmn_config = {0};
  49. static int8_t detection_enabled = 0;
  50. static int8_t recognition_enabled = 0;
  51. static int8_t is_enrolling = 0;
  52. static face_id_list id_list = {0};
  53.  
  54. static ra_filter_t * ra_filter_init(ra_filter_t * filter, size_t sample_size){
  55.     memset(filter, 0, sizeof(ra_filter_t));
  56.  
  57.     filter->values = (int *)malloc(sample_size * sizeof(int));
  58.     if(!filter->values){
  59.         return NULL;
  60.     }
  61.     memset(filter->values, 0, sample_size * sizeof(int));
  62.  
  63.     filter->size = sample_size;
  64.     return filter;
  65. }
  66.  
  67. static int ra_filter_run(ra_filter_t * filter, int value){
  68.     if(!filter->values){
  69.         return value;
  70.     }
  71.     filter->sum -= filter->values[filter->index];
  72.     filter->values[filter->index] = value;
  73.     filter->sum += filter->values[filter->index];
  74.     filter->index++;
  75.     filter->index = filter->index % filter->size;
  76.     if (filter->count < filter->size) {
  77.         filter->count++;
  78.     }
  79.     return filter->sum / filter->count;
  80. }
  81.  
  82. static void rgb_print(dl_matrix3du_t *image_matrix, uint32_t color, const char * str){
  83.     fb_data_t fb;
  84.     fb.width = image_matrix->w;
  85.     fb.height = image_matrix->h;
  86.     fb.data = image_matrix->item;
  87.     fb.bytes_per_pixel = 3;
  88.     fb.format = FB_BGR888;
  89.     fb_gfx_print(&fb, (fb.width - (strlen(str) * 14)) / 2, 10, color, str);
  90. }
  91.  
  92. static int rgb_printf(dl_matrix3du_t *image_matrix, uint32_t color, const char *format, ...){
  93.     char loc_buf[64];
  94.     char * temp = loc_buf;
  95.     int len;
  96.     va_list arg;
  97.     va_list copy;
  98.     va_start(arg, format);
  99.     va_copy(copy, arg);
  100.     len = vsnprintf(loc_buf, sizeof(loc_buf), format, arg);
  101.     va_end(copy);
  102.     if(len >= sizeof(loc_buf)){
  103.         temp = (char*)malloc(len+1);
  104.         if(temp == NULL) {
  105.             return 0;
  106.         }
  107.     }
  108.     vsnprintf(temp, len+1, format, arg);
  109.     va_end(arg);
  110.     rgb_print(image_matrix, color, temp);
  111.     if(len > 64){
  112.         free(temp);
  113.     }
  114.     return len;
  115. }
  116.  
  117. static void draw_face_boxes(dl_matrix3du_t *image_matrix, box_array_t *boxes, int face_id){
  118.     int x, y, w, h, i;
  119.     uint32_t color = FACE_COLOR_YELLOW;
  120.     if(face_id < 0){
  121.         color = FACE_COLOR_RED;
  122.     } else if(face_id > 0){
  123.         color = FACE_COLOR_GREEN;
  124.     }
  125.     fb_data_t fb;
  126.     fb.width = image_matrix->w;
  127.     fb.height = image_matrix->h;
  128.     fb.data = image_matrix->item;
  129.     fb.bytes_per_pixel = 3;
  130.     fb.format = FB_BGR888;
  131.     for (i = 0; i < boxes->len; i++){
  132.         // rectangle box
  133.         x = (int)boxes->box[i].box_p[0];
  134.         y = (int)boxes->box[i].box_p[1];
  135.         w = (int)boxes->box[i].box_p[2] - x + 1;
  136.         h = (int)boxes->box[i].box_p[3] - y + 1;
  137.         fb_gfx_drawFastHLine(&fb, x, y, w, color);
  138.         fb_gfx_drawFastHLine(&fb, x, y+h-1, w, color);
  139.         fb_gfx_drawFastVLine(&fb, x, y, h, color);
  140.         fb_gfx_drawFastVLine(&fb, x+w-1, y, h, color);
  141. #if 0
  142.         // landmark
  143.         int x0, y0, j;
  144.         for (j = 0; j < 10; j+=2) {
  145.             x0 = (int)boxes->landmark[i].landmark_p[j];
  146.             y0 = (int)boxes->landmark[i].landmark_p[j+1];
  147.             fb_gfx_fillRect(&fb, x0, y0, 3, 3, color);
  148.         }
  149. #endif
  150.     }
  151. }
  152.  
  153. static int run_face_recognition(dl_matrix3du_t *image_matrix, box_array_t *net_boxes){
  154.     dl_matrix3du_t *aligned_face = NULL;
  155.     int matched_id = 0;
  156.  
  157.     aligned_face = dl_matrix3du_alloc(1, FACE_WIDTH, FACE_HEIGHT, 3);
  158.     if(!aligned_face){
  159.         Serial.println("Could not allocate face recognition buffer");
  160.         return matched_id;
  161.     }
  162.     if (align_face(net_boxes, image_matrix, aligned_face) == ESP_OK){
  163.         if (is_enrolling == 1){
  164.             int8_t left_sample_face = enroll_face(&id_list, aligned_face);
  165.  
  166.             if(left_sample_face == (ENROLL_CONFIRM_TIMES - 1)){
  167.                 Serial.printf("Enrolling Face ID: %d\n", id_list.tail);
  168.             }
  169.             Serial.printf("Enrolling Face ID: %d sample %d\n", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
  170.             rgb_printf(image_matrix, FACE_COLOR_CYAN, "ID[%u] Sample[%u]", id_list.tail, ENROLL_CONFIRM_TIMES - left_sample_face);
  171.             if (left_sample_face == 0){
  172.                 is_enrolling = 0;
  173.                 Serial.printf("Enrolled Face ID: %d\n", id_list.tail);
  174.             }
  175.         } else {
  176.             matched_id = recognize_face(&id_list, aligned_face);
  177.             if (matched_id >= 0) {
  178.                 Serial.printf("Match Face ID: %u\n", matched_id);
  179.                 rgb_printf(image_matrix, FACE_COLOR_GREEN, "Hello Subject %u", matched_id);
  180.             } else {
  181.                 Serial.println("No Match Found");
  182.                 rgb_print(image_matrix, FACE_COLOR_RED, "Intruder Alert!");
  183.                 matched_id = -1;
  184.             }
  185.         }
  186.     } else {
  187.         Serial.println("Face Not Aligned");
  188.         //rgb_print(image_matrix, FACE_COLOR_YELLOW, "Human Detected");
  189.     }
  190.  
  191.     dl_matrix3du_free(aligned_face);
  192.     return matched_id;
  193. }
  194.  
  195. static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len){
  196.     jpg_chunking_t *j = (jpg_chunking_t *)arg;
  197.     if(!index){
  198.         j->len = 0;
  199.     }
  200.     if(httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK){
  201.         return 0;
  202.     }
  203.     j->len += len;
  204.     return len;
  205. }
  206.  
  207. static esp_err_t capture_handler(httpd_req_t *req){
  208.     camera_fb_t * fb = NULL;
  209.     esp_err_t res = ESP_OK;
  210.     int64_t fr_start = esp_timer_get_time();
  211.  
  212.     fb = esp_camera_fb_get();
  213.     if (!fb) {
  214.         Serial.println("Camera capture failed");
  215.         httpd_resp_send_500(req);
  216.         return ESP_FAIL;
  217.     }
  218.  
  219.     httpd_resp_set_type(req, "image/jpeg");
  220.     httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
  221.     httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  222.  
  223.     size_t out_len, out_width, out_height;
  224.     uint8_t * out_buf;
  225.     bool s;
  226.     bool detected = false;
  227.     int face_id = 0;
  228.     if(!detection_enabled || fb->width > 400){
  229.         size_t fb_len = 0;
  230.         if(fb->format == PIXFORMAT_JPEG){
  231.             fb_len = fb->len;
  232.             res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
  233.         } else {
  234.             jpg_chunking_t jchunk = {req, 0};
  235.             res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL;
  236.             httpd_resp_send_chunk(req, NULL, 0);
  237.             fb_len = jchunk.len;
  238.         }
  239.         esp_camera_fb_return(fb);
  240.         int64_t fr_end = esp_timer_get_time();
  241.         Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000));
  242.         return res;
  243.     }
  244.  
  245.     dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
  246.     if (!image_matrix) {
  247.         esp_camera_fb_return(fb);
  248.         Serial.println("dl_matrix3du_alloc failed");
  249.         httpd_resp_send_500(req);
  250.         return ESP_FAIL;
  251.     }
  252.  
  253.     out_buf = image_matrix->item;
  254.     out_len = fb->width * fb->height * 3;
  255.     out_width = fb->width;
  256.     out_height = fb->height;
  257.  
  258.     s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf);
  259.     esp_camera_fb_return(fb);
  260.     if(!s){
  261.         dl_matrix3du_free(image_matrix);
  262.         Serial.println("to rgb888 failed");
  263.         httpd_resp_send_500(req);
  264.         return ESP_FAIL;
  265.     }
  266.  
  267.     box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config);
  268.  
  269.     if (net_boxes){
  270.         detected = true;
  271.         if(recognition_enabled){
  272.             face_id = run_face_recognition(image_matrix, net_boxes);
  273.         }
  274.         draw_face_boxes(image_matrix, net_boxes, face_id);
  275.         free(net_boxes->score);
  276.         free(net_boxes->box);
  277.         free(net_boxes->landmark);
  278.         free(net_boxes);
  279.     }
  280.  
  281.     jpg_chunking_t jchunk = {req, 0};
  282.     s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk);
  283.     dl_matrix3du_free(image_matrix);
  284.     if(!s){
  285.         Serial.println("JPEG compression failed");
  286.         return ESP_FAIL;
  287.     }
  288.  
  289.     int64_t fr_end = esp_timer_get_time();
  290.     Serial.printf("FACE: %uB %ums %s%d\n", (uint32_t)(jchunk.len), (uint32_t)((fr_end - fr_start)/1000), detected?"DETECTED ":"", face_id);
  291.     return res;
  292. }
  293.  
  294. static esp_err_t stream_handler(httpd_req_t *req){
  295.     camera_fb_t * fb = NULL;
  296.     esp_err_t res = ESP_OK;
  297.     size_t _jpg_buf_len = 0;
  298.     uint8_t * _jpg_buf = NULL;
  299.     char * part_buf[64];
  300.     dl_matrix3du_t *image_matrix = NULL;
  301.     bool detected = false;
  302.     int face_id = 0;
  303.     int64_t fr_start = 0;
  304.     int64_t fr_ready = 0;
  305.     int64_t fr_face = 0;
  306.     int64_t fr_recognize = 0;
  307.     int64_t fr_encode = 0;
  308.  
  309.     static int64_t last_frame = 0;
  310.     if(!last_frame) {
  311.         last_frame = esp_timer_get_time();
  312.     }
  313.  
  314.     res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  315.     if(res != ESP_OK){
  316.         return res;
  317.     }
  318.  
  319.     httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  320.  
  321.     while(true){
  322.         detected = false;
  323.         face_id = 0;
  324.         fb = esp_camera_fb_get();
  325.         if (!fb) {
  326.             Serial.println("Camera capture failed");
  327.             res = ESP_FAIL;
  328.         } else {
  329.             fr_start = esp_timer_get_time();
  330.             fr_ready = fr_start;
  331.             fr_face = fr_start;
  332.             fr_encode = fr_start;
  333.             fr_recognize = fr_start;
  334.             if(!detection_enabled || fb->width > 400){
  335.                 if(fb->format != PIXFORMAT_JPEG){
  336.                     bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
  337.                     esp_camera_fb_return(fb);
  338.                     fb = NULL;
  339.                     if(!jpeg_converted){
  340.                         Serial.println("JPEG compression failed");
  341.                         res = ESP_FAIL;
  342.                     }
  343.                 } else {
  344.                     _jpg_buf_len = fb->len;
  345.                     _jpg_buf = fb->buf;
  346.                 }
  347.             } else {
  348.  
  349.                 image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
  350.  
  351.                 if (!image_matrix) {
  352.                     Serial.println("dl_matrix3du_alloc failed");
  353.                     res = ESP_FAIL;
  354.                 } else {
  355.                     if(!fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item)){
  356.                         Serial.println("fmt2rgb888 failed");
  357.                         res = ESP_FAIL;
  358.                     } else {
  359.                         fr_ready = esp_timer_get_time();
  360.                         box_array_t *net_boxes = NULL;
  361.                         if(detection_enabled){
  362.                             net_boxes = face_detect(image_matrix, &mtmn_config);
  363.                         }
  364.                         fr_face = esp_timer_get_time();
  365.                         fr_recognize = fr_face;
  366.                         if (net_boxes || fb->format != PIXFORMAT_JPEG){
  367.                             if(net_boxes){
  368.                                 detected = true;
  369.                                 if(recognition_enabled){
  370.                                     face_id = run_face_recognition(image_matrix, net_boxes);
  371.                                 }
  372.                                 fr_recognize = esp_timer_get_time();
  373.                                 draw_face_boxes(image_matrix, net_boxes, face_id);
  374.                                 free(net_boxes->score);
  375.                                 free(net_boxes->box);
  376.                                 free(net_boxes->landmark);
  377.                                 free(net_boxes);
  378.                             }
  379.                             if(!fmt2jpg(image_matrix->item, fb->width*fb->height*3, fb->width, fb->height, PIXFORMAT_RGB888, 90, &_jpg_buf, &_jpg_buf_len)){
  380.                                 Serial.println("fmt2jpg failed");
  381.                                 res = ESP_FAIL;
  382.                             }
  383.                             esp_camera_fb_return(fb);
  384.                             fb = NULL;
  385.                         } else {
  386.                             _jpg_buf = fb->buf;
  387.                             _jpg_buf_len = fb->len;
  388.                         }
  389.                         fr_encode = esp_timer_get_time();
  390.                     }
  391.                     dl_matrix3du_free(image_matrix);
  392.                 }
  393.             }
  394.         }
  395.         if(res == ESP_OK){
  396.             size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
  397.             res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
  398.         }
  399.         if(res == ESP_OK){
  400.             res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
  401.         }
  402.         if(res == ESP_OK){
  403.             res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
  404.         }
  405.         if(fb){
  406.             esp_camera_fb_return(fb);
  407.             fb = NULL;
  408.             _jpg_buf = NULL;
  409.         } else if(_jpg_buf){
  410.             free(_jpg_buf);
  411.             _jpg_buf = NULL;
  412.         }
  413.         if(res != ESP_OK){
  414.             break;
  415.         }
  416.         int64_t fr_end = esp_timer_get_time();
  417.  
  418.         int64_t ready_time = (fr_ready - fr_start)/1000;
  419.         int64_t face_time = (fr_face - fr_ready)/1000;
  420.         int64_t recognize_time = (fr_recognize - fr_face)/1000;
  421.         int64_t encode_time = (fr_encode - fr_recognize)/1000;
  422.         int64_t process_time = (fr_encode - fr_start)/1000;
  423.         
  424.         int64_t frame_time = fr_end - last_frame;
  425.         last_frame = fr_end;
  426.         frame_time /= 1000;
  427.         uint32_t avg_frame_time = ra_filter_run(&ra_filter, frame_time);
  428.         Serial.printf("MJPG: %uB %ums (%.1ffps), AVG: %ums (%.1ffps), %u+%u+%u+%u=%u %s%d\n",
  429.             (uint32_t)(_jpg_buf_len),
  430.             (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time,
  431.             avg_frame_time, 1000.0 / avg_frame_time,
  432.             (uint32_t)ready_time, (uint32_t)face_time, (uint32_t)recognize_time, (uint32_t)encode_time, (uint32_t)process_time,
  433.             (detected)?"DETECTED ":"", face_id
  434.         );
  435.     }
  436.  
  437.     last_frame = 0;
  438.     return res;
  439. }
  440.  
  441. static esp_err_t cmd_handler(httpd_req_t *req){
  442.     char*  buf;
  443.     size_t buf_len;
  444.     char variable[32] = {0,};
  445.     char value[32] = {0,};
  446.  
  447.     buf_len = httpd_req_get_url_query_len(req) + 1;
  448.     if (buf_len > 1) {
  449.         buf = (char*)malloc(buf_len);
  450.         if(!buf){
  451.             httpd_resp_send_500(req);
  452.             return ESP_FAIL;
  453.         }
  454.         if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
  455.             if (httpd_query_key_value(buf, "var", variable, sizeof(variable)) == ESP_OK &&
  456.                 httpd_query_key_value(buf, "val", value, sizeof(value)) == ESP_OK) {
  457.             } else {
  458.                 free(buf);
  459.                 httpd_resp_send_404(req);
  460.                 return ESP_FAIL;
  461.             }
  462.         } else {
  463.             free(buf);
  464.             httpd_resp_send_404(req);
  465.             return ESP_FAIL;
  466.         }
  467.         free(buf);
  468.     } else {
  469.         httpd_resp_send_404(req);
  470.         return ESP_FAIL;
  471.     }
  472.  
  473.     int val = atoi(value);
  474.     sensor_t * s = esp_camera_sensor_get();
  475.     int res = 0;
  476.  
  477.     if(!strcmp(variable, "framesize")) {
  478.         if(s->pixformat == PIXFORMAT_JPEG) res = s->set_framesize(s, (framesize_t)val);
  479.     }
  480.     else if(!strcmp(variable, "quality")) res = s->set_quality(s, val);
  481.     else if(!strcmp(variable, "contrast")) res = s->set_contrast(s, val);
  482.     else if(!strcmp(variable, "brightness")) res = s->set_brightness(s, val);
  483.     else if(!strcmp(variable, "saturation")) res = s->set_saturation(s, val);
  484.     else if(!strcmp(variable, "gainceiling")) res = s->set_gainceiling(s, (gainceiling_t)val);
  485.     else if(!strcmp(variable, "awb")) res = s->set_whitebal(s, val);
  486.     else if(!strcmp(variable, "hmirror")) res = s->set_hmirror(s, val);
  487.     else if(!strcmp(variable, "vflip")) res = s->set_vflip(s, val);
  488.     else if(!strcmp(variable, "dcw")) res = s->set_dcw(s, val);
  489.     else if(!strcmp(variable, "colorbar")) 
  490.       if(val == 1)
  491.         LedSwVal=3;
  492.       else
  493.         LedSwVal=4;
  494.     else if(!strcmp(variable, "face_detect")) {
  495.         detection_enabled = val;
  496.         if(!detection_enabled) {
  497.             recognition_enabled = 0;
  498.         }
  499.     }
  500.     else if(!strcmp(variable, "face_enroll")) is_enrolling = val;
  501.     else if(!strcmp(variable, "face_recognize")) {
  502.         recognition_enabled = val;
  503.         if(recognition_enabled){
  504.             detection_enabled = val;
  505.         }
  506.     }
  507.     else {
  508.         res = -1;
  509.     }
  510.  
  511.     if(res){
  512.         return httpd_resp_send_500(req);
  513.     }
  514.  
  515.     httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  516.     return httpd_resp_send(req, NULL, 0);
  517. }
  518.  
  519. static esp_err_t status_handler(httpd_req_t *req){
  520.     static char json_response[1024];
  521.  
  522.     sensor_t * s = esp_camera_sensor_get();
  523.     char * p = json_response;
  524.     *p++ = '{';
  525.  
  526.     p+=sprintf(p, "\"framesize\":%u,", s->status.framesize);
  527.     p+=sprintf(p, "\"quality\":%u,", s->status.quality);
  528.     p+=sprintf(p, "\"brightness\":%d,", s->status.brightness);
  529.     p+=sprintf(p, "\"contrast\":%d,", s->status.contrast);
  530.     p+=sprintf(p, "\"saturation\":%d,", s->status.saturation);
  531.     p+=sprintf(p, "\"sharpness\":%d,", s->status.sharpness);
  532.     p+=sprintf(p, "\"special_effect\":%u,", s->status.special_effect);
  533.     p+=sprintf(p, "\"wb_mode\":%u,", s->status.wb_mode);
  534.     p+=sprintf(p, "\"awb\":%u,", s->status.awb);
  535.     p+=sprintf(p, "\"awb_gain\":%u,", s->status.awb_gain);
  536.     p+=sprintf(p, "\"aec\":%u,", s->status.aec);
  537.     p+=sprintf(p, "\"aec2\":%u,", s->status.aec2);
  538.     p+=sprintf(p, "\"ae_level\":%d,", s->status.ae_level);
  539.     p+=sprintf(p, "\"aec_value\":%u,", s->status.aec_value);
  540.     p+=sprintf(p, "\"agc\":%u,", s->status.agc);
  541.     p+=sprintf(p, "\"agc_gain\":%u,", s->status.agc_gain);
  542.     p+=sprintf(p, "\"gainceiling\":%u,", s->status.gainceiling);
  543.     p+=sprintf(p, "\"bpc\":%u,", s->status.bpc);
  544.     p+=sprintf(p, "\"wpc\":%u,", s->status.wpc);
  545.     p+=sprintf(p, "\"raw_gma\":%u,", s->status.raw_gma);
  546.     p+=sprintf(p, "\"lenc\":%u,", s->status.lenc);
  547.     p+=sprintf(p, "\"vflip\":%u,", s->status.vflip);
  548.     p+=sprintf(p, "\"hmirror\":%u,", s->status.hmirror);
  549.     p+=sprintf(p, "\"dcw\":%u,", s->status.dcw);
  550.     p+=sprintf(p, "\"colorbar\":%u,", s->status.colorbar);
  551.     p+=sprintf(p, "\"face_detect\":%u,", detection_enabled);
  552.     p+=sprintf(p, "\"face_enroll\":%u,", is_enrolling);
  553.     p+=sprintf(p, "\"face_recognize\":%u", recognition_enabled);
  554.     *p++ = '}';
  555.     *p++ = 0;
  556.     httpd_resp_set_type(req, "application/json");
  557.     httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  558.     return httpd_resp_send(req, json_response, strlen(json_response));
  559. }
  560.  
  561. static esp_err_t index_handler(httpd_req_t *req){
  562.     httpd_resp_set_type(req, "text/html");
  563.     httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
  564.     sensor_t * s = esp_camera_sensor_get();
  565.     if (s->id.PID == OV3660_PID) {
  566.         return httpd_resp_send(req, (const char *)index_ov3660_html_gz, index_ov3660_html_gz_len);
  567.     }
  568.     return httpd_resp_send(req, (const char *)index_ov2640_html_gz, index_ov2640_html_gz_len);
  569. }
  570.  
  571. void startCameraServer(){
  572.     httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  573.  
  574.     httpd_uri_t index_uri = {
  575.         .uri       = "/",
  576.         .method    = HTTP_GET,
  577.         .handler   = index_handler,
  578.         .user_ctx  = NULL
  579.     };
  580.  
  581.     httpd_uri_t status_uri = {
  582.         .uri       = "/status",
  583.         .method    = HTTP_GET,
  584.         .handler   = status_handler,
  585.         .user_ctx  = NULL
  586.     };
  587.  
  588.     httpd_uri_t cmd_uri = {
  589.         .uri       = "/control",
  590.         .method    = HTTP_GET,
  591.         .handler   = cmd_handler,
  592.         .user_ctx  = NULL
  593.     };
  594.  
  595.     httpd_uri_t capture_uri = {
  596.         .uri       = "/capture",
  597.         .method    = HTTP_GET,
  598.         .handler   = capture_handler,
  599.         .user_ctx  = NULL
  600.     };
  601.  
  602.    httpd_uri_t stream_uri = {
  603.         .uri       = "/stream",
  604.         .method    = HTTP_GET,
  605.         .handler   = stream_handler,
  606.         .user_ctx  = NULL
  607.     };
  608.  
  609.  
  610.     ra_filter_init(&ra_filter, 20);
  611.     
  612.     mtmn_config.type = FAST;
  613.     mtmn_config.min_face = 80;
  614.     mtmn_config.pyramid = 0.707;
  615.     mtmn_config.pyramid_times = 4;
  616.     mtmn_config.p_threshold.score = 0.6;
  617.     mtmn_config.p_threshold.nms = 0.7;
  618.     mtmn_config.p_threshold.candidate_number = 20;
  619.     mtmn_config.r_threshold.score = 0.7;
  620.     mtmn_config.r_threshold.nms = 0.7;
  621.     mtmn_config.r_threshold.candidate_number = 10;
  622.     mtmn_config.o_threshold.score = 0.7;
  623.     mtmn_config.o_threshold.nms = 0.7;
  624.     mtmn_config.o_threshold.candidate_number = 1;
  625.     
  626.     face_id_init(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES);
  627.     
  628.     Serial.printf("Starting web server on port: '%d'\n", config.server_port);
  629.     if (httpd_start(&camera_httpd, &config) == ESP_OK) {
  630.         httpd_register_uri_handler(camera_httpd, &index_uri);
  631.         httpd_register_uri_handler(camera_httpd, &cmd_uri);
  632.         httpd_register_uri_handler(camera_httpd, &status_uri);
  633.         httpd_register_uri_handler(camera_httpd, &capture_uri);
  634.     }
  635.  
  636.     config.server_port += 1;
  637.     config.ctrl_port += 1;
  638.     Serial.printf("Starting stream server on port: '%d'\n", config.server_port);
  639.     if (httpd_start(&stream_httpd, &config) == ESP_OK) {
  640.         httpd_register_uri_handler(stream_httpd, &stream_uri);
  641.     }
  642. }
  643.  
  644. int getLedSw() { // send the value of 3 for ON or 4 for OFF to turn the floodlight on/off
  645.  return LedSwVal;
  646. }

修改後的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點亮時所實際拍攝畫面


沒有留言:

張貼留言

三、使用Line Notify傳送照片之安全監控系統之二---低功耗篇

在前一個章節中 , 我們建構了一個標準照片擷取 、 傳送與儲存的按全監控裝置 , 不過假如我們使用的場域中有許多的地方都必須按裝這類的裝置時 , 例如在一個有許多門 、 窗的家庭或辦公室 , 由於我們的系統使用無線WiFi作為信號傳輸之用 , 所以信號的傳輸除非裝置離WiFi分享...