├── .gitignore ├── LICENSE ├── README.md ├── examples ├── .DS_Store ├── Autonomous_Car │ └── Autonomous_Car.ino ├── Car_Test │ └── Car_Test.ino ├── Collect_Images_for_EdgeImpulse │ └── Collect_Images_for_EdgeImpulse.ino ├── EdgeImpulse_FOMO_NO_PSRAM │ └── EdgeImpulse_FOMO_NO_PSRAM.ino ├── Encode_Frame_on_the_Fly │ └── Encode_Frame_on_the_Fly.ino ├── Face_Detection │ └── Face_Detection.ino ├── Face_Recognition │ └── Face_Recognition.ino ├── MJPEG_Controls │ └── MJPEG_Controls.ino ├── MJPEG_Stream │ └── MJPEG_Stream.ino ├── Motion_Detection │ └── Motion_Detection.ino ├── Motion_Detection_Higher_Resolution │ └── Motion_Detection_Higher_Resolution.ino ├── Save_To_SD_MMC │ ├── .DS_Store │ ├── Incremental_Name │ │ └── Incremental_Name.ino │ ├── Manual_Name │ │ └── Manual_Name.ino │ └── NTP_Name │ │ └── NTP_Name.ino ├── Save_To_SPIFFS │ ├── .DS_Store │ ├── Incremental_Name │ │ └── Incremental_Name.ino │ ├── Manual_Name │ │ └── Manual_Name.ino │ └── NTP_Name │ │ └── NTP_Name.ino ├── Take_Picture │ └── Take_Picture.ino └── Telegram │ ├── Send_Image │ └── Send_Image.ino │ └── Send_Text │ └── Send_Text.ino ├── library.json ├── library.properties └── src ├── .DS_Store ├── eloquent_esp32cam.h └── eloquent_esp32cam ├── .DS_Store ├── assets ├── ImageBrowserServer.alpine.h ├── ImageBrowserServer.js.h ├── MotionDebugger.js.h ├── face_detection.html.h ├── mjpeg.html.h └── mjpeg.js.h ├── camera ├── brownout.h ├── camera.h ├── pinout.h ├── pixformat.h ├── quality.h ├── resolution.h ├── rgb_565.h ├── sensor.h └── xclk.h ├── car.h ├── car ├── fomo_driven_car.h ├── motor.h └── two_wheels_car.h ├── edgeimpulse ├── bbox.h ├── classifier.h ├── fomo.h ├── fomo_daemon.h └── image.h ├── extra ├── .DS_Store ├── car │ ├── car2wd.h │ └── motor.h ├── esp32 │ ├── fs │ │ ├── fs.h │ │ ├── sd.h │ │ ├── sdmmc.h │ │ ├── sdmmc_pins.h │ │ ├── spiffs.h │ │ └── write_session.h │ ├── html │ │ └── HtmlBuilder.h │ ├── http │ │ └── server.h │ ├── logging │ │ └── webserial.h │ ├── multiprocessing │ │ ├── mutex.h │ │ └── thread.h │ ├── ntp.h │ ├── nvs │ │ └── counter.h │ ├── telegram.h │ ├── wifi.h │ ├── wifi │ │ └── sta.h │ └── ws │ │ └── threaded_ws.h ├── exception.h ├── pubsub.h ├── time │ ├── benchmark.h │ └── rate_limit.h └── ulid.h ├── face ├── daemon.h ├── detection.h ├── detection_server.h ├── face_t.h ├── mnp_config.h ├── msr_config.h └── recognition.h ├── jpeg ├── Callbacks.h ├── JPEGDECWrapper.cpp ├── JPEGDECWrapper.h ├── JPEGENCWrapper.h ├── decoder_gray.h ├── picojpeg.cpp ├── picojpeg.h └── ycbcr.h ├── jpegdec.h ├── jpegenc.h ├── mem.h ├── motion ├── daemon.h ├── detection.h └── roi_detection.h ├── mqtt-stream.h ├── remote-stream.h ├── transform └── crop.h └── viz ├── car_stream.h ├── ei └── fomo_stream.h ├── face_stream.h ├── file_browser.h ├── image_collection.h ├── mjpeg.h └── motion └── roi_detection.h /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .idea 4 | .codelite 5 | .gitignore 6 | EloquentEsp32cam.workspace 7 | publish 8 | examples/ignore 9 | examples/PREMIUM_EXAMPLES 10 | ignore 11 | src/eloquent_esp32cam/avi/libavi.h 12 | tools 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unleash the full power of you ESP32 camera 2 | 3 | This Arduino library kickstarts your ESP32 camera projects 4 | by providing you a set of tools to easily interact your the camera. 5 | 6 | ## Take a picture 7 | 8 | Forget complex configurations and verbose code. 9 | 10 | ```cpp 11 | #include 12 | 13 | using eloq::camera; 14 | 15 | void setup() { 16 | delay(3000); 17 | Serial.begin(115200); 18 | Serial.println("___GET YOUR FIRST PICTURE___"); 19 | 20 | camera.pinout.aithinker(); 21 | camera.brownout.disable(); 22 | camera.resolution.vga(); 23 | 24 | while (!camera.begin().isOk()) 25 | Serial.println(camera.exception.toString()); 26 | } 27 | 28 | void loop() { 29 | if (!camera.capture().isOk()) { 30 | Serial.println(camera.exception.toString()); 31 | return; 32 | } 33 | 34 | Serial.printf( 35 | "JPEG size in bytes: %d. Width: %dpx. Height: %dpx.\n", 36 | camera.getSizeInBytes(), 37 | camera.resolution.getWidth(), 38 | camera.resolution.getHeight() 39 | ); 40 | } 41 | ``` 42 | 43 | ## Save picture to SD card 44 | 45 | Timestamped file storage on your SD card that you can finally understand! 46 | 47 | ```cpp 48 | #include 49 | #include 50 | #include 51 | 52 | void setup() { 53 | delay(3000); 54 | Serial.begin(115200); 55 | Serial.println("___SAVE PIC TO SD CARD___"); 56 | 57 | camera.pinout.freenove_s3(); 58 | camera.brownout.disable(); 59 | camera.resolution.vga(); 60 | camera.quality.high(); 61 | 62 | ntp.offsetFromGreenwhich(0); 63 | ntp.isDaylight(); 64 | ntp.server("pool.ntp.org"); 65 | 66 | // init camera 67 | while (!camera.begin().isOk()) 68 | Serial.println(camera.exception.toString()); 69 | 70 | // init SD 71 | while (!sdmmc.begin().isOk()) 72 | Serial.println(sdmmc.exception.toString()); 73 | 74 | // connect to WiFi to sync NTP 75 | while (!wifi.connect().isOk()) 76 | Serial.println(wifi.exception.toString()); 77 | 78 | // get NTP time 79 | while (!ntp.begin().isOk()) 80 | Serial.println(ntp.exception.toString()); 81 | 82 | Serial.println("Camera OK"); 83 | Serial.println("SD card OK"); 84 | Serial.println("NTP OK"); 85 | Serial.print("Current time is "); 86 | Serial.println(ntp.datetime()); 87 | } 88 | 89 | void loop() { 90 | if (!camera.capture().isOk()) { 91 | Serial.println(camera.exception.toString()); 92 | return; 93 | } 94 | 95 | // save under nested directory 96 | String date = ntp.date(); 97 | String datetime = ntp.datetime(); 98 | 99 | if (sdmmc.save(camera.frame).inside(date).to(datetime, "jpg").isOk()) { 100 | Serial.print("File written to "); 101 | Serial.println(sdmmc.session.lastFilename); 102 | } 103 | else Serial.println(sdmmc.session.exception.toString()); 104 | } 105 | ``` 106 | 107 | ## Object detection 108 | 109 | Running Edge Impulse FOMO object detection is a piece of cake. 110 | 111 | ```cpp 112 | #include 113 | #include 114 | #include 115 | 116 | using eloq::camera; 117 | using eloq::ei::fomo; 118 | 119 | void setup() { 120 | delay(3000); 121 | Serial.begin(115200); 122 | Serial.println("__EDGE IMPULSE FOMO (NO-PSRAM)__"); 123 | 124 | // camera settings 125 | camera.pinout.freenove_s3(); 126 | camera.brownout.disable(); 127 | camera.resolution.yolo(); 128 | camera.pixformat.rgb565(); 129 | 130 | // init camera 131 | while (!camera.begin().isOk()) 132 | Serial.println(camera.exception.toString()); 133 | 134 | Serial.println("Camera OK"); 135 | Serial.println("Put object in front of camera"); 136 | } 137 | 138 | 139 | void loop() { 140 | if (!camera.capture().isOk()) { 141 | Serial.println(camera.exception.toString()); 142 | return; 143 | } 144 | 145 | // run FOMO 146 | if (!fomo.run().isOk()) { 147 | Serial.println(fomo.exception.toString()); 148 | return; 149 | } 150 | 151 | // how many objects were found? 152 | Serial.printf( 153 | "Found %d object(s) in %dms\n", 154 | fomo.count(), 155 | fomo.benchmark.millis() 156 | ); 157 | 158 | // if no object is detected, return 159 | if (!fomo.foundAnyObject()) 160 | return; 161 | 162 | // if you expect to find a single object, use fomo.first 163 | Serial.printf( 164 | "Found %s at (x = %d, y = %d) (size %d x %d). " 165 | "Proba is %.2f\n", 166 | fomo.first.label, 167 | fomo.first.x, 168 | fomo.first.y, 169 | fomo.first.width, 170 | fomo.first.height, 171 | fomo.first.proba 172 | ); 173 | 174 | // if you expect to find many objects, use fomo.forEach 175 | if (fomo.count() > 1) { 176 | fomo.forEach([](int i, bbox_t bbox) { 177 | Serial.printf( 178 | "#%d) Found %s at (x = %d, y = %d) (size %d x %d). " 179 | "Proba is %.2f\n", 180 | i + 1, 181 | bbox.label, 182 | bbox.x, 183 | bbox.y, 184 | bbox.width, 185 | bbox.height, 186 | bbox.proba 187 | ); 188 | }); 189 | } 190 | } 191 | ``` -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/examples/.DS_Store -------------------------------------------------------------------------------- /examples/Autonomous_Car/Autonomous_Car.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Autonomous car driven by Edge Impulse FOMO 3 | * From: https://eloquentarduino.com/esp32-cam-autonomous-car 4 | * 5 | * Tested on ESP32S3 camera 6 | */ 7 | #include 8 | #include 9 | #include 10 | 11 | using eloq::camera; 12 | using eloq::car::Motor; 13 | using eloq::car::Car; 14 | 15 | /** 16 | * Replace with your motor pins 17 | */ 18 | Motor left(39, 40); 19 | Motor right(42, 41); 20 | Car fomoCar(left, right); 21 | 22 | 23 | 24 | /** 25 | * 26 | */ 27 | void setup() { 28 | delay(3000); 29 | Serial.begin(115200); 30 | Serial.println("___AUTONOMOUS CAR___"); 31 | 32 | // replace with your board 33 | camera.pinout.freenove_s3(); 34 | camera.brownout.disable(); 35 | camera.resolution.yolo(); 36 | camera.pixformat.rgb565(); 37 | 38 | // how many millis motors will run 39 | // to follow given object 40 | fomoCar.defaultDuration(100); 41 | fomoCar.stop(); 42 | 43 | // if you mounted the camera "backward" 44 | // (see video), you have to reverse the motors 45 | // left.reverse(); 46 | // right.reverse(); 47 | 48 | // init camera 49 | while (!camera.begin().isOk()) 50 | Serial.println(camera.exception.toString()); 51 | 52 | Serial.println("Camera OK"); 53 | Serial.println("Put object in front of camera"); 54 | } 55 | 56 | /** 57 | * 58 | */ 59 | void loop() { 60 | // capture picture 61 | if (!camera.capture().isOk()) { 62 | Serial.println(camera.exception.toString()); 63 | return; 64 | } 65 | 66 | // run FOMO 67 | if (!fomo.run().isOk()) { 68 | Serial.println(fomo.exception.toString()); 69 | return; 70 | } 71 | 72 | // let the car follow the object 73 | fomoCar.follow(fomo); 74 | } -------------------------------------------------------------------------------- /examples/Car_Test/Car_Test.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Test car wiring 3 | */ 4 | #include 5 | #include 6 | 7 | using eloq::camera; 8 | using eloq::car::Motor; 9 | using eloq::car::TwoWheelsCar; 10 | 11 | // replace with your motor pins 12 | Motor left(39, 40); 13 | Motor right(42, 41); 14 | TwoWheelsCar testCar(left, right); 15 | 16 | 17 | /** 18 | * 19 | */ 20 | void setup() { 21 | delay(3000); 22 | Serial.begin(115200); 23 | Serial.println("___CAR TEST___"); 24 | 25 | // how many millis motors will run 26 | testCar.defaultDuration(200); 27 | testCar.stop(); 28 | 29 | Serial.println("Enter one of f (forward), b (backward), l (left), r (right)"); 30 | } 31 | 32 | 33 | /** 34 | * 35 | */ 36 | void loop() { 37 | if (!Serial.available()) 38 | return; 39 | 40 | String cmd = Serial.readStringUntil('\n'); 41 | 42 | if (cmd.startsWith("f")) testCar.forward(); 43 | else if (cmd.startsWith("b")) testCar.backward(); 44 | else if (cmd.startsWith("l")) testCar.left(); 45 | else if (cmd.startsWith("r")) testCar.right(); 46 | } -------------------------------------------------------------------------------- /examples/Collect_Images_for_EdgeImpulse/Collect_Images_for_EdgeImpulse.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Collect images for Edge Impulse image 3 | * classification / object detection 4 | * 5 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 6 | * to turn on debug messages 7 | */ 8 | 9 | // if you define WIFI_SSID and WIFI_PASS before importing the library, 10 | // you can call connect() instead of connect(ssid, pass) 11 | // 12 | // If you set HOSTNAME and your router supports mDNS, you can access 13 | // the camera at http://{HOSTNAME}.local 14 | 15 | #define WIFI_SSID "SSID" 16 | #define WIFI_PASS "PASSWORD" 17 | #define HOSTNAME "esp32cam" 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | using eloq::camera; 25 | using eloq::wifi; 26 | using eloq::viz::collectionServer; 27 | 28 | 29 | void setup() { 30 | delay(3000); 31 | Serial.begin(115200); 32 | Serial.println("___IMAGE COLLECTION SERVER___"); 33 | 34 | // camera settings 35 | // replace with your own model! 36 | camera.pinout.wroom_s3(); 37 | camera.brownout.disable(); 38 | // Edge Impulse models work on square images 39 | // face resolution is 240x240 40 | camera.resolution.face(); 41 | camera.quality.high(); 42 | 43 | // init camera 44 | while (!camera.begin().isOk()) 45 | Serial.println(camera.exception.toString()); 46 | 47 | // connect to WiFi 48 | while (!wifi.connect().isOk()) 49 | Serial.println(wifi.exception.toString()); 50 | 51 | // init face detection http server 52 | while (!collectionServer.begin().isOk()) 53 | Serial.println(collectionServer.exception.toString()); 54 | 55 | Serial.println("Camera OK"); 56 | Serial.println("WiFi OK"); 57 | Serial.println("Image Collection Server OK"); 58 | Serial.println(collectionServer.address()); 59 | } 60 | 61 | 62 | void loop() { 63 | // server runs in a separate thread, no need to do anything here 64 | } -------------------------------------------------------------------------------- /examples/EdgeImpulse_FOMO_NO_PSRAM/EdgeImpulse_FOMO_NO_PSRAM.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Run Edge Impulse FOMO model. 3 | * It works on both PSRAM and non-PSRAM boards. 4 | * 5 | * The difference from the PSRAM version 6 | * is that this sketch only runs on 96x96 frames, 7 | * while PSRAM version runs on higher resolutions too. 8 | * 9 | * The PSRAM version can be found in my 10 | * "ESP32S3 Camera Mastery" course 11 | * at https://dub.sh/ufsDj93 12 | * 13 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 14 | * to turn on debug messages 15 | */ 16 | #include 17 | #include 18 | #include 19 | 20 | using eloq::camera; 21 | using eloq::ei::fomo; 22 | 23 | 24 | /** 25 | * 26 | */ 27 | void setup() { 28 | delay(3000); 29 | Serial.begin(115200); 30 | Serial.println("__EDGE IMPULSE FOMO (NO-PSRAM)__"); 31 | 32 | // camera settings 33 | // replace with your own model! 34 | camera.pinout.aithinker(); 35 | camera.brownout.disable(); 36 | // NON-PSRAM FOMO only works on 96x96 (yolo) RGB565 images 37 | camera.resolution.yolo(); 38 | camera.pixformat.rgb565(); 39 | 40 | // init camera 41 | while (!camera.begin().isOk()) 42 | Serial.println(camera.exception.toString()); 43 | 44 | Serial.println("Camera OK"); 45 | Serial.println("Put object in front of camera"); 46 | } 47 | 48 | 49 | void loop() { 50 | // capture picture 51 | if (!camera.capture().isOk()) { 52 | Serial.println(camera.exception.toString()); 53 | return; 54 | } 55 | 56 | // run FOMO 57 | if (!fomo.run().isOk()) { 58 | Serial.println(fomo.exception.toString()); 59 | return; 60 | } 61 | 62 | // how many objects were found? 63 | Serial.printf( 64 | "Found %d object(s) in %dms\n", 65 | fomo.count(), 66 | fomo.benchmark.millis() 67 | ); 68 | 69 | // if no object is detected, return 70 | if (!fomo.foundAnyObject()) 71 | return; 72 | 73 | // if you expect to find a single object, use fomo.first 74 | Serial.printf( 75 | "Found %s at (x = %d, y = %d) (size %d x %d). " 76 | "Proba is %.2f\n", 77 | fomo.first.label, 78 | fomo.first.x, 79 | fomo.first.y, 80 | fomo.first.width, 81 | fomo.first.height, 82 | fomo.first.proba 83 | ); 84 | 85 | // if you expect to find many objects, use fomo.forEach 86 | if (fomo.count() > 1) { 87 | fomo.forEach([](int i, bbox_t bbox) { 88 | Serial.printf( 89 | "#%d) Found %s at (x = %d, y = %d) (size %d x %d). " 90 | "Proba is %.2f\n", 91 | i + 1, 92 | bbox.label, 93 | bbox.x, 94 | bbox.y, 95 | bbox.width, 96 | bbox.height, 97 | bbox.proba 98 | ); 99 | }); 100 | } 101 | } -------------------------------------------------------------------------------- /examples/Encode_Frame_on_the_Fly/Encode_Frame_on_the_Fly.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Alter camera pixels before sending them via MJPEG stream 3 | * (requires enough RAM to run) 4 | * (expect 0.5 - 2 FPS) 5 | * 6 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 7 | * to turn on debug messages 8 | */ 9 | #define WIFI_SSID "SSID" 10 | #define WIFI_PASS "PASSWORD" 11 | #define HOSTNAME "esp32cam" 12 | 13 | #include 14 | #include 15 | 16 | using namespace eloq; 17 | using namespace eloq::viz; 18 | 19 | uint16_t jpeg_length = 0; 20 | size_t tick = 0; 21 | 22 | 23 | // prototype of the function that will 24 | // re-encode the frame on-the-fly 25 | void reencode_frame(WiFiClient *client, camera_fb_t* frame); 26 | 27 | // prototype of the functon that will 28 | // put JPEG-encoded data back into the frame 29 | size_t buffer_jpeg(void * arg, size_t index, const void* data, size_t len); 30 | 31 | 32 | /** 33 | * 34 | */ 35 | void setup() { 36 | delay(3000); 37 | Serial.begin(115200); 38 | Serial.println("__RE-ENCODE MJPEG STREAM__"); 39 | 40 | // camera settings 41 | // replace with your own model! 42 | camera.pinout.aithinker(); 43 | camera.brownout.disable(); 44 | // higher resolution cannot be handled 45 | camera.resolution.qvga(); 46 | camera.quality.best(); 47 | 48 | // since we want to access the raw pixels 49 | // capture in RGB565 format 50 | // keep in mind that you need a lot of RAM to store 51 | // all this data at high resolutions 52 | // (e.g. QVGA = 320 x 240 x 2 = 1536 kB) 53 | camera.pixformat.rgb565(); 54 | 55 | // MJPEG settings 56 | mjpeg.onFrame(&reencode_frame); 57 | 58 | // init camera 59 | while (!camera.begin().isOk()) 60 | Serial.println(camera.exception.toString()); 61 | 62 | // connect to WiFi 63 | while (!wifi.connect().isOk()) 64 | Serial.println(wifi.exception.toString()); 65 | 66 | // start mjpeg http server 67 | while (!mjpeg.begin().isOk()) 68 | Serial.println(mjpeg.exception.toString()); 69 | 70 | // assert camera can capture frames 71 | while (!camera.capture().isOk()) 72 | Serial.println(camera.exception.toString()); 73 | 74 | Serial.println("Camera OK"); 75 | Serial.println("ToF OK"); 76 | Serial.println("WiFi OK"); 77 | Serial.println("MjpegStream OK"); 78 | Serial.println(mjpeg.address()); 79 | } 80 | 81 | /** 82 | * 83 | */ 84 | void loop() { 85 | // nothing to do here, MJPEG server runs in background 86 | } 87 | 88 | 89 | /** 90 | * Apply your custom processing to pixels 91 | * then encode to JPEG. 92 | * You will need to modify this 93 | */ 94 | void reencode_frame(WiFiClient *client, camera_fb_t* frame) { 95 | // log how much time elapsed from last frame 96 | const size_t now = millis(); 97 | const uint16_t height = camera.resolution.getHeight(); 98 | const uint16_t width = camera.resolution.getWidth(); 99 | 100 | ESP_LOGI("benchmark", "%d ms elapsed from last frame", now - tick); 101 | tick = now; 102 | 103 | // frame->buf contains RGB565 data 104 | // that is, 2 bytes per pixel 105 | // 106 | // in this test, we're going to do a "negative" effect 107 | // feel free to replace this with your own code 108 | for (uint16_t y = 0; y < height; y++) { 109 | uint16_t *row = (uint16_t*) (frame->buf + width * 2 * y); 110 | 111 | for (uint16_t x = 0; x < width; x++) { 112 | // read pixel and parse to R, G, B components 113 | const uint16_t pixel = row[x]; 114 | uint16_t r = (pixel >> 11) & 0b11111; 115 | uint16_t g = (pixel >> 5) & 0b111111; 116 | uint16_t b = pixel & 0b11111; 117 | 118 | // actual work: make negative 119 | r = 31 - r; 120 | g = 63 - g; 121 | b = 31 - b; 122 | 123 | // re-pack to RGB565 124 | row[x] = (r << 11) | (g << 5) | b; 125 | } 126 | } 127 | 128 | // encode to jpeg 129 | uint8_t quality = 90; 130 | 131 | frame2jpg_cb(frame, quality, &buffer_jpeg, NULL); 132 | ESP_LOGI("var_dump", "JPEG size=%d", jpeg_length); 133 | } 134 | 135 | 136 | /** 137 | * Put JPEG-encoded data back into the original frame 138 | * (you don't have to modify this) 139 | */ 140 | size_t buffer_jpeg(void *arg, size_t index, const void* data, size_t len) { 141 | if (index == 0) { 142 | // first MCU block => reset jpeg length 143 | jpeg_length = 0; 144 | } 145 | 146 | if (len == 0) { 147 | // encoding is done 148 | camera.frame->len = jpeg_length; 149 | return 0; 150 | } 151 | 152 | jpeg_length += len; 153 | 154 | // override input data 155 | memcpy(camera.frame->buf + index, (uint8_t*) data, len); 156 | 157 | return len; 158 | } 159 | -------------------------------------------------------------------------------- /examples/Face_Detection/Face_Detection.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Face detection 3 | * ONLY WORKS ON ESP32S3 4 | * 5 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 6 | * to turn on debug messages 7 | */ 8 | #include 9 | #include 10 | 11 | using eloq::camera; 12 | using eloq::face_t; 13 | using eloq::face::detection; 14 | 15 | 16 | /** 17 | * 18 | */ 19 | void setup() { 20 | delay(3000); 21 | Serial.begin(115200); 22 | Serial.println("___FACE DETECTION___"); 23 | 24 | // camera settings 25 | // !!!!REPLACE WITH YOUR OWN MODEL!!!! 26 | camera.pinout.freenove_s3(); // e.g. xiao(), lilygo_tcamera_s3(), ... 27 | camera.brownout.disable(); 28 | // face detection only works at 240x240 29 | camera.resolution.face(); 30 | camera.quality.high(); 31 | 32 | // you can choose fast detection (50ms) 33 | detection.fast(); 34 | // or accurate detection (80ms) 35 | detection.accurate(); 36 | 37 | // you can set a custom confidence score 38 | // to consider a face valid 39 | // (in range 0 - 1, default is 0.5) 40 | detection.confidence(0.7); 41 | 42 | // init camera 43 | while (!camera.begin().isOk()) 44 | Serial.println(camera.exception.toString()); 45 | 46 | Serial.println("Camera OK"); 47 | Serial.println("Awaiting for face..."); 48 | } 49 | 50 | /** 51 | * 52 | */ 53 | void loop() { 54 | // capture picture 55 | if (!camera.capture().isOk()) { 56 | Serial.println(camera.exception.toString()); 57 | return; 58 | } 59 | 60 | // run detection 61 | if (!detection.run().isOk()) { 62 | Serial.println(detection.exception.toString()); 63 | return; 64 | } 65 | 66 | // if face is not found, abort 67 | if (detection.notFound()) 68 | return; 69 | 70 | Serial.printf( 71 | "Face(s) detected in %dms!\n", 72 | detection.benchmark.millis() 73 | ); 74 | 75 | // you can access the first detected face 76 | // at detection.first 77 | Serial.printf( 78 | " > face #1 located at (%d, %d)\n" 79 | " proba is %.2f\n", 80 | detection.first.x, 81 | detection.first.y, 82 | detection.first.score 83 | ); 84 | 85 | // if you expect multiple faces, you use forEach 86 | if (detection.count() > 1) { 87 | detection.forEach([](int i, face_t face) { 88 | Serial.printf( 89 | " > face #%d located at (%d, %d)\n", 90 | i + 1, 91 | face.x, 92 | face.y 93 | ); 94 | 95 | // if you enabled accurate detection 96 | // you have access to the keypoints 97 | if (face.hasKeypoints()) { 98 | Serial.printf( 99 | " > left eye at (%d, %d)\n" 100 | " > right eye at (%d, %d)\n" 101 | " > nose at (%d, %d)\n" 102 | " > left mouth at (%d, %d)\n" 103 | " > right mouth at (%d, %d)\n", 104 | face.leftEye.x, 105 | face.leftEye.y, 106 | face.rightEye.x, 107 | face.rightEye.y, 108 | face.nose.x, 109 | face.nose.y, 110 | face.leftMouth.x, 111 | face.leftMouth.y, 112 | face.rightMouth.x, 113 | face.rightMouth.y 114 | ); 115 | } 116 | 117 | }); 118 | } 119 | } -------------------------------------------------------------------------------- /examples/Face_Recognition/Face_Recognition.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * ESP32S3 Face Recognition 3 | * (not detection!) 4 | * 5 | * Enroll a couple of faces 2-3 times each, 6 | * then watch the ESP32 camera recognize between the two! 7 | * 8 | * Only works on ESP32 S3 chip. 9 | */ 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | using eloq::camera; 16 | using eloq::face::detection; 17 | using eloq::face::recognition; 18 | 19 | 20 | String prompt(String message); 21 | 22 | 23 | /** 24 | * 25 | */ 26 | void setup() { 27 | delay(4000); 28 | Serial.begin(115200); 29 | Serial.println("Begin"); 30 | 31 | // !!!!REPLACE WITH YOUR OWN MODEL!!!! 32 | camera.pinout.freenove_s3(); // e.g. xiao(), lilygo_tcamera_s3(), ... 33 | camera.brownout.disable(); 34 | // face recognition only works at 240x240 35 | camera.resolution.face(); 36 | camera.quality.high(); 37 | 38 | // face recognition only works with accurate detection 39 | detection.accurate(); 40 | detection.confidence(0.7); 41 | 42 | // face recognition confidence 43 | recognition.confidence(0.85); 44 | 45 | // init camera 46 | while (!camera.begin().isOk()) 47 | Serial.println(camera.exception.toString()); 48 | 49 | // init recognizer 50 | while (!recognition.begin().isOk()) 51 | Serial.println(recognition.exception.toString()); 52 | 53 | Serial.println("Camera OK"); 54 | Serial.println("Face recognizer OK"); 55 | 56 | // delete stored data, if user confirms 57 | if (prompt("Do you want to delete all existing faces? [yes|no]").startsWith("y")) { 58 | Serial.println("Deleting all existing faces..."); 59 | recognition.deleteAll(); 60 | } 61 | 62 | // dump stored faces, if user confirms 63 | if (prompt("Do you want to dump existing faces? [yes|no]").startsWith("y")) { 64 | recognition.dump(); 65 | } 66 | 67 | Serial.println("Awaiting for face..."); 68 | } 69 | 70 | 71 | /** 72 | * 73 | */ 74 | void loop() { 75 | // capture picture 76 | if (!camera.capture().isOk()) { 77 | Serial.println(camera.exception.toString()); 78 | return; 79 | } 80 | 81 | // run face detection (not recognition!) 82 | if (!recognition.detect().isOk()) 83 | return; 84 | 85 | // if face is found, ask user to enroll or recognize 86 | String answer = prompt("Do you want to enroll or recognize? [e|r]"); 87 | 88 | if (answer.startsWith("e")) 89 | enroll(); 90 | else if (answer.startsWith("r")) 91 | recognize(); 92 | 93 | Serial.println("Awaiting for face..."); 94 | } 95 | 96 | 97 | /** 98 | * Ask user for input 99 | */ 100 | String prompt(String message) { 101 | String answer; 102 | 103 | do { 104 | Serial.print(message); 105 | 106 | while (!Serial.available()) 107 | delay(1); 108 | 109 | answer = Serial.readStringUntil('\n'); 110 | answer.trim(); 111 | } while (!answer); 112 | 113 | Serial.print(" "); 114 | Serial.println(answer); 115 | return answer; 116 | } 117 | 118 | 119 | /** 120 | * Enroll new person 121 | */ 122 | void enroll() { 123 | String name = prompt("Enter person name:"); 124 | 125 | if (recognition.enroll(name).isOk()) 126 | Serial.println("Success!"); 127 | else 128 | Serial.println(recognition.exception.toString()); 129 | } 130 | 131 | 132 | /** 133 | * Recognize current face 134 | */ 135 | void recognize() { 136 | if (!recognition.recognize().isOk()) { 137 | Serial.println(recognition.exception.toString()); 138 | return; 139 | } 140 | 141 | Serial.print("Recognized face as "); 142 | Serial.print(recognition.match.name.c_str()); 143 | Serial.print(" with confidence "); 144 | Serial.print(recognition.match.similarity); 145 | Serial.print(" ("); 146 | Serial.print(recognition.benchmark.millis()); 147 | Serial.println("ms)"); 148 | } -------------------------------------------------------------------------------- /examples/MJPEG_Controls/MJPEG_Controls.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Play/Pause/Stop MJPEG stream 3 | * 4 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 5 | * to turn on debug messages 6 | */ 7 | 8 | // if you define WIFI_SSID and WIFI_PASS before importing the library, 9 | // you can call connect() instead of connect(ssid, pass) 10 | // 11 | // If you set HOSTNAME and your router supports mDNS, you can access 12 | // the camera at http://{HOSTNAME}.local 13 | 14 | #define WIFI_SSID "SSID" 15 | #define WIFI_PASS "PASSWORD" 16 | #define HOSTNAME "esp32cam" 17 | 18 | #include 19 | #include 20 | 21 | using namespace eloq; 22 | using namespace eloq::viz; 23 | 24 | 25 | /** 26 | * 27 | */ 28 | void setup() { 29 | delay(3000); 30 | Serial.begin(115200); 31 | Serial.println("___MJPEG STREAM SERVER CONTROLS___"); 32 | 33 | // camera settings 34 | // replace with your own model! 35 | camera.pinout.aithinker(); 36 | camera.brownout.disable(); 37 | camera.resolution.qvga(); 38 | camera.quality.high(); 39 | 40 | // init camera 41 | while (!camera.begin().isOk()) 42 | Serial.println(camera.exception.toString()); 43 | 44 | // connect to WiFi 45 | while (!wifi.connect().isOk()) 46 | Serial.println(wifi.exception.toString()); 47 | 48 | // start mjpeg http server 49 | while (!mjpeg.begin().isOk()) 50 | Serial.println(mjpeg.exception.toString()); 51 | 52 | Serial.println("Camera OK"); 53 | Serial.println("WiFi OK"); 54 | Serial.println("MjpegStream OK"); 55 | Serial.println(mjpeg.address()); 56 | Serial.println("Send play/pause/stop to control the server"); 57 | } 58 | 59 | /** 60 | * 61 | */ 62 | void loop() { 63 | if (!Serial.available()) 64 | return; 65 | 66 | String command = Serial.readStringUntil('\n'); 67 | 68 | if (command.startsWith("play")) 69 | mjpeg.play(); 70 | else if (command.startsWith("pause")) 71 | mjpeg.pause(); 72 | else if (command.startsWith("stop")) 73 | mjpeg.stop(); 74 | else 75 | Serial.println("Unknown command"); 76 | } -------------------------------------------------------------------------------- /examples/MJPEG_Stream/MJPEG_Stream.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * View camera MJPEG stream 3 | * 4 | * Start an HTTP server to access the live video feed 5 | * of the camera from the browser. 6 | * 7 | * Endpoints are: 8 | * - / -> displays the raw MJPEG stream 9 | * - /jpeg -> captures a still image 10 | * 11 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 12 | * to turn on debug messages 13 | */ 14 | 15 | // if you define WIFI_SSID and WIFI_PASS before importing the library, 16 | // you can call connect() instead of connect(ssid, pass) 17 | // 18 | // If you set HOSTNAME and your router supports mDNS, you can access 19 | // the camera at http://{HOSTNAME}.local 20 | 21 | #define WIFI_SSID "SSID" 22 | #define WIFI_PASS "PASSWORD" 23 | #define HOSTNAME "esp32cam" 24 | 25 | #include 26 | #include 27 | 28 | using namespace eloq; 29 | using namespace eloq::viz; 30 | 31 | 32 | /** 33 | * 34 | */ 35 | void setup() { 36 | delay(3000); 37 | Serial.begin(115200); 38 | Serial.println("___MJPEG STREAM SERVER___"); 39 | 40 | // camera settings 41 | // replace with your own model! 42 | camera.pinout.aithinker(); 43 | camera.brownout.disable(); 44 | camera.resolution.vga(); 45 | camera.quality.high(); 46 | 47 | // init camera 48 | while (!camera.begin().isOk()) 49 | Serial.println(camera.exception.toString()); 50 | 51 | // connect to WiFi 52 | while (!wifi.connect().isOk()) 53 | Serial.println(wifi.exception.toString()); 54 | 55 | // start mjpeg http server 56 | while (!mjpeg.begin().isOk()) 57 | Serial.println(mjpeg.exception.toString()); 58 | 59 | Serial.println("Camera OK"); 60 | Serial.println("WiFi OK"); 61 | Serial.println("MjpegStream OK"); 62 | Serial.println(mjpeg.address()); 63 | } 64 | 65 | 66 | void loop() { 67 | // HTTP server runs in a task, no need to do anything here 68 | } -------------------------------------------------------------------------------- /examples/Motion_Detection/Motion_Detection.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Motion detection 3 | * Detect when the frame changes by a reasonable amount 4 | * 5 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = DEBUG" 6 | * to turn on debug messages 7 | */ 8 | #include 9 | #include 10 | 11 | using eloq::camera; 12 | using eloq::motion::detection; 13 | 14 | 15 | /** 16 | * 17 | */ 18 | void setup() { 19 | delay(3000); 20 | Serial.begin(115200); 21 | Serial.println("___MOTION DETECTION___"); 22 | 23 | // camera settings 24 | // replace with your own model! 25 | camera.pinout.freenove_s3(); 26 | camera.brownout.disable(); 27 | camera.resolution.vga(); 28 | camera.quality.high(); 29 | 30 | // configure motion detection 31 | // the higher the stride, the faster the detection 32 | // the higher the stride, the lesser granularity 33 | detection.stride(1); 34 | // the higher the threshold, the lesser sensitivity 35 | // (at pixel level) 36 | detection.threshold(5); 37 | // the higher the threshold, the lesser sensitivity 38 | // (at image level, from 0 to 1) 39 | detection.ratio(0.2); 40 | // optionally, you can enable rate limiting (aka debounce) 41 | // motion won't trigger more often than the specified frequency 42 | detection.rate.atMostOnceEvery(5).seconds(); 43 | 44 | // init camera 45 | while (!camera.begin().isOk()) 46 | Serial.println(camera.exception.toString()); 47 | 48 | Serial.println("Camera OK"); 49 | Serial.println("Awaiting for motion..."); 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | void loop() { 56 | // capture picture 57 | if (!camera.capture().isOk()) { 58 | Serial.println(camera.exception.toString()); 59 | return; 60 | } 61 | 62 | // run motion detection 63 | if (!detection.run().isOk()) { 64 | Serial.println(detection.exception.toString()); 65 | return; 66 | } 67 | 68 | // on motion, perform action 69 | if (detection.triggered()) 70 | Serial.println("Motion detected!"); 71 | } -------------------------------------------------------------------------------- /examples/Motion_Detection_Higher_Resolution/Motion_Detection_Higher_Resolution.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Run motion detection at low resolution. 3 | * On motion, capture frame at higher resolution 4 | * for SD storage. 5 | * 6 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 7 | * to turn on debug messages 8 | */ 9 | #include 10 | #include 11 | 12 | using eloq::camera; 13 | using eloq::motion::detection; 14 | 15 | 16 | /** 17 | * 18 | */ 19 | void setup() { 20 | delay(3000); 21 | Serial.begin(115200); 22 | Serial.println("___MOTION DETECTION + SWITCH RESOLUTION___"); 23 | 24 | // camera settings 25 | // replace with your own model! 26 | camera.pinout.freenove_s3(); 27 | camera.brownout.disable(); 28 | camera.resolution.vga(); 29 | camera.quality.high(); 30 | 31 | // see example of motion detection for config values 32 | detection.skip(5); 33 | detection.stride(1); 34 | detection.threshold(5); 35 | detection.ratio(0.2); 36 | detection.rate.atMostOnceEvery(5).seconds(); 37 | 38 | // init camera 39 | while (!camera.begin().isOk()) 40 | Serial.println(camera.exception.toString()); 41 | 42 | Serial.println("Camera OK"); 43 | Serial.println("Awaiting for motion..."); 44 | } 45 | 46 | /** 47 | * 48 | */ 49 | void loop() { 50 | // capture picture 51 | if (!camera.capture().isOk()) { 52 | Serial.println(camera.exception.toString()); 53 | return; 54 | } 55 | 56 | // run motion detection 57 | if (!detection.run().isOk()) { 58 | Serial.println(detection.exception.toString()); 59 | return; 60 | } 61 | 62 | // on motion, perform action 63 | if (detection.triggered()) { 64 | Serial.printf( 65 | "Motion detected on frame of size %dx%d (%d bytes)\n", 66 | camera.resolution.getWidth(), 67 | camera.resolution.getHeight(), 68 | camera.getSizeInBytes() 69 | ); 70 | 71 | Serial.println("Taking photo of motion at higher resolution"); 72 | 73 | camera.resolution.at(FRAMESIZE_UXGA, []() { 74 | Serial.printf( 75 | "Switched to higher resolution: %dx%d. It took %d ms to switch\n", 76 | camera.resolution.getWidth(), 77 | camera.resolution.getHeight(), 78 | camera.resolution.benchmark.millis() 79 | ); 80 | 81 | camera.capture(); 82 | 83 | Serial.printf( 84 | "Frame size is now %d bytes\n", 85 | camera.getSizeInBytes() 86 | ); 87 | 88 | // save to SD... 89 | }); 90 | 91 | Serial.println("Resolution switched back to VGA"); 92 | } 93 | } -------------------------------------------------------------------------------- /examples/Save_To_SD_MMC/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/examples/Save_To_SD_MMC/.DS_Store -------------------------------------------------------------------------------- /examples/Save_To_SD_MMC/Incremental_Name/Incremental_Name.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * SDMMC file storage: incremental filename 3 | * 4 | * This sketch shows how to save a picture on the SD Card 5 | * filesystem by using an incremental filename that 6 | * persists across reboots 7 | * 8 | * Open the Serial Monitor and enter 'capture' (without quotes) 9 | * to capture a new image and save it to SD 10 | * 11 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 12 | * to turn on debug messages 13 | */ 14 | 15 | #include 16 | #include 17 | 18 | using namespace eloq; 19 | 20 | 21 | void setup() { 22 | delay(3000); 23 | Serial.begin(115200); 24 | Serial.println("___SAVE PIC TO SD CARD___"); 25 | 26 | // camera settings 27 | // replace with your own model! 28 | camera.pinout.freenove_s3(); 29 | camera.brownout.disable(); 30 | camera.resolution.vga(); 31 | camera.quality.high(); 32 | 33 | // you can configure each pin of SDMMC (if needed) 34 | // (delete these lines if you're not sure) 35 | sdmmc.pinout.clk(39); 36 | sdmmc.pinout.cmd(38); 37 | sdmmc.pinout.d0(40); 38 | 39 | // init camera 40 | while (!camera.begin().isOk()) 41 | Serial.println(camera.exception.toString()); 42 | 43 | // init SD 44 | while (!sdmmc.begin().isOk()) 45 | Serial.println(sdmmc.exception.toString()); 46 | 47 | Serial.println("Camera OK"); 48 | Serial.println("SD card OK"); 49 | Serial.println("Enter 'capture' to capture a new picture"); 50 | } 51 | 52 | 53 | void loop() { 54 | // await for "capture" from the Serial Monitor 55 | if (!Serial.available()) 56 | return; 57 | 58 | if (Serial.readStringUntil('\n') != "capture") { 59 | Serial.println("I only understand 'capture'"); 60 | return; 61 | } 62 | 63 | // capture picture 64 | if (!camera.capture().isOk()) { 65 | Serial.println(camera.exception.toString()); 66 | return; 67 | } 68 | 69 | // save under root folder 70 | if (sdmmc.save(camera.frame).to("", "jpg").isOk()) { 71 | Serial.print("File written to "); 72 | Serial.println(sdmmc.session.lastFilename); 73 | } 74 | else Serial.println(sdmmc.session.exception.toString()); 75 | 76 | // save under nested folder 77 | if (sdmmc.save(camera.frame).inside("myfolder").to("", "jpg").isOk()) { 78 | Serial.print("File written to "); 79 | Serial.println(sdmmc.session.lastFilename); 80 | } 81 | else Serial.println(sdmmc.session.exception.toString()); 82 | 83 | // restart the loop 84 | Serial.println("Enter 'capture' to capture a new picture"); 85 | } -------------------------------------------------------------------------------- /examples/Save_To_SD_MMC/Manual_Name/Manual_Name.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * SDMMC file storage: manual filename 3 | * 4 | * This sketch shows how to save a picture on the SD Card 5 | * filesystem by specifying the filename manually 6 | * 7 | * Open the Serial Monitor and enter 'capture' (without quotes) 8 | * to capture a new image and save it to SD 9 | * 10 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 11 | * to turn on debug messages 12 | */ 13 | 14 | #include 15 | #include 16 | 17 | using namespace eloq; 18 | 19 | 20 | void setup() { 21 | delay(3000); 22 | Serial.begin(115200); 23 | Serial.println("___SAVE PIC TO SD CARD___"); 24 | 25 | // camera settings 26 | // replace with your own model! 27 | camera.pinout.freenove_s3(); 28 | camera.brownout.disable(); 29 | camera.resolution.vga(); 30 | camera.quality.high(); 31 | 32 | // you can configure each pin of SDMMC (if needed) 33 | // (delete these lines if you are not sure) 34 | sdmmc.pinout.clk(39); 35 | sdmmc.pinout.cmd(38); 36 | sdmmc.pinout.d0(40); 37 | // or shorter 38 | sdmmc.pinout.freenove_s3(); 39 | 40 | // init camera 41 | while (!camera.begin().isOk()) 42 | Serial.println(camera.exception.toString()); 43 | 44 | // init SD 45 | while (!sdmmc.begin().isOk()) 46 | Serial.println(sdmmc.exception.toString()); 47 | 48 | Serial.println("Camera OK"); 49 | Serial.println("SD card OK"); 50 | Serial.println("Enter the filename where to save the picture"); 51 | } 52 | 53 | 54 | void loop() { 55 | // await for filename from the Serial Monitor 56 | if (!Serial.available()) 57 | return; 58 | 59 | String filename = Serial.readStringUntil('\n'); 60 | filename.trim(); 61 | 62 | if (!filename.endsWith(".jpg") && !filename.endsWith(".jpeg")) 63 | filename = filename + ".jpg"; 64 | 65 | // capture picture 66 | if (!camera.capture().isOk()) { 67 | Serial.println(camera.exception.toString()); 68 | return; 69 | } 70 | 71 | // save to SD 72 | if (sdmmc.save(camera.frame).to(filename).isOk()) { 73 | Serial.print("File written to "); 74 | Serial.println(sdmmc.session.lastFilename); 75 | } 76 | else Serial.println(sdmmc.session.exception.toString()); 77 | 78 | // you can also save under a nested folder 79 | if (sdmmc.save(camera.frame).inside("myfolder").to(filename).isOk()) { 80 | Serial.print("File written to "); 81 | Serial.println(sdmmc.session.lastFilename); 82 | } 83 | else Serial.println(sdmmc.session.exception.toString()); 84 | 85 | // restart the loop 86 | Serial.println("Enter another filename"); 87 | } -------------------------------------------------------------------------------- /examples/Save_To_SD_MMC/NTP_Name/NTP_Name.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * SDMMC file storage: NTP filename 3 | * 4 | * This sketch shows how to save a picture on the SD Card 5 | * filesystem by generating a filename using current time 6 | * 7 | * Open the Serial Monitor and enter 'capture' (without quotes) 8 | * to capture a new image and save it to SD 9 | * 10 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 11 | * to turn on debug messages 12 | */ 13 | 14 | // if you define WIFI_SSID and WIFI_PASS before importing the library 15 | // you can call wifi.connect() instead of wifi.connect(ssid, password) 16 | #define WIFI_SSID "SSID" 17 | #define WIFI_PASS "PASSWORD" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace eloq; 24 | 25 | 26 | void setup() { 27 | delay(3000); 28 | Serial.begin(115200); 29 | Serial.println("___SAVE PIC TO SD CARD___"); 30 | 31 | // camera settings 32 | // replace with your own model! 33 | camera.pinout.freenove_s3(); 34 | camera.brownout.disable(); 35 | camera.resolution.vga(); 36 | camera.quality.high(); 37 | 38 | // if connected to the internet, try to get time from NTP 39 | // you can set your timezone offset from Greenwich 40 | ntp.offsetFromGreenwhich(0); 41 | // or any of 42 | ntp.cst(); 43 | ntp.ist(); 44 | ntp.eest(); 45 | ntp.cest(); 46 | ntp.bst(); 47 | ntp.west(); 48 | ntp.cet(); 49 | ntp.gmt(); 50 | ntp.edt(); 51 | ntp.pdt(); 52 | 53 | // enable/disable daylight saving 54 | ntp.isntDaylight(); 55 | ntp.isDaylight(); 56 | 57 | ntp.server("pool.ntp.org"); 58 | 59 | // you can configure each pin of SDMMC (if needed) 60 | // (delete these lines if not sure) 61 | sdmmc.pinout.clk(39); 62 | sdmmc.pinout.cmd(38); 63 | sdmmc.pinout.d0(40); 64 | 65 | // init camera 66 | while (!camera.begin().isOk()) 67 | Serial.println(camera.exception.toString()); 68 | 69 | // init SD 70 | while (!sdmmc.begin().isOk()) 71 | Serial.println(sdmmc.exception.toString()); 72 | 73 | // connect to WiFi to sync NTP 74 | while (!wifi.connect().isOk()) 75 | Serial.println(wifi.exception.toString()); 76 | 77 | // get NTP time 78 | while (!ntp.begin().isOk()) 79 | Serial.println(ntp.exception.toString()); 80 | 81 | Serial.println("Camera OK"); 82 | Serial.println("SD card OK"); 83 | Serial.println("NTP OK"); 84 | Serial.print("Current time is "); 85 | Serial.println(ntp.datetime()); 86 | Serial.println("Enter 'capture' to capture a new picture"); 87 | } 88 | 89 | 90 | void loop() { 91 | // await for "capture" from the Serial Monitor 92 | if (!Serial.available()) 93 | return; 94 | 95 | if (Serial.readStringUntil('\n') != "capture") { 96 | Serial.println("I only understand 'capture'"); 97 | return; 98 | } 99 | 100 | // capture picture 101 | if (!camera.capture().isOk()) { 102 | Serial.println(camera.exception.toString()); 103 | return; 104 | } 105 | 106 | // save under root directory 107 | if (sdmmc.save(camera.frame).to(ntp.datetime(), "jpg").isOk()) { 108 | Serial.print("File written to "); 109 | Serial.println(sdmmc.session.lastFilename); 110 | } 111 | else Serial.println(sdmmc.session.exception.toString()); 112 | 113 | // save under nested directory 114 | String date = ntp.date(); 115 | String datetime = ntp.datetime(); 116 | 117 | if (sdmmc.save(camera.frame).inside(date).to(datetime, "jpg").isOk()) { 118 | Serial.print("File written to "); 119 | Serial.println(sdmmc.session.lastFilename); 120 | } 121 | else Serial.println(sdmmc.session.exception.toString()); 122 | 123 | // restart the loop 124 | Serial.println("Enter 'capture' to capture a new picture"); 125 | } -------------------------------------------------------------------------------- /examples/Save_To_SPIFFS/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/examples/Save_To_SPIFFS/.DS_Store -------------------------------------------------------------------------------- /examples/Save_To_SPIFFS/Incremental_Name/Incremental_Name.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * SPIFFS file storage: incremental filename 3 | * 4 | * This sketch shows how to save a picture on SPIFFS 5 | * filesystem by using an incremental filename that 6 | * persists across reboots 7 | * 8 | * Open the Serial Monitor and enter 'capture' (without quotes) 9 | * to capture a new image and save it 10 | * 11 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 12 | * to turn on debug messages 13 | */ 14 | 15 | #include 16 | #include 17 | 18 | using namespace eloq; 19 | 20 | 21 | void setup() { 22 | delay(3000); 23 | Serial.begin(115200); 24 | Serial.println("___SAVE PIC TO SPIFFS___"); 25 | 26 | // camera settings 27 | // replace with your own model! 28 | camera.pinout.wroom_s3(); 29 | camera.brownout.disable(); 30 | camera.resolution.vga(); 31 | camera.quality.high(); 32 | 33 | // init camera 34 | while (!camera.begin().isOk()) 35 | Serial.println(camera.exception.toString()); 36 | 37 | // init SPIFFS 38 | while (!spiffs.begin().isOk()) 39 | Serial.println(spiffs.exception.toString()); 40 | 41 | Serial.println("Camera OK"); 42 | Serial.println("SPIFFS OK"); 43 | Serial.println("Enter 'capture' to capture a new picture and save to SPIFFS"); 44 | } 45 | 46 | 47 | void loop() { 48 | // await for "capture" from the Serial Monitor 49 | if (!Serial.available()) 50 | return; 51 | 52 | if (Serial.readStringUntil('\n') != "capture") { 53 | Serial.println("I only understand 'capture' (without quotes)"); 54 | return; 55 | } 56 | 57 | // capture picture 58 | if (!camera.capture().isOk()) { 59 | Serial.println(camera.exception.toString()); 60 | return; 61 | } 62 | 63 | // save under root folder 64 | if (spiffs.save(camera.frame).to("", "jpg").isOk()) { 65 | Serial.print("File written to "); 66 | Serial.println(spiffs.session.lastFilename); 67 | } 68 | else Serial.println(spiffs.session.exception.toString()); 69 | 70 | // save under nested folder 71 | if (spiffs.save(camera.frame).inside("myfolder").to("", "jpg").isOk()) { 72 | Serial.print("File written to "); 73 | Serial.println(spiffs.session.lastFilename); 74 | } 75 | else Serial.println(spiffs.session.exception.toString()); 76 | 77 | // restart the loop 78 | Serial.println("Enter another filename"); 79 | } -------------------------------------------------------------------------------- /examples/Save_To_SPIFFS/Manual_Name/Manual_Name.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * SPIFFS file storage: manual filename 3 | * 4 | * This sketch shows how to save a picture on SPIFFS 5 | * filesystem by specifying the filename manually 6 | * 7 | * Open the Serial Monitor and enter 'capture' (without quotes) 8 | * to capture a new image and save it 9 | * 10 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 11 | * to turn on debug messages 12 | */ 13 | 14 | #include 15 | #include 16 | 17 | using namespace eloq; 18 | 19 | 20 | void setup() { 21 | delay(3000); 22 | Serial.begin(115200); 23 | Serial.println("___SAVE PIC TO SPIFFS___"); 24 | 25 | // camera settings 26 | // replace with your own model! 27 | camera.pinout.wroom_s3(); 28 | camera.brownout.disable(); 29 | camera.resolution.vga(); 30 | camera.quality.high(); 31 | 32 | // init camera 33 | while (!camera.begin().isOk()) 34 | Serial.println(camera.exception.toString()); 35 | 36 | // init SPIFFS 37 | while (!spiffs.begin().isOk()) 38 | Serial.println(spiffs.exception.toString()); 39 | 40 | Serial.println("Camera OK"); 41 | Serial.println("SPIFFS OK"); 42 | Serial.println("Enter the filename where to save the picture"); 43 | } 44 | 45 | 46 | void loop() { 47 | // await for filename from the Serial Monitor 48 | if (!Serial.available()) 49 | return; 50 | 51 | String filename = Serial.readStringUntil('\n'); 52 | filename.trim(); 53 | 54 | if (!filename.endsWith(".jpg") && !filename.endsWith(".jpeg")) 55 | filename = filename + ".jpg"; 56 | 57 | // capture picture 58 | if (!camera.capture().isOk()) { 59 | Serial.println(camera.exception.toString()); 60 | return; 61 | } 62 | 63 | // save to SPIFFS 64 | if (spiffs.save(camera.frame).to(filename).isOk()) { 65 | Serial.print("File written to "); 66 | Serial.println(spiffs.session.lastFilename); 67 | } 68 | else Serial.println(spiffs.session.exception.toString()); 69 | 70 | // you can also save under a nested folder 71 | if (spiffs.save(camera.frame).inside("myfolder").to(filename).isOk()) { 72 | Serial.print("File written to "); 73 | Serial.println(spiffs.session.lastFilename); 74 | } 75 | else Serial.println(spiffs.session.exception.toString()); 76 | 77 | // restart the loop 78 | Serial.println("Enter another filename"); 79 | } -------------------------------------------------------------------------------- /examples/Save_To_SPIFFS/NTP_Name/NTP_Name.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * SPIFFS file storage: NTP filename 3 | * 4 | * This sketch shows how to save a picture on SPIFFS 5 | * filesystem by generating a filename using current time 6 | * 7 | * Open the Serial Monitor and enter 'capture' (without quotes) 8 | * to capture a new image and save it 9 | * 10 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 11 | * to turn on debug messages 12 | */ 13 | 14 | // if you define WIFI_SSID and WIFI_PASS before importing the library 15 | // you can call wifi.connect() instead of wifi.connect(ssid, password) 16 | #define WIFI_SSID "SSID" 17 | #define WIFI_PASS "PASSWORD" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace eloq; 24 | 25 | 26 | void setup() { 27 | delay(3000); 28 | Serial.begin(115200); 29 | Serial.println("___SAVE PIC TO SPIFFS___"); 30 | 31 | // camera settings 32 | // replace with your own model! 33 | camera.pinout.wroom_s3(); 34 | camera.brownout.disable(); 35 | camera.resolution.vga(); 36 | camera.quality.high(); 37 | 38 | // if connected to the internet, try to get time from NTP 39 | // you can set your timezone offset from Greenwich 40 | ntp.offsetFromGreenwhich(0); 41 | // or any of 42 | ntp.cst(); 43 | ntp.ist(); 44 | ntp.eest(); 45 | ntp.cest(); 46 | ntp.bst(); 47 | ntp.west(); 48 | ntp.cet(); 49 | ntp.gmt(); 50 | ntp.edt(); 51 | ntp.pdt(); 52 | 53 | // enable/disable daylight saving 54 | ntp.isntDaylight(); 55 | ntp.isDaylight(); 56 | 57 | ntp.server("pool.ntp.org"); 58 | 59 | // init camera 60 | while (!camera.begin().isOk()) 61 | Serial.println(camera.exception.toString()); 62 | 63 | // init SPIFFS 64 | while (!spiffs.begin().isOk()) 65 | Serial.println(spiffs.exception.toString()); 66 | 67 | // connect to WiFi to sync NTP 68 | while (!wifi.connect().isOk()) 69 | Serial.println(wifi.exception.toString()); 70 | 71 | // get NTP time 72 | while (!ntp.begin().isOk()) 73 | Serial.println(ntp.exception.toString()); 74 | 75 | Serial.println("Camera OK"); 76 | Serial.println("SPIFFS OK"); 77 | Serial.println("NTP OK"); 78 | Serial.println("Enter 'capture' to capture a new picture and save to SPIFFS"); 79 | } 80 | 81 | 82 | void loop() { 83 | // await for "capture" from the Serial Monitor 84 | if (!Serial.available()) 85 | return; 86 | 87 | if (Serial.readStringUntil('\n') != "capture") { 88 | Serial.println("I only understand 'capture' (without quotes)"); 89 | return; 90 | } 91 | 92 | // capture picture 93 | if (!camera.capture().isOk()) { 94 | Serial.println(camera.exception.toString()); 95 | return; 96 | } 97 | 98 | // save under root directory 99 | if (spiffs.save(camera.frame).to(ntp.datetime(), "jpg").isOk()) { 100 | Serial.print("File written to "); 101 | Serial.println(spiffs.session.lastFilename); 102 | } 103 | else Serial.println(spiffs.session.exception.toString()); 104 | 105 | // save under nested directory 106 | if (spiffs.save(camera.frame).inside(ntp.date()).to(ntp.datetime(), "jpg").isOk()) { 107 | Serial.print("File written to "); 108 | Serial.println(spiffs.session.lastFilename); 109 | } 110 | else Serial.println(spiffs.session.exception.toString()); 111 | 112 | // restart the loop 113 | Serial.println("Enter another filename"); 114 | } -------------------------------------------------------------------------------- /examples/Take_Picture/Take_Picture.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Get your first picture with ESP32 3 | * 4 | * Open the Serial Monitor and enter 'capture' (without quotes) 5 | * to capture a new image 6 | * 7 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 8 | * to turn on debug messages 9 | */ 10 | #include 11 | 12 | // all global objects (e.g. `camera`) 13 | // are scoped under the `eloq` namespace 14 | using eloq::camera; 15 | 16 | 17 | /** 18 | * 19 | */ 20 | void setup() { 21 | delay(3000); 22 | Serial.begin(115200); 23 | Serial.println("___GET YOUR FIRST PICTURE___"); 24 | 25 | // camera settings 26 | // replace with your own model! 27 | // supported models: 28 | // - aithinker 29 | // - m5 30 | // - m5_wide 31 | // - m5_timer 32 | // - eye 33 | // - wrover 34 | // - wroom_s3 35 | // - freenove_s3 36 | // - xiao 37 | // - ttgo_lcd 38 | // - simcam 39 | camera.pinout.aithinker(); 40 | camera.brownout.disable(); 41 | // supported resolutions 42 | // - yolo (96x96) 43 | // - qqvga 44 | // - qcif 45 | // - face (240x240) 46 | // - qvga 47 | // - cif 48 | // - hvga 49 | // - vga 50 | // - svga 51 | // - xga 52 | // - hd 53 | // - sxga 54 | // - uxga 55 | // - fhd 56 | // - qxga 57 | // ... 58 | camera.resolution.vga(); 59 | // supported qualities: 60 | // - low 61 | // - high 62 | // - best 63 | camera.quality.high(); 64 | 65 | // init camera 66 | while (!camera.begin().isOk()) 67 | Serial.println(camera.exception.toString()); 68 | 69 | Serial.println("Camera OK"); 70 | Serial.println("Enter 'capture' (without quotes) to shot"); 71 | } 72 | 73 | /** 74 | * 75 | */ 76 | void loop() { 77 | // await for Serial command 78 | if (!Serial.available()) 79 | return; 80 | 81 | if (Serial.readStringUntil('\n') != "capture") { 82 | Serial.println("I only understand 'capture'"); 83 | return; 84 | } 85 | 86 | // capture picture 87 | if (!camera.capture().isOk()) { 88 | Serial.println(camera.exception.toString()); 89 | return; 90 | } 91 | 92 | // print image info 93 | Serial.printf( 94 | "JPEG size in bytes: %d. Width: %dpx. Height: %dpx.\n", 95 | camera.getSizeInBytes(), 96 | camera.resolution.getWidth(), 97 | camera.resolution.getHeight() 98 | ); 99 | 100 | Serial.println("Enter 'capture' (without quotes) to shot"); 101 | } -------------------------------------------------------------------------------- /examples/Telegram/Send_Image/Send_Image.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Send image from camera to Telegram 3 | * 4 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 5 | * to turn on debug messages 6 | */ 7 | // WiFi credentials 8 | #define WIFI_SSID "SSID" 9 | #define WIFI_PASS "PASSWORD" 10 | 11 | // replace with your bot token and chat id 12 | #define TELEGRAM_TOKEN "1234567890:AABBCCDDEEFFGGHHIILLMMN-NOOPPQQRRSS" 13 | #define TELEGRAM_CHAT "0123456789" 14 | 15 | #include 16 | #include 17 | 18 | using eloq::camera; 19 | using eloq::wifi; 20 | using eloq::telegram; 21 | 22 | 23 | /** 24 | * 25 | */ 26 | void setup() { 27 | delay(3000); 28 | Serial.begin(115200); 29 | Serial.println("___TELEGRAM IMAGE___"); 30 | 31 | // camera settings 32 | // replace with your own model! 33 | camera.pinout.aithinker(); 34 | camera.brownout.disable(); 35 | camera.resolution.vga(); 36 | camera.quality.high(); 37 | 38 | // init camera 39 | while (!camera.begin().isOk()) 40 | Serial.println(camera.exception.toString()); 41 | 42 | // connect to WiFi 43 | while (!wifi.connect().isOk()) 44 | Serial.println(wifi.exception.toString()); 45 | 46 | // connect to Telegram API 47 | while (!telegram.begin().isOk()) 48 | Serial.println(telegram.exception.toString()); 49 | 50 | Serial.println("Camera OK"); 51 | Serial.println("Telegram OK"); 52 | Serial.println("Enter 'image' (without quotes) to send image to Telegram chat"); 53 | } 54 | 55 | 56 | void loop() { 57 | // await for command 58 | if (!Serial.available()) 59 | return; 60 | 61 | if (Serial.readStringUntil('\n') != "image") { 62 | Serial.println("I only understand 'image' (without quotes)"); 63 | return; 64 | } 65 | 66 | Serial.println("Sending photo..."); 67 | 68 | // capture new frame 69 | if (!camera.capture().isOk()) { 70 | Serial.println(camera.exception.toString()); 71 | return; 72 | } 73 | 74 | // send 75 | if (telegram.to(TELEGRAM_CHAT).send(camera.frame).isOk()) 76 | Serial.println("Photo sent to Telegram"); 77 | else 78 | Serial.println(telegram.exception.toString()); 79 | 80 | Serial.println("Enter 'image' (without quotes) to send image to Telegram chat"); 81 | } -------------------------------------------------------------------------------- /examples/Telegram/Send_Text/Send_Text.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Send text to Telegram 3 | * 4 | * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO" 5 | * to turn on debug messages 6 | */ 7 | // WiFi credentials 8 | #define WIFI_SSID "SSID" 9 | #define WIFI_PASS "PASSWORD" 10 | 11 | // replace with your bot token and chat id 12 | #define TELEGRAM_TOKEN "1234567890:AABBCCDDEEFFGGHHIILLMMN-NOOPPQQRRSS" 13 | #define TELEGRAM_CHAT "0123456789" 14 | 15 | #include 16 | #include 17 | 18 | using eloq::wifi; 19 | using eloq::telegram; 20 | 21 | 22 | /** 23 | * 24 | */ 25 | void setup() { 26 | delay(3000); 27 | Serial.begin(115200); 28 | Serial.println("___TELEGRAM TEXT MESSAGE___"); 29 | 30 | // connect to WiFi 31 | while (!wifi.connect().isOk()) 32 | Serial.println(wifi.exception.toString()); 33 | 34 | // connect to Telegram API 35 | while (!telegram.begin().isOk()) 36 | Serial.println(telegram.exception.toString()); 37 | 38 | Serial.println("Telegram OK"); 39 | Serial.println("Enter the text you want to send to Telegram chat"); 40 | } 41 | 42 | 43 | void loop() { 44 | // await for text 45 | if (!Serial.available()) 46 | return; 47 | 48 | String text = Serial.readStringUntil('\n'); 49 | 50 | Serial.print("Sending text: "); 51 | Serial.println(text); 52 | 53 | // send 54 | if (telegram.to(TELEGRAM_CHAT).send(text).isOk()) 55 | Serial.println("Message sent to Telegram!"); 56 | else 57 | Serial.println(telegram.exception.toString()); 58 | 59 | Serial.println("Enter the text you want to send to Telegram chat"); 60 | } -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EloquentEsp32cam", 3 | "keywords": "", 4 | "description": "", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/eloquentarduino/EloquentEsp32cam" 8 | }, 9 | "version": "2.7.15", 10 | "authors": { 11 | "name": "Simone Salerno", 12 | "url": "https://github.com/eloquentarduino" 13 | }, 14 | "frameworks": "arduino", 15 | "platforms": "*" 16 | } 17 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=EloquentEsp32cam 2 | version=2.7.15 3 | author=Simone Salerno 4 | maintainer=Simone Salerno 5 | sentence=Use your Esp32-cam like an expert 6 | paragraph=Follow the project at eloquentarduino.com for details 7 | category=Other 8 | url=https://github.com/eloquentarduino/EloquentEsp32cam 9 | architectures=* 10 | includes=eloquent_esp32cam.h -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/src/.DS_Store -------------------------------------------------------------------------------- /src/eloquent_esp32cam.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM 2 | #define ELOQUENT_ESP32CAM 3 | #define MJPEG_HTTP_PORT 81 4 | #define LOG_HELP(x) ESP_LOGD("HELP", x) 5 | 6 | #include "./eloquent_esp32cam/camera/camera.h" 7 | 8 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/src/eloquent_esp32cam/.DS_Store -------------------------------------------------------------------------------- /src/eloquent_esp32cam/assets/ImageBrowserServer.alpine.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | #define IS_ALPINE_SPA 1 4 | 5 | static const char* ALPINE_SPA PROGMEM = R"=====( 6 | 7 | 8 |
NoFilenameFilesize
9 | 10 | )====="; 11 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/assets/face_detection.html.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Eloquent { namespace Assets { namespace Gz { namespace Html { static const struct { const char mime[32]; const uint16_t length; const uint8_t data[1329]; } FACE_DETECTION = {"text/html", 1329, {31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 109, 82, 193, 74, 196, 48, 16, 189, 251, 21, 33, 103, 219, 40, 94, 68, 218, 197, 131, 174, 32, 8, 130, 122, 150, 49, 153, 221, 78, 73, 210, 146, 25, 218, 245, 239, 205, 182, 202, 150, 173, 57, 101, 152, 151, 55, 239, 189, 73, 213, 72, 240, 155, 11, 149, 79, 213, 32, 184, 249, 58, 149, 66, 226, 113, 179, 5, 139, 234, 1, 5, 173, 80, 23, 213, 155, 36, 132, 80, 153, 185, 121, 2, 7, 20, 80, 182, 129, 196, 40, 181, 254, 120, 223, 22, 183, 250, 188, 29, 33, 96, 173, 7, 194, 177, 239, 146, 104, 101, 187, 40, 24, 51, 124, 36, 39, 77, 237, 112, 32, 139, 197, 84, 92, 42, 138, 36, 4, 190, 96, 11, 30, 235, 235, 242, 106, 73, 199, 54, 81, 47, 138, 147, 173, 117, 35, 210, 243, 157, 49, 214, 197, 82, 128, 252, 72, 209, 89, 230, 210, 118, 65, 111, 42, 51, 67, 127, 29, 154, 147, 197, 234, 171, 115, 223, 11, 74, 71, 131, 178, 30, 152, 107, 157, 208, 131, 208, 128, 139, 137, 19, 132, 194, 126, 30, 105, 66, 219, 227, 94, 43, 240, 89, 252, 203, 243, 235, 227, 147, 226, 41, 23, 173, 204, 130, 210, 100, 206, 181, 104, 135, 59, 76, 107, 233, 45, 59, 244, 52, 164, 50, 162, 152, 216, 7, 3, 190, 167, 136, 45, 223, 223, 148, 135, 242, 144, 185, 88, 38, 96, 160, 35, 248, 220, 218, 42, 22, 179, 203, 139, 251, 116, 127, 139, 251, 231, 73, 101, 230, 8, 114, 42, 199, 63, 240, 3, 71, 78, 147, 197, 10, 2, 0, 0}}; } } } } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/assets/mjpeg.html.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Eloquent { namespace Assets { namespace Gz { namespace Html { static const struct { const char mime[32]; const uint16_t length; const uint8_t data[1295]; } MJPEG = {"text/html", 1295, {31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 109, 145, 79, 75, 196, 48, 16, 197, 239, 126, 138, 144, 179, 77, 20, 47, 34, 237, 226, 69, 5, 97, 65, 80, 207, 50, 38, 179, 219, 41, 73, 90, 50, 67, 187, 126, 123, 251, 71, 216, 178, 53, 167, 76, 242, 203, 155, 55, 47, 101, 45, 49, 236, 174, 212, 184, 202, 26, 193, 47, 219, 185, 20, 146, 128, 187, 125, 211, 225, 81, 189, 75, 70, 136, 165, 93, 206, 206, 76, 68, 1, 229, 106, 200, 140, 82, 233, 207, 143, 231, 226, 94, 95, 94, 39, 136, 88, 233, 158, 112, 232, 218, 44, 90, 185, 54, 9, 166, 17, 31, 200, 75, 93, 121, 236, 201, 97, 49, 23, 215, 138, 18, 9, 65, 40, 216, 65, 192, 234, 214, 220, 172, 229, 216, 101, 234, 68, 113, 118, 149, 174, 69, 58, 126, 176, 214, 249, 100, 4, 40, 12, 148, 188, 99, 54, 174, 141, 122, 87, 218, 5, 253, 27, 204, 158, 39, 43, 191, 91, 255, 179, 146, 244, 212, 43, 23, 128, 185, 210, 25, 3, 8, 245, 184, 234, 56, 35, 20, 143, 75, 75, 27, 167, 44, 180, 130, 48, 154, 223, 191, 190, 61, 189, 40, 158, 115, 209, 202, 174, 36, 237, 168, 185, 53, 237, 241, 128, 121, 107, 189, 97, 143, 129, 250, 108, 18, 138, 77, 93, 180, 16, 58, 74, 216, 240, 227, 157, 57, 153, 211, 168, 197, 50, 131, 145, 38, 248, 114, 180, 77, 44, 246, 0, 14, 191, 60, 10, 58, 161, 246, 191, 39, 165, 93, 34, 24, 83, 153, 190, 254, 23, 4, 252, 117, 159, 1, 2, 0, 0}}; } } } } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/assets/mjpeg.js.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Eloquent { namespace Assets { namespace Gz { namespace Js { static const struct { const char mime[32]; const uint16_t length; const uint8_t data[640]; } MJPEG = {"text/javascript", 640, {31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 109, 142, 65, 10, 131, 48, 20, 68, 247, 57, 197, 184, 50, 201, 34, 255, 0, 98, 23, 93, 245, 16, 66, 72, 211, 32, 216, 180, 63, 36, 65, 11, 226, 221, 91, 139, 75, 223, 114, 30, 195, 12, 233, 63, 4, 41, 21, 250, 11, 86, 16, 97, 9, 247, 228, 252, 243, 202, 92, 75, 205, 46, 137, 217, 101, 88, 123, 196, 54, 124, 18, 231, 90, 172, 69, 143, 117, 235, 4, 233, 70, 159, 210, 232, 65, 0, 187, 132, 161, 146, 61, 189, 166, 20, 70, 51, 21, 236, 242, 167, 134, 243, 30, 9, 207, 239, 194, 49, 152, 200, 163, 108, 111, 33, 70, 198, 194, 57, 62, 90, 181, 207, 29, 151, 55, 37, 149, 232, 190, 158, 46, 9, 62, 194, 0, 0, 0}}; } } } } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/camera/brownout.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_BROWNOUT 2 | #define ELOQUENT_ESP32CAM_CAMERA_BROWNOUT 3 | 4 | 5 | #include "soc/soc.h" 6 | #include "soc/rtc_cntl_reg.h" 7 | 8 | 9 | namespace Eloquent { 10 | namespace Esp32cam { 11 | namespace Camera { 12 | /** 13 | * Disable brownout detector 14 | * (often triggers on AiThinker cam) 15 | */ 16 | class Brownout { 17 | public: 18 | /** 19 | * Disable detector 20 | */ 21 | void disable() { 22 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); 23 | } 24 | 25 | /** 26 | * Enable detector 27 | */ 28 | void enable() { 29 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); 30 | } 31 | }; 32 | } 33 | } 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/camera/pixformat.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_PIXFORMAT_H 2 | #define ELOQUENT_ESP32CAM_CAMERA_PIXFORMAT_H 3 | 4 | #include 5 | 6 | 7 | namespace Eloquent { 8 | namespace Esp32cam { 9 | namespace Camera { 10 | /** 11 | * 12 | */ 13 | class Pixformat { 14 | public: 15 | pixformat_t format; 16 | 17 | /** 18 | * 19 | */ 20 | Pixformat() : format(PIXFORMAT_JPEG) { 21 | 22 | } 23 | 24 | /** 25 | * Alias for grayscale 26 | */ 27 | void gray() { 28 | grayscale(); 29 | } 30 | 31 | /** 32 | * 33 | */ 34 | void grayscale() { 35 | format = PIXFORMAT_GRAYSCALE; 36 | } 37 | 38 | /** 39 | * 40 | */ 41 | void rgb565() { 42 | format = PIXFORMAT_RGB565; 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | void rgb888() { 49 | format = PIXFORMAT_RGB888; 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | void jpeg() { 56 | format = PIXFORMAT_JPEG; 57 | } 58 | 59 | /** 60 | * Test if grayscale 61 | */ 62 | inline bool isGray() { 63 | return is(PIXFORMAT_GRAYSCALE); 64 | } 65 | 66 | /** 67 | * Test if RGB565 68 | */ 69 | inline bool isRGB565() { 70 | return is(PIXFORMAT_RGB565); 71 | } 72 | 73 | /** 74 | * Test if RGB888 75 | */ 76 | inline bool isRGB888() { 77 | return is(PIXFORMAT_RGB888); 78 | } 79 | 80 | /** 81 | * Test if grayscale 82 | */ 83 | inline bool isJpeg() { 84 | return is(PIXFORMAT_JPEG); 85 | } 86 | 87 | /** 88 | * Test if given format 89 | */ 90 | inline bool is(pixformat_t fmt) { 91 | return format == fmt; 92 | } 93 | 94 | /** 95 | * Get bytes per pixel 96 | * @return 97 | */ 98 | inline uint8_t bpp() { 99 | return isGray() ? 1 : (isRGB888() ? 3 : 2); 100 | } 101 | }; 102 | } 103 | } 104 | } 105 | 106 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/camera/quality.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_JPEGQUALITY 2 | #define ELOQUENT_ESP32CAM_CAMERA_JPEGQUALITY 3 | 4 | 5 | namespace Eloquent { 6 | namespace Esp32cam { 7 | namespace Camera { 8 | /** 9 | * Configure quality of jpeg compression 10 | */ 11 | class JpegQuality { 12 | public: 13 | uint8_t quality; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | JpegQuality() { 19 | high(); 20 | } 21 | 22 | /** 23 | * Quality = 64 24 | */ 25 | void worst() { 26 | set(64); 27 | } 28 | 29 | /** 30 | * Quality = 30 31 | */ 32 | void low() { 33 | set(30); 34 | } 35 | 36 | /** 37 | * Quality = 20 38 | */ 39 | void high() { 40 | set(20); 41 | } 42 | 43 | /** 44 | * Quality = 10 45 | */ 46 | void best() { 47 | set(10); 48 | } 49 | 50 | /** 51 | * Set jpeg quality 52 | * 53 | * @param q 54 | */ 55 | void set(uint8_t q) { 56 | quality = constrain(q, 10, 64); 57 | } 58 | }; 59 | } 60 | } 61 | } 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/camera/rgb_565.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_CONVERTER 2 | #define ELOQUENT_ESP32CAM_CAMERA_CONVERTER 3 | 4 | #include "../extra/exception.h" 5 | 6 | using Eloquent::Error::Exception; 7 | 8 | 9 | namespace Eloquent { 10 | namespace Esp32cam { 11 | namespace Camera { 12 | /** 13 | * Convert frame between formats 14 | */ 15 | template 16 | class Converter565 { 17 | public: 18 | Camera *camera; 19 | Exception exception; 20 | uint16_t *data; 21 | size_t length; 22 | size_t width; 23 | size_t height; 24 | 25 | /** 26 | * 27 | */ 28 | Converter565(Camera *cam) : 29 | exception("RGB565Converter"), 30 | camera(cam), 31 | length(0), 32 | width(0), 33 | height(0) { 34 | } 35 | 36 | /** 37 | * Access data at given index 38 | */ 39 | uint16_t operator[](size_t i) { 40 | if (i >= length) 41 | return 0; 42 | 43 | if (data == NULL) 44 | return 0; 45 | 46 | return data[i]; 47 | } 48 | 49 | /** 50 | * Get RGB pixel at x, y 51 | */ 52 | inline uint16_t at(size_t x, size_t y) { 53 | return (*this)[y * width + x]; 54 | } 55 | 56 | /** 57 | * Get R or G or B pixel at x, y 58 | */ 59 | inline uint8_t at(size_t x, size_t y, uint8_t channel) { 60 | const uint16_t pixel = (*this)[y * width + x]; 61 | 62 | switch (channel) { 63 | case 0: 64 | return ((pixel >> 11) & 0x1F); 65 | case 1: 66 | return ((pixel >> 5) & 0x3F); 67 | case 2: 68 | return (pixel & 0x1F); 69 | default: 70 | ESP_LOGE("Converter565", "Channel %d is out of range (must be one of 0, 1 or 2)", channel); 71 | return 0; 72 | } 73 | } 74 | 75 | /** 76 | * 77 | */ 78 | uint32_t as888(size_t i) { 79 | uint32_t pixel = (*this)[i]; 80 | 81 | const int r = (pixel >> 11) & 0x1F; 82 | const int g = (pixel >> 5) & 0x3F; 83 | const int b = pixel & 0x1F; 84 | 85 | return (r << 19) | (g << 10) | (b << 3); 86 | } 87 | 88 | /** 89 | * Convert current frame to RGB565 format 90 | * Use 8X scale down 91 | */ 92 | Exception& convert() { 93 | if (!camera->hasFrame()) 94 | return exception.set("Can't convert empty frame to RGB565"); 95 | 96 | if (!width) { 97 | width = camera->resolution.getWidth() / 8; 98 | height = camera->resolution.getHeight() / 8; 99 | length = width * height; 100 | 101 | ESP_LOGI("Camera", "Allocating %d bytes for %dx%d RGB565 image", length * 2, width, height); 102 | data = (uint16_t*) ps_malloc(length * 2); 103 | } 104 | 105 | if (data == NULL) 106 | return exception.set("Cannot allocate memory"); 107 | 108 | camera->mutex.threadsafe([this]() { 109 | if (!jpg2rgb565(camera->frame->buf, camera->frame->len, (uint8_t*) data, JPG_SCALE_8X)) 110 | exception.set("Error converting frame from JPEG to RGB565"); 111 | }); 112 | 113 | if (!exception.isOk()) 114 | return exception; 115 | 116 | swapBytes(); 117 | 118 | return exception.clear(); 119 | } 120 | 121 | /** 122 | * With some boards, jpg2rgb565 swaps the bytes. 123 | * Use this function to revert the swap 124 | */ 125 | void swapBytes() { 126 | for (size_t i = 0; i < length; i++) { 127 | const uint16_t pixel = data[i]; 128 | const uint16_t h = (pixel >> 8); 129 | const uint16_t l = pixel & 0xFF; 130 | 131 | data[i] = (l << 8) | h; 132 | } 133 | } 134 | }; 135 | } 136 | } 137 | } 138 | 139 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/camera/xclk.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_XCLK 2 | #define ELOQUENT_ESP32CAM_CAMERA_XCLK 3 | 4 | 5 | namespace Eloquent { 6 | namespace Esp32cam { 7 | namespace Camera { 8 | /** 9 | * Configure camera XCLK freq 10 | */ 11 | class XCLK { 12 | public: 13 | size_t freq; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | XCLK() { 19 | fast(); 20 | } 21 | 22 | /** 23 | * Freq = 20 MHz 24 | */ 25 | void fast() { 26 | freq = 20000000ULL; 27 | } 28 | 29 | /** 30 | * Freq = 10 MHz 31 | */ 32 | void slow() { 33 | freq = 10000000ULL; 34 | } 35 | }; 36 | } 37 | } 38 | } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/car.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENTESP32CAM_CAR_H 2 | #define ELOQUENTESP32CAM_CAR_H 3 | 4 | #include "./car/fomo_driven_car.h" 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/car/fomo_driven_car.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENTESP32CAM_CAR_FOMO_DRIVEN_CAR_H 2 | #define ELOQUENTESP32CAM_CAR_FOMO_DRIVEN_CAR_H 3 | 4 | #ifndef EI_CLASSIFIER_INPUT_WIDTH 5 | #error "You must include a Edge Impulse library" 6 | #else 7 | 8 | #include "../edgeimpulse/fomo.h" 9 | #include "./two_wheels_car.h" 10 | 11 | using eloq::ei::bbox_t; 12 | using eloq::ei::fomo; 13 | using Eloquent::Esp32cam::EdgeImpulse::FOMO; 14 | 15 | 16 | namespace Eloquent { 17 | namespace Esp32cam { 18 | namespace Car { 19 | /** 20 | * Autonomous car that follows a bounding box 21 | */ 22 | class FomoDrivenCar : public TwoWheelsCar { 23 | public: 24 | 25 | /** 26 | * Constructor 27 | * @param leftMotor 28 | * @param rightMotor 29 | */ 30 | FomoDrivenCar(Motor& leftMotor, Motor& rightMotor) : 31 | TwoWheelsCar(leftMotor, rightMotor), 32 | _reversed(false) { 33 | 34 | _exploration.last = 0; 35 | _exploration.state = 0; 36 | } 37 | 38 | /** 39 | * Allows to accomodate camera rotation 40 | * (horizontal, top-down) 41 | */ 42 | void rotate(uint16_t angle) { 43 | if (angle != 0 && angle != 90 && angle != 180 && angle != 270) { 44 | ESP_LOGE("FomoDrivenCar", "Invalid rotation: %d. Only 0, 90, 180, 270 are allowed", angle); 45 | return; 46 | } 47 | 48 | _rotation = angle; 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | void reverse(bool reverse = true) { 55 | _reversed = reverse; 56 | } 57 | 58 | /** 59 | * 60 | * @param fomo 61 | */ 62 | void follow(FOMO& fomo) { 63 | if (!fomo.found()) { 64 | ESP_LOGD("FomoDrivenCar", "No object found"); 65 | _explore(); 66 | return; 67 | } 68 | 69 | uint16_t cx = 0; 70 | uint16_t dimension = 0; 71 | 72 | switch (_rotation) { 73 | case 0: 74 | cx = fomo.first.x + fomo.first.width / 2; 75 | dimension = EI_CLASSIFIER_INPUT_WIDTH; 76 | break; 77 | case 180: 78 | cx = EI_CLASSIFIER_INPUT_WIDTH - fomo.first.x - fomo.first.width / 2; 79 | dimension = EI_CLASSIFIER_INPUT_WIDTH; 80 | break; 81 | case 90: 82 | // todo 83 | break; 84 | case 270: 85 | // todo 86 | break; 87 | } 88 | 89 | ESP_LOGD("FomoDrivenCar", "bbox.x=%d, bbox.w=%d, cx=%d, dimension=%d", fomo.first.x, fomo.first.width, cx, dimension); 90 | 91 | const float band = dimension / 6; 92 | 93 | if (cx <= band * 2) 94 | _reversed ? left() : right(); 95 | else if (cx >= band * 4) 96 | _reversed ? right() : left(); 97 | 98 | forward(_defaultDuration * 2); 99 | 100 | _exploration.last = millis(); 101 | _exploration.state = 0; 102 | } 103 | 104 | protected: 105 | bool _reversed; 106 | uint16_t _rotation = 0; 107 | struct { 108 | size_t last; 109 | uint8_t state; 110 | } _exploration; 111 | 112 | /** 113 | * If no object is detected, look around 114 | */ 115 | void _explore() { 116 | // explore only if no object is detected for X seconds 117 | if (millis() - _exploration.last < 2000) 118 | return; 119 | 120 | switch (_exploration.state) { 121 | case 0: 122 | left(50); 123 | break; 124 | case 1: 125 | right(100); 126 | break; 127 | case 2: 128 | left(150); 129 | break; 130 | case 3: 131 | right(200); 132 | break; 133 | case 4: 134 | left(250); 135 | break; 136 | case 5: 137 | right(300); 138 | break; 139 | } 140 | 141 | _exploration.last = millis() - 1000; 142 | _exploration.state = (_exploration.state + 1) % 6; 143 | } 144 | }; 145 | } 146 | } 147 | } 148 | 149 | namespace eloq { 150 | namespace car { 151 | using Car = Eloquent::Esp32cam::Car::FomoDrivenCar; 152 | } 153 | } 154 | 155 | #endif 156 | #endif 157 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/car/motor.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENTESP32CAM_CAR_MOTOR_H 2 | #define ELOQUENTESP32CAM_CAR_MOTOR_H 3 | 4 | 5 | namespace Eloquent { 6 | namespace Esp32cam { 7 | namespace Car { 8 | /** 9 | * Control a single motor 10 | */ 11 | class Motor { 12 | public: 13 | /** 14 | * Constructor 15 | * @param pin1 16 | * @param pin2 17 | */ 18 | Motor(uint8_t pin1, uint8_t pin2) : 19 | _a(pin1), 20 | _b(pin2), 21 | _positive(true), 22 | _pretend(false) { 23 | 24 | } 25 | 26 | /** 27 | * Don't actually actuate the motor 28 | * (debug) 29 | * @param pretend 30 | */ 31 | void pretend(bool pretend = true) { 32 | _pretend = pretend; 33 | } 34 | 35 | /** 36 | * Reverse motor polarity 37 | */ 38 | void reverse() { 39 | _positive = !_positive; 40 | } 41 | 42 | /** 43 | * Move forward 44 | */ 45 | void forward() { 46 | write(_positive, !_positive); 47 | } 48 | 49 | /** 50 | * Move backward 51 | */ 52 | void backward() { 53 | write(!_positive, _positive); 54 | } 55 | 56 | /** 57 | * Stop 58 | */ 59 | void stop() { 60 | write(LOW, LOW); 61 | } 62 | 63 | protected: 64 | uint8_t _a; 65 | uint8_t _b; 66 | bool _positive; 67 | bool _pretend; 68 | 69 | /** 70 | * 71 | * @param dirA 72 | * @param dirB 73 | */ 74 | void write(bool dirA, bool dirB) { 75 | ESP_LOGV("Motor", "write(%d=%d, %d=%d)%s", _a, dirA, _b, dirB, _pretend ? " (pretended)" : ""); 76 | 77 | if (!_pretend) { 78 | pinMode(_a, OUTPUT); 79 | pinMode(_b, OUTPUT); 80 | digitalWrite(_a, dirA); 81 | digitalWrite(_b, dirB); 82 | } 83 | } 84 | }; 85 | } 86 | } 87 | } 88 | 89 | namespace eloq { 90 | namespace car { 91 | using Motor = Eloquent::Esp32cam::Car::Motor; 92 | } 93 | } 94 | 95 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/car/two_wheels_car.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENTESP32CAM_CAR_CAR_H 2 | #define ELOQUENTESP32CAM_CAR_CAR_H 3 | 4 | #include "./motor.h" 5 | 6 | 7 | namespace Eloquent { 8 | namespace Esp32cam { 9 | namespace Car { 10 | /** 11 | * Control a 2-wheels car 12 | */ 13 | class TwoWheelsCar { 14 | public: 15 | /** 16 | * Constructor 17 | * @param leftMotor 18 | * @param rightMotor 19 | */ 20 | TwoWheelsCar(Motor& leftMotor, Motor& rightMotor) : 21 | _left(&leftMotor), 22 | _right(&rightMotor), 23 | _defaultDuration(100) { 24 | 25 | } 26 | 27 | /** 28 | * Don't actually actuate the motors 29 | * (debug) 30 | * @param pretend 31 | */ 32 | void pretend(bool pretend = true) { 33 | _left->pretend(pretend); 34 | _right->pretend(pretend); 35 | } 36 | 37 | /** 38 | * How long should a motor action last 39 | * by default? 40 | * @param duration 41 | */ 42 | void defaultDuration(uint32_t duration) { 43 | if (duration > 0) 44 | _defaultDuration = duration; 45 | } 46 | 47 | /** 48 | * Move forward 49 | * @param duration 50 | */ 51 | void forward(uint32_t duration = 0) { 52 | withTimeout([this]() { 53 | ESP_LOGD("Car", "forward()"); 54 | _left->forward(); 55 | _right->forward(); 56 | }, duration); 57 | } 58 | 59 | /** 60 | * Move forward forever 61 | */ 62 | void forwardForever() { 63 | forward(0xFFFFFFFF); 64 | } 65 | 66 | /** 67 | * Move backward 68 | * @param duration 69 | */ 70 | void backward(uint32_t duration = 0) { 71 | withTimeout([this]() { 72 | ESP_LOGD("Car", "backward()"); 73 | _left->backward(); 74 | _right->backward(); 75 | }, duration); 76 | } 77 | 78 | /** 79 | * Move backward forever 80 | */ 81 | void backwardForever() { 82 | backward(0xFFFFFFFF); 83 | } 84 | 85 | /** 86 | * Turn left 87 | * @param duration 88 | */ 89 | void left(uint32_t duration = 0) { 90 | withTimeout([this]() { 91 | ESP_LOGD("Car", "left()"); 92 | _left->backward(); 93 | _right->forward(); 94 | }, duration); 95 | } 96 | 97 | /** 98 | * Turn left forever 99 | */ 100 | void leftForever() { 101 | left(0xFFFFFFFF); 102 | } 103 | 104 | /** 105 | * Turn right 106 | * @param duration 107 | */ 108 | void right(uint32_t duration = 0) { 109 | withTimeout([this]() { 110 | ESP_LOGD("Car", "right()"); 111 | _left->forward(); 112 | _right->backward(); 113 | }, duration); 114 | } 115 | 116 | /** 117 | * Turn right forever 118 | */ 119 | void rightForever() { 120 | right(0xFFFFFFFF); 121 | } 122 | 123 | /** 124 | * Stop 125 | * @param duration 126 | */ 127 | void stop(uint32_t duration = 0) { 128 | _left->stop(); 129 | _right->stop(); 130 | } 131 | 132 | protected: 133 | uint32_t _defaultDuration; 134 | Motor *_left, *_right; 135 | 136 | /** 137 | * Actuate motors for given time, then stop 138 | * @tparam Callback 139 | * @param callback 140 | * @param duration 141 | */ 142 | template 143 | void withTimeout(Callback callback, uint32_t duration) { 144 | callback(); 145 | delay(duration ? duration : _defaultDuration); 146 | stop(); 147 | } 148 | }; 149 | } 150 | } 151 | } 152 | 153 | namespace eloq { 154 | namespace car { 155 | using TwoWheelsCar = Eloquent::Esp32cam::Car::TwoWheelsCar; 156 | } 157 | } 158 | 159 | 160 | #endif 161 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/edgeimpulse/bbox.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_EDGEIMPULSE_BBOX_H 2 | #define ELOQUENT_ESP32CAM_EDGEIMPULSE_BBOX_H 3 | 4 | namespace eloq { 5 | namespace ei { 6 | class bbox_t { 7 | public: 8 | String label; 9 | float proba; 10 | uint16_t x; 11 | uint16_t y; 12 | uint16_t x1; 13 | uint16_t y1; 14 | uint16_t x2; 15 | uint16_t y2; 16 | uint16_t cx; 17 | uint16_t cy; 18 | uint16_t width; 19 | uint16_t height; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | bbox_t(String label_, float proba_, uint16_t x_, uint16_t y_, uint16_t width_, uint16_t height_) : 25 | label(label_), 26 | proba(proba_) { 27 | setDimensions(x_, y_, width_, height_); 28 | } 29 | 30 | /** 31 | * 32 | */ 33 | void setDimensions(uint16_t x_, uint16_t y_, uint16_t width_, uint16_t height_) { 34 | x = x1 = x_; 35 | y = y1 = y_; 36 | width = width_; 37 | height = height_; 38 | x2 = x1 + width; 39 | y2 = y1 + height; 40 | cx = (x1 + x2) / 2; 41 | cy = (y1 + y2) / 2; 42 | } 43 | }; 44 | } 45 | } 46 | 47 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/edgeimpulse/classifier.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_EDGEIMPULSE_CLASSIFIER_H 2 | #define ELOQUENT_ESP32CAM_EDGEIMPULSE_CLASSIFIER_H 3 | 4 | #include "../extra/exception.h" 5 | #include "../extra/time/benchmark.h" 6 | 7 | using Eloquent::Error::Exception; 8 | using Eloquent::Extra::Time::Benchmark; 9 | using ei::signal_t; 10 | 11 | 12 | namespace Eloquent { 13 | namespace Esp32cam { 14 | namespace EdgeImpulse { 15 | /** 16 | * Base class for Edge Impulse classifiers 17 | */ 18 | class Classifier { 19 | public: 20 | signal_t signal; 21 | ei_impulse_result_t result; 22 | EI_IMPULSE_ERROR error; 23 | Exception exception; 24 | Benchmark benchmark; 25 | struct { 26 | size_t dsp; 27 | size_t anomaly; 28 | size_t classification; 29 | size_t total; 30 | } timing; 31 | 32 | /** 33 | * 34 | */ 35 | Classifier(): 36 | exception("EI " EI_CLASSIFIER_PROJECT_NAME), 37 | _isDebugEnabled(false) { 38 | signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT; 39 | } 40 | 41 | /** 42 | * 43 | * @param enabled 44 | */ 45 | void debug(bool enabled = true) { 46 | _isDebugEnabled = enabled; 47 | } 48 | 49 | /** 50 | * Run model 51 | */ 52 | virtual Exception& run() { 53 | if (!beforeClassification()) 54 | return exception; 55 | 56 | benchmark.benchmark([this]() { 57 | error = run_classifier(&signal, &result, _isDebugEnabled); 58 | }); 59 | 60 | if (error != EI_IMPULSE_OK) 61 | return exception.set(String("Failed to run classifier with error code ") + error); 62 | 63 | afterClassification(); 64 | breakTiming(); 65 | 66 | return exception.clear(); 67 | } 68 | 69 | protected: 70 | bool _isDebugEnabled; 71 | 72 | /** 73 | * Run checks before classification. 74 | * If false is returned, don't run 75 | */ 76 | virtual bool beforeClassification() = 0; 77 | 78 | /** 79 | * Run actions after classification results 80 | */ 81 | virtual void afterClassification() = 0; 82 | 83 | /** 84 | * 85 | */ 86 | void breakTiming() { 87 | timing.dsp = result.timing.dsp; 88 | timing.classification = result.timing.classification; 89 | timing.anomaly = result.timing.anomaly; 90 | timing.total = timing.dsp + timing.classification + timing.anomaly; 91 | } 92 | }; 93 | } 94 | } 95 | } 96 | 97 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/edgeimpulse/fomo.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_EDGEIMPULSE_FOMO_H 2 | #define ELOQUENT_ESP32CAM_EDGEIMPULSE_FOMO_H 3 | 4 | #if defined(EI_CLASSIFIER_OBJECT_DETECTION) 5 | #include "./image.h" 6 | #include "./bbox.h" 7 | #include "./fomo_daemon.h" 8 | 9 | using eloq::ei::bbox_t; 10 | 11 | namespace Eloquent { 12 | namespace Esp32cam { 13 | namespace EdgeImpulse { 14 | /** 15 | * Run Edge Impulse FOMO model 16 | */ 17 | class FOMO : public ImageClassifier { 18 | public: 19 | bbox_t first; 20 | FOMODaemon daemon; 21 | 22 | /** 23 | * 24 | */ 25 | FOMO() : 26 | ImageClassifier(), 27 | first("", 0, 0, 0, 0, 0), 28 | daemon(this) { 29 | } 30 | 31 | /** 32 | * Check if objects were found 33 | */ 34 | bool found() { 35 | return result.bounding_boxes[0].value != 0; 36 | } 37 | 38 | /** 39 | * Check if objects were found 40 | */ 41 | bool foundAnyObject() { 42 | return found(); 43 | } 44 | 45 | /** 46 | * Run function on each bounding box found 47 | */ 48 | template 49 | void forEach(Callback callback) { 50 | for (size_t ix = 0, i = 0; ix < result.bounding_boxes_count; ix++) { 51 | auto bb = result.bounding_boxes[ix]; 52 | bbox_t bbox( 53 | bb.label, 54 | bb.value, 55 | bb.x, 56 | bb.y, 57 | bb.width, 58 | bb.height 59 | ); 60 | 61 | if (bbox.proba > 0) 62 | callback(i++, bbox); 63 | } 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | ei_impulse_result_bounding_box_t at(size_t ix) { 70 | return result.bounding_boxes[ix]; 71 | } 72 | 73 | /** 74 | * Get count of (non background) bounding boxes 75 | */ 76 | size_t count() { 77 | size_t count = 0; 78 | 79 | for (size_t ix = 0, i = 0; ix < result.bounding_boxes_count; ix++) { 80 | auto bb = result.bounding_boxes[ix]; 81 | 82 | if (bb.value != 0) 83 | count++; 84 | } 85 | 86 | return count; 87 | } 88 | 89 | /** 90 | * Convert to JSON string 91 | */ 92 | String toJSON() { 93 | static char buf[10 * 33] = {' '}; 94 | String json(buf); 95 | 96 | json = "[]"; 97 | 98 | if (!found()) 99 | return json; 100 | 101 | json = "["; 102 | 103 | forEach([&json](uint8_t i, bbox_t bbox) { 104 | if (i > 0) 105 | json += ','; 106 | 107 | json += '{'; 108 | json += "\"label\":\""; 109 | json += bbox.label; 110 | json += "\",\"proba\":"; 111 | json += bbox.proba; 112 | json += ",\"x\":"; 113 | json += bbox.x; 114 | json += ",\"y\":"; 115 | json += bbox.y; 116 | json += ",\"w\":"; 117 | json += bbox.width; 118 | json += ",\"h\":"; 119 | json += bbox.height; 120 | json += '}'; 121 | }); 122 | 123 | json += ']'; 124 | 125 | return json; 126 | } 127 | 128 | /** 129 | * Test if a MQTT payload is available 130 | */ 131 | bool shouldPub() { 132 | return found(); 133 | } 134 | 135 | protected: 136 | 137 | /** 138 | * Run actions after classification results 139 | */ 140 | virtual void afterClassification() { 141 | if (found()) { 142 | auto bb = result.bounding_boxes[0]; 143 | first.label = bb.label; 144 | first.proba = bb.value; 145 | first.setDimensions(bb.x, bb.y, bb.width, bb.height); 146 | } 147 | } 148 | }; 149 | } 150 | } 151 | } 152 | 153 | namespace eloq { 154 | namespace ei { 155 | static Eloquent::Esp32cam::EdgeImpulse::FOMO fomo; 156 | } 157 | } 158 | 159 | #else 160 | #error "EdgeImpulse FOMO library not found" 161 | #endif 162 | 163 | #endif //ELOQUENT_ESP32CAM_EDGEIMPULSE_FOMO 164 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/edgeimpulse/fomo_daemon.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_EDGEIMPULSE_FOMO_DAEMON_H 2 | #define ELOQUENT_ESP32CAM_EDGEIMPULSE_FOMO_DAEMON_H 1 3 | 4 | #include 5 | #include "../camera/camera.h" 6 | #include "../extra/esp32/multiprocessing/thread.h" 7 | #include "./bbox.h" 8 | 9 | using eloq::camera; 10 | using eloq::ei::bbox_t; 11 | using Eloquent::Extra::Esp32::Multiprocessing::Thread; 12 | using OnObjectCallback = std::function; 13 | using OnNothingCallback = std::function; 14 | 15 | 16 | namespace Eloquent { 17 | namespace Esp32cam { 18 | namespace EdgeImpulse { 19 | /** 20 | * Run FOMO in background 21 | * 22 | * @class FOMODaemon 23 | * @author Simone 24 | * @date 13/12/2023 25 | * @file fomo_daemon.h 26 | * @brief 27 | */ 28 | template 29 | class FOMODaemon { 30 | public: 31 | Thread thread; 32 | 33 | /** 34 | * @brief Constructor 35 | * @param fomo 36 | */ 37 | FOMODaemon(T *fomo) : 38 | thread("FOMO"), 39 | _fomo(fomo), 40 | _numListeners(0) { 41 | 42 | } 43 | 44 | /** 45 | * @brief Run function when an object is detected 46 | * @param callback 47 | */ 48 | bool whenYouSeeAny(OnObjectCallback callback) { 49 | return whenYouSee("*", callback); 50 | } 51 | 52 | /** 53 | * @brief Run function when nothing is detected 54 | * @param callback 55 | */ 56 | void whenYouDontSeeAnything(OnNothingCallback callback) { 57 | _onNothing = callback; 58 | } 59 | 60 | /** 61 | * @brief Run function when a specific object is detected 62 | * @param label 63 | * @param callback 64 | */ 65 | bool whenYouSee(String label, OnObjectCallback callback) { 66 | if (_numListeners >= EI_CLASSIFIER_LABEL_COUNT + 1) { 67 | ESP_LOGE("FOMO daemon", "Max number of listeners reached"); 68 | return false; 69 | } 70 | 71 | _callbacks[_numListeners].label = label; 72 | _callbacks[_numListeners].callback = callback; 73 | _numListeners += 1; 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * Start FOMO in background 80 | * 81 | * @brief 82 | */ 83 | void start() { 84 | thread 85 | .withArgs((void*) this) 86 | .withStackSize(4000) 87 | // @see https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-guides/performance/speed.html 88 | .withPriority(17) 89 | .run([](void *args) { 90 | FOMODaemon *self = (FOMODaemon*) args; 91 | 92 | delay(3000); 93 | 94 | while (true) { 95 | yield(); 96 | delay(1); 97 | 98 | if (!camera.capture().isOk()) 99 | continue; 100 | 101 | if (!self->_fomo->run().isOk()) 102 | continue; 103 | 104 | if (!self->_fomo->foundAnyObject()) { 105 | if (self->_onNothing) 106 | self->_onNothing(); 107 | 108 | continue; 109 | } 110 | 111 | self->_fomo->forEach([&self](int i, bbox_t& bbox) { 112 | // run specific label callback 113 | for (uint8_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT + 1; i++) { 114 | String label = self->_callbacks[i].label; 115 | 116 | if (label == "*" || label == bbox.label) 117 | self->_callbacks[i].callback(i, bbox); 118 | } 119 | }); 120 | } 121 | }); 122 | } 123 | 124 | protected: 125 | T *_fomo; 126 | uint8_t _numListeners; 127 | OnNothingCallback _onNothing; 128 | struct { 129 | String label; 130 | OnObjectCallback callback; 131 | } _callbacks[EI_CLASSIFIER_LABEL_COUNT + 1]; 132 | 133 | }; 134 | } 135 | } 136 | } 137 | 138 | 139 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/src/eloquent_esp32cam/extra/.DS_Store -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/car/car2wd.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_CAR_CAR2WD_H 2 | #define ELOQUENT_EXTRA_CAR_CAR2WD_H 3 | 4 | #include "./motor.h" 5 | 6 | 7 | namespace Eloquent { 8 | namespace Extra { 9 | namespace Car { 10 | class Car2WD { 11 | public: 12 | /** 13 | * 14 | */ 15 | Car2WD(Motor& left, Motor& right) : 16 | _left(left), 17 | _right(right) { 18 | 19 | } 20 | 21 | /** 22 | * 23 | */ 24 | void begin() { 25 | _left.begin(); 26 | _right.begin(); 27 | } 28 | 29 | /** 30 | * 31 | */ 32 | void forward() { 33 | _left.forward(); 34 | _right.forward(); 35 | } 36 | 37 | /** 38 | * 39 | */ 40 | void backward() { 41 | _left.backward(); 42 | _right.backward(); 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | void stop() { 49 | _left.stop(); 50 | _right.stop(); 51 | } 52 | 53 | /** 54 | * 55 | */ 56 | void left() { 57 | _left.backward(); 58 | _right.forward(); 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | void right() { 65 | _left.forward(); 66 | _right.backward(); 67 | } 68 | 69 | protected: 70 | Motor _left; 71 | Motor _right; 72 | }; 73 | } 74 | } 75 | } 76 | 77 | 78 | // sorter class 79 | namespace eloq { 80 | typedef Eloquent::Extra::Car::Car2WD car_t; 81 | } 82 | 83 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/car/motor.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_CAR_MOTOR_H 2 | #define ELOQUENT_EXTRA_CAR_MOTOR_H 3 | 4 | namespace Eloquent { 5 | namespace Extra { 6 | namespace Car { 7 | class Motor { 8 | public: 9 | /** 10 | * 11 | */ 12 | Motor(uint8_t fwd, uint8_t rev) : 13 | _fwd(fwd), 14 | _rev(rev) { 15 | 16 | } 17 | 18 | /** 19 | * 20 | */ 21 | void begin() { 22 | pinMode(_fwd, OUTPUT); 23 | pinMode(_rev, OUTPUT); 24 | } 25 | 26 | /** 27 | * 28 | */ 29 | void forward() { 30 | digitalWrite(_fwd, 1); 31 | digitalWrite(_rev, 0); 32 | } 33 | 34 | /** 35 | * 36 | */ 37 | void backward() { 38 | digitalWrite(_fwd, 0); 39 | digitalWrite(_rev, 1); 40 | } 41 | 42 | /** 43 | * 44 | */ 45 | void stop() { 46 | digitalWrite(_fwd, 0); 47 | digitalWrite(_rev, 0); 48 | } 49 | 50 | protected: 51 | uint8_t _fwd; 52 | uint8_t _rev; 53 | }; 54 | } 55 | } 56 | } 57 | 58 | // shorter class 59 | namespace eloq { 60 | typedef Eloquent::Extra::Car::Motor motor_t; 61 | } 62 | 63 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/fs/fs.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_FS_H 2 | #define ELOQUENT_EXTRA_ESP32_FS_H 3 | 4 | #include "../../exception.h" 5 | #include "./write_session.h" 6 | 7 | using Eloquent::Error::Exception; 8 | 9 | 10 | namespace Eloquent { 11 | namespace Extra { 12 | namespace Esp32 { 13 | namespace Fs { 14 | /** 15 | * Abstract filesystem 16 | */ 17 | class FileSystem { 18 | public: 19 | Exception exception; 20 | WriteSession session; 21 | String root; 22 | 23 | /** 24 | * 25 | */ 26 | FileSystem(const char *name, fs::FS *concrete, String root_) : 27 | exception(name), 28 | session(concrete), 29 | _fs(concrete), 30 | root(root_) { 31 | 32 | } 33 | 34 | /** 35 | * Get concrete filesystem implementation 36 | */ 37 | fs::FS* fs() { 38 | return _fs; 39 | } 40 | 41 | /** 42 | * 43 | */ 44 | void mountAt(String mountpoint) { 45 | root = mountpoint; 46 | } 47 | 48 | /** 49 | * Write string content 50 | */ 51 | WriteSession& save(const char *content) { 52 | session.open(content); 53 | 54 | return session; 55 | } 56 | 57 | /** 58 | * Write string content 59 | */ 60 | WriteSession& save(String& content) { 61 | return save(content.c_str()); 62 | } 63 | 64 | /** 65 | * See save(data, length) 66 | */ 67 | template 68 | WriteSession& save(BinaryContent& content) { 69 | return save(content->buf, content->len); 70 | } 71 | 72 | /** 73 | * Write binary content 74 | */ 75 | WriteSession& save(uint8_t *data, size_t length) { 76 | session.open(data, length); 77 | 78 | return session; 79 | } 80 | 81 | /** 82 | * Iterate over files 83 | */ 84 | template 85 | void forEach(Callback callback, size_t offset = 0, size_t limit = 0) { 86 | size_t i = 0; 87 | File root = _fs->open("/"); 88 | 89 | if (!root) { 90 | ESP_LOGE("FS", "Cannot open root"); 91 | return; 92 | } 93 | 94 | if (!root.isDirectory()) { 95 | ESP_LOGE("FS", "Root is not a directory"); 96 | return; 97 | } 98 | 99 | root.rewindDirectory(); 100 | File file = root.openNextFile(); 101 | 102 | if (!file) 103 | ESP_LOGW("FS", "No file found"); 104 | 105 | while (file) { 106 | ESP_LOGI("FS", "%d: %s", i, file.name()); 107 | 108 | // seek to required position 109 | if (i++ < offset) { 110 | file = root.openNextFile(); 111 | continue; 112 | } 113 | 114 | // break after reaching page limit 115 | if (i >= offset + limit) 116 | break; 117 | 118 | callback(file.name(), file.size(), file.isDirectory()); 119 | file = root.openNextFile(); 120 | } 121 | 122 | root.close(); 123 | } 124 | 125 | protected: 126 | fs::FS *_fs; 127 | 128 | }; 129 | } 130 | } 131 | } 132 | } 133 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/fs/sd.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_SD 2 | #define ELOQUENT_ESP32CAM_CAMERA_SD 3 | 4 | #include 5 | #include 6 | #include "../../exception.h" 7 | #include "./write_session.h" 8 | 9 | 10 | using Eloquent::Error::Exception; 11 | using namespace eloq; 12 | 13 | 14 | namespace Eloquent { 15 | namespace Extra { 16 | namespace Esp32 { 17 | namespace Fs { 18 | /** 19 | * Interact with the SD 20 | */ 21 | class SPISD { 22 | public: 23 | Exception exception; 24 | WriteSession session; 25 | 26 | /** 27 | * 28 | */ 29 | SPISD() : 30 | exception("SD"), 31 | session(&SD), 32 | _cs(255) { 33 | } 34 | 35 | /** 36 | * 37 | */ 38 | fs::FS& fs() { 39 | return SD; 40 | } 41 | 42 | /** 43 | * 44 | */ 45 | void cs(uint8_t cs) { 46 | _cs = cs; 47 | } 48 | 49 | /** 50 | * Init SD 51 | */ 52 | Exception& begin() { 53 | if (_cs == 255) 54 | return exception.set("You must set a CS pin"); 55 | 56 | if (!SD.begin(_cs)) 57 | return exception.set("Something went wrong with SD.begin()"); 58 | 59 | if (SD.cardType() == CARD_NONE) 60 | return exception.set("Cannot detect any SD card"); 61 | 62 | return exception.clear(); 63 | } 64 | 65 | /** 66 | * See save(data, length) 67 | */ 68 | template 69 | WriteSession& save(BinaryContent& content) { 70 | return save(content->buf, content->len); 71 | } 72 | 73 | /** 74 | * Write binary content 75 | */ 76 | WriteSession& save(uint8_t *data, size_t length) { 77 | session.open(data, length); 78 | 79 | return session; 80 | } 81 | 82 | protected: 83 | uint8_t _cs; 84 | }; 85 | } 86 | } 87 | } 88 | } 89 | 90 | namespace eloq { 91 | static Eloquent::Extra::Esp32::Fs::SPISD sd; 92 | } 93 | 94 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/fs/sdmmc.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_SDMMC 2 | #define ELOQUENT_ESP32CAM_CAMERA_SDMMC 3 | 4 | #include 5 | #include 6 | #include "./fs.h" 7 | #include "./sdmmc_pins.h" 8 | 9 | 10 | using namespace eloq; 11 | 12 | 13 | namespace Eloquent { 14 | namespace Extra { 15 | namespace Esp32 { 16 | namespace Fs { 17 | /** 18 | * Interact with the SD 19 | */ 20 | class SDMMC : public FileSystem { 21 | public: 22 | Pinout pinout; 23 | 24 | /** 25 | * 26 | */ 27 | SDMMC() : 28 | FileSystem("SDMMC", &SD_MMC, "/sdcard"), 29 | freq(SDMMC_FREQ_DEFAULT), 30 | maxFiles(5) { 31 | } 32 | 33 | /** 34 | * Set high read/write speed 35 | */ 36 | void highSpeed() { 37 | speed(SDMMC_FREQ_HIGHSPEED); 38 | } 39 | 40 | /** 41 | * Set custom speed 42 | */ 43 | void speed(int speed) { 44 | freq = speed; 45 | } 46 | 47 | /** 48 | * Set max number of open files 49 | */ 50 | void maxOpenFiles(uint8_t numMaxFiles) { 51 | maxFiles = numMaxFiles; 52 | } 53 | 54 | /** 55 | * Init SD 56 | */ 57 | Exception& begin() { 58 | bool isOneBit = (pinout.pins.d1 == 0 && pinout.pins.d2 == 0); 59 | bool formatOnFailure = true; 60 | 61 | if (pinout.pins.clk != pinout.pins.cmd) 62 | SD_MMC.setPins(pinout.pins.clk, pinout.pins.cmd, pinout.pins.d0); 63 | 64 | if (!isOneBit) 65 | SD_MMC.setPins(pinout.pins.clk, pinout.pins.cmd, pinout.pins.d0, pinout.pins.d1, pinout.pins.d2, pinout.pins.d3); 66 | 67 | if (!SD_MMC.begin(root.c_str(), isOneBit, formatOnFailure, freq, maxFiles)) 68 | return exception.set("Something went wrong with SD_MMC.begin()"); 69 | 70 | if (SD_MMC.cardType() == CARD_NONE) 71 | return exception.set("Cannot detect any SD card"); 72 | 73 | return exception.clear(); 74 | } 75 | 76 | protected: 77 | int freq; 78 | uint8_t maxFiles; 79 | }; 80 | } 81 | } 82 | } 83 | } 84 | 85 | namespace eloq { 86 | static Eloquent::Extra::Esp32::Fs::SDMMC sdmmc; 87 | } 88 | 89 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/fs/sdmmc_pins.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_FS_SDMMC_PINS 2 | #define ELOQUENT_EXTRA_ESP32_FS_SDMMC_PINS 3 | 4 | namespace Eloquent { 5 | namespace Extra { 6 | namespace Esp32 { 7 | namespace Fs { 8 | /** 9 | * Define SDMMC pins for common boards 10 | */ 11 | class Pinout { 12 | public: 13 | struct { 14 | uint8_t clk; 15 | uint8_t cmd; 16 | uint8_t d0; 17 | uint8_t d1; 18 | uint8_t d2; 19 | uint8_t d3; 20 | } pins; 21 | 22 | /** 23 | * 24 | */ 25 | Pinout() { 26 | pins.clk = 0; 27 | pins.cmd = 0; 28 | pins.d0 = 0; 29 | pins.d1 = 0; 30 | pins.d2 = 0; 31 | pins.d3 = 0; 32 | } 33 | 34 | /** 35 | * 36 | */ 37 | void freenove_s3() { 38 | clk(39); 39 | cmd(38); 40 | d0(40); 41 | } 42 | 43 | /** 44 | * Set CLK pin 45 | */ 46 | void clk(uint8_t clk) { 47 | pins.clk = clk; 48 | } 49 | 50 | /** 51 | * Set CMD pin 52 | */ 53 | void cmd(uint8_t cmd) { 54 | pins.cmd = cmd; 55 | } 56 | 57 | /** 58 | * Set D0 pin 59 | */ 60 | void d0(uint8_t d0) { 61 | pins.d0 = d0; 62 | } 63 | 64 | /** 65 | * Set D1 pin 66 | */ 67 | void d1(uint8_t d1) { 68 | pins.d1 = d1; 69 | } 70 | 71 | /** 72 | * Set D2 pin 73 | */ 74 | void d2(uint8_t d2) { 75 | pins.d2 = d2; 76 | } 77 | 78 | /** 79 | * Set D3 pin 80 | */ 81 | void d3(uint8_t d3) { 82 | pins.d3 = d3; 83 | } 84 | }; 85 | } 86 | } 87 | } 88 | } 89 | 90 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/fs/spiffs.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_CAMERA_SPIFFS 2 | #define ELOQUENT_ESP32CAM_CAMERA_SPIFFS 3 | 4 | #include 5 | #include 6 | #include "./fs.h" 7 | 8 | 9 | using namespace eloq; 10 | 11 | 12 | namespace Eloquent { 13 | namespace Extra { 14 | namespace Esp32 { 15 | namespace Fs { 16 | /** 17 | * Interact with SPIFFS 18 | */ 19 | class Spiffs : public FileSystem { 20 | public: 21 | /** 22 | * 23 | */ 24 | Spiffs() : 25 | FileSystem("SPIFFS", &SPIFFS, "/spiffs"), 26 | partitionLabel(""), 27 | format(false), 28 | maxFiles(5) { 29 | } 30 | 31 | /** 32 | * 33 | */ 34 | void formatOnFail() { 35 | format = true; 36 | } 37 | 38 | /** 39 | * 40 | */ 41 | void mountAt(String mountpoint) { 42 | root = mountpoint; 43 | } 44 | 45 | /** 46 | * Set partition label 47 | */ 48 | void partition(String partition) { 49 | partitionLabel = partition; 50 | } 51 | 52 | /** 53 | * Set max number of open files 54 | */ 55 | void maxOpenFiles(uint8_t numMaxFiles) { 56 | maxFiles = numMaxFiles; 57 | } 58 | 59 | /** 60 | * Init SD 61 | */ 62 | Exception& begin() { 63 | const char *part = partitionLabel != "" ? partitionLabel.c_str() : NULL; 64 | 65 | if (!SPIFFS.begin(format, root.c_str(), maxFiles, part)) 66 | return exception.set("Something went wrong with SPIFFS.begin()"); 67 | 68 | return exception.clear(); 69 | } 70 | 71 | 72 | 73 | protected: 74 | bool format; 75 | String partitionLabel; 76 | uint8_t maxFiles; 77 | }; 78 | } 79 | } 80 | } 81 | } 82 | 83 | namespace eloq { 84 | static Eloquent::Extra::Esp32::Fs::Spiffs spiffs; 85 | } 86 | 87 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/html/HtmlBuilder.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_HTML_HTMLBUILDER 2 | #define ELOQUENT_EXTRA_ESP32_HTML_HTMLBUILDER 3 | 4 | #include 5 | 6 | namespace Eloquent { 7 | namespace Extra { 8 | namespace Esp32 { 9 | namespace Html { 10 | /** 11 | * Convenience class for HTML generation 12 | */ 13 | class HtmlBuilder { 14 | public: 15 | WebServer *webServer; 16 | 17 | /** 18 | * Constructor 19 | */ 20 | HtmlBuilder(WebServer& server) : 21 | webServer(&server) { 22 | 23 | } 24 | 25 | /** 26 | * Render Svelte.js SPA 27 | */ 28 | void svelteSPA() { 29 | const char app[] PROGMEM = R"===( 30 |
31 | 32 | 33 | )==="; 34 | 35 | webServer->send(200, "text/html", app); 36 | } 37 | }; 38 | } 39 | } 40 | } 41 | } 42 | 43 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/logging/webserial.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eloquentarduino/EloquentEsp32cam/bd62b8937aa474e57e4a4a3dcf71ad2c44eb944f/src/eloquent_esp32cam/extra/esp32/logging/webserial.h -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/multiprocessing/mutex.h: -------------------------------------------------------------------------------- 1 | #ifdef ESP32 2 | #ifndef ELOQUENT_EXTRA_ESP32_MULTIPROCESSING_MUTEX 3 | #define ELOQUENT_EXTRA_ESP32_MULTIPROCESSING_MUTEX 4 | 5 | namespace Eloquent { 6 | namespace Extra { 7 | namespace Esp32 { 8 | namespace Multiprocessing { 9 | /** 10 | * Mutex for concurrent access to resource 11 | */ 12 | class Mutex { 13 | public: 14 | const char *name; 15 | SemaphoreHandle_t mutex; 16 | 17 | /** 18 | * 19 | */ 20 | Mutex(const char *name_) : 21 | mutex(NULL), 22 | name(name_), 23 | _ok(true) { 24 | } 25 | 26 | /** 27 | * 28 | */ 29 | bool isOk() { 30 | return _ok; 31 | } 32 | 33 | /** 34 | * Run function with mutex 35 | */ 36 | template 37 | bool threadsafe(Callback callback, size_t timeout = 0) { 38 | portTickType ticks = timeout / portTICK_RATE_MS; 39 | 40 | if (ticks == 0) 41 | ticks = portMAX_DELAY; 42 | 43 | if (mutex == NULL) { 44 | ESP_LOGI("Mutex", "Creating mutex %s", name); 45 | mutex = xSemaphoreCreateMutex(); 46 | } 47 | 48 | if (mutex == NULL) { 49 | ESP_LOGE("Mutex", "Cannot create mutex %s", name); 50 | return (_ok = false); 51 | } 52 | 53 | if (xSemaphoreTake(mutex, ticks) != pdTRUE) { 54 | ESP_LOGW("Mutex", "Cannot acquire mutex %s within timeout", name); 55 | return (_ok = false); 56 | } 57 | 58 | callback(); 59 | xSemaphoreGive(mutex); 60 | 61 | return (_ok = true); 62 | } 63 | 64 | protected: 65 | bool _ok; 66 | }; 67 | } 68 | } 69 | } 70 | } 71 | 72 | #endif 73 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/multiprocessing/thread.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_MULTIPROCESSING_THREAD 2 | #define ELOQUENT_EXTRA_ESP32_MULTIPROCESSING_THREAD 3 | 4 | namespace Eloquent { 5 | namespace Extra { 6 | namespace Esp32 { 7 | namespace Multiprocessing { 8 | /** 9 | * Run task on given core 10 | */ 11 | class Thread { 12 | public: 13 | 14 | /** 15 | * 16 | * @param name 17 | */ 18 | Thread(const char* threadName) : 19 | name(threadName), 20 | priority(0), 21 | stackSize(1000), 22 | args(NULL) { 23 | 24 | core = xPortGetCoreID(); 25 | } 26 | 27 | /** 28 | * Set task args 29 | * @return 30 | */ 31 | Thread& withArgs(void* newArgs) { 32 | args = newArgs; 33 | 34 | return *this; 35 | } 36 | 37 | /** 38 | * Set args if not set 39 | * @return 40 | */ 41 | Thread& withDefaultArgs(void* defaultArgs) { 42 | if (args == NULL) 43 | args = defaultArgs; 44 | 45 | return *this; 46 | } 47 | 48 | /** 49 | * Set task priority 50 | * @return 51 | */ 52 | Thread& withPriority(uint8_t priority) { 53 | this->priority = priority; 54 | 55 | return *this; 56 | } 57 | 58 | /** 59 | * Set stack size 60 | * @return 61 | */ 62 | Thread& withStackSize(uint16_t stackSize) { 63 | this->stackSize = stackSize; 64 | 65 | return *this; 66 | } 67 | 68 | /** 69 | * Set pinned core 70 | * @return 71 | */ 72 | Thread& onCore(uint8_t core) { 73 | this->core = core; 74 | 75 | return *this; 76 | } 77 | 78 | /** 79 | * Create task 80 | * @tparam Task 81 | * @param name 82 | * @param task 83 | */ 84 | template 85 | void run(Task task) { 86 | ESP_LOGI(name, "Starting thread with stack size %d bytes on core %d", (int) stackSize, (int) core); 87 | 88 | xTaskCreate( 89 | task, // Function to implement the task 90 | name, // Name of the task 91 | stackSize, // Stack size in bytes 92 | args, // Task input parameter 93 | priority, // Priority of the task 94 | NULL // Task handle. 95 | ); 96 | } 97 | 98 | private: 99 | const char *name; 100 | uint8_t core; 101 | void *args; 102 | uint8_t priority; 103 | uint16_t stackSize; 104 | }; 105 | } 106 | } 107 | } 108 | } 109 | 110 | #endif 111 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/ntp.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_NTP 2 | #define ELOQUENT_EXTRA_ESP32_NTP 3 | 4 | #include "../exception.h" 5 | #include "./wifi/sta.h" 6 | 7 | using namespace eloq; 8 | using Eloquent::Error::Exception; 9 | 10 | 11 | namespace Eloquent { 12 | namespace Extra { 13 | namespace Esp32 { 14 | class NTP { 15 | public: 16 | struct tm timeinfo; 17 | Exception exception; 18 | 19 | /** 20 | * 21 | */ 22 | NTP() : 23 | exception("NTP"), 24 | gmtOffset(0), 25 | daylightOffset(0), 26 | serverName("pool.ntp.org") { 27 | 28 | } 29 | 30 | /** 31 | * Test if NTP is working 32 | */ 33 | operator bool() const { 34 | return timeinfo.tm_year >= (2023 - 1900); 35 | } 36 | 37 | /** 38 | * Short for offsetFromGreenwhich 39 | */ 40 | void gmt(float offset = 0) { 41 | offsetFromGreenwhich(offset); 42 | } 43 | 44 | /** 45 | * 46 | */ 47 | void offsetFromGreenwhich(float offset) { 48 | gmtOffset = offset * 3600; 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | void isDaylight(bool is = true) { 55 | daylightOffset = is ? 3600 : 0; 56 | } 57 | 58 | /** 59 | * 60 | */ 61 | void isntDaylight() { 62 | isDaylight(false); 63 | } 64 | 65 | /** 66 | * 67 | */ 68 | void server(String server) { 69 | serverName = server; 70 | } 71 | 72 | /** 73 | * Configure NTP 74 | */ 75 | Exception& begin() { 76 | if (!wifi.isConnected()) 77 | return exception.set("You need WiFi connection to sync NTP"); 78 | 79 | configTime(gmtOffset, daylightOffset, (const char *) serverName.c_str()); 80 | 81 | return refresh(); 82 | } 83 | 84 | /** 85 | * Update time 86 | */ 87 | Exception& refresh() { 88 | getLocalTime(&timeinfo); 89 | 90 | if (timeinfo.tm_year < (2023 - 1900)) 91 | return exception.set("Cannot get time"); 92 | 93 | return exception.clear(); 94 | } 95 | 96 | /** 97 | * Format datetime 98 | */ 99 | String format(const char *fmt) { 100 | char buf[32]; 101 | 102 | if (!!exception) { 103 | ESP_LOGE("NTP", "Bad time. Empty filename will be returned"); 104 | return ""; 105 | } 106 | 107 | strftime(buf, sizeof(buf), fmt, &timeinfo); 108 | 109 | return buf; 110 | } 111 | 112 | /** 113 | * Get date as string 114 | */ 115 | String date() { 116 | return format("%Y%m%d"); 117 | } 118 | 119 | /** 120 | * Get time as string 121 | */ 122 | String time() { 123 | return format("%H%M%S"); 124 | } 125 | 126 | /** 127 | * Get datetime as string 128 | */ 129 | String datetime() { 130 | return format("%Y%m%dT%H%M%S"); 131 | } 132 | 133 | /** 134 | * Get datetime as valid string 135 | */ 136 | String filename(bool autorefresh = true) { 137 | if (autorefresh) 138 | refresh(); 139 | 140 | return format("%Y%m%dT%H%M%S"); 141 | } 142 | 143 | // synctactic sugar 144 | inline void cst() { gmt(8); } 145 | inline void ist() { gmt(5.5); } 146 | inline void eest() { gmt(3); } 147 | inline void cest() { gmt(2); } 148 | inline void bst() { gmt(1); } 149 | inline void west() { gmt(1); } 150 | inline void cet() { gmt(1); } 151 | inline void edt() { gmt(-4); } 152 | inline void pdt() { gmt(-7); } 153 | 154 | protected: 155 | uint16_t gmtOffset; 156 | uint16_t daylightOffset; 157 | String serverName; 158 | }; 159 | } 160 | } 161 | } 162 | 163 | namespace eloq { 164 | static Eloquent::Extra::Esp32::NTP ntp; 165 | } 166 | 167 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/nvs/counter.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_NVS_COUNTER 2 | #define ELOQUENT_EXTRA_ESP32_NVS_COUNTER 3 | 4 | #include 5 | #include "../../ulid.h" 6 | 7 | namespace Eloquent { 8 | namespace Extra { 9 | namespace Esp32 { 10 | namespace NVS { 11 | /** 12 | * Keep incremental counter in NVS 13 | */ 14 | class Counter { 15 | public: 16 | 17 | /** 18 | * 19 | */ 20 | Counter(const char *name) : 21 | key(name), 22 | count(0) { 23 | } 24 | 25 | /** 26 | * Read current value from NVS 27 | */ 28 | unsigned long current() { 29 | prefs.begin("e::counter", true); 30 | count = prefs.getULong(key, 0); 31 | prefs.end(); 32 | 33 | return count; 34 | } 35 | 36 | /** 37 | * Increment counter 38 | */ 39 | unsigned long next() { 40 | count = current(); 41 | count += 1; 42 | 43 | set(count); 44 | 45 | return count; 46 | } 47 | 48 | /** 49 | * 50 | */ 51 | void set(uint16_t n) { 52 | prefs.begin("e::counter", false); 53 | prefs.putULong(key, count); 54 | prefs.end(); 55 | } 56 | 57 | /** 58 | * 59 | */ 60 | void reset() { 61 | set(0); 62 | } 63 | 64 | /** 65 | * Get current count as padded string 66 | */ 67 | String toString(uint8_t length = 8) { 68 | readIfNecessary(); 69 | 70 | String s = String(count); 71 | 72 | while (s.length() < length) 73 | s = String("0") + s; 74 | 75 | return s; 76 | } 77 | 78 | /** 79 | * Get next count as padded string 80 | */ 81 | String nextString(uint8_t length = 8) { 82 | next(); 83 | 84 | return toString(length); 85 | } 86 | 87 | /** 88 | * Get current count as Ulid 89 | */ 90 | String toUlid() { 91 | readIfNecessary(); 92 | 93 | ULID ulid(count, count % 255); 94 | 95 | return ulid.toString(); 96 | } 97 | 98 | /** 99 | * Get next count as Ulid 100 | */ 101 | String nextUlid() { 102 | next(); 103 | 104 | return toUlid(); 105 | } 106 | 107 | protected: 108 | unsigned long count; 109 | const char *key; 110 | Preferences prefs; 111 | 112 | /** 113 | * 114 | */ 115 | unsigned long readIfNecessary() { 116 | return count > 0 ? count : current(); 117 | } 118 | }; 119 | } 120 | } 121 | } 122 | } 123 | 124 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/wifi.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_WIFI 2 | #define ELOQUENT_EXTRA_WIFI 3 | 4 | #include "./wifi/sta.h" 5 | 6 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/wifi/sta.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_ESP32_WIFI_WIFISTA 2 | #define ELOQUENT_EXTRA_ESP32_WIFI_WIFISTA 3 | 4 | #include 5 | #include 6 | #include "../../exception.h" 7 | 8 | using Eloquent::Error::Exception; 9 | 10 | 11 | namespace Eloquent { 12 | namespace Extra { 13 | namespace Esp32 { 14 | namespace Wifi { 15 | /** 16 | * Connect to WiFi in Station mode 17 | */ 18 | class Sta { 19 | public: 20 | Exception exception; 21 | 22 | /** 23 | * Constructor 24 | */ 25 | Sta() : 26 | exception("WiFi") { 27 | 28 | } 29 | 30 | /** 31 | * Test if there's an exception 32 | */ 33 | operator bool() const { 34 | return isConnected(); 35 | } 36 | 37 | #ifdef WIFI_SSID 38 | #ifdef WIFI_PASS 39 | Exception& connect(size_t timeout = 20000) { 40 | return connect(WIFI_SSID, WIFI_PASS, timeout); 41 | } 42 | #endif 43 | #endif 44 | 45 | /** 46 | * Connect to WiFi network 47 | */ 48 | Exception& connect(const char *ssid, const char* password, size_t timeout = 20000) { 49 | ESP_LOGI("WiFi", "Connecting to %s...", ssid); 50 | WiFi.mode(WIFI_STA); 51 | WiFi.disconnect(true, true); 52 | WiFi.begin(ssid, password); 53 | /*WiFi.persistent(true); 54 | WiFi.setAutoConnect(false); 55 | WiFi.setAutoReconnect(true); 56 | WiFi.setTxPower(WIFI_POWER_2dBm);*/ 57 | 58 | timeout += millis(); 59 | 60 | while (millis() < timeout) { 61 | if (isConnected()) { 62 | ESP_LOGI("WiFi", "Connected!"); 63 | 64 | #ifdef HOSTNAME 65 | MDNS.begin(HOSTNAME); 66 | #endif 67 | 68 | return exception.clear(); 69 | } 70 | 71 | delay(50); 72 | } 73 | 74 | if (WiFi.status() == 1) 75 | return exception.set("SSID not found"); 76 | 77 | return exception.set("Can't connect to WiFi (wrong password?)"); 78 | } 79 | 80 | /** 81 | * Test if camera is connected to WiFi 82 | * @return 83 | */ 84 | inline bool isConnected() const { 85 | return WiFi.status() == WL_CONNECTED; 86 | } 87 | 88 | /** 89 | * Get IP address as string 90 | * 91 | * @return 92 | */ 93 | String ip() const { 94 | IPAddress ip = WiFi.localIP(); 95 | 96 | return String(ip[0]) + '.' + ip[1] + '.' + ip[2] + '.' + ip[3]; 97 | } 98 | }; 99 | } 100 | } 101 | } 102 | } 103 | 104 | namespace eloq { 105 | static Eloquent::Extra::Esp32::Wifi::Sta wifi; 106 | } 107 | 108 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/esp32/ws/threaded_ws.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../multiprocessing/Thread.h" 5 | 6 | using Eloquent::Extra::Esp32::Multiprocessing::Thread; 7 | 8 | namespace Eloquent { 9 | namespace Extra { 10 | namespace Esp32 { 11 | namespace Ws { 12 | /** 13 | * Run WebSockets in thread 14 | */ 15 | class ThreadedWs { 16 | public: 17 | WebSocketsServer webSocket; 18 | Thread thread; 19 | 20 | /** 21 | * 22 | */ 23 | ThreadedWs(const char* threadName = "WebSocket", const uint8_t port = 82) : 24 | thread(threadName), 25 | stackSize(1000), 26 | webSocket(port), 27 | isConnected(false) { 28 | 29 | } 30 | 31 | /** 32 | * Set stack size 33 | */ 34 | ThreadedWs& withStackSize(uint16_t stackSize) { 35 | thread.withStackSize(stackSize); 36 | 37 | return *this; 38 | } 39 | 40 | /** 41 | * Start thread with WebSocket request handler 42 | */ 43 | template 44 | void begin(Handler handler) { 45 | webSocket.begin(); 46 | webSocket.onEvent([this, handler](uint8_t num, WStype_t type, uint8_t * payload, size_t length) { 47 | if (type == WStype_CONNECTED) 48 | isConnected = true; 49 | 50 | if (type == WStype_DISCONNECTED) 51 | isConnected = false; 52 | 53 | if (isConnected) 54 | handler(num, type, payload, length); 55 | }); 56 | 57 | thread 58 | .withArgs((void*) &webSocket) 59 | .withStackSize(stackSize) 60 | .run([](void *args) { 61 | WebSocketsServer *webSocket = (WebSocketsServer*) args; 62 | 63 | while (true) { 64 | webSocket->loop(); 65 | yield(); 66 | } 67 | }); 68 | } 69 | 70 | protected: 71 | uint16_t stackSize; 72 | bool isConnected; 73 | }; 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/exception.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXCEPTION_H 2 | #define ELOQUENT_EXCEPTION_H 3 | 4 | namespace Eloquent { 5 | namespace Error { 6 | /** 7 | * Application expcetion 8 | */ 9 | class Exception { 10 | public: 11 | /** 12 | * 13 | */ 14 | Exception(const char* tag) : 15 | _tag(tag), 16 | _message(""), 17 | _isSevere(true) { 18 | } 19 | 20 | /** 21 | * Test if there's an exception 22 | */ 23 | operator bool() const { 24 | return !isOk(); 25 | } 26 | 27 | /** 28 | * Test if there's an exception 29 | */ 30 | bool isOk() const { 31 | return _message == ""; 32 | } 33 | 34 | /** 35 | * Test if exception is severe 36 | */ 37 | bool isSevere() const { 38 | return _isSevere && !isOk(); 39 | } 40 | 41 | /** 42 | * Mark error as not severe 43 | */ 44 | Exception& soft() { 45 | _isSevere = false; 46 | 47 | return *this; 48 | } 49 | 50 | /** 51 | * Set exception message 52 | */ 53 | Exception& set(String error) { 54 | _message = error; 55 | _isSevere = true; 56 | 57 | if (error.length() > 0) { 58 | const char *c_str = error.c_str(); 59 | 60 | //if (_isSevere) 61 | // ESP_LOGE(_tag, "%s", c_str); 62 | //else 63 | // ESP_LOGW(_tag, "%s", c_str); 64 | } 65 | 66 | return *this; 67 | } 68 | 69 | /** 70 | * Clear exception 71 | */ 72 | Exception& clear() { 73 | return set(""); 74 | } 75 | 76 | /** 77 | * 78 | */ 79 | template 80 | Exception& propagate(Other& other) { 81 | set(other.exception.toString()); 82 | 83 | return *this; 84 | } 85 | 86 | /** 87 | * Convert exception to string 88 | */ 89 | inline String toString() { 90 | return _message; 91 | } 92 | 93 | /** 94 | * Convert exception to char* 95 | */ 96 | inline const char* toCString() { 97 | return toString().c_str(); 98 | } 99 | 100 | /** 101 | * 102 | */ 103 | static Exception none() { 104 | return Exception(""); 105 | } 106 | 107 | protected: 108 | const char* _tag; 109 | bool _isSevere; 110 | String _message; 111 | }; 112 | } 113 | } 114 | 115 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/pubsub.h: -------------------------------------------------------------------------------- 1 | #ifndef PubSubClient_h 2 | #else 3 | #ifndef ELOQUENT_EXTRA_PUBSUB_H 4 | #define ELOQUENT_EXTRA_PUBSUB_H 1 5 | 6 | #include 7 | #include 8 | #include "./exception.h" 9 | 10 | using Eloquent::Error::Exception; 11 | 12 | 13 | namespace Eloquent { 14 | namespace Extra { 15 | /** 16 | * @class PubSub 17 | * @author Simone 18 | * @date 13/12/2023 19 | * @file pubsub.h 20 | * @brief Send data over MQTT 21 | */ 22 | template 23 | class PubSub { 24 | public: 25 | String clientId; 26 | WiFiClient client; 27 | PubSubClient mqtt; 28 | Exception exception; 29 | 30 | /** 31 | * @brief Constructor 32 | * @param subject 33 | */ 34 | PubSub(T *subject) : 35 | _subject(subject), 36 | _server(""), 37 | _port(1883), 38 | mqtt(client), 39 | exception("PubSub") { 40 | // generate client id based on MAC address 41 | clientId = String("pubsub-") + WiFi.macAddress(); 42 | } 43 | 44 | /** 45 | * @brief Set client id 46 | * @param clientName 47 | */ 48 | void clientName(String clientName) { 49 | clientId = clientName; 50 | 51 | // replace vars 52 | clientId.replace("{mac}", WiFi.macAddress()); 53 | } 54 | 55 | /** 56 | * @brief Set MQTT server 57 | * @param server 58 | */ 59 | void server(const char* server, uint16_t port = 0) { 60 | mqtt.setServer(server, port ? port : _port); 61 | } 62 | 63 | /** 64 | * @brief Set username/password for broker 65 | * @param user 66 | * @param pass 67 | */ 68 | void auth(String user, String pass) { 69 | _user = user; 70 | _pass = pass; 71 | } 72 | 73 | /** 74 | * @brief Connect to MQTT client 75 | */ 76 | Exception& connect(size_t timeout = 10000) { 77 | if (mqtt.connected()) 78 | return exception.clear(); 79 | 80 | if (!_server) 81 | return exception.set("You must set an MQTT broker with mqtt.server(serverName)"); 82 | 83 | if (WiFi.status() != WL_CONNECTED) 84 | return exception.set("You must be connected to WiFi"); 85 | 86 | timeout += millis(); 87 | 88 | while (millis() < timeout) { 89 | const char *user = _user == "" ? NULL : _user.c_str(); 90 | const char *pass = _pass == "" ? NULL : _pass.c_str(); 91 | 92 | if (mqtt.connect(clientId.c_str(), user, pass)) 93 | return exception.clear(); 94 | } 95 | 96 | return exception.set("Cannot connect to MQTT server"); 97 | } 98 | 99 | /** 100 | * @brief Send MQTT message 101 | * @param topic 102 | */ 103 | Exception& publish(String topic) { 104 | //if (!_subject->shouldPub()) 105 | // return exception; 106 | 107 | if (!connect().isOk()) 108 | return exception; 109 | 110 | if (!mqtt.publish(topic.c_str(), _subject->toJSON().c_str())) 111 | return exception.set("Cannot send MQTT message"); 112 | 113 | return exception.clear(); 114 | } 115 | 116 | protected: 117 | T *_subject; 118 | uint16_t _port; 119 | String _server; 120 | String _user; 121 | String _pass; 122 | }; 123 | } 124 | } 125 | 126 | 127 | #endif 128 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/time/benchmark.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_TIME_BENCHMARK 2 | #define ELOQUENT_EXTRA_TIME_BENCHMARK 3 | 4 | namespace Eloquent { 5 | namespace Extra { 6 | namespace Time { 7 | /** 8 | * Run benchmark on code blocks 9 | */ 10 | class Benchmark { 11 | public: 12 | 13 | /** 14 | * Start timer 15 | */ 16 | void start() { 17 | timeStart = micros(); 18 | } 19 | 20 | /** 21 | * Stop timer 22 | */ 23 | size_t stop() { 24 | elapsedInMicros = micros() - timeStart; 25 | 26 | return millis(); 27 | } 28 | 29 | /** 30 | * Benchmark given function 31 | */ 32 | template 33 | size_t benchmark(Callback callback) { 34 | start(); 35 | callback(); 36 | 37 | return stop(); 38 | } 39 | 40 | /** 41 | * Get elapsed time in millis 42 | */ 43 | inline size_t millis() { 44 | return microseconds() / 1000; 45 | } 46 | 47 | /** 48 | * Alias for millis() 49 | */ 50 | inline size_t ms() { 51 | return millis(); 52 | } 53 | 54 | /** 55 | * Get elapsed time in micros 56 | */ 57 | inline size_t microseconds() { 58 | return elapsedInMicros; 59 | } 60 | 61 | /** 62 | * Alias for micros 63 | */ 64 | inline size_t us() { 65 | return microseconds(); 66 | } 67 | 68 | /** 69 | * 70 | */ 71 | template 72 | void timeit(Callback callback) { 73 | start(); 74 | callback(); 75 | stop(); 76 | } 77 | 78 | protected: 79 | size_t timeStart; 80 | size_t elapsedInMicros; 81 | }; 82 | } 83 | } 84 | } 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/time/rate_limit.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_EXTRA_RATELIMIT 2 | #define ELOQUENT_EXTRA_RATELIMIT 3 | 4 | namespace Eloquent { 5 | namespace Extra { 6 | namespace Time { 7 | 8 | /** 9 | * Prevent running code too often 10 | */ 11 | class RateLimit { 12 | public: 13 | 14 | /** 15 | * Constructor 16 | */ 17 | RateLimit() : 18 | debounceTime(0), 19 | lastEvent(0) { 20 | 21 | } 22 | 23 | /** 24 | * Test if debounce time has elapsed 25 | */ 26 | operator bool() const { 27 | const size_t now = millis(); 28 | 29 | return debounceTime == 0 || lastEvent == 0 || (now - lastEvent) >= debounceTime || lastEvent >= now; 30 | } 31 | 32 | /** 33 | * Set debounce interval 34 | */ 35 | inline RateLimit& none() { 36 | debounceTime = 0; 37 | 38 | return *this; 39 | } 40 | 41 | /** 42 | * Set debounce interval 43 | */ 44 | inline RateLimit& atMostOnceEvery(size_t x) { 45 | debounceTime = x; 46 | 47 | return *this; 48 | } 49 | 50 | /** 51 | * Set debounce fps 52 | */ 53 | inline RateLimit& atMost(size_t x) { 54 | debounceTime = x; 55 | 56 | return *this; 57 | } 58 | 59 | /** 60 | * Set debounce unit 61 | */ 62 | inline RateLimit& milliseconds() { 63 | return *this; 64 | } 65 | 66 | /** 67 | * Set debounce unit 68 | */ 69 | inline RateLimit& second() { 70 | return seconds(); 71 | } 72 | 73 | /** 74 | * Set debounce unit 75 | */ 76 | inline RateLimit& seconds() { 77 | debounceTime *= 1000; 78 | 79 | return *this; 80 | } 81 | 82 | /** 83 | * Set debounce unit 84 | */ 85 | inline RateLimit& minutes() { 86 | debounceTime *= 1000; 87 | debounceTime *= 60; 88 | 89 | return *this; 90 | } 91 | 92 | /** 93 | * Set debounce unit 94 | */ 95 | inline RateLimit& hours() { 96 | debounceTime *= 1000; 97 | debounceTime *= 3600; 98 | 99 | return *this; 100 | } 101 | 102 | /** 103 | * Set debounce unit 104 | */ 105 | inline RateLimit& fps() { 106 | debounceTime = 1000 / debounceTime; 107 | 108 | return *this; 109 | } 110 | 111 | /** 112 | * Update last event timestamp 113 | */ 114 | inline void touch() { 115 | lastEvent = millis(); 116 | } 117 | 118 | /** 119 | * Get informative text on when next event is allowed 120 | */ 121 | String getRetryInMessage() { 122 | return String("Rate limit exceeded. Retry in ") 123 | + (debounceTime - (millis() - lastEvent)) 124 | + "ms"; 125 | } 126 | 127 | protected: 128 | size_t debounceTime; 129 | size_t lastEvent; 130 | }; 131 | } 132 | } 133 | } 134 | 135 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/extra/ulid.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This code is taken from https://github.com/suyash/ulid/tree/master 3 | * Their license applies 4 | */ 5 | #ifndef ELOQUENT_EXTRA_ULID 6 | #define ELOQUENT_EXTRA_ULID 7 | 8 | 9 | namespace Eloquent { 10 | namespace Extra { 11 | /** 12 | * Generate ULIDs (sortable UIDs) 13 | */ 14 | class ULID { 15 | public: 16 | uint8_t data[16]; 17 | 18 | /** 19 | * Constructor 20 | */ 21 | ULID(uint32_t timestamp, uint8_t entropy) { 22 | memset(data, 0, 16); 23 | encodeTime(timestamp); 24 | encodeEntropy(entropy); 25 | } 26 | 27 | /** 28 | * Convert to string 29 | */ 30 | String toString() { 31 | static const char alphabet[33] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; 32 | char dst[27] = {'\0'}; 33 | 34 | // 10 byte timestamp 35 | dst[0] = alphabet[(data[0] & 224) >> 5]; 36 | dst[1] = alphabet[data[0] & 31]; 37 | dst[2] = alphabet[(data[1] & 248) >> 3]; 38 | dst[3] = alphabet[((data[1] & 7) << 2) | ((data[2] & 192) >> 6)]; 39 | dst[4] = alphabet[(data[2] & 62) >> 1]; 40 | dst[5] = alphabet[((data[2] & 1) << 4) | ((data[3] & 240) >> 4)]; 41 | dst[6] = alphabet[((data[3] & 15) << 1) | ((data[4] & 128) >> 7)]; 42 | dst[7] = alphabet[(data[4] & 124) >> 2]; 43 | dst[8] = alphabet[((data[4] & 3) << 3) | ((data[5] & 224) >> 5)]; 44 | dst[9] = alphabet[data[5] & 31]; 45 | 46 | // 16 bytes of entropy 47 | dst[10] = alphabet[(data[6] & 248) >> 3]; 48 | dst[11] = alphabet[((data[6] & 7) << 2) | ((data[7] & 192) >> 6)]; 49 | dst[12] = alphabet[(data[7] & 62) >> 1]; 50 | dst[13] = alphabet[((data[7] & 1) << 4) | ((data[8] & 240) >> 4)]; 51 | dst[14] = alphabet[((data[8] & 15) << 1) | ((data[9] & 128) >> 7)]; 52 | dst[15] = alphabet[(data[9] & 124) >> 2]; 53 | dst[16] = alphabet[((data[9] & 3) << 3) | ((data[10] & 224) >> 5)]; 54 | dst[17] = alphabet[data[10] & 31]; 55 | dst[18] = alphabet[(data[11] & 248) >> 3]; 56 | dst[19] = alphabet[((data[11] & 7) << 2) | ((data[12] & 192) >> 6)]; 57 | dst[20] = alphabet[(data[12] & 62) >> 1]; 58 | dst[21] = alphabet[((data[12] & 1) << 4) | ((data[13] & 240) >> 4)]; 59 | dst[22] = alphabet[((data[13] & 15) << 1) | ((data[14] & 128) >> 7)]; 60 | dst[23] = alphabet[(data[14] & 124) >> 2]; 61 | dst[24] = alphabet[((data[14] & 3) << 3) | ((data[15] & 224) >> 5)]; 62 | dst[25] = alphabet[data[15] & 31]; 63 | 64 | return String(dst + 2); 65 | } 66 | 67 | protected: 68 | 69 | /** 70 | * Encode time in the first 6 bytes 71 | * timestamp 72 | * */ 73 | inline void encodeTime(uint32_t timestamp) { 74 | data[0] = 0; 75 | data[1] = 0; 76 | data[2] = static_cast(timestamp >> 24); 77 | data[3] = static_cast(timestamp >> 16); 78 | data[4] = static_cast(timestamp >> 8); 79 | data[5] = static_cast(timestamp); 80 | } 81 | 82 | /** 83 | * Encode entropy from given random seed 84 | */ 85 | void encodeEntropy(uint8_t seed) { 86 | randomSeed(seed); 87 | 88 | data[6] = random(255); 89 | data[7] = random(255); 90 | data[8] = random(255); 91 | data[9] = random(255); 92 | data[10] = random(255); 93 | data[11] = random(255); 94 | data[12] = random(255); 95 | data[13] = random(255); 96 | data[14] = random(255); 97 | data[15] = random(255); 98 | } 99 | }; 100 | } 101 | } 102 | 103 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/face/daemon.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_FACE_DAEMON_H 2 | #define ELOQUENT_ESP32CAM_FACE_DAEMON_H 3 | 4 | #include 5 | #include "../camera/camera.h" 6 | #include "../extra/esp32/multiprocessing/thread.h" 7 | #include "./face_t.h" 8 | 9 | using eloq::camera; 10 | using eloq::face_t; 11 | using Eloquent::Extra::Esp32::Multiprocessing::Thread; 12 | using OnFaceCallback = std::function; 13 | using OnMultipleFacesCallback = std::function; 14 | 15 | 16 | namespace Eloquent { 17 | namespace Esp32cam { 18 | namespace Face { 19 | /** 20 | * Run face detection in a task 21 | * 22 | * @class Daemon 23 | * @author Simone 24 | * @date 13/12/2023 25 | * @file daemon.h 26 | * @brief 27 | */ 28 | template 29 | class Daemon { 30 | public: 31 | Thread thread; 32 | 33 | /** 34 | * Constructor 35 | * 36 | * @brief 37 | */ 38 | Daemon(T* detection) : 39 | thread("FaceDetection"), 40 | _detection(detection) { 41 | 42 | } 43 | 44 | /** 45 | * Run function when a face is detected 46 | * 47 | * @brief 48 | * @param callback 49 | */ 50 | void onFace(OnFaceCallback callback) { 51 | _onFace = callback; 52 | } 53 | 54 | /** 55 | * Run function when multiple faces are detected 56 | * 57 | * @brief 58 | * @param callback 59 | */ 60 | void onMultipleFaces(OnMultipleFacesCallback callback) { 61 | _onMultipleFaces = callback; 62 | } 63 | 64 | /** 65 | * Start face detection in background 66 | * 67 | * @brief 68 | */ 69 | void start() { 70 | thread 71 | .withArgs((void*) this) 72 | .withStackSize(4000) 73 | .run([](void *args) { 74 | Daemon *self = (Daemon*) args; 75 | 76 | delay(3000); 77 | 78 | while (true) { 79 | yield(); 80 | delay(1); 81 | 82 | if (!camera.capture().isOk()) 83 | continue; 84 | 85 | if (!self->_detection->run().isOk()) 86 | continue; 87 | 88 | if (self->_detection->notFound()) 89 | continue; 90 | 91 | if (self->_detection->count() == 1) { 92 | self->_onFace(self->_detection->first); 93 | continue; 94 | } 95 | 96 | self->_detection->forEach([&self](int i, face_t& face) { 97 | self->_onMultipleFaces(i, face); 98 | }); 99 | } 100 | }); 101 | } 102 | 103 | protected: 104 | T *_detection; 105 | OnFaceCallback _onFace; 106 | OnMultipleFacesCallback _onMultipleFaces; 107 | 108 | }; 109 | } 110 | } 111 | } 112 | 113 | 114 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/face/detection_server.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_FACE_DETECTION_SERVER 2 | #define ELOQUENT_ESP32CAM_FACE_DETECTION_SERVER 3 | 4 | #include "../extra/exception.h" 5 | #include "../extra/esp32/http/threaded_server.h" 6 | #include "../extra/esp32/ws/threaded_ws.h" 7 | #include "../assets/face_detection.html.h" 8 | #include "../assets/face_detection.js.h" 9 | 10 | using Eloquent::Error::Exception; 11 | using Eloquent::Extra::Esp32::Http::ThreadedServer; 12 | using Eloquent::Extra::Esp32::Ws::ThreadedWs; 13 | 14 | 15 | namespace Eloquent { 16 | namespace Esp32cam { 17 | namespace Face { 18 | /** 19 | * Add face detection to MJPEG HTTP stream 20 | */ 21 | class FaceDetectionServer { 22 | public: 23 | Exception exception; 24 | ThreadedServer http; 25 | ThreadedWs ws; 26 | 27 | /** 28 | * 29 | */ 30 | FaceDetectionServer() : 31 | exception("FaceDetectionWebServer"), 32 | http("FaceDetection"), 33 | ws("FaceDetectionWebSocket", 90) { 34 | 35 | } 36 | 37 | /** 38 | * 39 | */ 40 | Exception& begin() { 41 | http.thread.withStackSize(5000); 42 | 43 | // HTTP endpoints 44 | // render static files 45 | http.router.gzip("/", Eloquent::Assets::Gz::Html::FACE_DETECTION); 46 | //http.gzip("/face_detection.js", "text/javascript", Eloquent::Assets::Gz::Js::FACE_DETECTION); 47 | // serve MJPEG stream 48 | http.router.mjpeg(); 49 | 50 | if (!http.begin().isOk()) 51 | return exception.propagate(http); 52 | 53 | /*ws.begin([this](uint8_t num, WStype_t type, uint8_t * payload, size_t length) { 54 | Serial.println("ws event"); 55 | });*/ 56 | 57 | return exception.clear(); 58 | } 59 | 60 | protected: 61 | /** 62 | * 63 | */ 64 | template 65 | void sendMjpeg(Client client) { 66 | if (!camera.capture().isOk()) 67 | return; 68 | 69 | client.write((const char *) camera.frame->buf, camera.getSizeInBytes()); 70 | } 71 | }; 72 | } 73 | } 74 | } 75 | 76 | namespace eloq { 77 | static Eloquent::Esp32cam::Face::FaceDetectionServer faceDetectionServer; 78 | } 79 | 80 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/face/face_t.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_FACE_FACE_T 2 | #define ELOQUENT_ESP32CAM_FACE_FACE_T 3 | 4 | 5 | namespace Eloquent { 6 | namespace Esp32cam { 7 | namespace Face { 8 | /** 9 | * Face datatype 10 | */ 11 | class Face { 12 | public: 13 | int16_t x; 14 | int16_t y; 15 | int16_t x0; 16 | int16_t y0; 17 | int16_t x1; 18 | int16_t y1; 19 | /** 20 | * @added 2.7.3 21 | */ 22 | int16_t cx; 23 | /** 24 | * @added 2.7.3 25 | */ 26 | int16_t cy; 27 | uint16_t width; 28 | uint16_t height; 29 | float score; 30 | struct { 31 | int16_t x; 32 | int16_t y; 33 | } leftEye, rightEye, nose, leftMouth, rightMouth; 34 | 35 | /** 36 | * Check if face is valid 37 | */ 38 | bool isValid() { 39 | return width > 0 && height > 0; 40 | } 41 | 42 | /** 43 | * Check if face has keypoints 44 | */ 45 | bool hasKeypoints() { 46 | return leftEye.x != rightEye.x; 47 | } 48 | 49 | /** 50 | * Fill data from face detection result 51 | */ 52 | void copyFrom(const dl::detect::result_t& result) { 53 | if (result.box.size() != 4) { 54 | clear(); 55 | return; 56 | } 57 | 58 | x = x0 = result.box[0]; 59 | y = y0 = result.box[1]; 60 | x1 = result.box[2]; 61 | y1 = result.box[3]; 62 | width = x1 - x0 + 1; 63 | height = y1 - y0 + 1; 64 | score = result.score; 65 | /** 66 | * @added 2.7.3 67 | */ 68 | cx = x + width / 2; 69 | cy = y + height / 2; 70 | 71 | if (result.keypoint.size() == 10) { 72 | leftEye.x = result.keypoint[0]; 73 | leftEye.y = result.keypoint[1]; 74 | leftMouth.x = result.keypoint[2]; 75 | leftMouth.y = result.keypoint[3]; 76 | nose.x = result.keypoint[4]; 77 | nose.y = result.keypoint[5]; 78 | rightEye.x = result.keypoint[6]; 79 | rightEye.y = result.keypoint[7]; 80 | rightMouth.x = result.keypoint[8]; 81 | rightMouth.y = result.keypoint[9]; 82 | } 83 | } 84 | 85 | /** 86 | * Clear all coordinates 87 | */ 88 | void clear() { 89 | x = x0 = 0; 90 | y = y0 = 0; 91 | x1 = 0; 92 | y1 = 0; 93 | width = 0; 94 | height = 0; 95 | 96 | leftEye.x = 0; 97 | leftEye.y = 0; 98 | leftMouth.x = 0; 99 | leftMouth.y = 0; 100 | nose.x = 0; 101 | nose.y = 0; 102 | rightEye.x = 0; 103 | rightEye.y = 0; 104 | rightMouth.x = 0; 105 | rightMouth.y = 0; 106 | } 107 | }; 108 | } 109 | } 110 | } 111 | 112 | // create class alias 113 | namespace eloq { 114 | typedef Eloquent::Esp32cam::Face::Face face_t; 115 | } 116 | 117 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/face/mnp_config.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_FACE_MNPCONFIG 2 | #define ELOQUENT_ESP32CAM_FACE_MNPCONFIG 3 | 4 | 5 | namespace Eloquent { 6 | namespace Esp32cam { 7 | namespace Face { 8 | /** 9 | * Configure MNP detector 10 | */ 11 | class MNPConfig { 12 | public: 13 | struct { 14 | float score_thresh; 15 | float nms_thresh; 16 | int top_k; 17 | } config; 18 | 19 | /** 20 | * Constructor 21 | */ 22 | MNPConfig() { 23 | config.score_thresh = 0.5f; 24 | config.nms_thresh = 0.3f; 25 | config.top_k = 5; 26 | } 27 | }; 28 | } 29 | } 30 | } 31 | 32 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/face/msr_config.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_FACE_MSRCONFIG 2 | #define ELOQUENT_ESP32CAM_FACE_MSRCONFIG 3 | 4 | 5 | namespace Eloquent { 6 | namespace Esp32cam { 7 | namespace Face { 8 | /** 9 | * Detect MSR detector 10 | */ 11 | class MSRConfig { 12 | public: 13 | struct { 14 | float score_thresh; 15 | float nms_thresh; 16 | int top_k; 17 | float resize_scale; 18 | } config; 19 | 20 | /** 21 | * Constructor 22 | */ 23 | MSRConfig() { 24 | config.score_thresh = 0.1f; 25 | config.nms_thresh = 0.5f; 26 | config.top_k = 10; 27 | config.resize_scale = 0.2f; 28 | } 29 | }; 30 | } 31 | } 32 | } 33 | 34 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/jpeg/Callbacks.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENTESP32CAM_JPEG_CALLBACKS 2 | #define ELOQUENTESP32CAM_JPEG_CALLBACKS 3 | 4 | #include 5 | 6 | namespace Eloquent { 7 | namespace Esp32cam { 8 | namespace JPEG { 9 | /** 10 | * Callbacks for JPEGDECWrapper 11 | */ 12 | template 13 | class Callbacks { 14 | public: 15 | 16 | /** 17 | * Constructor 18 | */ 19 | Callbacks() : _i(0) { 20 | 21 | } 22 | 23 | /** 24 | * Add callback 25 | * @param callback 26 | * @return 27 | */ 28 | bool add(Callback callback) { 29 | if (_i == numCallbacks) 30 | return false; 31 | 32 | _callbacks[_i++] = callback; 33 | return true; 34 | } 35 | 36 | /** 37 | * Loop over callbacks 38 | * @param fn 39 | */ 40 | template 41 | void forEach(ForEach fn) { 42 | for (uint8_t i = 0; i < _i; i++) 43 | fn(_callbacks[i]); 44 | } 45 | 46 | protected: 47 | uint8_t _i; 48 | Callback _callbacks[numCallbacks]; 49 | }; 50 | } 51 | } 52 | } 53 | 54 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/jpeg/JPEGDECWrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "JPEGDECWrapper.h" 3 | 4 | #if defined(__JPEGDEC__) 5 | #include 6 | 7 | using Eloquent::Esp32cam::JPEG::JPEGDECWrapper; 8 | 9 | /** 10 | * 11 | * @param mcu 12 | * @return 13 | */ 14 | int __handleMCU__(jpeg_draw_tag *mcu) { 15 | JPEGDECWrapper *jpeg = (JPEGDECWrapper*) mcu->pUser; 16 | const auto crop = jpeg->crop; 17 | const uint8_t bpp = mcu->iBpp / 8; 18 | 19 | for (size_t i = 0; i < mcu->iHeight; i++) { 20 | const size_t inputOffset = i * mcu->iWidth * bpp; 21 | 22 | if (!jpeg->addMCU(mcu->x, mcu->y + i, bpp, ((uint8_t*) mcu->pPixels) + inputOffset, mcu->iWidthUsed)) 23 | return 0; 24 | } 25 | 26 | return 1; 27 | } 28 | 29 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/jpeg/JPEGENCWrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef __JPEGENC__ 2 | #error "You must include JPEGENC.h *before* eloquent_esp32cam" 3 | #else 4 | 5 | #ifndef ELOQUENTESP32CAM_JPEGENC 6 | #define ELOQUENTESP32CAM_JPEGENC 7 | 8 | #include "../mem.h" 9 | #include "../camera/camera.h" 10 | #include "../extra/exception.h" 11 | #include "../extra/time/benchmark.h" 12 | 13 | using eloq::camera; 14 | using Eloquent::Error::Exception; 15 | using Eloquent::Extra::Time::Benchmark; 16 | 17 | namespace Eloquent { 18 | namespace Esp32cam { 19 | namespace JPEG { 20 | /** 21 | * Eloquent interface to JPEGENC library 22 | */ 23 | class JPEGENCWrapper { 24 | public: 25 | uint8_t *bytes; 26 | JPEGENC jpeg; 27 | Exception exception; 28 | Benchmark benchmark; 29 | 30 | JPEGENCWrapper() : 31 | exception("JPEGENC"), 32 | _length(0), 33 | _numBytes(0), 34 | _pixelType(JPEGE_PIXEL_RGB565), 35 | _quality(JPEGE_Q_HIGH), 36 | _subsample(JPEGE_SUBSAMPLE_444) { 37 | 38 | } 39 | 40 | /** 41 | * 42 | */ 43 | void fromRGB565() { 44 | _pixelType = JPEGE_PIXEL_RGB565; 45 | } 46 | 47 | /** 48 | * 49 | */ 50 | void fromGrayscale() { 51 | _pixelType = JPEGE_PIXEL_GRAYSCALE; 52 | } 53 | 54 | /** 55 | * 56 | */ 57 | void toBestQuality() { 58 | _quality = JPEGE_Q_BEST; 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | void toGoodQuality() { 65 | _quality = JPEGE_Q_HIGH; 66 | } 67 | 68 | /** 69 | * 70 | */ 71 | void toMediumQuality() { 72 | _quality = JPEGE_Q_MED; 73 | } 74 | 75 | /** 76 | * 77 | */ 78 | void toLowQuality() { 79 | _quality = JPEGE_Q_LOW; 80 | } 81 | 82 | /** 83 | * 84 | */ 85 | void subsample() { 86 | _subsample = JPEGE_SUBSAMPLE_420; 87 | } 88 | 89 | /** 90 | * Allocate buffer for decoding 91 | */ 92 | Exception& allocate(size_t numBytes) { 93 | bytes = eloq::realloc(bytes, numBytes); 94 | _numBytes = numBytes; 95 | 96 | return bytes == NULL ? exception.set("Can't allocate memory") : exception.clear(); 97 | } 98 | 99 | /** 100 | * Encode raw pixels 101 | */ 102 | Exception& encode(uint8_t *pixels, uint16_t width, uint16_t height) { 103 | benchmark.benchmark([this, pixels, width, height]() { 104 | JPEGENCODE enc; 105 | 106 | if (JPEGE_SUCCESS != jpeg.open(bytes, _numBytes)) { 107 | exception.set("Cannot open JPEG"); 108 | return; 109 | } 110 | 111 | if (JPEGE_SUCCESS != jpeg.encodeBegin(&enc, width, height, _pixelType, _subsample, _quality)) { 112 | exception.set("Cannot setup encoding"); 113 | return; 114 | } 115 | 116 | if (JPEGE_SUCCESS != jpeg.addFrame(&enc, pixels, width * bpp())) { 117 | exception.set("Cannot add frame"); 118 | return; 119 | } 120 | 121 | _length = jpeg.close(); 122 | }); 123 | 124 | return _length > 0 ? exception.clear() : exception.set("Empty output"); 125 | } 126 | 127 | /** 128 | * Encode current frame 129 | */ 130 | Exception& encode() { 131 | if (!camera.hasFrame()) 132 | return exception.set("Can't encode empty frame"); 133 | 134 | return this->encode(camera.frame->buf, camera.frame->width, camera.frame->height); 135 | } 136 | 137 | /** 138 | * Get JPEG size 139 | */ 140 | inline size_t size() { 141 | return _length; 142 | } 143 | 144 | /** 145 | * Bytes per pixel 146 | */ 147 | inline uint8_t bpp() { 148 | return _pixelType == JPEGE_PIXEL_GRAYSCALE ? 1 : 2; 149 | } 150 | 151 | protected: 152 | size_t _length; 153 | size_t _numBytes; 154 | uint8_t _pixelType; 155 | uint8_t _quality; 156 | uint8_t _subsample; 157 | }; 158 | } 159 | } 160 | } 161 | 162 | namespace eloq { 163 | static Eloquent::Esp32cam::JPEG::JPEGENCWrapper jpegenc; 164 | } 165 | #endif 166 | 167 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/jpeg/picojpeg.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // picojpeg - Public domain, Rich Geldreich 3 | //------------------------------------------------------------------------------ 4 | #ifndef PICOJPEG_H 5 | #define PICOJPEG_H 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | // Error codes 12 | enum 13 | { 14 | PJPG_NO_MORE_BLOCKS = 1, 15 | PJPG_BAD_DHT_COUNTS, 16 | PJPG_BAD_DHT_INDEX, 17 | PJPG_BAD_DHT_MARKER, 18 | PJPG_BAD_DQT_MARKER, 19 | PJPG_BAD_DQT_TABLE, 20 | PJPG_BAD_PRECISION, 21 | PJPG_BAD_HEIGHT, 22 | PJPG_BAD_WIDTH, 23 | PJPG_TOO_MANY_COMPONENTS, 24 | PJPG_BAD_SOF_LENGTH, 25 | PJPG_BAD_VARIABLE_MARKER, 26 | PJPG_BAD_DRI_LENGTH, 27 | PJPG_BAD_SOS_LENGTH, 28 | PJPG_BAD_SOS_COMP_ID, 29 | PJPG_W_EXTRA_BYTES_BEFORE_MARKER, 30 | PJPG_NO_ARITHMITIC_SUPPORT, 31 | PJPG_UNEXPECTED_MARKER, 32 | PJPG_NOT_JPEG, 33 | PJPG_UNSUPPORTED_MARKER, 34 | PJPG_BAD_DQT_LENGTH, 35 | PJPG_TOO_MANY_BLOCKS, 36 | PJPG_UNDEFINED_QUANT_TABLE, 37 | PJPG_UNDEFINED_HUFF_TABLE, 38 | PJPG_NOT_SINGLE_SCAN, 39 | PJPG_UNSUPPORTED_COLORSPACE, 40 | PJPG_UNSUPPORTED_SAMP_FACTORS, 41 | PJPG_DECODE_ERROR, 42 | PJPG_BAD_RESTART_MARKER, 43 | PJPG_ASSERTION_ERROR, 44 | PJPG_BAD_SOS_SPECTRAL, 45 | PJPG_BAD_SOS_SUCCESSIVE, 46 | PJPG_STREAM_READ_ERROR, 47 | PJPG_NOTENOUGHMEM, 48 | PJPG_UNSUPPORTED_COMP_IDENT, 49 | PJPG_UNSUPPORTED_QUANT_TABLE, 50 | PJPG_UNSUPPORTED_MODE, // picojpeg doesn't support progressive JPEG's 51 | }; 52 | 53 | // Scan types 54 | typedef enum 55 | { 56 | PJPG_GRAYSCALE, 57 | PJPG_YH1V1, 58 | PJPG_YH2V1, 59 | PJPG_YH1V2, 60 | PJPG_YH2V2 61 | } pjpeg_scan_type_t; 62 | 63 | typedef struct 64 | { 65 | // Image resolution 66 | int m_width; 67 | int m_height; 68 | 69 | // Number of components (1 or 3) 70 | int m_comps; 71 | 72 | // Total number of minimum coded units (MCU's) per row/col. 73 | int m_MCUSPerRow; 74 | int m_MCUSPerCol; 75 | 76 | // Scan type 77 | pjpeg_scan_type_t m_scanType; 78 | 79 | // MCU width/height in pixels (each is either 8 or 16 depending on the scan type) 80 | int m_MCUWidth; 81 | int m_MCUHeight; 82 | 83 | // m_pMCUBufR, m_pMCUBufG, and m_pMCUBufB are pointers to internal MCU Y or RGB pixel component buffers. 84 | // Each time pjpegDecodeMCU() is called successfully these buffers will be filled with 8x8 pixel blocks of Y or RGB pixels. 85 | // Each MCU consists of (m_MCUWidth/8)*(m_MCUHeight/8) Y/RGB blocks: 1 for greyscale/no subsampling, 2 for H1V2/H2V1, or 4 blocks for H2V2 sampling factors. 86 | // Each block is a contiguous array of 64 (8x8) bytes of a single component: either Y for grayscale images, or R, G or B components for color images. 87 | // 88 | // The 8x8 pixel blocks are organized in these byte arrays like this: 89 | // 90 | // PJPG_GRAYSCALE: Each MCU is decoded to a single block of 8x8 grayscale pixels. 91 | // Only the values in m_pMCUBufR are valid. Each 8 bytes is a row of pixels (raster order: left to right, top to bottom) from the 8x8 block. 92 | // 93 | // PJPG_H1V1: Each MCU contains is decoded to a single block of 8x8 RGB pixels. 94 | // 95 | // PJPG_YH2V1: Each MCU is decoded to 2 blocks, or 16x8 pixels. 96 | // The 2 RGB blocks are at byte offsets: 0, 64 97 | // 98 | // PJPG_YH1V2: Each MCU is decoded to 2 blocks, or 8x16 pixels. 99 | // The 2 RGB blocks are at byte offsets: 0, 100 | // 128 101 | // 102 | // PJPG_YH2V2: Each MCU is decoded to 4 blocks, or 16x16 pixels. 103 | // The 2x2 block array is organized at byte offsets: 0, 64, 104 | // 128, 192 105 | // 106 | // It is up to the caller to copy or blit these pixels from these buffers into the destination bitmap. 107 | unsigned char *m_pMCUBufR; 108 | unsigned char *m_pMCUBufG; 109 | unsigned char *m_pMCUBufB; 110 | } pjpeg_image_info_t; 111 | 112 | typedef unsigned char (*pjpeg_need_bytes_callback_t)(unsigned char* pBuf, unsigned char buf_size, unsigned char *pBytes_actually_read, void *pCallback_data); 113 | 114 | // Initializes the decompressor. Returns 0 on success, or one of the above error codes on failure. 115 | // pNeed_bytes_callback will be called to fill the decompressor's internal input buffer. 116 | // If reduce is 1, only the first pixel of each block will be decoded. This mode is much faster because it skips the AC dequantization, IDCT and chroma upsampling of every image pixel. 117 | // Not thread safe. 118 | unsigned char pjpeg_decode_init(pjpeg_image_info_t *pInfo, pjpeg_need_bytes_callback_t pNeed_bytes_callback, void *pCallback_data, unsigned char reduce); 119 | 120 | // Decompresses the file's next MCU. Returns 0 on success, PJPG_NO_MORE_BLOCKS if no more blocks are available, or an error code. 121 | // Must be called a total of m_MCUSPerRow*m_MCUSPerCol times to completely decompress the image. 122 | // Not thread safe. 123 | unsigned char pjpeg_decode_mcu(void); 124 | 125 | #ifdef __cplusplus 126 | } 127 | #endif 128 | 129 | #endif // PICOJPEG_H -------------------------------------------------------------------------------- /src/eloquent_esp32cam/jpegdec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./jpeg/JPEGDECWrapper.h" -------------------------------------------------------------------------------- /src/eloquent_esp32cam/jpegenc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./jpeg/JPEGENCWrapper.h" -------------------------------------------------------------------------------- /src/eloquent_esp32cam/mem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace eloq { 4 | /** 5 | * Init buffer to zero 6 | * @tparam T 7 | * @param buffer 8 | * @param size 9 | * @return 10 | */ 11 | template 12 | T* zero(T* buffer, size_t size) { 13 | memset(buffer, 0, size); 14 | 15 | return buffer; 16 | } 17 | /** 18 | * Alias for malloc/ps_malloc 19 | * @tparam T 20 | * @param size 21 | * @return 22 | */ 23 | template 24 | T* alloc(size_t size) { 25 | if (psramFound()) { 26 | return zero(static_cast(ps_malloc(size)), size); 27 | } 28 | 29 | return zero(static_cast(malloc(size)), size); 30 | } 31 | 32 | /** 33 | * Alias for realloc/ps_realloc 34 | * @tparam T 35 | * @param existing 36 | * @param size 37 | * @return 38 | */ 39 | template 40 | T* realloc(T* existing, size_t size) { 41 | if (existing == NULL) 42 | return alloc(size); 43 | 44 | if (psramFound()) 45 | return zero(static_cast(ps_realloc(existing, size)), size); 46 | 47 | return zero(static_cast(realloc(existing, size)), size); 48 | } 49 | } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/motion/daemon.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_MOTION_DAEMON_H 2 | #define ELOQUENT_ESP32CAM_MOTION_DAEMON_H 3 | 4 | #include 5 | #include "../camera/camera.h" 6 | #include "../extra/esp32/multiprocessing/thread.h" 7 | 8 | using eloq::camera; 9 | using Eloquent::Extra::Esp32::Multiprocessing::Thread; 10 | using OnMotionCallback = std::function; 11 | 12 | 13 | namespace Eloquent { 14 | namespace Esp32cam { 15 | namespace Motion { 16 | /** 17 | * Run motion detection in a task 18 | * 19 | * @class Daemon 20 | * @author Simone 21 | * @date 13/12/2023 22 | * @file daemon.h 23 | * @brief 24 | */ 25 | template 26 | class Daemon { 27 | public: 28 | Thread thread; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @brief 34 | */ 35 | Daemon(T* detection) : 36 | thread("MotionDetection"), 37 | _detection(detection) { 38 | 39 | } 40 | 41 | /** 42 | * Run function when a face is detected 43 | * 44 | * @brief 45 | * @param callback 46 | */ 47 | void onMotion(OnMotionCallback callback) { 48 | _onMotion = callback; 49 | } 50 | 51 | /** 52 | * Start motion detection in background 53 | * 54 | * @brief 55 | */ 56 | void start() { 57 | thread 58 | .withArgs((void*) this) 59 | .withStackSize(5000) 60 | .run([](void *args) { 61 | Daemon *self = (Daemon*) args; 62 | 63 | delay(3000); 64 | 65 | while (true) { 66 | yield(); 67 | delay(1); 68 | 69 | if (!camera.capture().isOk()) 70 | continue; 71 | 72 | if (!self->_detection->run().isOk()) 73 | continue; 74 | 75 | if (!self->_detection->triggered()) 76 | continue; 77 | 78 | self->_onMotion(); 79 | } 80 | }); 81 | } 82 | 83 | protected: 84 | T *_detection; 85 | OnMotionCallback _onMotion; 86 | }; 87 | } 88 | } 89 | } 90 | 91 | 92 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/mqtt-stream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef MQTT_MAX_PACKET_SIZE 4 | #define MQTT_MAX_PACKET_SIZE 30000 5 | #endif 6 | 7 | #include 8 | #include 9 | #include "./extra/exception.h" 10 | 11 | using Eloquent::Error::Exception; 12 | 13 | typedef struct { 14 | uint8_t *buf; 15 | uint32_t len; 16 | } mqtt_frame_t; 17 | 18 | namespace Eloquent { 19 | namespace Esp32cam { 20 | /** 21 | * Stream camera frames over MQTT 22 | */ 23 | class MqttStream { 24 | public: 25 | Exception exception; 26 | 27 | /** 28 | * 29 | */ 30 | MqttStream() : 31 | exception("MqttStream"), 32 | _throttle(100) { 33 | 34 | } 35 | 36 | /** 37 | * Don't send frames too often 38 | * @param ms 39 | */ 40 | void throttle(size_t ms) { 41 | _throttle = ms; 42 | } 43 | 44 | /** 45 | * 46 | * @return 47 | */ 48 | Exception& begin(String clientID) { 49 | _clientID = clientID; 50 | _mqttClient.begin("mqtt.eloquentarduino.com", 1883, _wifiClient); 51 | 52 | if (WiFi.status() != WL_CONNECTED) 53 | return exception.set("WiFi not connected"); 54 | 55 | if (!_mqttClient.connect(_clientID.c_str(), _clientID.c_str(), "password")) { 56 | ESP_LOGE("MqttStream", "Can't connect to MQTT broker. Are you sure your CLIENT_ID is valid?"); 57 | return exception.set("Can't connect to MQTT broker"); 58 | } 59 | 60 | _queue = xQueueCreate(1, sizeof(mqtt_frame_t)); 61 | 62 | xTaskCreate([](void *args) { 63 | MqttStream *self = (MqttStream*) args; 64 | mqtt_frame_t frame; 65 | String topic = String("/") + self->_clientID + "/stream"; 66 | 67 | while (true) { 68 | // await for new frame 69 | if (xQueueReceive(self->_queue, &frame, 10) != pdPASS) 70 | continue; 71 | 72 | if (WiFi.status() != WL_CONNECTED) { 73 | ESP_LOGE("MqttStream", "WiFi not connected"); 74 | delay(500); 75 | continue; 76 | } 77 | 78 | if (!self->_mqttClient.connected()) { 79 | ESP_LOGE("MqttStream", "Can't connect to MQTT broker. Are you sure your CLIENT_ID is valid?"); 80 | delay(10); 81 | continue; 82 | } 83 | 84 | // publish 85 | if (frame.len > 0) { 86 | if (frame.len > MQTT_MAX_PACKET_SIZE) { 87 | ESP_LOGE("MqttStream", "Frame too large (%d bytes, max %d allowed)", frame.len, MQTT_MAX_PACKET_SIZE); 88 | continue; 89 | } 90 | 91 | if (!self->_mqttClient.publish(topic.c_str(), (const char*) frame.buf, frame.len)) 92 | ESP_LOGE("MqttStream", "Can't publish frame"); 93 | } 94 | 95 | // self->_mqttClient.loop(); 96 | } 97 | }, "MqttStream", 50000, this, 0, NULL); 98 | 99 | return exception.clear(); 100 | } 101 | 102 | /** 103 | * 104 | * @tparam Camera 105 | * @param camera 106 | * @return 107 | */ 108 | template 109 | Exception& queue(Camera& camera) { 110 | if (_queue == NULL) 111 | return exception.set("Queue is not initialized. Did you call begin()?"); 112 | 113 | const size_t now = millis(); 114 | 115 | // throttle 116 | if (now > _queuedAt && now - _queuedAt < _throttle) 117 | return exception.clear(); 118 | 119 | // discard old frame, if not consumed yet 120 | if (uxQueueSpacesAvailable(_queue) > 0) 121 | xQueueReset(_queue); 122 | 123 | if (!camera.hasFrame()) 124 | return exception.set("Cannot queue empty frame"); 125 | 126 | mqtt_frame_t frame = { 127 | .buf = camera.frame->buf, 128 | .len = camera.frame->len 129 | }; 130 | 131 | if (xQueueSendToFront(_queue, (void *) &frame, 0) != pdTRUE) 132 | return exception.set("Cannot queue frame"); 133 | 134 | _queuedAt = millis(); 135 | return exception.clear(); 136 | } 137 | 138 | protected: 139 | size_t _queuedAt; 140 | size_t _throttle; 141 | String _clientID; 142 | String _location; 143 | WiFiClient _wifiClient; 144 | MQTTClient _mqttClient; 145 | QueueHandle_t _queue; 146 | }; 147 | } 148 | } 149 | 150 | namespace eloq { 151 | static Eloquent::Esp32cam::MqttStream mqttStream; 152 | } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/remote-stream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "./extra/exception.h" 5 | 6 | namespace Eloquent { 7 | class RemoteStream { 8 | public: 9 | Exception exception; 10 | 11 | /** 12 | * 13 | */ 14 | RemoteStream() : 15 | exception("RemoteStream"), 16 | _throttle(100), 17 | _sentAt(0) { 18 | 19 | } 20 | 21 | /** 22 | * Don't send frames too often 23 | * @param ms 24 | */ 25 | void throttle(size_t ms) { 26 | _throttle = ms; 27 | } 28 | 29 | /** 30 | * 31 | * @param clientID 32 | * @return 33 | */ 34 | Exception& begin(String clientID) { 35 | _clientID = clientID; 36 | 37 | if (WiFi.status() != WL_CONNECTED) 38 | return exception.set("WiFi not connected"); 39 | 40 | if (!_wifiClient.connect(getServerName().c_str(), 10300)) 41 | return exception.set("Can't connect to streaming server"); 42 | 43 | return exception.clear(); 44 | } 45 | 46 | /** 47 | * 48 | * @param frame 49 | * @return 50 | */ 51 | Exception& send(camera_fb_t *frame) { 52 | const size_t now = millis(); 53 | 54 | if (now > _sentAt && now - _sentAt < _throttle) 55 | return exception.clear(); 56 | 57 | if (!_wifiClient.connected()) 58 | _wifiClient.connect(getServerName().c_str(), 10300); 59 | 60 | if (!_wifiClient.connected()) 61 | return exception.set("Can't connect to streaming server"); 62 | 63 | _wifiClient.print("##SOF##"); 64 | _wifiClient.print(_clientID); 65 | _wifiClient.print("##DATA##"); 66 | _wifiClient.write(frame->buf, frame->len); 67 | _wifiClient.print("##EOF##"); 68 | _wifiClient.flush(); 69 | 70 | return exception.clear(); 71 | } 72 | 73 | /** 74 | * 75 | * @return 76 | */ 77 | String address() { 78 | return String("http://") + getServerName() + "/esp32-cam-stream?client_id=" + _clientID; 79 | } 80 | 81 | protected: 82 | size_t _throttle; 83 | size_t _sentAt; 84 | String _clientID; 85 | WiFiClient _wifiClient; 86 | 87 | /** 88 | * 89 | * @return 90 | */ 91 | String getServerName() { 92 | return "mqtt.eloquentarduino.com"; 93 | } 94 | }; 95 | } 96 | 97 | namespace eloq { 98 | static Eloquent::RemoteStream remoteStream; 99 | } -------------------------------------------------------------------------------- /src/eloquent_esp32cam/viz/car_stream.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_VIZ_CAR_STREAM_H 2 | #define ELOQUENT_ESP32CAM_VIZ_CAR_STREAM_H 3 | 4 | #include "../extra/exception.h" 5 | #include "../extra/esp32/http/server.h" 6 | #include "../extra/car/car2wd.h" 7 | #include "./mjpeg.h" 8 | 9 | using namespace eloq; 10 | using Eloquent::Error::Exception; 11 | using Eloquent::Extra::Esp32::Http::HttpServer; 12 | using Eloquent::Extra::Car::Car2WD; 13 | 14 | 15 | namespace Eloquent { 16 | namespace Esp32cam { 17 | namespace Viz { 18 | /** 19 | * Display stream of camera 20 | * with car controls 21 | */ 22 | class CarStreamServer { 23 | public: 24 | Exception exception; 25 | HttpServer server; 26 | Car2WD *car; 27 | 28 | /** 29 | * 30 | */ 31 | CarStreamServer() : 32 | exception("CarStreamServer"), 33 | server("CarStreamServer") { 34 | 35 | } 36 | 37 | /** 38 | * Debug self IP address 39 | */ 40 | String address() const { 41 | return String("Car stream is available at http://") + wifi.ip(); 42 | } 43 | 44 | /** 45 | * Assign car 46 | */ 47 | void bind(Car2WD& car) { 48 | this->car = &car; 49 | } 50 | 51 | /** 52 | * Start server 53 | */ 54 | Exception& begin() { 55 | if (!viz::mjpeg.begin().isOk()) 56 | return exception.propagate(viz::mjpeg); 57 | 58 | onIndex(); 59 | onCommand(); 60 | 61 | return server.beginInThread(exception); 62 | } 63 | 64 | protected: 65 | 66 | /** 67 | * Display main page 68 | */ 69 | void onIndex() { 70 | 71 | } 72 | 73 | /** 74 | * Respond to commands over HTTP 75 | */ 76 | void onCommand() { 77 | server.onGET("cmd", [this](WebServer *web) { 78 | String cmd = web->arg("cmd"); 79 | 80 | if (cmd == "fwd") 81 | car->forward(); 82 | else if (cmd == "back") 83 | car->backward(); 84 | else if (cmd == "left") 85 | car->left(); 86 | else if (cmd == "right") 87 | car->right(); 88 | else 89 | car->stop(); 90 | }); 91 | } 92 | }; 93 | } 94 | } 95 | } 96 | 97 | 98 | namespace eloq { 99 | namespace viz { 100 | static Eloquent::Esp32cam::Viz::CarStreamServer carStream; 101 | } 102 | } 103 | 104 | #endif -------------------------------------------------------------------------------- /src/eloquent_esp32cam/viz/motion/roi_detection.h: -------------------------------------------------------------------------------- 1 | #ifndef ELOQUENT_ESP32CAM_VIZ_MOTION_ROI_DETECTION 2 | #define ELOQUENT_ESP32CAM_VIZ_MOTION_ROI_DETECTION 3 | 4 | #include "../../extra/exception.h" 5 | #include "../../extra/esp32/wifi/sta.h" 6 | #include "../../extra/esp32/http/server.h" 7 | #include "../../motion/roi_detection.h" 8 | #include "../mjpeg.h" 9 | 10 | using namespace eloq; 11 | using Eloquent::Error::Exception; 12 | using Eloquent::Extra::Esp32::Http::HttpServer; 13 | using Eloquent::Esp32cam::Motion::RoI; 14 | 15 | 16 | namespace Eloquent { 17 | namespace Esp32cam { 18 | namespace Viz { 19 | namespace Motion { 20 | /** 21 | * Visualize RoI motion detection 22 | */ 23 | class RoIMotionDetection { 24 | public: 25 | Exception exception; 26 | HttpServer server; 27 | 28 | /** 29 | * 30 | */ 31 | RoIMotionDetection() : 32 | _head(0), 33 | exception("RoIVizServer"), 34 | server("RoIVizServer") { 35 | 36 | } 37 | 38 | /** 39 | * Add roi to viz 40 | */ 41 | Exception& add(RoI &roi) { 42 | if (_head >= 10) 43 | return exception.set("Too many rois (10 max)"); 44 | 45 | _rois[_head++] = &roi; 46 | 47 | return exception.clear(); 48 | } 49 | 50 | /** 51 | * Debug self IP address 52 | */ 53 | String address() const { 54 | return String("RoI Viz is available at http://") + wifi.ip(); 55 | } 56 | 57 | /** 58 | * Start server 59 | */ 60 | Exception& begin() { 61 | if (!wifi.isConnected()) 62 | return exception.set("WiFi not connected"); 63 | 64 | // start mjpeg http server 65 | if (!mjpeg.begin().isOk()) 66 | return exception.propagate(mjpeg); 67 | 68 | onIndex(); 69 | onRoIs(); 70 | 71 | // run in thread 72 | server.thread.withStackSize(7000); 73 | 74 | if (!server.begin().isOk()) 75 | return exception.propagate(server); 76 | 77 | return exception.clear(); 78 | } 79 | 80 | 81 | protected: 82 | uint8_t _head; 83 | RoI *_rois[10]; 84 | 85 | /** 86 | * Register / endpoint to get main page 87 | */ 88 | void onIndex() { 89 | server.onGET("/", [this](WebServer *web) { 90 | web->send(200, "text/plain", "RoI Viz Server"); 91 | }); 92 | } 93 | 94 | /** 95 | * Register / endpoint to get main page 96 | */ 97 | void onRoIs() { 98 | server.onGET("/rois", [this](WebServer *web) { 99 | for (int i = 0; i < _head; i++) { 100 | RoI *roi = _rois[i]; 101 | 102 | roi->updateCoords(camera.resolution.getWidth(), camera.resolution.getHeight()); 103 | 104 | web->sendContent("{\"x1\":"); 105 | web->sendContent(String(roi->coords.x1)); 106 | web->sendContent(",\"x2\":"); 107 | web->sendContent(String(roi->coords.x2)); 108 | web->sendContent(",\"y1\":"); 109 | web->sendContent(String(roi->coords.y1)); 110 | web->sendContent(",\"y2\":"); 111 | web->sendContent(String(roi->coords.y2)); 112 | web->sendContent("}\n"); 113 | } 114 | 115 | web->sendContent(""); 116 | }); 117 | } 118 | }; 119 | } 120 | } 121 | } 122 | } 123 | 124 | namespace eloq { 125 | static Eloquent::Esp32cam::Viz::Motion::RoIMotionDetection roiViz; 126 | } 127 | 128 | #endif --------------------------------------------------------------------------------