From 66cd8f73961b50230c03b23ac8b78ebdd83ec234 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 10 Feb 2026 00:38:26 +0800 Subject: [PATCH] added wifi creds serial interface --- esp32_sense_cam.ino | 1067 ++++++++++++++++++------------------------- 1 file changed, 447 insertions(+), 620 deletions(-) diff --git a/esp32_sense_cam.ino b/esp32_sense_cam.ino index 402187a..ee4dad9 100644 --- a/esp32_sense_cam.ino +++ b/esp32_sense_cam.ino @@ -1,674 +1,501 @@ /* - * XIAO ESP32S3 Sense - Face Detection Web Server - * - * This sketch captures camera frames, runs face detection, - * and serves both the video stream and detection results via web server. - * - * Board: XIAO_ESP32S3 - * Required: ESP32 board package 2.0.8+ - * - * IMPORTANT: In Arduino IDE, go to Tools menu and set: - * - PSRAM: "OPI PSRAM" + * XIAO ESP32S3 Sense - Minimal MJPEG Stream + * Streams 320x240 JPEG over HTTP on port 81. */ #include "esp_camera.h" #include #include "esp_http_server.h" +#include -// Try to include face detection - available in ESP32 Arduino Core with ESP-WHO -#if __has_include("human_face_detect_msr01.hpp") - #include "human_face_detect_msr01.hpp" - #include "human_face_detect_mnp01.hpp" - #define FACE_DETECTION_AVAILABLE 1 -#elif __has_include("esp_face_detect.h") - #include "esp_face_detect.h" - #define FACE_DETECTION_AVAILABLE 2 -#else - #define FACE_DETECTION_AVAILABLE 0 - #warning "Face detection headers not found - using motion detection fallback" -#endif +// WiFi credentials (can be changed via serial, persisted in NVM) +char ssid[64] = "Police Surveillance Van"; +char password[64] = "ourpassword"; -// WiFi credentials -const char* ssid = "Police Surveillance Van"; -const char* password = "ourpassword"; +// HTTP server handle (global for reconnection) +httpd_handle_t server = NULL; -// =========================================== -// XIAO ESP32S3 Sense Camera Pin Definitions -// =========================================== -#define PWDN_GPIO_NUM -1 -#define RESET_GPIO_NUM -1 -#define XCLK_GPIO_NUM 10 -#define SIOD_GPIO_NUM 40 -#define SIOC_GPIO_NUM 39 +// NVM preferences for WiFi credentials +Preferences preferences; -#define Y9_GPIO_NUM 48 -#define Y8_GPIO_NUM 11 -#define Y7_GPIO_NUM 12 -#define Y6_GPIO_NUM 14 -#define Y5_GPIO_NUM 16 -#define Y4_GPIO_NUM 18 -#define Y3_GPIO_NUM 17 -#define Y2_GPIO_NUM 15 -#define VSYNC_GPIO_NUM 38 -#define HREF_GPIO_NUM 47 -#define PCLK_GPIO_NUM 13 +// XIAO ESP32S3 Sense camera pins +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 10 +#define SIOD_GPIO_NUM 40 +#define SIOC_GPIO_NUM 39 +#define Y9_GPIO_NUM 48 +#define Y8_GPIO_NUM 11 +#define Y7_GPIO_NUM 12 +#define Y6_GPIO_NUM 14 +#define Y5_GPIO_NUM 16 +#define Y4_GPIO_NUM 18 +#define Y3_GPIO_NUM 17 +#define Y2_GPIO_NUM 15 +#define VSYNC_GPIO_NUM 38 +#define HREF_GPIO_NUM 47 +#define PCLK_GPIO_NUM 13 -// LED pin for status -#define LED_GPIO_NUM 21 +#define PART_BOUNDARY "frame" +static const char *STREAM_CT = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; +static const char *STREAM_BOND = "\r\n--" PART_BOUNDARY "\r\n"; +static const char *STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; -// Global variables -httpd_handle_t stream_httpd = NULL; -httpd_handle_t camera_httpd = NULL; - -// Detection settings -static bool detectionEnabled = true; -static int detectionCount = 0; -static unsigned long lastDetectionTime = 0; - -// For motion/change detection fallback -static uint8_t* prevFrame = NULL; -static size_t prevFrameLen = 0; - -// Part boundary for MJPEG stream -#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\nX-Faces: %d\r\n\r\n"; - -#if FACE_DETECTION_AVAILABLE == 1 -// ESP-DL based face detection -HumanFaceDetectMSR01 *s_detector = nullptr; -HumanFaceDetectMNP01 *s_detector2 = nullptr; - -static int detect_faces_dl(camera_fb_t *fb, uint8_t **out_buf, size_t *out_len) { - if (!s_detector) { - s_detector = new HumanFaceDetectMSR01(0.1F, 0.5F, 10, 0.2F); - s_detector2 = new HumanFaceDetectMNP01(0.5F, 0.3F, 5); - } +// Simple test endpoint +static esp_err_t test_handler(httpd_req_t *req) { + Serial.println("[TEST] Client connected!"); + Serial.printf("[TEST] Method: %s, URI: %s\n", + req->method == HTTP_GET ? "GET" : "OTHER", req->uri); - int faces = 0; - - if (fb->format == PIXFORMAT_RGB565) { - // Convert to RGB888 - size_t rgb_len = fb->width * fb->height * 3; - uint8_t *rgb_buf = (uint8_t*)ps_malloc(rgb_len); - - if (rgb_buf) { - // Convert RGB565 to RGB888 - uint16_t *src = (uint16_t*)fb->buf; - for (size_t i = 0; i < fb->width * fb->height; i++) { - uint16_t p = src[i]; - rgb_buf[i*3] = ((p >> 11) & 0x1F) << 3; - rgb_buf[i*3+1] = ((p >> 5) & 0x3F) << 2; - rgb_buf[i*3+2] = (p & 0x1F) << 3; - } - - // Run detection - std::list &results = s_detector->infer(rgb_buf, {(int)fb->height, (int)fb->width, 3}); - - if (results.size() > 0) { - results = s_detector2->infer(rgb_buf, {(int)fb->height, (int)fb->width, 3}, results); - faces = results.size(); - - // Draw boxes - for (auto &r : results) { - int x1 = constrain(r.box[0], 0, fb->width-1); - int y1 = constrain(r.box[1], 0, fb->height-1); - int x2 = constrain(r.box[2], 0, fb->width-1); - int y2 = constrain(r.box[3], 0, fb->height-1); - - // Draw green rectangle - for (int x = x1; x <= x2; x++) { - rgb_buf[(y1 * fb->width + x) * 3 + 1] = 255; - rgb_buf[(y2 * fb->width + x) * 3 + 1] = 255; - } - for (int y = y1; y <= y2; y++) { - rgb_buf[(y * fb->width + x1) * 3 + 1] = 255; - rgb_buf[(y * fb->width + x2) * 3 + 1] = 255; - } - } - } - - // Convert to JPEG - if (!fmt2jpg(rgb_buf, rgb_len, fb->width, fb->height, PIXFORMAT_RGB888, 80, out_buf, out_len)) { - *out_buf = NULL; - *out_len = 0; - } - - free(rgb_buf); - } - } - - return faces; -} -#endif - -// Simple skin-tone based face detection (works without ESP-DL) -static int detect_faces_simple(uint8_t *rgb565_buf, int width, int height) { - int skinPixels = 0; - int totalPixels = width * height; - uint16_t *pixels = (uint16_t*)rgb565_buf; - - // Count skin-tone pixels (simplified detection) - for (int i = 0; i < totalPixels; i++) { - uint16_t p = pixels[i]; - uint8_t r = ((p >> 11) & 0x1F) << 3; - uint8_t g = ((p >> 5) & 0x3F) << 2; - uint8_t b = (p & 0x1F) << 3; - - // Simple skin tone detection in RGB - // Skin typically has R > 95, G > 40, B > 20 - // and R > G > B with R-G > 15 - if (r > 95 && g > 40 && b > 20 && - r > g && g > b && (r - g) > 15 && - (r - b) > 15) { - skinPixels++; - } - } - - // If more than 5% skin pixels, likely a face is present - float skinRatio = (float)skinPixels / totalPixels; - - if (skinRatio > 0.05 && skinRatio < 0.6) { - return 1; // Face likely detected - } - return 0; -} - -// Stream handler -static esp_err_t stream_handler(httpd_req_t *req) { - camera_fb_t *fb = NULL; - esp_err_t res = ESP_OK; - char part_buf[128]; - - res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE); - if (res != ESP_OK) return res; + char resp[256]; + snprintf(resp, sizeof(resp), + "ESP32 Camera Server is running!\n" + "IP: %s\n" + "Free Heap: %d bytes\n" + "Uptime: %lu seconds\n", + WiFi.localIP().toString().c_str(), + ESP.getFreeHeap(), + millis() / 1000); + httpd_resp_set_type(req, "text/plain"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - httpd_resp_set_hdr(req, "X-Framerate", "15"); - - while (true) { - fb = esp_camera_fb_get(); - if (!fb) { - Serial.println("Camera capture failed"); - res = ESP_FAIL; - break; - } - - uint8_t *jpg_buf = NULL; - size_t jpg_len = 0; - int faces = 0; - - if (detectionEnabled) { -#if FACE_DETECTION_AVAILABLE == 1 - if (fb->format == PIXFORMAT_RGB565) { - faces = detect_faces_dl(fb, &jpg_buf, &jpg_len); - } -#else - // Fallback: simple skin-tone detection - if (fb->format == PIXFORMAT_RGB565) { - faces = detect_faces_simple(fb->buf, fb->width, fb->height); - } -#endif - } - - // Use original frame if detection didn't produce output - if (jpg_buf == NULL) { - if (fb->format == PIXFORMAT_JPEG) { - jpg_buf = fb->buf; - jpg_len = fb->len; - } else { - bool converted = frame2jpg(fb, 80, &jpg_buf, &jpg_len); - if (!converted) { - esp_camera_fb_return(fb); - continue; - } - } - } - - // Update detection status - if (faces > 0) { - detectionCount = faces; - lastDetectionTime = millis(); - digitalWrite(LED_GPIO_NUM, LOW); // LED on - } else if (millis() - lastDetectionTime > 500) { - detectionCount = 0; - digitalWrite(LED_GPIO_NUM, HIGH); // LED off - } - - // Send frame - if (res == ESP_OK) { - res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); - } - if (res == ESP_OK) { - size_t hlen = snprintf(part_buf, sizeof(part_buf), _STREAM_PART, jpg_len, detectionCount); - res = httpd_resp_send_chunk(req, part_buf, hlen); - } - if (res == ESP_OK) { - res = httpd_resp_send_chunk(req, (const char*)jpg_buf, jpg_len); - } - - // Free JPEG buffer if we allocated it - if (jpg_buf != fb->buf) { - free(jpg_buf); - } - - esp_camera_fb_return(fb); - - if (res != ESP_OK) break; - } - - return res; + esp_err_t ret = httpd_resp_send(req, resp, strlen(resp)); + Serial.printf("[TEST] Response sent, result: 0x%x\n", ret); + return ret; } -// Single capture handler -static esp_err_t capture_handler(httpd_req_t *req) { - camera_fb_t *fb = esp_camera_fb_get(); - if (!fb) { - httpd_resp_send_500(req); - return ESP_FAIL; - } - - httpd_resp_set_type(req, "image/jpeg"); - httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - - esp_err_t res; - if (fb->format == PIXFORMAT_JPEG) { - res = httpd_resp_send(req, (const char*)fb->buf, fb->len); - } else { - uint8_t *jpg_buf = NULL; - size_t jpg_len = 0; - if (frame2jpg(fb, 80, &jpg_buf, &jpg_len)) { - res = httpd_resp_send(req, (const char*)jpg_buf, jpg_len); - free(jpg_buf); - } else { - res = ESP_FAIL; - httpd_resp_send_500(req); - } - } - - esp_camera_fb_return(fb); - return res; -} - -// Status handler -static esp_err_t status_handler(httpd_req_t *req) { - char json[256]; - snprintf(json, sizeof(json), - "{\"detection\":%s,\"count\":%d,\"method\":\"%s\",\"heap\":%lu,\"psram\":%lu}", - detectionEnabled ? "true" : "false", - detectionCount, -#if FACE_DETECTION_AVAILABLE == 1 - "neural-network", -#else - "skin-tone", -#endif - (unsigned long)ESP.getFreeHeap(), - (unsigned long)ESP.getFreePsram()); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - return httpd_resp_send(req, json, strlen(json)); -} - -// Toggle detection -static esp_err_t toggle_handler(httpd_req_t *req) { - detectionEnabled = !detectionEnabled; - - char json[64]; - snprintf(json, sizeof(json), "{\"detection\":%s}", detectionEnabled ? "true" : "false"); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - return httpd_resp_send(req, json, strlen(json)); -} - -// Main page - HTML in static storage to avoid stack overflow +// Simple HTML page with embedded stream static esp_err_t index_handler(httpd_req_t *req) { - static const char html[] = R"rawliteral( - - - - - - XIAO ESP32S3 Face Detection - - - -
-

