// Original AI-Thinker ESP32-CAM / OV2640. Arduino ESP32 core 3.x; enable PSRAM. // Put your downloaded config.h beside this file. Never commit Wi-Fi credentials. #include #include #include #include #include #include #include #include #if __has_include("config.h") #include "config.h" #endif #ifndef WIFI_SSID #define WIFI_SSID "" #endif #ifndef WIFI_PASSWORD #define WIFI_PASSWORD "" #endif #ifndef LAB_DEVICE_TOKEN #define LAB_DEVICE_TOKEN "" #endif #ifndef LAB_BASE_URL #define LAB_BASE_URL "https://umfstimgpai.haloandrei.com" #endif // ISRG Root X1, valid through 2035. TLS hostname verification stays enabled. static const char ROOT_CA[] PROGMEM = R"CERT( -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- )CERT"; constexpr int CAPTURE_BUTTON = 13; // Optional button to GND. Conflicts with SD card. bool cameraReady = false; unsigned long lastPoll = 0, lastButton = 0; unsigned long lastWifiRetry = 0, lastClockNotice = 0; bool previousButton = HIGH; Preferences preferences; WebServer setupServer(80); String wifiSsid, wifiPassword, deviceToken; bool setupMode = false, wasConnected = false; bool validCredentials() { return wifiSsid.length()>0 && !wifiSsid.startsWith("YOUR_") && deviceToken.length()>=32 && !deviceToken.startsWith("GENERATE_"); } String randomSetupString() { char buffer[17]; snprintf(buffer,sizeof(buffer),"%08lx%08lx",(unsigned long)esp_random(),(unsigned long)esp_random()); return String(buffer); } void startSetupPortal() { setupMode=true; WiFi.mode(WIFI_AP); // STA MAC reads as zeros in AP-only mode; use the unique eFuse device bytes. String suffix=String((uint32_t)(ESP.getEfuseMac()>>24),HEX);suffix.toUpperCase(); String apName="UMFST-CAM-"+suffix; String apPassword=preferences.getString("setup_pass",""); if(apPassword.length()<8){apPassword=randomSetupString();preferences.putString("setup_pass",apPassword);} String nonce=randomSetupString(); if(!WiFi.softAP(apName.c_str(),apPassword.c_str())){Serial.println("Setup access point failed");return;} setupServer.on("/",HTTP_GET,[nonce](){ String html=R"HTML(UMFST camera setup

Connect your camera

AI-Thinker ESP32-CAM. No SD card needed.

Generate a camera configuration in umfstimgpai.haloandrei.com → Camera setup. Paste only the LAB_DEVICE_TOKEN value, without quotation marks. Settings are saved in the camera's internal flash memory.

)HTML"; html.replace("__NONCE__",nonce); setupServer.sendHeader("Cache-Control","no-store"); setupServer.send(200,"text/html",html); }); setupServer.on("/save",HTTP_POST,[nonce](){ if(setupServer.arg("nonce")!=nonce){setupServer.send(403,"text/plain","Reload the setup page and try again.");return;} String ssid=setupServer.arg("ssid"),password=setupServer.arg("password"),token=setupServer.arg("token"); token.trim(); bool tokenValid=token.length()>=32 && token.length()<=128; for(size_t i=0;i32 || password.length()>64 || (password.length()>0 && password.length()<8) || !tokenValid){ setupServer.send(400,"text/plain","Check the Wi-Fi name, password (8-64 characters), and token value. Go back to edit.");return; } bool saved=preferences.putString("ssid",ssid)>0; saved=(preferences.putString("wifi_pass",password)==password.length()) && saved; saved=(preferences.putString("token",token)>0) && saved; if(!saved){setupServer.send(500,"text/plain","Could not save settings. Restart the camera and try again.");return;} setupServer.sendHeader("Cache-Control","no-store"); setupServer.send(200,"text/html","

Settings saved

The camera is restarting. Reconnect your phone or computer to your normal Wi-Fi, then open https://umfstimgpai.haloandrei.com/ and choose Paired capture firmware. No RST press is needed.

