Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /****************************************************************************************
- * ESP32-CAM “Maze Navigator” – resilient MJPEG stream + treasure detection + gallery
- * Board : AI-Thinker ESP32-CAM (OV2640 @ 160 MHz, PSRAM)
- * Features
- * • Live MJPEG stream (/stream)
- * • Yellow “treasure” auto-detect → photo saved (max 10 images, oldest evicted)
- * • Manual capture, gallery thumbnails, clear-all
- * • Mutex-protected camera access, automatic camera re-init on failure
- * • SPIFFS wear-aware size cap (MAX_IMAGES = 10)
- * • Single-page responsive UI
- ****************************************************************************************/
- #include <WiFi.h>
- #include <esp_camera.h>
- #include <esp_timer.h>
- #include <Arduino.h>
- #include "soc/soc.h"
- #include "soc/rtc_cntl_reg.h" // Brown-out detector disable
- #include "driver/rtc_io.h"
- #include <ESPAsyncWebServer.h>
- #include <FS.h>
- #include <SPIFFS.h>
- #include "freertos/semphr.h"
- /* ────────────────────────────── Wi-Fi creds ─────────────────────────────── */
- const char* ssid = "***";
- const char* password = "***";
- /* ───────────────────────────── Global objects ───────────────────────────── */
- AsyncWebServer server(80);
- SemaphoreHandle_t camMux;
- /* Runtime state */
- bool takeNewPhoto = false;
- bool autoDetectionEnabled = true;
- uint16_t photoCounter = 0;
- unsigned long lastDetection = 0;
- const unsigned long DETECT_COOLDOWN_MS = 3000; // 3-s debounce
- const int MAX_IMAGES = 10; // storage cap
- /* Simple yellow-pixel heuristic */
- const int YELLOW_MIN = 20;
- const int YELLOW_MAX = 500;
- const int SAMPLE_SKIP = 4;
- /* ───────────────────────── camera pin-map (AI-Thinker) ──────────────────── */
- #define PWDN_GPIO_NUM 32
- #define RESET_GPIO_NUM -1
- #define XCLK_GPIO_NUM 0
- #define SIOD_GPIO_NUM 26
- #define SIOC_GPIO_NUM 27
- #define Y9_GPIO_NUM 35
- #define Y8_GPIO_NUM 34
- #define Y7_GPIO_NUM 39
- #define Y6_GPIO_NUM 36
- #define Y5_GPIO_NUM 21
- #define Y4_GPIO_NUM 19
- #define Y3_GPIO_NUM 18
- #define Y2_GPIO_NUM 5
- #define VSYNC_GPIO_NUM 25
- #define HREF_GPIO_NUM 23
- #define PCLK_GPIO_NUM 22
- /* ───────────────────────────── HTML UI page ─────────────────────────────── */
- const char index_html[] PROGMEM = R"rawliteral(
- <!DOCTYPE html><html><head>
- <meta name=viewport content="width=device-width,initial-scale=1">
- <style>
- body{font-family:Arial;text-align:center;margin:0;padding:20px}
- button{padding:10px 20px;margin:5px;font-size:16px;cursor:pointer}
- .thumb{width:150px;margin:5px;border:2px solid #ccc;cursor:pointer}
- .thumb:hover{border-color:#007bff}
- </style></head><body>
- <h2>ESP32-CAM Maze Navigator</h2>
- Auto Detection: <span id=dstat>Enabled</span><br>
- Photos Stored : <span id=pcount>0</span><br>
- <button onclick="tog()">Toggle Detection</button>
- <button onclick="cap()">Manual Capture</button>
- <button onclick="clr()">Clear Photos</button>
- <button onclick="gal()">Refresh Gallery</button><br><br>
- <img id=stream src="/stream" style="width:90%;max-width:600px" loading=lazy><br>
- <div id=galbox></div>
- <script>
- function tog(){fetch('/toggle-detection').then(r=>r.text()).then(t=>dstat.textContent=t)}
- function cap(){fetch('/capture').then(()=>setTimeout(gal,1500))}
- function clr(){if(confirm('Delete all photos?'))fetch('/clear-photos').then(()=>gal())}
- function gal(){fetch('/gallery-data').then(r=>r.json()).then(js=>{
- galbox.innerHTML='';pcount.textContent=js.length;
- js.forEach(f=>{let i=document.createElement('img');i.src='/photo/'+f;i.className='thumb';
- i.onclick=()=>open('/photo/'+f,'_blank');galbox.appendChild(i);});
- });}
- setInterval(gal,4000);gal();
- stream.onerror=()=>stream.src='/stream?'+Date.now();
- </script></body></html>)rawliteral";
- /* ─────────── forward declarations so everything compiles cleanly ────────── */
- void setupWebServer();
- size_t streamHandler(uint8_t*, size_t, size_t);
- bool detectTreasure(camera_fb_t*);
- void savePhoto(camera_fb_t*, const String&);
- void captureManual();
- void enforceMaxImages();
- bool kickCamera();
- /* camera config kept global so kickCamera() can reuse it */
- camera_config_t camCfg{};
- /* ──────────────────────────────── setup() ───────────────────────────────── */
- void setup() {
- Serial.begin(115200);
- /* Wi-Fi connect */
- WiFi.begin(ssid, password);
- while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print('.'); }
- /* SPIFFS */
- if (!SPIFFS.begin(true)) { Serial.println("SPIFFS mount failed"); ESP.restart(); }
- /* Disable brown-out */
- WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
- /* Configure camera */
- camCfg.ledc_channel = LEDC_CHANNEL_0;
- camCfg.ledc_timer = LEDC_TIMER_0;
- camCfg.pin_d0 = Y2_GPIO_NUM; camCfg.pin_d1 = Y3_GPIO_NUM; camCfg.pin_d2 = Y4_GPIO_NUM; camCfg.pin_d3 = Y5_GPIO_NUM;
- camCfg.pin_d4 = Y6_GPIO_NUM; camCfg.pin_d5 = Y7_GPIO_NUM; camCfg.pin_d6 = Y8_GPIO_NUM; camCfg.pin_d7 = Y9_GPIO_NUM;
- camCfg.pin_xclk = XCLK_GPIO_NUM; camCfg.pin_pclk = PCLK_GPIO_NUM; camCfg.pin_vsync = VSYNC_GPIO_NUM;
- camCfg.pin_href = HREF_GPIO_NUM; camCfg.pin_sccb_sda = SIOD_GPIO_NUM; camCfg.pin_sccb_scl = SIOC_GPIO_NUM;
- camCfg.pin_pwdn = PWDN_GPIO_NUM; camCfg.pin_reset = RESET_GPIO_NUM;
- camCfg.xclk_freq_hz = 20000000;
- camCfg.pixel_format = PIXFORMAT_JPEG;
- if (psramFound()) { camCfg.frame_size = FRAMESIZE_HVGA; camCfg.jpeg_quality = 12; camCfg.fb_count = 2; }
- else { camCfg.frame_size = FRAMESIZE_CIF; camCfg.jpeg_quality = 15; camCfg.fb_count = 1; }
- if (esp_camera_init(&camCfg) != ESP_OK) { Serial.println("Camera init failed"); ESP.restart(); }
- camMux = xSemaphoreCreateMutex(); // protect camera across tasks
- setupWebServer();
- server.begin();
- Serial.println("\nReady UI : http://" + WiFi.localIP().toString());
- Serial.println(" MJPEG stream : http://" + WiFi.localIP().toString() + "/stream");
- }
- /* ──────────────────────────────── loop() ───────────────────────────────── */
- void loop() {
- if (takeNewPhoto) { captureManual(); takeNewPhoto = false; }
- delay(1); // watchdog-friendly idle
- }
- /* ──────────────────────── Web-server routes ────────────────────────────── */
- void setupWebServer() {
- server.on("/", HTTP_GET, [](AsyncWebServerRequest *r){ r->send(200, "text/html", index_html); });
- /* MJPEG stream */
- server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *r) {
- AsyncWebServerResponse *resp = r->beginResponse(
- "multipart/x-mixed-replace; boundary=frame",
- [](uint8_t *b, size_t m, size_t i){ return streamHandler(b, m, i); });
- resp->addHeader("Cache-Control", "no-store");
- r->send(resp);
- });
- /* Manual capture */
- server.on("/capture", HTTP_GET, [](AsyncWebServerRequest *r){
- takeNewPhoto = true; r->send(200, "text/plain", "OK");
- });
- /* Toggle auto-detection */
- server.on("/toggle-detection", HTTP_GET, [](AsyncWebServerRequest *r){
- autoDetectionEnabled = !autoDetectionEnabled;
- r->send(200, "text/plain", autoDetectionEnabled ? "Enabled" : "Disabled");
- });
- /* Gallery JSON list */
- server.on("/gallery-data", HTTP_GET, [](AsyncWebServerRequest *r){
- String j = "[";
- File root = SPIFFS.open("/"); File f = root.openNextFile(); bool first = true;
- while (f) {
- String n = f.name();
- if (n.endsWith(".jpg")) { if (!first) j += ','; first = false; j += '\"' + n.substring(1) + '\"'; }
- f = root.openNextFile();
- }
- j += "]";
- r->send(200, "application/json", j);
- });
- /* Serve individual file (/photo/<filename>) */
- server.on("^\\/photo\\/([\\w\\-\\.]+)$", HTTP_GET,
- [](AsyncWebServerRequest *r){
- String path = "/" + r->pathArg(0);
- SPIFFS.exists(path) ? r->send(SPIFFS, path, "image/jpeg")
- : r->send(404, "text/plain", "Not found");
- });
- /* Clear all */
- server.on("/clear-photos", HTTP_GET, [](AsyncWebServerRequest *r){
- File root = SPIFFS.open("/"); File f = root.openNextFile();
- while (f) { String n = f.name(); if (n.endsWith(".jpg")) SPIFFS.remove(n); f = root.openNextFile(); }
- photoCounter = 0;
- r->send(200, "text/plain", "Cleared");
- });
- }
- /* ───────────────────── MJPEG stream handler ────────────────────────────── */
- size_t streamHandler(uint8_t *buf, size_t maxLen, size_t index) {
- static camera_fb_t *fb = nullptr;
- static String header;
- static size_t jpgLen = 0;
- if (index == 0) { // first chunk of a new frame
- if (xSemaphoreTake(camMux, portMAX_DELAY) != pdTRUE) return 0;
- fb = esp_camera_fb_get();
- int retries = 3;
- while (!fb && retries-- && kickCamera()) fb = esp_camera_fb_get();
- if (!fb) { xSemaphoreGive(camMux); return 0; }
- if (autoDetectionEnabled && millis() - lastDetection > DETECT_COOLDOWN_MS &&
- detectTreasure(fb)) {
- savePhoto(fb, "treasure");
- lastDetection = millis();
- }
- jpgLen = fb->len;
- header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + String(jpgLen) + "\r\n\r\n";
- }
- /* send header or image slices */
- size_t headerLen = header.length();
- if (index < headerLen) {
- size_t l = min(maxLen, headerLen - index);
- memcpy(buf, header.c_str() + index, l);
- return l;
- }
- size_t imgIndex = index - headerLen;
- if (imgIndex < jpgLen) {
- size_t l = min(maxLen, jpgLen - imgIndex);
- memcpy(buf, fb->buf + imgIndex, l);
- return l;
- }
- /* end of frame */
- esp_camera_fb_return(fb);
- fb = nullptr;
- xSemaphoreGive(camMux);
- return 0;
- }
- /* ─────────── enforce storage cap – delete oldest until < MAX_IMAGES ────── */
- void enforceMaxImages() {
- /* count current jpgs */
- int count = 0;
- File root = SPIFFS.open("/"); File f = root.openNextFile();
- while (f) { if (String(f.name()).endsWith(".jpg")) count++; f = root.openNextFile(); }
- /* evict oldest while over cap */
- while (count >= MAX_IMAGES) {
- time_t oldest = 0x7FFFFFFF; String victim = "";
- root = SPIFFS.open("/"); f = root.openNextFile();
- while (f) {
- if (String(f.name()).endsWith(".jpg") && f.getLastWrite() < oldest) {
- oldest = f.getLastWrite(); victim = f.name();
- }
- f = root.openNextFile();
- }
- if (victim == "") break; // should not happen
- SPIFFS.remove(victim);
- count--;
- }
- }
- /* ─────────── save photo with unique name & storage cap enforcement ─────── */
- void savePhoto(camera_fb_t *fb, const String &prefix) {
- enforceMaxImages();
- String name = "/" + prefix + "_" + String(photoCounter++) + ".jpg";
- if (photoCounter > 9999) photoCounter = 0;
- File f = SPIFFS.open(name, FILE_WRITE);
- if (!f) { Serial.println("SPIFFS write error"); return; }
- f.write(fb->buf, fb->len);
- f.close();
- Serial.println("Saved " + name + " (" + String(fb->len) + " B)");
- }
- /* ─────────── manual capture (called from loop) ─────────────────────────── */
- void captureManual() {
- if (xSemaphoreTake(camMux, portMAX_DELAY) != pdTRUE) return;
- camera_fb_t *fb = esp_camera_fb_get();
- int retries = 3;
- while (!fb && retries-- && kickCamera()) fb = esp_camera_fb_get();
- if (!fb) { xSemaphoreGive(camMux); Serial.println("Manual capture fail"); return; }
- savePhoto(fb, "manual");
- esp_camera_fb_return(fb);
- xSemaphoreGive(camMux);
- }
- /* ─────────── quick yellow-pixel heuristic on JPEG payload ──────────────── */
- bool detectTreasure(camera_fb_t *fb) {
- uint8_t *b = fb->buf; size_t len = fb->len;
- int yellow = 0;
- for (size_t i = 0; i < len - 2; i += SAMPLE_SKIP)
- if (b[i] > 200 && b[i + 1] < 150 && b[i + 2] > 150) yellow++;
- return (yellow >= YELLOW_MIN && yellow <= YELLOW_MAX);
- }
- /* ─────────── camera re-initialise on failure ───────────────────────────── */
- bool kickCamera() {
- esp_camera_deinit();
- Serial.println("Re-init camera");
- return esp_camera_init(&camCfg) == ESP_OK;
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement