diff --git a/app_httpd.cpp b/app_httpd.cpp index e363d34..a75ebc5 100644 --- a/app_httpd.cpp +++ b/app_httpd.cpp @@ -26,6 +26,12 @@ #include "src/logo.h" #include "storage.h" +#if !defined(HTTP_PORT) + #define HTTP_PORT 80 +#endif +int httpPort = HTTP_PORT; + + // Functions from the main .ino extern void flashLED(int flashtime); extern void setLamp(int newVal); @@ -40,8 +46,6 @@ extern IPAddress gw; extern bool accesspoint; extern char apName[]; extern bool captivePortal; -extern int httpPort; -extern int streamPort; extern char httpURL[]; extern char streamURL[]; extern char default_index[]; @@ -60,200 +64,33 @@ extern String sketchMD5; #include "fr_forward.h" #define ENROLL_CONFIRM_TIMES 5 -#define FACE_ID_SAVE_NUMBER 7 - -#define FACE_COLOR_WHITE 0x00FFFFFF -#define FACE_COLOR_BLACK 0x00000000 -#define FACE_COLOR_RED 0x000000FF -#define FACE_COLOR_GREEN 0x0000FF00 -#define FACE_COLOR_BLUE 0x00FF0000 -#define FACE_COLOR_YELLOW (FACE_COLOR_RED | FACE_COLOR_GREEN) -#define FACE_COLOR_CYAN (FACE_COLOR_BLUE | FACE_COLOR_GREEN) -#define FACE_COLOR_PURPLE (FACE_COLOR_BLUE | FACE_COLOR_RED) - -typedef struct { - size_t size; //number of values used for filtering - size_t index; //current value index - size_t count; //value count - int sum; - int * values; //array to be filled with values -} ra_filter_t; - -typedef struct { - httpd_req_t *req; - size_t len; +typedef struct +{ + httpd_req_t *req; + size_t len; } jpg_chunking_t; -#define PART_BOUNDARY "123456789000000000000987654321" -static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; -static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; -static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; - -static ra_filter_t ra_filter; -httpd_handle_t stream_httpd = NULL; +#define PART_BOUNDARY "123456789000000000000987654321"; httpd_handle_t camera_httpd = NULL; -static mtmn_config_t mtmn_config = {0}; -static int8_t is_enrolling = 0; -static face_id_list id_list = {0}; -int id_list_alloc = 0; - -static ra_filter_t * ra_filter_init(ra_filter_t * filter, size_t sample_size){ - memset(filter, 0, sizeof(ra_filter_t)); - - filter->values = (int *)malloc(sample_size * sizeof(int)); - if(!filter->values){ - return NULL; - } - memset(filter->values, 0, sample_size * sizeof(int)); - - filter->size = sample_size; - return filter; -} - -static int ra_filter_run(ra_filter_t * filter, int value) { - if(!filter->values){ - return value; - } - filter->sum -= filter->values[filter->index]; - filter->values[filter->index] = value; - filter->sum += filter->values[filter->index]; - filter->index++; - filter->index = filter->index % filter->size; - if (filter->count < filter->size) { - filter->count++; - } - return filter->sum / filter->count; -} - -static void rgb_print(dl_matrix3du_t *image_matrix, uint32_t color, const char * str){ - fb_data_t fb; - fb.width = image_matrix->w; - fb.height = image_matrix->h; - fb.data = image_matrix->item; - fb.bytes_per_pixel = 3; - fb.format = FB_BGR888; - fb_gfx_print(&fb, (fb.width - (strlen(str) * 14)) / 2, 10, color, str); -} - -static int rgb_printf(dl_matrix3du_t *image_matrix, uint32_t color, const char *format, ...){ - char loc_buf[64]; - char * temp = loc_buf; - int len; - va_list arg; - va_list copy; - va_start(arg, format); - va_copy(copy, arg); - len = vsnprintf(loc_buf, sizeof(loc_buf), format, arg); - va_end(copy); - if(len >= sizeof(loc_buf)){ - temp = (char*)malloc(len+1); - if(temp == NULL) { - return 0; - } - } - vsnprintf(temp, len+1, format, arg); - va_end(arg); - rgb_print(image_matrix, color, temp); - if(len > 64){ - free(temp); - } - return len; -} - -static void draw_face_boxes(dl_matrix3du_t *image_matrix, box_array_t *boxes, int face_id){ - int x, y, w, h, i; - uint32_t color = FACE_COLOR_YELLOW; - if(face_id < 0){ - color = FACE_COLOR_RED; - } else if(face_id > 0){ - color = FACE_COLOR_GREEN; - } - fb_data_t fb; - fb.width = image_matrix->w; - fb.height = image_matrix->h; - fb.data = image_matrix->item; - fb.bytes_per_pixel = 3; - fb.format = FB_BGR888; - for (i = 0; i < boxes->len; i++){ - // rectangle box - x = (int)boxes->box[i].box_p[0]; - y = (int)boxes->box[i].box_p[1]; - w = (int)boxes->box[i].box_p[2] - x + 1; - h = (int)boxes->box[i].box_p[3] - y + 1; - fb_gfx_drawFastHLine(&fb, x, y, w, color); - fb_gfx_drawFastHLine(&fb, x, y+h-1, w, color); - fb_gfx_drawFastVLine(&fb, x, y, h, color); - fb_gfx_drawFastVLine(&fb, x+w-1, y, h, color); - #if 0 - // landmark - int x0, y0, j; - for (j = 0; j < 10; j+=2) { - x0 = (int)boxes->landmark[i].landmark_p[j]; - y0 = (int)boxes->landmark[i].landmark_p[j+1]; - fb_gfx_fillRect(&fb, x0, y0, 3, 3, color); - } - #endif - } -} - -static int run_face_recognition(dl_matrix3du_t *image_matrix, box_array_t *net_boxes){ - dl_matrix3du_t *aligned_face = NULL; - int matched_id = 0; - - aligned_face = dl_matrix3du_alloc(1, FACE_WIDTH, FACE_HEIGHT, 3); - if(!aligned_face){ - Serial.println("Could not allocate face recognition buffer"); - return matched_id; - } - if (align_face(net_boxes, image_matrix, aligned_face) == ESP_OK){ - if (is_enrolling == 1){ - int8_t this_face = id_list.tail; - int8_t left_sample_face = enroll_face(&id_list, aligned_face); - - if(left_sample_face == (ENROLL_CONFIRM_TIMES - 1)){ - Serial.printf("Enrolling Face ID: %d\n", this_face); - } - Serial.printf("Enrolling Face ID: %d sample %d\n", this_face, ENROLL_CONFIRM_TIMES - left_sample_face); - rgb_printf(image_matrix, FACE_COLOR_CYAN, "ID[%u] Sample[%u]", this_face, ENROLL_CONFIRM_TIMES - left_sample_face); - if (left_sample_face == 0){ - is_enrolling = 0; - Serial.printf("Enrolled Face ID: %d\n", this_face); - } - } else { - matched_id = recognize_face(&id_list, aligned_face); - if (matched_id >= 0) { - Serial.printf("Match Face ID: %u\n", matched_id); - rgb_printf(image_matrix, FACE_COLOR_GREEN, "Hello Subject %u", matched_id); - } else { - Serial.println("No Match Found"); - rgb_print(image_matrix, FACE_COLOR_RED, "Intruder Alert!"); - matched_id = -1; - } - } - } else { - Serial.println("Face Not Aligned"); - //rgb_print(image_matrix, FACE_COLOR_YELLOW, "Human Detected"); - } - - dl_matrix3du_free(aligned_face); - return matched_id; -} - -static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len){ +static size_t jpg_encode_stream(void *arg, size_t index, const void *data, size_t len) +{ jpg_chunking_t *j = (jpg_chunking_t *)arg; - if(!index){ + if (!index) + { j->len = 0; } - if(httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK){ + if (httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK) + { return 0; } j->len += len; return len; } -static esp_err_t capture_handler(httpd_req_t *req){ - camera_fb_t * fb = NULL; +static esp_err_t capture_handler(httpd_req_t *req) +{ + camera_fb_t *fb = NULL; esp_err_t res = ESP_OK; Serial.println("Capture Requested"); @@ -263,7 +100,8 @@ static esp_err_t capture_handler(httpd_req_t *req){ int64_t fr_start = esp_timer_get_time(); fb = esp_camera_fb_get(); - if (!fb) { + if (!fb) + { Serial.println("Camera capture failed"); httpd_resp_send_500(req); return ESP_FAIL; @@ -274,29 +112,32 @@ static esp_err_t capture_handler(httpd_req_t *req){ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); size_t out_len, out_width, out_height; - uint8_t * out_buf; + uint8_t *out_buf; bool s; - bool detected = false; - int face_id = 0; - if(!detection_enabled || fb->width > 400){ + if (!detection_enabled || fb->width > 400) + { size_t fb_len = 0; - if(fb->format == PIXFORMAT_JPEG){ + if (fb->format == PIXFORMAT_JPEG) + { fb_len = fb->len; res = httpd_resp_send(req, (const char *)fb->buf, fb->len); - } else { + } + else + { jpg_chunking_t jchunk = {req, 0}; - res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL; + res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk) ? ESP_OK : ESP_FAIL; httpd_resp_send_chunk(req, NULL, 0); fb_len = jchunk.len; } esp_camera_fb_return(fb); int64_t fr_end = esp_timer_get_time(); - Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000)); + Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start) / 1000)); return res; } dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3); - if (!image_matrix) { + if (!image_matrix) + { esp_camera_fb_return(fb); Serial.println("dl_matrix3du_alloc failed"); httpd_resp_send_500(req); @@ -310,347 +151,218 @@ static esp_err_t capture_handler(httpd_req_t *req){ s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf); esp_camera_fb_return(fb); - if(!s){ + if (!s) + { dl_matrix3du_free(image_matrix); Serial.println("to rgb888 failed"); httpd_resp_send_500(req); return ESP_FAIL; } - box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config); - - if (net_boxes){ - detected = true; - if(recognition_enabled){ - face_id = run_face_recognition(image_matrix, net_boxes); - } - draw_face_boxes(image_matrix, net_boxes, face_id); - free(net_boxes->score); - free(net_boxes->box); - free(net_boxes->landmark); - free(net_boxes); - } - jpg_chunking_t jchunk = {req, 0}; s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk); dl_matrix3du_free(image_matrix); - if(!s){ + if (!s) + { Serial.println("JPEG compression failed"); return ESP_FAIL; } int64_t fr_end = esp_timer_get_time(); - if (debugData) { - Serial.printf("FACE: %uB %ums %s%d\n", (uint32_t)(jchunk.len), (uint32_t)((fr_end - fr_start)/1000), detected?"DETECTED ":"", face_id); + if (debugData) + { + Serial.printf("FACE: %uB %ums\n", (uint32_t)(jchunk.len), (uint32_t)((fr_end - fr_start) / 1000)); } return res; } -static esp_err_t stream_handler(httpd_req_t *req){ - camera_fb_t * fb = NULL; - esp_err_t res = ESP_OK; - size_t _jpg_buf_len = 0; - uint8_t * _jpg_buf = NULL; - char * part_buf[64]; - dl_matrix3du_t *image_matrix = NULL; - int face_id = 0; - bool detected = false; - int64_t fr_start = 0; - int64_t fr_face = 0; - int64_t fr_recognize = 0; - int64_t fr_encode = 0; - int64_t fr_ready = 0; - - - Serial.println("Stream requested"); - - flashLED(75); // double flash of status LED - delay(75); - flashLED(75); - - static int64_t last_frame = 0; - if(!last_frame) { - last_frame = esp_timer_get_time(); - } - - res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE); - if(res != ESP_OK){ - return res; - } - - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - - while(true){ - detected = false; - face_id = 0; - fb = esp_camera_fb_get(); - if (!fb) { - Serial.println("Camera capture failed"); - res = ESP_FAIL; - } else { - fr_start = esp_timer_get_time(); - fr_ready = fr_start; - fr_face = fr_start; - fr_encode = fr_start; - fr_recognize = fr_start; - if(!detection_enabled || fb->width > 400){ - if(fb->format != PIXFORMAT_JPEG){ - bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); - esp_camera_fb_return(fb); - fb = NULL; - if(!jpeg_converted){ - Serial.println("JPEG compression failed"); - res = ESP_FAIL; - } - } else { - _jpg_buf_len = fb->len; - _jpg_buf = fb->buf; - } - } else { - - image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3); - - if (!image_matrix) { - Serial.println("dl_matrix3du_alloc failed"); - res = ESP_FAIL; - } else { - if(!fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item)){ - Serial.println("fmt2rgb888 failed"); - res = ESP_FAIL; - } else { - fr_ready = esp_timer_get_time(); - box_array_t *net_boxes = NULL; - if(detection_enabled){ - net_boxes = face_detect(image_matrix, &mtmn_config); - } - fr_face = esp_timer_get_time(); - fr_recognize = fr_face; - if (net_boxes || fb->format != PIXFORMAT_JPEG){ - if(net_boxes){ - detected = true; - if(recognition_enabled){ - face_id = run_face_recognition(image_matrix, net_boxes); - } - fr_recognize = esp_timer_get_time(); - draw_face_boxes(image_matrix, net_boxes, face_id); - free(net_boxes->score); - free(net_boxes->box); - free(net_boxes->landmark); - free(net_boxes); - } - if(!fmt2jpg(image_matrix->item, fb->width*fb->height*3, fb->width, fb->height, PIXFORMAT_RGB888, 90, &_jpg_buf, &_jpg_buf_len)){ - Serial.println("fmt2jpg failed"); - res = ESP_FAIL; - } - esp_camera_fb_return(fb); - fb = NULL; - } else { - _jpg_buf = fb->buf; - _jpg_buf_len = fb->len; - } - fr_encode = esp_timer_get_time(); - } - dl_matrix3du_free(image_matrix); - } - } - } - if(res == ESP_OK){ - res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); - } - 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(fb){ - esp_camera_fb_return(fb); - fb = NULL; - _jpg_buf = NULL; - } else if(_jpg_buf){ - free(_jpg_buf); - _jpg_buf = NULL; - } - if(res != ESP_OK){ - break; - } - - int64_t fr_end = esp_timer_get_time(); - int64_t ready_time = (fr_ready - fr_start)/1000; - int64_t face_time = (fr_face - fr_ready)/1000; - int64_t recognize_time = (fr_recognize - fr_face)/1000; - int64_t encode_time = (fr_encode - fr_recognize)/1000; - int64_t process_time = (fr_encode - fr_start)/1000; - int64_t frame_time = fr_end - last_frame; - last_frame = fr_end; - frame_time /= 1000; - uint32_t avg_frame_time = ra_filter_run(&ra_filter, frame_time); - if (debugData) { - Serial.printf("MJPG: %uB %ums (%.1ffps), AVG: %ums (%.1ffps), %u+%u+%u+%u=%u %s%d\n", - (uint32_t)(_jpg_buf_len), - (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time, - avg_frame_time, 1000.0 / avg_frame_time, - (uint32_t)ready_time, (uint32_t)face_time, (uint32_t)recognize_time, (uint32_t)encode_time, (uint32_t)process_time, - (detected)?"DETECTED ":"", face_id - ); - } - } - - last_frame = 0; - return res; -} - -static esp_err_t cmd_handler(httpd_req_t *req){ - char* buf; +static esp_err_t cmd_handler(httpd_req_t *req) +{ + char *buf; size_t buf_len; - char variable[32] = {0,}; - char value[32] = {0,}; + char variable[32] = { + 0, + }; + char value[32] = { + 0, + }; flashLED(75); buf_len = httpd_req_get_url_query_len(req) + 1; - if (buf_len > 1) { - buf = (char*)malloc(buf_len); - if(!buf){ + 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_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 { + httpd_query_key_value(buf, "val", value, sizeof(value)) == ESP_OK) + { + } + else + { free(buf); httpd_resp_send_404(req); return ESP_FAIL; } - } else { + } + else + { free(buf); httpd_resp_send_404(req); return ESP_FAIL; } free(buf); - } else { + } + else + { httpd_resp_send_404(req); return ESP_FAIL; } int val = atoi(value); - sensor_t * s = esp_camera_sensor_get(); + 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); + 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, "rotate")) myRotation = val; - else if(!strcmp(variable, "face_detect")) { + 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, "rotate")) + myRotation = val; + else if (!strcmp(variable, "face_detect")) + { detection_enabled = val; - if(!detection_enabled) { + if (!detection_enabled) + { recognition_enabled = 0; } } - else if(!strcmp(variable, "face_enroll")) is_enrolling = val; - else if(!strcmp(variable, "face_recognize")) { - recognition_enabled = val; - if(recognition_enabled){ - detection_enabled = val; - } - } - else if(!strcmp(variable, "lamp") && (lampVal != -1)) { - lampVal = constrain(val,0,100); + else if (!strcmp(variable, "lamp") && (lampVal != -1)) + { + lampVal = constrain(val, 0, 100); setLamp(lampVal); } - else if(!strcmp(variable, "save_face")) { - if (filesystem) saveFaceDB(SPIFFS); + else if (!strcmp(variable, "save_prefs")) + { + if (filesystem) + savePrefs(SPIFFS); } - else if(!strcmp(variable, "clear_face")) { - if (filesystem) removeFaceDB(SPIFFS); + else if (!strcmp(variable, "clear_prefs")) + { + if (filesystem) + removePrefs(SPIFFS); } - else if(!strcmp(variable, "save_prefs")) { - if (filesystem) savePrefs(SPIFFS); - } - else if(!strcmp(variable, "clear_prefs")) { - if (filesystem) removePrefs(SPIFFS); - } - else if(!strcmp(variable, "reboot")) { + else if (!strcmp(variable, "reboot")) + { Serial.print("REBOOT requested"); - for (int i=0; i<20; i++) { - flashLED(50); - delay(150); - Serial.print('.'); + for (int i = 0; i < 20; i++) + { + flashLED(50); + delay(150); + Serial.print('.'); } Serial.printf(" Thats all folks!\n\n"); ESP.restart(); } - else { + else + { res = -1; } - if(res){ + if (res) + { return httpd_resp_send_500(req); } httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); return httpd_resp_send(req, NULL, 0); } -static esp_err_t status_handler(httpd_req_t *req){ +static 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; + sensor_t *s = esp_camera_sensor_get(); + char *p = json_response; *p++ = '{'; - p+=sprintf(p, "\"lamp\":%d,", lampVal); - 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, "\"cam_name\":\"%s\",", myName); - p+=sprintf(p, "\"code_ver\":\"%s\",", myVer); - p+=sprintf(p, "\"rotate\":\"%d\",", myRotation); - p+=sprintf(p, "\"stream_url\":\"%s\"", streamURL); + p += sprintf(p, "\"lamp\":%d,", lampVal); + 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, "\"cam_name\":\"%s\",", myName); + p += sprintf(p, "\"code_ver\":\"%s\",", myVer); + p += sprintf(p, "\"rotate\":\"%d\",", myRotation); + p += sprintf(p, "\"stream_url\":\"%s\"", streamURL); *p++ = '}'; *p++ = 0; httpd_resp_set_type(req, "application/json"); @@ -658,210 +370,220 @@ static esp_err_t status_handler(httpd_req_t *req){ return httpd_resp_send(req, json_response, strlen(json_response)); } -static esp_err_t info_handler(httpd_req_t *req){ - static char json_response[256]; - char * p = json_response; - *p++ = '{'; - p+=sprintf(p, "\"cam_name\":\"%s\",", myName); - p+=sprintf(p, "\"rotate\":\"%d\",", myRotation); - p+=sprintf(p, "\"stream_url\":\"%s\"", streamURL); - *p++ = '}'; - *p++ = 0; - httpd_resp_set_type(req, "application/json"); - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - return httpd_resp_send(req, json_response, strlen(json_response)); -} - -static esp_err_t favicon_16x16_handler(httpd_req_t *req){ +static esp_err_t favicon_16x16_handler(httpd_req_t *req) +{ httpd_resp_set_type(req, "image/png"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)favicon_16x16_png, favicon_16x16_png_len); } -static esp_err_t favicon_32x32_handler(httpd_req_t *req){ +static esp_err_t favicon_32x32_handler(httpd_req_t *req) +{ httpd_resp_set_type(req, "image/png"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)favicon_32x32_png, favicon_32x32_png_len); } -static esp_err_t favicon_ico_handler(httpd_req_t *req){ +static esp_err_t favicon_ico_handler(httpd_req_t *req) +{ httpd_resp_set_type(req, "image/x-icon"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)favicon_ico, favicon_ico_len); } -static esp_err_t logo_svg_handler(httpd_req_t *req){ +static esp_err_t logo_svg_handler(httpd_req_t *req) +{ httpd_resp_set_type(req, "image/svg+xml"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)logo_svg, logo_svg_len); } -static esp_err_t dump_handler(httpd_req_t *req){ +static esp_err_t dump_handler(httpd_req_t *req) +{ flashLED(75); Serial.println("\nDump Requested"); Serial.print("Preferences file: "); dumpPrefs(SPIFFS); static char dumpOut[1200] = ""; - char * d = dumpOut; + char *d = dumpOut; // Header - d+= sprintf(d,"\n"); - d+= sprintf(d,"\n"); - d+= sprintf(d,"%s - Status\n", myName); - d+= sprintf(d,"\n"); - d+= sprintf(d,"\n"); - d+= sprintf(d,"\n"); - d+= sprintf(d,"\n\n"); - d+= sprintf(d,"\n"); - d+= sprintf(d,"

ESP32 Cam Webserver

\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "%s - Status\n", myName); + d += sprintf(d, "\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "\n\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "

ESP32 Cam Webserver

\n"); // Module - d+= sprintf(d,"Name: %s
\n", myName); + d += sprintf(d, "Name: %s
\n", myName); Serial.printf("Name: %s\n", myName); - d+= sprintf(d,"Firmware: %s (base: %s)
\n", myVer, baseVersion); + d += sprintf(d, "Firmware: %s (base: %s)
\n", myVer, baseVersion); Serial.printf("Firmware: %s (base: %s)\n", myVer, baseVersion); float sketchPct = 100 * sketchSize / sketchSpace; - d+= sprintf(d,"Sketch Size: %i (total: %i, %.1f%% used)
\n", sketchSize, sketchSpace, sketchPct); + d += sprintf(d, "Sketch Size: %i (total: %i, %.1f%% used)
\n", sketchSize, sketchSpace, sketchPct); Serial.printf("Sketch Size: %i (total: %i, %.1f%% used)\n", sketchSize, sketchSpace, sketchPct); - d+= sprintf(d,"MD5: %s
\n", sketchMD5.c_str()); + d += sprintf(d, "MD5: %s
\n", sketchMD5.c_str()); Serial.printf("MD5: %s\n", sketchMD5.c_str()); - d+= sprintf(d,"ESP sdk: %s
\n", ESP.getSdkVersion()); + d += sprintf(d, "ESP sdk: %s
\n", ESP.getSdkVersion()); Serial.printf("ESP sdk: %s\n", ESP.getSdkVersion()); // Network - d+= sprintf(d,"

WiFi

\n"); - if (accesspoint) { - if (captivePortal) { - d+= sprintf(d,"Mode: AccessPoint with captive portal
\n"); + d += sprintf(d, "

WiFi

\n"); + if (accesspoint) + { + if (captivePortal) + { + d += sprintf(d, "Mode: AccessPoint with captive portal
\n"); Serial.printf("Mode: AccessPoint with captive portal\n"); - } else { - d+= sprintf(d,"Mode: AccessPoint
\n"); + } + else + { + d += sprintf(d, "Mode: AccessPoint
\n"); Serial.printf("Mode: AccessPoint\n"); } - d+= sprintf(d,"SSID: %s
\n", apName); + d += sprintf(d, "SSID: %s
\n", apName); Serial.printf("SSID: %s\n", apName); - } else { - d+= sprintf(d,"Mode: Client
\n"); + } + else + { + d += sprintf(d, "Mode: Client
\n"); Serial.printf("Mode: Client\n"); String ssidName = WiFi.SSID(); - d+= sprintf(d,"SSID: %s
\n", ssidName.c_str()); + d += sprintf(d, "SSID: %s
\n", ssidName.c_str()); Serial.printf("Ssid: %s\n", ssidName.c_str()); - d+= sprintf(d,"Rssi: %i
\n", WiFi.RSSI()); + d += sprintf(d, "Rssi: %i
\n", WiFi.RSSI()); Serial.printf("Rssi: %i\n", WiFi.RSSI()); String bssid = WiFi.BSSIDstr(); - d+= sprintf(d,"BSSID: %s
\n", bssid.c_str()); + d += sprintf(d, "BSSID: %s
\n", bssid.c_str()); Serial.printf("BSSID: %s\n", bssid.c_str()); } - d+= sprintf(d,"IP address: %d.%d.%d.%d
\n", ip[0], ip[1], ip[2], ip[3]); + d += sprintf(d, "IP address: %d.%d.%d.%d
\n", ip[0], ip[1], ip[2], ip[3]); Serial.printf("IP address: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); - if (!accesspoint) { - d+= sprintf(d,"Netmask: %d.%d.%d.%d
\n", net[0], net[1], net[2], net[3]); + if (!accesspoint) + { + d += sprintf(d, "Netmask: %d.%d.%d.%d
\n", net[0], net[1], net[2], net[3]); Serial.printf("Netmask: %d.%d.%d.%d\n", net[0], net[1], net[2], net[3]); - d+= sprintf(d,"Gateway: %d.%d.%d.%d
\n", gw[0], gw[1], gw[2], gw[3]); + d += sprintf(d, "Gateway: %d.%d.%d.%d
\n", gw[0], gw[1], gw[2], gw[3]); Serial.printf("Gateway: %d.%d.%d.%d\n", gw[0], gw[1], gw[2], gw[3]); } - d+= sprintf(d,"Http port: %i, Stream port: %i
\n", httpPort, streamPort); - Serial.printf("Http port: %i, Stream port: %i\n", httpPort, streamPort); + d += sprintf(d, "Http port: %i
\n", httpPort); + Serial.printf("Http port: %i\n", httpPort); byte mac[6]; WiFi.macAddress(mac); - d+= sprintf(d,"MAC: %02X:%02X:%02X:%02X:%02X:%02X
\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + d += sprintf(d, "MAC: %02X:%02X:%02X:%02X:%02X:%02X
\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); Serial.printf("MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // System - d+= sprintf(d,"

System

\n"); + d += sprintf(d, "

System

\n"); int64_t sec = esp_timer_get_time() / 1000000; - int64_t upDays = int64_t(floor(sec/86400)); - int upHours = int64_t(floor(sec/3600)) % 24; - int upMin = int64_t(floor(sec/60)) % 60; + int64_t upDays = int64_t(floor(sec / 86400)); + int upHours = int64_t(floor(sec / 3600)) % 24; + int upMin = int64_t(floor(sec / 60)) % 60; int upSec = sec % 60; - d+= sprintf(d,"Up: %" PRId64 ":%02i:%02i:%02i (d:h:m:s)
\n", upDays, upHours, upMin, upSec); + d += sprintf(d, "Up: %" PRId64 ":%02i:%02i:%02i (d:h:m:s)
\n", upDays, upHours, upMin, upSec); Serial.printf("Up: %" PRId64 ":%02i:%02i:%02i (d:h:m:s)\n", upDays, upHours, upMin, upSec); - d+= sprintf(d,"Freq: %i MHz
\n", ESP.getCpuFreqMHz()); + d += sprintf(d, "Freq: %i MHz
\n", ESP.getCpuFreqMHz()); Serial.printf("Freq: %i MHz\n", ESP.getCpuFreqMHz()); - d+= sprintf(d,"Heap: %i, free: %i, min free: %i, max block: %i
\n", ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); + d += sprintf(d, "Heap: %i, free: %i, min free: %i, max block: %i
\n", ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); Serial.printf("Heap: %i, free: %i, min free: %i, max block: %i\n", ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); - d+= sprintf(d,"Psram: %i, free: %i, min free: %i, max block: %i
\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); + d += sprintf(d, "Psram: %i, free: %i, min free: %i, max block: %i
\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); Serial.printf("Psram: %i, free: %i, min free: %i, max block: %i\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); - if (filesystem) { - d+= sprintf(d,"Spiffs: %i, used: %i
\n", SPIFFS.totalBytes(), SPIFFS.usedBytes()); + if (filesystem) + { + d += sprintf(d, "Spiffs: %i, used: %i
\n", SPIFFS.totalBytes(), SPIFFS.usedBytes()); Serial.printf("Spiffs: %i, used: %i\n", SPIFFS.totalBytes(), SPIFFS.usedBytes()); } - d+= sprintf(d,"Enrolled faces: %i (max %i)
\n", id_list.count, id_list.size); - Serial.printf("Enrolled faces: %i (max %i)\n", id_list.count, id_list.size); // Footer - d+= sprintf(d,"
\n"); - d+= sprintf(d,"\n"); - d+= sprintf(d,"\n"); - d+= sprintf(d,"
\n\n\n"); + d += sprintf(d, "
\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "\n"); + d += sprintf(d, "
\n\n\n"); *d++ = 0; httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, dumpOut, strlen(dumpOut)); } -static esp_err_t style_handler(httpd_req_t *req){ +static esp_err_t style_handler(httpd_req_t *req) +{ httpd_resp_set_type(req, "text/css"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)style_css, style_css_len); } -static esp_err_t streamviewer_handler(httpd_req_t *req){ - flashLED(75); - Serial.println("Stream Viewer requested"); - httpd_resp_set_type(req, "text/html"); - httpd_resp_set_hdr(req, "Content-Encoding", "identity"); - return httpd_resp_send(req, (const char *)streamviewer_html, streamviewer_html_len); -} - -static esp_err_t index_handler(httpd_req_t *req){ - char* buf; +static esp_err_t index_handler(httpd_req_t *req) +{ + char *buf; size_t buf_len; - char view[32] = {0,}; + char view[32] = { + 0, + }; flashLED(75); // See if we have a specific target (full/simple/portal) and serve as appropriate buf_len = httpd_req_get_url_query_len(req) + 1; - if (buf_len > 1) { - buf = (char*)malloc(buf_len); - if(!buf){ + 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, "view", view, sizeof(view)) == ESP_OK) { - } else { + if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) + { + if (httpd_query_key_value(buf, "view", view, sizeof(view)) == ESP_OK) + { + } + else + { free(buf); httpd_resp_send_404(req); return ESP_FAIL; } - } else { + } + else + { free(buf); httpd_resp_send_404(req); return ESP_FAIL; } free(buf); - } else { + } + else + { // no target specified; default. - strcpy(view,default_index); + strcpy(view, default_index); // If captive portal is active send that instead - if (captivePortal) { - strcpy(view,"portal"); + if (captivePortal) + { + strcpy(view, "portal"); } } - if (strncmp(view,"simple", sizeof(view)) == 0) { + if (strncmp(view, "simple", sizeof(view)) == 0) + { Serial.println("Simple index page requested"); httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)index_simple_html, index_simple_html_len); - } else if(strncmp(view,"full", sizeof(view)) == 0) { + } + else if (strncmp(view, "full", sizeof(view)) == 0) + { Serial.println("Full index page requested"); httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); - sensor_t * s = esp_camera_sensor_get(); - if (s->id.PID == OV3660_PID) { + sensor_t *s = esp_camera_sensor_get(); + if (s->id.PID == OV3660_PID) + { return httpd_resp_send(req, (const char *)index_ov3660_html, index_ov3660_html_len); } return httpd_resp_send(req, (const char *)index_ov2640_html, index_ov2640_html_len); - } else if(strncmp(view,"portal", sizeof(view)) == 0) { + } + else if (strncmp(view, "portal", sizeof(view)) == 0) + { //Prototype captive portal landing page. Serial.println("Portal page requested"); std::string s(portal_html); @@ -875,7 +597,9 @@ static esp_err_t index_handler(httpd_req_t *req){ httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)s.c_str(), s.length()); - } else { + } + else + { Serial.print("Unknown page requested: "); Serial.println(view); httpd_resp_send_404(req); @@ -883,119 +607,67 @@ static esp_err_t index_handler(httpd_req_t *req){ } } -void startCameraServer(int hPort, int sPort){ +void startCameraServer() +{ httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.max_uri_handlers = 12; // we use more than the default 8 (on port 80) httpd_uri_t index_uri = { - .uri = "/", - .method = HTTP_GET, - .handler = index_handler, - .user_ctx = NULL - }; + .uri = "/", + .method = HTTP_GET, + .handler = index_handler, + .user_ctx = NULL}; httpd_uri_t status_uri = { - .uri = "/status", - .method = HTTP_GET, - .handler = status_handler, - .user_ctx = NULL - }; + .uri = "/status", + .method = HTTP_GET, + .handler = status_handler, + .user_ctx = NULL}; httpd_uri_t cmd_uri = { - .uri = "/control", - .method = HTTP_GET, - .handler = cmd_handler, - .user_ctx = NULL - }; + .uri = "/control", + .method = HTTP_GET, + .handler = cmd_handler, + .user_ctx = NULL}; httpd_uri_t capture_uri = { - .uri = "/capture", - .method = HTTP_GET, - .handler = capture_handler, - .user_ctx = NULL - }; + .uri = "/capture", + .method = HTTP_GET, + .handler = capture_handler, + .user_ctx = NULL}; httpd_uri_t style_uri = { - .uri = "/style.css", - .method = HTTP_GET, - .handler = style_handler, - .user_ctx = NULL - }; + .uri = "/style.css", + .method = HTTP_GET, + .handler = style_handler, + .user_ctx = NULL}; httpd_uri_t favicon_16x16_uri = { - .uri = "/favicon-16x16.png", - .method = HTTP_GET, - .handler = favicon_16x16_handler, - .user_ctx = NULL - }; + .uri = "/favicon-16x16.png", + .method = HTTP_GET, + .handler = favicon_16x16_handler, + .user_ctx = NULL}; httpd_uri_t favicon_32x32_uri = { - .uri = "/favicon-32x32.png", - .method = HTTP_GET, - .handler = favicon_32x32_handler, - .user_ctx = NULL - }; + .uri = "/favicon-32x32.png", + .method = HTTP_GET, + .handler = favicon_32x32_handler, + .user_ctx = NULL}; httpd_uri_t favicon_ico_uri = { - .uri = "/favicon.ico", - .method = HTTP_GET, - .handler = favicon_ico_handler, - .user_ctx = NULL - }; + .uri = "/favicon.ico", + .method = HTTP_GET, + .handler = favicon_ico_handler, + .user_ctx = NULL}; httpd_uri_t logo_svg_uri = { - .uri = "/logo.svg", - .method = HTTP_GET, - .handler = logo_svg_handler, - .user_ctx = NULL - }; + .uri = "/logo.svg", + .method = HTTP_GET, + .handler = logo_svg_handler, + .user_ctx = NULL}; httpd_uri_t dump_uri = { - .uri = "/dump", - .method = HTTP_GET, - .handler = dump_handler, - .user_ctx = NULL - }; - httpd_uri_t stream_uri = { - .uri = "/", - .method = HTTP_GET, - .handler = stream_handler, - .user_ctx = NULL - }; - httpd_uri_t streamviewer_uri = { - .uri = "/view", - .method = HTTP_GET, - .handler = streamviewer_handler, - .user_ctx = NULL - }; - httpd_uri_t info_uri = { - .uri = "/info", - .method = HTTP_GET, - .handler = info_handler, - .user_ctx = NULL - }; + .uri = "/dump", + .method = HTTP_GET, + .handler = dump_handler, + .user_ctx = NULL}; - // Filter list; used during face detection - ra_filter_init(&ra_filter, 20); - - // Mtmn config values (face detection and recognition parameters) - mtmn_config.type = FAST; - mtmn_config.min_face = 80; - mtmn_config.pyramid = 0.707; - mtmn_config.pyramid_times = 4; - mtmn_config.p_threshold.score = 0.6; - mtmn_config.p_threshold.nms = 0.7; - mtmn_config.p_threshold.candidate_number = 20; - mtmn_config.r_threshold.score = 0.7; - mtmn_config.r_threshold.nms = 0.7; - mtmn_config.r_threshold.candidate_number = 10; - mtmn_config.o_threshold.score = 0.7; - mtmn_config.o_threshold.nms = 0.7; - mtmn_config.o_threshold.candidate_number = 1; - - // Face ID list (settings + pointer to the data allocation) - face_id_init(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES); - // The size of the allocated data block; calculated in dl_lib_calloc() - id_list_alloc = FACE_ID_SAVE_NUMBER * sizeof(dl_matrix3d_t *) + sizeof(void *); - Serial.print("FACE DB SIZE: "); - Serial.println(id_list_alloc); - Serial.printf("FACE DB POINTER: %p\n", id_list.id_list); - - config.server_port = hPort; - config.ctrl_port = hPort; + config.server_port = httpPort; + config.ctrl_port = httpPort; Serial.printf("Starting web server on port: '%d'\n", config.server_port); - if (httpd_start(&camera_httpd, &config) == ESP_OK) { + if (httpd_start(&camera_httpd, &config) == ESP_OK) + { // Note; config.max_uri_handlers (above) must be >= the number of handlers httpd_register_uri_handler(camera_httpd, &index_uri); httpd_register_uri_handler(camera_httpd, &cmd_uri); @@ -1008,17 +680,4 @@ void startCameraServer(int hPort, int sPort){ httpd_register_uri_handler(camera_httpd, &logo_svg_uri); httpd_register_uri_handler(camera_httpd, &dump_uri); } - - - config.server_port = sPort; - config.ctrl_port = sPort; - Serial.printf("Starting stream server on port: '%d'\n", config.server_port); - if (httpd_start(&stream_httpd, &config) == ESP_OK) { - httpd_register_uri_handler(stream_httpd, &stream_uri); - httpd_register_uri_handler(stream_httpd, &info_uri); - httpd_register_uri_handler(stream_httpd, &streamviewer_uri); - httpd_register_uri_handler(stream_httpd, &favicon_16x16_uri); - httpd_register_uri_handler(stream_httpd, &favicon_32x32_uri); - httpd_register_uri_handler(stream_httpd, &favicon_ico_uri); - } } diff --git a/esp32-cam-webserver.cpp b/esp32-cam-webserver.cpp new file mode 100644 index 0000000..810d9e6 --- /dev/null +++ b/esp32-cam-webserver.cpp @@ -0,0 +1,681 @@ +#include +#include "esp_camera.h" +#include +#include + +#define APP_CPU 1 +#define PRO_CPU 0 +/* This sketch is a extension/expansion/reork of the 'official' ESP32 Camera example + * sketch from Expressif: + * https://github.com/espressif/arduino-esp32/tree/master/libraries/ESP32/examples/Camera/CameraWebServer + * + * It is modified to allow control of Illumination LED Lamps's (present on some modules), + * greater feedback via a status LED, and the HTML contents are present in plain text + * for easy modification. + * + * A camera name can now be configured, and wifi details can be stored in an optional + * header file to allow easier updated of the repo. + * + * The web UI has had changes to add the lamp control, rotation, a standalone viewer, + * more feeedback, new controls and other tweaks and changes, + * note: Make sure that you have either selected ESP32 AI Thinker, + * or another board which has PSRAM enabled to use high resolution camera modes + */ + +/* + * FOR NETWORK AND HARDWARE SETTINGS COPY OR RENAME 'myconfig.sample.h' TO 'myconfig.h' AND EDIT THAT. + * + * By default this sketch will assume an AI-THINKER ESP-CAM and create + * an accesspoint called "ESP32-CAM-CONNECT" (password: "InsecurePassword") + * + */ + +// Primary config, or defaults. +#if __has_include("myconfig.h") +#include "myconfig.h" +#else +#warning "Using Defaults: Copy myconfig.sample.h to myconfig.h and edit that to use your own settings" +#define WIFI_AP_ENABLE +#define CAMERA_MODEL_AI_THINKER +struct station +{ + const char ssid[64]; + const char password[64]; + const bool dhcp; +} stationList[] = {{"ESP32-CAM-CONNECT", "InsecurePassword", true}}; +#endif + +// Upstream version string +#include "src/version.h" + +// Pin Mappings +#include "camera_pins.h" + +// Internal filesystem (SPIFFS) +// used for non-volatile camera settings and face DB store +#include "storage.h" + +// Sketch Info +int sketchSize; +int sketchSpace; +String sketchMD5; + +// Start with accesspoint mode disabled, wifi setup will activate it if +// no known networks are found, and WIFI_AP_ENABLE has been defined +bool accesspoint = false; + +// IP address, Netmask and Gateway, populated when connected +IPAddress ip; +IPAddress net; +IPAddress gw; + +// Declare external function from app_httpd.cpp +extern void startCameraServer(); +// Declare external function from esp32-streamserver.cpp +extern void startStreamServer(); + +// A Name for the Camera. (set in myconfig.h) +#if defined(CAM_NAME) +char myName[] = CAM_NAME; +#else +char myName[] = "ESP32 camera server"; +#endif + +#if !defined(WIFI_WATCHDOG) +#define WIFI_WATCHDOG 5000 +#endif + +// Number of known networks in stationList[] +int stationCount = sizeof(stationList) / sizeof(stationList[0]); + +// If we have AP mode enabled, ignore first entry in the stationList[] +#if defined(WIFI_AP_ENABLE) +int firstStation = 1; +#else +int firstStation = 0; +#endif + +// Select bvetween full and simple index as the default. +#if defined(DEFAULT_INDEX_FULL) +char default_index[] = "full"; +#else +char default_index[] = "simple"; +#endif + +// DNS server +const byte DNS_PORT = 53; +DNSServer dnsServer; +bool captivePortal = false; +char apName[64] = "Undefined"; + +// The app and stream URLs +char httpURL[64] = {"Undefined"}; +char streamURL[64] = {"Undefined"}; + +// This will be displayed to identify the firmware +char myVer[] PROGMEM = __DATE__ " @ " __TIME__; + +// initial rotation +// can be set in myconfig.h +#if !defined(CAM_ROTATION) +#define CAM_ROTATION 0 +#endif +int myRotation = CAM_ROTATION; + +// Illumination LAMP/LED +#if defined(LAMP_DISABLE) +int lampVal = -1; // lamp is disabled in config +#elif defined(LAMP_PIN) +#if defined(LAMP_DEFAULT) +int lampVal = constrain(LAMP_DEFAULT, 0, 100); // initial lamp value, range 0-100 +#else +int lampVal = 0; //default to off +#endif +#else +int lampVal = -1; // no lamp pin assigned +#endif + +int lampChannel = 7; // a free PWM channel (some channels used by camera) +const int pwmfreq = 50000; // 50K pwm frequency +const int pwmresolution = 9; // duty cycle bit range +const int pwmMax = pow(2, pwmresolution) - 1; + +#if defined(NO_FS) +bool filesystem = false; +#else +bool filesystem = true; +#endif + +#if defined(FACE_DETECTION) +int8_t detection_enabled = 1; +#if defined(FACE_RECOGNITION) +int8_t recognition_enabled = 1; +#else +int8_t recognition_enabled = 0; +#endif +#else +int8_t detection_enabled = 0; +int8_t recognition_enabled = 0; +#endif + +// Debug Data for stream and capture +#if defined(DEBUG_DEFAULT_ON) +bool debugData = true; +#else +bool debugData = false; +#endif + +//defs +void flashLED(int flashtime); +void setLamp(int newVal); +void WifiSetup(); + +// Notification LED +void flashLED(int flashtime) +{ +#ifdef LED_PIN // If we have it; flash it. + digitalWrite(LED_PIN, LED_ON); // On at full power. + delay(flashtime); // delay + digitalWrite(LED_PIN, LED_OFF); // turn Off +#else + return; // No notifcation LED, do nothing, no delay +#endif +} + +// Lamp Control +void setLamp(int newVal) +{ + if (newVal != -1) + { + // Apply a logarithmic function to the scale. + int brightness = round((pow(2, (1 + (newVal * 0.02))) - 2) / 6 * pwmMax); + ledcWrite(lampChannel, brightness); + Serial.print("Lamp: "); + Serial.print(newVal); + Serial.print("%, pwm = "); + Serial.println(brightness); + } +} + +void WifiSetup() +{ + // Feedback that we are now attempting to connect + flashLED(300); + delay(100); + flashLED(300); + Serial.println("Starting WiFi"); + Serial.print("Known external SSIDs: "); + if (stationCount > firstStation) + { + for (int i = firstStation; i < stationCount; i++) + Serial.printf(" '%s'", stationList[i].ssid); + } + else + { + Serial.print("None"); + } + Serial.println(); + byte mac[6]; + WiFi.macAddress(mac); + Serial.printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + int bestStation = -1; + long bestRSSI = -1024; + if (stationCount > firstStation) + { + // We have a list to scan + Serial.printf("Scanning local Wifi Networks\n"); + int stationsFound = WiFi.scanNetworks(); + Serial.printf("%i networks found\n", stationsFound); + if (stationsFound > 0) + { + for (int i = 0; i < stationsFound; ++i) + { + // Print SSID and RSSI for each network found + String thisSSID = WiFi.SSID(i); + int thisRSSI = WiFi.RSSI(i); + Serial.printf("%3i : %s (%i)", i + 1, thisSSID.c_str(), thisRSSI); + // Scan our list of known external stations + for (int sta = firstStation; sta < stationCount; sta++) + { + if (strcmp(stationList[sta].ssid, thisSSID.c_str()) == 0) + { + Serial.print(" - Known!"); + // Chose the strongest RSSI seen + if (thisRSSI > bestRSSI) + { + bestStation = sta; + bestRSSI = thisRSSI; + } + } + } + Serial.println(); + } + } + } + else + { + // No list to scan, therefore we are an accesspoint + accesspoint = true; + } + + if (bestStation == -1) + { + if (!accesspoint) + { +#if defined(WIFI_AP_ENABLE) + Serial.println("No known networks found, entering AccessPoint fallback mode"); + accesspoint = true; +#else + Serial.println("No known networks found"); +#endif + } + else + { + Serial.println("AccessPoint mode selected in config"); + } + } + else + { + Serial.printf("Connecting to Wifi Network: %s\n", stationList[bestStation].ssid); + if (stationList[bestStation].dhcp == false) + { +#if defined(ST_IP) + Serial.println("Applying static IP settings"); +#if !defined(ST_GATEWAY) || !defined(ST_NETMASK) +#error "You must supply both Gateway and NetMask when specifying a static IP address" +#endif + IPAddress staticIP(ST_IP); + IPAddress gateway(ST_GATEWAY); + IPAddress subnet(ST_NETMASK); +#if !defined(ST_DNS1) + WiFi.config(staticIP, gateway, subnet); +#else + IPAddress dns1(ST_DNS1); +#if !defined(ST_DNS2) + WiFi.config(staticIP, gateway, subnet, dns1); +#else + IPAddress dns2(ST_DNS2); + WiFi.config(staticIP, gateway, subnet, dns1, dns2); +#endif +#endif +#else + Serial.println("Static IP settings requested but not defined in config, falling back to dhcp"); +#endif + } + // Initiate network connection request + WiFi.begin(stationList[bestStation].ssid, stationList[bestStation].password); + + // Wait to connect, or timeout + unsigned long start = millis(); + while ((millis() - start <= WIFI_WATCHDOG) && (WiFi.status() != WL_CONNECTED)) + { + delay(500); + Serial.print('.'); + } + // If we have connected, inform user + if (WiFi.status() == WL_CONNECTED) + { + Serial.println("Client connection succeeded"); + accesspoint = false; + // Note IP details + ip = WiFi.localIP(); + net = WiFi.subnetMask(); + gw = WiFi.gatewayIP(); + Serial.printf("IP address: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); + Serial.printf("Netmask : %d.%d.%d.%d\n", net[0], net[1], net[2], net[3]); + Serial.printf("Gateway : %d.%d.%d.%d\n", gw[0], gw[1], gw[2], gw[3]); + // Flash the LED to show we are connected + for (int i = 0; i < 5; i++) + { + flashLED(50); + delay(150); + } + } + else + { + Serial.println("Client connection Failed"); + WiFi.disconnect(); // (resets the WiFi scan) + } + } + + if (accesspoint && (WiFi.status() != WL_CONNECTED)) + { +// The accesspoint has been enabled, and we have not connected to any existing networks +#if defined(AP_CHAN) + Serial.println("Setting up Fixed Channel AccessPoint"); + Serial.print(" SSID : "); + Serial.println(stationList[0].ssid); + Serial.print(" Password : "); + Serial.println(stationList[0].password); + Serial.print(" Channel : "); + Serial.println(AP_CHAN); + WiFi.softAP(stationList[0].ssid, stationList[0].password, AP_CHAN); +#else + Serial.println("Setting up AccessPoint"); + Serial.print(" SSID : "); + Serial.println(stationList[0].ssid); + Serial.print(" Password : "); + Serial.println(stationList[0].password); + WiFi.softAP(stationList[0].ssid, stationList[0].password); +#endif +#if defined(AP_ADDRESS) + // User has specified the AP details; apply them after a short delay + // (https://github.com/espressif/arduino-esp32/issues/985#issuecomment-359157428) + delay(100); + IPAddress local_IP(AP_ADDRESS); + IPAddress gateway(AP_ADDRESS); + IPAddress subnet(255, 255, 255, 0); + WiFi.softAPConfig(local_IP, gateway, subnet); +#endif + // Note AP details + ip = WiFi.softAPIP(); + net = WiFi.subnetMask(); + gw = WiFi.gatewayIP(); + strcpy(apName, stationList[0].ssid); + Serial.printf("IP address: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); + // Flash the LED to show we are connected + for (int i = 0; i < 5; i++) + { + flashLED(150); + delay(50); + } + // Start the DNS captive portal if requested + if (stationList[0].dhcp == true) + { + Serial.println("Starting Captive Portal"); + dnsServer.start(DNS_PORT, "*", ip); + captivePortal = true; + } + } +} + +void setup() +{ + Serial.begin(115200); + Serial.setDebugOutput(true); + Serial.println(); + Serial.println("===="); + Serial.print("esp32-cam-webserver: "); + Serial.println(myName); + Serial.print("Code Built: "); + Serial.println(myVer); + Serial.print("Base Release: "); + Serial.println(baseVersion); + + if (stationCount == 0) + { + Serial.println("\nFatal Error; Halting"); + Serial.println("No wifi ssid details have been configured; we cannot connect to WiFi or start our own AccessPoint"); + while (true) + delay(1000); + } + +#if defined(LED_PIN) // If we have a notification LED, set it to output + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LED_ON); +#endif + + // Create camera config structure; and populate with hardware and other defaults + 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 highest supported specs to pre-allocate large buffers + if (psramFound()) + { + config.frame_size = FRAMESIZE_UXGA; + config.jpeg_quality = 10; + config.fb_count = 2; + } + else + { + config.frame_size = FRAMESIZE_XGA; + config.jpeg_quality = 12; + config.fb_count = 1; + } + +#if defined(CAMERA_MODEL_ESP_EYE) + pinMode(13, INPUT_PULLUP); + pinMode(14, INPUT_PULLUP); +#endif + + // camera init + esp_err_t err = esp_camera_init(&config); + if (err == ESP_OK) + { + Serial.println("Camera init succeeded"); + } + else + { + delay(100); // need a delay here or the next serial o/p gets missed + Serial.println("Halted: Camera sensor failed to initialise"); + Serial.println("Will reboot to try again in 10s\n"); + delay(10000); + ESP.restart(); + } + sensor_t *s = esp_camera_sensor_get(); + + // Dump camera module, warn for unsupported modules. + switch (s->id.PID) + { + case OV9650_PID: + Serial.println("WARNING: OV9650 camera module is not properly supported, will fallback to OV2640 operation"); + break; + case OV7725_PID: + Serial.println("WARNING: OV7725 camera module is not properly supported, will fallback to OV2640 operation"); + break; + case OV2640_PID: + Serial.println("OV2640 camera module detected"); + break; + case OV3660_PID: + Serial.println("OV3660 camera module detected"); + break; + default: + Serial.println("WARNING: Camera module is unknown and not properly supported, will fallback to OV2640 operation"); + } + + // OV3660 initial sensors are flipped vertically and colors are a bit saturated + if (s->id.PID == OV3660_PID) + { + s->set_vflip(s, 1); //flip it back + s->set_brightness(s, 1); //up the blightness just a bit + s->set_saturation(s, -2); //lower the saturation + } + +// M5 Stack Wide has special needs +#if defined(CAMERA_MODEL_M5STACK_WIDE) + s->set_vflip(s, 1); + s->set_hmirror(s, 1); +#endif + +// Config can override mirror and flip +#if defined(H_MIRROR) + s->set_hmirror(s, H_MIRROR); +#endif +#if defined(V_FLIP) + s->set_vflip(s, V_FLIP); +#endif + +// set initial frame rate +#if defined(DEFAULT_RESOLUTION) + s->set_framesize(s, DEFAULT_RESOLUTION); +#else + s->set_framesize(s, FRAMESIZE_SVGA); +#endif + + /* + * Add any other defaults you want to apply at startup here: + * uncomment the line and set the value as desired (see the comments) + * + * these are defined in the esp headers here: + * https://github.com/espressif/esp32-camera/blob/master/driver/include/sensor.h#L149 + */ + + //s->set_framesize(s, FRAMESIZE_SVGA); // FRAMESIZE_[QQVGA|HQVGA|QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA|QXGA(ov3660)]); + //s->set_quality(s, val); // 10 to 63 + //s->set_brightness(s, 0); // -2 to 2 + //s->set_contrast(s, 0); // -2 to 2 + //s->set_saturation(s, 0); // -2 to 2 + //s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia) + //s->set_whitebal(s, 1); // aka 'awb' in the UI; 0 = disable , 1 = enable + //s->set_awb_gain(s, 1); // 0 = disable , 1 = enable + //s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) + //s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable + //s->set_aec2(s, 0); // 0 = disable , 1 = enable + //s->set_ae_level(s, 0); // -2 to 2 + //s->set_aec_value(s, 300); // 0 to 1200 + //s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable + //s->set_agc_gain(s, 0); // 0 to 30 + //s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6 + //s->set_bpc(s, 0); // 0 = disable , 1 = enable + //s->set_wpc(s, 1); // 0 = disable , 1 = enable + //s->set_raw_gma(s, 1); // 0 = disable , 1 = enable + //s->set_lenc(s, 1); // 0 = disable , 1 = enable + //s->set_hmirror(s, 0); // 0 = disable , 1 = enable + //s->set_vflip(s, 0); // 0 = disable , 1 = enable + //s->set_dcw(s, 1); // 0 = disable , 1 = enable + //s->set_colorbar(s, 0); // 0 = disable , 1 = enable + + // We now have camera with default init + // check for saved preferences and apply them + + if (filesystem) + { + filesystemStart(); + loadPrefs(SPIFFS); + loadFaceDB(SPIFFS); + } + else + { + Serial.println("No Internal Filesystem, cannot save preferences or face DB"); + } + + /* + * Camera setup complete; initialise the rest of the hardware. + */ + + // Initialise and set the lamp + if (lampVal != -1) + { + ledcSetup(lampChannel, pwmfreq, pwmresolution); // configure LED PWM channel + setLamp(lampVal); // set default value + ledcAttachPin(LAMP_PIN, lampChannel); // attach the GPIO pin to the channel + } + else + { + Serial.println("No lamp, or lamp disabled in config"); + } + + // Having got this far; start Wifi and loop until we are connected or have started an AccessPoint + while ((WiFi.status() != WL_CONNECTED) && !accesspoint) + { + WifiSetup(); + delay(1000); + } + + // Now we have a network we can start the two http handlers for the UI and Stream. + startCameraServer(); + startStreamServer(); + + // Construct the app and stream URLs + sprintf(httpURL, "http://%d.%d.%d.%d:%d/", ip[0], ip[1], ip[2], ip[3], HTTP_PORT); + + Serial.printf("\nCamera Ready!\nUse '%s' to connect\n", httpURL); + + if (debugData) + Serial.println("Camera debug data is enabled (send any char to disable)"); + else + Serial.println("Camera debug data is disabled (send any char to enable)"); + + // Used when dumping status; these are slow functions, so just do them once during startup + sketchSize = ESP.getSketchSize(); + sketchSpace = ESP.getFreeSketchSpace(); + sketchMD5 = ESP.getSketchMD5(); +} + +void loop() +{ + /* + * Just loop forever, reconnecting Wifi As necesscary in client mode + * The stream and URI handler processes initiated by the startCameraServer() call at the + * end of setup() will handle the camera and UI processing from now on. + */ + if (accesspoint) + { + // Accespoint is permanently up, so just loop, servicing the captive portal as needed + unsigned long start = millis(); + while (millis() - start < WIFI_WATCHDOG) + { + delay(100); + if (captivePortal) + dnsServer.processNextRequest(); + } + } + else + { + // client mode can fail; so reconnect as appropriate + static bool warned = false; + if (WiFi.status() == WL_CONNECTED) + { + // We are connected, wait a bit and re-check + if (warned) + { + // Tell the user if we have just reconnected + Serial.println("WiFi reconnected"); + warned = false; + } + // loop here for WIFI_WATCHDOG, turning debugData true/false depending on serial input.. + unsigned long start = millis(); + while (millis() - start < WIFI_WATCHDOG) + { + delay(100); + if (Serial.available()) + { + // Toggle debug output on serial input + if (debugData) + { + debugData = false; + Serial.println("Camera debug data is disabled (send any char to enable)"); + } + else + { + debugData = true; + Serial.println("Camera debug data is enabled (send any char to disable)"); + } + } + while (Serial.available()) + Serial.read(); // chomp the buffer + } + } + else + { + // disconnected; attempt to reconnect + if (!warned) + { + // Tell the user if we just disconnected + WiFi.disconnect(); // ensures disconnect is complete, wifi scan cleared + Serial.println("WiFi disconnected, retrying"); + warned = true; + } + WifiSetup(); + } + } +} diff --git a/platformio.ini b/platformio.ini index 74a34ef..94d3d36 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,3 +15,8 @@ src_dir = ./ platform = espressif32 board = esp32cam framework = arduino +monitor_speed = 115200 +build_flags = + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + \ No newline at end of file diff --git a/src/OV2640.cpp b/src/OV2640.cpp new file mode 100644 index 0000000..02d04d5 --- /dev/null +++ b/src/OV2640.cpp @@ -0,0 +1,193 @@ +#include "OV2640.h" + +#define TAG "OV2640" + +// definitions appropriate for the ESP32-CAM devboard (and most clones) +camera_config_t esp32cam_config{ + + .pin_pwdn = -1, // FIXME: on the TTGO T-Journal I think this is GPIO 0 + .pin_reset = 15, + + .pin_xclk = 27, + + .pin_sscb_sda = 25, + .pin_sscb_scl = 23, + + .pin_d7 = 19, + .pin_d6 = 36, + .pin_d5 = 18, + .pin_d4 = 39, + .pin_d3 = 5, + .pin_d2 = 34, + .pin_d1 = 35, + .pin_d0 = 17, + .pin_vsync = 22, + .pin_href = 26, + .pin_pclk = 21, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_0, + .ledc_channel = LEDC_CHANNEL_0, + .pixel_format = PIXFORMAT_JPEG, + // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space + // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer + // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 12, //0-63 lower numbers are higher quality + .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg +}; + +camera_config_t esp32cam_aithinker_config{ + + .pin_pwdn = 32, + .pin_reset = -1, + + .pin_xclk = 0, + + .pin_sscb_sda = 26, + .pin_sscb_scl = 27, + + // Note: LED GPIO is apparently 4 not sure where that goes + // per https://github.com/donny681/ESP32_CAMERA_QR/blob/e4ef44549876457cd841f33a0892c82a71f35358/main/led.c + .pin_d7 = 35, + .pin_d6 = 34, + .pin_d5 = 39, + .pin_d4 = 36, + .pin_d3 = 21, + .pin_d2 = 19, + .pin_d1 = 18, + .pin_d0 = 5, + .pin_vsync = 25, + .pin_href = 23, + .pin_pclk = 22, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_1, + .ledc_channel = LEDC_CHANNEL_1, + .pixel_format = PIXFORMAT_JPEG, + // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space + // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer + // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 12, //0-63 lower numbers are higher quality + .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg +}; + +camera_config_t esp32cam_ttgo_t_config{ + + .pin_pwdn = 26, + .pin_reset = -1, + + .pin_xclk = 32, + + .pin_sscb_sda = 13, + .pin_sscb_scl = 12, + + .pin_d7 = 39, + .pin_d6 = 36, + .pin_d5 = 23, + .pin_d4 = 18, + .pin_d3 = 15, + .pin_d2 = 4, + .pin_d1 = 14, + .pin_d0 = 5, + .pin_vsync = 27, + .pin_href = 25, + .pin_pclk = 19, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_0, + .ledc_channel = LEDC_CHANNEL_0, + .pixel_format = PIXFORMAT_JPEG, + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 12, //0-63 lower numbers are higher quality + .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg +}; + +void OV2640::run(void) +{ + if (fb) + //return the frame buffer back to the driver for reuse + esp_camera_fb_return(fb); + + fb = esp_camera_fb_get(); +} + +void OV2640::runIfNeeded(void) +{ + if (!fb) + run(); +} + +int OV2640::getWidth(void) +{ + runIfNeeded(); + return fb->width; +} + +int OV2640::getHeight(void) +{ + runIfNeeded(); + return fb->height; +} + +size_t OV2640::getSize(void) +{ + runIfNeeded(); + if (!fb) + return 0; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes? + return fb->len; +} + +uint8_t *OV2640::getfb(void) +{ + runIfNeeded(); + if (!fb) + return NULL; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes? + + return fb->buf; +} + +framesize_t OV2640::getFrameSize(void) +{ + return _cam_config.frame_size; +} + +void OV2640::setFrameSize(framesize_t size) +{ + _cam_config.frame_size = size; +} + +pixformat_t OV2640::getPixelFormat(void) +{ + return _cam_config.pixel_format; +} + +void OV2640::setPixelFormat(pixformat_t format) +{ + switch (format) + { + case PIXFORMAT_RGB565: + case PIXFORMAT_YUV422: + case PIXFORMAT_GRAYSCALE: + case PIXFORMAT_JPEG: + _cam_config.pixel_format = format; + break; + default: + _cam_config.pixel_format = PIXFORMAT_GRAYSCALE; + break; + } +} + +esp_err_t OV2640::init(camera_config_t config) +{ + memset(&_cam_config, 0, sizeof(_cam_config)); + memcpy(&_cam_config, &config, sizeof(config)); + + esp_err_t err = esp_camera_init(&_cam_config); + if (err != ESP_OK) + { + printf("Camera probe failed with error 0x%x", err); + return err; + } + // ESP_ERROR_CHECK(gpio_install_isr_service(0)); + + return ESP_OK; +} diff --git a/src/OV2640.h b/src/OV2640.h new file mode 100644 index 0000000..b9b5706 --- /dev/null +++ b/src/OV2640.h @@ -0,0 +1,43 @@ +#ifndef OV2640_H_ +#define OV2640_H_ + +#include +#include +#include +#include "esp_log.h" +#include "esp_attr.h" +#include "esp_camera.h" + +extern camera_config_t esp32cam_config, esp32cam_aithinker_config, esp32cam_ttgo_t_config; + +class OV2640 +{ +public: + OV2640(){ + fb = NULL; + }; + ~OV2640(){ + }; + esp_err_t init(camera_config_t config); + void run(void); + size_t getSize(void); + uint8_t *getfb(void); + int getWidth(void); + int getHeight(void); + framesize_t getFrameSize(void); + pixformat_t getPixelFormat(void); + + void setFrameSize(framesize_t size); + void setPixelFormat(pixformat_t format); + +private: + void runIfNeeded(); // grab a frame if we don't already have one + + // camera_framesize_t _frame_size; + // camera_pixelformat_t _pixel_format; + camera_config_t _cam_config; + + camera_fb_t *fb; +}; + +#endif //OV2640_H_ diff --git a/stream_httpd.cpp b/stream_httpd.cpp new file mode 100644 index 0000000..75b3175 --- /dev/null +++ b/stream_httpd.cpp @@ -0,0 +1,463 @@ +/* + + This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM + and ESP-EYE modules. + This is tested to work with VLC and Blynk video widget and can support up to 10 + simultaneously connected streaming clients. + Simultaneous streaming is implemented with FreeRTOS tasks. + + Inspired by and based on this Instructable: $9 RTSP Video Streamer Using the ESP32-CAM Board + (https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/) + + Board: AI-Thinker ESP32-CAM or ESP-EYE + Compile as: + ESP32 Dev Module + CPU Freq: 240 + Flash Freq: 80 + Flash mode: QIO + Flash Size: 4Mb + Patrition: Minimal SPIFFS + PSRAM: Enabled +*/ + +// ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) +#define APP_CPU 1 +#define PRO_CPU 0 + +#include "Arduino.h" +#include "src/OV2640.h" +#include +#include +#include + +#include +#include +#include +#include + +// Select camera model +//#define CAMERA_MODEL_WROVER_KIT +//#define CAMERA_MODEL_ESP_EYE +//#define CAMERA_MODEL_M5STACK_PSRAM +//#define CAMERA_MODEL_M5STACK_WIDE +#define CAMERA_MODEL_AI_THINKER // default + +#include "camera_pins.h" + +/* + Next one is an include with wifi credentials. + This is what you need to do: + + 1. Create a file called "home_wifi_multi.h" in the same folder OR under a separate subfolder of the "libraries" folder of Arduino IDE. (You are creating a "fake" library really - I called it "MySettings"). + 2. Place the following text in the file: + #define SSID1 "replace with your wifi ssid" + #define PWD1 "replace your wifi password" + 3. Save. + + Should work then +*/ + +#if !defined(STREAM_PORT) +#define STREAM_PORT 81 +#endif +int streamPort = STREAM_PORT; +WebServer server(streamPort); + +// ===== rtos task handles ========================= +// Streaming is implemented with 3 tasks: +TaskHandle_t tMjpeg; // handles client connections to the webserver +TaskHandle_t tCam; // handles getting picture frames from the camera and storing them locally +TaskHandle_t tStream; // actually streaming frames to all connected clients + +// frameSync semaphore is used to prevent streaming buffer as it is replaced with the next frame +SemaphoreHandle_t frameSync = NULL; + +// Queue stores currently connected clients to whom we are streaming +QueueHandle_t streamingClients; + +// We will try to achieve 25 FPS frame rate +const int FPS = 14; + +// We will handle web client requests every 50 ms (20 Hz) +const int WSINTERVAL = 100; + +// def +void mjpegCB(void *pvParameters); +void camCB(void *pvParameters); +char *allocateMemory(char *aPtr, size_t aSize); +void handleJPGSstream(void); +void streamCB(void *pvParameters); +void handleJPG(void); +void handleNotFound(); +void startStreamServer(int sPort); + +// ==== Memory allocator that takes advantage of PSRAM if present ======================= +char *allocateMemory(char *aPtr, size_t aSize) +{ + + // Since current buffer is too smal, free it + if (aPtr != NULL) + free(aPtr); + + size_t freeHeap = ESP.getFreeHeap(); + char *ptr = NULL; + + // If memory requested is more than 2/3 of the currently free heap, try PSRAM immediately + if (aSize > freeHeap * 2 / 3) + { + if (psramFound() && ESP.getFreePsram() > aSize) + { + ptr = (char *)ps_malloc(aSize); + } + } + else + { + // Enough free heap - let's try allocating fast RAM as a buffer + ptr = (char *)malloc(aSize); + + // If allocation on the heap failed, let's give PSRAM one more chance: + if (ptr == NULL && psramFound() && ESP.getFreePsram() > aSize) + { + ptr = (char *)ps_malloc(aSize); + } + } + + // Finally, if the memory pointer is NULL, we were not able to allocate any memory, and that is a terminal condition. + if (ptr == NULL) + { + ESP.restart(); + } + return ptr; +} + +// ======== Server Connection Handler Task ========================== +void mjpegCB(void *pvParameters) +{ + TickType_t xLastWakeTime; + const TickType_t xFrequency = pdMS_TO_TICKS(WSINTERVAL); + + // Creating frame synchronization semaphore and initializing it + frameSync = xSemaphoreCreateBinary(); + xSemaphoreGive(frameSync); + + // Creating a queue to track all connected clients + streamingClients = xQueueCreate(10, sizeof(WiFiClient *)); + + //=== setup section ================== + + // Creating RTOS task for grabbing frames from the camera + xTaskCreatePinnedToCore( + camCB, // callback + "cam", // name + 4096, // stacj size + NULL, // parameters + 2, // priority + &tCam, // RTOS task handle + APP_CPU); // core + + // Creating task to push the stream to all connected clients + xTaskCreatePinnedToCore( + streamCB, + "strmCB", + 4 * 1024, + NULL, //(void*) handler, + 2, + &tStream, + APP_CPU); + + // Registering webserver handling routines + server.on("/mjpeg/1", HTTP_GET, handleJPGSstream); + server.on("/jpg", HTTP_GET, handleJPG); + server.onNotFound(handleNotFound); + + // Starting webserver + server.begin(); + + //=== loop() section =================== + xLastWakeTime = xTaskGetTickCount(); + for (;;) + { + server.handleClient(); + + // After every server client handling request, we let other tasks run and then pause + taskYIELD(); + vTaskDelayUntil(&xLastWakeTime, xFrequency); + } +} + +// Commonly used variables: +volatile size_t camSize; // size of the current frame, byte +volatile char *camBuf; // pointer to the current frame + +// ==== RTOS task to grab frames from the camera ========================= +void camCB(void *pvParameters) +{ + + TickType_t xLastWakeTime; + + // A running interval associated with currently desired frame rate + const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS); + + // Mutex for the critical section of swithing the active frames around + portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED; + + //=== loop() section =================== + xLastWakeTime = xTaskGetTickCount(); + + // Pointers to the 2 frames, their respective sizes and index of the current frame + char *fbs[2] = {NULL, NULL}; + size_t fSize[2] = {0, 0}; + int ifb = 0; + + camera_fb_t *fb = NULL; + size_t _jpg_buf_len = 0; + uint8_t *_jpg_buf = NULL; + + for (;;) + { + + // Grab a frame from the camera and query its size + // cam.run(); + // size_t s = cam.getSize(); + fb = esp_camera_fb_get(); + if (!fb) + { + Serial.println("Camera capture failed"); + } + else + { + + 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"); + } + } + else + { + _jpg_buf_len = fb->len; + _jpg_buf = fb->buf; + } + + // If frame size is more that we have previously allocated - request 125% of the current frame space + if (_jpg_buf_len > fSize[ifb]) + { + fSize[ifb] = _jpg_buf_len * 4 / 3; + fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]); + } + + // Copy current frame into local buffer + memcpy(fbs[ifb], _jpg_buf, _jpg_buf_len); + } + + if (fb) + { + esp_camera_fb_return(fb); + fb = NULL; + _jpg_buf = NULL; + } + else if (_jpg_buf) + { + free(_jpg_buf); + _jpg_buf = NULL; + } + + // Let other tasks run and wait until the end of the current frame rate interval (if any time left) + taskYIELD(); + vTaskDelayUntil(&xLastWakeTime, xFrequency); + + // Only switch frames around if no frame is currently being streamed to a client + // Wait on a semaphore until client operation completes + xSemaphoreTake(frameSync, portMAX_DELAY); + + // Do not allow interrupts while switching the current frame + portENTER_CRITICAL(&xSemaphore); + camBuf = fbs[ifb]; + camSize = _jpg_buf_len; + ifb++; + ifb &= 1; // this should produce 1, 0, 1, 0, 1 ... sequence + portEXIT_CRITICAL(&xSemaphore); + + // Let anyone waiting for a frame know that the frame is ready + xSemaphoreGive(frameSync); + + // Technically only needed once: let the streaming task know that we have at least one frame + // and it could start sending frames to the clients, if any + xTaskNotifyGive(tStream); + + // Immediately let other (streaming) tasks run + taskYIELD(); + + // If streaming task has suspended itself (no active clients to stream to) + // there is no need to grab frames from the camera. We can save some juice + // by suspedning the tasks + if (eTaskGetState(tStream) == eSuspended) + { + vTaskSuspend(NULL); // passing NULL means "suspend yourself" + } + } +} + +// ==== STREAMING ====================================================== +const char HEADER[] = "HTTP/1.1 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n"; +const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n"; +const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: "; +const int hdrLen = strlen(HEADER); +const int bdrLen = strlen(BOUNDARY); +const int cntLen = strlen(CTNTTYPE); + +// ==== Handle connection request from clients =============================== +void handleJPGSstream(void) +{ + Serial.println("Handle stream request"); + // Can only acommodate 10 clients. The limit is a default for WiFi connections + if (!uxQueueSpacesAvailable(streamingClients)) + return; + + // Create a new WiFi Client object to keep track of this one + WiFiClient *client = new WiFiClient(); + *client = server.client(); + + // Immediately send this client a header + client->write(HEADER, hdrLen); + client->write(BOUNDARY, bdrLen); + + // Push the client to the streaming queue + xQueueSend(streamingClients, (void *)&client, 0); + + // Wake up streaming tasks, if they were previously suspended: + if (eTaskGetState(tCam) == eSuspended) + vTaskResume(tCam); + if (eTaskGetState(tStream) == eSuspended) + vTaskResume(tStream); +} + +// ==== Actually stream content to all connected clients ======================== +void streamCB(void *pvParameters) +{ + Serial.println("Stream request"); + char buf[16]; + TickType_t xLastWakeTime; + TickType_t xFrequency; + + // Wait until the first frame is captured and there is something to send + // to clients + ulTaskNotifyTake(pdTRUE, /* Clear the notification value before exiting. */ + portMAX_DELAY); /* Block indefinitely. */ + + xLastWakeTime = xTaskGetTickCount(); + for (;;) + { + // Default assumption we are running according to the FPS + xFrequency = pdMS_TO_TICKS(1000 / FPS); + + // Only bother to send anything if there is someone watching + UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients); + + if (activeClients) + { + // Adjust the period to the number of connected clients + xFrequency /= activeClients; + + Serial.printf("ActiveClients %d\n", activeClients); + + // Since we are sending the same frame to everyone, + // pop a client from the the front of the queue + WiFiClient *client; + xQueueReceive(streamingClients, (void *)&client, 0); + + // Check if this client is still connected. + + if (!client->connected()) + { + // delete this client reference if s/he has disconnected + // and don't put it back on the queue anymore. Bye! + delete client; + } + else + { + + // Ok. This is an actively connected client. + // Let's grab a semaphore to prevent frame changes while we + // are serving this frame + xSemaphoreTake(frameSync, portMAX_DELAY); + + client->write(CTNTTYPE, cntLen); + sprintf(buf, "%d\r\n\r\n", camSize); + client->write(buf, strlen(buf)); + client->write((char *)camBuf, (size_t)camSize); + client->write(BOUNDARY, bdrLen); + + // Since this client is still connected, push it to the end + // of the queue for further processing + xQueueSend(streamingClients, (void *)&client, 0); + + // The frame has been served. Release the semaphore and let other tasks run. + // If there is a frame switch ready, it will happen now in between frames + xSemaphoreGive(frameSync); + taskYIELD(); + } + } + else + { + // Since there are no connected clients, there is no reason to waste battery running + vTaskSuspend(NULL); + } + // Let other tasks run after serving every client + taskYIELD(); + vTaskDelayUntil(&xLastWakeTime, xFrequency); + } +} + +const char JHEADER[] = "HTTP/1.1 200 OK\r\n" + "Content-disposition: inline; filename=capture.jpg\r\n" + "Content-type: image/jpeg\r\n\r\n"; +const int jhdLen = strlen(JHEADER); + +// ==== Serve up one JPEG frame ============================================= +void handleJPG(void) +{ + WiFiClient client = server.client(); + + if (!client.connected()) + return; + client.write(JHEADER, jhdLen); + client.write((char *)camBuf, (size_t)camSize); +} + +// ==== Handle invalid URL requests ============================================ +void handleNotFound() +{ + String message = "Server is running!\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + server.send(200, "text / plain", message); +} + +// ==== SETUP method ================================================================== +void startStreamServer() +{ + + Serial.printf("Starting web server on port: '%d'\n", streamPort); + + // Start mainstreaming RTOS task + xTaskCreatePinnedToCore( + mjpegCB, + "mjpeg", + 4 * 1024, + NULL, + 2, + &tMjpeg, + APP_CPU); +}