"); delay(800);ESP.restart(); }); setupServer.begin(); Serial.println("\n=== CAMERA SETUP REQUIRED ==="); Serial.println("Setup Wi-Fi: "+apName); Serial.println("Setup password: "+apPassword); Serial.println("Setup page: http://192.168.4.1/"); Serial.println("No SD card is used. Leave RST released."); } bool initializeCamera() { camera_config_t c = {}; c.ledc_channel = LEDC_CHANNEL_0; c.ledc_timer = LEDC_TIMER_0; c.pin_d0 = 5; c.pin_d1 = 18; c.pin_d2 = 19; c.pin_d3 = 21; c.pin_d4 = 36; c.pin_d5 = 39; c.pin_d6 = 34; c.pin_d7 = 35; c.pin_xclk = 0; c.pin_pclk = 22; c.pin_vsync = 25; c.pin_href = 23; c.pin_sccb_sda = 26; c.pin_sccb_scl = 27; c.pin_pwdn = 32; c.pin_reset = -1; c.xclk_freq_hz = 20000000; c.pixel_format = PIXFORMAT_JPEG; c.frame_size = psramFound() ? FRAMESIZE_VGA : FRAMESIZE_QVGA; c.jpeg_quality = 12; c.fb_count = 1; c.fb_location = psramFound() ? CAMERA_FB_IN_PSRAM : CAMERA_FB_IN_DRAM; c.grab_mode = CAMERA_GRAB_WHEN_EMPTY; esp_err_t error = esp_camera_init(&c); if(error != ESP_OK) { // Container's GC2145 path uses native RGB565 and server-side JPEG encoding. Serial.println("JPEG initialization failed; trying native RGB565 at QVGA."); esp_camera_deinit(); c.pixel_format = PIXFORMAT_RGB565; c.frame_size = FRAMESIZE_QVGA; error = esp_camera_init(&c); } if (error != ESP_OK) { Serial.printf("Camera initialization failed: 0x%x\n", error); return false; } sensor_t *sensor=esp_camera_sensor_get(); camera_sensor_info_t *info=esp_camera_sensor_get_info(&sensor->id); Serial.printf("Camera ready: %s; PSRAM: %u bytes; format: %s\n",info?info->name:"unknown",ESP.getPsramSize(),c.pixel_format==PIXFORMAT_JPEG?"JPEG":"RGB565"); // Discard startup frames while auto-exposure and white balance settle. for(int i=0;i<5;i++){camera_fb_t *f=esp_camera_fb_get();if(f)esp_camera_fb_return(f);delay(80);} return true; } bool openHttp(HTTPClient &http, WiFiClientSecure &client, const String &path) { client.setCACert(ROOT_CA); client.setHandshakeTimeout(15); http.setConnectTimeout(10000); http.setTimeout(20000); http.setReuse(false); if (!http.begin(client, String(LAB_BASE_URL) + path)) return false; http.addHeader("Authorization", String("Bearer ") + deviceToken); return true; } void captureAndSend(const String &captureId = "") { if (!cameraReady || WiFi.status() != WL_CONNECTED) return; // Throw away any buffered frame so the submitted image is fresh. camera_fb_t *old = esp_camera_fb_get(); if(old) esp_camera_fb_return(old); camera_fb_t *frame = esp_camera_fb_get(); if (!frame) { Serial.println("No camera frame"); return; } WiFiClientSecure client; HTTPClient http; String path = "/api/device/frames"; if (captureId.length()) path += "?capture_id=" + captureId; if (openHttp(http, client, path)) { bool native = frame->format == PIXFORMAT_RGB565; http.addHeader("Content-Type", native ? "application/x-rgb565" : "image/jpeg"); http.addHeader("X-Frame-Format", native ? "rgb565-be" : "jpeg"); http.addHeader("X-Frame-Width", String(frame->width)); http.addHeader("X-Frame-Height", String(frame->height)); int status = http.POST(frame->buf, frame->len); Serial.printf("Capture: %u bytes, HTTP %d\n", unsigned(frame->len), status); if(status < 200 || status >= 300) Serial.println(http.getString()); http.end(); } esp_camera_fb_return(frame); } void pollCommand() { WiFiClientSecure client; HTTPClient http; if (!openHttp(http, client, "/api/device/commands")) return; int status = http.GET(); String command; if(status == 200) {command = http.getString();command.trim();} else Serial.printf("Poll HTTP %d (check token, clock, TLS and Wi-Fi)\n",status); http.end(); // Accept only UUID-shaped commands from the authenticated server. if (command.length() == 36) { bool valid = true; for (size_t i=0;i=10000) {lastWifiRetry=millis();WiFi.reconnect();} delay(100);return; } if(!wasConnected){wasConnected=true;Serial.println("Wi-Fi connected. IP: "+WiFi.localIP().toString());} if(time(nullptr)<1700000000) { if(millis()-lastClockNotice>=10000) { lastClockNotice=millis();Serial.println("Waiting for NTP time for TLS; retrying."); configTime(0,0,"pool.ntp.org","time.google.com"); } delay(100);return; } bool pressed = digitalRead(CAPTURE_BUTTON); if(previousButton==HIGH && pressed==LOW && millis()-lastButton>750) {lastButton=millis();captureAndSend();} previousButton=pressed; if(millis()-lastPoll>=2000) {lastPoll=millis();pollCommand();} delay(20); }