ESP32-CAM: upgrade CameraWebServer with flash feature – 2

Spread the love

ESP32-CAM clone upgrade web interface with flash light
ESP32-CAM clone upgrade web interface with the flashlight

I use this esp32-cam for all my monitoring systems and videos with multiple windows. The integrated led feature in the esp32-cam board can be very useful, so I thought about adding the control on the web interface.

But remember, the flash pin is the same as SD, so you must stop using SD if you want led features.

ESP32 CAM pinout
ESP32 CAM pinout

Now you can refer to the article “ESP32-CAM: pinout, specs and Arduino IDE configuration” to have additional information on uploading sketches. In the pinout, you can find Flashlight on GPIO4.

You can find ESP32-CAM on AliExpress ESP32 Dev Kit v1 - AliExpress selectable - AliExpress TTGO T-Display 1.14 ESP32 - AliExpress NodeMCU V3 V2 ESP8266 Lolin32 - AliExpress WeMos Lolin32 CP2104 CH340 - AliExpress ESP32-CAM programmer - AliExpress ESP32-CAM bundle - AliExpress ESP32-WROOM-32 - AliExpress ESP32-S

Retrieve the sketch for editing

This example work with 1.0.6 version of ESP32 Framework, for the 2.x.x or more go to the relative section.

First, download the CameraWebServer from here or by opening the example in Arduino or directly from your esp32 core installation with these steps:

Retrieve the preferences path from your Arduino IDE
File --> Preferences

ESP32 S2 download Arduino IDE preferences folder
ESP32 S2 download Arduino IDE preferences folder

from here, go to


You can find in that folder four file

  • CameraWebServer.ino: the main sketch;
  • app_httpd.cpp: practically is the back end of the application;
  • camera_pins.h: contains the pins schema for all variants;
  • camera_index.h: contains the web page in base64 gzipped version.

Extract the HTML page

Now, as you can see, we don’t find the HTML page, but you can use this utility to transform the HEX array from camera_index.h into the classic HTML file.

Online converter: HEX Array to file

copy the HEX array values and paste in the text area (from first { to the last } excluded) and insert index_ov2640.html.gz as filename, download the GZip file, extract index_ov2640.html and put it in your folder, open with an editor.

Now we have all material to apply a change to CameraWebServer.

In the HTML page, find the last slider (face_recognize-group), as in the following code

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

and add flash-group like this

                        <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 class="input-group" id="flash-group">
                            <label for="flash">Flash</label>
                            <div class="switch">
                                <input id="flash" type="checkbox" class="default-action">
                                <label class="slider" for="flash"></label>
                        <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>

Back-end end-point

All the elements with class default-action had a listener

  // Attach default on change action
    .forEach(el => {
      el.onchange = () => updateConfig(el)

that on change call a function updateConfig

  function updateConfig (el) {
    let value
    switch (el.type) {
      case 'checkbox':
        value = el.checked ? 1 : 0
      case 'range':
      case 'select-one':
        value = el.value
      case 'button':
      case 'submit':
        value = '1'

    const query = `${baseHost}/control?var=${}&val=${value}`

      .then(response => {
        console.log(`request to ${query} finished, status: ${response.status}`)

this function does a GET (refer to the tutorial “How to create a REST server“) that call a handler specified in the file app_https.cpp

static esp_err_t cmd_handler(httpd_req_t *req){
    char*  buf;
    size_t buf_len;
    char variable[32] = {0,};
    char value[32] = {0,};

    buf_len = httpd_req_get_url_query_len(req) + 1;
    if (buf_len > 1) {
        buf = (char*)malloc(buf_len);
            return ESP_FAIL;
        if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
            if (httpd_query_key_value(buf, "var", variable, sizeof(variable)) == ESP_OK &&
                httpd_query_key_value(buf, "val", value, sizeof(value)) == ESP_OK) {
            } else {
                return ESP_FAIL;
        } else {
            return ESP_FAIL;
    } else {
        return ESP_FAIL;

    int val = atoi(value);
    sensor_t * s = esp_camera_sensor_get();
    int res = 0;

    if(!strcmp(variable, "framesize")) {
        if(s->pixformat == PIXFORMAT_JPEG) res = s->set_framesize(s, (framesize_t)val);
    else if(!strcmp(variable, "quality")) res = s->set_quality(s, val);
    else if(!strcmp(variable, "contrast")) res = s->set_contrast(s, val);
    else if(!strcmp(variable, "brightness")) res = s->set_brightness(s, val);
    else if(!strcmp(variable, "saturation")) res = s->set_saturation(s, val);
    else if(!strcmp(variable, "gainceiling")) res = s->set_gainceiling(s, (gainceiling_t)val);
    else if(!strcmp(variable, "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;
            detection_enabled = val;
    else if(!strcmp(variable, "flash") ) {
		 pinMode(4, OUTPUT);
		 digitalWrite(4, atoi(value));
		 flash_enabled = atoi(value);
    else {
        res = -1;

        return httpd_resp_send_500(req);

    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    return httpd_resp_send(req, NULL, 0);

The handler manages all the actions of the web interface. We can also add flash management digitalWrite on pin 4 (builtin led) with the slider’s value.

You can call the end-point directly from the browser with


to activate the flash, and


to deactivate.

Set an initial state

We also set the value on the flash_enabled variable, declared with the other state variable

ESP32-cam IP camera flash on 3D printer with octoprint
ESP32-cam IP camera flash on a 3D printer with Octoprint
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;
static int8_t is_enrolling = 0;
static face_id_list id_list = {0};

used to restore the value on the end-point in GET status, and next managed by status_handler

static esp_err_t status_handler(httpd_req_t *req){
    static char json_response[1024];

    sensor_t * s = esp_camera_sensor_get();
    char * p = json_response;
    *p++ = '{';

    p+=sprintf(p, "\"framesize\":%u,", s->status.framesize);
    p+=sprintf(p, "\"quality\":%u,", s->status.quality);
    p+=sprintf(p, "\"brightness\":%d,", s->status.brightness);
    p+=sprintf(p, "\"contrast\":%d,", s->status.contrast);
    p+=sprintf(p, "\"saturation\":%d,", s->status.saturation);
    p+=sprintf(p, "\"sharpness\":%d,", s->status.sharpness);
    p+=sprintf(p, "\"special_effect\":%u,", s->status.special_effect);
    p+=sprintf(p, "\"wb_mode\":%u,", s->status.wb_mode);
    p+=sprintf(p, "\"awb\":%u,", s->status.awb);
    p+=sprintf(p, "\"awb_gain\":%u,", s->status.awb_gain);
    p+=sprintf(p, "\"aec\":%u,", s->status.aec);
    p+=sprintf(p, "\"aec2\":%u,", s->status.aec2);
    p+=sprintf(p, "\"ae_level\":%d,", s->status.ae_level);
    p+=sprintf(p, "\"aec_value\":%u,", s->status.aec_value);
    p+=sprintf(p, "\"agc\":%u,", s->status.agc);
    p+=sprintf(p, "\"agc_gain\":%u,", s->status.agc_gain);
    p+=sprintf(p, "\"gainceiling\":%u,", s->status.gainceiling);
    p+=sprintf(p, "\"bpc\":%u,", s->status.bpc);
    p+=sprintf(p, "\"wpc\":%u,", s->status.wpc);
    p+=sprintf(p, "\"raw_gma\":%u,", s->status.raw_gma);
    p+=sprintf(p, "\"lenc\":%u,", s->status.lenc);
    p+=sprintf(p, "\"vflip\":%u,", s->status.vflip);
    p+=sprintf(p, "\"hmirror\":%u,", s->status.hmirror);
    p+=sprintf(p, "\"dcw\":%u,", s->status.dcw);
    p+=sprintf(p, "\"colorbar\":%u,", s->status.colorbar);
    p+=sprintf(p, "\"face_detect\":%u,", detection_enabled);
    p+=sprintf(p, "\"face_enroll\":%u,", is_enrolling);
    p+=sprintf(p, "\"face_recognize\":%u,", recognition_enabled);
    p+=sprintf(p, "\"flash\":%u", flash_enabled);
    *p++ = '}';
    *p++ = 0;
    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    return httpd_resp_send(req, json_response, strlen(json_response));

to show the change, you must only put the URL on the browser (It’s GET)


with this result

  "framesize": 4,
  "quality": 10,
  "brightness": 0,
  "contrast": 0,
  "saturation": 0,
  "sharpness": 0,
  "special_effect": 0,
  "wb_mode": 0,
  "awb": 1,
  "awb_gain": 1,
  "aec": 1,
  "aec2": 0,
  "ae_level": 0,
  "aec_value": 168,
  "agc": 1,
  "agc_gain": 0,
  "gainceiling": 0,
  "bpc": 0,
  "wpc": 1,
  "raw_gma": 1,
  "lenc": 1,
  "vflip": 0,
  "hmirror": 0,
  "dcw": 1,
  "colorbar": 0,
  "face_detect": 0,
  "face_enroll": 0,
  "face_recognize": 0,
  "flash": 0

which is called upon initialization of the web interface

  // read initial values
    .then(function (response) {
      return response.json()
    .then(function (state) {
        .forEach(el => {
          updateValue(el, state[], false)

and set the correct value of the slider at the first load.

Re-Conversion to HEX array

When you change the page, you must regenerate the HEX array from the file in GZipped format. To simplify the operation on my “File to GZipped filearray” I generate the “plain” and gzipped filerray in one step.

Remember that the device wants a quiet amount of power

Operation modePower
Stand by80mHa
In streaming100~160mAh
In streaming with flash270mAh

So to use It correctly, you must connect the device to an external power source

esp32-cam upload sketch and normal connection schema 5v external power
esp32-cam upload sketch and standard connection schema 5v external power

Very bright the LED, but the amperage is relevant.

ESP32-cam IP camera flash opened with tester
ESP32-cam IP camera flash opened with the tester.

Use CameraWebServer of Framework version 2.x.x

In the new version, Espressiff has changed the face recognition image and added the management of flash features in the IDF framework.

To activate that features, the process is quite simple.

You must add these lines after the camera_config_t config; settings in the main CameraWebServer.ino file.

  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.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG; // for streaming
  //config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  const int pwmfreq = 50000;     // 50K pwm frequency
  const int pwmresolution = 9;   // duty cycle bit range
  ledcSetup(config.ledc_channel, pwmfreq, pwmresolution);  // configure LED PWM channel
  ledcAttachPin(4, config.ledc_channel);            // attach the GPIO pin to the channel

And in the first part of app_attpd.cpp file after the includes these defines.

// LED Illuminator
// end of LED Illuminator

The result is in this screenshot.

New ESP32 cam (framework 2.x.x) CameraWebServer with flash
New ESP32 cam (framework 2.x.x) CameraWebServer with flash

Set a static IP

Another very useful thing is to set the static IP. To do this, add this line before WiFi.begin

  IPAddress ip(192,168,1,40);
  IPAddress gateway(192,168,1,1);
  IPAddress subnet(255,255,255,0);
  WiFi.config(ip, gateway, subnet);


  1. ESP32-CAM: pinout, specs and Arduino IDE configuration
  2. ESP32-CAM: upgrade CamerWebServer with flash feature
  3. ESP32-CAM: control CameraWebServer from your own web page

Firmware with additional features.

Spread the love

15 Responses

  1. rodyeo says:

    The ESP32-CAM Flight Light LED Illuminator solution which everyone is awaiting is now resolved by the blog author and programmer Bye Renzo

    Thank you 😉

  2. Rodney Yeo says:

    Thanks you for releasing the “Use CameraWebServer of Framework version 2.x.x” 🙂

  3. Tomas Pilny says:

    Hi, this is a great improvement. I would like to ask you to open a PR into Arduino-ESP32 making this available as a default version for everyone.
    In general, Espressif is happy to receive community PRs. On Arduino-ESP32 support, we are just a small team, but the community is large and we don’t refuse good help.

  4. tomek says:

    how I can download, compile and write without manual modification? all the time I have errors (maybe im doing something wrong)

Leave a Reply

Your email address will not be published. Required fields are marked *