502 lines
18 KiB
C++
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
|
|
}
|