// UDP library which is how we communicate with Time Server
122 | const uint16_t localPort = 8888; // Just an open port we can use for the UDP packets coming back in
123 | const char timeServer[] = "uk.pool.ntp.org";
124 | const uint16_t NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
125 | byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets
126 | WiFiUDP NTPUdp; // A UDP instance to let us send and receive packets over UDP
127 | const uint16_t timeZone = 0; // timezone (0=GMT)
128 | const String DoW[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
129 | // How often to resync the time (under normal and error conditions)
130 | const uint16_t _resyncSeconds = 7200; // 7200 = 2 hours
131 | const uint16_t _resyncErrorSeconds = 60; // 60 = 1 min
132 | bool NTPok = 0; // Flag if NTP is curently connecting ok
133 |
134 |
135 | // ******************************************************************************************************************
136 |
137 |
138 | // ---------------------------------------------------------------
139 | // -SETUP SETUP SETUP SETUP SETUP SETUP
140 | // ---------------------------------------------------------------
141 |
142 | void setup() {
143 |
144 | Serial.begin(serialSpeed); // Start serial communication
145 |
146 | Serial.println("\n\n\n"); // line feeds
147 | Serial.println("-----------------------------------");
148 | Serial.printf("Starting - %s - %s \n", stitle, sversion);
149 | Serial.println("-----------------------------------");
150 |
151 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // Turn-off the 'brownout detector'
152 |
153 | // Define small indicator led
154 | pinMode(indicatorLED, OUTPUT);
155 | digitalWrite(indicatorLED,HIGH);
156 |
157 | // Connect to wifi
158 | digitalWrite(indicatorLED,LOW); // small indicator led on
159 | Serial.print("\nConnecting to ");
160 | Serial.print(ssid);
161 | Serial.print("\n ");
162 | WiFi.begin(ssid, password);
163 | while (WiFi.status() != WL_CONNECTED) {
164 | delay(500);
165 | Serial.print(".");
166 | }
167 | Serial.print("\nWiFi connected, ");
168 | Serial.print("IP address: ");
169 | Serial.println(WiFi.localIP());
170 | server.begin();
171 | digitalWrite(indicatorLED,HIGH); // small indicator led off
172 |
173 | // define the web pages (i.e. call these procedures when url is requested)
174 | server.on("/", handleRoot); // root page
175 | server.on("/stream", handleStream); // stream live video
176 | server.on("/photo", handlePhoto); // save image to sd card
177 | server.on("/img", handleImg); // show image from sd card
178 | server.onNotFound(handleNotFound); // invalid url requested
179 |
180 | // set up camera
181 | Serial.print(("\nInitialising camera: "));
182 | if (setupCameraHardware()) Serial.println("OK");
183 | else {
184 | Serial.println("Error!");
185 | showError(2); // critical error so stop and flash led
186 | }
187 |
188 | // SD Card - if one is detected set 'sdcardPresent' High
189 | if (!SD_MMC.begin("/sdcard", true)) { // if loading sd card fails
190 | // note: ('/sdcard", true)' = 1bit mode - see: https://www.reddit.com/r/esp32/comments/d71es9/a_breakdown_of_my_experience_trying_to_talk_to_an/
191 | Serial.println("No SD Card detected");
192 | sdcardPresent = 0; // flag no sd card available
193 | } else {
194 | uint8_t cardType = SD_MMC.cardType();
195 | if (cardType == CARD_NONE) { // if invalid card found
196 | Serial.println("SD Card type detect failed");
197 | sdcardPresent = 0; // flag no sd card available
198 | } else {
199 | // valid sd card detected
200 | uint16_t SDfreeSpace = (uint64_t)(SD_MMC.totalBytes() - SD_MMC.usedBytes()) / (1024 * 1024);
201 | Serial.printf("SD Card found, free space = %dMB \n", SDfreeSpace);
202 | sdcardPresent = 1; // flag sd card available
203 | }
204 | }
205 | fs::FS &fs = SD_MMC; // sd card file system
206 |
207 | // discover the number of image files stored in '/img' folder of the sd card and set image file counter accordingly
208 | imageCounter = 0;
209 | if (sdcardPresent) {
210 | int tq=fs.mkdir("/img"); // create the '/img' folder on sd card (in case it is not already there)
211 | if (!tq) Serial.println("Unable to create IMG folder on sd card");
212 |
213 | // open the image folder and step through all files in it
214 | File root = fs.open("/img");
215 | while (true)
216 | {
217 | File entry = root.openNextFile(); // open next file in the folder
218 | if (!entry) break; // if no more files in the folder
219 | imageCounter ++; // increment image counter
220 | entry.close();
221 | }
222 | root.close();
223 | Serial.printf("Image file count = %d",imageCounter);
224 | }
225 |
226 | // define io pins
227 | pinMode(indicatorLED, OUTPUT); // defined again as sd card config can reset it
228 | digitalWrite(indicatorLED,HIGH); // led off = High
229 | pinMode(brightLED, OUTPUT); // flash LED
230 | digitalWrite(brightLED,LOW); // led off = Low
231 | pinMode(iopinA, OUTPUT); // pin 13 - free io pin, can be input or output
232 | pinMode(iopinB, OUTPUT); // pin 12 - free io pin, can be input or output (must be low at boot)
233 |
234 | if (!psramFound()) {
235 | Serial.println("Warning: No PSRam found so defaulting to image size 'CIF'");
236 | framesize_t FRAME_SIZE_IMAGE = FRAMESIZE_CIF;
237 | }
238 |
239 | // start NTP
240 | NTPUdp.begin(localPort); // What port will the UDP/NTP packet respond on?
241 | setSyncProvider(getNTPTime); // What is the function that gets the time (in ms since 01/01/1900)?
242 | setSyncInterval(_resyncErrorSeconds); // How often should we synchronise the time on this machine (in seconds)
243 |
244 | Serial.println("\n\nStarted...");
245 |
246 | cameraImageSettings(); // Apply camera image settings
247 |
248 | } // setup
249 |
250 |
251 |
252 | // ******************************************************************************************************************
253 |
254 |
255 | // ----------------------------------------------------------------
256 | // -LOOP LOOP LOOP LOOP LOOP LOOP LOOP
257 | // ----------------------------------------------------------------
258 |
259 |
260 | void loop() {
261 |
262 | server.handleClient(); // handle any incoming web page requests
263 |
264 |
265 |
266 |
267 |
268 |
269 | // <<< your code here >>>
270 |
271 |
272 |
273 |
274 |
275 | // // Capture an image and save to sd card every 5 seconds
276 | // if ((unsigned long)(millis() - lastCamera) >= 5000) {
277 | // lastCamera = millis(); // reset timer
278 | // storeImage(); // save an image to sd card
279 | // }
280 |
281 |
282 | // flash status LED to show sketch is running
283 | if ((unsigned long)(millis() - lastStatus) >= TimeBetweenStatus) {
284 | lastStatus = millis(); // reset timer
285 | digitalWrite(indicatorLED,!digitalRead(indicatorLED)); // flip indicator led status
286 | time_t t=now(); // read current time to ensure NTP auto refresh keeps triggering (otherwise only triggers when time is required causing a delay in response)
287 | }
288 |
289 | } // loop
290 |
291 |
292 |
293 | // ******************************************************************************************************************
294 |
295 |
296 | // ----------------------------------------------------------------
297 | // Configure the camera
298 | // ----------------------------------------------------------------
299 | // returns TRUE if sucessful
300 |
301 | bool setupCameraHardware() {
302 |
303 | config.ledc_channel = LEDC_CHANNEL_0;
304 | config.ledc_timer = LEDC_TIMER_0;
305 | config.pin_d0 = Y2_GPIO_NUM;
306 | config.pin_d1 = Y3_GPIO_NUM;
307 | config.pin_d2 = Y4_GPIO_NUM;
308 | config.pin_d3 = Y5_GPIO_NUM;
309 | config.pin_d4 = Y6_GPIO_NUM;
310 | config.pin_d5 = Y7_GPIO_NUM;
311 | config.pin_d6 = Y8_GPIO_NUM;
312 | config.pin_d7 = Y9_GPIO_NUM;
313 | config.pin_xclk = XCLK_GPIO_NUM;
314 | config.pin_pclk = PCLK_GPIO_NUM;
315 | config.pin_vsync = VSYNC_GPIO_NUM;
316 | config.pin_href = HREF_GPIO_NUM;
317 | config.pin_sscb_sda = SIOD_GPIO_NUM;
318 | config.pin_sscb_scl = SIOC_GPIO_NUM;
319 | config.pin_pwdn = PWDN_GPIO_NUM;
320 | config.pin_reset = RESET_GPIO_NUM;
321 | config.xclk_freq_hz = 20000000; // XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
322 | config.pixel_format = PIXFORMAT_JPEG; // Options = YUV422, GRAYSCALE, RGB565, JPEG, RGB888
323 | config.frame_size = FRAME_SIZE_IMAGE; // Image sizes: 160x120 (QQVGA), 128x160 (QQVGA2), 176x144 (QCIF), 240x176 (HQVGA), 320x240 (QVGA),
324 | // 400x296 (CIF), 640x480 (VGA, default), 800x600 (SVGA), 1024x768 (XGA), 1280x1024 (SXGA), 1600x1200 (UXGA)
325 | config.jpeg_quality = 5; // 0-63 lower number means higher quality
326 | config.fb_count = 1; // if more than one, i2s runs in continuous mode. Use only with JPEG
327 |
328 | esp_err_t camerr = esp_camera_init(&config); // initialise the camera
329 | if (camerr != ESP_OK) Serial.printf("ERROR: Camera init failed with error 0x%x", camerr);
330 |
331 |
332 | return (camerr == ESP_OK); // return boolean result of camera initialisation
333 | }
334 |
335 |
336 |
337 | // ******************************************************************************************************************
338 |
339 |
340 | // ----------------------------------------------------------------
341 | // Misc small procedures
342 | // ----------------------------------------------------------------
343 |
344 |
345 |
346 | // flash the indicator led 'reps' number of times
347 | void flashLED(int reps) {
348 | for(int x=0; x < reps; x++) {
349 | digitalWrite(indicatorLED,LOW);
350 | delay(1000);
351 | digitalWrite(indicatorLED,HIGH);
352 | delay(500);
353 | }
354 | }
355 |
356 |
357 |
358 | // critical error - stop sketch and continually flash error code on indicator led
359 | void showError(int errorNo) {
360 | while(1) {
361 | flashLED(errorNo);
362 | delay(4000);
363 | }
364 | }
365 |
366 |
367 |
368 | // ******************************************************************************************************************
369 |
370 |
371 | // ----------------------------------------------------------------
372 | // Capture image from camera and save to sd card
373 | // ----------------------------------------------------------------
374 | // returns TRUE if sucessful
375 |
376 | bool storeImage() {
377 |
378 | if (sdcardPresent) {
379 | if (debugInfo) Serial.printf("Storing image #%d to sd card \n", imageCounter);
380 | } else {
381 | if (debugInfo) Serial.println("Storing image requested but there is no sd card");
382 | return 0; // no sd card available so exit procedure
383 | }
384 |
385 | fs::FS &fs = SD_MMC; // sd card file system
386 | bool tResult = 0; // result flag
387 |
388 | // capture live image from camera
389 | if (flashRequired) digitalWrite(brightLED,HIGH); // turn flash on
390 | camera_fb_t *fb = esp_camera_fb_get(); // capture image frame from camera
391 | digitalWrite(brightLED,LOW); // turn flash off
392 | if (!fb) {
393 | Serial.println("Error: Camera capture failed");
394 | flashLED(3);
395 | }
396 |
397 | // save the image to sd card
398 | String SDfilename = "/img/" + String(imageCounter + 1) + ".jpg"; // build the image file name
399 | File file = fs.open(SDfilename, FILE_WRITE); // create file on sd card
400 | if (!file) {
401 | Serial.println("Error: Failed to create file on sd-card: " + SDfilename);
402 | flashLED(4);
403 | } else {
404 | if (file.write(fb->buf, fb->len)) { // File created ok so save image to it
405 | if (debugInfo) Serial.println("Image saved to sd card");
406 | tResult = 1; // set sucess flag
407 | imageCounter ++; // increment image counter
408 | } else {
409 | Serial.println("Error: failed to save image to sd card");
410 | flashLED(4);
411 | }
412 | file.close(); // close image file on sd card
413 | }
414 | esp_camera_fb_return(fb); // return frame so memory can be released
415 |
416 | return tResult; // return image save sucess flag
417 |
418 | } // storeImage
419 |
420 |
421 |
422 | // ******************************************************************************************************************
423 |
424 |
425 | // ----------------------------------------------------------------
426 | // -root web page requested i.e. http://x.x.x.x/
427 | // ----------------------------------------------------------------
428 |
429 | void handleRoot() {
430 |
431 | WiFiClient client = server.client(); // open link with client
432 |
433 | // log the page request including clients IP address
434 | if (debugInfo) {
435 | IPAddress cip = client.remoteIP();
436 | Serial.printf("Root page requested from: %d.%d.%d.%d \n", cip[0], cip[1], cip[2], cip[3]);
437 | }
438 |
439 |
440 | // Action any button presses or settings entered on web page
441 |
442 | // if button1 was pressed (toggle io pin A)
443 | // Note: if using an input box etc. you would read the value with the command: String Bvalue = server.arg("demobutton1");
444 | if (server.hasArg("button1")) {
445 | digitalWrite(iopinA,!digitalRead(iopinA)); // toggle output pin on/off
446 | if (debugInfo) Serial.println("Button 1 pressed");
447 | }
448 |
449 | // if button2 was pressed (toggle io pin B)
450 | if (server.hasArg("button2")) {
451 | digitalWrite(iopinB,!digitalRead(iopinB)); // toggle output pin on/off
452 | if (debugInfo) Serial.println("Button 2 pressed");
453 | }
454 |
455 | // if button3 was pressed (toggle flash LED)
456 | if (server.hasArg("button3")) {
457 | digitalWrite(brightLED,!digitalRead(brightLED)); // toggle flash LED on/off
458 | if (debugInfo) Serial.println("Button 3 pressed");
459 | }
460 |
461 | // if exposure was adjusted - cameraImageExposure
462 | if (server.hasArg("exp")) {
463 | String Tvalue = server.arg("exp"); // read value
464 | if (Tvalue != NULL) {
465 | int val = Tvalue.toInt();
466 | if (val >= 0 && val <= 1200 && val != cameraImageExposure) {
467 | if (debugInfo) Serial.printf("Exposure changed to %d\n", val);
468 | cameraImageExposure = val;
469 | cameraImageSettings(); // Apply camera image settings
470 | }
471 | }
472 | }
473 |
474 | // if image gain was adjusted - cameraImageGain
475 | if (server.hasArg("gain")) {
476 | String Tvalue = server.arg("gain"); // read value
477 | if (Tvalue != NULL) {
478 | int val = Tvalue.toInt();
479 | if (val >= 0 && val <= 31 && val != cameraImageGain) {
480 | if (debugInfo) Serial.printf("Gain changed to %d\n", val);
481 | cameraImageGain = val;
482 | cameraImageSettings(); // Apply camera image settings
483 | }
484 | }
485 | }
486 |
487 |
488 | // html header
489 | client.write(" root \n"); // basic html header
490 | client.write("\n");
543 | delay(3);
544 | client.stop();
545 |
546 | } // handleRoot
547 |
548 |
549 | // ******************************************************************************************************************
550 |
551 |
552 | // ----------------------------------------------------------------
553 | // -photo save to sd card i.e. http://x.x.x.x/photo
554 | // ----------------------------------------------------------------
555 |
556 | void handlePhoto() {
557 |
558 | WiFiClient client = server.client(); // open link with client
559 |
560 | // log page request including clients IP address
561 | if (debugInfo) {
562 | IPAddress cip = client.remoteIP();
563 | Serial.printf("Photo requested from: %d.%d.%d.%d \n", cip[0], cip[1], cip[2], cip[3]);
564 | }
565 |
566 | // save an image to sd card
567 | bool sRes = storeImage(); // save an image to sd card (store sucess or failed flag)
568 |
569 | // html header
570 | client.write(" photo \n"); // basic html header
571 |
572 | // html body
573 | if (sRes == 1) {
574 | client.printf("Image saved to sd card as image number %d
\n", imageCounter);
575 | client.write("View Image\n"); // link to the image
576 | } else {
577 | client.write("Error: Failed to save image to sd card
\n");
578 | }
579 |
580 | // end html
581 | client.write("\n");
582 | delay(3);
583 | client.stop();
584 |
585 | } // handlePhoto
586 |
587 |
588 |
589 |
590 |
591 |
592 | // ----------------------------------------------------------------
593 | // -show image from sd card i.e. http://x.x.x.x/img?img=x
594 | // ----------------------------------------------------------------
595 | // default image = most recent
596 | // returns 1 if image displayed ok
597 |
598 | bool handleImg() {
599 |
600 | WiFiClient client = server.client(); // open link with client
601 |
602 | // log page request including clients IP address
603 | if (debugInfo) {
604 | IPAddress cip = client.remoteIP();
605 | Serial.printf("Image display requested from: %d.%d.%d.%d \n", cip[0], cip[1], cip[2], cip[3]);
606 | if (imageCounter == 0) Serial.println("Error: no images to display");
607 | }
608 |
609 | int imgToShow = imageCounter; // default to showing most recent file
610 |
611 | // get image number from url parameter
612 | if (server.hasArg("img")) {
613 | String Tvalue = server.arg("img"); // read value
614 | imgToShow = Tvalue.toInt(); // convert string to int
615 | if (imgToShow < 1 || imgToShow > imageCounter) imgToShow = imageCounter; // validate image number
616 | }
617 |
618 | if (debugInfo) Serial.printf("Displaying image #%d from sd card", imgToShow);
619 |
620 | String tFileName = "/img/" + String(imgToShow) + ".jpg";
621 | fs::FS &fs = SD_MMC; // sd card file system
622 | File timg = fs.open(tFileName, "r");
623 | if (timg) {
624 | size_t sent = server.streamFile(timg, "image/jpeg"); // send the image
625 | timg.close();
626 | } else {
627 | if (debugInfo) Serial.println("Error: image file not found");
628 | WiFiClient client = server.client(); // open link with client
629 | client.write(" \n");
630 | client.write("Error: Image not found
len; // store size of image (i.e. buffer length)
714 | client.write(CTNTTYPE, cntLen); // send content type html (i.e. jpg image)
715 | sprintf( buf, "%d\r\n\r\n", s ); // format the image's size as html and put in to 'buf'
716 | client.write(buf, strlen(buf)); // send result (image size)
717 | client.write((char *)fb->buf, s); // send the image data
718 | client.write(BOUNDARY, bdrLen); // send html boundary see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
719 | esp_camera_fb_return(fb); // return image so memory can be released
720 | }
721 |
722 | if (debugInfo) Serial.println("Video stream stopped");
723 | delay(3);
724 | client.stop();
725 |
726 |
727 | } // handleStream
728 |
729 |
730 | // ******************************************************************************************************************
731 |
732 | // ----------------------------------------------------------------
733 | // -Change camera settings
734 | // ----------------------------------------------------------------
735 | // Returns TRUE is successful
736 |
737 | bool cameraImageSettings() {
738 |
739 | sensor_t *s = esp_camera_sensor_get();
740 | if (s == NULL) {
741 | Serial.println("Error: problem getting camera sensor settings");
742 | return 0;
743 | }
744 |
745 | if (cameraImageExposure == 0 && cameraImageGain == 0) {
746 | // enable auto adjust
747 | s->set_gain_ctrl(s, 1); // auto gain on
748 | s->set_exposure_ctrl(s, 1); // auto exposure on
749 | } else {
750 | // Apply manual settings
751 | s->set_gain_ctrl(s, 0); // auto gain off
752 | s->set_exposure_ctrl(s, 0); // auto exposure off
753 | s->set_agc_gain(s, cameraImageGain); // set gain manually (0 - 30)
754 | s->set_aec_value(s, cameraImageExposure); // set exposure manually (0-1200)
755 | }
756 |
757 | return 1;
758 | } // cameraImageSettings
759 |
760 |
761 | // // More camera settings available:
762 | // // If you enable gain_ctrl or exposure_ctrl it will prevent a lot of the other settings having any effect
763 | // // more info on settings here: https://randomnerdtutorials.com/esp32-cam-ov2640-camera-settings/
764 | // s->set_gain_ctrl(s, 0); // auto gain off (1 or 0)
765 | // s->set_exposure_ctrl(s, 0); // auto exposure off (1 or 0)
766 | // s->set_agc_gain(s, cameraImageGain); // set gain manually (0 - 30)
767 | // s->set_aec_value(s, cameraImageExposure); // set exposure manually (0-1200)
768 | // s->set_vflip(s, cameraImageInvert); // Invert image (0 or 1)
769 | // s->set_quality(s, 10); // (0 - 63)
770 | // s->set_gainceiling(s, GAINCEILING_32X); // Image gain (GAINCEILING_x2, x4, x8, x16, x32, x64 or x128)
771 | // s->set_brightness(s, cameraImageBrightness); // (-2 to 2) - set brightness
772 | // s->set_lenc(s, 1); // lens correction? (1 or 0)
773 | // s->set_saturation(s, 0); // (-2 to 2)
774 | // s->set_contrast(s, cameraImageContrast); // (-2 to 2)
775 | // s->set_sharpness(s, 0); // (-2 to 2)
776 | // s->set_hmirror(s, 0); // (0 or 1) flip horizontally
777 | // s->set_colorbar(s, 0); // (0 or 1) - show a testcard
778 | // s->set_special_effect(s, 0); // (0 to 6?) apply special effect
779 | // s->set_whitebal(s, 0); // white balance enable (0 or 1)
780 | // s->set_awb_gain(s, 1); // Auto White Balance enable (0 or 1)
781 | // s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
782 | // s->set_dcw(s, 0); // downsize enable? (1 or 0)?
783 | // s->set_raw_gma(s, 1); // (1 or 0)
784 | // s->set_aec2(s, 0); // automatic exposure sensor? (0 or 1)
785 | // s->set_ae_level(s, 0); // auto exposure levels (-2 to 2)
786 | // s->set_bpc(s, 0); // black pixel correction
787 | // s->set_wpc(s, 0); // white pixel correction
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 | // --------------------------------------------------------------------------------------------------------------
796 | // NTP Real Time
797 | // --------------------------------------------------------------------------------------------------------------
798 |
799 |
800 | // ----------------------------------------------------------------
801 | // -Return current time and date as string
802 | // ----------------------------------------------------------------
803 | // supplies time in the format: '23-04-2020_09-23-10_Mon'
804 |
805 | String currentTime() {
806 |
807 | time_t t=now(); // get current time
808 | t+=timeZone; // adjust for timezone
809 |
810 | if (IsBST()) t+=3600; // add one hour if it is Summer Time
811 |
812 | String ttime = formatDateNumber(day(t));
813 | ttime += "-";
814 | ttime += formatDateNumber(month(t));
815 | ttime += "-";
816 | ttime += formatDateNumber(year(t));
817 | ttime += "_";
818 | ttime += formatDateNumber(hour(t));
819 | ttime += "-";
820 | ttime += formatDateNumber(minute(t));
821 | ttime += "-";
822 | ttime += formatDateNumber(second(t));
823 | ttime += "_";
824 | ttime += DoW[weekday(t)-1];
825 |
826 | return ttime;
827 | }
828 |
829 |
830 | // convert number to String and add leading zero if required
831 | String formatDateNumber(int input) {
832 | String tval = "";
833 | if (input < 10) tval = "0"; // add leading zero if required
834 | tval += String(input);
835 | return tval;
836 | }
837 |
838 |
839 |
840 | //-----------------------------------------------------------------------------
841 | // -British Summer Time check
842 | //-----------------------------------------------------------------------------
843 |
844 | // returns true if it is British Summer time
845 | // code from https://my-small-projects.blogspot.com/2015/05/arduino-checking-for-british-summer-time.html
846 |
847 | boolean IsBST()
848 | {
849 | int imonth = month();
850 | int iday = day();
851 | int hr = hour();
852 |
853 | //January, february, and november are out.
854 | if (imonth < 3 || imonth > 10) { return false; }
855 | //April to September are in
856 | if (imonth > 3 && imonth < 10) { return true; }
857 |
858 | // find last sun in mar and oct - quickest way I've found to do it
859 | // last sunday of march
860 | int lastMarSunday = (31 - (5* year() /4 + 4) % 7);
861 | //last sunday of october
862 | int lastOctSunday = (31 - (5 * year() /4 + 1) % 7);
863 |
864 | //In march, we are BST if is the last sunday in the month
865 | if (imonth == 3) {
866 |
867 | if( iday > lastMarSunday)
868 | return true;
869 | if( iday < lastMarSunday)
870 | return false;
871 |
872 | if (hr < 1)
873 | return false;
874 |
875 | return true;
876 |
877 | }
878 | //In October we must be before the last sunday to be bst.
879 | //That means the previous sunday must be before the 1st.
880 | if (imonth == 10) {
881 |
882 | if( iday < lastOctSunday)
883 | return true;
884 | if( iday > lastOctSunday)
885 | return false;
886 |
887 | if (hr >= 1)
888 | return false;
889 |
890 | return true;
891 | }
892 |
893 | }
894 |
895 |
896 |
897 | //-----------------------------------------------------------------------------
898 | // send an NTP request to the time server at the given address
899 | //-----------------------------------------------------------------------------
900 |
901 | void sendNTPpacket(const char* address) {
902 |
903 | // set all bytes in the buffer to 0
904 | memset(packetBuffer, 0, NTP_PACKET_SIZE);
905 | // Initialize values needed to form NTP request
906 | // (see URL above for details on the packets)
907 | packetBuffer[0] = 0b11100011; // LI, Version, Mode
908 | packetBuffer[1] = 0; // Stratum, or type of clock
909 | packetBuffer[2] = 6; // Polling Interval
910 | packetBuffer[3] = 0xEC; // Peer Clock Precision
911 | // 8 bytes of zero for Root Delay & Root Dispersion
912 | packetBuffer[12] = 49;
913 | packetBuffer[13] = 0x4E;
914 | packetBuffer[14] = 49;
915 | packetBuffer[15] = 52;
916 |
917 | // all NTP fields have been given values, now you can send a packet requesting a timestamp:
918 | // Note that Udp.begin will request automatic translation (via a DNS server) from a
919 | // name (eg pool.ntp.org) to an IP address. Never use a specific IP address yourself,
920 | // let the DNS give back a random server IP address
921 | NTPUdp.beginPacket(address, 123); //NTP requests are to port 123
922 |
923 | // Get the data back
924 | NTPUdp.write(packetBuffer, NTP_PACKET_SIZE);
925 |
926 | // All done, the underlying buffer is now updated
927 | NTPUdp.endPacket();
928 |
929 | }
930 |
931 |
932 |
933 | //-----------------------------------------------------------------------------
934 | // contact the NTP pool and retrieve the time
935 | //-----------------------------------------------------------------------------
936 | //
937 | // code from https://github.com/RalphBacon/No-Real-Time-Clock-RTC-required---use-an-NTP
938 |
939 | time_t getNTPTime() {
940 |
941 | // Send a UDP packet to the NTP pool address
942 | Serial.print("\nSending NTP packet to ");
943 | Serial.println(timeServer);
944 | sendNTPpacket(timeServer);
945 |
946 | // Wait to see if a reply is available - timeout after X seconds. At least
947 | // this way we exit the 'delay' as soon as we have a UDP packet to process
948 | #define UDPtimeoutSecs 3
949 | int timeOutCnt = 0;
950 | while (NTPUdp.parsePacket() == 0 && ++timeOutCnt < (UDPtimeoutSecs * 10)){
951 | delay(100);
952 | // yield();
953 | }
954 |
955 | // Is there UDP data present to be processed? Sneak a peek!
956 | if (NTPUdp.peek() != -1) {
957 | // We've received a packet, read the data from it
958 | NTPUdp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
959 |
960 | // The time-stamp starts at byte 40 of the received packet and is four bytes,
961 | // or two words, long. First, extract the two words:
962 | unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
963 | unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
964 |
965 | // combine the four bytes (two words) into a long integer
966 | // this is NTP time (seconds since Jan 1 1900)
967 | unsigned long secsSince1900 = highWord << 16 | lowWord; // shift highword 16 binary places to the left then combine with lowword
968 | Serial.print("Seconds since Jan 1 1900 = ");
969 | Serial.println(secsSince1900);
970 |
971 | // now convert NTP time into everyday time:
972 | //Serial.print("Unix time = ");
973 |
974 | // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
975 | const unsigned long seventyYears = 2208988800UL; // UL denotes it is 'unsigned long'
976 |
977 | // subtract seventy years:
978 | unsigned long epoch = secsSince1900 - seventyYears;
979 |
980 | // Reset the interval to get the time from NTP server in case we previously changed it
981 | setSyncInterval(_resyncSeconds);
982 | NTPok = 1; // flag NTP is currently connecting ok
983 |
984 | return epoch;
985 | }
986 |
987 | // Failed to get an NTP/UDP response
988 | Serial.println("No response received from NTP");
989 | setSyncInterval(_resyncErrorSeconds); // try more frequently until a response is received
990 | NTPok = 0; // flag NTP not currently connecting
991 |
992 | return 0;
993 |
994 | }
995 |
996 |
997 |
998 |
999 | // ******************************************************************************************************************
1000 | // end
1001 |
--------------------------------------------------------------------------------
/Misc/esp32camdemo-greyscale.ino:
--------------------------------------------------------------------------------
1 | /*******************************************************************************************************************
2 | *
3 | * ESP32Cam development board demo sketch using Arduino IDE
4 | *
5 | * demo using greyscale data
6 | * root web page shows captured greyscale image data
7 | *
8 | *
9 | *******************************************************************************************************************/
10 |
11 | #if !defined ESP32
12 | #error This sketch is only for an ESP32Cam module
13 | #endif
14 |
15 | #include "esp_camera.h" // https://github.com/espressif/esp32-camera
16 | // #include "camera_pins.h"
17 |
18 |
19 | // ******************************************************************************************************************
20 |
21 |
22 |
23 |
24 | // ---------------------------------------------------------------------------------------------------------
25 |
26 | // Wifi Settings
27 |
28 | #include // delete this line, un-comment the below two lines and enter your wifi details
29 |
30 | //const char *SSID = "your_wifi_ssid";
31 |
32 | //const char *PWD = "your_wifi_pwd";
33 |
34 |
35 | // ---------------------------------------------------------------------------------------------------------
36 |
37 |
38 |
39 |
40 | // ---------------------------------------------------------------
41 | // -SETTINGS
42 | // ---------------------------------------------------------------
43 |
44 | const char* stitle = "ESP32Cam-demo-gs"; // title of this sketch
45 | const char* sversion = "07Jun21"; // Sketch version
46 |
47 | const bool serialDebug = 1; // show info. on serial port (1=enabled, disable if using pins 1 and 3 as gpio)
48 |
49 | #define useMCP23017 0 // if MCP23017 IO expander chip is being used (on pins 12 and 13)
50 |
51 | // Camera related
52 | const bool flashRequired = 1; // If flash to be used when capturing image (1 = yes)
53 | const framesize_t FRAME_SIZE_IMAGE = FRAMESIZE_QQVGA;// Image resolution:
54 | // default = "const framesize_t FRAME_SIZE_IMAGE = FRAMESIZE_VGA"
55 | // 160x120 (QQVGA), 128x160 (QQVGA2), 176x144 (QCIF), 240x176 (HQVGA),
56 | // 320x240 (QVGA), 400x296 (CIF), 640x480 (VGA, default), 800x600 (SVGA),
57 | // 1024x768 (XGA), 1280x1024 (SXGA), 1600x1200 (UXGA)
58 | #define PIXFORMAT PIXFORMAT_GRAYSCALE; // image format, Options = YUV422, GRAYSCALE, RGB565, JPEG, RGB888
59 | #define WIDTH 160 // image size
60 | #define HEIGHT 120
61 |
62 | int cameraImageExposure = 0; // Camera exposure (0 - 1200) If gain and exposure both set to zero then auto adjust is enabled
63 | int cameraImageGain = 0; // Image gain (0 - 30)
64 |
65 | const int TimeBetweenStatus = 600; // speed of flashing system running ok status light (milliseconds)
66 |
67 | const int indicatorLED = 33; // onboard small LED pin (33)
68 |
69 | const int brightLED = 4; // onboard Illumination/flash LED pin (4)
70 |
71 | const int iopinA = 13; // general io pin 13
72 | const int iopinB = 12; // general io pin 12 (must not be high at boot)
73 | const int iopinC = 16; // input only pin 16 (used by PSRam but you may get away with using it for a button)
74 |
75 | const int serialSpeed = 115200; // Serial data speed to use
76 |
77 |
78 | // camera settings (for the standard - OV2640 - CAMERA_MODEL_AI_THINKER)
79 | // see: https://randomnerdtutorials.com/esp32-cam-camera-pin-gpios/
80 | // set camera resolution etc. in 'initialiseCamera()' and 'cameraImageSettings()'
81 | #define CAMERA_MODEL_AI_THINKER
82 | #define PWDN_GPIO_NUM 32 // power to camera (on/off)
83 | #define RESET_GPIO_NUM -1 // -1 = not used
84 | #define XCLK_GPIO_NUM 0
85 | #define SIOD_GPIO_NUM 26 // i2c sda
86 | #define SIOC_GPIO_NUM 27 // i2c scl
87 | #define Y9_GPIO_NUM 35
88 | #define Y8_GPIO_NUM 34
89 | #define Y7_GPIO_NUM 39
90 | #define Y6_GPIO_NUM 36
91 | #define Y5_GPIO_NUM 21
92 | #define Y4_GPIO_NUM 19
93 | #define Y3_GPIO_NUM 18
94 | #define Y2_GPIO_NUM 5
95 | #define VSYNC_GPIO_NUM 25 // vsync_pin
96 | #define HREF_GPIO_NUM 23 // href_pin
97 | #define PCLK_GPIO_NUM 22 // pixel_clock_pin
98 |
99 |
100 |
101 | // ******************************************************************************************************************
102 |
103 |
104 | #include
105 | #include
106 | #include // used by requestWebPage()
107 | #include "driver/ledc.h" // used to configure pwm on illumination led
108 |
109 |
110 | WebServer server(80); // serve web pages on port 80
111 |
112 | // Used to disable brownout detection
113 | #include "soc/soc.h"
114 | #include "soc/rtc_cntl_reg.h"
115 |
116 | // sd-card
117 | #include "SD_MMC.h" // sd card - see https://randomnerdtutorials.com/esp32-cam-take-photo-save-microsd-card/
118 | #include
119 | #include // gives file access
120 | #define SD_CS 5 // sd chip select pin = 5
121 |
122 | // MCP23017 IO expander on pins 12 and 13 (optional)
123 | #if useMCP23017 == 1
124 | #include
125 | #include "Adafruit_MCP23017.h"
126 | Adafruit_MCP23017 mcp;
127 | // Wire.setClock(1700000); // set frequency to 1.7mhz
128 | #endif
129 |
130 | // Define some global variables:
131 | uint32_t lastStatus = millis(); // last time status light changed status (to flash all ok led)
132 | uint32_t lastCamera = millis(); // timer for periodic image capture
133 | bool sdcardPresent; // flag if an sd card is detected
134 | int imageCounter; // image file name on sd card counter
135 | uint32_t illuminationLEDstatus; // current brightness setting of the illumination led
136 |
137 |
138 |
139 | // ******************************************************************************************************************
140 |
141 |
142 | // ---------------------------------------------------------------
143 | // -SETUP SETUP SETUP SETUP SETUP SETUP
144 | // ---------------------------------------------------------------
145 |
146 | void setup() {
147 |
148 | if (serialDebug) {
149 | Serial.begin(serialSpeed); // Start serial communication
150 |
151 | Serial.println("\n\n\n"); // line feeds
152 | Serial.println("-----------------------------------");
153 | Serial.printf("Starting - %s - %s \n", stitle, sversion);
154 | Serial.println("-----------------------------------");
155 | // Serial.print("Reset reason: " + ESP.getResetReason());
156 | }
157 |
158 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // Turn-off the 'brownout detector'
159 |
160 | // small indicator led on rear of esp32cam board
161 | pinMode(indicatorLED, OUTPUT);
162 | digitalWrite(indicatorLED,HIGH);
163 |
164 | // Connect to wifi
165 | digitalWrite(indicatorLED,LOW); // small indicator led on
166 | if (serialDebug) {
167 | Serial.print("\nConnecting to ");
168 | Serial.print(SSID);
169 | Serial.print("\n ");
170 | }
171 | WiFi.begin(SSID, PWD);
172 | while (WiFi.status() != WL_CONNECTED) {
173 | delay(500);
174 | if (serialDebug) Serial.print(".");
175 | }
176 | if (serialDebug) {
177 | Serial.print("\nWiFi connected, ");
178 | Serial.print("IP address: ");
179 | Serial.println(WiFi.localIP());
180 | }
181 | server.begin(); // start web server
182 | digitalWrite(indicatorLED,HIGH); // small indicator led off
183 |
184 | // define the web pages (i.e. call these procedures when url is requested)
185 | server.on("/", capture_still); // demo converting image to RGB
186 |
187 |
188 | // set up camera
189 | if (serialDebug) Serial.print(("\nInitialising camera: "));
190 | if (initialiseCamera()) {
191 | if (serialDebug) Serial.println("OK");
192 | }
193 | else {
194 | if (serialDebug) Serial.println("Error!");
195 | }
196 |
197 | // define i/o pins
198 | pinMode(indicatorLED, OUTPUT); // defined again as sd card config can reset it
199 | digitalWrite(indicatorLED,HIGH); // led off = High
200 | pinMode(iopinA, OUTPUT); // pin 13 - free io pin, can be used for input or output
201 | pinMode(iopinB, OUTPUT); // pin 12 - free io pin, can be used for input or output (must not be high at boot)
202 | pinMode(iopinC, INPUT); // pin 16 - free input only pin
203 |
204 | // MCP23017 io expander (requires adafruit MCP23017 library)
205 | #if useMCP23017 == 1
206 | Wire.begin(12,13); // use pins 12 and 13 for i2c
207 | mcp.begin(&Wire); // use default address 0
208 | mcp.pinMode(0, OUTPUT); // Define GPA0 (physical pin 21) as output pin
209 | mcp.pinMode(8, INPUT); // Define GPB0 (physical pin 1) as input pin
210 | mcp.pullUp(8, HIGH); // turn on a 100K pullup internally
211 | // change pin state with mcp.digitalWrite(0, HIGH);
212 | // read pin state with mcp.digitalRead(8)
213 | #endif
214 |
215 | // startup complete
216 | if (serialDebug) Serial.println("\nSetup complete...");
217 |
218 | } // setup
219 |
220 |
221 | // ******************************************************************************************************************
222 |
223 |
224 | // ----------------------------------------------------------------
225 | // -LOOP LOOP LOOP LOOP LOOP LOOP LOOP
226 | // ----------------------------------------------------------------
227 |
228 |
229 | void loop() {
230 |
231 | server.handleClient(); // handle any incoming web page requests
232 |
233 |
234 |
235 |
236 |
237 |
238 | // <<< YOUR CODE HERE >>>
239 |
240 |
241 |
242 |
243 |
244 |
245 | // // demo to Capture an image and save to sd card every 5 seconds (i.e. time lapse)
246 | // if ( ((unsigned long)(millis() - lastCamera) >= 5000) && sdcardPresent ) {
247 | // lastCamera = millis(); // reset timer
248 | // storeImage(); // save an image to sd card
249 | // }
250 |
251 | // flash status LED to show sketch is running ok
252 | if ((unsigned long)(millis() - lastStatus) >= TimeBetweenStatus) {
253 | lastStatus = millis(); // reset timer
254 | digitalWrite(indicatorLED,!digitalRead(indicatorLED)); // flip indicator led status
255 | }
256 |
257 | } // loop
258 |
259 |
260 |
261 | // ******************************************************************************************************************
262 |
263 |
264 | // ----------------------------------------------------------------
265 | // Initialise the camera
266 | // ----------------------------------------------------------------
267 | // returns TRUE if successful
268 |
269 | bool initialiseCamera() {
270 |
271 | camera_config_t config;
272 |
273 | config.ledc_channel = LEDC_CHANNEL_0;
274 | config.ledc_timer = LEDC_TIMER_0;
275 | config.pin_d0 = Y2_GPIO_NUM;
276 | config.pin_d1 = Y3_GPIO_NUM;
277 | config.pin_d2 = Y4_GPIO_NUM;
278 | config.pin_d3 = Y5_GPIO_NUM;
279 | config.pin_d4 = Y6_GPIO_NUM;
280 | config.pin_d5 = Y7_GPIO_NUM;
281 | config.pin_d6 = Y8_GPIO_NUM;
282 | config.pin_d7 = Y9_GPIO_NUM;
283 | config.pin_xclk = XCLK_GPIO_NUM;
284 | config.pin_pclk = PCLK_GPIO_NUM;
285 | config.pin_vsync = VSYNC_GPIO_NUM;
286 | config.pin_href = HREF_GPIO_NUM;
287 | config.pin_sscb_sda = SIOD_GPIO_NUM;
288 | config.pin_sscb_scl = SIOC_GPIO_NUM;
289 | config.pin_pwdn = PWDN_GPIO_NUM;
290 | config.pin_reset = RESET_GPIO_NUM;
291 | config.xclk_freq_hz = 20000000; // XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
292 | config.pixel_format = PIXFORMAT; // Options = YUV422, GRAYSCALE, RGB565, JPEG, RGB888
293 | config.frame_size = FRAME_SIZE_IMAGE; // Image sizes: 160x120 (QQVGA), 128x160 (QQVGA2), 176x144 (QCIF), 240x176 (HQVGA), 320x240 (QVGA),
294 | // 400x296 (CIF), 640x480 (VGA, default), 800x600 (SVGA), 1024x768 (XGA), 1280x1024 (SXGA),
295 | // 1600x1200 (UXGA)
296 | config.jpeg_quality = 10; // 0-63 lower number means higher quality
297 | config.fb_count = 1; // if more than one, i2s runs in continuous mode. Use only with JPEG
298 |
299 | // check the esp32cam board has a psram chip installed (extra memory used for storing captured images)
300 | // Note: if not using "AI thinker esp32 cam" in the Arduino IDE, PSRAM must be enabled
301 | if (!psramFound()) {
302 | if (serialDebug) Serial.println("Warning: No PSRam found so defaulting to image size 'CIF'");
303 | config.frame_size = FRAMESIZE_CIF;
304 | }
305 |
306 | //#if defined(CAMERA_MODEL_ESP_EYE)
307 | // pinMode(13, INPUT_PULLUP);
308 | // pinMode(14, INPUT_PULLUP);
309 | //#endif
310 |
311 | esp_err_t camerr = esp_camera_init(&config); // initialise the camera
312 | if (camerr != ESP_OK) {
313 | if (serialDebug) Serial.printf("ERROR: Camera init failed with error 0x%x", camerr);
314 | }
315 |
316 | cameraImageSettings(); // apply custom camera settings
317 |
318 | return (camerr == ESP_OK); // return boolean result of camera initialisation
319 | }
320 |
321 |
322 | // ******************************************************************************************************************
323 |
324 |
325 | // ----------------------------------------------------------------
326 | // -Change camera image settings
327 | // ----------------------------------------------------------------
328 | // Adjust image properties (brightness etc.)
329 | // Defaults to auto adjustments if exposure and gain are both set to zero
330 | // - Returns TRUE if successful
331 | // BTW - some interesting info on exposure times here: https://github.com/raduprv/esp32-cam_ov2640-timelapse
332 |
333 | bool cameraImageSettings() {
334 |
335 | sensor_t *s = esp_camera_sensor_get();
336 | // something to try?: if (s->id.PID == OV3660_PID)
337 | if (s == NULL) {
338 | if (serialDebug) Serial.println("Error: problem reading camera sensor settings");
339 | return 0;
340 | }
341 |
342 | // if both set to zero enable auto adjust
343 | if (cameraImageExposure == 0 && cameraImageGain == 0) {
344 | // enable auto adjust
345 | s->set_gain_ctrl(s, 1); // auto gain on
346 | s->set_exposure_ctrl(s, 1); // auto exposure on
347 | s->set_awb_gain(s, 1); // Auto White Balance enable (0 or 1)
348 | } else {
349 | // Apply manual settings
350 | s->set_gain_ctrl(s, 0); // auto gain off
351 | s->set_awb_gain(s, 1); // Auto White Balance enable (0 or 1)
352 | s->set_exposure_ctrl(s, 0); // auto exposure off
353 | s->set_agc_gain(s, cameraImageGain); // set gain manually (0 - 30)
354 | s->set_aec_value(s, cameraImageExposure); // set exposure manually (0-1200)
355 | }
356 |
357 | return 1;
358 | } // cameraImageSettings
359 |
360 |
361 | // // More camera settings available:
362 | // // If you enable gain_ctrl or exposure_ctrl it will prevent a lot of the other settings having any effect
363 | // // more info on settings here: https://randomnerdtutorials.com/esp32-cam-ov2640-camera-settings/
364 | // s->set_gain_ctrl(s, 0); // auto gain off (1 or 0)
365 | // s->set_exposure_ctrl(s, 0); // auto exposure off (1 or 0)
366 | // s->set_agc_gain(s, cameraImageGain); // set gain manually (0 - 30)
367 | // s->set_aec_value(s, cameraImageExposure); // set exposure manually (0-1200)
368 | // s->set_vflip(s, cameraImageInvert); // Invert image (0 or 1)
369 | // s->set_quality(s, 10); // (0 - 63)
370 | // s->set_gainceiling(s, GAINCEILING_32X); // Image gain (GAINCEILING_x2, x4, x8, x16, x32, x64 or x128)
371 | // s->set_brightness(s, cameraImageBrightness); // (-2 to 2) - set brightness
372 | // s->set_lenc(s, 1); // lens correction? (1 or 0)
373 | // s->set_saturation(s, 0); // (-2 to 2)
374 | // s->set_contrast(s, cameraImageContrast); // (-2 to 2)
375 | // s->set_sharpness(s, 0); // (-2 to 2)
376 | // s->set_hmirror(s, 0); // (0 or 1) flip horizontally
377 | // s->set_colorbar(s, 0); // (0 or 1) - show a testcard
378 | // s->set_special_effect(s, 0); // (0 to 6?) apply special effect
379 | // s->set_whitebal(s, 0); // white balance enable (0 or 1)
380 | // s->set_awb_gain(s, 1); // Auto White Balance enable (0 or 1)
381 | // s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
382 | // s->set_dcw(s, 0); // downsize enable? (1 or 0)?
383 | // s->set_raw_gma(s, 1); // (1 or 0)
384 | // s->set_aec2(s, 0); // automatic exposure sensor? (0 or 1)
385 | // s->set_ae_level(s, 0); // auto exposure levels (-2 to 2)
386 | // s->set_bpc(s, 0); // black pixel correction
387 | // s->set_wpc(s, 0); // white pixel correction
388 |
389 |
390 |
391 | // ----------------------------------------------------------------
392 | // -access image as greyscale data - i.e. http://x.x.x.x/
393 | // ----------------------------------------------------------------
394 |
395 | bool capture_still() {
396 |
397 | WiFiClient client = server.client(); // open link with client
398 |
399 | // log page request including clients IP address
400 | if (serialDebug) {
401 | IPAddress cip = client.remoteIP();
402 | Serial.printf("Greyscale data requested from: %d.%d.%d.%d \n", cip[0], cip[1], cip[2], cip[3]);
403 | }
404 |
405 | client.write(" photo \n"); // basic html header
406 | client.write("
Greyscale data
");
407 |
408 |
409 | camera_fb_t *frame = esp_camera_fb_get();
410 |
411 | if (!frame)
412 | return false;
413 |
414 | // for each pixel in image
415 | // only shows first 3 lines of image as otherwise there is an awful lot of data
416 | // to show all data use the line: for (size_t i = 0; i < frame->len; i++) {
417 | for (size_t i = 0; i < (160 * 3); i++) {
418 | const uint16_t x = i % WIDTH; // x position in image
419 | const uint16_t y = floor(i / WIDTH); // y position in image
420 | byte pixel = frame->buf[i]; // pixel value
421 |
422 | // show data
423 | if (x==0) client.println("
"); // new line
424 | client.print(String(pixel)); // print byte as a string
425 | client.print(", ");
426 |
427 | }
428 |
429 | esp_camera_fb_return(frame); // return storage space
430 |
431 | // end html
432 | client.print("
Finished");
433 | client.print("\n");
434 | delay(3);
435 | client.stop();
436 |
437 | return true;
438 | }
439 |
440 |
441 | // ******************************************************************************************************************
442 | // end
443 |
--------------------------------------------------------------------------------
/Misc/motionDetect/beep.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanesq/esp32cam-demo/b0937eae7c5a692f9a805c7552007c34c296fb6b/Misc/motionDetect/beep.wav
--------------------------------------------------------------------------------
/Misc/motionDetect/images/imagesSaveHere:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanesq/esp32cam-demo/b0937eae7c5a692f9a805c7552007c34c296fb6b/Misc/motionDetect/images/imagesSaveHere
--------------------------------------------------------------------------------
/Misc/motionDetect/motionDetect.pde:
--------------------------------------------------------------------------------
1 | /*
2 | ----------------------------------------------------
3 |
4 | ESP32cam motion detection using Processing - 02Apr25
5 |
6 | uses libraries OpenCV and Minim
7 |
8 | ----------------------------------------------------
9 |
10 | Notes:
11 | Convert jpg files to amovie: convert -delay 10 *.jpg output.mp4
12 | Set up individual camera settings in 'setCam()'
13 |
14 | */
15 |
16 | // ----------------------------------------------------
17 | // Settings
18 | // ----------------------------------------------------
19 |
20 | String myParam = "ESPCam"; // camera title
21 | String imgUrl = "http://192.168.1.2/jpg" + "?image.jpg"; // url of the esp32cam
22 | String imageFolder = sketchPath() + "/images/"; // folder to store images
23 |
24 | boolean showDiags = true; // show extra diagnostic information on screen
25 | int lineSpace = 20; // line spacing for text/buttons on screen
26 | boolean soundEnabled = false; // if sound when motion detected
27 | boolean UDPenabled = false; // if sending UDP broadcasts is enabled
28 | int minTriggerTime = 15; // Minimum time between repeat triggers of sound or UDP (seconds)
29 | int maxLineTriggers = 40; // If trigger level of a horizontal line excedes this it is ignored (percentage 0-100)
30 | int triggerLevel = 65; // movement trigger level above baseline level
31 | int enableAdaptiveTriggerLevel = 1; // if trigger level adapts to previous movement levels (1 or 0)
32 | int windowSize = 50; // Sliding window size for average calculation
33 | long imageAgeLimit = 7; // days to keep stored images
34 | int resizeX = 160; // size of image created to motion detect
35 | int resizeY = 120;
36 | int changeThreshold = 20; // Pixel intensity change threshold
37 | int delay = 800; // Delay in milliseconds between draw calls (ms)
38 | float alpha = 0.1; // Smoothing factor (0.0 < alpha <= 1.0)
39 | int graphHeight = 80; // Height of the graph area
40 | int attemptsToTry = 5; // Max retries when capturing image
41 | int overlayTint = 80; // how srong the overlaed movement/mask is on the image (0-255)
42 |
43 |
44 | // A 4x4 movement detection mask (true = ignore area) - masked area is shown as yellow
45 | boolean[][] mask = {
46 | { false, false, false, false }, // Top row
47 | { false, false, false, false },
48 | { false, false, false, false },
49 | { false, false, false, false } // Bottom row
50 | };
51 |
52 |
53 | // ----------------------------------------------------
54 |
55 | // required to broadcast UDP
56 | import java.net.*;
57 | import java.io.*;
58 | DatagramSocket socket;
59 | InetAddress broadcastAddress;
60 | int port = 12345;
61 |
62 | // required to delete old image files
63 | import java.io.File;
64 | import java.util.Date;
65 |
66 | // diag variables
67 | int lineErrorCounter = 0; // for monitoring how many horizontal line rejections are occuring
68 | int maxHLTriggers = 0; // max triggers on a horizontal line
69 | long fameCompareTime = 0; // timing the frame compare procedure
70 | long fameCaptureTime = 0; // timing the frame capture from URL
71 | int TriggerCounter = 0; // trigger counter
72 |
73 | // control buttons
74 | int buttonWidth = 70;
75 | int buttonHeight = 17;
76 | int buttonSpacing = 80; // horizontal spacing of the buttons
77 | int buttonY = 3 * lineSpace - buttonHeight + 4; // Y position of the buttons
78 | int soundButtonX = 5; // X position of the sound button
79 | int UDPbuttonX = soundButtonX + buttonSpacing; // X position of the sound button
80 | int imageButtonX = soundButtonX + buttonSpacing * 2; // X position of the save image button
81 |
82 |
83 | // ----------------------------------------------------
84 | // camera settings
85 | // ----------------------------------------------------
86 | // camera selected from command line parameter
87 |
88 | void setCam(String param) {
89 |
90 | if (param.equals("front")) {
91 | imgUrl = "http://192.168.1.144/jpg" + "?image.jpg";
92 | imageFolder = sketchPath() + "/../images/front/";
93 | // change mask
94 | mask[3][0] = true; mask[3][1] = true; mask[3][2] = true; mask[3][3] = true;
95 | mask[0][3] = true; mask[1][3] = true; mask[2][3] = true;
96 | }
97 |
98 | if (param.equals("side")) {
99 | imgUrl = "http://192.168.1.192/jpg" + "?image.jpg";
100 | imageFolder = sketchPath() + "/../images/side/";
101 | }
102 |
103 | if (param.equals("back")) {
104 | imgUrl = "http://192.168.1.222/jpg" + "?image.jpg";
105 | imageFolder = sketchPath() + "/../images/back/";
106 | // change mask
107 | //mask[0][0] = true; mask[0][1] = true; mask[0][2] = true; mask[0][3] = true;
108 | //mask[1][3] = true;
109 | }
110 |
111 | deleteOldFiles(imageFolder); // delete any image files older than 2 weeks
112 | }
113 |
114 |
115 | PImage currentImg, previousImg, motionOverlay;
116 | int lastRun = 0; // time the daily deletion of older images was last performed
117 | int saveThreshold = 1000000 - triggerLevel; // self adaptive movement trigger level
118 | int refreshRate = 4; // frame rate
119 | int lastDrawTime = 0; // Stores the last time draw() was called
120 | int movementLevel = 0; // Accumulated movement level
121 | ArrayList recentReadings = new ArrayList(); // Store past readings
122 | int maxReading = 0; // Maximum reading for bar graph normalization
123 | String lastDownloadTime = ""; // Store last image fetch time
124 | long lastUDPtrigger = 0; // Last time a UDP broadcast was sent
125 | long lastSoundtrigger = 0; // Last time a sound was triggered
126 |
127 | // sound
128 | import ddf.minim.*;
129 | Minim minim;
130 | AudioPlayer beep;
131 |
132 |
133 | // ----------------------------------------------------
134 | // -setup
135 | // ----------------------------------------------------
136 |
137 | void setup() {
138 |
139 | // audio
140 | minim = new Minim(this);
141 | beep = minim.loadFile("../beep.wav");
142 |
143 | // get camera from parameter
144 | myParam = System.getenv("motionCAM");
145 | if (myParam == null) myParam = "side"; // default camera
146 | println(getCurrentDateTime() + "[camera] motionCAM parameter: " + myParam);
147 | setCam(myParam); // set camera parameters
148 |
149 | // setup display
150 | frameRate(refreshRate);
151 | size(640, 480);
152 | surface.setResizable(true);
153 | surface.setTitle("Motion: " + myParam);
154 |
155 | println(getCurrentDateTime() + "[camera] '" + myParam + "' starting");
156 |
157 | // request image from camera
158 | currentImg = requestImg(); // load first image
159 | normalizeBrightness(currentImg); // adjust brigtness to compensate for sudden changes in sunlight
160 | if (currentImg != null) previousImg = currentImg.get(); // store this image as the reference for comparison
161 |
162 | if (soundEnabled == true) makeSound();
163 |
164 | if (enableAdaptiveTriggerLevel == 0) saveThreshold = 0; // if adaptive trigger is disabled
165 |
166 | // setup for UDP broadcasting
167 | try {
168 | socket = new DatagramSocket(null);
169 | socket.setReuseAddress(true); // Enable reuse
170 | socket.setBroadcast(true); // Enable broadcast
171 | socket.bind(new InetSocketAddress(port));
172 | broadcastAddress = InetAddress.getByName("192.168.1.255");
173 | } catch (Exception e) {
174 | e.printStackTrace();
175 | }
176 | sendUDPmessage("GM:Camera " + myParam + " starting"); // send a UDP broadcast
177 | }
178 |
179 |
180 | // ----------------------------------------------------
181 | // -draw
182 | // ----------------------------------------------------
183 |
184 | void draw() {
185 | // delay each time to ensure the camera is not overloaded
186 | if (millis() - lastDrawTime >= delay) {
187 |
188 | lastDrawTime = millis(); // Update the last draw time
189 |
190 | // Refresh image
191 | if (currentImg != null) previousImg = currentImg.get();
192 | currentImg = requestImg();
193 | normalizeBrightness(currentImg); // adjust brigtness to compensate for sudden changes in sunlight
194 |
195 | if (previousImg != null && currentImg != null) {
196 |
197 | int movementLevel = compareImages(previousImg, currentImg, mask);
198 |
199 | // Update graph data
200 | recentReadings.add(movementLevel);
201 | if (recentReadings.size() > windowSize) {
202 | recentReadings.remove(0);
203 | }
204 | maxReading = max(maxReading, movementLevel);
205 |
206 | // display perameters
207 | background(0);
208 | tint(255, 255);
209 | fill(0, 0, 255);
210 |
211 | // Display new image
212 | image(currentImg, 0, 0, width, height);
213 |
214 | // display text
215 | textSize(18);
216 | textAlign(LEFT);
217 | text(myParam + ": " + lastDownloadTime, 10, 1 * lineSpace);
218 | text("Movement: " + movementLevel + " Trigger: " + (saveThreshold + triggerLevel), 10, 2 * lineSpace);
219 |
220 | // if extra diagnostic info display is enabled
221 | if (showDiags == true) {
222 | text("Line rejections: " + lineErrorCounter + " (" + maxHLTriggers + "/" + resizeX + ")", width / 2, 1 * lineSpace);
223 | text("Triggers: " + TriggerCounter, width / 2, 2 * lineSpace);
224 | text("Time to capture image: " + fameCaptureTime + "ms", width / 2, 3 * lineSpace);
225 | text("Time to compare images: " + fameCompareTime + "ms", width / 2, 4 * lineSpace);
226 | }
227 |
228 | // if movement threshold exceeded
229 | if (enableAdaptiveTriggerLevel == 1) updateThreshold(); // adapt threshold level
230 | if (movementLevel > saveThreshold + triggerLevel) {
231 | println(getCurrentDateTime() + "[camera] Movement detected (" + myParam +")");
232 | //currentImg.save(imageFolder + lastDownloadTime + ".jpg");
233 | save(imageFolder + lastDownloadTime + ".jpg");
234 | if (soundEnabled == true) makeSound();
235 | if (UDPenabled == true) {
236 | sendUDPmessage("IN:Movement detected"); // send UDP broadcast
237 | }
238 | TriggerCounter++; // trigger counter for extra diag display
239 | }
240 |
241 | // Draw movement graph
242 | tint(255, 80);
243 | drawGraph();
244 |
245 | // display movement detection image on top of camera image
246 | tint(255, 255);
247 | image(motionOverlay, 0, 0, width, height);
248 |
249 | // delete older images once per day
250 | if ((millis() - lastRun) % (24 * 60 * 60 * 1000) == 0) {
251 | deleteOldFiles(imageFolder);
252 | lastRun = millis(); // update the last run time
253 | }
254 | }
255 | }
256 | togButton(); // Sound on/off toggle button
257 | saveButton(); // save image button
258 |
259 | } // draw
260 |
261 |
262 | // ----------------------------------------------------
263 | // sound and UDP on/off toggle buttons
264 | // ----------------------------------------------------
265 |
266 | void togButton() {
267 | // Draw the sound toggle button
268 | if (soundEnabled) {
269 | fill(0, 255, 0); // Green when ON
270 | } else {
271 | fill(255, 0, 0); // Red when OFF
272 | }
273 | tint(255, 64);
274 | rect(soundButtonX, buttonY, buttonWidth, buttonHeight);
275 |
276 | // Draw the UDP toggle button
277 | if (UDPenabled) {
278 | fill(0, 255, 0); // Green when ON
279 | } else {
280 | fill(255, 0, 0); // Red when OFF
281 | }
282 | tint(255, 64);
283 | rect(UDPbuttonX, buttonY, buttonWidth, buttonHeight);
284 |
285 | // Draw sound button label
286 | fill(0, 0, 255); // blue
287 | tint(255, 64);
288 | textSize(12);
289 | textAlign(CENTER, CENTER);
290 | text(soundEnabled ? "Sound ON" : "Sound OFF", soundButtonX + buttonWidth / 2, buttonY + buttonHeight / 2);
291 |
292 | // Draw UDP button label
293 | fill(0, 0, 255); // blue
294 | tint(255, 64);
295 | textSize(12);
296 | textAlign(CENTER, CENTER);
297 | text(UDPenabled ? "UDP ON" : "UDP OFF", UDPbuttonX + buttonWidth / 2, buttonY + buttonHeight / 2);
298 | }
299 |
300 |
301 | // ----------------------------------------------------
302 | // save image button
303 | // ----------------------------------------------------
304 |
305 | void saveButton() {
306 | // Draw the save button
307 | fill(0, 255, 0); // Green when ON
308 | tint(255, 64);
309 | rect(imageButtonX, buttonY, buttonWidth, buttonHeight);
310 | // Draw button label
311 | fill(0, 0, 255); // blue
312 | tint(255, 64);
313 | textSize(12);
314 | textAlign(CENTER, CENTER);
315 | text("Save", imageButtonX + buttonWidth / 2 , buttonY + buttonHeight / 2);
316 | }
317 |
318 |
319 |
320 | // ----------------------------------------------------
321 | // if mouse was clicked action buttons
322 | // ----------------------------------------------------
323 |
324 | void mousePressed() {
325 | if (mouseX > soundButtonX && mouseX < soundButtonX + buttonWidth && mouseY > buttonY && mouseY < buttonY + buttonHeight) {
326 | soundEnabled = !soundEnabled; // Toggle the flag
327 | }
328 | if (mouseX > UDPbuttonX && mouseX < UDPbuttonX + buttonWidth && mouseY > buttonY && mouseY < buttonY + buttonHeight) {
329 | UDPenabled = !UDPenabled; // Toggle the UDP broadcasts flag
330 | }
331 | if (mouseX > imageButtonX && mouseX < imageButtonX + buttonWidth && mouseY > buttonY && mouseY < buttonY + buttonHeight) {
332 | save(imageFolder + lastDownloadTime + ".jpg"); // save image
333 | }
334 | }
335 |
336 |
337 | // ----------------------------------------------------
338 | // load image from camera
339 | // ----------------------------------------------------
340 |
341 | PImage requestImg() {
342 | int startTime3 = millis(); // used to time this procedure
343 |
344 | for (int attempt = 1; attempt <= attemptsToTry; attempt++) {
345 | PImage img = requestImage(imgUrl); // Start loading the image asynchronously
346 | int startTime2 = millis();
347 | boolean loaded = false;
348 |
349 | // Wait (up to 5 seconds) for the image to load without blocking the main thread
350 | while (millis() - startTime2 < 5000) {
351 | if (img.width > 1 && img.height > 1) {
352 | loaded = true;
353 | break;
354 | }
355 | try {
356 | Thread.sleep(50);
357 | } catch (InterruptedException e) {
358 | println(getCurrentDateTime() + "[camera] '" + myParam + "' interrupted: " + e.getMessage());
359 | return null;
360 | }
361 | }
362 |
363 | if (loaded) {
364 | lastDownloadTime = day() + "-" + month() + "-" + year() + "--" +
365 | hour() + ":" + nf(minute(), 2) + ":" + nf(second(), 2);
366 | if (attempt > 1)
367 | println(getCurrentDateTime() + "[camera] '" + myParam + "' image captured ok on attempt " + attempt);
368 | fameCaptureTime = millis() - startTime3; // store time to compare images
369 | return img;
370 | }
371 | else {
372 | println(getCurrentDateTime() + "[camera] '" + myParam + "' image capture timed out on attempt " + attempt);
373 | }
374 |
375 | // Cooldown before retrying
376 | try {
377 | Thread.sleep(800);
378 | } catch (InterruptedException e) {
379 | println(getCurrentDateTime() + "[camera] '" + myParam + "' interrupted during cooldown: " + e.getMessage());
380 | return null;
381 | }
382 | }
383 |
384 | println(getCurrentDateTime() + "[camera '" + myParam + "' - Failed to fetch image after " + attemptsToTry + " attempts");
385 | exit(); // close app
386 | return null;
387 | }
388 |
389 |
390 |
391 | // ----------------------------------------------------
392 | // compare two images
393 | // ----------------------------------------------------
394 | // a mask in the form of a 4x4 grid can be supplied (true = ignore area)
395 |
396 | int compareImages(PImage img1, PImage img2, boolean[][] mask) {
397 | long startTime4 = millis(); // used to time this procedure
398 | maxHLTriggers = 0; // maximum triggers on a horizontal line
399 |
400 | // Resize images for faster computation
401 | PImage smallImg1 = img1.get(); // Make a copy of img1
402 | PImage smallImg2 = img2.get(); // Make a copy of img2
403 | smallImg1.resize(resizeX, resizeY); // Resize modifies the image in place
404 | smallImg2.resize(resizeX, resizeY); // Resize modifies the image in place
405 |
406 | smallImg1.loadPixels();
407 | smallImg2.loadPixels();
408 |
409 | motionOverlay = createImage(smallImg1.width, smallImg1.height, ARGB);
410 | motionOverlay.loadPixels();
411 |
412 | int movementLevel = 0;
413 |
414 | int gridWidth = smallImg1.width / 4;
415 | int gridHeight = smallImg1.height / 4;
416 | int startOfLineTriggers = 0;
417 |
418 | for (int y = 0; y < smallImg1.height; y++) {
419 | startOfLineTriggers = movementLevel; // store how many triggers at start of this horizontal line
420 | for (int x = 0; x < smallImg1.width; x++) {
421 | int i = x + y * smallImg1.width;
422 |
423 | // Determine which grid cell the pixel belongs to
424 | int gridX = x / gridWidth;
425 | int gridY = y / gridHeight;
426 |
427 | // Check if the mask excludes this cell
428 | if (mask != null && mask[gridY][gridX]) {
429 | motionOverlay.pixels[i] = color(128, 128, 0, overlayTint); // highlight mask in yellow
430 | continue; // skip detection for this pixel
431 | }
432 |
433 | float diff = brightness(smallImg1.pixels[i]) - brightness(smallImg2.pixels[i]);
434 | if (abs(diff) > changeThreshold) { // if motion detected
435 | movementLevel++; // increment movement level detected value
436 | motionOverlay.pixels[i] = color(0, 128, 0, overlayTint); // Highlight motion in green
437 | } else {
438 | motionOverlay.pixels[i] = color(0, 0, 0, 0); // clear pixel
439 | //motionOverlay.pixels[i] = smallImg1.pixels[i]; // Keep original pixel
440 | }
441 | } // x
442 |
443 | // if whole line is triggered then assune it is error (interference in image or whole image has changed)
444 | int Triggers = movementLevel - startOfLineTriggers; // triggers on this line
445 | if (Triggers > (smallImg1.width * maxLineTriggers) / 100) {
446 | movementLevel = startOfLineTriggers; // discard this line as error
447 | lineErrorCounter++; // diag variable for monitoring line rejections
448 | }
449 | if (Triggers > maxHLTriggers) maxHLTriggers = Triggers; // update diag variable maximum triggers per line
450 |
451 | } // y
452 |
453 | motionOverlay.updatePixels();
454 |
455 | // Display the image with motion highlighted
456 | image(motionOverlay, 0, 0, width, height);
457 |
458 | // store time to compare images
459 | fameCompareTime = millis() - startTime4;
460 |
461 | return movementLevel;
462 | }
463 |
464 |
465 | // ----------------------------------------------------
466 | // normalise image brightness (to compensate for sun brightness changes)
467 | // ----------------------------------------------------
468 |
469 | void normalizeBrightness(PImage img) {
470 |
471 | if (img == null) return;
472 | img.loadPixels();
473 | float totalBrightness = 0;
474 | int numPixels = img.pixels.length;
475 |
476 | for (color c : img.pixels) {
477 | totalBrightness += brightness(c);
478 | }
479 |
480 | float avgBrightness = totalBrightness / numPixels;
481 | float targetBrightness = 128;
482 | float brightnessFactor = targetBrightness / avgBrightness;
483 |
484 | for (int i = 0; i < img.pixels.length; i++) {
485 | color c = img.pixels[i];
486 | float r = constrain(red(c) * brightnessFactor, 0, 255);
487 | float g = constrain(green(c) * brightnessFactor, 0, 255);
488 | float b = constrain(blue(c) * brightnessFactor, 0, 255);
489 | img.pixels[i] = color(r, g, b);
490 | }
491 | img.updatePixels();
492 | }
493 |
494 |
495 | // ----------------------------------------------------
496 | // Draw bar graph for recent readings
497 | // ----------------------------------------------------
498 |
499 | void drawGraph() {
500 | int graphTransparency = 100;
501 | int graphTop = height - graphHeight; // Start of graph
502 | int topSpacing = 15;
503 |
504 | // Calculate the displayed range manually from the ArrayList
505 | int displayedMax = Integer.MIN_VALUE; // Start with the smallest possible integer
506 | int displayedMin = Integer.MAX_VALUE; // Start with the largest possible integer
507 |
508 | for (int i = 0; i < recentReadings.size(); i++) {
509 | int reading = recentReadings.get(i);
510 | displayedMax = max(displayedMax, reading);
511 | displayedMin = min(displayedMin, reading);
512 | }
513 |
514 | int range = displayedMax - displayedMin == 0 ? 1 : displayedMax - displayedMin; // Avoid divide-by-zero error
515 |
516 | // Calculate bar width based on the size of the recentReadings
517 | float barWidth = (float) width / recentReadings.size(); // Use float for precise width
518 |
519 | // Set a translucent background for the graph area
520 | //fill(50, 50, 50, graphTransparency); // Dark gray with some transparency
521 | //rect(0, graphTop - topSpacing, width, graphHeight + topSpacing);
522 |
523 | for (int i = 0; i < recentReadings.size(); i++) {
524 | int reading = recentReadings.get(i);
525 |
526 | // Map the readings to the x-axis and bar height
527 | float xPos = i * barWidth; // Use float for xPos
528 | int barHeight = 0;
529 | if (displayedMin != displayedMax) {
530 | barHeight = (int) map(reading, displayedMin, displayedMax, 0, graphHeight);
531 | }
532 | // Use semi-transparent green for bars
533 | fill(0, 255, 0, graphTransparency); // Green with alpha set to 150 (semi-transparent)
534 | rect(xPos, graphTop + graphHeight - barHeight, barWidth - 1, barHeight); // Draw the bar
535 |
536 | // Draw the value on top of the bar
537 | fill(255); // White text
538 | textSize(8);
539 | textAlign(CENTER, BOTTOM);
540 | text(reading, xPos + barWidth / 2, graphTop + graphHeight - barHeight - 2); // Display the value
541 | }
542 | }
543 |
544 |
545 | // ----------------------------------------------------
546 | // adaptive movement detected threshold
547 | // ----------------------------------------------------
548 | // The procedure sorts recentReadings, calculates the median and interquartile
549 | // range (IQR), and sets saveThreshold to the median plus 1.5 times the IQR.
550 | // This adapts the threshold to current data, ignoring outliers.
551 |
552 | void updateThreshold() {
553 | // Create a copy of recentReadings
554 | ArrayList sortedReadings = new ArrayList(recentReadings);
555 |
556 | // Sort the ArrayList using a simple sorting algorithm
557 | sortedReadings.sort((a, b) -> a - b);
558 |
559 | int n = sortedReadings.size();
560 | if (n == 0) return; // Avoid division by zero
561 |
562 | // Calculate median
563 | float median = (n % 2 == 0) ?
564 | (sortedReadings.get(n/2 - 1) + sortedReadings.get(n/2)) / 2.0 :
565 | sortedReadings.get(n/2);
566 |
567 | // Calculate Q1 and Q3 for IQR
568 | float q1 = sortedReadings.get(n/4);
569 | float q3 = sortedReadings.get(3*n/4);
570 | float iqr = q3 - q1;
571 |
572 | // Adjust saveThreshold based on median and IQR
573 | saveThreshold = int(median + 1.5f * iqr); // Example factor
574 | }
575 |
576 |
577 | // ----------------------------------------------------
578 | // delete older image files
579 | // ----------------------------------------------------
580 |
581 | void deleteOldFiles(String folderPath) {
582 | println(getCurrentDateTime() + "[camera] Deleting older image files (" + myParam + ")");
583 | File folder = new File(folderPath);
584 |
585 | if (!folder.exists() || !folder.isDirectory()) {
586 | println("[camera]" + myParam + " - Invalid folder path: " + folderPath);
587 | return;
588 | }
589 |
590 | // Get all files in the folder
591 | File[] files = folder.listFiles();
592 |
593 | if (files == null || files.length == 0) {
594 | println("[camera]" + myParam + " - No files to check in: " + folderPath);
595 | return;
596 | }
597 |
598 | // Calculate the cutoff date
599 | long cuttofInMillis = imageAgeLimit * 24 * 60 * 60 * 1000; // 14 days in milliseconds
600 | long cutoffDate = System.currentTimeMillis() - cuttofInMillis;
601 |
602 | // Loop through each file
603 | for (File file : files) {
604 | if (file.isFile()) {
605 | // Check the last modified date
606 | long lastModified = file.lastModified();
607 |
608 | // If the file is older than 2 weeks, delete it
609 | if (lastModified < cutoffDate) {
610 | if (file.delete()) {
611 | println("[camera]" + myParam + " - Deleted: " + file.getName());
612 | } else {
613 | println("[camera]" + myParam + " - Failed to delete: " + file.getName());
614 | }
615 | } else {
616 | //println("[camera]" + myParam +" - Retained: " + file.getName());
617 | }
618 | }
619 | }
620 | }
621 |
622 |
623 | // ----------------------------------------------------
624 | // return the date and time
625 | // ----------------------------------------------------
626 |
627 | String getCurrentDateTime() {
628 | // Get the current date and time
629 | int year = year();
630 | int month = month();
631 | int day = day();
632 | int hour = hour();
633 | int minute = minute();
634 | int second = second();
635 |
636 | // Construct the date and time string
637 | String dateTime = nf(day, 2) + "/" + nf(month, 2) + "/" + year + " " + nf(hour, 2) + ":" + nf(minute, 2) + ":" + nf(second, 2);
638 |
639 | return dateTime;
640 | }
641 |
642 |
643 | // ----------------------------------------------------
644 | // send a UDP broadcast
645 | // ----------------------------------------------------
646 |
647 | void sendUDPmessage(String message) {
648 | if (UDPenabled == false) return;
649 | long currentTime = millis();
650 | if (currentTime - lastUDPtrigger >= (minTriggerTime * 1000) || lastUDPtrigger == 0) {
651 | lastUDPtrigger = currentTime;
652 | byte[] buffer = message.getBytes();
653 | DatagramPacket packet = new DatagramPacket(buffer, buffer.length, broadcastAddress, port);
654 | try {
655 | socket.send(packet);
656 | println("Message sent!");
657 | } catch (IOException e) {
658 | e.printStackTrace();
659 | }
660 | }
661 | }
662 |
663 |
664 | // ----------------------------------------------------
665 | // Make a sound
666 | // ----------------------------------------------------
667 |
668 | void makeSound() {
669 | long currentTime = millis();
670 | if (currentTime - lastSoundtrigger >= (minTriggerTime * 1000) || lastSoundtrigger == 0) {
671 | beep.rewind();
672 | beep.play();
673 | lastSoundtrigger = currentTime;
674 | }
675 | }
676 |
677 |
678 | // ----------------------------------------------------
679 | // end
680 |
--------------------------------------------------------------------------------
/Misc/multiCameraViewer.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Camera Viewer
26 |
27 |
28 |
29 |
172 |
173 |
174 |
175 |
176 |
177 |
845 |
846 |
847 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## ESP32Cam-demo sketch for use with the Arduino IDE
3 |
4 | I show in this sketch how to use the esp32cam as easily as possible. Everything I learn I try to add to it, please let me know if you have anything which you think can be added or changed to improve it - I am not a professional programmer so am sure there is plenty of room for improvement...
5 |
6 | This sketch has got a bit larger than I anticipated but this is just because it now has so many individual demonstrations of ways to use the camera, I have tried to make each part as easy to follow as possible with lots of comments etc..
7 | In fact I actually just use this sketch for most of my projects now. The /jpg url is very handy for external apps to grab images, e.g. I have created a Processing sketch which works very well as a motion sensing security camera [link here](https://github.com/alanesq/esp32cam-demo/tree/master/Misc/motionDetect)
8 | I also have nice mutli camera viewer HTML file which allows resizing and zooming of the camera images (created by ChatGPT) [link here](https://github.com/alanesq/esp32cam-demo/blob/master/Misc/multiCameraViewer.htm)
9 |
10 | The camera is not great quality and very poor in low light conditions but it is very cheap (around £5 each if buying several) and I think has lots of potential for interesting applications.
11 | This sketch is just a collection of all I have discovered/learned in the process of using them myself
12 |
13 | Note: This sketch now has the facility for OTA updates over the network, you need to copy the file ota.h in to your sketch folder and enable it in settings (#define ENABLE_OTA 1)
14 |
15 | If you have issues with the camera keep stopping working etc. I have had a couple of these with dodgy camera modules so it is worth trying another one to see if this is the
16 | source of your problems.
17 |
18 |
23 |
24 | This can be used as a starting point sketch for projects using the ESP32cam development board, it has the following features.
25 | - Web server with live video streaming and control buttons
26 | - SD card support (using 1-bit mode - gpio pins are usually 2, 4, 12 & 13 but using 1bit mode only uses pin 2) - I have heard there may be problems reading the sd card, I have only used it to write files myself?
27 | - Stores captured image on sd-card or in spiffs if no sd card is present
28 | - IO pins available for general use are 12 and 13 (12 must not be pulled high at boot)
29 | - Option to connect a MCP23017 chip to pins 12 and 13 to give you 16 gpio pins to use (this requires the Adafruit MCP23017 library)
30 | - The flash led is still available for use on pin 4 when using an sd card
31 | - PWM control of flash/illumination lED brighness implemented (i.e. to give brightness control)
32 | - Can read the image as RGB data (i.e. 3 bytes per pixel for red, green and blue value)
33 | - Act as web client (reading the web page in to a string) - see requestWebPage()
34 |
35 | The root web page uses AJAX to update info. on the page. This is not done in the conventional way where variable data is passed but
36 | instead passes complete lines of text, it may not be elegant but it makes changing what information is displayed much easier as all you
37 | have to do is modify what info handleData() sends.
38 |
39 | I have created a timelapse sketch based on this one which may be of interest: https://github.com/alanesq/esp32cam-Timelapse
40 |
41 | LATEST NEWS!!!
42 | There is now a very cheap motherboard available for the esp32cam which make it as easy to use as any other esp development board.
43 | Search eBay for "esp32cam mb" - see http://www.hpcba.com/en/latest/source/DevelopmentBoard/HK-ESP32-CAM-MB.html
44 | It looks like the esp32cam suplied with them are not standard and have one of the GND pins modified to act as a reset pin?
45 | So on esp32cam modules without this feature you have to plug the USB in whilst holding the program button to upload a sketch
46 | I find I have to use the lowest serial upload speed or it fails (Select 'ESP32 dev module' in the Arduino IDE to have the option and
47 | make sure PSRam is enabled).
48 | The wifi can be very poor whilst in the motherboard (I find this happens if you have something near the antenna on the esp32cam modules)
49 | but if I rest my thumb above the antenna I find the signal works ok).
50 | There is also now a esp32cam with built in USB available called the "ESP32-CAM-CH340"
51 | Many of the ebay listing include an external antenna and I would suggest this would be a good option if ordering one.
52 |
53 | I have tried to make the sketch as easy to follow/modify as possible with lots of comments etc. and no additional libraries used,
54 | as I found it quiet confusing as an amateur trying to do much with this module and difficult to find easy to understand
55 | examples/explanations of what I wanted to do, so I am publishing this sketch in the hope it will encourage/help others to have a
56 | try with these powerful and VERY affordable modules.
57 | The greyscale procedure I think is the most interesting as it shows how to switch camera modes and process the raw data very well.
58 | BTW - For some examples of serving web pages with en ESP module you may like to have a look
59 | at: https://github.com/alanesq/BasicWebserver/blob/master/misc/VeryBasicWebserver.ino
60 |
61 | BTW - Even if you do not require the camera I think these modules have some uses in many projects as they are very cheap, have a
62 | built in sd card reader,
63 | bright LED and the 4mb psram could prove useful for storing large amounts of temp data etc? (see the RGB section of the code to
64 | see how it can be used).
65 |
66 | created using the Arduino IDE with ESP32 module installed
67 | (See https://randomnerdtutorials.com/installing-the-esp32-board-in-arduino-ide-windows-instructions/)
68 | No additional libraries required
69 |
70 | [Youtube video on using the ESP32Cam board](https://www.youtube.com/watch?v=FmlxC0goKew)
71 |
72 | [Schematic](https://github.com/SeeedDocument/forum_doc/blob/master/reg/ESP32_CAM_V1.6.pdf)
73 |
74 | [Info on camera settings](https://randomnerdtutorials.com/esp32-cam-ov2640-camera-settings/)
75 |
76 |
77 | ----------------
78 |
79 | How to use:
80 |
81 | Enter your network details (ssid and password in to the sketch) and upload it to your esp32cam module
82 | If you monitor the serial port (speed 15200) it will tell you what ip address it has been given.
83 | If you now enter this ip address in to your browser you should be greated with the message "Hello from esp32cam"
84 |
85 | If you now put /stream at the end of the url i.e. http://x.x.x.x/stream
86 | It will live stream video from the camera
87 |
88 | If you have an sd card inserted then accessing http://x/x/x/x/photo
89 | Will capture an image and save it to the sd card
90 |
91 | There is a procedure which demonstrates how to get RGB data from an image which will allow for processing the images
92 | as data (http://x.x.x.x/rgb).
93 |
94 | URLs:
95 |
http://x.x.x.x/ Main page
96 |
http://x.x.x.x/jpg capture image and display as a JPG
97 |
http://x.x.x.x/jpeg as above but refreshes every 2 seconds
98 |
http://x.x.x.x/photo Capture an image and save to sd card
99 |
http://x.x.x.x/stream Show live streaming video
100 |
http://x.x.x.x/img Show most recent image saved to sd card
101 |
http://x.x.x.x//switch?on=1 Switch the GPIO pin on ('on=0' to turn it off)
102 |
http://x.x.x.x/img?img=1 Show image number 1 on sd card
103 |
http://x.x.x.x/greyscale Show how to capture a greyscale image and look at the raw data
104 |
http://x.x.x.x/rgb Captures an image and converts to RGB data (will not work with the highest
105 | resolution images as there is not enough memory)
106 |
107 | GPIO PINS:
108 |
13 free (used by sd card but free if using 1 bit mode)
109 |
12 free (must be low at boot, used by sd card but free if using 1 bit mode)
110 |
14 used by sd card (usable is SPI clock?)
111 |
2 used by sd card (usable as SPI MISO?)
112 |
15 used by sd card (usable as SPI CS?)
113 |
1 serial - output only?
114 |
3 serial - input only?
115 |
4 has the illumination/flash led on it - led could be removed and use as output?
116 |
33 onboard led - use as output?
117 |
118 | Some great info here: https://github.com/raphaelbs/esp32-cam-ai-thinker/blob/master/docs/esp32cam-pin-notes.md
119 | BTW-You can use an MCP23017 io expander chip on pins 12 and 13 to give you 16 general purpose gpio pins, this requires the adafruit MCP23017 library to be installed.
120 | Note: I have been told there may be issues reading files when sd-card is in 1-bit mode, I have only used it for writing them myself.
121 |
122 |
123 | ----------------
124 |
125 | Notes
126 | -----
127 |
128 | See the test procedure at the end of the sketch for several demos of what you can do
129 |
130 | You can see an example Processing sketch for displaying the raw rgb data from this sketch
131 | here: https://github.com/alanesq/esp32cam-demo/blob/master/Misc/displayRGB.pde
132 | This would read in a file created from the Arduino command: client.write(rgb, ARRAY_LENGTH);
133 | You can create such a file by setting the 'sendRGBfile' flag in settings and then accessing the /rgb page
134 |
135 | This looks like it may contain useful info. on another way of getting RGB data from the camera:
136 | https://eloquentarduino.github.io/2020/01/image-recognition-with-esp32-and-arduino/
137 |
138 | These modules require a good power supply. I find it best to put a good sized smoothing capacitor across the
139 | upply as the wifi especially can put lots
140 | of spikes on the line.
141 | If you get strange error messages, random reboots, wifi dropping out etc. first thing to do is make sure it is
142 | not just a power problem.
143 |
144 | BTW - You may like to have a look at the security camera sketch I have created as this has lots more going on
145 | including FTP, email, OTA updates
146 | https://github.com/alanesq/CameraWifiMotion
147 |
148 | When streaming video these units can get very hot so if you plan to do a lot of streaming this is worth checking to make sure it is not going to over heat.
149 |
150 | [Demo of accessing grayscale image data](https://github.com/alanesq/esp32cam-demo/blob/master/Misc/esp32camdemo-greyscale.ino)
151 |
152 | [sketch which can record AVI video to sd card](https://github.com/mtnbkr88/ESP32CAMVideoRecorder)
153 |
154 | [Handy way to upload images to a computer/web server via php which is very reliable and easy to use](https://RandomNerdTutorials.com/esp32-cam-post-image-photo-server/)
155 |
156 | [How to crop images on the ESP32cam](https://makexyz.fun/esp32-cam-cropping-images-on-device/)
157 |
158 | [Some good info here](https://github.com/raphaelbs/esp32-cam-ai-thinker) - [and here](https://randomnerdtutorials.com/projects-esp32-cam/)
159 |
160 | [Wokwi - handy for testing out bits of ESP32 code](https://wokwi.com/)
161 |
162 | Some sites I find handy when creating HTML:
163 | - [test html](https://www.w3schools.com/tryit/tryit.asp?filename=tryhtml_hello )
164 | - [check html for errors](http://www.freeformatter.com/html-formatter.html#ad-output)
165 | - [learn HTML](https://www.w3schools.com/)
166 |
167 | You may like to have a play with a Processing sketch I created which could be used to grab JPG images from this camera and motion detect:
168 | https://github.com/alanesq/imageChangeMonitor
169 |
170 | I have a demo sketch of how to capture and save a raw RGB file (see comments at top of how you can view the resulting file)
171 | https://github.com/alanesq/misc/blob/main/saveAndViewRGBfiles.ino
172 |
173 | If you have any handy info, tips, or improvements to my code etc. please feel let me know at: alanesq@disroot.org
174 |
175 |
176 |
177 |
178 | https://github.com/alanesq/esp32cam-demo
179 |
--------------------------------------------------------------------------------
/images/esp32cam.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanesq/esp32cam-demo/b0937eae7c5a692f9a805c7552007c34c296fb6b/images/esp32cam.jpeg
--------------------------------------------------------------------------------
/images/grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanesq/esp32cam-demo/b0937eae7c5a692f9a805c7552007c34c296fb6b/images/grey.png
--------------------------------------------------------------------------------
/images/rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanesq/esp32cam-demo/b0937eae7c5a692f9a805c7552007c34c296fb6b/images/rgb.png
--------------------------------------------------------------------------------
/images/root.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanesq/esp32cam-demo/b0937eae7c5a692f9a805c7552007c34c296fb6b/images/root.png
--------------------------------------------------------------------------------
/ota.h:
--------------------------------------------------------------------------------
1 | /**************************************************************************************************
2 | *
3 | * Over The Air updates (OTA) - 02Aug23
4 | * MODIFIED FOR USE WITH ESP32CamDemo (no log, different header)
5 | *
6 | * part of the BasicWebserver sketch - https://github.com/alanesq/BasicWebserver
7 | *
8 | * If using an esp32cam module In Arduino IDE Select "ESP32 dev module" not "ESP32-cam" with PSRAM enabled
9 | *
10 | **************************************************************************************************
11 |
12 | Make sure partition with OTA enabled is selected
13 |
14 | To enable/disable OTA updates see setting at top of main sketch (#define ENABLE_OTA 1)
15 |
16 | Then access with http:///ota
17 |
18 |
19 | **************************************************************************************************/
20 |
21 |
22 | #if defined ESP32
23 | #include
24 | #endif
25 |
26 |
27 | // forward declarations (i.e. details of all functions in this file)
28 | void otaSetup();
29 | void handleOTA();
30 |
31 |
32 | // some useful html/css
33 | const char colRed[] = ""; // red text
34 | const char colGreen[] = ""; // green text
35 | const char colBlue[] = ""; // blue text
36 | const char colEnd[] = ""; // end coloured text
37 | const char htmlSpace[] = " "; // leave a space (see 'HTML entity')
38 |
39 |
40 | // ----------------------------------------------------------------
41 | // -enable OTA
42 | // ----------------------------------------------------------------
43 | // Enable OTA updates, called when correct password has been entered
44 |
45 | void otaSetup() {
46 |
47 | OTAEnabled = 1; // flag that OTA has been enabled
48 |
49 | // esp32 version (using webserver.h)
50 | #if defined ESP32
51 | server.on("/update", HTTP_POST, []() {
52 | server.sendHeader("Connection", "close");
53 | server.send(200, "text/plain", (Update.hasError()) ? "Update Failed!, rebooting..." : "Update complete, rebooting...");
54 | delay(2000);
55 | ESP.restart();
56 | delay(2000);
57 | }, []() {
58 | HTTPUpload& upload = server.upload();
59 | if (upload.status == UPLOAD_FILE_START) {
60 | if (serialDebug) Serial.setDebugOutput(true);
61 | if (serialDebug) Serial.printf("Update: %s\n", upload.filename.c_str());
62 | if (!Update.begin()) { //start with max available size
63 | if (serialDebug) Update.printError(Serial);
64 | }
65 | } else if (upload.status == UPLOAD_FILE_WRITE) {
66 | if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
67 | if (serialDebug) Update.printError(Serial);
68 | }
69 | } else if (upload.status == UPLOAD_FILE_END) {
70 | if (Update.end(true)) { //true to set the size to the current progress
71 | if (serialDebug) Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
72 | } else {
73 | if (serialDebug) Update.printError(Serial);
74 | }
75 | if (serialDebug) Serial.setDebugOutput(false);
76 | } else {
77 | if (serialDebug) Serial.printf("Update Failed Unexpectedly (likely broken connection): status=%d\n", upload.status);
78 | }
79 | });
80 | #endif
81 |
82 | // esp8266 version (using ESP8266WebServer.h)
83 | #if defined ESP8266
84 | server.on("/update", HTTP_POST, []() {
85 | server.sendHeader("Connection", "close");
86 | server.send(200, "text/plain", (Update.hasError()) ? "Update Failed!, rebooting..." : "Update complete, rebooting...");
87 | delay(2000);
88 | ESP.restart();
89 | delay(2000);
90 | }, []() {
91 | HTTPUpload& upload = server.upload();
92 | if (upload.status == UPLOAD_FILE_START) {
93 | if (serialDebug) Serial.setDebugOutput(true);
94 | WiFiUDP::stopAll();
95 | if (serialDebug) Serial.printf("Update: %s\n", upload.filename.c_str());
96 | uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
97 | if (!Update.begin(maxSketchSpace)) { //start with max available size
98 | if (serialDebug) Update.printError(Serial);
99 | }
100 | } else if (upload.status == UPLOAD_FILE_WRITE) {
101 | if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
102 | if (serialDebug) Update.printError(Serial);
103 | }
104 | } else if (upload.status == UPLOAD_FILE_END) {
105 | if (Update.end(true)) { //true to set the size to the current progress
106 | if (serialDebug) Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
107 | } else {
108 | if (serialDebug) Update.printError(Serial);
109 | }
110 | if (serialDebug) Serial.setDebugOutput(false);
111 | }
112 | yield();
113 | });
114 | #endif
115 |
116 | }
117 |
118 |
119 | // ----------------------------------------------------------------
120 | // -OTA web page requested i.e. http://x.x.x.x/ota
121 | // ----------------------------------------------------------------
122 | // Request OTA password or implement OTA update if already entered
123 |
124 | void handleOTA(){
125 |
126 | WiFiClient client = server.client(); // open link with client
127 |
128 | // check if valid password supplied
129 | if (server.hasArg("pwd")) {
130 | if (server.arg("pwd") == OTAPassword) otaSetup(); // Enable over The Air updates (OTA)
131 | }
132 |
133 |
134 | // -----------------------------------------
135 |
136 | if (OTAEnabled == 0) {
137 |
138 | // OTA is not enabled so request password to enable it
139 |
140 | sendHeader(client, stitle);
141 |
142 | // This is the below javascript/html compacted to save flash memory via https://www.textfixer.com/html/compress-html-compression.php
143 | client.print (R"=====( )=====");
144 | /*
145 | client.print (R"=====(
146 |
160 |
166 | )=====");
167 | */
168 |
169 |
170 | sendFooter(client); // close web page
171 |
172 | }
173 |
174 | // -----------------------------------------
175 |
176 |
177 | if (OTAEnabled == 1) { // if OTA is enabled implement it
178 |
179 | sendHeader(client, stitle);
180 |
181 | client.write("
Update firmware
\n");
182 | client.printf("Current version = %s, %s \n\n", stitle, sversion);
183 |
184 | client.write("
\n");
187 |
188 | client.write("
Device will reboot when upload complete");
189 | client.printf("%s
To disable OTA restart device
%s \n", colRed, colEnd);
190 |
191 | sendFooter(client); // close web page
192 | }
193 |
194 | // -----------------------------------------
195 |
196 |
197 | // close html page
198 | delay(3);
199 | client.stop();
200 |
201 | }
202 |
203 |
204 | // ---------------------------------------------- end ----------------------------------------------
205 |
--------------------------------------------------------------------------------
/platformio.ini:
--------------------------------------------------------------------------------
1 | ; PlatformIO Project Configuration File
2 | ;
3 | ; Build options: build flags, source filter
4 | ; Upload options: custom upload port, speed and extra flags
5 | ; Library options: dependencies, extra library storages
6 | ; Advanced options: extra scripting
7 | ;
8 | ; Please visit documentation for the other options and examples
9 | ; https://docs.platformio.org/page/projectconf.html
10 |
11 | [env:esp32cam]
12 | platform = espressif32
13 | board = esp32cam
14 | framework = arduino
15 | build_flags =
16 | ${env.build_flags}
17 | -D SSID_NAME=\"wifi ssid\" ; wifi details
18 | -D SSID_PASWORD=\"wifi password\"
19 | -w ;supress all warnings
20 | -DCORE_DEBUG_LEVEL=2
21 | -DBOARD_HAS_PSRAM
22 | -mfix-esp32-psram-cache-issue
23 | lib_deps =
24 | esp32-camera
25 |
--------------------------------------------------------------------------------