diff --git a/README.md b/README.md index 850b941..64e9e34 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # ESP32 MJPEG Multiclient Streaming Server +# Working example of this fork you can check out [here](http://sha256-mining.go.ro). This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM or ESP-EYE modules. @@ -9,7 +10,6 @@ This is tested to work with **VLC** and **Blynk** video widget. **This version uses FreeRTOS tasks to enable streaming to up to 10 connected clients** - Inspired by and based on this Instructable: [$9 RTSP Video Streamer Using the ESP32-CAM Board](https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/) Full story: https://www.hackster.io/anatoli-arkhipenko/multi-client-mjpeg-streaming-from-esp32-47768f diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index 9a998d3..604e90a 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -1,5 +1,4 @@ /* - This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM and ESP-EYE modules. This is tested to work with VLC and Blynk video widget and can support up to 10 @@ -18,67 +17,239 @@ Flash Size: 4Mb Patrition: Minimal SPIFFS PSRAM: Enabled + +# Modifications on this build: +# web interface added for viewing, configuring camera settings and wifi settings +# simple ftp server to upload the webserver html files to internal SPIFFS + ( edit the FtpServerKey.h file and modify to #define DEFAULT_STORAGE_TYPE_ESP32 STORAGE_SPIFFS ) +# arduino OTA for firmware upload +# some performance tweaks +# use of external wifi antena is highly recommended for the esp32cam board +# set "Events Run On: core 0" and "Arduino Run On: core 0" +# set "Erase All Flash Before Sketch Upload: Disabled" to prevent SPIFFS deletion +# used the board flash on gpio 4 +# used the board led on gpio 33 +# added external led on gpio 2 +# added a button to enter AP mode and configure Wifi credentials which are then saved to SPIFFS and loaded on boot along with camera settings + (button is connected through a 220 ohm series rezistor from gpio 13 to 14) */ // ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) #define APP_CPU 1 #define PRO_CPU 0 - -#include "src/OV2640.h" -#include -#include -#include - -#include -#include -#include -#include - // Select camera model //#define CAMERA_MODEL_WROVER_KIT -#define CAMERA_MODEL_ESP_EYE +//#define CAMERA_MODEL_ESP_EYE //#define CAMERA_MODEL_M5STACK_PSRAM //#define CAMERA_MODEL_M5STACK_WIDE -//#define CAMERA_MODEL_AI_THINKER +#define CAMERA_MODEL_AI_THINKER +#define FLASH_PIN 4 // Define the GPIO pin for the flash LED +#define LED_PIN 33 // Define the GPIO pin for the internal LED +#define RED_LED_PIN 2 // Define the GPIO pin for the external RED LED +#define BUTTON_PIN_INPUT 13 // GPIO 13 +#define BUTTON_PIN_OUTPUT 14 // GPIO 14 +#include "src/OV2640.h" +#include +#include +#include +#include +#include +#include +#include +#include #include "camera_pins.h" +#include -/* - Next one is an include with wifi credentials. - This is what you need to do: - - 1. Create a file called "home_wifi_multi.h" in the same folder OR under a separate subfolder of the "libraries" folder of Arduino IDE. (You are creating a "fake" library really - I called it "MySettings"). - 2. Place the following text in the file: - #define SSID1 "replace with your wifi ssid" - #define PWD1 "replace your wifi password" - 3. Save. +unsigned long lastTime = 0; +unsigned long timerDelay = 50; +int led_state = LOW; +bool led_flag = false; - Should work then -*/ -#include "home_wifi_multi.h" +String ssid; +String password; +String hostname; OV2640 cam; - +Ticker debounceTicker; WebServer server(80); +FtpServer ftpSrv; // ===== rtos task handles ========================= // Streaming is implemented with 3 tasks: -TaskHandle_t tMjpeg; // handles client connections to the webserver -TaskHandle_t tCam; // handles getting picture frames from the camera and storing them locally -TaskHandle_t tStream; // actually streaming frames to all connected clients - +TaskHandle_t tMjpeg = NULL; // handles client connections to the webserver +//TaskHandle_t tCam; // handles getting picture frames from the camera and storing them locally +TaskHandle_t camTaskHandle = NULL; +//TaskHandle_t tStream; // actually streaming frames to all connected clients +TaskHandle_t streamTaskHandle = NULL; // frameSync semaphore is used to prevent streaming buffer as it is replaced with the next frame SemaphoreHandle_t frameSync = NULL; - +// camSync semaphore is used to sync access to the camera +SemaphoreHandle_t camSync = NULL; // Queue stores currently connected clients to whom we are streaming QueueHandle_t streamingClients; +// We will try to achieve 14 FPS frame rate +int FPS = 14; // Default FPS value +// We will handle web client requests every 10 ms (100 Hz) +const int WSINTERVAL = 10; + +// Function to handle camera settings +void handleCameraSettings() { + if (server.hasArg("plain") == false) { // Check if body received + server.send(400, "application/json", "{\"status\":\"Invalid Request\"}"); + return; + } -// We will try to achieve 25 FPS frame rate -const int FPS = 14; + String body = server.arg("plain"); + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, body); -// We will handle web client requests every 50 ms (20 Hz) -const int WSINTERVAL = 100; + if (error) { + server.send(400, "application/json", "{\"status\":\"Invalid JSON\"}"); + return; + } + + int quality = doc["quality"]; + int brightness = doc["brightness"]; + int contrast = doc["contrast"]; + int saturation = doc["saturation"]; + int specialEffect = doc["specialEffect"]; + int whiteBalance = doc["whiteBalance"]; + int awbGain = doc["awbGain"]; + int wbMode = doc["wbMode"]; + int hmirror = doc["hmirror"]; + int vflip = doc["vflip"]; + int colorbar = doc["colorbar"]; + int gammaCorrection = doc["gammaCorrection"]; + int aec2 = doc["aec2"]; + int aeLevel = doc["aeLevel"]; + int aecValue = doc["aecValue"]; + int exposureControl = doc["exposureControl"]; + int gainControl = doc["gainControl"]; + int agcGain = doc["agcGain"]; + int dcw = doc["dcw"]; + int fps = doc["fps"]; // Retrieve the FPS setting + int led = doc["led"]; // Retrieve the led setting + String resolution = doc["resolution"]; + // Apply camera settings + sensor_t* s = esp_camera_sensor_get(); + if (resolution == "QVGA") { + s->set_framesize(s, FRAMESIZE_QVGA); + } else if (resolution == "VGA") { + s->set_framesize(s, FRAMESIZE_VGA); + } else if (resolution == "SVGA") { + s->set_framesize(s, FRAMESIZE_SVGA); + } else if (resolution == "XGA") { + s->set_framesize(s, FRAMESIZE_XGA); + } else if (resolution == "SXGA") { + s->set_framesize(s, FRAMESIZE_SXGA); + } else if (resolution == "UXGA") { + s->set_framesize(s, FRAMESIZE_UXGA); + } + s->set_quality(s, quality); + s->set_brightness(s, brightness); + s->set_contrast(s, contrast); + s->set_saturation(s, saturation); + s->set_special_effect(s, specialEffect); + s->set_whitebal(s, whiteBalance); // Auto White Balance + s->set_awb_gain(s, awbGain); // AWB Gain + s->set_wb_mode(s, wbMode); // White Balance Mode + s->set_hmirror(s, hmirror); // Horizontal Mirror + s->set_vflip(s, vflip); // Vertical Flip + s->set_colorbar(s, colorbar); // Color Bar + s->set_raw_gma(s, gammaCorrection); // RAW Gamma Correction + s->set_aec2(s, aec2); // AEC2 + s->set_ae_level(s, aeLevel); // AE Level + s->set_aec_value(s, aecValue); // AEC Value + s->set_exposure_ctrl(s, exposureControl); // Exposure Control + s->set_gain_ctrl(s, gainControl); // Gain Control + s->set_agc_gain(s, agcGain); // AGC Gain + s->set_dcw(s, dcw); // Downsize Mode + FPS = fps; // Update the global FPS variable + if (led == 1) { + digitalWrite(FLASH_PIN, HIGH); + } else { + digitalWrite(FLASH_PIN, LOW); + } + // Save settings to SPIFFS + saveSettings(body); + // Send response + server.send(200, "application/json", "{\"status\":\"Settings applied\"}"); +} +// Apply settings on reboot +void applySettings(const String& settings) { + // Parse the settings and apply them to the camera + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, settings); + + if (error) { + Serial.println("Failed to parse settings"); + return; + } + int quality = doc["quality"]; + int brightness = doc["brightness"]; + int contrast = doc["contrast"]; + int saturation = doc["saturation"]; + int specialEffect = doc["specialEffect"]; + int whiteBalance = doc["whiteBalance"]; + int awbGain = doc["awbGain"]; + int wbMode = doc["wbMode"]; + int hmirror = doc["hmirror"]; + int vflip = doc["vflip"]; + int colorbar = doc["colorbar"]; + int gammaCorrection = doc["gammaCorrection"]; + int aec2 = doc["aec2"]; + int aeLevel = doc["aeLevel"]; + int aecValue = doc["aecValue"]; + int exposureControl = doc["exposureControl"]; + int gainControl = doc["gainControl"]; + int agcGain = doc["agcGain"]; + int dcw = doc["dcw"]; + int fps = doc["fps"]; // Retrieve the FPS setting + int led = doc["led"]; // Retrieve the led setting + String resolution = doc["resolution"]; + // Apply camera settings + sensor_t* s = esp_camera_sensor_get(); + if (resolution == "QVGA") { + s->set_framesize(s, FRAMESIZE_QVGA); + } else if (resolution == "VGA") { + s->set_framesize(s, FRAMESIZE_VGA); + } else if (resolution == "SVGA") { + s->set_framesize(s, FRAMESIZE_SVGA); + } else if (resolution == "XGA") { + s->set_framesize(s, FRAMESIZE_XGA); + } else if (resolution == "SXGA") { + s->set_framesize(s, FRAMESIZE_SXGA); + } else if (resolution == "UXGA") { + s->set_framesize(s, FRAMESIZE_UXGA); + } + s->set_quality(s, quality); + s->set_brightness(s, brightness); + s->set_contrast(s, contrast); + s->set_saturation(s, saturation); + s->set_special_effect(s, specialEffect); + s->set_whitebal(s, whiteBalance); // Auto White Balance + s->set_awb_gain(s, awbGain); // AWB Gain + s->set_wb_mode(s, wbMode); // White Balance Mode + s->set_hmirror(s, hmirror); // Horizontal Mirror + s->set_vflip(s, vflip); // Vertical Flip + s->set_colorbar(s, colorbar); // Color Bar + s->set_raw_gma(s, gammaCorrection); // RAW Gamma Correction + s->set_aec2(s, aec2); // AEC2 + s->set_ae_level(s, aeLevel); // AE Level + s->set_aec_value(s, aecValue); // AEC Value + s->set_exposure_ctrl(s, exposureControl); // Exposure Control + s->set_gain_ctrl(s, gainControl); // Gain Control + s->set_agc_gain(s, agcGain); // AGC Gain + s->set_dcw(s, dcw); // Downsize Mode + FPS = fps; // Update the global FPS variable + if (led == 1) { + digitalWrite(FLASH_PIN, HIGH); + } else { + digitalWrite(FLASH_PIN, LOW); + } +} // ======== Server Connection Handler Task ========================== void mjpegCB(void* pvParameters) { @@ -87,101 +258,151 @@ void mjpegCB(void* pvParameters) { // Creating frame synchronization semaphore and initializing it frameSync = xSemaphoreCreateBinary(); - xSemaphoreGive( frameSync ); + xSemaphoreGive(frameSync); + // Creating camera synchronization semaphore and initializing it + camSync = xSemaphoreCreateBinary(); + xSemaphoreGive(camSync); // Creating a queue to track all connected clients - streamingClients = xQueueCreate( 10, sizeof(WiFiClient*) ); + streamingClients = xQueueCreate(10, sizeof(WiFiClient*)); //=== setup section ================== - // Creating RTOS task for grabbing frames from the camera xTaskCreatePinnedToCore( - camCB, // callback - "cam", // name - 4096, // stacj size - NULL, // parameters - 2, // priority - &tCam, // RTOS task handle - APP_CPU); // core + camCB, // callback + "cam", // name + 6 * 1024, // stack size + NULL, // parameters + 6, // priority + &camTaskHandle, // RTOS task handle + APP_CPU); // core // Creating task to push the stream to all connected clients xTaskCreatePinnedToCore( streamCB, "strmCB", - 4 * 1024, - NULL, //(void*) handler, - 2, - &tStream, - APP_CPU); + 6 * 1024, + NULL, // parameters + 6, // priority + &streamTaskHandle, // RTOS task handle + PRO_CPU); // core // Registering webserver handling routines - server.on("/mjpeg/1", HTTP_GET, handleJPGSstream); + server.on("/stream", HTTP_GET, handleJPGSstream); server.on("/jpg", HTTP_GET, handleJPG); + server.on("/", HTTP_GET, []() { + File file = SPIFFS.open("/index.html", "r"); + server.streamFile(file, "text/html"); + file.close(); + }); + server.on("/cam_settings", HTTP_GET, []() { + File file = SPIFFS.open("/cam_settings.html", "r"); + server.streamFile(file, "text/html"); + file.close(); + }); + server.on("/wifi_settings", HTTP_GET, []() { + File file = SPIFFS.open("/wifi_settings.html", "r"); + server.streamFile(file, "text/html"); + file.close(); + }); + server.on("/getSettings", HTTP_GET, []() { + String settings = loadSettings(); + server.send(200, "application/json", settings); + }); + server.on("/saveSettings", HTTP_POST, []() { + if (server.hasArg("plain")) { + String body = server.arg("plain"); + File file = SPIFFS.open("/wifi_settings.json", FILE_WRITE); + if (!file) { + server.send(500, "application/json", "{\"message\":\"Failed to save settings\"}"); + return; + } + file.print(body); + file.close(); + server.send(200, "application/json", "{\"message\":\"Settings saved successfully\"}"); + } + }); + server.on("/reboot", HTTP_POST, []() { + server.send(200, "application/json", "{\"message\":\"Rebooting...\"}"); + // Delete the ap_mode.flag file + if (SPIFFS.exists("/ap_mode.flag")) { + SPIFFS.remove("/ap_mode.flag"); + } + // Deinitialize camera + camDeinit(); + ESP.restart(); + }); + server.on("/rebootAp", HTTP_POST, []() { + File file = SPIFFS.open("/ap_mode.flag", FILE_WRITE); + if (!file) { + server.send(500, "application/json", "{\"message\":\"Failed to set AP mode flag\"}"); + return; + } + file.close(); + server.send(200, "application/json", "{\"message\":\"Rebooting into AP mode...\"}"); + // Deinitialize camera + camDeinit(); + ESP.restart(); + }); + // Serve static files + server.serveStatic("/cam_styles.css", SPIFFS, "/cam_styles.css"); + server.serveStatic("/cam_script.js", SPIFFS, "/cam_script.js"); + server.serveStatic("/wifi_styles.css", SPIFFS, "/wifi_styles.css"); + server.serveStatic("/wifi_script.js", SPIFFS, "/wifi_script.js"); + server.serveStatic("/index_script.js", SPIFFS, "/index_script.js"); + server.on("/settings", HTTP_POST, handleCameraSettings); server.onNotFound(handleNotFound); // Starting webserver server.begin(); - //=== loop() section =================== xLastWakeTime = xTaskGetTickCount(); for (;;) { server.handleClient(); - // After every server client handling request, we let other tasks run and then pause taskYIELD(); vTaskDelayUntil(&xLastWakeTime, xFrequency); } } - // Commonly used variables: -volatile size_t camSize; // size of the current frame, byte -volatile char* camBuf; // pointer to the current frame - +volatile size_t camSize; // size of the current frame, byte +volatile char* camBuf; // pointer to the current frame // ==== RTOS task to grab frames from the camera ========================= void camCB(void* pvParameters) { - TickType_t xLastWakeTime; - // A running interval associated with currently desired frame rate const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS); - // Mutex for the critical section of swithing the active frames around portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED; - // Pointers to the 2 frames, their respective sizes and index of the current frame char* fbs[2] = { NULL, NULL }; size_t fSize[2] = { 0, 0 }; int ifb = 0; - //=== loop() section =================== xLastWakeTime = xTaskGetTickCount(); - for (;;) { - // Grab a frame from the camera and query its size + // lock access to the camera until we've read the frame + xSemaphoreTake(camSync, portMAX_DELAY); cam.run(); size_t s = cam.getSize(); - // If frame size is more that we have previously allocated - request 125% of the current frame space if (s > fSize[ifb]) { fSize[ifb] = s * 4 / 3; fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]); } - // Copy current frame into local buffer - char* b = (char*) cam.getfb(); + char* b = (char*)cam.getfb(); memcpy(fbs[ifb], b, s); - + xSemaphoreGive(camSync); // Let other tasks run and wait until the end of the current frame rate interval (if any time left) taskYIELD(); vTaskDelayUntil(&xLastWakeTime, xFrequency); - // Only switch frames around if no frame is currently being streamed to a client // Wait on a semaphore until client operation completes - xSemaphoreTake( frameSync, portMAX_DELAY ); - + xSemaphoreTake(frameSync, portMAX_DELAY); // Do not allow interrupts while switching the current frame portENTER_CRITICAL(&xSemaphore); camBuf = fbs[ifb]; @@ -189,53 +410,42 @@ void camCB(void* pvParameters) { ifb++; ifb &= 1; // this should produce 1, 0, 1, 0, 1 ... sequence portEXIT_CRITICAL(&xSemaphore); - // Let anyone waiting for a frame know that the frame is ready - xSemaphoreGive( frameSync ); - + xSemaphoreGive(frameSync); // Technically only needed once: let the streaming task know that we have at least one frame // and it could start sending frames to the clients, if any - xTaskNotifyGive( tStream ); - + xTaskNotifyGive(streamTaskHandle); // Immediately let other (streaming) tasks run taskYIELD(); - // If streaming task has suspended itself (no active clients to stream to) // there is no need to grab frames from the camera. We can save some juice // by suspedning the tasks - if ( eTaskGetState( tStream ) == eSuspended ) { + if (eTaskGetState(streamTaskHandle) == eSuspended) { vTaskSuspend(NULL); // passing NULL means "suspend yourself" } } } - // ==== Memory allocator that takes advantage of PSRAM if present ======================= char* allocateMemory(char* aPtr, size_t aSize) { - // Since current buffer is too smal, free it if (aPtr != NULL) free(aPtr); - - size_t freeHeap = ESP.getFreeHeap(); char* ptr = NULL; - // If memory requested is more than 2/3 of the currently free heap, try PSRAM immediately - if ( aSize > freeHeap * 2 / 3 ) { - if ( psramFound() && ESP.getFreePsram() > aSize ) { - ptr = (char*) ps_malloc(aSize); + if (aSize > freeHeap * 2 / 3) { + if (psramFound() && ESP.getFreePsram() > aSize) { + ptr = (char*)ps_malloc(aSize); } - } - else { + } else { // Enough free heap - let's try allocating fast RAM as a buffer - ptr = (char*) malloc(aSize); + ptr = (char*)malloc(aSize); // If allocation on the heap failed, let's give PSRAM one more chance: - if ( ptr == NULL && psramFound() && ESP.getFreePsram() > aSize) { - ptr = (char*) ps_malloc(aSize); + if (ptr == NULL && psramFound() && ESP.getFreePsram() > aSize) { + ptr = (char*)ps_malloc(aSize); } } - // Finally, if the memory pointer is NULL, we were not able to allocate any memory, and that is a terminal condition. if (ptr == NULL) { ESP.restart(); @@ -243,10 +453,9 @@ char* allocateMemory(char* aPtr, size_t aSize) { return ptr; } - // ==== STREAMING ====================================================== -const char HEADER[] = "HTTP/1.1 200 OK\r\n" \ - "Access-Control-Allow-Origin: *\r\n" \ +const char HEADER[] = "HTTP/1.1 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n"; const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n"; const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: "; @@ -256,87 +465,77 @@ const int cntLen = strlen(CTNTTYPE); // ==== Handle connection request from clients =============================== -void handleJPGSstream(void) -{ +void handleJPGSstream(void) { // Can only acommodate 10 clients. The limit is a default for WiFi connections - if ( !uxQueueSpacesAvailable(streamingClients) ) return; - - + if (!uxQueueSpacesAvailable(streamingClients)) return; // Create a new WiFi Client object to keep track of this one WiFiClient* client = new WiFiClient(); *client = server.client(); - // Immediately send this client a header client->write(HEADER, hdrLen); client->write(BOUNDARY, bdrLen); - // Push the client to the streaming queue - xQueueSend(streamingClients, (void *) &client, 0); - + xQueueSend(streamingClients, (void*)&client, 0); // Wake up streaming tasks, if they were previously suspended: - if ( eTaskGetState( tCam ) == eSuspended ) vTaskResume( tCam ); - if ( eTaskGetState( tStream ) == eSuspended ) vTaskResume( tStream ); + if (eTaskGetState(camTaskHandle) == eSuspended) vTaskResume(camTaskHandle); + if (eTaskGetState(streamTaskHandle) == eSuspended) vTaskResume(streamTaskHandle); } // ==== Actually stream content to all connected clients ======================== -void streamCB(void * pvParameters) { +void streamCB(void* pvParameters) { char buf[16]; TickType_t xLastWakeTime; TickType_t xFrequency; // Wait until the first frame is captured and there is something to send // to clients - ulTaskNotifyTake( pdTRUE, /* Clear the notification value before exiting. */ - portMAX_DELAY ); /* Block indefinitely. */ - + ulTaskNotifyTake(pdTRUE, /* Clear the notification value before exiting. */ + portMAX_DELAY); /* Block indefinitely. */ xLastWakeTime = xTaskGetTickCount(); + for (;;) { // Default assumption we are running according to the FPS xFrequency = pdMS_TO_TICKS(1000 / FPS); - // Only bother to send anything if there is someone watching UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients); - if ( activeClients ) { + if (activeClients) { // Adjust the period to the number of connected clients xFrequency /= activeClients; - // Since we are sending the same frame to everyone, // pop a client from the the front of the queue - WiFiClient *client; - xQueueReceive (streamingClients, (void*) &client, 0); + WiFiClient* client; + xQueueReceive(streamingClients, (void*)&client, 0); // Check if this client is still connected. - if (!client->connected()) { // delete this client reference if s/he has disconnected // and don't put it back on the queue anymore. Bye! delete client; - } - else { - + led_flag = false; + } else { + led_flag = true; // Ok. This is an actively connected client. // Let's grab a semaphore to prevent frame changes while we // are serving this frame - xSemaphoreTake( frameSync, portMAX_DELAY ); + xSemaphoreTake(frameSync, portMAX_DELAY); client->write(CTNTTYPE, cntLen); sprintf(buf, "%d\r\n\r\n", camSize); client->write(buf, strlen(buf)); - client->write((char*) camBuf, (size_t)camSize); + client->write((char*)camBuf, (size_t)camSize); client->write(BOUNDARY, bdrLen); // Since this client is still connected, push it to the end // of the queue for further processing - xQueueSend(streamingClients, (void *) &client, 0); + xQueueSend(streamingClients, (void*)&client, 0); // The frame has been served. Release the semaphore and let other tasks run. // If there is a frame switch ready, it will happen now in between frames - xSemaphoreGive( frameSync ); + xSemaphoreGive(frameSync); taskYIELD(); } - } - else { + } else { // Since there are no connected clients, there is no reason to waste battery running vTaskSuspend(NULL); } @@ -346,29 +545,28 @@ void streamCB(void * pvParameters) { } } - - -const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \ - "Content-disposition: inline; filename=capture.jpg\r\n" \ +const char JHEADER[] = "HTTP/1.1 200 OK\r\n" + "Content-disposition: inline; filename=capture.jpg\r\n" "Content-type: image/jpeg\r\n\r\n"; const int jhdLen = strlen(JHEADER); // ==== Serve up one JPEG frame ============================================= -void handleJPG(void) -{ +void handleJPG(void) { WiFiClient client = server.client(); if (!client.connected()) return; + digitalWrite(FLASH_PIN, HIGH); // flash on for capture jpg + xSemaphoreTake(camSync, portMAX_DELAY); cam.run(); client.write(JHEADER, jhdLen); client.write((char*)cam.getfb(), cam.getSize()); + xSemaphoreGive(camSync); + digitalWrite(FLASH_PIN, LOW); } - - // ==== Handle invalid URL requests ============================================ -void handleNotFound() -{ +void handleNotFound() { String message = "Server is running!\n\n"; + message += "input address is wrong!\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; @@ -378,18 +576,33 @@ void handleNotFound() message += "\n"; server.send(200, "text / plain", message); } +// ==== SETUP method ================================================================== +void setup() { + // Setup Serial connection: + Serial.begin(115200); + delay(1000); // wait for a second to let Serial connect + // Initialize LED pins + pinMode(FLASH_PIN, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(RED_LED_PIN, OUTPUT); + digitalWrite(LED_PIN, HIGH); // internal led pin is inverted + pinMode(BUTTON_PIN_INPUT, INPUT_PULLUP); // Set GPIO 13 as input + digitalWrite(BUTTON_PIN_INPUT, HIGH); // Initialize GPIO 13 as high -// ==== SETUP method ================================================================== -void setup() -{ + pinMode(BUTTON_PIN_OUTPUT, OUTPUT); // Set GPIO 14 as output + digitalWrite(BUTTON_PIN_OUTPUT, LOW); // Initialize GPIO 14 as LOW - // Setup Serial connection: - Serial.begin(115200); - delay(1000); // wait for a second to let Serial connect + // Attach the interrupt to pin 13, triggering on FALLING (HIGH to LOW transition) + attachInterrupt(digitalPinToInterrupt(BUTTON_PIN_INPUT), handleButtonInterrupt, FALLING); + // Initialize SPIFFS + if (!SPIFFS.begin(true)) { + Serial.println("An error has occurred while mounting SPIFFS"); + return; + } // Configure the camera camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; @@ -412,58 +625,334 @@ void setup() config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; - + config.fb_location = CAMERA_FB_IN_PSRAM; + //config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; /*!< Fills buffers when they are empty. Less resources but first 'fb_count' frames might be old */ + config.grab_mode = CAMERA_GRAB_LATEST; /*!< Except when 1 frame buffer is used, queue will always contain the last 'fb_count' frames */ // Frame parameters: pick one // config.frame_size = FRAMESIZE_UXGA; // config.frame_size = FRAMESIZE_SVGA; // config.frame_size = FRAMESIZE_QVGA; - config.frame_size = FRAMESIZE_VGA; - config.jpeg_quality = 12; + config.frame_size = FRAMESIZE_UXGA; + config.jpeg_quality = 20; config.fb_count = 2; -#if defined(CAMERA_MODEL_ESP_EYE) - pinMode(13, INPUT_PULLUP); - pinMode(14, INPUT_PULLUP); -#endif - if (cam.init(config) != ESP_OK) { Serial.println("Error initializing the camera"); delay(10000); ESP.restart(); } + sensor_t* s = esp_camera_sensor_get(); + s->set_brightness(s, 0); // -2 to 2 + s->set_contrast(s, 0); // -2 to 2 + s->set_saturation(s, 0); // -2 to 2 + //s->set_sharpness(s, 2); // -2 to 2 //unsuported + s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia) + s->set_whitebal(s, 1); // 0 = disable , 1 = enable + s->set_awb_gain(s, 1); // 0 = disable , 1 = enable + s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) + s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable + s->set_aec2(s, 1); // 0 = disable , 1 = enable + s->set_ae_level(s, 0); // -2 to 2 + s->set_aec_value(s, 600); // 0 to 1200 + s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable + s->set_agc_gain(s, 15); // 0 to 30 + s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6 + s->set_bpc(s, 1); // 0 = disable , 1 = enable + s->set_wpc(s, 1); // 0 = disable , 1 = enable + s->set_raw_gma(s, 1); // 0 = disable , 1 = enable + s->set_lenc(s, 1); // 0 = disable , 1 = enable + s->set_hmirror(s, 1); // 0 = disable , 1 = enable + s->set_vflip(s, 1); // 0 = disable , 1 = enable + s->set_dcw(s, 1); // 0 = disable , 1 = enable + s->set_colorbar(s, 0); // 0 = disable , 1 = enable + //s->set_denoise(s, 2); //unsuported + delay(1000); + + createDefault_cam_Settings(); // Create default cam settings if they don't exist + // Load and apply camera settings on startup + String settings = loadSettings(); + if (settings != "") { + applySettings(settings); + } + // Configure and connect to WiFi + createDefault_wifi_Settings(); // Create default wifi settings if they don't exist + // Check if we should boot into AP mode + if (shouldBootAPMode()) { + setupAPMode(); + } else { + load_wifi_Settings(); + wifi_Connect(); + } + // Start mainstreaming RTOS task + xTaskCreatePinnedToCore( + mjpegCB, + "mjpeg", + 4 * 1024, + NULL, + 6, + &tMjpeg, + PRO_CPU); + + ftpSrv.begin("user", "pasw"); //username, password for ftp. set ports in ESP8266FtpServer.h (default 21, 50009 for PASV) + Serial.println("FTP Server Ready"); + ota_setup(); +} + +// ISR to start debounce timer +void handleButtonInterrupt() { + // Start the debounceTicker + debounceTicker.attach_ms(50, button_check); // check button state after 50ms debouncing time +} + +void loop() { + unsigned long currentMillis = millis(); + // Handle LED blinking logic, signal if there are any clients watching + if (((currentMillis - lastTime) >= timerDelay) && (led_flag)) { + lastTime = currentMillis; + led_state = !led_state; // Toggle LED state + digitalWrite(LED_PIN, led_state); + digitalWrite(RED_LED_PIN, led_state); + } else if (!led_flag && led_state == HIGH) { + led_state = LOW; + digitalWrite(LED_PIN, HIGH); + digitalWrite(RED_LED_PIN, LOW); + } + + ArduinoOTA.handle(); // Check for OTA updates + ftpSrv.handleFTP(); // Handle FTP server + taskYIELD(); + vTaskDelay(pdMS_TO_TICKS(1)); +} + +void camDeinit() { + // Deinitialize camera + if (cam.deinit() != ESP_OK) { + Serial.println("Error deinitializing the camera!"); + } else { + Serial.println("Deinitializing the camera!"); + } +} +// Check for button press and if pressed reboot into AP mode +void button_check() { - // Configure and connect to WiFi - IPAddress ip; + debounceTicker.detach(); // Stops the debounce ticker + + if (digitalRead(BUTTON_PIN_INPUT) == LOW) { + File file = SPIFFS.open("/ap_mode.flag", FILE_WRITE); + if (!file) { + Serial.println("Failed to create file"); + return; + } + file.close(); + // Deinitialize camera + camDeinit(); + ESP.restart(); + } +} +void setupAPMode() { + WiFi.softAP("ESP32CAM-AP", "12345678"); + // Set custom IP address + IPAddress local_IP(192, 168, 0, 1); // Change to your desired IP address + IPAddress gateway(192, 168, 0, 1); // Change to your desired gateway address + IPAddress subnet(255, 255, 255, 0); // Subnet mask + WiFi.softAPConfig(local_IP, gateway, subnet); + + IPAddress IP = WiFi.softAPIP(); + Serial.print("AP IP address: "); + Serial.println(IP); +} + +// Function to save settings to SPIFFS +void saveSettings(const String& settings) { + File file = SPIFFS.open("/cam_settings.json", FILE_WRITE); + if (!file) { + Serial.println("Failed to open file for writing"); + return; + } + file.print(settings); + file.close(); + Serial.println("Cam Settings saved to SPIFFS"); +} + +// Function to load settings from SPIFFS +String loadSettings() { + File file = SPIFFS.open("/cam_settings.json"); + if (!file) { + Serial.println("Failed to open file for reading"); + return ""; + } + + String settings = file.readString(); + file.close(); + Serial.println("Cam Settings loaded from SPIFFS"); + return settings; +} + +void createDefault_wifi_Settings() { + if (!SPIFFS.exists("/wifi_settings.json")) { + File file = SPIFFS.open("/wifi_settings.json", FILE_WRITE); + if (!file) { + Serial.println("Failed to create wifi settings file"); + return; + } + DynamicJsonDocument doc(1024); + doc["ssid"] = "free-wifi"; + doc["password"] = "default"; + doc["hostname"] = "esp32cam"; + serializeJson(doc, file); + file.close(); + Serial.println("Default wifi settings file created"); + } +} + +void createDefault_cam_Settings() { + if (!SPIFFS.exists("/cam_settings.json")) { + File file = SPIFFS.open("/cam_settings.json", FILE_WRITE); + if (!file) { + Serial.println("Failed to create cam settings file"); + return; + } + DynamicJsonDocument doc(1024); + doc["quality"] = "20"; + doc["fps"] = "14"; + doc["brightness"] = "0"; + doc["contrast"] = "0"; + doc["saturation"] = "0"; + doc["specialEffect"] = "0"; + doc["whiteBalance"] = "1"; + doc["awbGain"] = "1"; + doc["wbMode"] = "0"; + doc["hmirror"] = "0"; + doc["vflip"] = "0"; + doc["colorbar"] = "0"; + doc["gammaCorrection"] = "1"; + doc["aec2"] = "1"; + doc["aeLevel"] = "0"; + doc["aecValue"] = "600"; + doc["exposureControl"] = "1"; + doc["gainControl"] = "1"; + doc["agcGain"] = "15"; + doc["dcw"] = "1"; + doc["led"] = "0"; + doc["resolution"] = "VGA"; + serializeJson(doc, file); + file.close(); + Serial.println("Default cam settings file created"); + } +} + +void wifi_Connect() { + IPAddress ip; WiFi.mode(WIFI_STA); - WiFi.begin(SSID1, PWD1); + WiFi.setTxPower(WIFI_POWER_19_5dBm); + WiFi.setSleep(false); + WiFi.setHostname(hostname.c_str()); + WiFi.begin(ssid.c_str(), password.c_str()); Serial.print("Connecting to WiFi"); - while (WiFi.status() != WL_CONNECTED) - { + while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(F(".")); } ip = WiFi.localIP(); Serial.println(F("WiFi connected")); - Serial.println(""); Serial.print("Stream Link: http://"); - Serial.print(ip); - Serial.println("/mjpeg/1"); - + Serial.println(ip); +} - // Start mainstreaming RTOS task - xTaskCreatePinnedToCore( - mjpegCB, - "mjpeg", - 4 * 1024, - NULL, - 2, - &tMjpeg, - APP_CPU); +bool shouldBootAPMode() { + if (SPIFFS.exists("/ap_mode.flag")) { + return true; + } else { + return false; + } } +void load_wifi_Settings() { + if (!SPIFFS.exists("/wifi_settings.json")) { + Serial.println("wifi_settings.json file does not exist"); + return; + } -void loop() { - vTaskDelay(1000); + File file = SPIFFS.open("/wifi_settings.json", FILE_READ); + if (!file) { + Serial.println("Failed to open wifi_settings.json file for reading"); + return; + } + + size_t size = file.size(); + if (size == 0) { + Serial.println("wifi_settings.json file is empty"); + file.close(); + return; + } + + std::unique_ptr buf(new char[size + 1]); + file.readBytes(buf.get(), size); + buf[size] = '\0'; // Null-terminate the buffer + file.close(); + Serial.println("wifi_settings.json content:"); + Serial.println(buf.get()); + + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, buf.get()); + if (error) { + Serial.print("Failed to parse JSON: "); + Serial.println(error.f_str()); + return; + } + ssid = doc["ssid"].as(); + password = doc["password"].as(); + hostname = doc["hostname"].as(); + + Serial.println("WiFi settings loaded from SPIFFS"); + Serial.print("SSID: "); + Serial.println(ssid); + Serial.print("Password: "); + Serial.println(password); + Serial.print("Hostname: "); + Serial.println(hostname); +} + +// OTA setup +void ota_setup() { + ArduinoOTA.setHostname(hostname.c_str()); + ArduinoOTA.setPassword("OTA-pasw"); // Set a strong password for OTA updates + ArduinoOTA.onStart([]() { + // Deinitialize camera + camDeinit(); + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "sketch"; + } else { // U_SPIFFS + type = "filesystem"; + } + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + delay(30000); + ESP.restart(); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + Serial.println("Auth Failed"); + } else if (error == OTA_BEGIN_ERROR) { + Serial.println("Begin Failed"); + } else if (error == OTA_CONNECT_ERROR) { + Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + Serial.println("End Failed"); + } + }); + ArduinoOTA.begin(); + Serial.println("OTA updates Ready"); } diff --git a/html/cam_script.js b/html/cam_script.js new file mode 100644 index 0000000..a216c33 --- /dev/null +++ b/html/cam_script.js @@ -0,0 +1,195 @@ +var baseHost = document.location.origin; +var streamUrl = `${baseHost}/stream`; +var jpgUrl = `${baseHost}/jpg`; +const view = document.getElementById('videoStream'); +const viewContainer = document.getElementById('streamContainer'); +const toggleStreamButton = document.getElementById('toggleStreamButton'); +const getJpgButton = document.getElementById('getJpgButton'); + +const startStream = () => { + view.src = streamUrl; + viewContainer.style.display = 'flex'; + toggleStreamButton.innerHTML = 'Stop Video Stream'; + // getJpgButton.disabled = true; // Disable "Save Picture" button +}; + +const stopStream = () => { + // Remove the event listener to prevent unintended downloads + view.onload = null; + view.src = ''; + viewContainer.style.display = 'none'; + toggleStreamButton.innerHTML = 'Start Video Stream'; + // getJpgButton.disabled = false; // Enable "Save Picture" button +}; + +const getJpg = async () => { + try { + const response = await fetch(jpgUrl); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + + view.src = blobUrl; // Display the image + viewContainer.style.display = 'flex'; + + setTimeout(() => { + downloadJpg(blobUrl); // Download the same image + }, 500); + } catch (error) { + console.error('Error fetching image:', error); + } +}; + +const downloadJpg = (url) => { + fetch(url) + .then(response => response.blob()) + .then(blob => { + const link = document.createElement('a'); + const blobUrl = URL.createObjectURL(blob); + + // Get the current timestamp + const now = new Date(); + const timestamp = now.toISOString().replace(/[:.-]/g, ''); // Format the timestamp + + // Append the timestamp to the filename + link.href = blobUrl; + link.download = `image_${timestamp}.jpg`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Revoke the Blob URL + URL.revokeObjectURL(blobUrl); + }) + .catch(error => console.error('Error downloading the image:', error)); +}; + +toggleStreamButton.addEventListener('click', () => { + if (viewContainer.style.display === 'none' || view.src !== streamUrl) { + startStream(); + } else { + stopStream(); + } +}); + +getJpgButton.addEventListener('click', () => { + getJpg(); +}); + +document.addEventListener('DOMContentLoaded', () => { + fetch('/getSettings') + .then(response => response.json()) + .then(settings => { + document.getElementById('quality').value = settings.quality; + document.getElementById('brightness').value = settings.brightness; + document.getElementById('contrast').value = settings.contrast; + document.getElementById('saturation').value = settings.saturation; + document.getElementById('resolution').value = settings.resolution; + document.getElementById('specialEffect').value = settings.specialEffect; + document.getElementById('whiteBalance').value = settings.whiteBalance; + document.getElementById('awbGain').value = settings.awbGain; + document.getElementById('wbMode').value = settings.wbMode; + document.getElementById('hmirror').value = settings.hmirror; + document.getElementById('vflip').value = settings.vflip; + document.getElementById('colorbar').value = settings.colorbar; + document.getElementById('gammaCorrection').value = settings.gammaCorrection; + document.getElementById('exposureControl').value = settings.exposureControl; + document.getElementById('aec2').value = settings.aec2; + document.getElementById('aeLevel').value = settings.aeLevel; + document.getElementById('aecValue').value = settings.aecValue; + document.getElementById('gainControl').value = settings.gainControl; + document.getElementById('agcGain').value = settings.agcGain; + document.getElementById('dcw').value = settings.dcw; + document.getElementById('led').value = settings.led; + document.getElementById('fps').value = settings.fps; + }) + .catch(error => { + console.error('Error loading settings:', error); + }); +}); + +document.getElementById('resetDefaultsButton').addEventListener('click', () => { + // Resetting the values to their defaults + document.getElementById('quality').value = '20'; + document.getElementById('fps').value = '14'; + document.getElementById('brightness').value = '0'; + document.getElementById('contrast').value = '0'; + document.getElementById('saturation').value = '0'; + document.getElementById('resolution').value = 'SVGA'; + document.getElementById('specialEffect').value = '0'; + document.getElementById('whiteBalance').value = '1'; + document.getElementById('awbGain').value = '1'; + document.getElementById('wbMode').value = '0'; + document.getElementById('hmirror').value = '0'; + document.getElementById('vflip').value = '0'; + document.getElementById('colorbar').value = '0'; + document.getElementById('gammaCorrection').value = '1'; + document.getElementById('exposureControl').value = '1'; + document.getElementById('aec2').value = '1'; + document.getElementById('aeLevel').value = '0'; + document.getElementById('aecValue').value = '10'; + document.getElementById('gainControl').value = '1'; + document.getElementById('agcGain').value = '15'; + document.getElementById('dcw').value = '1'; + document.getElementById('led').value = '0'; + + // Trigger the "Save Settings" button click + document.getElementById('saveSettingsButton').click(); +}); + + +/* ... existing code ... */ + +const saveButton = document.getElementById('saveSettingsButton'); + +const infoBar = document.getElementById('infoBar'); +const showMessage = (message) => { + infoBar.innerHTML = message; + infoBar.style.display = 'block'; + setTimeout(() => { + infoBar.style.display = 'none'; + }, 3000); +}; + +saveButton.addEventListener('click', () => { + const settings = { + quality: document.getElementById('quality').value, + brightness: document.getElementById('brightness').value, + contrast: document.getElementById('contrast').value, + saturation: document.getElementById('saturation').value, + resolution: document.getElementById('resolution').value, + specialEffect: document.getElementById('specialEffect').value, + whiteBalance: document.getElementById('whiteBalance').value, + awbGain: document.getElementById('awbGain').value, + wbMode: document.getElementById('wbMode').value, + hmirror: document.getElementById('hmirror').value, + vflip: document.getElementById('vflip').value, + colorbar: document.getElementById('colorbar').value, + gammaCorrection: document.getElementById('gammaCorrection').value, + exposureControl: document.getElementById('exposureControl').value, + aec2: document.getElementById('aec2').value, + aeLevel: document.getElementById('aeLevel').value, + aecValue: document.getElementById('aecValue').value, + gainControl: document.getElementById('gainControl').value, + agcGain: document.getElementById('agcGain').value, + dcw: document.getElementById('dcw').value, + fps: document.getElementById('fps').value, + led: document.getElementById('led').value + }; + + fetch('/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(settings) + }) + .then(response => response.json()) + .then(data => { + console.log('Settings saved:', data); + showMessage('Settings applied successfully'); + }) + .catch((error) => { + console.error('Error:', error); + showMessage('Error applying settings'); + }); +}); diff --git a/html/cam_settings.html b/html/cam_settings.html new file mode 100644 index 0000000..36b6db0 --- /dev/null +++ b/html/cam_settings.html @@ -0,0 +1,173 @@ + + + + ESP32-CAM Settings + + + +
+