XIAO ESP32S3 Face Detection

-
- Camera Stream -
- - No faces -
-
-
- - - -
-
-
-
Detection
-
ON
-
-
-
Faces
-
0
-
-
-
Method
-
-
-
-
-
Free RAM
-
-
-
-
-
- - - -)rawliteral"; - + Serial.println("[INDEX] Client connected!"); + Serial.printf("[INDEX] Method: %s, URI: %s\n", + req->method == HTTP_GET ? "GET" : "OTHER", req->uri); + + char html[512]; + snprintf(html, sizeof(html), + "" + "ESP32 Camera Stream" + "

ESP32 Camera Stream

" + "

Server IP: %s

" + "
" + "

If you see this page, the server is working.

" + "

If the image doesn't load, check Serial Monitor for errors.

" + "

Test endpoint | Direct stream

" + "", + WiFi.localIP().toString().c_str()); + httpd_resp_set_type(req, "text/html"); - return httpd_resp_send(req, html, strlen(html)); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + esp_err_t ret = httpd_resp_send(req, html, strlen(html)); + Serial.printf("[INDEX] Response sent, result: 0x%x\n", ret); + return ret; } -void startCameraServer() { - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.server_port = 80; - config.ctrl_port = 32768; - config.max_open_sockets = 7; +static esp_err_t stream_handler(httpd_req_t *req) { + Serial.println("[STREAM] Client connected"); + httpd_resp_set_type(req, STREAM_CT); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler }; - httpd_uri_t capture_uri = { .uri = "/capture", .method = HTTP_GET, .handler = capture_handler }; - httpd_uri_t status_uri = { .uri = "/status", .method = HTTP_GET, .handler = status_handler }; - httpd_uri_t toggle_uri = { .uri = "/toggle", .method = HTTP_GET, .handler = toggle_handler }; - httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler }; + char hdr[64]; + int frame_count = 0; + int fail_count = 0; + while (true) { + camera_fb_t *fb = esp_camera_fb_get(); + if (!fb) { + fail_count++; + Serial.printf("[STREAM] Capture failed (failures: %d)\n", fail_count); + if (fail_count > 10) { + Serial.println("[STREAM] Too many failures, closing connection"); + return ESP_FAIL; + } + delay(100); + continue; + } - Serial.printf("Starting web server on port %d\n", config.server_port); - if (httpd_start(&camera_httpd, &config) == ESP_OK) { - httpd_register_uri_handler(camera_httpd, &index_uri); - httpd_register_uri_handler(camera_httpd, &capture_uri); - httpd_register_uri_handler(camera_httpd, &status_uri); - httpd_register_uri_handler(camera_httpd, &toggle_uri); + frame_count++; + if (frame_count % 30 == 1) { + Serial.printf("[STREAM] Frame %d: %dx%d fmt=%d len=%u\n", + frame_count, fb->width, fb->height, fb->format, fb->len); + } + + size_t hlen = snprintf(hdr, sizeof(hdr), STREAM_PART, fb->len); + esp_err_t res = httpd_resp_send_chunk(req, STREAM_BOND, strlen(STREAM_BOND)); + if (res == ESP_OK) res = httpd_resp_send_chunk(req, hdr, hlen); + if (res == ESP_OK) res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len); + + esp_camera_fb_return(fb); + if (res != ESP_OK) { + Serial.printf("[STREAM] Send failed: 0x%x (client may have disconnected)\n", res); + return res; + } + fail_count = 0; // Reset on success } +} - config.server_port = 81; - config.ctrl_port = 32769; +// Serial command buffer +#define SERIAL_BUFFER_SIZE 256 +char serial_buffer[SERIAL_BUFFER_SIZE]; +int serial_buffer_idx = 0; + +// Print serial command instructions +void print_serial_instructions() { + Serial.println("\n=== Serial Commands ==="); + Serial.println("? - Show this help"); + Serial.println("$ssid,password - Update WiFi credentials"); + Serial.println(" Example: $My Network,secret123"); + Serial.println(" Note: Spaces are allowed in SSID and password"); + Serial.println("========================\n"); +} + +// Load WiFi credentials from NVM +void load_wifi_creds_from_nvm() { + preferences.begin("wifi", true); // Read-only mode - 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); + if (preferences.isKey("ssid")) { + String saved_ssid = preferences.getString("ssid", ""); + String saved_password = preferences.getString("password", ""); + + if (saved_ssid.length() > 0) { + saved_ssid.toCharArray(ssid, sizeof(ssid)); + saved_password.toCharArray(password, sizeof(password)); + Serial.println("[NVM] Loaded WiFi credentials from NVM"); + Serial.printf(" SSID: %s\n", ssid); + } else { + Serial.println("[NVM] No saved credentials found, using defaults"); + } + } else { + Serial.println("[NVM] No saved credentials found, using defaults"); + } + + preferences.end(); +} + +// Save WiFi credentials to NVM +void save_wifi_creds_to_nvm() { + preferences.begin("wifi", false); // Read-write mode + + preferences.putString("ssid", String(ssid)); + preferences.putString("password", String(password)); + + preferences.end(); + + Serial.println("[NVM] WiFi credentials saved to NVM"); +} + +// Parse and update WiFi credentials from serial command +void parse_wifi_command(const char* cmd) { + // Format: $ssid,password + if (cmd[0] != '$') { + Serial.println("[ERROR] Command must start with $"); + return; + } + + // Find the comma separator + const char* comma = strchr(cmd + 1, ','); + if (!comma) { + Serial.println("[ERROR] Format: $ssid,password"); + return; + } + + // Extract SSID (between $ and comma) + int ssid_len = comma - (cmd + 1); + if (ssid_len >= sizeof(ssid)) { + Serial.println("[ERROR] SSID too long (max 63 chars)"); + return; + } + strncpy(ssid, cmd + 1, ssid_len); + ssid[ssid_len] = '\0'; + + // Extract password (after comma) + int pwd_len = strlen(comma + 1); + if (pwd_len >= sizeof(password)) { + Serial.println("[ERROR] Password too long (max 63 chars)"); + return; + } + strncpy(password, comma + 1, sizeof(password) - 1); + password[sizeof(password) - 1] = '\0'; + + Serial.printf("[WIFI] Updated credentials:\n"); + Serial.printf(" SSID: %s\n", ssid); + Serial.printf(" Password: %s\n", password); + + // Save to NVM + save_wifi_creds_to_nvm(); +} + +// Reconnect WiFi with new credentials +void reconnect_wifi() { + Serial.println("\n[WIFI] Disconnecting..."); + WiFi.disconnect(); + delay(500); + + Serial.println("[WIFI] Connecting with new credentials..."); + WiFi.begin(ssid, password); + + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 30) { + delay(500); + Serial.print("."); + attempts++; + } + Serial.println(); + + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("[WIFI] Connected! IP: %s\n", WiFi.localIP().toString().c_str()); + + // Restart HTTP server if it was running + if (server != NULL) { + Serial.println("[HTTP] Stopping server..."); + httpd_stop(server); + server = NULL; + } + + Serial.println("[HTTP] Starting server..."); + httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG(); + hcfg.server_port = 81; + hcfg.max_open_sockets = 7; + hcfg.stack_size = 8192; + hcfg.ctrl_port = 32768; + hcfg.max_uri_handlers = 10; + hcfg.max_resp_headers = 8; + hcfg.backlog_conn = 5; + + esp_err_t http_err = httpd_start(&server, &hcfg); + if (http_err != ESP_OK) { + Serial.printf("[HTTP] Server start FAILED: 0x%x\n", http_err); + return; + } + + // Re-register handlers + httpd_uri_t test_uri = { .uri = "/test", .method = HTTP_GET, .handler = test_handler }; + httpd_register_uri_handler(server, &test_uri); + + httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler }; + httpd_register_uri_handler(server, &index_uri); + + httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler }; + httpd_register_uri_handler(server, &stream_uri); + + Serial.printf("[HTTP] Server restarted. New URL: http://%s:81/\n", + WiFi.localIP().toString().c_str()); + } else { + Serial.println("[WIFI] Connection FAILED!"); + } +} + +// Process serial input +void process_serial_input() { + while (Serial.available() > 0) { + char c = Serial.read(); + + // Handle line endings + if (c == '\n' || c == '\r') { + if (serial_buffer_idx > 0) { + serial_buffer[serial_buffer_idx] = '\0'; + + // Process command + if (strcmp(serial_buffer, "?") == 0) { + print_serial_instructions(); + } else if (serial_buffer[0] == '$') { + parse_wifi_command(serial_buffer); + reconnect_wifi(); + } else { + Serial.printf("[ERROR] Unknown command: %s\n", serial_buffer); + Serial.println("Type ? for help"); + } + + serial_buffer_idx = 0; + } + } else if (serial_buffer_idx < SERIAL_BUFFER_SIZE - 1) { + serial_buffer[serial_buffer_idx++] = c; + } else { + // Buffer overflow + Serial.println("[ERROR] Command too long!"); + serial_buffer_idx = 0; + } } } void setup() { Serial.begin(115200); - for (int i = 0; i < 10; i++){ - Serial.println(i); - delay(500); - } - Serial.setDebugOutput(true); - Serial.println(); + delay(1000); - // Configure LED - pinMode(LED_GPIO_NUM, OUTPUT); - digitalWrite(LED_GPIO_NUM, HIGH); + // Load WiFi credentials from NVM (if saved) + load_wifi_creds_from_nvm(); + Serial.println("\n=== XIAO ESP32S3 Camera Stream ==="); + // Check PSRAM if (psramFound()) { - Serial.printf("PSRAM found: %d bytes\n", ESP.getPsramSize()); + Serial.printf("PSRAM: %d bytes available\n", ESP.getPsramSize()); } else { - Serial.println("WARNING: No PSRAM found! Face detection may not work."); - Serial.println("Make sure PSRAM is set to 'OPI PSRAM' in Tools menu."); + Serial.println("WARNING: No PSRAM detected!"); } - - // Camera configuration - 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_sccb_sda = SIOD_GPIO_NUM; - config.pin_sccb_scl = SIOC_GPIO_NUM; - config.pin_pwdn = PWDN_GPIO_NUM; - config.pin_reset = RESET_GPIO_NUM; - config.xclk_freq_hz = 20000000; - config.grab_mode = CAMERA_GRAB_LATEST; - config.fb_location = CAMERA_FB_IN_PSRAM; - -#if FACE_DETECTION_AVAILABLE == 1 - // For neural network face detection, use RGB565 - config.frame_size = FRAMESIZE_QVGA; // 320x240 - config.pixel_format = PIXFORMAT_RGB565; - config.fb_count = 2; - config.jpeg_quality = 12; - Serial.println("Face detection: Neural Network (ESP-DL)"); -#else - // For skin-tone detection, JPEG is fine and faster - config.frame_size = FRAMESIZE_VGA; // 640x480 - config.pixel_format = PIXFORMAT_JPEG; - config.fb_count = 2; - config.jpeg_quality = 10; - Serial.println("Face detection: Skin-tone heuristic (fallback)"); -#endif - - // Initialize camera - esp_err_t err = esp_camera_init(&config); - if (err != ESP_OK) { - Serial.printf("Camera init failed with error 0x%x\n", err); + Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap()); + + camera_config_t cfg = {}; + cfg.ledc_channel = LEDC_CHANNEL_0; + cfg.ledc_timer = LEDC_TIMER_0; + cfg.pin_d0 = Y2_GPIO_NUM; + cfg.pin_d1 = Y3_GPIO_NUM; + cfg.pin_d2 = Y4_GPIO_NUM; + cfg.pin_d3 = Y5_GPIO_NUM; + cfg.pin_d4 = Y6_GPIO_NUM; + cfg.pin_d5 = Y7_GPIO_NUM; + cfg.pin_d6 = Y8_GPIO_NUM; + cfg.pin_d7 = Y9_GPIO_NUM; + cfg.pin_xclk = XCLK_GPIO_NUM; + cfg.pin_pclk = PCLK_GPIO_NUM; + cfg.pin_vsync = VSYNC_GPIO_NUM; + cfg.pin_href = HREF_GPIO_NUM; + cfg.pin_sccb_sda = SIOD_GPIO_NUM; + cfg.pin_sccb_scl = SIOC_GPIO_NUM; + cfg.pin_pwdn = PWDN_GPIO_NUM; + cfg.pin_reset = RESET_GPIO_NUM; + cfg.xclk_freq_hz = 20000000; + cfg.frame_size = FRAMESIZE_QVGA; // 320x240 + cfg.pixel_format = PIXFORMAT_JPEG; + cfg.grab_mode = CAMERA_GRAB_LATEST; + cfg.fb_location = CAMERA_FB_IN_PSRAM; + cfg.jpeg_quality = 10; + cfg.fb_count = 2; + + Serial.println("Initializing camera..."); + esp_err_t cam_err = esp_camera_init(&cfg); + if (cam_err != ESP_OK) { + Serial.printf("Camera init FAILED: 0x%x\n", cam_err); + Serial.println("Check:"); + Serial.println(" - Camera cable connection"); + Serial.println(" - Camera power (should be 3.3V)"); + Serial.println(" - Pin connections"); while (true) { - digitalWrite(LED_GPIO_NUM, LOW); - delay(100); - digitalWrite(LED_GPIO_NUM, HIGH); - delay(100); + delay(1000); + Serial.print("."); } } - - Serial.println("Camera initialized successfully"); - - // Camera settings + Serial.println("Camera init OK"); + + // Fix green cast: enable AWB, tune saturation/brightness sensor_t *s = esp_camera_sensor_get(); if (s) { + Serial.printf("Sensor PID: 0x%04x\n", s->id.PID); + if (s->id.PID == OV2640_PID) Serial.println("Sensor: OV2640 detected"); + else if (s->id.PID == OV3660_PID) Serial.println("Sensor: OV3660 detected"); + else Serial.println("Sensor: Unknown model"); + + s->set_whitebal(s, 1); // 1 = auto white balance + s->set_awb_gain(s, 0); // enable AWB gain + s->set_brightness(s, 0); // 0 = neutral + s->set_contrast(s, 0); // 0 = neutral + s->set_saturation(s, -1); // slight -1 can reduce green push on OV26xx/OV36xx s->set_vflip(s, 0); s->set_hmirror(s, 0); - s->set_brightness(s, 1); - s->set_contrast(s, 1); + Serial.println("Sensor settings applied"); + } else { + Serial.println("WARNING: Could not get sensor handle!"); } - - // Connect to WiFi + + // Test capture + Serial.println("Testing camera capture..."); + camera_fb_t *test_fb = esp_camera_fb_get(); + if (test_fb) { + Serial.printf("Test capture OK: %dx%d fmt=%d len=%u\n", + test_fb->width, test_fb->height, test_fb->format, test_fb->len); + esp_camera_fb_return(test_fb); + } else { + Serial.println("WARNING: Test capture FAILED - camera may not be working!"); + } + + Serial.println("\nConnecting to WiFi..."); WiFi.begin(ssid, password); - WiFi.setSleep(false); - - Serial.print("Connecting to WiFi"); - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { + int wifi_attempts = 0; + while (WiFi.status() != WL_CONNECTED && wifi_attempts < 30) { delay(500); Serial.print("."); - digitalWrite(LED_GPIO_NUM, (attempts % 2) ? LOW : HIGH); - attempts++; + wifi_attempts++; + } + Serial.println(); + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("WiFi connected! IP: %s\n", WiFi.localIP().toString().c_str()); + + Serial.println("Starting HTTP server..."); + httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG(); + hcfg.server_port = 81; + hcfg.max_open_sockets = 7; + hcfg.stack_size = 8192; + hcfg.ctrl_port = 32768; + hcfg.max_uri_handlers = 10; + hcfg.max_resp_headers = 8; + hcfg.backlog_conn = 5; + + esp_err_t http_err = httpd_start(&server, &hcfg); + if (http_err != ESP_OK) { + Serial.printf("HTTP server start FAILED: 0x%x\n", http_err); + Serial.println("Possible causes:"); + Serial.println(" - Port 81 already in use"); + Serial.println(" - Insufficient memory"); + Serial.println(" - Network stack issue"); + Serial.println("You can still use serial commands to update WiFi credentials."); + } else { + Serial.println("HTTP server started, registering handlers..."); + + // Register endpoints + httpd_uri_t test_uri = { .uri = "/test", .method = HTTP_GET, .handler = test_handler }; + esp_err_t reg_err = httpd_register_uri_handler(server, &test_uri); + Serial.printf(" /test handler: %s\n", reg_err == ESP_OK ? "OK" : "FAILED"); + + httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler }; + reg_err = httpd_register_uri_handler(server, &index_uri); + Serial.printf(" / handler: %s\n", reg_err == ESP_OK ? "OK" : "FAILED"); + + httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler }; + reg_err = httpd_register_uri_handler(server, &stream_uri); + Serial.printf(" /stream handler: %s\n", reg_err == ESP_OK ? "OK" : "FAILED"); + + Serial.printf("\n=== Server Ready ===\n"); + Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str()); + Serial.printf("Subnet Mask: %s\n", WiFi.subnetMask().toString().c_str()); + Serial.printf("Gateway: %s\n", WiFi.gatewayIP().toString().c_str()); + Serial.printf("RSSI: %d dBm\n", WiFi.RSSI()); + Serial.printf("\nTest URLs:\n"); + Serial.printf(" http://%s:81/test\n", WiFi.localIP().toString().c_str()); + Serial.printf(" http://%s:81/\n", WiFi.localIP().toString().c_str()); + Serial.printf(" http://%s:81/stream\n", WiFi.localIP().toString().c_str()); + Serial.println("\nIf you can't connect:"); + Serial.println(" 1. Verify you're on the same WiFi network"); + Serial.println(" 2. Check Windows Firewall isn't blocking port 81"); + Serial.println(" 3. Try: ping " + WiFi.localIP().toString()); + Serial.println(" 4. Check Serial Monitor for connection attempts\n"); + } + } else { + Serial.println("WiFi connection FAILED!"); + Serial.println("You can update WiFi credentials via serial:"); + Serial.println(" Type '?' for help"); + Serial.println(" Use: $ssid,password to set new credentials"); + Serial.println(" Example: $My Network,secret123"); } - if (WiFi.status() == WL_CONNECTED) { - Serial.println("\nWiFi connected!"); - digitalWrite(LED_GPIO_NUM, HIGH); - - startCameraServer(); - - Serial.println("\n========================================"); - Serial.println(" Face Detection Web Server Ready!"); - Serial.println("========================================"); - Serial.print(" Open: http://"); - Serial.println(WiFi.localIP()); - Serial.print(" Stream: http://"); - Serial.print(WiFi.localIP()); - Serial.println(":81/stream"); - Serial.println("========================================\n"); - } else { - Serial.println("\nWiFi connection failed!"); - while (true) { - digitalWrite(LED_GPIO_NUM, LOW); - delay(1000); - digitalWrite(LED_GPIO_NUM, HIGH); - delay(1000); - } - } + Serial.println("Type ? for serial command help\n"); } void loop() { - delay(10000); - Serial.printf("Status - Heap: %lu, PSRAM: %lu, Faces: %d\n", - (unsigned long)ESP.getFreeHeap(), - (unsigned long)ESP.getFreePsram(), - detectionCount); + // Check for serial commands + process_serial_input(); + + // Periodic status update + static unsigned long last_status = 0; + if (millis() - last_status > 10000) { + last_status = millis(); + camera_fb_t *fb = esp_camera_fb_get(); + if (fb) { + Serial.printf("[STATUS] Heap: %dKB PSRAM: %dKB Frame: %dx%d len=%u\n", + ESP.getFreeHeap() / 1024, + ESP.getFreePsram() / 1024, + fb->width, fb->height, fb->len); + esp_camera_fb_return(fb); + } else { + Serial.println("[STATUS] Camera capture FAILED!"); + } + } + + delay(10); // Small delay to prevent watchdog issues }