/* * 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 // WiFi credentials (can be changed via serial, persisted in NVM) char ssid[64] = "Police Surveillance Van"; char password[64] = "ourpassword"; // HTTP server handle (global for reconnection) httpd_handle_t server = NULL; // NVM preferences for WiFi credentials Preferences preferences; // 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 #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"; // 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); 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", "*"); esp_err_t ret = httpd_resp_send(req, resp, strlen(resp)); Serial.printf("[TEST] Response sent, result: 0x%x\n", ret); return ret; } // Simple HTML page with embedded stream static esp_err_t index_handler(httpd_req_t *req) { 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"); 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; } 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", "*"); 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; } 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 } } // 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 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); delay(1000); // 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: %d bytes available\n", ESP.getPsramSize()); } else { Serial.println("WARNING: No PSRAM detected!"); } 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) { delay(1000); Serial.print("."); } } 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); Serial.println("Sensor settings applied"); } else { Serial.println("WARNING: Could not get sensor handle!"); } // 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); int wifi_attempts = 0; while (WiFi.status() != WL_CONNECTED && wifi_attempts < 30) { delay(500); Serial.print("."); 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"); } Serial.println("Type ? for serial command help\n"); } void loop() { // 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 }