Camera Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ + + diff --git a/html/cam_styles.css b/html/cam_styles.css new file mode 100644 index 0000000..1ca9024 --- /dev/null +++ b/html/cam_styles.css @@ -0,0 +1,88 @@ +body { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + height: 100vh; + background-color: #000000; + margin: 0; + font-family: Arial, sans-serif; + color: white; +} +.settings { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: flex-end; + background-color: #333; + padding: 2px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + margin-right: 5px; +} +h1 { + color: #fff; + font-size: 16px; +} +label { + margin: 2px; + font-size: 14px; + color: #fff; +} +input, select { + margin: 2px; + padding: 2px; + font-size: 12px; + width: 120px; + box-sizing: border-box; +} +.button-row { + display: flex; + flex-direction: row; + justify-content: flex-end; + width: 100%; +} +button { + margin: 2px; + padding: 5px 10px; + font-size: 14px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} +button:hover { + background-color: #0056b3; +} +.stream { + flex: 1 1 auto; + display: flex; + justify-content: center; + align-items: center; + background-color: #000000; + border-radius: 10px; + padding: 10px; +} +iframe { + width: 100%; + height: 100%; + border: none; +} +.info-bar { + position: fixed; + bottom: 0; + width: 100%; + padding: 10px; + background-color: #007bff; + color: white; + text-align: center; + font-size: 14px; + display: none; /* Initially hidden */ +} +button:disabled { + background-color: #ccc; /* Light gray background */ + color: #666; /* Darker gray text */ + cursor: not-allowed; /* Show 'not allowed' cursor */ + opacity: 0.6; /* Slightly faded */ +} diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..719dde1 --- /dev/null +++ b/html/index.html @@ -0,0 +1,83 @@ + + + + + + ESP32 Camera Server + + + +

ESP32 Camera Server

+
+ +
+
+

Live stream from your ESP32 camera module. Below you can find links to configure your camera and Wi-Fi settings.

+
+ + + + diff --git a/html/index_script.js b/html/index_script.js new file mode 100644 index 0000000..25f5a6b --- /dev/null +++ b/html/index_script.js @@ -0,0 +1,86 @@ +document.addEventListener('DOMContentLoaded', () => { + const baseHost = document.location.origin; + const streamUrl = `${baseHost}/stream`; + + const videoContainer = document.getElementById('videoContainer'); // The container for the video + const mjpegStreamImg = document.createElement('img'); // Dynamically create an element + + // Set the class for the element + mjpegStreamImg.className = 'mjpeg-stream'; + mjpegStreamImg.src = streamUrl; + + // Set dimensions to scale responsively while maintaining aspect ratio + const aspectRatio = 640 / 480; // VGA aspect ratio (width / height) + + // Adjust dynamically based on the container's width + function adjustStreamSize() { + const containerWidth = videoContainer.offsetWidth; + const calculatedHeight = containerWidth / aspectRatio; // Maintain aspect ratio + + mjpegStreamImg.style.width = `${containerWidth}px`; + mjpegStreamImg.style.height = `${calculatedHeight}px`; + videoContainer.style.height = `${calculatedHeight}px`; + } + + // Initial adjustment + adjustStreamSize(); + + // Re-adjust on window resize + window.addEventListener('resize', adjustStreamSize); + + // Add the element to the video container + videoContainer.appendChild(mjpegStreamImg); + + // Create the date and time element + const dateTimeOverlay = document.createElement('div'); + dateTimeOverlay.className = 'date-time-overlay'; + videoContainer.appendChild(dateTimeOverlay); + + // Update the date and time dynamically + setInterval(() => { + const now = new Date(); + const formattedTime = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const formattedDate = now.toLocaleDateString(); + dateTimeOverlay.innerText = `${formattedDate} ${formattedTime}`; + }, 1000); + + // Create the text element + const overlayText = document.createElement('div'); + overlayText.className = 'overlay-text'; + overlayText.innerText = 'ESP32-CAM live video stream'; // Replace with your desired text + videoContainer.appendChild(overlayText); + + // Add CSS styles for the overlays + const style = document.createElement('style'); + style.innerHTML = ` + #videoContainer { + position: relative; /* Make container the positioning context */ + max-width: 100%; /* Ensure container doesn't exceed screen width */ + margin: 0 auto; /* Center the video container */ + max-width: 800px; /* Set a maximum width for desktop browsers */ + } + .overlay-text { + position: absolute; + top: 10px; + left: 10px; + color: white; + background-color: rgba(0, 0, 0, 0.5); + padding: 5px 10px; + font-size: 16px; + font-family: Arial, sans-serif; + border-radius: 5px; + } + .date-time-overlay { + position: absolute; + bottom: 10px; + right: 10px; + color: white; + background-color: rgba(0, 0, 0, 0.5); + padding: 5px 10px; + font-size: 12px; + font-family: Arial, sans-serif; + border-radius: 5px; + } + `; + document.head.appendChild(style); +}); diff --git a/html/wifi_script.js b/html/wifi_script.js new file mode 100644 index 0000000..c771a24 --- /dev/null +++ b/html/wifi_script.js @@ -0,0 +1,55 @@ +function areFieldsFilled() { + const ssid = document.getElementById('ssid').value.trim(); + const password = document.getElementById('password').value.trim(); + const hostname = document.getElementById('hostname').value.trim(); + + return ssid && password && hostname; +} + +document.getElementById('saveButton').addEventListener('click', () => { + const ssid = document.getElementById('ssid').value.trim(); + const password = document.getElementById('password').value.trim(); + const hostname = document.getElementById('hostname').value.trim(); + + if (!ssid || !password || !hostname) { + alert('Please fill out all fields.'); + return; + } + + const settings = { + ssid, + password, + hostname + }; + + fetch('/saveSettings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(settings) + }).then(response => response.json()) + .then(data => alert(data.message)); +}); + +document.getElementById('rebootButton').addEventListener('click', () => { + if (!areFieldsFilled()) { + alert('Please fill out all fields before rebooting.'); + return; + } + + fetch('/reboot', { + method: 'POST' + }).then(response => response.json()) + .then(data => alert(data.message)); +}); + +document.getElementById('rebootApButton').addEventListener('click', () => { + fetch('/rebootAp', { + method: 'POST' + }).then(response => response.json()) + .then(data => { + document.getElementById('apInfo').style.display = 'block'; + alert(data.message); + }); +}); diff --git a/html/wifi_settings.html b/html/wifi_settings.html new file mode 100644 index 0000000..34cbdc2 --- /dev/null +++ b/html/wifi_settings.html @@ -0,0 +1,35 @@ + + + + ESP32 Settings + + + +

Wi-Fi Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ + + + diff --git a/html/wifi_styles.css b/html/wifi_styles.css new file mode 100644 index 0000000..9383575 --- /dev/null +++ b/html/wifi_styles.css @@ -0,0 +1,40 @@ +body { + font-family: Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 20px; +} + +h1 { + color: #333; +} + +label { + display: block; + margin-top: 10px; +} + +input[type="text"], +input[type="password"] { + width: 100%; + padding: 8px; + margin-top: 5px; + box-sizing: border-box; +} + +.button-row { + margin-top: 20px; +} + +button { + padding: 10px 20px; + background-color: #4CAF50; + color: white; + border: none; + cursor: pointer; + margin-right: 10px; +} + +button:hover { + background-color: #45a049; +} diff --git a/src/OV2640.cpp b/src/OV2640.cpp index 02d04d5..d77ee20 100644 --- a/src/OV2640.cpp +++ b/src/OV2640.cpp @@ -191,3 +191,16 @@ esp_err_t OV2640::init(camera_config_t config) return ESP_OK; } + +esp_err_t OV2640::deinit() { + + esp_err_t err = esp_camera_deinit(); + + if (err != ESP_OK) { + printf("Camera deinit failed with error 0x%x", err); + return err; + } + // ESP_ERROR_CHECK(gpio_install_isr_service(0)); + + return ESP_OK; +} diff --git a/src/OV2640.h b/src/OV2640.h index b9b5706..ff289fb 100644 --- a/src/OV2640.h +++ b/src/OV2640.h @@ -2,10 +2,10 @@ #define OV2640_H_ #include -#include -#include -#include "esp_log.h" -#include "esp_attr.h" +//#include +//#include +//#include "esp_log.h" +//#include "esp_attr.h" #include "esp_camera.h" extern camera_config_t esp32cam_config, esp32cam_aithinker_config, esp32cam_ttgo_t_config; @@ -19,6 +19,7 @@ class OV2640 ~OV2640(){ }; esp_err_t init(camera_config_t config); + esp_err_t deinit(void); void run(void); size_t getSize(void); uint8_t *getfb(void);