Advertisement
Guest User

Untitled

a guest
May 22nd, 2025
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.13 KB | None | 0 0
  1. /****************************************************************************************
  2. * ESP32-CAM “Maze Navigator” – resilient MJPEG stream + treasure detection + gallery
  3. * Board : AI-Thinker ESP32-CAM (OV2640 @ 160 MHz, PSRAM)
  4. * Features
  5. * • Live MJPEG stream (/stream)
  6. * • Yellow “treasure” auto-detect → photo saved (max 10 images, oldest evicted)
  7. * • Manual capture, gallery thumbnails, clear-all
  8. * • Mutex-protected camera access, automatic camera re-init on failure
  9. * • SPIFFS wear-aware size cap (MAX_IMAGES = 10)
  10. * • Single-page responsive UI
  11. ****************************************************************************************/
  12.  
  13. #include <WiFi.h>
  14. #include <esp_camera.h>
  15. #include <esp_timer.h>
  16. #include <Arduino.h>
  17. #include "soc/soc.h"
  18. #include "soc/rtc_cntl_reg.h" // Brown-out detector disable
  19. #include "driver/rtc_io.h"
  20. #include <ESPAsyncWebServer.h>
  21. #include <FS.h>
  22. #include <SPIFFS.h>
  23. #include "freertos/semphr.h"
  24.  
  25. /* ────────────────────────────── Wi-Fi creds ─────────────────────────────── */
  26. const char* ssid = "***";
  27. const char* password = "***";
  28.  
  29. /* ───────────────────────────── Global objects ───────────────────────────── */
  30. AsyncWebServer server(80);
  31. SemaphoreHandle_t camMux;
  32.  
  33. /* Runtime state */
  34. bool takeNewPhoto = false;
  35. bool autoDetectionEnabled = true;
  36. uint16_t photoCounter = 0;
  37. unsigned long lastDetection = 0;
  38. const unsigned long DETECT_COOLDOWN_MS = 3000; // 3-s debounce
  39. const int MAX_IMAGES = 10; // storage cap
  40.  
  41. /* Simple yellow-pixel heuristic */
  42. const int YELLOW_MIN = 20;
  43. const int YELLOW_MAX = 500;
  44. const int SAMPLE_SKIP = 4;
  45.  
  46. /* ───────────────────────── camera pin-map (AI-Thinker) ──────────────────── */
  47. #define PWDN_GPIO_NUM 32
  48. #define RESET_GPIO_NUM -1
  49. #define XCLK_GPIO_NUM 0
  50. #define SIOD_GPIO_NUM 26
  51. #define SIOC_GPIO_NUM 27
  52. #define Y9_GPIO_NUM 35
  53. #define Y8_GPIO_NUM 34
  54. #define Y7_GPIO_NUM 39
  55. #define Y6_GPIO_NUM 36
  56. #define Y5_GPIO_NUM 21
  57. #define Y4_GPIO_NUM 19
  58. #define Y3_GPIO_NUM 18
  59. #define Y2_GPIO_NUM 5
  60. #define VSYNC_GPIO_NUM 25
  61. #define HREF_GPIO_NUM 23
  62. #define PCLK_GPIO_NUM 22
  63.  
  64. /* ───────────────────────────── HTML UI page ─────────────────────────────── */
  65. const char index_html[] PROGMEM = R"rawliteral(
  66. <!DOCTYPE html><html><head>
  67. <meta name=viewport content="width=device-width,initial-scale=1">
  68. <style>
  69. body{font-family:Arial;text-align:center;margin:0;padding:20px}
  70. button{padding:10px 20px;margin:5px;font-size:16px;cursor:pointer}
  71. .thumb{width:150px;margin:5px;border:2px solid #ccc;cursor:pointer}
  72. .thumb:hover{border-color:#007bff}
  73. </style></head><body>
  74. <h2>ESP32-CAM Maze Navigator</h2>
  75. Auto Detection: <span id=dstat>Enabled</span><br>
  76. Photos Stored : <span id=pcount>0</span><br>
  77. <button onclick="tog()">Toggle Detection</button>
  78. <button onclick="cap()">Manual Capture</button>
  79. <button onclick="clr()">Clear Photos</button>
  80. <button onclick="gal()">Refresh Gallery</button><br><br>
  81. <img id=stream src="/stream" style="width:90%;max-width:600px" loading=lazy><br>
  82. <div id=galbox></div>
  83. <script>
  84. function tog(){fetch('/toggle-detection').then(r=>r.text()).then(t=>dstat.textContent=t)}
  85. function cap(){fetch('/capture').then(()=>setTimeout(gal,1500))}
  86. function clr(){if(confirm('Delete all photos?'))fetch('/clear-photos').then(()=>gal())}
  87. function gal(){fetch('/gallery-data').then(r=>r.json()).then(js=>{
  88. galbox.innerHTML='';pcount.textContent=js.length;
  89. js.forEach(f=>{let i=document.createElement('img');i.src='/photo/'+f;i.className='thumb';
  90. i.onclick=()=>open('/photo/'+f,'_blank');galbox.appendChild(i);});
  91. });}
  92. setInterval(gal,4000);gal();
  93. stream.onerror=()=>stream.src='/stream?'+Date.now();
  94. </script></body></html>)rawliteral";
  95.  
  96. /* ─────────── forward declarations so everything compiles cleanly ────────── */
  97. void setupWebServer();
  98. size_t streamHandler(uint8_t*, size_t, size_t);
  99. bool detectTreasure(camera_fb_t*);
  100. void savePhoto(camera_fb_t*, const String&);
  101. void captureManual();
  102. void enforceMaxImages();
  103. bool kickCamera();
  104.  
  105. /* camera config kept global so kickCamera() can reuse it */
  106. camera_config_t camCfg{};
  107.  
  108. /* ──────────────────────────────── setup() ───────────────────────────────── */
  109. void setup() {
  110. Serial.begin(115200);
  111.  
  112. /* Wi-Fi connect */
  113. WiFi.begin(ssid, password);
  114. while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print('.'); }
  115.  
  116. /* SPIFFS */
  117. if (!SPIFFS.begin(true)) { Serial.println("SPIFFS mount failed"); ESP.restart(); }
  118.  
  119. /* Disable brown-out */
  120. WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
  121.  
  122. /* Configure camera */
  123. camCfg.ledc_channel = LEDC_CHANNEL_0;
  124. camCfg.ledc_timer = LEDC_TIMER_0;
  125. camCfg.pin_d0 = Y2_GPIO_NUM; camCfg.pin_d1 = Y3_GPIO_NUM; camCfg.pin_d2 = Y4_GPIO_NUM; camCfg.pin_d3 = Y5_GPIO_NUM;
  126. camCfg.pin_d4 = Y6_GPIO_NUM; camCfg.pin_d5 = Y7_GPIO_NUM; camCfg.pin_d6 = Y8_GPIO_NUM; camCfg.pin_d7 = Y9_GPIO_NUM;
  127. camCfg.pin_xclk = XCLK_GPIO_NUM; camCfg.pin_pclk = PCLK_GPIO_NUM; camCfg.pin_vsync = VSYNC_GPIO_NUM;
  128. camCfg.pin_href = HREF_GPIO_NUM; camCfg.pin_sccb_sda = SIOD_GPIO_NUM; camCfg.pin_sccb_scl = SIOC_GPIO_NUM;
  129. camCfg.pin_pwdn = PWDN_GPIO_NUM; camCfg.pin_reset = RESET_GPIO_NUM;
  130. camCfg.xclk_freq_hz = 20000000;
  131. camCfg.pixel_format = PIXFORMAT_JPEG;
  132.  
  133. if (psramFound()) { camCfg.frame_size = FRAMESIZE_HVGA; camCfg.jpeg_quality = 12; camCfg.fb_count = 2; }
  134. else { camCfg.frame_size = FRAMESIZE_CIF; camCfg.jpeg_quality = 15; camCfg.fb_count = 1; }
  135.  
  136. if (esp_camera_init(&camCfg) != ESP_OK) { Serial.println("Camera init failed"); ESP.restart(); }
  137.  
  138. camMux = xSemaphoreCreateMutex(); // protect camera across tasks
  139. setupWebServer();
  140. server.begin();
  141.  
  142. Serial.println("\nReady UI : http://" + WiFi.localIP().toString());
  143. Serial.println(" MJPEG stream : http://" + WiFi.localIP().toString() + "/stream");
  144. }
  145.  
  146. /* ──────────────────────────────── loop() ───────────────────────────────── */
  147. void loop() {
  148. if (takeNewPhoto) { captureManual(); takeNewPhoto = false; }
  149. delay(1); // watchdog-friendly idle
  150. }
  151.  
  152. /* ──────────────────────── Web-server routes ────────────────────────────── */
  153. void setupWebServer() {
  154.  
  155. server.on("/", HTTP_GET, [](AsyncWebServerRequest *r){ r->send(200, "text/html", index_html); });
  156.  
  157. /* MJPEG stream */
  158. server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *r) {
  159. AsyncWebServerResponse *resp = r->beginResponse(
  160. "multipart/x-mixed-replace; boundary=frame",
  161. [](uint8_t *b, size_t m, size_t i){ return streamHandler(b, m, i); });
  162. resp->addHeader("Cache-Control", "no-store");
  163. r->send(resp);
  164. });
  165.  
  166. /* Manual capture */
  167. server.on("/capture", HTTP_GET, [](AsyncWebServerRequest *r){
  168. takeNewPhoto = true; r->send(200, "text/plain", "OK");
  169. });
  170.  
  171. /* Toggle auto-detection */
  172. server.on("/toggle-detection", HTTP_GET, [](AsyncWebServerRequest *r){
  173. autoDetectionEnabled = !autoDetectionEnabled;
  174. r->send(200, "text/plain", autoDetectionEnabled ? "Enabled" : "Disabled");
  175. });
  176.  
  177. /* Gallery JSON list */
  178. server.on("/gallery-data", HTTP_GET, [](AsyncWebServerRequest *r){
  179. String j = "[";
  180. File root = SPIFFS.open("/"); File f = root.openNextFile(); bool first = true;
  181. while (f) {
  182. String n = f.name();
  183. if (n.endsWith(".jpg")) { if (!first) j += ','; first = false; j += '\"' + n.substring(1) + '\"'; }
  184. f = root.openNextFile();
  185. }
  186. j += "]";
  187. r->send(200, "application/json", j);
  188. });
  189.  
  190. /* Serve individual file (/photo/<filename>) */
  191. server.on("^\\/photo\\/([\\w\\-\\.]+)$", HTTP_GET,
  192. [](AsyncWebServerRequest *r){
  193. String path = "/" + r->pathArg(0);
  194. SPIFFS.exists(path) ? r->send(SPIFFS, path, "image/jpeg")
  195. : r->send(404, "text/plain", "Not found");
  196. });
  197.  
  198. /* Clear all */
  199. server.on("/clear-photos", HTTP_GET, [](AsyncWebServerRequest *r){
  200. File root = SPIFFS.open("/"); File f = root.openNextFile();
  201. while (f) { String n = f.name(); if (n.endsWith(".jpg")) SPIFFS.remove(n); f = root.openNextFile(); }
  202. photoCounter = 0;
  203. r->send(200, "text/plain", "Cleared");
  204. });
  205. }
  206.  
  207. /* ───────────────────── MJPEG stream handler ────────────────────────────── */
  208. size_t streamHandler(uint8_t *buf, size_t maxLen, size_t index) {
  209. static camera_fb_t *fb = nullptr;
  210. static String header;
  211. static size_t jpgLen = 0;
  212.  
  213. if (index == 0) { // first chunk of a new frame
  214. if (xSemaphoreTake(camMux, portMAX_DELAY) != pdTRUE) return 0;
  215.  
  216. fb = esp_camera_fb_get();
  217. int retries = 3;
  218. while (!fb && retries-- && kickCamera()) fb = esp_camera_fb_get();
  219. if (!fb) { xSemaphoreGive(camMux); return 0; }
  220.  
  221. if (autoDetectionEnabled && millis() - lastDetection > DETECT_COOLDOWN_MS &&
  222. detectTreasure(fb)) {
  223. savePhoto(fb, "treasure");
  224. lastDetection = millis();
  225. }
  226.  
  227. jpgLen = fb->len;
  228. header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + String(jpgLen) + "\r\n\r\n";
  229. }
  230.  
  231. /* send header or image slices */
  232. size_t headerLen = header.length();
  233. if (index < headerLen) {
  234. size_t l = min(maxLen, headerLen - index);
  235. memcpy(buf, header.c_str() + index, l);
  236. return l;
  237. }
  238. size_t imgIndex = index - headerLen;
  239. if (imgIndex < jpgLen) {
  240. size_t l = min(maxLen, jpgLen - imgIndex);
  241. memcpy(buf, fb->buf + imgIndex, l);
  242. return l;
  243. }
  244.  
  245. /* end of frame */
  246. esp_camera_fb_return(fb);
  247. fb = nullptr;
  248. xSemaphoreGive(camMux);
  249. return 0;
  250. }
  251.  
  252. /* ─────────── enforce storage cap – delete oldest until < MAX_IMAGES ────── */
  253. void enforceMaxImages() {
  254. /* count current jpgs */
  255. int count = 0;
  256. File root = SPIFFS.open("/"); File f = root.openNextFile();
  257. while (f) { if (String(f.name()).endsWith(".jpg")) count++; f = root.openNextFile(); }
  258.  
  259. /* evict oldest while over cap */
  260. while (count >= MAX_IMAGES) {
  261. time_t oldest = 0x7FFFFFFF; String victim = "";
  262. root = SPIFFS.open("/"); f = root.openNextFile();
  263. while (f) {
  264. if (String(f.name()).endsWith(".jpg") && f.getLastWrite() < oldest) {
  265. oldest = f.getLastWrite(); victim = f.name();
  266. }
  267. f = root.openNextFile();
  268. }
  269. if (victim == "") break; // should not happen
  270. SPIFFS.remove(victim);
  271. count--;
  272. }
  273. }
  274.  
  275. /* ─────────── save photo with unique name & storage cap enforcement ─────── */
  276. void savePhoto(camera_fb_t *fb, const String &prefix) {
  277. enforceMaxImages();
  278. String name = "/" + prefix + "_" + String(photoCounter++) + ".jpg";
  279. if (photoCounter > 9999) photoCounter = 0;
  280.  
  281. File f = SPIFFS.open(name, FILE_WRITE);
  282. if (!f) { Serial.println("SPIFFS write error"); return; }
  283. f.write(fb->buf, fb->len);
  284. f.close();
  285. Serial.println("Saved " + name + " (" + String(fb->len) + " B)");
  286. }
  287.  
  288. /* ─────────── manual capture (called from loop) ─────────────────────────── */
  289. void captureManual() {
  290. if (xSemaphoreTake(camMux, portMAX_DELAY) != pdTRUE) return;
  291.  
  292. camera_fb_t *fb = esp_camera_fb_get();
  293. int retries = 3;
  294. while (!fb && retries-- && kickCamera()) fb = esp_camera_fb_get();
  295.  
  296. if (!fb) { xSemaphoreGive(camMux); Serial.println("Manual capture fail"); return; }
  297.  
  298. savePhoto(fb, "manual");
  299. esp_camera_fb_return(fb);
  300. xSemaphoreGive(camMux);
  301. }
  302.  
  303. /* ─────────── quick yellow-pixel heuristic on JPEG payload ──────────────── */
  304. bool detectTreasure(camera_fb_t *fb) {
  305. uint8_t *b = fb->buf; size_t len = fb->len;
  306. int yellow = 0;
  307. for (size_t i = 0; i < len - 2; i += SAMPLE_SKIP)
  308. if (b[i] > 200 && b[i + 1] < 150 && b[i + 2] > 150) yellow++;
  309. return (yellow >= YELLOW_MIN && yellow <= YELLOW_MAX);
  310. }
  311.  
  312. /* ─────────── camera re-initialise on failure ───────────────────────────── */
  313. bool kickCamera() {
  314. esp_camera_deinit();
  315. Serial.println("Re-init camera");
  316. return esp_camera_init(&camCfg) == ESP_OK;
  317. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement