Search code examples
ajaxhttpvideo-streamingwebserveresp32

Issue of ESP32 CAM using esp_http_server, webserver and WiFiClient libraries and Ajax for dual communication


I wish to make use of the arduino standard library of http server to make dual communication between the web page and the esp32 server with camera streaming through WiFi.

It seems I got my result in the screen, I got the camera video as well as some data (LED PWM value as well as antenna received signal strength) from the esp32, but I found continuous and intermittent errors of no response (status 404) in the network traffics of the web page. (Tests were done on both AP mode and Client mode of the esp32, and the results are the same)

My guess is that there may be crash between the libraries of esp_http_server, WebServer and WiFiClient, but it is out my knowledge limit to further investigate this. So I wish to find answer from you. Appreciate for any comment.

Here, I post my code as follow: (Acknowledgement: I modified the source code from "Random Nerd" to achieve the dual communication.)

/*********
  Modified from project at https://RandomNerdTutorials.com
  From Project: ESP32-CAM Remote Controlled Car Robot Web Server
  https://randomnerdtutorials.com/esp32-cam-car-robot-web-server/
  HM, 5 Nov 2023
*********/
// Sample server commands: (assume ip address of the ESP32 is 192.168.8.5)
// 192.168.8.5/control?var=forward&val=1
// 192.168.8.5/control?var=led_PWM&val=10
// If more httpd_query_key_value() is added, use "&" to seperate key/value pair
// 192.168.8.5:81/stream


#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
// #include <AsyncTCP.h>
// #include <ESPAsyncWebSrv.h> XXXX
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif

#include <WiFiClient.h>
#include <WebServer.h>
WebServer server(80);


#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "fb_gfx.h"
#include "soc/soc.h"              // disable brownout problems
#include "soc/rtc_cntl_reg.h"     // disable brownout problems
#include "esp_http_server.h"

#include "esp32-hal-ledc.h"       // For Buildin LED



#define Flashlight 4              // Build-in flashlight of ESP32-CAM Board
const int ledFreq = 1000;
const int ledChannel = 4;
const int ledResolution = 12;
int led_intensity = 0;

// Replace with your network credentials
const char* ssid = "SSID for Android phone or PC";
const char* password = "123456";



#define CAMERA_MODEL_AI_THINKER
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WITHOUT_PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM_B
//#define CAMERA_MODEL_WROVER_KIT

#if defined(CAMERA_MODEL_WROVER_KIT)
  #define PWDN_GPIO_NUM    -1
  #define RESET_GPIO_NUM   -1
  #define XCLK_GPIO_NUM    21
  #define SIOD_GPIO_NUM    26
  #define SIOC_GPIO_NUM    27
  
  #define Y9_GPIO_NUM      35
  #define Y8_GPIO_NUM      34
  #define Y7_GPIO_NUM      39
  #define Y6_GPIO_NUM      36
  #define Y5_GPIO_NUM      19
  #define Y4_GPIO_NUM      18
  #define Y3_GPIO_NUM       5
  #define Y2_GPIO_NUM       4
  #define VSYNC_GPIO_NUM   25
  #define HREF_GPIO_NUM    23
  #define PCLK_GPIO_NUM    22

#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       32
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_M5STACK_WITHOUT_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       17
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_AI_THINKER)
  #define PWDN_GPIO_NUM     32
  #define RESET_GPIO_NUM    -1
  #define XCLK_GPIO_NUM      0
  #define SIOD_GPIO_NUM     26
  #define SIOC_GPIO_NUM     27
  
  #define Y9_GPIO_NUM       35
  #define Y8_GPIO_NUM       34
  #define Y7_GPIO_NUM       39
  #define Y6_GPIO_NUM       36
  #define Y5_GPIO_NUM       21
  #define Y4_GPIO_NUM       19
  #define Y3_GPIO_NUM       18
  #define Y2_GPIO_NUM        5
  #define VSYNC_GPIO_NUM    25
  #define HREF_GPIO_NUM     23
  #define PCLK_GPIO_NUM     22

#elif defined(CAMERA_MODEL_M5STACK_PSRAM_B)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     22
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       32
  #define VSYNC_GPIO_NUM    25
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#else
  #error "Camera model not selected"
#endif

#define MOTOR_1_PIN_1    14
#define MOTOR_1_PIN_2    15
#define MOTOR_2_PIN_1    13
#define MOTOR_2_PIN_2    12

#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;



static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<!DOCTYPE html>
<html>
  <head>
    <title>ESP32-CAM Robot</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { font-family: Arial; text-align: center; margin:0px auto; padding-top: 30px;}
      table { margin-left: auto; margin-right: auto; }
      td { padding: 8 px; }
      .button {
        background-color: #2f4468;
        border: none;
        color: white;
        padding: 10px 20px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 18px;
        margin: 6px 3px;
        cursor: pointer;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
        -webkit-tap-highlight-color: rgba(0,0,0,0);
      }
      img {  width: auto ;
        max-width: 100% ;
        height: auto ; 
      }
    </style>
  </head>
  <body>
    <h1>ESP32-CAM Robot</h1>
    <img src="" id="photo" >
    <table>
      <tr>
        <td colspan="3" align="center">
          <button class="button" id="button_forward" ontouchstart="button_state_forward = true;" >forward</button>
        </td>
      </tr>
      <tr>
        <td align="center">
          <button class="button" onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Left</button>
        </td>
        <td align="center">
          <button class="button" onmousedown="toggleCheckbox('stop');" ontouchstart="toggleCheckbox('stop');">Stop</button></td><td align="center"><button class="button" onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Right</button>
        </td>
      </tr>
      <tr>
        <td colspan="3" align="center">
          <button class="button" onmousedown="toggleCheckbox('backward');" ontouchstart="toggleCheckbox('backward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Backward</button>
        </td>
      </tr>
    </table>
    <div align="center">
    <input type="range" class="slider" id="LEDslider" min="0" max="255" value="0"
      onchange="try{
      fetch(document.location.origin+'/control?var=led_PWM&val='+this.value);
      }catch(e){}">
    </div>
    <div class="LED_feedback_value">
      <h4>The feedback of LED intensity: <span id="LED_feedback_value">0</span></h4>
    </div>
    <div class="RSSI">
      <h4>The Received Signal Strength (RSSI): <span id="RSSI_feedback_value">N/A </span>dBm</h4><br>
    </div>

    <script>
    var counter1 = 0;
    let panelButtons = 0;
    var button_state_forward = false;

    function toggleCheckbox(x) {
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/control?var=" + x + "&val=0", true);              // URL = server_ip_address/control?var=xxx&val=xxx
      xhr.send();
    }
    
    setInterval(function(){
      controlPanel();
      counter1+=1;
      if (counter1==2){
        getLEDval();              //It was found that the 1st XML Request will be lost
        getLEDval();              //So a 2nd XML Request will be repeated.
      }
      if (counter1==4){
        counter1=0;
        getRSSI();
        getRSSI();
      }
    },200); 


    function controlPanel(){
      fetch(document.location.origin+'/control?var=led_PWM&val='+ document.getElementById("LEDslider").value);
      if(button_state_forward){
        toggleCheckbox('forward');
        button_state_forward=false;                           // URL = server_ip_address/control?var=xxx&val=xxx
      }
    }

    function getLEDval(){
      var xhttpr = new XMLHttpRequest();
      xhttpr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
          document.getElementById("LED_feedback_value").innerHTML = this.responseText;
        }
      }
      xhttpr.open("GET", "readLED", true);
      xhttpr.send();
    }

    function getRSSI(){
      var xhttpr = new XMLHttpRequest();
      xhttpr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
          document.getElementById("RSSI_feedback_value").innerHTML = this.responseText;
        }
      }
      xhttpr.open("GET", "readRSSI", true);
      xhttpr.send();
    }

    window.onload = document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
    // setInterval(controlPanel,1000);                  // Javascript does not support multiple setInterval()

    
    </script>
  </body>
</html>
)rawliteral";


static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_set_type(req, "text/html");
  return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}

static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(fb->format != PIXFORMAT_JPEG){
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if(!jpeg_converted){
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if(res == ESP_OK){
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if(fb){
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if(_jpg_buf){
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if(res != ESP_OK){
      break;
    }
    //Serial.printf("MJPG: %uB\n",(uint32_t)(_jpg_buf_len));
  }
  return res;
}

static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char variable[32] = {0,};
  char value[32] = {0,};                                        // Assume max. of 32 characters??

  // Serial.println("Received a command!");

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

  sensor_t * s = esp_camera_sensor_get();
  int res = 0;
  
  /* Control Decode */
  if(!strcmp(variable, "forward")) {
    Serial.println("Forward");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
  }
  else if(!strcmp(variable, "left")) {
    Serial.println("Left");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 1);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
  }
  else if(!strcmp(variable, "right")) {
    Serial.println("Right");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 1);
  }
  else if(!strcmp(variable, "backward")) {
    Serial.println("Backward");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 1);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 1);
  }
  else if(!strcmp(variable, "stop")) {
    Serial.println("Stop");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 0);
  }
  else if(!strcmp(variable, "led_PWM")){
    led_intensity = atoi(value)/4;                                // val =
    Serial.printf("Command: LED PWM = %u\n",led_intensity);       // val
    ledcWrite(Flashlight, led_intensity);                         // val 
  }
  else {
    res = -1;
  }

  if(res){
    return httpd_resp_send_500(req);
  }

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

void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t cmd_uri = {
    .uri       = "/control",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };
  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &cmd_uri);
  }
  config.server_port += 1;
  config.ctrl_port += 1;
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}

void handleRSSI() {
  server.send(200, "text/plane", String(WiFi.RSSI())); //Send LED  value only to client ajax request  
  Serial.println("RSSI request received and XMLHttpResponse sent!");
// Serial.printf("RSSi: %ld dBm\n",WiFi.RSSI())
}

void handleLED() {
  server.send(200, "text/plane", String(led_intensity)); //Send LED  value only to client ajax request  
  Serial.println("LED PWM request received and XMLHttpResponse sent!");
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);                //disable brownout detector
  
  ledcSetup(ledChannel, ledFreq, ledResolution);            // Setup LED PWM freq & resolution
  ledcAttachPin(Flashlight, 4);

  pinMode(MOTOR_1_PIN_1, OUTPUT);
  pinMode(MOTOR_1_PIN_2, OUTPUT);
  pinMode(MOTOR_2_PIN_1, OUTPUT);
  pinMode(MOTOR_2_PIN_2, OUTPUT);
  
  Serial.begin(115200);
  Serial.setDebugOutput(false);
  
  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; 
  
  if(psramFound()){
    config.frame_size = FRAMESIZE_VGA;
    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;
  }
  // Wi-Fi connection
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  
  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.println(WiFi.localIP());
  
  // Start streaming web server
  startCameraServer();

  server.on("/readRSSI", handleRSSI);
  server.on("/readLED", handleLED);                                 //To get update of ADC Value only

  server.begin();                                                   //Start server
  Serial.println("HTTP server started");


  ledcWrite(Flashlight, 0x01);
}

void loop() {
  server.handleClient();
  delay(1);
}

The web page with Network traffic & errors:

enter image description here

Tried the code in both AP & Client Modes and its works, but with intermittant error 404, also due to properties of Ajax, it seem I should keep the socket as 80 for Webserver, and it may be another reason of crash ....

**

-- Further Edited with other test

**

For simplicity, as usual, I ran all the code, and observed many network errors occured, next, for simplicity, I keeped the Web page being run and I reprogramed the ESP32 without the startCameraServer() function of the official http server, then I found no error at all for the xhr requests/ returns for every interval of 200ms.

So it seem esp_http_server and WebServer libraries have some collision there ....

experimental result


