esp32_sense_cam/esp32_sense_cam.ino

502 lines
18 KiB
C++

/*
* XIAO ESP32S3 Sense - Minimal MJPEG Stream
* Streams 320x240 JPEG over HTTP on port 81.
*/
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"
#include <Preferences.h>
// 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),
"<!DOCTYPE html><html><head><meta charset='UTF-8'>"
"<title>ESP32 Camera Stream</title></head><body>"
"<h1>ESP32 Camera Stream</h1>"
"<p>Server IP: %s</p>"
"<img src='/stream' style='max-width:100%%;'><br>"
"<p>If you see this page, the server is working.</p>"
"<p>If the image doesn't load, check Serial Monitor for errors.</p>"
"<p><a href='/test'>Test endpoint</a> | <a href='/stream'>Direct stream</a></p>"
"</body></html>",
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
}