ESP32-CAMのCameraWebServerにLEDフラッシュボタンを追加する

# はじめに
* 前回、[1300円のESP32-CAMでWebカメラを試す](https://qiita.com/Nabeshin/items/b195cad1afe99ce29f1e)でサンプルコードのCameraWebServerを動かしてみた。
* さらにESP32-CAMには大光量のLEDフラッシュライトが実装されており、暗所での撮影が可能。
* ESP32-CAMの回路図を参考に、サンプルコード(CameraWebServer)にLEDライトをON/OFFする機能を追加してみた。


![IMG_2341.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/42f8e1d8-bb7e-5b61-6d8d-afb467ff4108.jpeg)


# 開発環境
* iMac
* Arduino IDE

# 機能追加ポイント
* 実際に自分で書くコードは数行程度。
* ESP32-CAMにLEDフラッシュライトが実装されているのはGPIO 4。
* これを制御するコードをCameraWebServerを構成する以下のファイルに追加する。

```
iMac-nabeshin:~ nabeshin$ ls -l ~/Documents/Arduino/ESP32/CameraWebServer/
total 144
-rw-r--r--  1 nabeshin  staff   3812  4 25 00:08 CameraWebServer.ino
-rw-r--r--  1 nabeshin  staff  23610  4 25 00:28 app_httpd.cpp
-rw-r--r--  1 nabeshin  staff  44846  4 25 00:12 camera_index.h
```

* うち、以下についてはArduinoIDEで修正可能。
 * CameraWebServer.ino
 * app_httpd.cpp 
* 一方、camera_index.h については若干めんどくさい。
 * [ここによると](https://github.com/espressif/esp-who/issues/21) camera_index.hの中身はHTMLファイルをgz圧縮した物をヘキサ変換したものらしい。
 * （Content-Encodingをgzipにするとブラウザって容量縮小のためにgz圧縮したHTMLを展開して表示することができるのね。。。知らなかった。）
 * Arduinoのサンプルコードに含まれるのは変換後のコードなので、変換前のHTMLファイルを以下から取得する必要がある。
 * [https://github.com/espressif/esp-who/blob/master/examples/single_chip/camera_web_server/main/www/index_ov2640.html](https://github.com/espressif/esp-who/blob/master/examples/single_chip/camera_web_server/main/www/index_ov2640.html)
 * これを編集後、別途以下の変換プログラムを使わせて頂き、出力される結果をcamera_index.hのindex_html_gz[]の中身の値としてコピペする。
 * [https://gist.github.com/me-no-dev/f137a950ce6dedb641d427d8db6355d2](https://gist.github.com/me-no-dev/f137a950ce6dedb641d427d8db6355d2) 



# コードの修正
## CameraWebServer.ino
* setup()関数の最終行にLEDライトが実装されている GPIO 4 を出力モードに指定するコードを追加する。

```
void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  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;
  }

  // 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;
  }

  //drop down frame size for higher initial frame rate
  sensor_t * s = esp_camera_sensor_get();
  s->set_framesize(s, FRAMESIZE_QVGA);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  startCameraServer();

  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");

  pinMode(4, OUTPUT);    //LEDフラッシュライト用 GPIO出力
}
```


## app_httpd.cpp
* static変数にLEDフラッシュライト制御変数(flash_enabled)を追加。(62行目あたり)
 * 0がOFF
 * 1がON

```
static mtmn_config_t mtmn_config = {0};
static int8_t detection_enabled = 0;
static int8_t recognition_enabled = 0;
static int8_t flash_enabled = 0;        //LEDフラッシュライト制御変数
```

* cmd_handler()関数に"flash_enabled"コマンド用のelse ifを追加(526行目あたり)
 * flash_enabledがTRUEだったらLEDをON
 * FALSEだったらLEDをOFF

```
    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, "colorbar")) res = s->set_colorbar(s, val);
    else if(!strcmp(variable, "awb")) res = s->set_whitebal(s, val);
    else if(!strcmp(variable, "agc")) res = s->set_gain_ctrl(s, val);
    else if(!strcmp(variable, "aec")) res = s->set_exposure_ctrl(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, "awb_gain")) res = s->set_awb_gain(s, val);
    else if(!strcmp(variable, "agc_gain")) res = s->set_agc_gain(s, val);
    else if(!strcmp(variable, "aec_value")) res = s->set_aec_value(s, val);
    else if(!strcmp(variable, "aec2")) res = s->set_aec2(s, val);
    else if(!strcmp(variable, "dcw")) res = s->set_dcw(s, val);
    else if(!strcmp(variable, "bpc")) res = s->set_bpc(s, val);
    else if(!strcmp(variable, "wpc")) res = s->set_wpc(s, val);
    else if(!strcmp(variable, "raw_gma")) res = s->set_raw_gma(s, val);
    else if(!strcmp(variable, "lenc")) res = s->set_lenc(s, val);
    else if(!strcmp(variable, "special_effect")) res = s->set_special_effect(s, val);
    else if(!strcmp(variable, "wb_mode")) res = s->set_wb_mode(s, val);
    else if(!strcmp(variable, "ae_level")) res = s->set_ae_level(s, val);
    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;
        }
    }
    //LEDフラッシュ制御処理
    else if(!strcmp(variable, "flash_enabled")) {
      flash_enabled = val;
      if(flash_enabled){
        digitalWrite(4, HIGH);  //LED ON
      } else{
        digitalWrite(4, LOW);   //LED OFF
      }
    }
    else {
        res = -1;
    }
```

* status_handler()関数にstatic変数(flash_enabled)を更新する処理を追加(580行目あたり)

```
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, "\"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+=sprintf(p, "\"flash_enabled\":%u", flash_enabled);          //HTMLからのLEDフラッシュ制御コマンド
    *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));
}
```

## camera_index.h
* 上述の通り、Arduino IDEには元のHTMLファイルが無いので、以下からindex_ov2640.htmlを取得し、ローカルに保存する。
 * [https://github.com/espressif/esp-who/blob/master/examples/single_chip/camera_web_server/main/www/index_ov2640.html](https://github.com/espressif/esp-who/blob/master/examples/single_chip/camera_web_server/main/www/index_ov2640.html)
* フラッシュ制御用の「スライダ部品」を追加する。(548行目あたり)
 * 追加する場所は、nav要素(id="menu")のdiv要素(既存のボタン群)の最終行
 * input id は"flash_enabled"とする。
 * 表示名は"Flash Light"とする。

```
<!-- 長いので省略 -->

    <body>
        <section class="main">
            <div id="logo">
                <label for="nav-toggle-cb" id="nav-toggle">&#9776;&nbsp;&nbsp;Toggle settings</label>
            </div>
            <div id="content">
                <div id="sidebar">
                    <input type="checkbox" id="nav-toggle-cb" checked="checked">
                    <nav id="menu">

                        <!-- 長いので省略 -->

                        <div class="input-group" id="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>

                        <!-- LEDフラッシュON/OFF 制御用スイッチ ここから -->
                        <div class="input-group" id="flash_light-group">
                            <label for="flash_enabled">Flash Light</label>
                            <div class="switch">
                                <input id="flash_enabled" type="checkbox" class="default-action">
                                <label class="slider" for="flash_enabled"></label>
                            </div>
                        </div>
                        <!-- LEDフラッシュON/OFF 制御用スイッチ ここまで -->

                        <section id="buttons">
                            <button id="get-still">Get Still</button>
                            <button id="toggle-stream">Start Stream</button>
                            <button id="face_enroll" class="disabled" disabled="disabled">Enroll Face</button>
                        </section>
                    </nav>
                </div>
                <figure>
                    <div id="stream-container" class="image-container hidden">
                        <div class="close" id="close-stream">×</div>
                        <img id="stream" src="">
                    </div>
                </figure>
            </div>
        </section>
        <script>
```

* (作業ディレクトリは~/Downloads/filetoarrayとする)
* HTMLが編集できたら、圧縮してヘキサに変換する。
 * 上述のfiletoarray.cをダウンロードし、コンパイル。
 * [https://gist.github.com/me-no-dev/f137a950ce6dedb641d427d8db6355d2](https://gist.github.com/me-no-dev/f137a950ce6dedb641d427d8db6355d2)
 * 先ほどのindex_ov2640.htmlをgunzipし、引数に指定して実行し、cameraIndex.hを生成。
 * ここで"cameraIndex.h"は中間成果物であり、最終成果物であるArduino IDEのcamera_index.hとファイル名が異なるので注意。

```
iMac-nabeshin:~ nabeshin$ mkdir ~/Downloads/filetoarray
iMac-nabeshin:~ nabeshin$ cd Downloads/filetoarray/
iMac-nabeshin:filetoarray nabeshin$ ls -l
total 56
-rw-r--r--  1 nabeshin  staff   1007  4 24 23:50 filetoarray.c
-rw-r--r--@ 1 nabeshin  staff  20542  4 24 23:39 index_ov2640.html
iMac-nabeshin:filetoarray nabeshin$ gcc ./filetoarray.c 
iMac-nabeshin:filetoarray nabeshin$ gzip ./index_ov2640.html 
iMac-nabeshin:filetoarray nabeshin$ ls -l
total 40
-rwxr-xr-x  1 nabeshin  staff  9036  8 23 21:47 a.out
-rw-r--r--  1 nabeshin  staff  1007  4 24 23:50 filetoarray.c
-rw-r--r--@ 1 nabeshin  staff  3706  4 24 23:39 index_ov2640.html.gz
iMac-nabeshin:filetoarray nabeshin$ ./a.out index_ov2640.html.gz > cameraIndex.h
iMac-nabeshin:filetoarray nabeshin$ ls -l
total 88
-rwxr-xr-x  1 nabeshin  staff   9036  8 23 21:47 a.out
-rw-r--r--  1 nabeshin  staff  22600  8 23 21:49 cameraIndex.h
-rw-r--r--  1 nabeshin  staff   1007  4 24 23:50 filetoarray.c
-rw-r--r--@ 1 nabeshin  staff   3706  4 24 23:39 index_ov2640.html.gz
iMac-nabeshin:filetoarray nabeshin$ 
``` 

* cameraIndex.hをviで表示。

```
iMac-nabeshin:filetoarray nabeshin$ vi cameraIndex.h 
```

* この中から以下の２箇所をコピーし、Arduino IDEのcamera_index.hの該当部分に貼り付け。

### 1箇所目: HTMLサイズ

```

//File: index_ov2640.html.gz, Size: 3706
#define index_ov2640_html_gz_len 3706
```

* "3706"を、Arduino IDEのcamera_index.hの該当部分に貼り付ける。

```
//File: index.html.gz, Size: 3706
#define index_html_gz_len 3706
```

### 2箇所目: HTML本体データのヘキサ値

* index_ov2640_html_gz[] のヘキサ変換値を、丸ごとコピー。
 * { と } の間を丸ごとコピー。

```
const uint8_t index_ov2640_html_gz[] PROGMEM = {
 0x1F, 0x8B, 0x08, 0x08, 0x06, 0x75, 0xC0, 0x5C, 0x00, 0x03, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x5F,
 0x6F, 0x76, 0x32, 0x36, 0x34, 0x30, 0x2E, 0x68, 0x74, 0x6D, 0x6C, 0x00, 0xDD, 0x5C, 0xEB, 0x72,

//省略

 0x7E, 0x01, 0xC0, 0x66, 0x19, 0xD0, 0x93, 0x1A, 0x82, 0x19, 0x65, 0x3D, 0xEE, 0x85, 0xD0, 0xBA,
 0xEA, 0xD9, 0x6D, 0x66, 0xBC, 0xA9, 0x94, 0x47, 0xF2, 0x55, 0xFB, 0x8B, 0x63, 0xF9, 0x33, 0x81,
 0xFF, 0x07, 0x85, 0x41, 0x39, 0xE1, 0x3E, 0x50, 0x00, 0x00
};
```

* コピーしたヘキサ変換値をArduino IDEのcamera_index.hの該当部分に貼り付ける。
 * const uint8_t index_html_gz[] の { と } の間の値を上書きで貼り付け。

```
const uint8_t index_html_gz[] = {
 0x1F, 0x8B, 0x08, 0x08, 0x06, 0x75, 0xC0, 0x5C, 0x00, 0x03, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x5F,
 0x6F, 0x76, 0x32, 0x36, 0x34, 0x30, 0x2E, 0x68, 0x74, 0x6D, 0x6C, 0x00, 0xDD, 0x5C, 0xEB, 0x72,

//省略

 0x7E, 0x01, 0xC0, 0x66, 0x19, 0xD0, 0x93, 0x1A, 0x82, 0x19, 0x65, 0x3D, 0xEE, 0x85, 0xD0, 0xBA,
 0xEA, 0xD9, 0x6D, 0x66, 0xBC, 0xA9, 0x94, 0x47, 0xF2, 0x55, 0xFB, 0x8B, 0x63, 0xF9, 0x33, 0x81,
 0xFF, 0x07, 0x85, 0x41, 0x39, 0xE1, 0x3E, 0x50, 0x00, 0x00
};
```

# コンパイル＆書き込み
* Arduino IDEで[1300円のESP32-CAMでWebカメラを試す](https://qiita.com/Nabeshin/items/b195cad1afe99ce29f1e)の設定に従ってコンパイルし、ESP32-CAMにファームを書き込む。
* 手順ポイント
 * **IO0/GNDピンが接続されている事を確認して書き込む。**
 * **書き込めたらピンを外して再起動する。**

# 動作確認
* ESP32-CAMが取得したIPアドレスに接続。
* 左側のメニュー画面の下部に「Flash Light」のボタンが表示される。
* ボタンをONにすると、フラッシュLEDが発光する。

![スクリーンショット 2019-08-23 19.23.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/454c2d43-bce3-5b0f-b556-022c8c97be56.png)

# フラッシュ性能
* 普通のLEDとは違って直視できないほど明るい。
* これなら十分夜でも撮影可能。
* ただし、**かなり発熱するので、長時間発光した状態で絶対に素手で触らないように！！！**
 * 基板は持てる熱さですが、自分、無意識にLED部分を触ってしまい本気で火傷しました。。。

![IMG_2342.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/90af0f92-82e4-d472-1e3f-1e3a21c9f1a8.jpeg)


# 後述
* これと同じやり方で、空いてるGPIOを使ってWeb画面で色々制御できる。
 * とりあえず、タミヤのカムロボのモータを制御できるようにしてカメラ付き自走ロボットをお手軽に作ってみようと準備中。