Solution

  • After some efforts, it make simplier to me to use one http server libary for all the video streaming, handling http requests and response ajax requests. Finally ESPAsycnWebSvr library was adopted. A sample code from GitHub of this library was modified (https://gist.github.com/me-no-dev)

    Now, the ESP32-CAM (AI Thinker) provides 2 ways communication with Camera streaming. Both AP mode and Client mode was tested.

    Test Result: Network Performance

    The code is shared for those who need this functions:

    #include <WiFi.h>
    #include "Arduino.h"
    #include "esp_camera.h"
    #include "ESPAsyncWebSrv.h"
    
    #include "soc/soc.h"              // disable brownout problems
    #include "soc/rtc_cntl_reg.h"     // disable brownout problems
    
    
    #define Flashlight 4              // Build-in flashlight of ESP32-CAM Board
    const int ledFreq = 1000;
    const int ledChannel = 4;
    const int ledResolution = 12;     // 12 bits give good resolution at low lux
    int led_intensity = 0;
    
    
    // For AI Thinker ESP32-CAM Board ONLY
    #define PWDN_GPIO_NUM     32
    #define RESET_GPIO_NUM    -1
    #define XCLK_GPIO_NUM      0
    #define SIOD_GPIO_NUM     26
    #define SIOC_GPIO_NUM     27
    
    #define Y9_GPIO_NUM       35
    #define Y8_GPIO_NUM       34
    #define Y7_GPIO_NUM       39
    #define Y6_GPIO_NUM       36
    #define Y5_GPIO_NUM       21
    #define Y4_GPIO_NUM       19
    #define Y3_GPIO_NUM       18
    #define Y2_GPIO_NUM        5
    #define VSYNC_GPIO_NUM    25
    #define HREF_GPIO_NUM     23
    #define PCLK_GPIO_NUM     22
    
    
    
    // WiFi credentials
    const char* ssid = "SSID";
    const char* password = "Password";
    
    
    typedef struct {
            camera_fb_t * fb;
            size_t index;
    } camera_frame_t;
    
    #define PART_BOUNDARY "123456789000000000000987654321"
    static const char* STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
    static const char* STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
    static const char* STREAM_PART = "Content-Type: %s\r\nContent-Length: %u\r\n\r\n";
    
    static const char * JPG_CONTENT_TYPE = "image/jpeg";
    static const char * BMP_CONTENT_TYPE = "image/x-windows-bmp";
    
    AsyncWebServer server(80);
    
    
    const char* htmlHomePage PROGMEM = R"HTMLHOMEPAGE(
    <!DOCTYPE html>
    <html>
      <head>
      <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <style>
          .slider {
            width: 20%;
            height: 10px;
            border-radius: 3px;
          }
        </style>
      
      </head>
      <body class="noselect" align="center" style="background-color:white">
        <h1><br></h1>
        <img id="cameraImage" src="" style="width:400px;height:250px">
        <h2><br></h2>
        
        <div class="slidecontainer">LED_Brightness
            <input type="range" min="0" max="255" value="0" class="slider" id="LEDslider" oninput='sendButtonInput("LED",value)'>
        </div>
        <div class="LED_feedback_value">
          <h4>The feedback of LED intensity: <span id="LED_feedback_value">0</span></h4>
        </div>
        <div class="RSSI">
          <h4>The Received Signal Strength (RSSI): <span id="RSSI_feedback_value">N/A </span>dBm</h4><br>
        </div>
    
    
        <script>
          var counter1 = 0;
          let panelButtons = 0;
    
          function controlPanel(){
            fetch(document.location.origin+'/control?var=led_PWM&val='+ document.getElementById("LEDslider").value);
          }
    
          function getLEDval(){
            var xhttpr = new XMLHttpRequest();
            xhttpr.onreadystatechange = function(){
              if (this.readyState == 4 && this.status == 200){
                document.getElementById("LED_feedback_value").innerHTML = this.responseText;
              }
            }
            xhttpr.open("GET", "readLED", true);
            xhttpr.send();
          }
    
          function getRSSI(){
            var xhttpr = new XMLHttpRequest();
            xhttpr.onreadystatechange = function(){
              if (this.readyState == 4 && this.status == 200){
                document.getElementById("RSSI_feedback_value").innerHTML = this.responseText;
              }
            }
            xhttpr.open("GET", "readRSSI", true);
            xhttpr.send();
          }
    
    
          setInterval(function(){                     // pressure test,  every 200ms
            controlPanel();
            counter1+=1;
            if (counter1==2){
              getLEDval();
            }
            if (counter1==4){
              counter1=0;
              getRSSI();
            }
          },200); 
    
    
          window.onload = document.getElementById("cameraImage").src = window.location + "stream";
        </script>
      </body>    
    </html>
    )HTMLHOMEPAGE";
    
    
    class AsyncBufferResponse: public AsyncAbstractResponse {
        private:
            uint8_t * _buf;
            size_t _len;
            size_t _index;
        public:
            AsyncBufferResponse(uint8_t * buf, size_t len, const char * contentType){
                _buf = buf;
                _len = len;
                _callback = nullptr;
                _code = 200;
                _contentLength = _len;
                _contentType = contentType;
                _index = 0;
            }
            ~AsyncBufferResponse(){
                if(_buf != nullptr){
                    free(_buf);
                }
            }
            bool _sourceValid() const { return _buf != nullptr; }
            virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override{
                size_t ret = _content(buf, maxLen, _index);
                if(ret != RESPONSE_TRY_AGAIN){
                    _index += ret;
                }
                return ret;
            }
            size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
                memcpy(buffer, _buf+index, maxLen);
                if((index+maxLen) == _len){
                    free(_buf);
                    _buf = nullptr;
                }
                return maxLen;
            }
    };
    
    class AsyncFrameResponse: public AsyncAbstractResponse {
        private:
            camera_fb_t * fb;
            size_t _index;
        public:
            AsyncFrameResponse(camera_fb_t * frame, const char * contentType){
                _callback = nullptr;
                _code = 200;
                _contentLength = frame->len;
                _contentType = contentType;
                _index = 0;
                fb = frame;
            }
            ~AsyncFrameResponse(){
                if(fb != nullptr){
                    esp_camera_fb_return(fb);
                }
            }
            bool _sourceValid() const { return fb != nullptr; }
            virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override{
                size_t ret = _content(buf, maxLen, _index);
                if(ret != RESPONSE_TRY_AGAIN){
                    _index += ret;
                }
                return ret;
            }
            size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
                memcpy(buffer, fb->buf+index, maxLen);
                if((index+maxLen) == fb->len){
                    esp_camera_fb_return(fb);
                    fb = nullptr;
                }
                return maxLen;
            }
    };
    
    class AsyncJpegStreamResponse: public AsyncAbstractResponse {
        private:
            camera_frame_t _frame;
            size_t _index;
            size_t _jpg_buf_len;
            uint8_t * _jpg_buf;
            uint64_t lastAsyncRequest;
        public:
            AsyncJpegStreamResponse(){
                _callback = nullptr;
                _code = 200;
                _contentLength = 0;
                _contentType = STREAM_CONTENT_TYPE;
                _sendContentLength = false;
                _chunked = true;
                _index = 0;
                _jpg_buf_len = 0;
                _jpg_buf = NULL;
                lastAsyncRequest = 0;
                memset(&_frame, 0, sizeof(camera_frame_t));
            }
            ~AsyncJpegStreamResponse(){
                if(_frame.fb){
                    if(_frame.fb->format != PIXFORMAT_JPEG){
                        free(_jpg_buf);
                    }
                    esp_camera_fb_return(_frame.fb);
                }
            }
            bool _sourceValid() const {
                return true;
            }
            virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override {
                size_t ret = _content(buf, maxLen, _index);
                if(ret != RESPONSE_TRY_AGAIN){
                    _index += ret;
                }
                return ret;
            }
            size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
                if(!_frame.fb || _frame.index == _jpg_buf_len){
                    if(index && _frame.fb){
                        uint64_t end = (uint64_t)micros();
                        int fp = (end - lastAsyncRequest) / 1000;
                        // log_printf("Size: %uKB, Time: %.0fms (%.1ffps)\n", _jpg_buf_len/1024, fp, 1000./fp);           // METER !
                        lastAsyncRequest = end;
                        if(_frame.fb->format != PIXFORMAT_JPEG){
                            free(_jpg_buf);
                        }
                        esp_camera_fb_return(_frame.fb);
                        _frame.fb = NULL;
                        _jpg_buf_len = 0;
                        _jpg_buf = NULL;
                    }
                    if(maxLen < (strlen(STREAM_BOUNDARY) + strlen(STREAM_PART) + strlen(JPG_CONTENT_TYPE) + 8)){
                        //log_w("Not enough space for headers");
                        return RESPONSE_TRY_AGAIN;
                    }
                    //get frame
                    _frame.index = 0;
    
                    _frame.fb = esp_camera_fb_get();
                    if (_frame.fb == NULL) {
                        log_e("Camera frame failed");
                        return 0;
                    }
    
                    if(_frame.fb->format != PIXFORMAT_JPEG){
                        unsigned long st = millis();
                        bool jpeg_converted = frame2jpg(_frame.fb, 80, &_jpg_buf, &_jpg_buf_len);
                        if(!jpeg_converted){
                            log_e("JPEG compression failed");
                            esp_camera_fb_return(_frame.fb);
                            _frame.fb = NULL;
                            _jpg_buf_len = 0;
                            _jpg_buf = NULL;
                            return 0;
                        }
                        // log_i("JPEG: %lums, %uB", millis() - st, _jpg_buf_len);                                        // METER !!
                    } else {
                        _jpg_buf_len = _frame.fb->len;
                        _jpg_buf = _frame.fb->buf;
                    }
    
                    //send boundary
                    size_t blen = 0;
                    if(index){
                        blen = strlen(STREAM_BOUNDARY);
                        memcpy(buffer, STREAM_BOUNDARY, blen);
                        buffer += blen;
                    }
                    //send header
                    size_t hlen = sprintf((char *)buffer, STREAM_PART, JPG_CONTENT_TYPE, _jpg_buf_len);
                    buffer += hlen;
                    //send frame
                    hlen = maxLen - hlen - blen;
                    if(hlen > _jpg_buf_len){
                        maxLen -= hlen - _jpg_buf_len;
                        hlen = _jpg_buf_len;
                    }
                    memcpy(buffer, _jpg_buf, hlen);
                    _frame.index += hlen;
                    return maxLen;
                }
    
                size_t available = _jpg_buf_len - _frame.index;
                if(maxLen > available){
                    maxLen = available;
                }
                memcpy(buffer, _jpg_buf+_frame.index, maxLen);
                _frame.index += maxLen;
    
                return maxLen;
            }
    };
    
    
    void handleRoot(AsyncWebServerRequest *request) 
    {
      request->send_P(200, "text/html", htmlHomePage);
    }
    
    void handleNotFound(AsyncWebServerRequest *request) 
    {
        request->send(404, "text/plain", "ESP32: File Not Found");
    }
    
    
    
    void streamJpg(AsyncWebServerRequest *request){
        AsyncJpegStreamResponse *response = new AsyncJpegStreamResponse();
        if(!response){
            request->send(501);
            return;
        }
        response->addHeader("Access-Control-Allow-Origin", "*");
        request->send(response);
    }
    
    
    
    void handleControl(AsyncWebServerRequest *request){
        if(!request->hasArg("var") || !request->hasArg("val")){
            request->send(404);
            return;
        }
        String var = request->arg("var");
        const char * variable = var.c_str();
        int val = atoi(request->arg("val").c_str());
    
        sensor_t * s = esp_camera_sensor_get();
        if(s == NULL){
            request->send(501);
            return;
        }
    
    
        if(!strcmp(variable, "led_PWM")){
          led_intensity = val/4;
          // Serial.printf("Command: LED PWM = %u\n",led_intensity);                  // This line would slow down the ESP32
          ledcWrite(Flashlight, led_intensity);                                       // val 
        }  
        else {
            log_e("unknown command %s", var.c_str());
            request->send(404);
            return;
        }
        // log_d("Got setting %s with value %d. Res: %d", var.c_str(), val, res);
    
        AsyncWebServerResponse * response = request->beginResponse(200);
        response->addHeader("Access-Control-Allow-Origin", "*");
        request->send(response);
    }
    
    void setup(){
    
      WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);                // Disable brownout detector
    
      ledcSetup(ledChannel, ledFreq, ledResolution);            // Setup LED PWM freq & resolution
      ledcAttachPin(Flashlight, 4);                             // Channel 4
      ledcWrite(Flashlight, 0x03);                              // Startup indicated by low light
    
      Serial.begin(115200);
      Serial.setDebugOutput(true);
    
    
    
      // Start Wifi and init camera //
    
      // Start Wifi
    
    
     IPAddress local_ip(192,168,4,1);
     IPAddress gateway(192,168,4,1);
     IPAddress subnet(255,255,255,0);
     WiFi.softAPConfig(local_ip, gateway, subnet);             //AP Mode
     WiFi.softAP(ssid, password);                              //
     Serial.print("AP IP address: ");                          //
     Serial.println(WiFi.softAPIP());                          //
    
    
      WiFi.begin(ssid, password);                               // Client Mode
      WiFi.setSleep(false);                                     //
      while (WiFi.status() != WL_CONNECTED) {                   //
        delay(500);                                             //
        Serial.print(".");                                      //
      }                                                         //
      Serial.println("");                                       //
      Serial.println("WiFi connected");                         //
      Serial.print("Camera Stream Ready! Go to: http://");      //
      Serial.println(WiFi.localIP());                           //
    
    
    
      // init camera
      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; 
      if(psramFound()){
        config.frame_size = FRAMESIZE_VGA;
        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) {
        log_e("ERROR : Camera init failed with error 0x%x\n", err);
        return;
      }
      else {
        log_d("Camera init OK");
      }
    
      sensor_t * s = esp_camera_sensor_get();
      log_d("Sensor PID : %d\n",s->id.PID);
        
      // =============================================
    
    
    
    
      server.on("/", HTTP_GET, handleRoot);
      server.on("/stream", HTTP_GET, streamJpg);
      server.on("/control", HTTP_GET, handleControl);
      server.onNotFound(handleNotFound);
    
    
    // Ajax xhr response to Web page
      server.on("/readLED", HTTP_GET, [](AsyncWebServerRequest *req){
        req->send(200, "text/plain",String(led_intensity));
      });
    
      server.on("/readRSSI", HTTP_GET, [](AsyncWebServerRequest *req){
        req->send(200, "text/plain",String(WiFi.RSSI()));
      });
    
    
      server.begin();
    }
    
    // ==== UPDATE : main loop ====
    void loop(){
    vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
    // ============================