├── ESP32-CAM_MJPEG2SD.ino ├── LICENSE ├── README.md ├── appGlobals.h ├── appSpecific.cpp ├── audio.cpp ├── avi.cpp ├── camera_pins.h ├── certificates.cpp ├── data ├── Auxil.htm ├── MJPEG2SD.htm └── common.js ├── externalHeartbeat.cpp ├── extras ├── CyberChef.png ├── I2C.jpg ├── TinyML.png ├── hasio_device.png ├── motion.png ├── partitions.csv ├── portForward.png ├── setupPage.html ├── telegram.png └── webdav.png ├── ftp.cpp ├── globals.h ├── mcpwm.cpp ├── mjpeg2sd.cpp ├── motionDetect.cpp ├── mqtt.cpp ├── peripherals.cpp ├── periphsI2C.cpp ├── photogram.cpp ├── prefs.cpp ├── rtsp.cpp ├── setupAssist.cpp ├── smtp.cpp ├── streamServer.cpp ├── telegram.cpp ├── telemetry.cpp ├── uart.cpp ├── utils.cpp ├── utilsFS.cpp ├── webDav.cpp └── webServer.cpp /ESP32-CAM_MJPEG2SD.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Capture ESP32 Cam JPEG images into a AVI file and store on SD 3 | * AVI files stored on the SD card can also be selected and streamed to a browser as MJPEG. 4 | * 5 | * s60sc 2020 - 2024 6 | */ 7 | 8 | #include "appGlobals.h" 9 | 10 | void setup() { 11 | logSetup(); 12 | LOG_INF("Selected board %s", CAM_BOARD); 13 | // prep storage 14 | if (startStorage()) { 15 | // Load saved user configuration 16 | if (loadConfig()) { 17 | #ifndef AUXILIARY 18 | // initialise camera 19 | if (psramFound()) { 20 | if (ESP.getPsramSize() > 1 * ONEMEG) prepCam(); 21 | else snprintf(startupFailure, SF_LEN, STARTUP_FAIL "Insufficient PSRAM for app: %s", fmtSize(ESP.getPsramSize())); 22 | } else snprintf(startupFailure, SF_LEN, STARTUP_FAIL "Need PSRAM to be enabled"); 23 | #else 24 | LOG_INF("AUXILIARY mode without camera"); 25 | #endif 26 | } 27 | } 28 | 29 | #ifdef DEV_ONLY 30 | devSetup(); 31 | #endif 32 | 33 | // connect wifi or start config AP if router details not available 34 | startWifi(); 35 | 36 | startWebServer(); 37 | if (strlen(startupFailure)) LOG_WRN("%s", startupFailure); 38 | else { 39 | // start rest of services 40 | #ifndef AUXILIARY 41 | startSustainTasks(); 42 | #endif 43 | #if INCLUDE_SMTP 44 | prepSMTP(); 45 | #endif 46 | #if INCLUDE_FTP_HFS 47 | prepUpload(); 48 | #endif 49 | #if INCLUDE_UART 50 | prepUart(); 51 | #endif 52 | #if INCLUDE_PERIPH 53 | prepPeripherals(); 54 | #if INCLUDE_MCPWM 55 | prepMotors(); 56 | #endif 57 | #endif 58 | #if INCLUDE_AUDIO 59 | prepAudio(); 60 | #endif 61 | #if INCLUDE_TGRAM 62 | prepTelegram(); 63 | #endif 64 | #if INCLUDE_I2C 65 | prepI2C(); 66 | #if INCLUDE_TELEM 67 | prepTelemetry(); 68 | #endif 69 | #endif 70 | #if INCLUDE_PERIPH 71 | startHeartbeat(); 72 | #endif 73 | #ifndef AUXILIARY 74 | #if INCLUDE_RTSP 75 | prepRTSP(); 76 | #endif 77 | if (!prepRecording()) { 78 | snprintf(startupFailure, SF_LEN, STARTUP_FAIL "Insufficient memory, remove optional features"); 79 | LOG_WRN("%s", startupFailure); 80 | } 81 | #endif 82 | checkMemory(); 83 | } 84 | } 85 | 86 | void loop() { 87 | // confirm not blocked in setup 88 | LOG_INF("=============== Total tasks: %u ===============\n", uxTaskGetNumberOfTasks() - 1); 89 | delay(1000); 90 | vTaskDelete(NULL); // free 8k ram 91 | } 92 | -------------------------------------------------------------------------------- /audio.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Handle microphone input, and speaker output via amp. 3 | // The microphone input, and the output to amplifier, each make use of a 4 | // separate I2S peripheral in the ESP32 or ESP32S3. 5 | // I2S and PDM microphones are supported. 6 | // I2S amplifiers are supported. 7 | // 8 | // If using I2S mic and I2S amp, then the following pins should be set to same values: 9 | // - micSckPin = mampBckIo 10 | // - micSWsPin = mampSwsIo 11 | // 12 | // A browser microphone and on a PC or phone can be used: 13 | // - for VoiceChanger app, this is used instead of local mic 14 | // - need to press PC Mic button before selecting an action 15 | // - for MJPEG2SD app, this is passed thru to speaker, independent of local mic 16 | // - need to enable use amp and pins in Config / Peripherals for Start Mic button to be available on web page 17 | // - browser mic should only be activated when need to speak 18 | // Windows needs to allow microphone use in Microphone Privacy Settings 19 | // In Microphone Properties / Advanced, check bit depth and sample rate (normally 16 bit 48kHz) 20 | // Chrome needs to allow access to mic from insecure (http) site: 21 | // Go to : chrome://flags/#unsafely-treat-insecure-origin-as-secure 22 | // Enter app URL in box: http:// 23 | // 24 | // s60sc 2024 25 | 26 | #include "appGlobals.h" 27 | 28 | #if INCLUDE_AUDIO 29 | 30 | #include 31 | I2SClass I2Spdm; 32 | I2SClass I2Sstd; 33 | 34 | // On ESP32, only I2S1 available with camera 35 | i2s_port_t MIC_CHAN = I2S_NUM_1; 36 | i2s_port_t AMP_CHAN = I2S_NUM_0; 37 | 38 | static bool micUse = false; // esp mic available 39 | bool micRem = false; // use browser mic (depends on app) 40 | static bool ampUse = false; // whether esp amp / speaker available 41 | bool spkrRem = false; // use browser speaker 42 | bool volatile stopAudio = false; 43 | static bool micRecording = false; 44 | 45 | // I2S devices 46 | bool I2Smic; // true if I2S, false if PDM 47 | // I2S SCK and I2S BCLK can share same pin 48 | // I2S external Microphone pins 49 | // INMP441 I2S microphone pinout, connect L/R to GND for left channel 50 | // MP34DT01 PDM microphone pinout, connect SEL to GND for left channel 51 | int micSckPin = -1; // I2S SCK 52 | int micSWsPin = -1; // I2S WS, PDM CLK 53 | int micSdPin = -1; // I2S SD, PDM DAT 54 | 55 | // I2S Amplifier pins 56 | // MAX98357A 57 | // SD leave as mono (unconnected) 58 | // Gain: 100k to GND works, not direct to GND. Unconnected is 9 dB 59 | int mampBckIo = -1; // I2S BCLK or SCK 60 | int mampSwsIo = -1; // I2S LRCLK or WS 61 | int mampSdIo = -1; // I2S DIN 62 | 63 | int ampTimeout = 1000; // ms for amp write abandoned if no output 64 | uint32_t SAMPLE_RATE = 16000; // audio rate in Hz 65 | int micGain = 0; // microphone gain 0 is off 66 | int8_t ampVol = 0; // amplifier volume factor 0 is off 67 | 68 | TaskHandle_t audioHandle = NULL; 69 | 70 | static int totalSamples = 0; 71 | static const uint8_t sampleWidth = sizeof(int16_t); 72 | const size_t sampleBytes = DMA_BUFF_LEN * sampleWidth; 73 | int16_t* sampleBuffer = NULL; 74 | static uint8_t* wsBuffer = NULL; 75 | static size_t wsBufferLen = 0; 76 | uint8_t* audioBuffer = NULL; // mic input streamed to NVR or RTSP 77 | size_t audioBytes = 0; 78 | 79 | static const char* micLabels[2] = {"PDM", "I2S"}; 80 | 81 | #ifdef CONFIG_IDF_TARGET_ESP32S3 82 | #define psramMax (ONEMEG * 6) 83 | #else 84 | #define psramMax (ONEMEG * 2) 85 | #endif 86 | #ifdef ISCAM 87 | bool AudActive = false; // whether to show audio features 88 | static File wavFile; 89 | #endif 90 | #ifdef ISVC 91 | uint8_t* recAudioBuffer = NULL; 92 | size_t recAudioBytes = 0; 93 | #endif 94 | static uint8_t wavHeader[WAV_HDR_LEN] = { // WAV header template 95 | 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, 0x20, 96 | 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x11, 0x2B, 0x00, 0x00, 0x11, 0x2B, 0x00, 0x00, 97 | 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 98 | }; 99 | 100 | void applyVolume() { 101 | // determine required volume setting 102 | int8_t adjVol = ampVol * 2; // use web page setting 103 | #ifdef ISVC 104 | adjVol = checkPotVol(adjVol); // use potentiometer setting if available 105 | #endif 106 | if (adjVol) { 107 | // increase or reduce volume, 6 is unity eg midpoint of pot / web slider 108 | adjVol = adjVol > 5 ? adjVol - 5 : adjVol - 7; 109 | // apply volume control to samples 110 | for (int i = 0; i < DMA_BUFF_LEN; i++) { 111 | // apply volume control 112 | sampleBuffer[i] = adjVol < 0 ? sampleBuffer[i] / abs(adjVol) : constrain((int32_t)sampleBuffer[i] * adjVol, SHRT_MIN, SHRT_MAX); 113 | } 114 | } // else turn off volume 115 | } 116 | 117 | static bool setupMic() { 118 | bool res; 119 | if (I2Smic) { 120 | // I2S mic and I2S amp can share same I2S channel 121 | I2Sstd.setPins(micSckPin, micSWsPin, mampSdIo, micSdPin, -1); // BCLK/SCK, LRCLK/WS, SDOUT, SDIN, MCLK 122 | res = I2Sstd.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT); 123 | } else { 124 | // PDM mic need separate channel to I2S 125 | I2Spdm.setPinsPdmRx(micSWsPin, micSdPin); 126 | res = I2Spdm.begin(I2S_MODE_PDM_RX, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT); 127 | } 128 | return res; 129 | } 130 | 131 | static bool setupAmp() { 132 | bool res = true; 133 | if (!micUse || !I2Smic) { 134 | // if not already started by setupMic() 135 | I2Sstd.setPins(mampBckIo, mampSwsIo, mampSdIo, -1, -1); // BCLK/SCK, LRCLK/WS, SDOUT, SDIN, MCLK 136 | res = I2Sstd.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT); 137 | } // already started by setupMic() 138 | return res; 139 | } 140 | 141 | void closeI2S() { 142 | I2Sstd.end(); 143 | I2Spdm.end(); 144 | } 145 | 146 | static void applyMicGain(size_t bytesRead) { 147 | // change esp mic gain by required factor 148 | uint8_t gainFactor = pow(2, micGain - MIC_GAIN_CENTER); 149 | for (int i = 0; i < bytesRead / sampleWidth; i++) { 150 | sampleBuffer[i] = constrain(sampleBuffer[i] * gainFactor, SHRT_MIN, SHRT_MAX); 151 | } 152 | } 153 | 154 | static size_t espMicInput() { 155 | // read esp mic 156 | size_t bytesRead = 0; 157 | if (micUse) { 158 | bytesRead = I2Smic ? I2Sstd.readBytes((char*)sampleBuffer, sampleBytes) : I2Spdm.readBytes((char*)sampleBuffer, sampleBytes); 159 | applyMicGain(bytesRead); 160 | } 161 | return bytesRead; 162 | } 163 | 164 | size_t updateWavHeader() { 165 | // update wav header 166 | uint32_t dataBytes = totalSamples * sampleWidth; 167 | uint32_t wavFileSize = dataBytes ? dataBytes + WAV_HDR_LEN - 8 : 0; // wav file size excluding chunk header 168 | memcpy(wavHeader+4, &wavFileSize, 4); 169 | memcpy(wavHeader+24, &SAMPLE_RATE, 4); // sample rate 170 | uint32_t byteRate = SAMPLE_RATE * sampleWidth; // byte rate (SampleRate * NumChannels * BitsPerSample/8) 171 | memcpy(wavHeader+28, &byteRate, 4); 172 | memcpy(wavHeader+WAV_HDR_LEN-4, &dataBytes, 4); // wav data size 173 | memcpy(audioBuffer, wavHeader, WAV_HDR_LEN); 174 | return dataBytes; 175 | } 176 | 177 | /*********************************************************************/ 178 | 179 | #ifdef ISVC 180 | 181 | #if !INCLUDE_RTSP 182 | bool rtspAudio = false; 183 | #endif 184 | 185 | static size_t micInput() { 186 | // get input from browser mic or else esp mic 187 | size_t bytesRead = (micRem) ? wsBufferLen : espMicInput(); 188 | if (bytesRead && micRem) { 189 | // double buffer browser mic input 190 | memcpy(sampleBuffer, wsBuffer, bytesRead); 191 | wsBufferLen = 0; 192 | applyMicGain(bytesRead); 193 | } else if (micRem) delay(20); 194 | return bytesRead; 195 | } 196 | 197 | void browserMicInput(uint8_t* wsMsg, size_t wsMsgLen) { 198 | // input from browser mic via websocket 199 | if (micRem && !wsBufferLen) { 200 | // copy browser mic input into sampleBuffer for amp 201 | wsBufferLen = wsMsgLen; 202 | memcpy(wsBuffer, wsMsg, wsMsgLen); 203 | } 204 | } 205 | 206 | static void ampOutput(size_t bytesRead = sampleBytes) { 207 | // output to amplifier, apply required filtering and volume 208 | applyFilters(); 209 | if (spkrRem) wsAsyncSendBinary((uint8_t*)sampleBuffer, bytesRead); // browser speaker 210 | else if (ampUse) I2Sstd.write((uint8_t*)sampleBuffer, bytesRead); // esp amp speaker 211 | if (!audioBytes) { 212 | // fill audio buffer to send to RTSP 213 | memcpy(audioBuffer, sampleBuffer, bytesRead); 214 | audioBytes = bytesRead; 215 | } 216 | displayAudioLed(sampleBuffer[0]); 217 | } 218 | 219 | static void passThru() { 220 | // play buffer from mic direct to amp 221 | size_t bytesRead = micInput(); 222 | if (bytesRead) ampOutput(bytesRead); 223 | } 224 | 225 | static void makeRecording() { 226 | if (psramFound()) { 227 | LOG_INF("Recording ..."); 228 | recAudioBytes = WAV_HDR_LEN; // leave space for wave header 229 | wsBufferLen = 0; 230 | while (recAudioBytes < psramMax) { 231 | size_t bytesRead = micInput(); 232 | if (bytesRead) { 233 | memcpy(recAudioBuffer + recAudioBytes, sampleBuffer, bytesRead); 234 | recAudioBytes += bytesRead; 235 | } 236 | if (stopAudio) break; 237 | } // psram full 238 | if (!stopAudio) wsJsonSend("stopRec", "1"); 239 | totalSamples = (recAudioBytes - WAV_HDR_LEN) / sampleWidth; 240 | LOG_INF("%s recording of %d samples", stopAudio ? "Stopped" : "Finished", totalSamples); 241 | stopAudio = true; 242 | } else LOG_WRN("PSRAM needed to record and play"); 243 | } 244 | 245 | static void playRecording() { 246 | if (psramFound()) { 247 | LOG_INF("Playing %d samples, initial volume: %d", totalSamples, ampVol); 248 | for (int i = WAV_HDR_LEN; i < totalSamples * sampleWidth; i += sampleBytes) { 249 | memcpy(sampleBuffer, recAudioBuffer+i, sampleBytes); 250 | ampOutput(); 251 | if (stopAudio) break; 252 | } 253 | if (!stopAudio) wsJsonSend("stopPlay", "1"); 254 | LOG_INF("%s playing of %d samples", stopAudio ? "Stopped" : "Finished", totalSamples); 255 | stopAudio = true; 256 | } else LOG_WRN("PSRAM needed to record and play"); 257 | } 258 | 259 | static void VCactions() { 260 | // action user request 261 | stopAudio = false; 262 | closeI2S(); 263 | prepAudio(); 264 | setupFilters(); 265 | 266 | switch (THIS_ACTION) { 267 | case RECORD_ACTION: 268 | if (micRem) wsAsyncSendText("#M1"); 269 | if (micUse || micRem) makeRecording(); 270 | break; 271 | case PLAY_ACTION: 272 | // continues till stopped 273 | if (ampUse || spkrRem || rtspAudio) playRecording(); // play previous recording 274 | break; 275 | case PASS_ACTION: 276 | if (ampUse || spkrRem || rtspAudio) { 277 | if (micRem) wsAsyncSendText("#M1"); 278 | LOG_INF("Passthru started"); 279 | wsBufferLen = 0; 280 | while (!stopAudio) passThru(); 281 | LOG_INF("Passthru stopped"); 282 | } 283 | break; 284 | default: 285 | break; 286 | } 287 | displayAudioLed(0); 288 | xSemaphoreGive(audioSemaphore); 289 | } 290 | 291 | #endif 292 | 293 | /*****************************************************************/ 294 | 295 | #ifdef ISCAM 296 | 297 | void browserMicInput(uint8_t* wsMsg, size_t wsMsgLen) { 298 | // input from browser mic via websocket, send to esp amp 299 | if (micRem && !wsBufferLen) { 300 | wsBufferLen = wsMsgLen; 301 | memcpy(wsBuffer, wsMsg, wsMsgLen); 302 | int8_t adjVol = ampVol * 2; // use web page setting 303 | if (adjVol) { 304 | // increase or reduce volume, 6 is unity eg midpoint of web slider 305 | adjVol = adjVol > 5 ? adjVol - 5 : adjVol - 7; 306 | // apply volume control to samples 307 | int16_t* wsPtr = (int16_t*) wsBuffer; 308 | for (int i = 0; i < wsBufferLen / sizeof(int16_t); i++) { 309 | // apply volume control 310 | wsPtr[i] = adjVol < 0 ? wsPtr[i] / abs(adjVol) : constrain((int32_t)wsPtr[i] * adjVol, SHRT_MIN, SHRT_MAX); 311 | } 312 | } 313 | I2Sstd.write(wsBuffer, wsBufferLen); 314 | wsBufferLen = 0; 315 | } 316 | } 317 | 318 | void startAudioRecord() { 319 | // called from openAvi() in mjpeg2sd.cpp 320 | // start audio recording and write recorded audio to SD card as WAV file 321 | // combined into AVI file as PCM channel on FTP upload or browser download 322 | // so can be read by media players 323 | if (micUse && micGain) { 324 | wavFile = STORAGE.open(WAVTEMP, FILE_WRITE); 325 | wavFile.write(wavHeader, WAV_HDR_LEN); 326 | micRecording = true; 327 | totalSamples = 0; 328 | } else { 329 | micRecording = false; 330 | LOG_WRN("No ESP mic defined or mic is off"); 331 | } 332 | } 333 | 334 | void finishAudioRecord(bool isValid) { 335 | // called from closeAvi() in mjpeg2sd.cpp 336 | if (micRecording) { 337 | // finish a recording and save if valid 338 | micRecording = false; 339 | if (isValid) { 340 | size_t dataBytes = updateWavHeader(); 341 | wavFile.seek(0, SeekSet); // start of file 342 | wavFile.write(wavHeader, WAV_HDR_LEN); // overwrite default header 343 | wavFile.close(); 344 | LOG_INF("Captured %d audio samples with gain factor %i", totalSamples, micGain - MIC_GAIN_CENTER); 345 | LOG_INF("Saved %s to SD for %s", fmtSize(dataBytes + WAV_HDR_LEN), WAVTEMP); 346 | } 347 | } 348 | } 349 | 350 | static void camActions() { 351 | // apply esp mic input to required outputs 352 | while (true) { 353 | size_t bytesRead = 0; 354 | if (micRecording || !audioBytes || spkrRem) bytesRead = espMicInput(); // load sampleBuffer 355 | if (bytesRead) { 356 | if (micRecording) { 357 | // record mic input to SD 358 | wavFile.write((uint8_t*)sampleBuffer, bytesRead); 359 | totalSamples += bytesRead / sampleWidth; 360 | } 361 | if (!audioBytes) { 362 | // fill audioBuffer to send to NVR 363 | memcpy(audioBuffer, sampleBuffer, bytesRead); 364 | audioBytes = bytesRead; 365 | } 366 | // intercom esp mic to browser speaker 367 | if (spkrRem) wsAsyncSendBinary((uint8_t*)sampleBuffer, bytesRead); 368 | } else delay(20); 369 | } 370 | } 371 | 372 | #endif 373 | 374 | /************************************************************************/ 375 | 376 | void setI2Schan(int whichChan) { 377 | // set I2S port for microphone, amp is opposite 378 | if (whichChan) { 379 | MIC_CHAN = I2S_NUM_1; 380 | AMP_CHAN = I2S_NUM_0; 381 | } else { 382 | MIC_CHAN = I2S_NUM_0; 383 | AMP_CHAN = I2S_NUM_1; 384 | } 385 | } 386 | 387 | static void predefPins() { 388 | #if defined(I2S_SD) 389 | char micPin[3]; 390 | sprintf(micPin, "%d", I2S_SD); 391 | updateStatus("micSdPin", micPin); 392 | sprintf(micPin, "%d", I2S_WS); 393 | updateStatus("micSWsPin", micPin); 394 | sprintf(micPin, "%d", I2S_SCK); 395 | updateStatus("micSckPin", micPin); 396 | #endif 397 | 398 | I2Smic = micSckPin == -1 ? false : true; 399 | 400 | #ifdef CONFIG_IDF_TARGET_ESP32S3 401 | MIC_CHAN = I2S_NUM_0; 402 | #endif 403 | } 404 | 405 | static void audioTask(void* parameter) { 406 | // loops to service each requirement for audio processing 407 | while (true) { 408 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 409 | #ifdef ISCAM 410 | camActions(); // runs constantly 411 | #endif 412 | #ifdef ISVC 413 | VCactions(); // runs once 414 | #endif 415 | } 416 | vTaskDelete(NULL); 417 | } 418 | 419 | void prepAudio() { 420 | // VC uses audio task for all activities 421 | // Cam uses audio task for microphone and intercom task for amplifier 422 | #ifdef ISCAM 423 | predefPins(); 424 | #endif 425 | if (MIC_CHAN == I2S_NUM_1 && !I2Smic) LOG_WRN("Only I2S devices supported on I2S_NUM_1"); 426 | else { 427 | if (micSdPin <= 0) LOG_WRN("Microphone pins not defined"); 428 | else { 429 | micUse = setupMic(); 430 | if (micUse) LOG_INF("Sound capture is available using %s mic on I2S%i with gain %d", micLabels[I2Smic], MIC_CHAN, micGain); 431 | else LOG_WRN("Unable to start ESP mic"); 432 | } 433 | if (mampSdIo <= 0) LOG_WRN("Amplifier pins not defined"); 434 | else { 435 | ampUse = setupAmp(); 436 | if (ampUse) LOG_INF("Speaker output is available using I2S amp on I2S%i with vol %d", AMP_CHAN, ampVol); 437 | else LOG_WRN("Unable to start ESP amp"); 438 | } 439 | } 440 | 441 | if (sampleBuffer == NULL) sampleBuffer = (int16_t*)malloc(sampleBytes); 442 | if (wsBuffer == NULL) wsBuffer = (uint8_t*)malloc(MAX_PAYLOAD_LEN); 443 | if (audioBuffer == NULL && psramFound()) audioBuffer = (uint8_t*)ps_malloc(sampleBytes); 444 | #ifdef ISVC 445 | if (recAudioBuffer == NULL && psramFound()) recAudioBuffer = (uint8_t*)ps_malloc(psramMax + (sizeof(int16_t) * DMA_BUFF_LEN)); 446 | // VC can still use audio task without esp mic or amp 447 | if (!micUse && !ampUse) LOG_WRN("Only browser mic and speaker can be used"); 448 | #endif 449 | #ifdef ISCAM 450 | wsBufferLen = 0; 451 | // Audio task only needed for esp microphone 452 | if (!micUse) return; 453 | #endif 454 | if (audioHandle == NULL) xTaskCreate(audioTask, "audioTask", AUDIO_STACK_SIZE, NULL, AUDIO_PRI, &audioHandle); 455 | #ifdef ISCAM 456 | xTaskNotifyGive(audioHandle); 457 | #endif 458 | debugMemory("prepAudio"); 459 | } 460 | 461 | #endif 462 | -------------------------------------------------------------------------------- /avi.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Generate AVI format for recorded videos 4 | 5 | s60sc 2020, 2022 6 | */ 7 | 8 | /* AVI file format: 9 | header: 10 | 310 bytes 11 | per jpeg: 12 | 4 byte 00dc marker 13 | 4 byte jpeg size 14 | jpeg frame content 15 | 0-3 bytes filler to align on DWORD boundary 16 | per PCM (audio file) 17 | 4 byte 01wb marker 18 | 4 byte pcm size 19 | pcm content 20 | 0-3 bytes filler to align on DWORD boundary 21 | footer: 22 | 4 byte idx1 marker 23 | 4 byte index size 24 | per jpeg: 25 | 4 byte 00dc marker 26 | 4 byte 0000 27 | 4 byte jpeg location 28 | 4 byte jpeg size 29 | per pcm: 30 | 4 byte 01wb marker 31 | 4 byte 0000 32 | 4 byte pcm location 33 | 4 byte pcm size 34 | */ 35 | 36 | #include "appGlobals.h" 37 | 38 | // avi header data 39 | const uint8_t dcBuf[4] = {0x30, 0x30, 0x64, 0x63}; // 00dc 40 | const uint8_t wbBuf[4] = {0x30, 0x31, 0x77, 0x62}; // 01wb 41 | static const uint8_t idx1Buf[4] = {0x69, 0x64, 0x78, 0x31}; // idx1 42 | static const uint8_t zeroBuf[4] = {0x00, 0x00, 0x00, 0x00}; // 0000 43 | static uint8_t* idxBuf[2] = {NULL, NULL}; 44 | 45 | uint8_t aviHeader[AVI_HEADER_LEN] = { // AVI header template 46 | 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x41, 0x56, 0x49, 0x20, 0x4C, 0x49, 0x53, 0x54, 47 | 0x16, 0x01, 0x00, 0x00, 0x68, 0x64, 0x72, 0x6C, 0x61, 0x76, 0x69, 0x68, 0x38, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x6C, 0x00, 0x00, 0x00, 52 | 0x73, 0x74, 0x72, 0x6C, 0x73, 0x74, 0x72, 0x68, 0x30, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x73, 53 | 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x66, 56 | 0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 57 | 0x01, 0x00, 0x18, 0x00, 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x4C, 0x49, 0x53, 0x54, 0x56, 0x00, 0x00, 0x00, 60 | 0x73, 0x74, 0x72, 0x6C, 0x73, 0x74, 0x72, 0x68, 0x30, 0x00, 0x00, 0x00, 0x61, 0x75, 0x64, 0x73, 61 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 62 | 0x01, 0x00, 0x00, 0x00, 0x11, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 63 | 0x11, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x66, 64 | 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x11, 0x2B, 0x00, 0x00, 0x11, 0x2B, 0x00, 0x00, 65 | 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, 66 | 0x4C, 0x49, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x76, 0x69, 67 | }; 68 | 69 | struct frameSizeStruct { 70 | uint8_t frameWidth[2]; 71 | uint8_t frameHeight[2]; 72 | }; 73 | // indexed by frame type - needs to be consistent with sensor.h framesize_t enum 74 | static const frameSizeStruct frameSizeData[] = { 75 | {{0x60, 0x00}, {0x60, 0x00}}, // 96X96 76 | {{0xA0, 0x00}, {0x78, 0x00}}, // qqvga 77 | {{0x80, 0x00}, {0x80, 0x00}}, // 128X128 78 | {{0xB0, 0x00}, {0x90, 0x00}}, // qcif 79 | {{0xF0, 0x00}, {0xB0, 0x00}}, // hqvga 80 | {{0xF0, 0x00}, {0xF0, 0x00}}, // 240X240 81 | {{0x40, 0x01}, {0xF0, 0x00}}, // qvga 82 | {{0x40, 0x01}, {0x40, 0x01}}, // 320X320 83 | {{0x90, 0x01}, {0x28, 0x01}}, // cif 84 | {{0xE0, 0x01}, {0x40, 0x01}}, // hvga 85 | {{0x80, 0x02}, {0xE0, 0x01}}, // vga 86 | {{0x20, 0x03}, {0x58, 0x02}}, // svga 87 | {{0x00, 0x04}, {0x00, 0x03}}, // xga 88 | {{0x00, 0x05}, {0xD0, 0x02}}, // hd 89 | {{0x00, 0x05}, {0x00, 0x04}}, // sxga 90 | {{0x40, 0x06}, {0xB0, 0x04}}, // uxga 91 | {{0x98, 0x03}, {0x38, 0x04}}, // FHD 92 | {{0xD0, 0x02}, {0x00, 0x05}}, // P_HD 93 | {{0x60, 0x03}, {0x00, 0x06}}, // P_3MP 94 | {{0x00, 0x08}, {0x00, 0x06}}, // QXGA 95 | {{0x00, 0x0A}, {0xA0, 0x05}}, // QHD 96 | {{0x00, 0x0A}, {0x40, 0x06}}, // WQXGA 97 | {{0x38, 0x04}, {0x80, 0x07}}, // P_FHD 98 | {{0x00, 0x0A}, {0x80, 0x07}}, // QSXGA 99 | {{0x20, 0x0A}, {0x98, 0x07}} // 5MP 100 | }; 101 | 102 | #define IDX_ENTRY 16 // bytes per index entry 103 | 104 | // separate index for motion capture and timelapse 105 | static size_t idxPtr[2]; 106 | static size_t idxOffset[2]; 107 | static size_t moviSize[2]; 108 | static size_t audSize; 109 | static size_t indexLen[2]; 110 | static File wavFile; 111 | bool haveSoundFile = false; 112 | 113 | 114 | void prepAviIndex(bool isTL) { 115 | // prep buffer to store index data, gets appended to end of file 116 | if (idxBuf[isTL] == NULL) idxBuf[isTL] = (uint8_t*)ps_malloc((maxFrames+1)*IDX_ENTRY); // include some space for audio index 117 | memcpy(idxBuf[isTL], idx1Buf, 4); // index header 118 | idxPtr[isTL] = CHUNK_HDR; // leave 4 bytes for index size 119 | moviSize[isTL] = indexLen[isTL] = 0; 120 | idxOffset[isTL] = 4; // 4 byte offset 121 | } 122 | 123 | void buildAviHdr(uint8_t FPS, uint8_t frameType, uint16_t frameCnt, bool isTL) { 124 | // update AVI header template with file specific details 125 | size_t aviSize = moviSize[isTL] + AVI_HEADER_LEN + ((CHUNK_HDR+IDX_ENTRY) * (frameCnt+(haveSoundFile?1:0))); // AVI content size 126 | // update aviHeader with relevant stats 127 | memcpy(aviHeader+4, &aviSize, 4); 128 | uint32_t usecs = (uint32_t)round(1000000.0f / FPS); // usecs_per_frame 129 | memcpy(aviHeader+0x20, &usecs, 4); 130 | memcpy(aviHeader+0x30, &frameCnt, 2); 131 | memcpy(aviHeader+0x8C, &frameCnt, 2); 132 | memcpy(aviHeader+0x84, &FPS, 1); 133 | uint32_t dataSize = moviSize[isTL] + ((frameCnt+(haveSoundFile?1:0)) * CHUNK_HDR) + 4; 134 | memcpy(aviHeader+0x12E, &dataSize, 4); // data size 135 | 136 | // apply video framesize to avi header 137 | memcpy(aviHeader+0x40, frameSizeData[frameType].frameWidth, 2); 138 | memcpy(aviHeader+0xA8, frameSizeData[frameType].frameWidth, 2); 139 | memcpy(aviHeader+0x44, frameSizeData[frameType].frameHeight, 2); 140 | memcpy(aviHeader+0xAC, frameSizeData[frameType].frameHeight, 2); 141 | 142 | #if INCLUDE_AUDIO 143 | uint8_t withAudio = 2; // increase number of streams for audio 144 | if (isTL) memcpy(aviHeader+0x100, zeroBuf, 4); // no audio for timelapse 145 | else { 146 | if (haveSoundFile) memcpy(aviHeader+0x38, &withAudio, 1); 147 | memcpy(aviHeader+0x100, &audSize, 4); // audio data size 148 | } 149 | // apply audio details to avi header 150 | memcpy(aviHeader+0xF8, &SAMPLE_RATE, 4); 151 | uint32_t bytesPerSec = SAMPLE_RATE * 2; 152 | memcpy(aviHeader+0x104, &bytesPerSec, 4); // suggested buffer size 153 | memcpy(aviHeader+0x11C, &SAMPLE_RATE, 4); 154 | memcpy(aviHeader+0x120, &bytesPerSec, 4); // bytes per sec 155 | #else 156 | memcpy(aviHeader+0x100, zeroBuf, 4); 157 | #endif 158 | 159 | // reset state for next recording 160 | moviSize[isTL] = idxPtr[isTL] = 0; 161 | idxOffset[isTL] = 4; // 4 byte offset 162 | } 163 | 164 | void buildAviIdx(size_t dataSize, bool isVid, bool isTL) { 165 | // build AVI video index into buffer - 16 bytes per frame 166 | // called from saveFrame() for each frame 167 | moviSize[isTL] += dataSize; 168 | if (isVid) memcpy(idxBuf[isTL]+idxPtr[isTL], dcBuf, 4); 169 | else memcpy(idxBuf[isTL]+idxPtr[isTL], wbBuf, 4); 170 | memcpy(idxBuf[isTL]+idxPtr[isTL]+4, zeroBuf, 4); 171 | memcpy(idxBuf[isTL]+idxPtr[isTL]+8, &idxOffset[isTL], 4); 172 | memcpy(idxBuf[isTL]+idxPtr[isTL]+12, &dataSize, 4); 173 | idxOffset[isTL] += dataSize + CHUNK_HDR; 174 | idxPtr[isTL] += IDX_ENTRY; 175 | } 176 | 177 | size_t writeAviIndex(byte* clientBuf, size_t buffSize, bool isTL) { 178 | // write completed index to avi file 179 | // called repeatedly from closeAvi() until return 0 180 | if (idxPtr[isTL] < indexLen[isTL]) { 181 | if (indexLen[isTL]-idxPtr[isTL] > buffSize) { 182 | memcpy(clientBuf, idxBuf[isTL]+idxPtr[isTL], buffSize); 183 | idxPtr[isTL] += buffSize; 184 | return buffSize; 185 | } else { 186 | // final part of index 187 | size_t final = indexLen[isTL]-idxPtr[isTL]; 188 | memcpy(clientBuf, idxBuf[isTL]+idxPtr[isTL], final); 189 | idxPtr[isTL] = indexLen[isTL]; 190 | return final; 191 | } 192 | } 193 | return idxPtr[isTL] = 0; 194 | } 195 | 196 | void finalizeAviIndex(uint16_t frameCnt, bool isTL) { 197 | // update index with size 198 | uint32_t sizeOfIndex = (frameCnt+(haveSoundFile?1:0))*IDX_ENTRY; 199 | memcpy(idxBuf[isTL]+4, &sizeOfIndex, 4); // size of index 200 | indexLen[isTL] = sizeOfIndex + CHUNK_HDR; 201 | idxPtr[isTL] = 0; // pointer to index buffer 202 | } 203 | 204 | bool haveWavFile(bool isTL) { 205 | haveSoundFile = false; 206 | audSize = 0; 207 | #if INCLUDE_AUDIO 208 | if (isTL) return false; 209 | // check if wave file exists 210 | if (!STORAGE.exists(WAVTEMP)) return 0; 211 | // open it and get its size 212 | wavFile = STORAGE.open(WAVTEMP, FILE_READ); 213 | if (wavFile) { 214 | // add sound file index 215 | audSize = wavFile.size() - WAV_HDR_LEN; 216 | buildAviIdx(audSize, false); 217 | // add sound file header 218 | wavFile.seek(WAV_HDR_LEN, SeekSet); // skip over header 219 | haveSoundFile = true; 220 | } 221 | #endif 222 | return haveSoundFile; 223 | } 224 | 225 | size_t writeWavFile(byte* clientBuf, size_t buffSize) { 226 | // read in wav file and write to avi file 227 | // called repeatedly from closeAvi() until return 0 228 | static size_t offsetWav = CHUNK_HDR; 229 | if (offsetWav) { 230 | // add sound file header 231 | memcpy(clientBuf, wbBuf, 4); 232 | memcpy(clientBuf+4, &audSize, 4); 233 | } 234 | size_t readLen = wavFile.read(clientBuf+offsetWav, buffSize-offsetWav) + offsetWav; 235 | offsetWav = 0; 236 | if (readLen) return readLen; 237 | // get here if finished 238 | wavFile.close(); 239 | STORAGE.remove(WAVTEMP); 240 | offsetWav = CHUNK_HDR; 241 | return 0; 242 | } 243 | -------------------------------------------------------------------------------- /certificates.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | NetworkClientSecure encrypts connections to remote servers (eg github, smtp) 3 | To optionally validate identity of remote server (prevent man-in-middle threats), 4 | its public certificate needs to be checked by the app. 5 | Use openssl tool to obtain public certificate of remote server, eg: 6 | openssl s_client -showcerts -verify 5 -connect raw.githubusercontent.com:443 7 | openssl s_client -showcerts -verify 5 -connect smtp.gmail.com:465 8 | Copy and paste last listed certificate (usually root CA certificate) into relevant constant below. 9 | To disable certificate checking (NetworkClientSecure) leave relevant constant empty, and / or 10 | on web page under Access Settings / Authentication settings set Use Secure to off 11 | 12 | FTP connection is plaintext as FTPS not implemented. 13 | 14 | 15 | To set app as HTTPS server, a server private key and public certificate are required 16 | Create keys and certificates using openssl tool 17 | 18 | Define app to have static IP address, and use as variable substitution for openssl: 19 | set APP_IP="192.168.1.135" 20 | Create app server private key and public certificate: 21 | openssl req -nodes -x509 -sha256 -newkey rsa:4096 -subj "/CN=%APP_IP%" -addext "subjectAltName = IP:%APP_IP%" -extensions v3_ca -keyout prvtkey.pem -out cacert.pem -days 800 22 | 23 | Paste content of prvtkey.pem and cacert.pem files into prvtkey_pem and cacert_pem constants below. 24 | View server cert content: 25 | openssl x509 -in cacert.pem -noout -text 26 | 27 | Use of HTTPS is controlled on web page by option 'Use HTTPS' under Access Settings / Authentication settings 28 | If the private key or public certificate constants are empty, the Use HTTPS setting is ignored. 29 | 30 | Enter `https://static_ip` to access the app from the browser. A security warning will be displayed as the certificate is self signed so untrusted. To trust the certificate it needs to be installed on the device: 31 | - open the Chrome settings page. 32 | - in the Privacy and security panel, expand the Security section, click on Manage certificates. 33 | - in the Certificates popup, select the Trusted Root Certification Authorities tab, click the Import... button to launch the Import Wizard. 34 | - click Next, on the next page, select Browse... and locate the cacert.pem file. 35 | - click Next, then Finish,then in the Security Warning popup, click on Yes and another popup indicates that the import was successful. 36 | 37 | s60sc 2023 38 | */ 39 | 40 | #include "appGlobals.h" 41 | 42 | #if INCLUDE_CERTS 43 | 44 | // GitHub public certificate valid till April 2031 45 | const char* git_rootCACertificate = R"~( 46 | -----BEGIN CERTIFICATE----- 47 | MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh 48 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 49 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH 50 | MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT 51 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh 52 | bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD 53 | ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV 54 | cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy 55 | FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc 56 | 3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8 57 | osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT 58 | zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud 59 | EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G 60 | A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd 61 | BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG 62 | CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG 63 | NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH 64 | Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t 65 | L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC 66 | ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG 67 | 9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t 68 | wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS 69 | slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R 70 | bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4 71 | chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN 72 | JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA== 73 | -----END CERTIFICATE----- 74 | )~"; 75 | 76 | // Paste in app server private key 77 | const char* prvtkey_pem = R"~( 78 | -----BEGIN PRIVATE KEY----- 79 | MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDy/QRi/nRv+I1q 80 | woddDXtvGHMkMA8TznvgtVBkh4FBaPQl/0v455/hUVKbyHq9MyH+S9mel0nVuJIQ 81 | KZPfF/hSttt1C5963Hp8c6SvoGBfS6iIVp7DKBupiMXYKn+4k1SK7v9LAPRcGYAC 82 | Z2juuthhixD+Z1ylsOvIFu5il4cdm7e3HLL0eM5cw52+I+MrcjwVhP37IEdEflhp 83 | jZfgZb8PAhSiK/gj+N4UeVYSsegvXk0fss4MAenttJ6g9XJJolqfMGI+abYsdg9j 84 | gKfaMZJCqx+RpD7VrsQvv3NTeXL4v7u4+H8FGVbkvpiVP8QKfk2ptiDQ3FC2yvyZ 85 | yKgVTHQXo1htxR8dMQW2eHoUMCxXEP5tHNBiQoHVudL0eItqHS3Zsv6dWMDpEZb3 86 | VShnaGxNqEpiz/cnTOZyTtLqItofnmsOOu/GeAwV7FfnKgDhuvBxe+dAuJWs4lk4 87 | 2hgMNvpHm811Gw51ucYC+2Kshw1VQmhYQGYnR2ynNf99kcc3NCqsdU3xwOIliily 88 | xbagcyDa11y7L8UIpYmCHTzBzUEdyAFNBPnyiSMUVXdG7FTGnyDbkz10zdmFBTys 89 | CEsB5tgt5Ul46XXAUkTc314dqNi4qMz4oBhv2lBnJLbcn7EgqXJJYh13JCmJcT31 90 | vMD232wVMWNPs0NwFKpT27cAtIBFTQIDAQABAoICAADJb/ezAj8/H6uRPoSE8T+c 91 | /MrUb2AQdCiycfptubmQumWsjr4yd0bwvcubFADbHk54vFFySsqwy3UFTizILn+i 92 | zMXzfHUK/LyOKa2XX3/LI22faozWDi/JORmzmp8YtlCaIrYkVMknv3BrR/IHDjuw 93 | 0DAjcXV/uCAcQFq8j7M5WHV07zLgSi7Jib6kSwS3IqshK+UzPvfV80USarhDceda 94 | rLBXBr9d+6EIhqWaNxEKv3QBx6I3ry/tX6whrHAG4PnLFbn/MkaZw9FycYdA8qjl 95 | EFUP0MgX6NXp1sGKZl6YTRTqCLqKLiYVNex25YjZ5E994edpyiPZwuHrsni8o8fH 96 | HA0DcY1UsuQEjpGmAdA6PsLnln9BzzBi1pWP29+0LtPvieYxQxo5uVSxrJzQ5eqn 97 | ze55Q2hene8/HTB1mMJ8spKflypF4jEStqKtRvxKVQ3/bmhaG+1Yy0/sznok+mLg 98 | tzp2J3eMqXxo1OytJ/glxGzOSGmozizfLuLkcllKG00+dS7Q2fuHlh7QFFiGOvh1 99 | ArWVbAncjiOgDa/mM+cTi8DVYndqOYN1c4hkuIeKQ+lFAGF1DQJ4WSFXLQfPDRwN 100 | lIqQUd2D+RvSYgd8VTEHM5tmV51WflPZZ4WzJrzU9igbNpjtsM5P4DYWdsHoaDxg 101 | IIBmv2y/LVIXhNkSlsYTAoIBAQD7foX7Gm6ISxpNb/DkpUXVC3Zss7goQxocIjQk 102 | PFQiyMQEyFqECncAcBRpwJwNf/RGeFPB8HcTC10ae4awbVKCn+jOK9lBgkedZu2u 103 | jAdLrsLN2Jm5ezE/FpP/JHiNVF3obRDsVk38bTRd9I39WrNWdqtMH/EtmoEpqr4/ 104 | Line+H9VCHVmuEkC6N+yEL17B5qeGd5QtlCOqRRrLGYgk17pjoPzdUoM0qNcSotB 105 | V2I8BkBJpgg0asmlwDiThoXh5rUbaBh4W8j5MYWzsqeHx6AGEpb8ld0aWBihmmZJ 106 | +0NrvFm1U3F3gOOpWQhmiHhg8HE3A2y5N8Bk0gTS6xSLMV6bAoIBAQD3V3tJsUjB 107 | ywRaXNj2wIUg4Go7R1gzehjZN2yCK1DoCyFpGzmy9zblVHBtzfQwwLHigra1WFWC 108 | B1SjgPh1STmrC9eIw6cU+GUYTHQFtKX2iz/iztFcFkJnFbF8wyqbrzBA0Z7Fzn/c 109 | HlO9+tt+VjUEv81Tvk2iRa4yWH6NB5P+WGiFy28KvTj/bC0SXC4MgGD61ChSteHB 110 | q6EO18arqBcm62RKH9lF2E6xP4/94C6goHVzWuMdOnQ2BgKvl0S1C43/f+9i0laC 111 | DEuqnaNrpAsOomh/sFuuHYGppaFUzId3Z8bfwSIc1PJgyUgymoUYKXBUVX1ZxMxL 112 | euIr58Z1tPY3AoIBAACnAV33YZYE69qLkcpmC1pUH0iE5tNj6Sttg0kcxvMYJjoE 113 | 8wcop8pegA8OKtl2HYIZSc5U+1oXS3SIIX9PqUkhdQ8j2fprhhgIblFnl5VArMyv 114 | 5SYwBZ6uRlABHjbvoxa5QbP7PVSMS/h6a+veUlzFDgiyhIOjxPYAtWGgkwc7CcmE 115 | rhlIHRhe1kW1+WfaSzJhysvWzTqxgZYNlW48M6DTd9An27tQyI+yuc2/lkellIEc 116 | ZyULqd4+M2dej/ZYDNw3VujpBApxcHFY40pc4DNj1PRuxxYMaHPy3JUQi8o5wNnR 117 | j5fJw81qp7TsYbOOrByCa8PHOz6HtO9/IJyD0kUCggEALMSixgXWm2z5jrl7c74I 118 | 2piD4dLZ/gc9dCN5+l2IuVc6ZuHMob3pK70K1HUQm7pk+BCcrVodr/lPsoBneCMW 119 | 0wTDsDdpiHwlIC7GWToHSAaQO6cfccF9p1bf1yskDSW6YCEQ0dC8h8Tdd2duTwGf 120 | ewqUSXIKbzKZgvdNgI08li6+TGkz4ge5x1F3HvmcRBsAcqXv3niZMgq0jhE0HmHA 121 | PwUgE+KL2v55z88natYm2l/woj5zGRk5a4XO+qUwhGxg+TvYwlQ74DIFiA4cRCFe 122 | 9vkiXOo4zdz9WQ1nlAepBU29S0aTvBA3Bpmn/bDGIkdt03XdyF+8cnT9duDupONq 123 | JQKCAQBQ0pvRSVfW3QbXRH6j6IAYBCJmm35A5D5E+H3FdenkGqKh04hbCSy1rMtd 124 | 6kcTCZaCmRpYl4JoE7jVIl/WPg6cLeD8PvQEYPoBFBCyOoLVVLCiTduHoHqgO3bV 125 | f5UT/2FThSybboP97JwEZRtk62WOxsWZVfy/187XuVGpigKw+R2lqyVfAeu8+k6+ 126 | GVYwsQtR4Htmv42d7UXdT01OR3x45ciC0ezH9tnk5b3gJuaRUEmxaxt5R1YIT3W4 127 | hZvnVPO6Hvk2Bb/xViqGkjNrLhXkN3BieJ+iIJ3Hb/k33mLocYZk0hOXTZn6O/73 128 | B5e2Vlm3qrdvy8qCTHRrjxiMEceq 129 | -----END PRIVATE KEY----- 130 | )~"; 131 | 132 | // Paste in self signed app server public certificate 133 | const char* cacert_pem = R"~( 134 | -----BEGIN CERTIFICATE----- 135 | MIIFIjCCAwqgAwIBAgIUM5ivBIoTo1Mdi/HXg0OSH0S8ww8wDQYJKoZIhvcNAQEL 136 | BQAwGDEWMBQGA1UEAwwNMTkyLjE2OC4xLjEzNTAeFw0yMzEyMjIxNzU0MjdaFw0y 137 | NjAzMDExNzU0MjdaMBgxFjAUBgNVBAMMDTE5Mi4xNjguMS4xMzUwggIiMA0GCSqG 138 | SIb3DQEBAQUAA4ICDwAwggIKAoICAQDy/QRi/nRv+I1qwoddDXtvGHMkMA8Tznvg 139 | tVBkh4FBaPQl/0v455/hUVKbyHq9MyH+S9mel0nVuJIQKZPfF/hSttt1C5963Hp8 140 | c6SvoGBfS6iIVp7DKBupiMXYKn+4k1SK7v9LAPRcGYACZ2juuthhixD+Z1ylsOvI 141 | Fu5il4cdm7e3HLL0eM5cw52+I+MrcjwVhP37IEdEflhpjZfgZb8PAhSiK/gj+N4U 142 | eVYSsegvXk0fss4MAenttJ6g9XJJolqfMGI+abYsdg9jgKfaMZJCqx+RpD7VrsQv 143 | v3NTeXL4v7u4+H8FGVbkvpiVP8QKfk2ptiDQ3FC2yvyZyKgVTHQXo1htxR8dMQW2 144 | eHoUMCxXEP5tHNBiQoHVudL0eItqHS3Zsv6dWMDpEZb3VShnaGxNqEpiz/cnTOZy 145 | TtLqItofnmsOOu/GeAwV7FfnKgDhuvBxe+dAuJWs4lk42hgMNvpHm811Gw51ucYC 146 | +2Kshw1VQmhYQGYnR2ynNf99kcc3NCqsdU3xwOIliilyxbagcyDa11y7L8UIpYmC 147 | HTzBzUEdyAFNBPnyiSMUVXdG7FTGnyDbkz10zdmFBTysCEsB5tgt5Ul46XXAUkTc 148 | 314dqNi4qMz4oBhv2lBnJLbcn7EgqXJJYh13JCmJcT31vMD232wVMWNPs0NwFKpT 149 | 27cAtIBFTQIDAQABo2QwYjAdBgNVHQ4EFgQULbmd0u6MQvmz8NjD5kSew1jKXg0w 150 | HwYDVR0jBBgwFoAULbmd0u6MQvmz8NjD5kSew1jKXg0wDwYDVR0TAQH/BAUwAwEB 151 | /zAPBgNVHREECDAGhwTAqAGHMA0GCSqGSIb3DQEBCwUAA4ICAQByghxxDQ9AGlK0 152 | t2+HKUnd/+rTn1YsD7uNNYaKK0Nmm9O6Bq0/cARsD0YGwpBGVloWoWoWKIuvJA+9 153 | p2UmKGAlTWz0+JaVbDEpi1XegIi2ZR8CQNnngpy7lBzCwiKxils/kwTv7Hzakia7 154 | Ddbd+0qxJcA5MUg45jCamqY/jNChdNe9TPupfWJ9E+6E6d5aIlo50zXBfDlfES+Z 155 | YS5TL6wxomCaWI33a/I+pZE5wtAy+bGzznSkF8Sx4kn3I6ab60rjG+prqiqHwTt2 156 | 00JZJhe6bQc+shPe7qmuNJeW/uFwPAdE1df6h5A6biSLUCenfZP+7FgL9tl0baMn 157 | LjpOB9PTJ5sK1S/GrnwmdKXOiluY7Mqd+vumUluOSGaZdDSrnhop+juI4C603QSs 158 | dBjNKqJJ48QYRTW4qlW9QARcuBq/aX0qLiLTE/rpUaqqhi4qPADb/GVw9e7Iay8r 159 | 0nCPHSAony2uTcDEVYxTp/WSL7fxTCXEvwHkJNAZf3qR0NESwbqYGKYdpxDFEDa5 160 | aZm1Jd72d2IvFvXUUPq6FFWKu55qf16QMV76+Ls/19idTrVD1JHf0sd0NS1+Bzwt 161 | R5IfapcWlNOtjpA5AF9AGSor9+rekXtgK1NmXyT9g1zYk/gHlEQNoDAjHP76p7ei 162 | G8kPCt8uxFlnuaH9HPstmlY3qRj9BA== 163 | -----END CERTIFICATE----- 164 | )~"; 165 | 166 | 167 | // Your FTPS Server's root public certificate (not implemented) 168 | const char* ftps_rootCACertificate = R"~( 169 | )~"; 170 | 171 | 172 | // Your SMTP Server's public certificate (eg smtp.gmail.com valid till Jan 2028) 173 | const char* smtp_rootCACertificate = R"~( 174 | -----BEGIN CERTIFICATE----- 175 | MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX 176 | MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE 177 | CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx 178 | OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT 179 | GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx 180 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63 181 | ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS 182 | iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k 183 | KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ 184 | DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk 185 | j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5 186 | cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW 187 | CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499 188 | iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei 189 | Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap 190 | sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b 191 | 9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP 192 | BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf 193 | BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw 194 | JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH 195 | MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al 196 | oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy 197 | MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF 198 | AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9 199 | NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9 200 | WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw 201 | 9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy 202 | +qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi 203 | d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8= 204 | -----END CERTIFICATE----- 205 | )~"; 206 | 207 | 208 | // Your MQTT Server's public certificate 209 | const char* mqtt_rootCACertificate = R"~( 210 | )~"; 211 | 212 | // Telegram server certificate for api.telegram.org, valid till May 2031 213 | const char* telegram_rootCACertificate = R"~( 214 | -----BEGIN CERTIFICATE----- 215 | MIIEfTCCA2WgAwIBAgIDG+cVMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVT 216 | MSEwHwYDVQQKExhUaGUgR28gRGFkZHkgR3JvdXAsIEluYy4xMTAvBgNVBAsTKEdv 217 | IERhZGR5IENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMTAx 218 | MDcwMDAwWhcNMzEwNTMwMDcwMDAwWjCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT 219 | B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHku 220 | Y29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1 221 | dGhvcml0eSAtIEcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv3Fi 222 | CPH6WTT3G8kYo/eASVjpIoMTpsUgQwE7hPHmhUmfJ+r2hBtOoLTbcJjHMgGxBT4H 223 | Tu70+k8vWTAi56sZVmvigAf88xZ1gDlRe+X5NbZ0TqmNghPktj+pA4P6or6KFWp/ 224 | 3gvDthkUBcrqw6gElDtGfDIN8wBmIsiNaW02jBEYt9OyHGC0OPoCjM7T3UYH3go+ 225 | 6118yHz7sCtTpJJiaVElBWEaRIGMLKlDliPfrDqBmg4pxRyp6V0etp6eMAo5zvGI 226 | gPtLXcwy7IViQyU0AlYnAZG0O3AqP26x6JyIAX2f1PnbU21gnb8s51iruF9G/M7E 227 | GwM8CetJMVxpRrPgRwIDAQABo4IBFzCCARMwDwYDVR0TAQH/BAUwAwEB/zAOBgNV 228 | HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9BUFuIMGU2g/eMB8GA1Ud 229 | IwQYMBaAFNLEsNKR1EwRcbNhyz2h/t2oatTjMDQGCCsGAQUFBwEBBCgwJjAkBggr 230 | BgEFBQcwAYYYaHR0cDovL29jc3AuZ29kYWRkeS5jb20vMDIGA1UdHwQrMCkwJ6Al 231 | oCOGIWh0dHA6Ly9jcmwuZ29kYWRkeS5jb20vZ2Ryb290LmNybDBGBgNVHSAEPzA9 232 | MDsGBFUdIAAwMzAxBggrBgEFBQcCARYlaHR0cHM6Ly9jZXJ0cy5nb2RhZGR5LmNv 233 | bS9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAWQtTvZKGEacke+1bMc8d 234 | H2xwxbhuvk679r6XUOEwf7ooXGKUwuN+M/f7QnaF25UcjCJYdQkMiGVnOQoWCcWg 235 | OJekxSOTP7QYpgEGRJHjp2kntFolfzq3Ms3dhP8qOCkzpN1nsoX+oYggHFCJyNwq 236 | 9kIDN0zmiN/VryTyscPfzLXs4Jlet0lUIDyUGAzHHFIYSaRt4bNYC8nY7NmuHDKO 237 | KHAN4v6mF56ED71XcLNa6R+ghlO773z/aQvgSMO3kwvIClTErF0UZzdsyqUvMQg3 238 | qm5vjLyb4lddJIGvl5echK1srDdMZvNhkREg5L4wn3qkKQmw4TRfZHcYQFHfjDCm 239 | rw== 240 | -----END CERTIFICATE----- 241 | )~"; 242 | 243 | 244 | // Your HTTPS File Server public certificate 245 | const char* hfs_rootCACertificate = R"~( 246 | -----BEGIN CERTIFICATE----- 247 | MIIDKzCCAhOgAwIBAgIQJOOyowYpLEiLnhpjD0HR5DANBgkqhkiG9w0BAQsFADAg 248 | MR4wHAYDVQQDExVSZWJleCBUaW55IFdlYiBTZXJ2ZXIwHhcNMjMxMTI1MTQ1NzU4 249 | WhcNMzMxMTI1MTQ1NzU4WjAgMR4wHAYDVQQDExVSZWJleCBUaW55IFdlYiBTZXJ2 250 | ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGgUPfvM+SI1OiOozW 251 | MQUtj/E9461SRFIYfce7b0KANrc/0S4qD11tTQvKvoY7HY5gvc0jgWZBGqQ6/KO7 252 | CIrLnXtsldeUXokcd8cvr8Ko704iOboHLLhewZ4egeBgu6+/1dw6REeExHjNIjiO 253 | 1InShPIV8yxX1oUo+EztIo5qecVvrousyIL9KBmAAi7Pdw0/yKQaoXzL9Ehkpv7g 254 | Pbabt6k7W+CofXRhaoGlf5ERvb5T/921PXXdCq7mnB9OjGu0almqYIWh1jRjnBQH 255 | e2avg+LD/+1dakIXXzByhbEpECxtQHZ17iB3DvW0ExiMH+0A8bqQIMp3sOgEzf2J 256 | 42XpAgMBAAGjYTBfMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUUmrFj3+h5tsV 257 | ASwGkjFZ9W5FDf8wEwYDVR0lBAwwCgYIKwYBBQUHAwEwGQYDVR0RBBIwEIIJbG9j 258 | YWxob3N0ggNtcG8wDQYJKoZIhvcNAQELBQADggEBAJ+rf1UUwaZAhsHrL2KX0WPm 259 | E9lCcnBFeQHUILSfM7r7fbEuIXa68mZDeMIV9xs4ex45wN4AAZW1l79Okia9kin8 260 | JkqkhZ/rCvqWsbNt3ryOvWCB2a2JEWW6yRA6EgK+STo3T/Z8Sau0ys8woc7y486l 261 | 5BhGu7rlXcbXl8hcEORD/ILxxdae7hHi7sXIReyS2kGiYJUwj+1+6mm26TXuRyCV 262 | jqlsBxH8gnwIlupODKZ/7jU/HhiYaKEbrnNxiOiPeWAw/KJJH5lUxt0piOYIXhj4 263 | DuDay+U7jeJKpND7EYheZY/U6c1wqwXt1DHuFnCCzK8jdOGT9aUSqZUeWfNn9cc= 264 | -----END CERTIFICATE----- 265 | )~"; 266 | 267 | #endif 268 | -------------------------------------------------------------------------------- /externalHeartbeat.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Contributed by alojzjakob 3 | 4 | #include "appGlobals.h" 5 | 6 | #if INCLUDE_EXTHB 7 | 8 | // External Heartbeat 9 | char external_heartbeat_domain[32] = ""; //External Heartbeat domain/IP 10 | char external_heartbeat_uri[64] = ""; //External Heartbeat uri (i.e. /myesp32-cam-hub/index.php) 11 | int external_heartbeat_port; //External Heartbeat server port to connect. 12 | char external_heartbeat_token[32] = ""; //External Heartbeat server username. 13 | 14 | bool external_heartbeat_active = false; 15 | 16 | void sendExternalHeartbeat() { 17 | 18 | // external_heartbeat_active~0~2~C~External Heartbeat Server enabled 19 | // external_heartbeat_domain~~2~T~Heartbeat receiver domain or IP (i.e. www.mydomain.com) 20 | // external_heartbeat_uri~~2~T~Heartbeat receiver URI (i.e. /my-esp32cam-hub/index.php) 21 | // external_heartbeat_port~443~2~N~Heartbeat receiver port 22 | // external_heartbeat_token~~2~T~Heartbeat receiver auth token 23 | 24 | // POST to external heartbeat address 25 | char uri[104] = ""; 26 | strcpy(uri, external_heartbeat_uri); 27 | strcat(uri, "?token="); 28 | strcat(uri, external_heartbeat_token); 29 | 30 | NetworkClientSecure hclient; 31 | 32 | buildJsonString(false); 33 | 34 | //hclient.setInsecure(); 35 | if (remoteServerConnect(hclient, external_heartbeat_domain, external_heartbeat_port, "", EXTERNALHB)) { 36 | HTTPClient https; 37 | int httpCode = HTTP_CODE_NOT_FOUND; 38 | if (https.begin(hclient, external_heartbeat_domain, external_heartbeat_port, uri, true)) { 39 | 40 | https.addHeader("Content-Type", "application/json"); 41 | 42 | httpCode = https.POST(jsonBuff); 43 | //httpCode = https.GET(); 44 | if (httpCode == HTTP_CODE_OK) { 45 | LOG_INF("External Heartbeat sent to: %s%s", external_heartbeat_domain, uri); 46 | } else LOG_WRN("External Heartbeat request failed, error: %s", https.errorToString(httpCode).c_str()); 47 | //if (httpCode != HTTP_CODE_OK) doGetExtIP = false; 48 | https.end(); 49 | } 50 | remoteServerClose(hclient); 51 | } 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /extras/CyberChef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/CyberChef.png -------------------------------------------------------------------------------- /extras/I2C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/I2C.jpg -------------------------------------------------------------------------------- /extras/TinyML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/TinyML.png -------------------------------------------------------------------------------- /extras/hasio_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/hasio_device.png -------------------------------------------------------------------------------- /extras/motion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/motion.png -------------------------------------------------------------------------------- /extras/partitions.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x5000, 3 | otadata, data, ota, 0xe000, 0x2000, 4 | app0, app, ota_0, 0x10000, 0x1c0000, 5 | app1, app, ota_1, 0x1d0000, 0x1c0000, 6 | spiffs, data, spiffs, 0x390000, 0x60000, 7 | coredump, data, coredump, 0x3f0000, 0x10000, -------------------------------------------------------------------------------- /extras/portForward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/portForward.png -------------------------------------------------------------------------------- /extras/setupPage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Application setup 7 | 8 | 9 | 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 |
Wifi setup..
29 | 30 | 34 |
38 | 39 | 40 |
47 | 48 | 49 |
52 |

54 |
55 | 56 | -------------------------------------------------------------------------------- /extras/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/telegram.png -------------------------------------------------------------------------------- /extras/webdav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s60sc/ESP32-CAM_MJPEG2SD/4ec29450d0973b3eb2064f4db6b5b42d2838cf64/extras/webdav.png -------------------------------------------------------------------------------- /ftp.cpp: -------------------------------------------------------------------------------- 1 | // Upload SD card or SPIFFS content to a remote server using FTP or HTTPS 2 | // 3 | // s60sc 2022, 2023 4 | 5 | #include "appGlobals.h" 6 | 7 | #if INCLUDE_FTP_HFS 8 | #if (!INCLUDE_CERTS) 9 | const char* hfs_rootCACertificate = ""; 10 | const char* ftps_rootCACertificate = ""; 11 | #endif 12 | 13 | // File server params (FTP or HTTPS), setup via web page 14 | char fsServer[MAX_HOST_LEN]; 15 | uint16_t fsPort = 21; 16 | char FS_Pass[MAX_PWD_LEN]; // FTP password or HTTPS passcode 17 | char fsWd[FILE_NAME_LEN]; 18 | 19 | static bool uploadInProgress = false; 20 | uint8_t percentLoaded = 0; 21 | TaskHandle_t fsHandle = NULL; 22 | static char storedPathName[FILE_NAME_LEN]; 23 | static char folderPath[FILE_NAME_LEN]; 24 | static byte* fsChunk; 25 | bool deleteAfter = false; // auto delete after upload 26 | bool autoUpload = false; // Automatically upload every created file to remote file server 27 | bool fsUse = false; // FTP if false, HTTPS if true 28 | 29 | 30 | /******************** HTTPS ********************/ 31 | 32 | // Upload file of folder of files from local storage to remote HTTPS file server 33 | // Requires significant heap space due to TLS. 34 | // Each file POST has following format, where the following values are derived 35 | // from the web page: 36 | // Host: FS Server 37 | // port: FS port 38 | // passcode: FS password 39 | // pathname: FS root dir + selected day folder/file 40 | /* 41 | POST /upload HTTP/1.1 42 | Host: 192.168.1.135 43 | Content-Length: 2412358 44 | Content-Type: multipart/form-data; boundary=123456789000000000000987654321 45 | 46 | --123456789000000000000987654321 47 | Content-disposition: form-data; name="json" 48 | Content-Type: "application/json" 49 | 50 | {"pathname":"/FS/root/dir/20231119/20231119_140513_SVGA_20_6_120.avi","passcode":"abcd1234"} 51 | --123456789000000000000987654321 52 | Content-disposition: form-data; name="file"; filename="20231119_140513_SVGA_20_6_120.avi" 53 | Content-Type: "application/octet-stream" 54 | 55 | 56 | --123456789000000000000987654321 57 | */ 58 | 59 | #define CONTENT_TYPE "Content-Type: \"%s\"\r\n\r\n" 60 | #define POST_HDR "POST /%s HTTP/1.1\r\nHost: %s\r\nContent-Length: %u\r\n" CONTENT_TYPE 61 | #define MULTI_TYPE "multipart/form-data; boundary=" BOUNDARY_VAL 62 | #define JSON_TYPE "application/json" 63 | #define BIN_TYPE "application/octet-stream" 64 | #define FORM_DATA "--" BOUNDARY_VAL "\r\nContent-disposition: form-data; name=\"%s%s\"\r\n" CONTENT_TYPE 65 | #define END_BOUNDARY "\r\n--" BOUNDARY_VAL "--\r\n" 66 | #define FILE_NAME "file\"; filename=\"" 67 | #define JSON_DATA "{\"pathname\":\"%s%s/%s\",\"passcode\":\"%s\"}" 68 | #define FORM_OFFSET 256 // offset in fsBuff to prepare form data 69 | 70 | NetworkClientSecure hclient; 71 | char* fsBuff; 72 | 73 | static void postHeader(const char* tmethod, const char* contentType, bool isFile, 74 | size_t fileSize, const char* fileName) { 75 | // create http post header 76 | char* p = fsBuff + FORM_OFFSET; // leave space for http request data 77 | if (isFile) { 78 | p += sprintf(p, FORM_DATA, "json", "", JSON_TYPE); 79 | // fsBuff initially contains folder name 80 | p += sprintf(p, JSON_DATA, fsWd, folderPath, fileName, FS_Pass); 81 | p += sprintf(p, "\r\n" FORM_DATA, FILE_NAME, fileName, BIN_TYPE); 82 | } // else JSON data already loaded by hfsCreateFolder() 83 | size_t formLen = strlen(fsBuff + FORM_OFFSET); 84 | // create http request header 85 | p = fsBuff; 86 | if (isFile) fileSize += formLen + strlen(END_BOUNDARY); 87 | p += sprintf(p, POST_HDR, tmethod, fsServer, fileSize, isFile ? MULTI_TYPE : JSON_TYPE); 88 | size_t reqLen = strlen(fsBuff); 89 | // concatenate request and form data 90 | if (formLen) { 91 | memmove(fsChunk + reqLen, fsChunk + FORM_OFFSET, formLen); 92 | fsChunk[reqLen + formLen] = 0; 93 | } 94 | hclient.print(fsBuff); // http header 95 | } 96 | 97 | static bool hfsStoreFile(File &fh) { 98 | // Upload individual file to HTTPS server 99 | // reject if folder or not valid file type 100 | #ifdef ISCAM 101 | if (!strstr(fh.name(), AVI_EXT) && !strstr(fh.name(), CSV_EXT) && !strstr(fh.name(), SRT_EXT)) return false; 102 | #else 103 | if (!strstr(fh.name(), FILE_EXT)) return false; 104 | #endif 105 | LOG_INF("Upload file: %s, size: %s", fh.name(), fmtSize(fh.size())); 106 | 107 | // prep POST header and send file to HTTPS server 108 | postHeader("upload", BIN_TYPE, true, fh.size(), fh.name()); 109 | // upload file content in chunks 110 | uint8_t percentLoaded = 0; 111 | size_t chunksize = 0, totalSent = 0; 112 | while ((chunksize = fh.read((uint8_t*)fsChunk, CHUNKSIZE))) { 113 | hclient.write((uint8_t*)fsChunk, chunksize); 114 | totalSent += chunksize; 115 | if (calcProgress(totalSent, fh.size(), 5, percentLoaded)) LOG_INF("Uploaded %u%%", percentLoaded); 116 | } 117 | percentLoaded = 100; 118 | hclient.println(END_BOUNDARY); 119 | return true; 120 | } 121 | 122 | /******************** FTP ********************/ 123 | 124 | // FTP control 125 | bool useFtps = false; 126 | char ftpUser[MAX_HOST_LEN]; 127 | static char rspBuf[256]; // Ftp response buffer 128 | static char respCodeRx[4]; // ftp response code 129 | static fs::FS fp = STORAGE; 130 | #define NO_CHECK "999" 131 | 132 | // WiFi Clients 133 | NetworkClient rclient; 134 | NetworkClient dclient; 135 | 136 | static bool sendFtpCommand(const char* cmd, const char* param, const char* respCode, const char* respCode2 = NO_CHECK) { 137 | // build and send ftp command 138 | if (strlen(cmd)) { 139 | rclient.print(cmd); 140 | rclient.println(param); 141 | } 142 | LOG_VRB("Sent cmd: %s%s", cmd, param); 143 | 144 | // wait for ftp server response 145 | uint32_t start = millis(); 146 | while (!rclient.available() && millis() < start + (responseTimeoutSecs * 1000)) delay(1); 147 | if (!rclient.available()) { 148 | LOG_WRN("FTP server response timeout"); 149 | return false; 150 | } 151 | // read in response code and message 152 | rclient.read((uint8_t*)respCodeRx, 3); 153 | respCodeRx[3] = 0; // terminator 154 | int readLen = rclient.read((uint8_t*)rspBuf, 255); 155 | rspBuf[readLen] = 0; 156 | while (rclient.available()) rclient.read(); // bin the rest of response 157 | 158 | // check response code with expected 159 | LOG_VRB("Rx code: %s, resp: %s", respCodeRx, rspBuf); 160 | if (strcmp(respCode, NO_CHECK) == 0) return true; // response code not checked 161 | if (strcmp(respCodeRx, respCode) != 0) { 162 | if (strcmp(respCodeRx, respCode2) != 0) { 163 | // incorrect response code 164 | LOG_ERR("Command %s got wrong response: %s %s", cmd, respCodeRx, rspBuf); 165 | return false; 166 | } 167 | } 168 | return true; 169 | } 170 | 171 | static bool ftpConnect() { 172 | // Connect to ftp or ftps 173 | if (rclient.connect(fsServer, fsPort)) {LOG_VRB("FTP connected at %s:%u", fsServer, fsPort);} 174 | else { 175 | LOG_WRN("Error opening ftp connection to %s:%u", fsServer, fsPort); 176 | return false; 177 | } 178 | if (!sendFtpCommand("", "", "220")) return false; 179 | if (useFtps) { 180 | if (sendFtpCommand("AUTH ", "TLS", "234")) { 181 | /* NOT IMPLEMENTED */ 182 | } else LOG_WRN("FTPS not available"); 183 | } 184 | if (!sendFtpCommand("USER ", ftpUser, "331")) return false; 185 | if (!sendFtpCommand("PASS ", FS_Pass, "230")) return false; 186 | // change to supplied folder 187 | if (!sendFtpCommand("CWD ", fsWd, "250")) return false; 188 | if (!sendFtpCommand("Type I", "", "200")) return false; 189 | return true; 190 | } 191 | 192 | static void ftpDisconnect() { 193 | // Disconnect from ftp server 194 | rclient.println("QUIT"); 195 | dclient.stop(); 196 | rclient.stop(); 197 | } 198 | 199 | static bool ftpCreateFolder(const char* folderName) { 200 | // create folder if non existent then change to it 201 | LOG_VRB("Check for folder %s", folderName); 202 | sendFtpCommand("CWD ", folderName, NO_CHECK); 203 | if (strcmp(respCodeRx, "550") == 0) { 204 | // non existent folder, create it 205 | if (!sendFtpCommand("MKD ", folderName, "257")) return false; 206 | //sendFtpCommand("SITE CHMOD 755 ", folderName, "200", "550"); // unix only 207 | if (!sendFtpCommand("CWD ", folderName, "250")) return false; 208 | } 209 | return true; 210 | } 211 | 212 | static bool openDataPort() { 213 | // set up port for data transfer 214 | if (!sendFtpCommand("PASV", "", "227")) return false; 215 | // derive data port number 216 | char* p = strchr(rspBuf, '('); // skip over initial text 217 | int p1, p2; 218 | int items = sscanf(p, "(%*d,%*d,%*d,%*d,%d,%d)", &p1, &p2); 219 | if (items != 2) { 220 | LOG_ERR("Failed to parse data port"); 221 | return false; 222 | } 223 | int dataPort = (p1 << 8) + p2; 224 | 225 | // Connect to data port 226 | LOG_VRB("Data port: %i", dataPort); 227 | if (!dclient.connect(fsServer, dataPort)) { 228 | LOG_WRN("Data connection failed"); 229 | return false; 230 | } 231 | return true; 232 | } 233 | 234 | static bool ftpStoreFile(File &fh) { 235 | // Upload individual file to current folder, overwrite any existing file 236 | // reject if folder, or not valid file type 237 | #ifdef ISCAM 238 | if (!strstr(fh.name(), AVI_EXT) && !strstr(fh.name(), CSV_EXT) && !strstr(fh.name(), SRT_EXT)) return false; 239 | #else 240 | if (!strstr(fh.name(), FILE_EXT)) return false; 241 | #endif 242 | char ftpSaveName[FILE_NAME_LEN]; 243 | strcpy(ftpSaveName, fh.name()); 244 | size_t fileSize = fh.size(); 245 | LOG_INF("Upload file: %s, size: %s", ftpSaveName, fmtSize(fileSize)); 246 | 247 | // open data connection 248 | openDataPort(); 249 | uint32_t writeBytes = 0; 250 | uint32_t uploadStart = millis(); 251 | size_t readLen, writeLen; 252 | if (!sendFtpCommand("STOR ", ftpSaveName, "150", "125")) return false; 253 | do { 254 | // upload file in chunks 255 | readLen = fh.read(fsChunk, CHUNKSIZE); 256 | if (readLen) { 257 | writeLen = dclient.write((const uint8_t*)fsChunk, readLen); 258 | writeBytes += writeLen; 259 | if (writeLen == 0) { 260 | LOG_WRN("Upload file to ftp failed"); 261 | return false; 262 | } 263 | if (calcProgress(writeBytes, fileSize, 5, percentLoaded)) LOG_INF("Uploaded %u%%", percentLoaded); 264 | } 265 | } while (readLen > 0); 266 | dclient.stop(); 267 | percentLoaded = 100; 268 | bool res = sendFtpCommand("", "", "226"); 269 | if (res) { 270 | LOG_ALT("Uploaded %s in %u sec", fmtSize(writeBytes), (millis() - uploadStart) / 1000); 271 | //sendFtpCommand("SITE CHMOD 644 ", ftpSaveName, "200", "550"); // unix only 272 | } else LOG_WRN("File transfer not successful"); 273 | return res; 274 | } 275 | 276 | 277 | /******************** Common ********************/ 278 | 279 | static bool getFolderName(const char* folderName) { 280 | // extract folder names from path name 281 | strcpy(folderPath, folderName); 282 | int pos = 1; // skip 1st '/' 283 | // get each folder name in sequence 284 | bool res = true; 285 | for (char* p = strchr(folderPath, '/'); (p = strchr(++p, '/')) != NULL; pos = p + 1 - folderPath) { 286 | *p = 0; // terminator 287 | if (!fsUse) res = ftpCreateFolder(folderPath + pos); 288 | } 289 | return res; 290 | } 291 | 292 | static bool uploadFolderOrFileFs(const char* fileOrFolder) { 293 | // Upload a single file or whole folder using FTP or HTTPS server 294 | // folder is uploaded file by file 295 | fsBuff = (char*)fsChunk; 296 | bool res = fsUse ? remoteServerConnect(hclient, fsServer, fsPort, hfs_rootCACertificate, FSFTP) : ftpConnect(); 297 | 298 | if (!res) { 299 | LOG_WRN("Unable to connect to %s server", fsUse ? "HTTPS" : "FTP"); 300 | return false; 301 | } 302 | res = false; 303 | const int saveRefreshVal = refreshVal; 304 | refreshVal = 1; 305 | File root = fp.open(fileOrFolder); 306 | if (!root.isDirectory()) { 307 | // Upload a single file 308 | char fsSaveName[FILE_NAME_LEN]; 309 | strcpy(fsSaveName, root.path()); 310 | if (getFolderName(root.path())) res = fsUse ? hfsStoreFile(root) : ftpStoreFile(root); 311 | #ifdef ISCAM 312 | // upload corresponding csv and srt files if exist 313 | if (res) { 314 | changeExtension(fsSaveName, CSV_EXT); 315 | if (fp.exists(fsSaveName)) { 316 | File csv = fp.open(fsSaveName); 317 | res = fsUse ? hfsStoreFile(csv) : ftpStoreFile(csv); 318 | csv.close(); 319 | } 320 | changeExtension(fsSaveName, SRT_EXT); 321 | if (fp.exists(fsSaveName)) { 322 | File srt = fp.open(fsSaveName); 323 | res = fsUse ? hfsStoreFile(srt) : ftpStoreFile(srt); 324 | srt.close(); 325 | } 326 | } 327 | if (!res) LOG_WRN("Failed to upload: %s", fsSaveName); 328 | #endif 329 | } else { 330 | // Upload a whole folder, file by file 331 | LOG_INF("Uploading folder: ", root.name()); 332 | strncpy(folderPath, root.name(), FILE_NAME_LEN - 1); 333 | res = fsUse ? true : ftpCreateFolder(root.name()); 334 | if (!res) { 335 | refreshVal = saveRefreshVal; 336 | return false; 337 | } 338 | File fh = root.openNextFile(); 339 | while (fh) { 340 | res = fsUse ? hfsStoreFile(fh) : ftpStoreFile(fh); 341 | if (!res) break; // abandon rest of files 342 | fh.close(); 343 | fh = root.openNextFile(); 344 | } 345 | if (fh) fh.close(); 346 | } 347 | refreshVal = saveRefreshVal; 348 | root.close(); 349 | fsUse ? remoteServerClose(hclient) : ftpDisconnect(); 350 | return res; 351 | } 352 | 353 | static void fileServerTask(void* parameter) { 354 | // process an FTP or HTTPS request 355 | #ifdef ISCAM 356 | doPlayback = false; // close any current playback 357 | #endif 358 | fsChunk = psramFound() ? (byte*)ps_malloc(CHUNKSIZE) : (byte*)malloc(CHUNKSIZE); 359 | if (strlen(storedPathName) >= 2) { 360 | File root = fp.open(storedPathName); 361 | if (!root) LOG_WRN("Failed to open: %s", storedPathName); 362 | else { 363 | bool res = uploadFolderOrFileFs(storedPathName); 364 | if (res && deleteAfter) deleteFolderOrFile(storedPathName); 365 | } 366 | } else LOG_VRB("Root or null is not allowed %s", storedPathName); 367 | uploadInProgress = false; 368 | free(fsChunk); 369 | fsHandle = NULL; 370 | vTaskDelete(NULL); 371 | } 372 | 373 | bool fsStartTransfer(const char* fileFolder) { 374 | // called from other functions to commence transfer of file or folder to file server 375 | setFolderName(fileFolder, storedPathName); 376 | if (!uploadInProgress) { 377 | uploadInProgress = true; 378 | if (fsHandle == NULL) xTaskCreate(&fileServerTask, "fileServerTask", FS_STACK_SIZE, NULL, FTP_PRI, &fsHandle); 379 | debugMemory("fsStartTransfer"); 380 | return true; 381 | } else LOG_WRN("Unable to transfer %s as another transfer in progress", storedPathName); 382 | return false; 383 | } 384 | 385 | void prepUpload() { 386 | LOG_INF("File uploads will use %s server", fsUse ? "HTTPS" : "FTP"); 387 | } 388 | #endif 389 | -------------------------------------------------------------------------------- /globals.h: -------------------------------------------------------------------------------- 1 | // Global generic declarations 2 | // 3 | // s60sc 2021, 2022 4 | 5 | #include "esp_arduino_version.h" 6 | 7 | #if ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3, 1, 1) 8 | #error Must be compiled with arduino-esp32 core v3.1.1 or higher 9 | #endif 10 | 11 | #pragma once 12 | 13 | //#define DEV_ONLY // leave commented out 14 | #ifdef DEV_ONLY 15 | // to compile with -Wall -Werror=all -Wextra 16 | #pragma GCC diagnostic error "-Wformat=2" 17 | #pragma GCC diagnostic ignored "-Wformat-y2k" 18 | #pragma GCC diagnostic ignored "-Wunused-function" 19 | #pragma GCC diagnostic ignored "-Wmissing-field-initializers" 20 | //#pragma GCC diagnostic ignored "-Wunused-variable" 21 | //#pragma GCC diagnostic ignored "-Wunused-but-set-variable" 22 | //#pragma GCC diagnostic ignored "-Wignored-qualifiers" 23 | //#pragma GCC diagnostic ignored "-Wclass-memaccess" 24 | #pragma GCC diagnostic ignored "-Wvolatile" 25 | #endif 26 | 27 | /******************** Libraries *******************/ 28 | 29 | #include "Arduino.h" 30 | #include 31 | #include "lwip/sockets.h" 32 | #include 33 | #include "ping/ping_sock.h" 34 | #include 35 | #include 36 | #if (!CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32S2) 37 | #include 38 | #endif 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | 49 | // ADC 50 | #define ADC_ATTEN ADC_11db 51 | #define ADC_SAMPLES 16 52 | #if CONFIG_IDF_TARGET_ESP32S3 53 | #define ADC_BITS 13 54 | #define MAX_ADC 8191 // maximum ADC value at given resolution 55 | #else 56 | #define ADC_BITS 12 57 | #define MAX_ADC 4095 // maximum ADC value at given resolution 58 | #endif 59 | #define CENTER_ADC (MAX_ADC / 2) 60 | 61 | // data folder defs 62 | #define DATA_DIR "/data" 63 | #define HTML_EXT ".htm" 64 | #define TEXT_EXT ".txt" 65 | #define JS_EXT ".js" 66 | #define CSS_EXT ".css" 67 | #define ICO_EXT ".ico" 68 | #define SVG_EXT ".svg" 69 | #define JPG_EXT ".jpg" 70 | #define CONFIG_FILE_PATH DATA_DIR "/configs" TEXT_EXT 71 | #define LOG_FILE_PATH DATA_DIR "/log" TEXT_EXT 72 | #define OTA_FILE_PATH DATA_DIR "/OTA" HTML_EXT 73 | #define COMMON_JS_PATH DATA_DIR "/common" JS_EXT 74 | #define WEBDAV "/webdav" 75 | #define GITHUB_HOST "raw.githubusercontent.com" 76 | 77 | #define FILLSTAR "****************************************************************" 78 | #define DELIM '~' 79 | #define ONEMEG (1024 * 1024) 80 | #define MAX_PWD_LEN 64 81 | #define MAX_HOST_LEN 32 82 | #define MAX_IP_LEN 16 83 | #define BOUNDARY_VAL "123456789000000000000987654321" 84 | #define SF_LEN 128 85 | #define WAV_HDR_LEN 44 86 | #define RAM_LOG_LEN (1024 * 7) // size of system message log in bytes stored in slow RTC ram (max 8KB - vars) 87 | #define MIN_STACK_FREE 512 88 | #define STARTUP_FAIL "Startup Failure: " 89 | #define MAX_PAYLOAD_LEN 672 // set bigger than any incoming websocket payload (20ms audio) 90 | #define NULL_TEMP -127 91 | #define OneMHz 1000000 92 | #define USECS 1000000 93 | #define MAGIC_NUM 987654321 94 | #define MAX_FAIL 5 95 | 96 | // global mandatory app specific functions, in appSpecific.cpp 97 | bool appDataFiles(); 98 | esp_err_t appSpecificSustainHandler(httpd_req_t* req); 99 | esp_err_t appSpecificWebHandler(httpd_req_t *req, const char* variable, const char* value); 100 | void appSpecificWsBinHandler(uint8_t* wsMsg, size_t wsMsgLen); 101 | void appSpecificWsHandler(const char* wsMsg); 102 | void appSpecificTelegramTask(void* p); 103 | void buildAppJsonString(bool filter); 104 | bool updateAppStatus(const char* variable, const char* value, bool fromUser = true); 105 | 106 | // global general utility functions in utils.cpp / utilsFS.cpp / peripherals.cpp 107 | void buildJsonString(uint8_t filter); 108 | bool calcProgress(int progressVal, int totalVal, int percentReport, uint8_t &pcProgress); 109 | bool changeExtension(char* fileName, const char* newExt); 110 | bool checkAlarm(); 111 | bool checkAuth(httpd_req_t* req); 112 | bool checkDataFiles(); 113 | bool checkFreeStorage(); 114 | bool checkI2Cdevice(const char* devName); 115 | void checkMemory(const char* source = ""); 116 | uint32_t checkStackUse(TaskHandle_t thisTask, int taskIdx); 117 | void debugMemory(const char* caller); 118 | void dateFormat(char* inBuff, size_t inBuffLen, bool isFolder); 119 | void deleteFolderOrFile(const char* deleteThis); 120 | void devSetup(); 121 | void doAppPing(); 122 | void doRestart(const char* restartStr); 123 | esp_err_t downloadFile(File& df, httpd_req_t* req); 124 | void emailAlert(const char* _subject, const char* _message); 125 | const char* encode64(const char* inp); 126 | const uint8_t* encode64chunk(const uint8_t* inp, int rem); 127 | const char* espErrMsg(esp_err_t errCode); 128 | void externalAlert(const char* subject, const char* message); 129 | bool externalPeripheral(byte pinNum, uint32_t outputData = 0); 130 | esp_err_t extractHeaderVal(httpd_req_t *req, const char* variable, char* value); 131 | esp_err_t extractQueryKeyVal(httpd_req_t *req, char* variable, char* value); 132 | esp_err_t fileHandler(httpd_req_t* req, bool download = false); 133 | void flush_log(bool andClose = false); 134 | char* fmtSize (uint64_t sizeVal); 135 | void forceCrash(); 136 | void formatElapsedTime(char* timeStr, uint32_t timeVal, bool noDays = false); 137 | void formatHex(const char* inData, size_t inLen); 138 | bool formatSDcard(); 139 | bool fsStartTransfer(const char* fileFolder); 140 | const char* getEncType(int ssidIndex); 141 | void getExtIP(); 142 | time_t getEpoch(); 143 | size_t getFreeStorage(); 144 | uint32_t getFrequency(); 145 | bool getLocalNTP(); 146 | float getNTCcelsius(uint16_t resistance, float oldTemp); 147 | void goToSleep(int wakeupPin, bool deepSleep); 148 | bool handleWebDav(httpd_req_t* rreq); 149 | void initStatus(int cfgGroup, int delayVal); 150 | void killSocket(int skt = -99); 151 | void listBuff(const uint8_t* b, size_t len); 152 | bool listDir(const char* fname, char* jsonBuff, size_t jsonBuffLen, const char* extension); 153 | bool loadConfig(); 154 | void logLine(); 155 | void logPrint(const char *fmtStr, ...); 156 | void logSetup(); 157 | void OTAprereq(); 158 | bool parseJson(int rxSize); 159 | bool prepFreq(int maxFreq, int sampleInterval); 160 | bool prepI2C(); 161 | void prepPeripherals(); 162 | void prepSMTP(); 163 | bool prepTelegram(); 164 | void prepTemperature(); 165 | void prepUpload(); 166 | void reloadConfigs(); 167 | float readInternalTemp(); 168 | float readTemperature(bool isCelsius, bool onlyDS18 = false); 169 | float readVoltage(); 170 | void remote_log_init(); 171 | void remoteServerClose(NetworkClientSecure& sclient); 172 | bool remoteServerConnect(NetworkClientSecure& sclient, const char* serverName, uint16_t serverPort, const char* serverCert, uint8_t connIdx); 173 | void remoteServerReset(); 174 | void removeChar(char* s, char c); 175 | void replaceChar(char* s, char c, char r); 176 | void reset_log(); 177 | void resetWatchDog(); 178 | bool retrieveConfigVal(const char* variable, char* value); 179 | void runTaskStats(); 180 | esp_err_t sendChunks(File df, httpd_req_t *req, bool endChunking = true); 181 | void setFolderName(const char* fname, char* fileName); 182 | void setPeripheralResponse(const byte pinNum, const uint32_t responseData); 183 | void setupADC(); 184 | void showProgress(const char* marker = "."); 185 | void showHttpHeaders(httpd_req_t *req); 186 | uint16_t smoothAnalog(int analogPin, int samples = ADC_SAMPLES); 187 | float smoothSensor(float latestVal, float smoothedVal, float alpha); 188 | void startOTAtask(); 189 | void startSecTimer(bool startTimer); 190 | bool startStorage(); 191 | void startWebServer(); 192 | bool startWifi(bool firstcall = true); 193 | void stopPing(); 194 | void syncToBrowser(uint32_t browserUTC); 195 | bool updateConfigVect(const char* variable, const char* value); 196 | void updateStatus(const char* variable, const char* _value, bool fromUser = true); 197 | esp_err_t uploadHandler(httpd_req_t *req); 198 | void urlDecode(char* inVal); 199 | bool urlEncode(const char* inVal, char* encoded, size_t maxSize); 200 | uint32_t usePeripheral(const byte pinNum, const uint32_t receivedData); 201 | esp_sleep_wakeup_cause_t wakeupResetReason(); 202 | void wsAsyncSendBinary(uint8_t* data, size_t len); 203 | bool wsAsyncSendText(const char* wsData); 204 | // mqtt.cpp 205 | void startMqttClient(); 206 | void stopMqttClient(); 207 | void mqttPublish(const char* payload); 208 | void mqttPublishPath(const char* suffix, const char* payload, const char *device = "sensor"); 209 | // telegram.cpp 210 | bool getTgramUpdate(char* response); 211 | bool sendTgramMessage(const char* info, const char* item, const char* parseMode); 212 | bool sendTgramPhoto(uint8_t* photoData, size_t photoSize, const char* caption); 213 | bool sendTgramFile(const char* fileName, const char* contentType, const char* caption); 214 | void tgramAlert(const char* subject, const char* message); 215 | // externalHeartbeat.cpp 216 | void sendExternalHeartbeat(); 217 | 218 | /******************** Global utility declarations *******************/ 219 | 220 | extern char AP_SSID[]; 221 | extern char AP_Pass[]; 222 | extern char AP_ip[]; 223 | extern char AP_sn[]; 224 | extern char AP_gw[]; 225 | 226 | extern char hostName[]; //Host name for ddns 227 | extern char ST_SSID[]; //Router ssid 228 | extern char ST_Pass[]; //Router passd 229 | extern bool useHttps; 230 | extern bool useSecure; 231 | extern bool useFtps; 232 | 233 | extern char ST_ip[]; //Leave blank for dhcp 234 | extern char ST_sn[]; 235 | extern char ST_gw[]; 236 | extern char ST_ns1[]; 237 | extern char ST_ns2[]; 238 | extern char extIP[]; 239 | 240 | extern char Auth_Name[]; 241 | extern char Auth_Pass[]; 242 | 243 | extern int responseTimeoutSecs; // how long to wait for remote server in secs 244 | extern bool allowAP; // set to true to allow AP to startup if cannot reconnect to STA (router) 245 | extern uint32_t wifiTimeoutSecs; // how often to check wifi status 246 | extern uint8_t percentLoaded; 247 | extern int refreshVal; 248 | extern bool dataFilesChecked; 249 | extern char ipExtAddr[]; 250 | extern bool doGetExtIP; 251 | extern bool usePing; // set to false if problems related to this issue occur: https://github.com/s60sc/ESP32-CAM_MJPEG2SD/issues/221 252 | extern bool wsLog; 253 | extern uint16_t sustainId; 254 | extern bool heartBeatDone; 255 | extern TaskHandle_t heartBeatHandle; 256 | 257 | // remote file server 258 | extern char fsServer[]; 259 | extern char ftpUser[]; 260 | extern uint16_t fsPort; 261 | extern char FS_Pass[]; 262 | extern char fsWd[]; 263 | extern bool autoUpload; 264 | extern bool deleteAfter; 265 | extern bool fsUse; 266 | extern char inFileName[]; 267 | 268 | // SMTP server 269 | extern char smtp_login[]; 270 | extern char SMTP_Pass[]; 271 | extern char smtp_email[]; 272 | extern char smtp_server[]; 273 | extern uint16_t smtp_port; 274 | extern bool smtpUse; // whether or not to use smtp 275 | extern int emailCount; 276 | 277 | // Mqtt broker 278 | extern bool mqtt_active; 279 | extern char mqtt_broker[]; 280 | extern char mqtt_port[]; 281 | extern char mqtt_user[]; 282 | extern char mqtt_user_Pass[]; 283 | extern char mqtt_topic_prefix[]; 284 | 285 | // control sending alerts 286 | extern size_t alertBufferSize; 287 | extern byte* alertBuffer; 288 | 289 | // Telegram 290 | extern bool tgramUse; 291 | extern char tgramToken[]; 292 | extern char tgramChatId[]; 293 | extern char tgramHdr[]; 294 | 295 | // certificates 296 | extern const char* git_rootCACertificate; 297 | extern const char* ftps_rootCACertificate; 298 | extern const char* smtp_rootCACertificate; 299 | extern const char* mqtt_rootCACertificate; 300 | extern const char* telegram_rootCACertificate; 301 | extern const char* hfs_rootCACertificate; 302 | extern const char* prvtkey_pem; // app https server private key 303 | extern const char* cacert_pem; // app https server public certificate 304 | 305 | // app status 306 | extern char timezone[]; 307 | extern char ntpServer[]; 308 | extern uint8_t alarmHour; 309 | extern char* jsonBuff; 310 | extern bool dbgVerbose; 311 | extern bool sdLog; 312 | extern char alertMsg[]; 313 | extern int logType; 314 | extern char messageLog[]; 315 | extern uint16_t mlogEnd; 316 | extern bool timeSynchronized; 317 | extern bool monitorOpen; 318 | extern const uint8_t setupPage_html_gz[]; 319 | extern const size_t setupPage_html_gz_len; 320 | extern const char* otaPage_html; 321 | extern const char* failPageS_html; 322 | extern const char* failPageE_html; 323 | extern char startupFailure[]; 324 | extern time_t currEpoch; 325 | extern bool RCactive; 326 | 327 | extern UBaseType_t uxHighWaterMarkArr[]; 328 | 329 | // SD storage 330 | extern int sdMinCardFreeSpace; // Minimum amount of card free Megabytes before freeSpaceMode action is enabled 331 | extern int sdFreeSpaceMode; // 0 - No Check, 1 - Delete oldest dir, 2 - Upload to ftp and then delete folder on SD 332 | extern bool formatIfMountFailed ; // Auto format the file system if mount failed. Set to false to not auto format. 333 | 334 | // I2C pins 335 | extern int I2Csda; 336 | extern int I2Cscl; 337 | 338 | #define HTTP_METHOD_STRING(method) \ 339 | (method == HTTP_DELETE) ? "DELETE" : \ 340 | (method == HTTP_GET) ? "GET" : \ 341 | (method == HTTP_HEAD) ? "HEAD" : \ 342 | (method == HTTP_POST) ? "POST" : \ 343 | (method == HTTP_PUT) ? "PUT" : \ 344 | (method == HTTP_CONNECT) ? "CONNECT" : \ 345 | (method == HTTP_OPTIONS) ? "OPTIONS" : \ 346 | (method == HTTP_TRACE) ? "TRACE" : \ 347 | (method == HTTP_COPY) ? "COPY" : \ 348 | (method == HTTP_LOCK) ? "LOCK" : \ 349 | (method == HTTP_MKCOL) ? "MKCOL" : \ 350 | (method == HTTP_MOVE) ? "MOVE" : \ 351 | (method == HTTP_PROPFIND) ? "PROPFIND" : \ 352 | (method == HTTP_PROPPATCH) ? "PROPPATCH" : \ 353 | (method == HTTP_SEARCH) ? "SEARCH" : \ 354 | (method == HTTP_UNLOCK) ? "UNLOCK" : \ 355 | (method == HTTP_BIND) ? "BIND" : \ 356 | (method == HTTP_REBIND) ? "REBIND" : \ 357 | (method == HTTP_UNBIND) ? "UNBIND" : \ 358 | (method == HTTP_ACL) ? "ACL" : \ 359 | (method == HTTP_REPORT) ? "REPORT" : \ 360 | (method == HTTP_MKACTIVITY) ? "MKACTIVITY" : \ 361 | (method == HTTP_CHECKOUT) ? "CHECKOUT" : \ 362 | (method == HTTP_MERGE) ? "MERGE" : \ 363 | (method == HTTP_MSEARCH) ? "MSEARCH" : \ 364 | (method == HTTP_NOTIFY) ? "NOTIFY" : \ 365 | (method == HTTP_SUBSCRIBE) ? "SUBSCRIBE" : \ 366 | (method == HTTP_UNSUBSCRIBE) ? "UNSUBSCRIBE" : \ 367 | (method == HTTP_PATCH) ? "PATCH" : \ 368 | (method == HTTP_PURGE) ? "PURGE" : \ 369 | (method == HTTP_MKCALENDAR) ? "MKCALENDAR" : \ 370 | (method == HTTP_LINK) ? "LINK" : \ 371 | (method == HTTP_UNLINK) ? "UNLINK" : \ 372 | "UNKNOWN" 373 | 374 | enum RemoteFail {SETASSIST, GETEXTIP, TGRAMCONN, FSFTP, EMAILCONN, EXTERNALHB, BLOCKLIST, REMFAILCNT}; // REMFAILCNT always last 375 | 376 | /*********************** Log formatting ************************/ 377 | 378 | //#define USE_LOG_COLORS // uncomment to colorise log messages (eg if using idf.py, but not arduino) 379 | #ifdef USE_LOG_COLORS 380 | // ANSI color codes 381 | #define LOG_COLOR_ERR "\033[0;31m" // red 382 | #define LOG_COLOR_WRN "\033[0;33m" // yellow 383 | #define LOG_COLOR_VRB "\033[0;36m" // cyan 384 | #define LOG_COLOR_DBG "\033[0;34m" // blue 385 | #define LOG_NO_COLOR 386 | #else 387 | #define LOG_COLOR_ERR 388 | #define LOG_COLOR_WRN 389 | #define LOG_COLOR_VRB 390 | #define LOG_COLOR_DBG 391 | #define LOG_NO_COLOR 392 | #endif 393 | 394 | #define INF_FORMAT(format) "[%s %s] " format "\n", esp_log_system_timestamp(), __FUNCTION__ 395 | #define LOG_INF(format, ...) logPrint(INF_FORMAT(format), ##__VA_ARGS__) 396 | #define LOG_ALT(format, ...) logPrint(INF_FORMAT(format "~"), ##__VA_ARGS__) 397 | #define WRN_FORMAT(format) LOG_COLOR_WRN "[%s WARN %s] " format LOG_NO_COLOR "\n", esp_log_system_timestamp(), __FUNCTION__ 398 | #define LOG_WRN(format, ...) logPrint(WRN_FORMAT(format "~"), ##__VA_ARGS__) 399 | #define ERR_FORMAT(format) LOG_COLOR_ERR "[%s ERROR @ %s:%u] " format LOG_NO_COLOR "\n", esp_log_system_timestamp(), pathToFileName(__FILE__), __LINE__ 400 | #define LOG_ERR(format, ...) logPrint(ERR_FORMAT(format "~"), ##__VA_ARGS__) 401 | #define VRB_FORMAT(format) LOG_COLOR_VRB "[%s VERBOSE @ %s:%u] " format LOG_NO_COLOR "\n", esp_log_system_timestamp(), pathToFileName(__FILE__), __LINE__ 402 | #define LOG_VRB(format, ...) if (dbgVerbose) logPrint(VRB_FORMAT(format), ##__VA_ARGS__) 403 | #define DBG_FORMAT(format) LOG_COLOR_DBG "[%s ### DEBUG @ %s:%u] " format LOG_NO_COLOR "\n", esp_log_system_timestamp(), pathToFileName(__FILE__), __LINE__ 404 | #define LOG_DBG(format, ...) do { logPrint(DBG_FORMAT(format), ##__VA_ARGS__); delay(FLUSH_DELAY); } while (0) 405 | #define LOG_PRT(buff, bufflen) log_print_buf((const uint8_t*)buff, bufflen) 406 | -------------------------------------------------------------------------------- /mcpwm.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Support for MCPWM, eg for H-bridge motor controller 3 | // 4 | // s60sc 2024 5 | // 6 | 7 | /* 8 | MCPWM peripheral has 2 units, each unit can support: 9 | - 3 pairs of PWM outputs (6 pins) 10 | - 3 fault input pins to detect faults like overcurrent, overvoltage, etc. 11 | - 3 sync input pins to synchronize output signals 12 | - 3 input pins to gather feedback from controlled motors, using e.g. hall sensors 13 | 14 | MX1508 DC Motor Driver with PWM Control 15 | - 4 PWM gpio inputs, 2 per motor (forward & reverse) 16 | - Two H-channel drive circuits for 2 DC motors 17 | - 1.5A (peak 2A) 18 | - 2-10V DC input, 1.8-7V Dc output 19 | - Outputs are OUT1 - OUT4 corresponding to IN1 to IN4 20 | - IN1 / OUT1 A1 21 | - IN2 / OUT2 B1 22 | - IN3 / OUT3 A2 23 | - IN4 / OUT4 B2 24 | */ 25 | 26 | #include "appGlobals.h" 27 | 28 | #if INCLUDE_MCPWM 29 | #if !INCLUDE_PERIPH 30 | #error "Need INCLUDE_PERIPH true" 31 | #endif 32 | 33 | // Includes code from github.com/espressif/idf-extra-components/blob/master/bdc_motor 34 | // modified to compile with c++: 35 | // - github.com/espressif/idf-extra-components/blob/master/bdc_motor/include/bdc_motor.h 36 | // - github.com/espressif/idf-extra-components/blob/master/bdc_motor/interface/bdc_motor_interface.h 37 | // - github.com/espressif/idf-extra-components/blob/master/bdc_motor/src/bdc_motor.c 38 | // - github.com/espressif/idf-extra-components/tree/master/bdc_motor/src/bdc_motor_mcpwm_impl.c 39 | 40 | /* 41 | * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD 42 | * 43 | * SPDX-License-Identifier: Apache-2.0 44 | */ 45 | 46 | #include 47 | #include 48 | #include 49 | #include "esp_log.h" 50 | #include "esp_check.h" 51 | #include "driver/mcpwm_prelude.h" 52 | 53 | /** 54 | * @brief BDC Motor Configuration 55 | */ 56 | typedef struct { 57 | uint32_t pwma_gpio_num; /*!< BDC Motor PWM A gpio number */ 58 | uint32_t pwmb_gpio_num; /*!< BDC Motor PWM B gpio number */ 59 | uint32_t pwm_freq_hz; /*!< PWM frequency, in Hz */ 60 | } bdc_motor_config_t; 61 | 62 | /** 63 | * @brief BDC Motor MCPWM specific configuration 64 | */ 65 | typedef struct { 66 | int group_id; /*!< MCPWM group number */ 67 | uint32_t resolution_hz; /*!< MCPWM timer resolution */ 68 | } bdc_motor_mcpwm_config_t; 69 | 70 | 71 | /** 72 | * @brief Brushed DC Motor handle 73 | */ 74 | struct bdc_motor_t { 75 | esp_err_t (*enable)(bdc_motor_t *motor); 76 | esp_err_t (*disable)(bdc_motor_t *motor); 77 | esp_err_t (*set_speed)(bdc_motor_t *motor, uint32_t speed); 78 | esp_err_t (*forward)(bdc_motor_t *motor); 79 | esp_err_t (*reverse)(bdc_motor_t *motor); 80 | esp_err_t (*coast)(bdc_motor_t *motor); 81 | esp_err_t (*brake)(bdc_motor_t *motor); 82 | esp_err_t (*del)(bdc_motor_t *motor); 83 | }; 84 | 85 | typedef struct { 86 | bdc_motor_t base; 87 | mcpwm_timer_handle_t timer; 88 | mcpwm_oper_handle_t oper; 89 | mcpwm_cmpr_handle_t cmpa; 90 | mcpwm_cmpr_handle_t cmpb; 91 | mcpwm_gen_handle_t gena; 92 | mcpwm_gen_handle_t genb; 93 | } bdc_motor_mcpwm_obj; 94 | 95 | typedef struct bdc_motor_t *bdc_motor_handle_t; 96 | 97 | static const char *TAG = "bdc_motor"; 98 | 99 | static esp_err_t bdc_motor_mcpwm_set_speed(bdc_motor_t *motor, uint32_t speed) 100 | { 101 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 102 | ESP_RETURN_ON_ERROR(mcpwm_comparator_set_compare_value(mcpwm_motor->cmpa, speed), TAG, "set compare value failed"); 103 | ESP_RETURN_ON_ERROR(mcpwm_comparator_set_compare_value(mcpwm_motor->cmpb, speed), TAG, "set compare value failed"); 104 | return ESP_OK; 105 | } 106 | 107 | static esp_err_t bdc_motor_mcpwm_enable(bdc_motor_t *motor) 108 | { 109 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 110 | ESP_RETURN_ON_ERROR(mcpwm_timer_enable(mcpwm_motor->timer), TAG, "enable timer failed"); 111 | ESP_RETURN_ON_ERROR(mcpwm_timer_start_stop(mcpwm_motor->timer, MCPWM_TIMER_START_NO_STOP), TAG, "start timer failed"); 112 | return ESP_OK; 113 | } 114 | 115 | static esp_err_t bdc_motor_mcpwm_disable(bdc_motor_t *motor) 116 | { 117 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 118 | ESP_RETURN_ON_ERROR(mcpwm_timer_start_stop(mcpwm_motor->timer, MCPWM_TIMER_STOP_EMPTY), TAG, "stop timer failed"); 119 | ESP_RETURN_ON_ERROR(mcpwm_timer_disable(mcpwm_motor->timer), TAG, "disable timer failed"); 120 | return ESP_OK; 121 | } 122 | 123 | static esp_err_t bdc_motor_mcpwm_forward(bdc_motor_t *motor) 124 | { 125 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 126 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, -1, true), TAG, "disable force level for gena failed"); 127 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, 0, true), TAG, "set force level for genb failed"); 128 | return ESP_OK; 129 | } 130 | 131 | static esp_err_t bdc_motor_mcpwm_reverse(bdc_motor_t *motor) 132 | { 133 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 134 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, -1, true), TAG, "disable force level for genb failed"); 135 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, 0, true), TAG, "set force level for gena failed"); 136 | return ESP_OK; 137 | } 138 | 139 | static esp_err_t bdc_motor_mcpwm_coast(bdc_motor_t *motor) 140 | { 141 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 142 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, 0, true), TAG, "set force level for gena failed"); 143 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, 0, true), TAG, "set force level for genb failed"); 144 | return ESP_OK; 145 | } 146 | 147 | static esp_err_t bdc_motor_mcpwm_brake(bdc_motor_t *motor) 148 | { 149 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 150 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, 1, true), TAG, "set force level for gena failed"); 151 | ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, 1, true), TAG, "set force level for genb failed"); 152 | return ESP_OK; 153 | } 154 | 155 | static esp_err_t bdc_motor_mcpwm_del(bdc_motor_t *motor) 156 | { 157 | bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); 158 | mcpwm_del_generator(mcpwm_motor->gena); 159 | mcpwm_del_generator(mcpwm_motor->genb); 160 | mcpwm_del_comparator(mcpwm_motor->cmpa); 161 | mcpwm_del_comparator(mcpwm_motor->cmpb); 162 | mcpwm_del_operator(mcpwm_motor->oper); 163 | mcpwm_del_timer(mcpwm_motor->timer); 164 | free(mcpwm_motor); 165 | return ESP_OK; 166 | } 167 | 168 | static esp_err_t bdc_motor_new_mcpwm_device(const bdc_motor_config_t *motor_config, const bdc_motor_mcpwm_config_t *mcpwm_config, bdc_motor_handle_t *ret_motor) 169 | { 170 | bdc_motor_mcpwm_obj *mcpwm_motor = NULL; 171 | esp_err_t ret = ESP_OK; 172 | 173 | // mcpwm timer 174 | mcpwm_timer_config_t timer_config = { 175 | .group_id = mcpwm_config->group_id, 176 | .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, 177 | .resolution_hz = mcpwm_config->resolution_hz, 178 | .count_mode = MCPWM_TIMER_COUNT_MODE_UP, 179 | .period_ticks = mcpwm_config->resolution_hz / motor_config->pwm_freq_hz, 180 | }; 181 | 182 | mcpwm_operator_config_t operator_config = { 183 | .group_id = mcpwm_config->group_id, 184 | }; 185 | 186 | mcpwm_comparator_config_t comparator_config = { 187 | .flags = { 188 | .update_cmp_on_tez = true, 189 | } 190 | }; 191 | 192 | mcpwm_generator_config_t generator_config = { 193 | .gen_gpio_num = (int)motor_config->pwma_gpio_num, 194 | }; 195 | 196 | ESP_GOTO_ON_FALSE(motor_config && mcpwm_config && ret_motor, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); 197 | 198 | mcpwm_motor = (bdc_motor_mcpwm_obj*)calloc(1, sizeof(bdc_motor_mcpwm_obj)); 199 | ESP_GOTO_ON_FALSE(mcpwm_motor, ESP_ERR_NO_MEM, err, TAG, "no mem for rmt motor"); 200 | 201 | ESP_GOTO_ON_ERROR(mcpwm_new_timer(&timer_config, &mcpwm_motor->timer), err, TAG, "create MCPWM timer failed"); 202 | 203 | ESP_GOTO_ON_ERROR(mcpwm_new_operator(&operator_config, &mcpwm_motor->oper), err, TAG, "create MCPWM operator failed"); 204 | 205 | ESP_GOTO_ON_ERROR(mcpwm_operator_connect_timer(mcpwm_motor->oper, mcpwm_motor->timer), err, TAG, "connect timer and operator failed"); 206 | 207 | ESP_GOTO_ON_ERROR(mcpwm_new_comparator(mcpwm_motor->oper, &comparator_config, &mcpwm_motor->cmpa), err, TAG, "create comparator failed"); 208 | ESP_GOTO_ON_ERROR(mcpwm_new_comparator(mcpwm_motor->oper, &comparator_config, &mcpwm_motor->cmpb), err, TAG, "create comparator failed"); 209 | 210 | // set the initial compare value for both comparators 211 | mcpwm_comparator_set_compare_value(mcpwm_motor->cmpa, 0); 212 | mcpwm_comparator_set_compare_value(mcpwm_motor->cmpb, 0); 213 | 214 | ESP_GOTO_ON_ERROR(mcpwm_new_generator(mcpwm_motor->oper, &generator_config, &mcpwm_motor->gena), err, TAG, "create generator failed"); 215 | generator_config.gen_gpio_num = motor_config->pwmb_gpio_num; 216 | ESP_GOTO_ON_ERROR(mcpwm_new_generator(mcpwm_motor->oper, &generator_config, &mcpwm_motor->genb), err, TAG, "create generator failed"); 217 | 218 | mcpwm_generator_set_actions_on_timer_event(mcpwm_motor->gena, 219 | MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), 220 | MCPWM_GEN_TIMER_EVENT_ACTION_END()); 221 | mcpwm_generator_set_actions_on_compare_event(mcpwm_motor->gena, 222 | MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, mcpwm_motor->cmpa, MCPWM_GEN_ACTION_LOW), 223 | MCPWM_GEN_COMPARE_EVENT_ACTION_END()); 224 | mcpwm_generator_set_actions_on_timer_event(mcpwm_motor->genb, 225 | MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), 226 | MCPWM_GEN_TIMER_EVENT_ACTION_END()); 227 | mcpwm_generator_set_actions_on_compare_event(mcpwm_motor->genb, 228 | MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, mcpwm_motor->cmpb, MCPWM_GEN_ACTION_LOW), 229 | MCPWM_GEN_COMPARE_EVENT_ACTION_END()); 230 | 231 | mcpwm_motor->base.enable = bdc_motor_mcpwm_enable; 232 | mcpwm_motor->base.disable = bdc_motor_mcpwm_disable; 233 | mcpwm_motor->base.forward = bdc_motor_mcpwm_forward; 234 | mcpwm_motor->base.reverse = bdc_motor_mcpwm_reverse; 235 | mcpwm_motor->base.coast = bdc_motor_mcpwm_coast; 236 | mcpwm_motor->base.brake = bdc_motor_mcpwm_brake; 237 | mcpwm_motor->base.set_speed = bdc_motor_mcpwm_set_speed; 238 | mcpwm_motor->base.del = bdc_motor_mcpwm_del; 239 | *ret_motor = &mcpwm_motor->base; 240 | return ESP_OK; 241 | 242 | err: 243 | if (mcpwm_motor) { 244 | if (mcpwm_motor->gena) { 245 | mcpwm_del_generator(mcpwm_motor->gena); 246 | } 247 | if (mcpwm_motor->genb) { 248 | mcpwm_del_generator(mcpwm_motor->genb); 249 | } 250 | if (mcpwm_motor->cmpa) { 251 | mcpwm_del_comparator(mcpwm_motor->cmpa); 252 | } 253 | if (mcpwm_motor->cmpb) { 254 | mcpwm_del_comparator(mcpwm_motor->cmpb); 255 | } 256 | if (mcpwm_motor->oper) { 257 | mcpwm_del_operator(mcpwm_motor->oper); 258 | } 259 | if (mcpwm_motor->timer) { 260 | mcpwm_del_timer(mcpwm_motor->timer); 261 | } 262 | free(mcpwm_motor); 263 | } 264 | return ret; 265 | } 266 | 267 | static esp_err_t bdc_motor_enable(bdc_motor_handle_t motor) 268 | { 269 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 270 | return motor->enable(motor); 271 | } 272 | 273 | static esp_err_t bdc_motor_disable(bdc_motor_handle_t motor) 274 | { 275 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 276 | return motor->disable(motor); 277 | } 278 | 279 | static esp_err_t bdc_motor_set_speed(bdc_motor_handle_t motor, uint32_t speed) 280 | { 281 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 282 | return motor->set_speed(motor, speed); 283 | } 284 | 285 | static esp_err_t bdc_motor_forward(bdc_motor_handle_t motor) 286 | { 287 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 288 | return motor->forward(motor); 289 | } 290 | 291 | static esp_err_t bdc_motor_reverse(bdc_motor_handle_t motor) 292 | { 293 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 294 | return motor->reverse(motor); 295 | } 296 | 297 | static esp_err_t bdc_motor_coast(bdc_motor_handle_t motor) 298 | { 299 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 300 | return motor->coast(motor); 301 | } 302 | 303 | esp_err_t bdc_motor_brake(bdc_motor_handle_t motor) 304 | { 305 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 306 | return motor->brake(motor); 307 | } 308 | 309 | static esp_err_t bdc_motor_del(bdc_motor_handle_t motor) 310 | { 311 | ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); 312 | return motor->del(motor); 313 | } 314 | 315 | /************************* custom code s60sc *************************/ 316 | 317 | #define MCPWM_TIMER_HZ 100000 318 | static bdc_motor_handle_t BDCmotor[6] = {NULL, NULL, NULL, NULL, NULL, NULL}; // max 6 motors 319 | bool useBDC = false; 320 | int motorRevPin; 321 | int motorFwdPin; 322 | int motorRevPinR; 323 | int motorFwdPinR; 324 | int pwmFreq = 50; 325 | bool trackSteer = false; 326 | 327 | static bool prepBDCmotor(int groupId, int motorId, int pwmAgpio, int pwmBgpio) { 328 | bdc_motor_config_t BDCmotorConfig = { 329 | .pwma_gpio_num = (uint32_t)pwmAgpio, // forward pin 330 | .pwmb_gpio_num = (uint32_t)pwmBgpio, // reverse pin 331 | .pwm_freq_hz = (uint32_t)pwmFreq, 332 | }; 333 | bdc_motor_mcpwm_config_t BDCmcpwmConfig = { 334 | .group_id = groupId, // MCPWM peripheral number (0, 1) 335 | .resolution_hz = MCPWM_TIMER_HZ, 336 | }; 337 | esp_err_t res = bdc_motor_new_mcpwm_device(&BDCmotorConfig, &BDCmcpwmConfig, &BDCmotor[motorId]); 338 | if (res == ESP_OK) res = bdc_motor_enable(BDCmotor[motorId]); 339 | if (res == ESP_OK) LOG_INF("Initialising MCPWM unit %d, motor %d, using pins %d, %d", groupId, motorId, pwmAgpio, pwmBgpio); 340 | else LOG_ERR("%s", espErrMsg(res)); 341 | return res == ESP_OK ? true : false; 342 | } 343 | 344 | static void motorDirection(uint32_t dutyTicks, int motorId, bool goFwd) { 345 | if (dutyTicks > 0) { 346 | // set direction 347 | goFwd ? bdc_motor_forward(BDCmotor[motorId]) : bdc_motor_reverse(BDCmotor[motorId]); 348 | } 349 | // set speed 350 | bdc_motor_set_speed(BDCmotor[motorId], dutyTicks); 351 | } 352 | 353 | void motorSpeed(int speedVal, bool leftMotor) { 354 | // speedVal is signed duty cycle, convert to unsigned uint32_t duty ticks 355 | if (abs(speedVal) < minDutyCycle) speedVal = 0; 356 | uint32_t dutyTicks = abs(speedVal) * MCPWM_TIMER_HZ / pwmFreq / 100; 357 | if (leftMotor) { 358 | // left motor steering or all motor direction 359 | if (motorRevPin > 0 && speedVal < 0) motorDirection(dutyTicks, 0, false); 360 | else if (motorFwdPin > 0) motorDirection(dutyTicks, 0, true); 361 | } else { 362 | // right motor steering 363 | if (motorRevPinR > 0 && speedVal < 0) motorDirection(dutyTicks, 1, false); 364 | else if (motorFwdPinR > 0) motorDirection(dutyTicks, 1, true); 365 | } 366 | } 367 | 368 | static inline int clampValue(int value, int maxValue) { 369 | // clamp value to the allowable range 370 | return value > maxValue ? maxValue : (value < -maxValue ? -maxValue : value); 371 | } 372 | 373 | void trackSteeering(int controlVal, bool steering) { 374 | // set left and right motor speed values depending on requested speed and request steering angle 375 | // steering = true ? controlVal = steer angle : controlVal = speed change 376 | static int driveSpeed = 0; // -ve for reverse 377 | static int steerAngle = 0; // -ve for left turn 378 | steering ? steerAngle = controlVal - servoCenter : driveSpeed = controlVal; 379 | int turnSpeed = (clampValue(steerAngle, maxSteerAngle) * maxTurnSpeed / 2) / maxSteerAngle; 380 | if (driveSpeed < 0) turnSpeed = 0 - turnSpeed; 381 | motorSpeed(clampValue(driveSpeed + turnSpeed, maxDutyCycle)); // left 382 | motorSpeed(clampValue(driveSpeed - turnSpeed, maxDutyCycle), false); //right 383 | } 384 | 385 | void prepMotors() { 386 | if (useBDC) { 387 | if (motorFwdPin > 0) { 388 | prepBDCmotor(0, 0, motorFwdPin, motorRevPin); 389 | if (trackSteer) prepBDCmotor(0, 1, motorFwdPinR, motorRevPinR); 390 | } else LOG_WRN("BDC motor pins not defined"); 391 | } 392 | } 393 | 394 | #endif 395 | -------------------------------------------------------------------------------- /motionDetect.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Detect movement in sequential images using background subtraction. 4 | 5 | Very small (98x98) bitmaps are used both to provide image smoothing to reduce spurious motion changes 6 | and to enable rapid processing 7 | Bitmaps can either be color or grayscale. Color requires triple memory 8 | of grayscale and more processing. 9 | 10 | The amount of change between images will depend on the frame rate. 11 | A faster frame rate will need a higher sensitivity 12 | 13 | When frame size is changed the OV2640 outputs a few glitched frames whilst it 14 | makes the transition. These could be interpreted as spurious motion. 15 | 16 | Machine Learning can be incorporated to further discriminate when motion detection 17 | has occurred by classsifying whether the object in the frame is of a particular 18 | type of interest, eg a human, animal, vehicle etc. 19 | 20 | s60sc 2020, 2023 21 | */ 22 | 23 | #include "appGlobals.h" 24 | 25 | #if INCLUDE_TINYML 26 | #include TINY_ML_LIB 27 | #endif 28 | 29 | using namespace std; 30 | 31 | #define RESIZE_DIM 96 // dimensions of resized motion bitmap 32 | #define RESIZE_DIM_SQ (RESIZE_DIM * RESIZE_DIM) // pixels in bitmap 33 | #define INACTIVE_COLOR 96 // color for inactive motion pixel 34 | #define JPEG_QUAL 80 // % quality for generated motion detect jpeg 35 | 36 | // motion recording parameters 37 | int detectMotionFrames = 5; // min sequence of changed frames to confirm motion 38 | int detectNightFrames = 10; // frames of sequential darkness to avoid spurious day / night switching 39 | // define region of interest, ie exclude top and bottom of image from movement detection if required 40 | // divide image into detectNumBands horizontal bands, define start and end bands of interest, 1 = top 41 | int detectNumBands = 10; 42 | int detectStartBand = 3; 43 | int detectEndBand = 8; // inclusive 44 | int detectChangeThreshold = 15; // min difference in pixel comparison to indicate a change 45 | uint8_t colorDepth; // set by depthColor config 46 | static size_t stride; 47 | bool mlUse = false; // whether to use ML for motion detection, requires INCLUDE_TINYML to be true 48 | float mlProbability = 0.8; // minimum probability (0.0 - 1.0) for positive classification 49 | 50 | uint8_t lightLevel; // Current ambient light level 51 | uint8_t nightSwitch = 20; // initial white level % for night/day switching 52 | float motionVal = 8.0; // initial motion sensitivity setting 53 | uint8_t* motionJpeg = NULL; 54 | size_t motionJpegLen = 0; 55 | static uint8_t* currBuff = NULL; 56 | 57 | /**********************************************************************************/ 58 | 59 | static bool jpg2rgb(const uint8_t* src, size_t src_len, uint8_t* out, jpg_scale_t scale); 60 | 61 | bool isNight(uint8_t nightSwitch) { 62 | // check if night time for suspending recording 63 | // or for switching relay if enabled 64 | static bool nightTime = false; 65 | static uint16_t nightCnt = 0; 66 | if (nightTime) { 67 | if (lightLevel > nightSwitch) { 68 | // light image 69 | nightCnt--; 70 | // signal day time after given sequence of light frames 71 | if (nightCnt == 0) { 72 | nightTime = false; 73 | LOG_INF("Day time"); 74 | } 75 | } 76 | } else { 77 | if (lightLevel < nightSwitch) { 78 | // dark image 79 | nightCnt++; 80 | // signal night time after given sequence of dark frames 81 | if (nightCnt > detectNightFrames) { 82 | nightTime = true; 83 | LOG_INF("Night time"); 84 | } 85 | } 86 | } 87 | return nightTime; 88 | } 89 | 90 | static void rescaleImage(const uint8_t* input, int inputWidth, int inputHeight, uint8_t* output, int outputWidth, int outputHeight) { 91 | // use bilinear interpolation to resize image 92 | float xRatio = (float)inputWidth / (float)outputWidth; 93 | float yRatio = (float)inputHeight / (float)outputHeight; 94 | 95 | for (int i = 0; i < outputHeight; ++i) { 96 | for (int j = 0; j < outputWidth; ++j) { 97 | int xL = (int)floor(xRatio * j); 98 | int yL = (int)floor(yRatio * i); 99 | int xH = (int)ceil(xRatio * j); 100 | int yH = (int)ceil(yRatio * i); 101 | float xWeight = xRatio * j - xL; 102 | float yWeight = yRatio * i - yL; 103 | for (int channel = 0; channel < colorDepth; ++channel) { 104 | uint8_t a = input[(yL * inputWidth + xL) * colorDepth + channel]; 105 | uint8_t b = input[(yL * inputWidth + xH) * colorDepth + channel]; 106 | uint8_t c = input[(yH * inputWidth + xL) * colorDepth + channel]; 107 | uint8_t d = input[(yH * inputWidth + xH) * colorDepth + channel]; 108 | 109 | float pixel = a * (1 - xWeight) * (1 - yWeight) + b * xWeight * (1 - yWeight) 110 | + c * yWeight * (1 - xWeight) + d * xWeight * yWeight; 111 | output[(i * outputWidth + j) * colorDepth + channel] = (uint8_t)pixel; 112 | } 113 | } 114 | } 115 | } 116 | 117 | #if INCLUDE_TINYML 118 | 119 | static int getImageData(size_t offset, size_t length, float *out_ptr) { 120 | // copy to features as grayscale or RGB 121 | size_t pixelPtr = offset * colorDepth; 122 | size_t out_ptr_idx = 0; 123 | while (out_ptr_idx < length) { 124 | out_ptr[out_ptr_idx++] = (colorDepth == RGB888_BYTES) 125 | ? (float)((currBuff[pixelPtr] << 16) + (currBuff[pixelPtr + 1] << 8) + currBuff[pixelPtr + 2]) 126 | : (float)((currBuff[pixelPtr] << 16) + (currBuff[pixelPtr] << 8) + currBuff[pixelPtr]); 127 | pixelPtr += colorDepth; 128 | } 129 | return 0; 130 | } 131 | 132 | static bool tinyMLclassify() { 133 | // convert input data to appropriate format 134 | bool out = false; 135 | uint32_t dTime = millis(); 136 | // reduce size of bitmap to that required by classifier and copy to features as grayscale or RGB 137 | if (RESIZE_DIM != EI_CLASSIFIER_INPUT_WIDTH) { 138 | uint8_t* tempBuff = (uint8_t*)ps_malloc(EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT * colorDepth); 139 | rescaleImage(currBuff, RESIZE_DIM, RESIZE_DIM, tempBuff, EI_CLASSIFIER_INPUT_WIDTH, EI_CLASSIFIER_INPUT_HEIGHT); 140 | memcpy(currBuff, tempBuff, EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT * colorDepth); 141 | free(tempBuff); 142 | } 143 | signal_t features_signal; 144 | features_signal.total_length = EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT; 145 | features_signal.get_data = &getImageData; 146 | 147 | // Run the classifier 148 | ei_impulse_result_t result = { 0 }; 149 | EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, false); 150 | if (res == EI_IMPULSE_OK) { 151 | if (result.classification[0].value > mlProbability) { 152 | out = true; // sufficient classification match, so keep motion detection 153 | if (dbgVerbose) { 154 | LOG_VRB("Prob: %0.2f, Timing: DSP %d ms, inference %d ms, anomaly %d ms", 155 | result.classification[0].value, result.timing.dsp, result.timing.classification, result.timing.anomaly); 156 | char outcome[200] = {0}; 157 | for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) 158 | sprintf(outcome + strlen(outcome), "%s: %.2f, ", ei_classifier_inferencing_categories[i], result.classification[i].value); 159 | LOG_VRB("Predictions - %s in %ums", outcome, millis() - dTime); 160 | } 161 | } 162 | } else LOG_WRN("Failed to run classifier (%d)", res); 163 | return out; 164 | } 165 | #endif 166 | 167 | bool checkMotion(camera_fb_t* fb, bool motionStatus, bool lightLevelOnly) { 168 | // check difference between current and previous image (subtract background) 169 | // convert image from JPEG to downscaled RGB888 or 8 bit grayscale bitmap 170 | if (fsizePtr > FRAMESIZE_SXGA) return false; 171 | uint32_t dTime = millis(); 172 | uint32_t lux = 0; 173 | static uint32_t motionCnt = 0; 174 | uint8_t* jpg_buf = NULL; 175 | // calculate parameters for sample size 176 | uint8_t scaling = frameData[fsizePtr].scaleFactor; 177 | uint16_t reducer = frameData[fsizePtr].sampleRate; 178 | uint8_t downsize = pow(2, scaling) * reducer; 179 | int sampleWidth = frameData[fsizePtr].frameWidth / downsize; 180 | int sampleHeight = frameData[fsizePtr].frameHeight / downsize; 181 | stride = (colorDepth == RGB888_BYTES) ? GRAYSCALE_BYTES : RGB888_BYTES; // stride is inverse of colorDepth 182 | 183 | static uint8_t* rgb_buf = (uint8_t*)ps_malloc(sampleWidth * sampleHeight * RGB888_BYTES); 184 | if (!jpg2rgb((uint8_t*)fb->buf, fb->len, rgb_buf, (jpg_scale_t)scaling)) return motionStatus; 185 | LOG_VRB("JPEG to rescaled %s bitmap conversion %u bytes in %lums", colorDepth == RGB888_BYTES ? "color" : "grayscale", sampleWidth * sampleHeight * colorDepth, millis() - dTime); 186 | 187 | // allocate buffer space on heap 188 | size_t resizeDimLen = RESIZE_DIM_SQ * colorDepth; // byte size of bitmap 189 | if (motionJpeg == NULL) motionJpeg = (uint8_t*)ps_malloc(32 * 1024); 190 | if (currBuff == NULL) currBuff = (uint8_t*)ps_malloc(RESIZE_DIM_SQ * RGB888_BYTES); 191 | static uint8_t* prevBuff = (uint8_t*)ps_malloc(RESIZE_DIM_SQ * RGB888_BYTES); 192 | static uint8_t* changeMap = (uint8_t*)ps_malloc(RESIZE_DIM_SQ * RGB888_BYTES); 193 | 194 | dTime = millis(); 195 | rescaleImage(rgb_buf, sampleWidth, sampleHeight, currBuff, RESIZE_DIM, RESIZE_DIM); 196 | LOG_VRB("Bitmap rescale to %u bytes in %lums", resizeDimLen, millis() - dTime); 197 | 198 | // compare each pixel in current frame with previous frame 199 | dTime = millis(); 200 | int changeCount = 0; 201 | // set horizontal region of interest in image 202 | uint16_t startPixel = (RESIZE_DIM*(detectStartBand-1)/detectNumBands) * RESIZE_DIM * colorDepth; 203 | uint16_t endPixel = (RESIZE_DIM*(detectEndBand)/detectNumBands) * RESIZE_DIM * colorDepth; 204 | int moveThreshold = ((endPixel-startPixel)/colorDepth) * (11-motionVal)/100; // number of changed pixels that constitute a movement 205 | for (int i = 0; i < resizeDimLen; i += colorDepth) { 206 | uint16_t currPix = 0, prevPix = 0; 207 | for (int j = 0; j < colorDepth; j++) { 208 | currPix += currBuff[i + j]; 209 | prevPix += prevBuff[i + j]; 210 | } 211 | currPix /= colorDepth; 212 | prevPix /= colorDepth; 213 | lux += currPix; // for calculating light level 214 | uint8_t pixVal = 255; // show active changed pixel as bright red color in changeMap image 215 | // set up display image for motion tracking debug 216 | if (dbgMotion) for (int j = 0; j < RGB888_BYTES; j++) changeMap[(i * stride) + j] = currPix; // grayscale 217 | // determine pixel change status 218 | if (abs((int)currPix - (int)prevPix) > detectChangeThreshold) { 219 | if (i > startPixel && i < endPixel) changeCount++; // number of changed pixels 220 | else pixVal = 80; // show inactive changed pixel as dark red color in changeMap image 221 | if (dbgMotion) { 222 | changeMap[(i * stride) + 2] = pixVal; 223 | for (int j = 0; j < RGB888_BYTES - 1; j++) changeMap[(i * stride) + j] = 0; 224 | } 225 | } 226 | } 227 | lightLevel = (lux*100)/(RESIZE_DIM_SQ*255); // light value as a % 228 | nightTime = isNight(nightSwitch); 229 | memcpy(prevBuff, currBuff, resizeDimLen); // save image for next comparison 230 | LOG_VRB("Detected %u changes, threshold %u, light level %u, in %lums", changeCount, moveThreshold, lightLevel, millis() - dTime); 231 | if (lightLevelOnly) return false; // no motion checking, only calc of light level 232 | 233 | if (dbgMotion) { 234 | // show motion detection during streaming for tuning 235 | if (!motionJpegLen) { 236 | // ready to setup next movement map for streaming 237 | dTime = millis(); 238 | // build jpeg of changeMap for debug streaming 239 | if (!fmt2jpg(changeMap, resizeDimLen, RESIZE_DIM, RESIZE_DIM, PIXFORMAT_RGB888, JPEG_QUAL, &jpg_buf, &motionJpegLen)) 240 | LOG_WRN("motionDetect: fmt2jpg() failed"); 241 | memcpy(motionJpeg, jpg_buf, motionJpegLen); 242 | free(jpg_buf); // releases 128kB in to_jpg.cpp 243 | jpg_buf = NULL; 244 | xSemaphoreGive(motionSemaphore); 245 | LOG_VRB("Created changeMap JPEG %d bytes in %lums", motionJpegLen, millis() - dTime); 246 | } 247 | } else { 248 | // normal motion detection 249 | dTime = millis(); 250 | if (!nightTime && changeCount > moveThreshold) { 251 | LOG_VRB("### Change detected"); 252 | motionCnt++; // number of consecutive changes 253 | // need minimum sequence of changes to signal valid movement 254 | if (!motionStatus && motionCnt >= detectMotionFrames) { 255 | LOG_VRB("***** Motion - START"); 256 | motionStatus = true; // motion started 257 | #if INCLUDE_TINYML 258 | // pass image to TinyML for classification 259 | if (mlUse) if (!tinyMLclassify()) motionCnt = 0; // not classified, so cancel motion 260 | #endif 261 | if (motionCnt) notifyMotion(fb); 262 | dTime = millis(); 263 | #if INCLUDE_MQTT 264 | if (mqtt_active && motionCnt) { 265 | sprintf(jsonBuff, "{\"MOTION\":\"ON\",\"TIME\":\"%s\"}", esp_log_system_timestamp()); 266 | mqttPublish(jsonBuff); 267 | mqttPublishPath("motion", "on"); 268 | #if INCLUDE_HASIO 269 | mqttPublishPath("cmd", "still"); 270 | #endif 271 | } 272 | #endif 273 | } 274 | } else motionCnt = 0; 275 | 276 | if (motionStatus && !motionCnt) { 277 | // insufficient change or motion not classified 278 | LOG_VRB("***** Motion - STOP"); 279 | motionStatus = false; // motion stopped 280 | #if INCLUDE_MQTT 281 | if (mqtt_active) { 282 | sprintf(jsonBuff, "{\"MOTION\":\"OFF\",\"TIME\":\"%s\"}", esp_log_system_timestamp()); 283 | mqttPublish(jsonBuff); 284 | mqttPublishPath("motion", "off"); 285 | } 286 | #endif 287 | } 288 | if (motionStatus) LOG_VRB("*** Motion - ongoing %u frames", motionCnt); 289 | } 290 | 291 | if (dbgVerbose) checkMemory(); 292 | LOG_VRB("============================"); 293 | // motionStatus indicates whether motion previously ongoing or not 294 | return nightTime ? false : motionStatus; 295 | } 296 | 297 | void notifyMotion(camera_fb_t* fb) { 298 | // send out notification of motion if requested 299 | #if INCLUDE_SMTP 300 | if (smtpUse) { 301 | // send email with movement image 302 | keepFrame(fb); 303 | char subjectMsg[50]; 304 | snprintf(subjectMsg, sizeof(subjectMsg) - 1, "from %s", hostName); 305 | emailAlert("Motion Alert", subjectMsg); 306 | } 307 | #endif 308 | #if INCLUDE_TGRAM 309 | if (tgramUse) keepFrame(fb); // for telegram, wait till filename available 310 | #endif 311 | } 312 | 313 | /************* copied and modified from esp32-camera/to_bmp.c to access jpg_scale_t *****************/ 314 | 315 | typedef struct { 316 | uint16_t width; 317 | uint16_t height; 318 | uint16_t data_offset; 319 | const uint8_t *input; 320 | uint8_t *output; 321 | } rgb_jpg_decoder; 322 | 323 | static bool _rgb_write(void * arg, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t *data) { 324 | // mpjpeg2sd: modified to generate 24 bit RGB or 8 bit grayscale 325 | rgb_jpg_decoder * jpeg = (rgb_jpg_decoder *)arg; 326 | if (!data){ 327 | if (x == 0 && y == 0) { 328 | // write start 329 | jpeg->width = w; 330 | jpeg->height = h; 331 | } 332 | return true; 333 | } 334 | 335 | size_t jw = jpeg->width*RGB888_BYTES; 336 | size_t t = y * jw; 337 | size_t b = t + (h * jw); 338 | size_t l = x * RGB888_BYTES; 339 | uint8_t *out = jpeg->output+jpeg->data_offset; 340 | uint8_t *o = out; 341 | size_t iy, ix; 342 | w *= RGB888_BYTES; 343 | 344 | for (iy=t; iyinput + index, len); 364 | return len; 365 | } 366 | 367 | static bool jpg2rgb(const uint8_t* src, size_t src_len, uint8_t* out, jpg_scale_t scale) { 368 | rgb_jpg_decoder jpeg; 369 | jpeg.width = 0; 370 | jpeg.height = 0; 371 | jpeg.input = src; 372 | jpeg.output = out; 373 | jpeg.data_offset = 0; 374 | esp_err_t res = esp_jpg_decode(src_len, scale, _jpg_read, _rgb_write, (void*)&jpeg); 375 | if (res != ESP_OK) LOG_WRN("jpg2rgb failure: %s", espErrMsg(res)); 376 | return (res == ESP_OK) ? true : false; 377 | } 378 | -------------------------------------------------------------------------------- /mqtt.cpp: -------------------------------------------------------------------------------- 1 | 2 | // contributed by gemi254 and genehand 3 | 4 | #include "appGlobals.h" 5 | 6 | #if (INCLUDE_HASIO && !INCLUDE_MQTT) 7 | #error "Need INCLUDE_MQTT true" 8 | #endif 9 | 10 | #if INCLUDE_MQTT 11 | #define CONFIG_MQTT_PROTOCOL_311 12 | #include "mqtt_client.h" 13 | 14 | #if (!INCLUDE_CERTS) 15 | const char* mqtt_rootCACertificate = ""; 16 | #endif 17 | #if (INCLUDE_HASIO) 18 | #define HASIO_AVAILABILITY "homeassistant/status" 19 | char image_topic[FILE_NAME_LEN] = ""; //Mqtt server topic to publish image payloads. 20 | void sendMqttHasDiscovery(); 21 | void sendMqttHasState(); 22 | #endif 23 | char mqtt_broker[MAX_HOST_LEN] = ""; //Mqtt server ip to connect. 24 | char mqtt_port[5] = ""; //Mqtt server port to connect. 25 | char mqtt_user[MAX_HOST_LEN] = ""; //Mqtt server username. 26 | char mqtt_user_Pass[MAX_PWD_LEN] = ""; //Mqtt server password. 27 | char mqtt_topic_prefix[FILE_NAME_LEN / 2] = ""; //Mqtt server topic to publish payloads. 28 | 29 | #define MQTT_LWT_QOS 2 30 | #define MQTT_LWT_RETAIN 1 31 | #define MQTT_RETAIN 0 32 | #define MQTT_QOS 1 33 | 34 | bool mqtt_active = false; //Is enabled 35 | bool mqttRunning = false; //Is mqtt task running 36 | bool mqttConnected = false; //Is connected to broker? 37 | esp_mqtt_client_handle_t mqtt_client = nullptr; 38 | TaskHandle_t mqttTaskHandle = NULL; 39 | static char remoteQuery[FILE_NAME_LEN * 2] = ""; 40 | static char lwt_topic[FILE_NAME_LEN]; 41 | static char cmd_topic[FILE_NAME_LEN]; 42 | static int mqttTaskDelay = 0; 43 | static char mqttPublishTopic[FILE_NAME_LEN] = ""; 44 | 45 | void mqtt_client_publish(const char* topic, const char* payload){ 46 | if (!mqtt_client || !mqttConnected) return; 47 | int id = esp_mqtt_client_publish(mqtt_client, topic, payload, strlen(payload), MQTT_QOS, MQTT_RETAIN); 48 | LOG_VRB("Mqtt pub, topic:%s, ID:%d, length:%i", topic, id, strlen(payload)); 49 | LOG_VRB("Mqtt pub, payload:%s", payload); 50 | } 51 | 52 | void mqttPublish(const char* payload) { 53 | if (!strlen(mqtt_topic_prefix)) return; //Called before load config? 54 | if (!strlen(mqttPublishTopic)) snprintf(mqttPublishTopic, FILE_NAME_LEN, "%ssensor/%s/state", mqtt_topic_prefix, hostName); 55 | mqtt_client_publish(mqttPublishTopic, payload); 56 | } 57 | 58 | void mqttPublishPath(const char* suffix, const char* payload, const char *device) { 59 | char topic[2 * FILE_NAME_LEN]; 60 | if (!strlen(mqtt_topic_prefix)) return; 61 | snprintf(topic, 2 * FILE_NAME_LEN, "%s%s/%s/%s", mqtt_topic_prefix, device, hostName, suffix); 62 | mqtt_client_publish(topic, payload); 63 | } 64 | 65 | static void mqtt_connected_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { 66 | LOG_INF("Mqtt connected"); 67 | esp_mqtt_client_publish(mqtt_client, lwt_topic, "online", 0, MQTT_LWT_QOS, MQTT_LWT_RETAIN); 68 | mqttConnected = true; 69 | int id = esp_mqtt_client_subscribe(mqtt_client, cmd_topic, 1); 70 | if (id == -1){ 71 | LOG_WRN("Mqtt failed to subscribe: %s", cmd_topic ); 72 | stopMqttClient(); 73 | return; 74 | } 75 | else LOG_VRB("Mqtt subscribed: %s", cmd_topic ); 76 | 77 | #if (INCLUDE_HASIO) 78 | sendMqttHasDiscovery(); 79 | vTaskDelay(1000 / portTICK_RATE_MS); 80 | sendMqttHasState(); 81 | id = esp_mqtt_client_subscribe(mqtt_client, HASIO_AVAILABILITY, 1); 82 | if (id == -1){ 83 | LOG_WRN("Mqtt failed to subscribe: %s", HASIO_AVAILABILITY ); 84 | }else LOG_VRB("Mqtt subscribed: %s", HASIO_AVAILABILITY ); 85 | #endif 86 | } 87 | 88 | static void mqtt_disconnected_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { 89 | LOG_INF("Mqtt disconnect"); 90 | mqttConnected = false; 91 | //xTaskNotifyGive(mqttTaskHandle); //Unblock task 92 | } 93 | 94 | static void mqtt_data_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { 95 | esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; 96 | LOG_VRB("Mqtt topic=%.*s ", event->topic_len, event->topic); 97 | LOG_VRB("Mqtt data=%.*s ", event->data_len, event->data); 98 | #if INCLUDE_HASIO 99 | if(strncmp(event->topic, HASIO_AVAILABILITY, event->topic_len) == 0){ 100 | sendMqttHasDiscovery(); 101 | vTaskDelay(1000 / portTICK_RATE_MS); 102 | sendMqttHasState(); 103 | return; 104 | } 105 | #endif 106 | if(strncmp(event->topic, cmd_topic, event->topic_len) == 0){ 107 | if (strlen(remoteQuery) == 0) sprintf(remoteQuery, "%.*s", event->data_len, (char*)event->data); 108 | mqttConnected = true; 109 | LOG_VRB("Resuming mqtt thread.."); 110 | xTaskNotifyGive(mqttTaskHandle); 111 | } 112 | } 113 | 114 | static void mqtt_error_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { 115 | LOG_VRB("Event base=%s, event_id=%d", base, event_id); 116 | esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; 117 | LOG_VRB("Mqtt event error %i", event->msg_id); 118 | if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { 119 | // log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err); 120 | // log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err); 121 | // log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno); 122 | LOG_WRN("Last err string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); 123 | mqttConnected = false; 124 | } 125 | } 126 | void sendMqttImage(){ 127 | uint32_t startTime = millis(); 128 | if (!strlen(mqtt_topic_prefix)) return; 129 | doKeepFrame = true; 130 | while (doKeepFrame && millis() - startTime < 4 * MAX_FRAME_WAIT) delay(100); 131 | if (!doKeepFrame && alertBufferSize) { 132 | const char* picBuff = (const char*)(alertBuffer); 133 | int id = esp_mqtt_client_publish(mqtt_client, image_topic, picBuff, alertBufferSize, MQTT_QOS, 0); 134 | LOG_VRB("Sent pic, size: %lu", alertBufferSize ); 135 | }else{ 136 | LOG_INF("Fail to send image"); 137 | } 138 | } 139 | 140 | void checkForRemoteQuery() { 141 | //Execute remote query i.e. dbgVerbose=1;framesize=7;fps=1 142 | if (strlen(remoteQuery) > 0) { 143 | char* query = strtok(remoteQuery, ";"); 144 | while (query != NULL) { 145 | char* value = strchr(query, '='); 146 | if (value != NULL) { 147 | *value = 0; // split remoteQuery into 2 strings, first is key name 148 | value++; // second is value 149 | LOG_VRB("Mqtt exec q: %s v: %s", query, value); 150 | //Extra handling 151 | if (!strcmp(query, "clockUTC")) { //Set time from browser clock 152 | 153 | } else { 154 | #ifdef ISCAM 155 | //Block other tasks from accessing the camera 156 | if (!strcmp(query, "fps")) setFPS(atoi(value)); 157 | else if (!strcmp(query, "framesize")) setFPSlookup(fsizePtr); 158 | #endif 159 | updateStatus(query, value); 160 | } 161 | } else { //No params command 162 | LOG_VRB("Execute cmd: %s", query); 163 | if (!strcmp(query, "reset")) { //Reboot 164 | doRestart("Mqtt remote restart"); 165 | }else if (!strcmp(query, "status")) { 166 | buildJsonString(false); 167 | mqttPublishPath("status", jsonBuff); 168 | } else if (!strcmp(query, "status?q")) { 169 | buildJsonString(true); 170 | mqttPublishPath("status", jsonBuff); 171 | #if (INCLUDE_HASIO) 172 | } else if (!strcmp(query, "still")) { 173 | sendMqttImage(); 174 | sendMqttHasState(); 175 | } else if (!strcmp(query, "state")) { 176 | sendMqttHasState(); 177 | } else if (!strcmp(query, "disc")) { 178 | sendMqttHasDiscovery(); 179 | vTaskDelay(1000 / portTICK_RATE_MS); 180 | sendMqttHasState(); 181 | #endif 182 | } 183 | } 184 | query = strtok(NULL, ";"); 185 | } 186 | remoteQuery[0] = '\0'; 187 | } 188 | } 189 | 190 | static void mqttTask(void* parameter) { 191 | LOG_VRB("Mqtt task start"); 192 | while (mqtt_active) { 193 | //LOG_VRB("Waiting for signal.."); 194 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 195 | //LOG_VRB("Wake.."); 196 | if (mqttConnected) { 197 | //Check if server sends a remote command 198 | checkForRemoteQuery(); 199 | if (mqttTaskDelay > 0 ) vTaskDelay(mqttTaskDelay / portTICK_RATE_MS); 200 | } else { //Disconnected 201 | LOG_WRN("Disconnected wait.."); 202 | vTaskDelay(2000 / portTICK_RATE_MS); 203 | } 204 | //xTaskNotifyGive(mqttTaskHandle); 205 | } 206 | mqttRunning = false; 207 | LOG_VRB("Mqtt Task exiting.."); 208 | vTaskDelete(NULL); 209 | } 210 | 211 | void stopMqttClient() { 212 | if (mqtt_client == nullptr) return; 213 | if (mqttConnected){ 214 | esp_mqtt_client_publish(mqtt_client, lwt_topic, "offline", 0, MQTT_LWT_QOS, MQTT_LWT_RETAIN); 215 | vTaskDelay(1000 / portTICK_RATE_MS); 216 | } 217 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_stop(mqtt_client)); 218 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_destroy(mqtt_client)); 219 | LOG_VRB("Checking task..%u", mqttTaskHandle); 220 | if ( mqttTaskHandle != NULL ) { 221 | LOG_VRB("Unlock task.."); 222 | xTaskNotifyGive(mqttTaskHandle); //Unblock task 223 | vTaskDelay(1500 / portTICK_RATE_MS); 224 | LOG_VRB("Deleted task..?"); 225 | } 226 | LOG_VRB("Exiting.."); 227 | mqttConnected = false; 228 | mqtt_client = nullptr; 229 | } 230 | 231 | void startMqttClient(void){ 232 | if (!mqtt_active) { 233 | LOG_VRB("MQTT not active.."); 234 | return; 235 | } 236 | 237 | if (mqttConnected) { 238 | LOG_VRB("MQTT already running.. Exiting"); 239 | return; 240 | } 241 | 242 | if (WiFi.status() != WL_CONNECTED) { 243 | mqttConnected = false; 244 | LOG_VRB("Wifi disconnected.. Retry mqtt on connect"); 245 | return; 246 | } 247 | 248 | char mqtt_uri[FILE_NAME_LEN]; 249 | sprintf(mqtt_uri, "mqtt://%s:%s", mqtt_broker, mqtt_port); 250 | snprintf(lwt_topic, FILE_NAME_LEN, "%ssensor/%s/lwt", mqtt_topic_prefix, hostName); 251 | snprintf(cmd_topic, FILE_NAME_LEN, "%ssensor/%s/cmd", mqtt_topic_prefix, hostName); 252 | snprintf(image_topic, FILE_NAME_LEN, "%ssensor/%s/still", mqtt_topic_prefix, hostName); 253 | 254 | esp_mqtt_client_config_t mqtt_cfg = { 255 | .broker = { 256 | .address = { .uri = mqtt_uri }, 257 | }, 258 | .credentials = { 259 | .username = mqtt_user, 260 | .client_id = hostName, 261 | .authentication = { .password = mqtt_user_Pass }, 262 | }, 263 | .session = { 264 | .last_will = { 265 | .topic = lwt_topic, 266 | .msg = "offline", 267 | .qos = MQTT_LWT_QOS, 268 | .retain = MQTT_LWT_RETAIN, 269 | }, 270 | }, 271 | }; 272 | 273 | mqtt_client = esp_mqtt_client_init(&mqtt_cfg); 274 | LOG_INF("Mqtt connect to %s...", mqtt_uri); 275 | //LOG_INF("Mqtt connect pass: %s...", mqtt_user_Pass); 276 | if (mqtt_client != NULL) { 277 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_CONNECTED, mqtt_connected_handler, NULL)); 278 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_DISCONNECTED, mqtt_disconnected_handler, NULL)); 279 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_DATA, mqtt_data_handler, mqtt_client)); 280 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_register_event(mqtt_client, esp_mqtt_event_id_t::MQTT_EVENT_ERROR, mqtt_error_handler, mqtt_client)); 281 | if (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_start(mqtt_client)) != ESP_OK) { 282 | LOG_WRN("Mqtt start failed"); 283 | } else { 284 | LOG_VRB("Mqtt started"); 285 | // Create a mqtt task 286 | BaseType_t xReturned = xTaskCreate(&mqttTask, "mqttTask", MQTT_STACK_SIZE, NULL, MQTT_PRI, &mqttTaskHandle); 287 | LOG_INF("Created mqtt task: %u", xReturned ); 288 | mqttRunning = true; 289 | } 290 | } 291 | } 292 | 293 | #if (INCLUDE_HASIO) 294 | void sendHasEntities (const char *name, const char *displayName, const char *units = "", 295 | const char *icon = "", const char *category = "", const char *topic = "", 296 | const char *payload_on = "",const char *payload_off = ""){ 297 | char* p = jsonBuff; 298 | *p++ = '{'; 299 | p += sprintf(p, "\"name\":\"%s\",", displayName); 300 | p += sprintf(p, "\"uniq_id\":\"%s_%012llX\",", name, ESP.getEfuseMac() ); 301 | //p += sprintf(p, "\"obj_id\":\"%s %s\",", hostName, name); 302 | if(strlen(units)>0) 303 | p += sprintf(p, "\"unit_of_meas\":\"%s\",", units); 304 | if(strlen(icon)>0) 305 | p += sprintf(p, "\"ic\":\"%s\",", icon); 306 | 307 | if(strcmp(category, "camera") == 0 ){ 308 | p += sprintf(p, "\"t\":\"%ssensor/%s/%s\",", mqtt_topic_prefix, hostName, topic); 309 | 310 | }else{ 311 | if(strlen(category)>0){ 312 | p += sprintf(p, "\"ent_cat\":\"%s\",", category); 313 | } 314 | if(strlen(topic)) 315 | p += sprintf(p, "\"stat_t\":\"%ssensor/%s/%s\",", mqtt_topic_prefix, hostName, topic); 316 | else 317 | p += sprintf(p, "\"stat_t\":\"%ssensor/%s/%s\",", mqtt_topic_prefix, hostName, name); 318 | 319 | if(strlen(payload_on) && strlen(payload_off) ){ 320 | p += sprintf(p, "\"pl_on\":\"%s\",", payload_on); 321 | p += sprintf(p, "\"pl_off\":\"%s\",", payload_off); 322 | p += sprintf(p, "\"cmd_t\":\"%ssensor/%s/%s\",", mqtt_topic_prefix, hostName, "cmd"); 323 | }else if(strlen(payload_on) && !strlen(payload_off) ){ 324 | p += sprintf(p, "\"pl_prs\":\"%s\",", payload_on); 325 | p += sprintf(p, "\"cmd_t\":\"%ssensor/%s/%s\",", mqtt_topic_prefix, hostName, "cmd"); 326 | } 327 | } 328 | 329 | if(strcmp(category, "diagnostic") != 0 && strlen(payload_on) == 0){ 330 | p += sprintf(p, "\"avty_t\":\"%ssensor/%s/%s\",", mqtt_topic_prefix, hostName, "lwt"); 331 | p += sprintf(p, "\"pl_avail\":\"%s\",", "online"); 332 | p += sprintf(p, "\"pl_not_avail\":\"%s\",", "offline"); 333 | } 334 | p += sprintf(p, "\"device\":"); 335 | *p++ = '{'; 336 | p += sprintf(p, "\"name\":\"%s\",", hostName); 337 | p += sprintf(p, "\"ids\":[\"%s-%s\"],", hostName, ESP.getChipModel()); 338 | p += sprintf(p, "\"sw\":\"%s\",", APP_VER); 339 | p += sprintf(p, "\"cns\":[[ \"mac\",\"%s\"]],", WiFi.macAddress().c_str() ); 340 | p += sprintf(p, "\"mdl\":\"%s-%i\",", ESP.getChipModel(), ESP.getChipRevision()); 341 | p += sprintf(p, "\"cu\":\"http://%s/\",", WiFi.localIP().toString().c_str()); 342 | p += sprintf(p, "\"mf\":\"%s\"", "esp32cam"); 343 | *p++ = '}'; 344 | *p++ = '}'; 345 | *p = 0; 346 | 347 | char suffix[FILE_NAME_LEN] = ""; 348 | sprintf(suffix, "%s/config", name); 349 | if(strlen(payload_on) && strlen(payload_off) ) 350 | mqttPublishPath(suffix, jsonBuff, "switch"); 351 | else if(strlen(payload_on) && !strlen(payload_off)) 352 | mqttPublishPath(suffix, jsonBuff, "button"); 353 | else if(strcmp(category, "camera") == 0 ) 354 | mqttPublishPath("config", jsonBuff, "camera"); 355 | else 356 | mqttPublishPath(suffix, jsonBuff); 357 | 358 | } 359 | 360 | void sendMqttHasDiscovery(){ 361 | //Home Asssistant sensors 362 | sendHasEntities ("motion", "Motion", "", "mdi:motion-sensor"); 363 | sendHasEntities ("record", "Record","", "mdi:video-check"); 364 | //Home Asssistant Diagnostic 365 | sendHasEntities ("clock", "Camera clock", "", "mdi:clock-outline", "diagnostic", "clock"); 366 | sendHasEntities ("up_time", "Up time", "", "mdi:clock", "diagnostic", "up_time"); 367 | sendHasEntities ("atemp", "Camera temperature", "C", "mdi:coolant-temperature", "diagnostic", "atemp"); 368 | sendHasEntities ("wifi_rssi", "Signal Strength", "dBm", "mdi:wifi", "diagnostic", "wifi_rssi"); 369 | sendHasEntities ("wifi_ip", "Wifi IP", "", "mdi:wifi", "diagnostic", "wifi_ip"); 370 | sendHasEntities ("free_heap", "Free Heap", "", "mdi:memory", "diagnostic", "free_heap"); 371 | sendHasEntities ("free_psram", "Free PSRAM", "", "mdi:memory", "diagnostic", "free_psram"); 372 | sendHasEntities ("free_bytes", "Free SD", "", "mdi:memory", "diagnostic", "free_bytes"); 373 | //Home Asssistant Buttons 374 | sendHasEntities ("led", "Camera led", "", "mdi:led-on", "", "", "lampLevel=15","lampLevel=0"); 375 | sendHasEntities ("forceRecord", "Start Record", "", "mdi:video-check", "", "", "forceRecord=1","forceRecord=0"); 376 | //Home Asssistant Config Buttons 377 | sendHasEntities ("still", "Get Picture", "", "mdi:list-status", "config", "", "still"); 378 | sendHasEntities ("state", "Get diagnostics", "", "mdi:list-status", "config", "", "state"); 379 | sendHasEntities ("restart", "Restart device", "", "mdi:restart", "config", "", "reset"); 380 | //Home Asssistant Camera 381 | sendHasEntities (hostName, "cam", "", "mdi:video", "camera", "still"); 382 | mqttPublishPath("cmd", "still"); 383 | 384 | if (isCapturing) mqttPublishPath("record", "on"); 385 | else mqttPublishPath("record", "off"); 386 | mqttPublishPath("motion", "off"); 387 | } 388 | void sendMqttHasState(){ 389 | char* p = jsonBuff; 390 | char timeBuff[20]; 391 | strftime(timeBuff, 20, "%Y-%m-%d %H:%M:%S", localtime(&currEpoch)); 392 | mqttPublishPath("clock", timeBuff); 393 | formatElapsedTime(timeBuff, millis()); 394 | mqttPublishPath("up_time", timeBuff); 395 | float aTemp = readTemperature(true); 396 | if (aTemp > -127.0){ 397 | sprintf(p, "%0.1f", aTemp); 398 | mqttPublishPath("atemp", p); 399 | } 400 | sprintf(p, "%i", WiFi.RSSI()); 401 | mqttPublishPath("wifi_rssi", p); 402 | sprintf(p, "%s", WiFi.localIP().toString().c_str()); 403 | mqttPublishPath("wifi_ip", p); 404 | sprintf(p, "%s", fmtSize(ESP.getFreeHeap()) ); 405 | mqttPublishPath("free_heap", p); 406 | sprintf(p, "%s", fmtSize(ESP.getFreePsram()) ); 407 | mqttPublishPath("free_psram", p); 408 | sprintf(p, "%s", fmtSize(STORAGE.totalBytes() - STORAGE.usedBytes()) ); 409 | mqttPublishPath("free_bytes", p); 410 | } 411 | #endif 412 | #endif 413 | -------------------------------------------------------------------------------- /photogram.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Photogrammetry uses photographs taken from various angles to collect data about a 3D object 3 | // that can converted by software to create a 3D image, eg for 3D printing a replica. 4 | // To enable photographs to be taken from different angles a turntable hosting the object can be rotated at intervals 5 | // in front of a static camera. 6 | // 7 | // The ESP can be used to control the turntable using a stepper motor, and take photograps either using its built in camera, 8 | // or by remotely controlling the shutter of a DSLR camera. 9 | 10 | // A turntable can be 3D printed and driven by a 28BYJ-48 stepper motor with ULN2003 Motor Driver. 11 | // 12 | // Example of 3D printed turntable: www.thingiverse.com/thing:4817279 13 | // Example of circuit to interface to RS-60E3 Remote Switch for DSLR cameras: github.com/ch3p4ll3/ESP-Intervallometer#how-to-make-your-intervallometer 14 | // Use Meshroom software to create a 3D image: alicevision.org/#meshroom 15 | // Use Blender software to convert and modify the image for 3D printing: www.blender.org 16 | 17 | // Use web interface to specify the parameters and pins to be used listed below. The turntable will make a complete rotation, stopping at regular intervals 18 | // to take a photo depending on number of photos required. If the ESP camera is used, the photos are stored on the SD card as JPEG images in a folder 19 | // named after the date time when the Start button was pressed. 20 | // If the ESP lamp LED is enabled, it will be used as a flash. 21 | 22 | // s60sc 2024 23 | 24 | #include "appGlobals.h" 25 | 26 | #if INCLUDE_PGRAM 27 | #if !INCLUDE_PERIPH 28 | #error "Need INCLUDE_PERIPH true" 29 | #endif 30 | 31 | // Use web interface to specify the following parameters 32 | uint8_t numberOfPhotos; // number of photos to be taken in a rotation of the turntable 33 | float tRPM; // required turntable RPM 34 | bool clockWise; // rotation direction of turntable 35 | uint8_t timeForFocus; // time for DSLR auto focus in secs 36 | // timeForPhoto is total time to allow for a photo in secs, need to: 37 | // - wait for turntable to stabilize 38 | // - wait for ESP Lamp LED to illuminate if required 39 | // - time allowed for auto focus if required 40 | // - shutter time for photo 41 | uint8_t timeForPhoto; 42 | int pinShutter; // pin used for RS-60E3 shutter control 43 | int pinFocus; // pin used for RS-60E3 shutter control 44 | uint8_t photosDone; // read only count of number of photos taken so far 45 | float gearing; // number of rotation of stepper motor for one rotation of turntable 46 | bool extCam = false; // whether to use external DSLR camera (true) or built in ESP Cam (false) 47 | bool PGactive = false; 48 | 49 | static float mRPM; // stepper RPM derived from tRPM and gearing 50 | static TaskHandle_t pgramHandle = NULL; 51 | static char pFolder[20]; 52 | 53 | #define MAX_RPM 15.0 // max allowed stepper motor RPM 54 | #define shutterTime 100 // time in ms to allow DSLR shutter to open and close 55 | 56 | static void prepPgram() { 57 | if (extCam) { 58 | pinMode(pinShutter, OUTPUT); 59 | if (pinFocus) pinMode(pinFocus, OUTPUT); 60 | LOG_INF("External cam, shutter pin %d", pinShutter); 61 | #ifdef AUXILIARY 62 | } else { 63 | // use built in cam 64 | lampAuto = true; 65 | useMotion = doRecording = doPlayback = timeLapseOn = false; 66 | setLamp(0); 67 | // create folder 68 | time_t currEpoch = getEpoch(); 69 | strftime(pFolder, sizeof(pFolder), "/%Y%m%d_%H%M%S", localtime(&currEpoch)); 70 | STORAGE.mkdir(pFolder); 71 | LOG_INF("Built in cam, created photogrammetry folder %s", pFolder); 72 | #endif 73 | } 74 | } 75 | 76 | #ifdef AUXILIARY 77 | static void getPhoto() { 78 | LOG_WRN("Internal camera not available on auxiliary board"); 79 | photosDone = numberOfPhotos; 80 | stepperDone(); 81 | } 82 | #else 83 | static void getPhoto() { 84 | // use built in esp cam 85 | setLamp(lampLevel); // turn on lamp led as flash if required 86 | if (timeForPhoto * 1000 > MAX_FRAME_WAIT) delay((timeForPhoto * 1000) - MAX_FRAME_WAIT); // allow time for turntable to stabilise 87 | uint32_t startTime = millis(); 88 | doKeepFrame = true; 89 | while (doKeepFrame && (millis() - startTime < MAX_FRAME_WAIT)) delay(100); 90 | if (!doKeepFrame && alertBufferSize) { 91 | // create file name 92 | char pName[FILE_NAME_LEN]; 93 | strcpy(pName, pFolder); 94 | time_t currEpoch = getEpoch(); 95 | strftime(pName + strlen(pFolder), sizeof(pName), "/%Y%m%d_%H%M%S", localtime(&currEpoch)); 96 | strcat(pName, JPG_EXT); 97 | File pFile = STORAGE.open(pName, FILE_WRITE); 98 | // save file to SD 99 | pFile.write((uint8_t*)alertBuffer, alertBufferSize); 100 | pFile.close(); 101 | LOG_INF("Photo %u of % u saved in %s", photosDone + 1, numberOfPhotos, pName); 102 | alertBufferSize = 0; 103 | } else LOG_WRN("Failed to get photo"); 104 | setLamp(0); 105 | } 106 | #endif 107 | 108 | static void takePhoto() { 109 | // control external camera 110 | if (timeForFocus * 1000 > timeForPhoto * 1000 - shutterTime) timeForFocus = timeForPhoto - 1; 111 | uint32_t waitTime = (timeForPhoto - timeForFocus) * 1000 - shutterTime; 112 | delay(waitTime); // allow time for turntable to stabilise 113 | if (pinFocus) { 114 | // if using auto focus 115 | digitalWrite(pinFocus, HIGH); 116 | delay(timeForFocus * 1000); // allow time for auto focus 117 | } 118 | digitalWrite(pinShutter, HIGH); 119 | delay(shutterTime); 120 | digitalWrite(pinShutter, LOW); 121 | if (pinFocus) digitalWrite(pinFocus, LOW); 122 | if (photosDone < numberOfPhotos) LOG_INF("Photo %u of %u taken", photosDone + 1, numberOfPhotos); 123 | } 124 | 125 | static void pgramTask (void *pvParameter) { 126 | // take sequence of photos in one revolution of turntable 127 | // turntable rotation requires gearing number of shutter motor rotations 128 | float angle = 1.0 / (float)numberOfPhotos; // ie angular fraction of one revolution 129 | photosDone = 0; 130 | prepPgram(); 131 | LOG_INF("Start taking %u photos each %0.1f deg at %0.1f RPM", numberOfPhotos, angle * 360, tRPM); 132 | do { 133 | extCam ? takePhoto() : getPhoto(); 134 | // !clockwise as turntable rotates opp to motor 135 | stepperRun(mRPM, angle * gearing, !clockWise); 136 | // wait for stepper task to finish 137 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 138 | } while (++photosDone < numberOfPhotos); 139 | LOG_INF("Completed taking photos"); 140 | if (extCam) { 141 | pinMode(pinShutter, INPUT); // stop unneccessary power use 142 | if (pinFocus) pinMode(pinFocus, INPUT); // stop unneccessary power use 143 | } 144 | pgramHandle = NULL; 145 | vTaskDelete(NULL); 146 | } 147 | 148 | void takePhotos(bool startPhotos) { 149 | // start task 150 | if (stepperUse) { 151 | if (startPhotos) { 152 | mRPM = tRPM * gearing; 153 | if (mRPM > MAX_RPM) LOG_WRN("Requested stepper RPM %0.1f is too high", mRPM); 154 | else { 155 | if (pgramHandle == NULL) xTaskCreate(&pgramTask, "pgramTask", STICK_STACK_SIZE , NULL, STICK_PRI, &pgramHandle); 156 | else LOG_WRN("pgramTask still running"); 157 | } 158 | } else { 159 | LOG_INF("User aborted taking photos"); 160 | photosDone = numberOfPhotos; 161 | stepperDone(); 162 | } 163 | } 164 | } 165 | 166 | void stepperDone() { 167 | // notify photogrammetry task for next step 168 | if (pgramHandle) xTaskNotifyGive(pgramHandle); 169 | } 170 | 171 | #endif 172 | -------------------------------------------------------------------------------- /rtsp.cpp: -------------------------------------------------------------------------------- 1 | // Library can be found here https://github.com/rjsachse/ESP32-RTSPServer.git or in Arduino library 2 | 3 | // Initialize the RTSP server 4 | /** 5 | * @brief Initializes the RTSP server with the specified configuration. 6 | * 7 | * This method can be called with specific parameters, or the parameters 8 | * can be set directly in the RTSPServer instance before calling begin(). 9 | * If any parameter is not explicitly set, the method uses default values. 10 | * 11 | * @param transport The transport type. Default is VIDEO_AND_SUBTITLES. Options are (VIDEO_ONLY, AUDIO_ONLY, VIDEO_AND_AUDIO, VIDEO_AND_SUBTITLES, AUDIO_AND_SUBTITLES, VIDEO_AUDIO_SUBTITLES). 12 | * @param rtspPort The RTSP port to use. Default is 554. 13 | * @param sampleRate The sample rate for audio streaming. Default is 0 must pass or set if using audio. 14 | * @param port1 The first port (used for video, audio or subtitles depending on transport). Default is 5430. 15 | * @param port2 The second port (used for audio or subtitles depending on transport). Default is 5432. 16 | * @param port3 The third port (used for subtitles). Default is 5434. 17 | * @param rtpIp The IP address for RTP multicast streaming. Default is IPAddress(239, 255, 0, 1). 18 | * @param rtpTTL The TTL value for RTP multicast packets. Default is 64. 19 | * @return true if initialization is successful, false otherwise. 20 | */ 21 | // RjSachse 2025 22 | 23 | #include "appGlobals.h" 24 | 25 | #if INCLUDE_RTSP 26 | 27 | #include 28 | 29 | RTSPServer rtspServer; 30 | 31 | //Comment out to enable multiple clients for all transports (TCP, UDP, Multicast) 32 | //#define OVERRIDE_RTSP_SINGLE_CLIENT_MODE 33 | 34 | bool rtspVideo; 35 | bool rtspAudio; 36 | bool rtspSubtitles; 37 | int rtspPort; 38 | uint16_t rtpVideoPort; 39 | uint16_t rtpAudioPort; 40 | uint16_t rtpSubtitlesPort; 41 | char RTP_ip[MAX_IP_LEN]; 42 | uint8_t rtspMaxClients; 43 | uint8_t rtpTTL; 44 | char RTSP_Name[MAX_HOST_LEN-1] = ""; 45 | char RTSP_Pass[MAX_PWD_LEN-1] = ""; 46 | bool useAuth; 47 | 48 | IPAddress rtpIp; 49 | char transportStr[30]; // Adjust the size as needed 50 | 51 | RTSPServer::TransportType determineTransportType() { 52 | if (rtspVideo && rtspAudio && rtspSubtitles) { 53 | strcpy(transportStr, "s: Video, Audio & Subtitles"); 54 | return RTSPServer::VIDEO_AUDIO_SUBTITLES; 55 | } else if (rtspVideo && rtspAudio) { 56 | strcpy(transportStr, "s: Video & Audio"); 57 | return RTSPServer::VIDEO_AND_AUDIO; 58 | } else if (rtspVideo && rtspSubtitles) { 59 | strcpy(transportStr, "s: Video & Subtitles"); 60 | return RTSPServer::VIDEO_AND_SUBTITLES; 61 | } else if (rtspAudio && rtspSubtitles) { 62 | strcpy(transportStr, "s: Audio & Subtitles"); 63 | return RTSPServer::AUDIO_AND_SUBTITLES; 64 | } else if (rtspVideo) { 65 | strcpy(transportStr, ": Video"); 66 | return RTSPServer::VIDEO_ONLY; 67 | } else if (rtspAudio) { 68 | strcpy(transportStr, ": Audio"); 69 | return RTSPServer::AUDIO_ONLY; 70 | } else if (rtspSubtitles) { 71 | strcpy(transportStr, ": Subtitles"); 72 | return RTSPServer::SUBTITLES_ONLY; 73 | } else { 74 | strcpy(transportStr, ": None!"); 75 | return RTSPServer::NONE; 76 | } 77 | } 78 | 79 | #ifdef ISCAM 80 | 81 | static void sendRTSPVideo(void* p) { 82 | // Send jpeg frames via RTSP at current frame rate 83 | uint8_t taskNum = 1; 84 | streamBufferSize[taskNum] = 0; 85 | while (true) { 86 | if (frameSemaphore[taskNum] != NULL) { 87 | if (xSemaphoreTake(frameSemaphore[taskNum], pdMS_TO_TICKS(MAX_FRAME_WAIT)) == pdTRUE) { 88 | if (streamBufferSize[taskNum] && rtspServer.readyToSendFrame()) { 89 | // use frame stored by processFrame() 90 | rtspServer.sendRTSPFrame(streamBuffer[taskNum], streamBufferSize[taskNum], quality, frameData[fsizePtr].frameWidth, frameData[fsizePtr].frameHeight); 91 | } 92 | } 93 | streamBufferSize[taskNum] = 0; 94 | } else delay(100); 95 | } 96 | vTaskDelete(NULL); 97 | } 98 | 99 | void sendRTSPSubtitles(void* arg) { 100 | char data[100]; 101 | time_t currEpoch = getEpoch(); 102 | size_t len = strftime(data, 12, "%H:%M:%S ", localtime(&currEpoch)); 103 | len += sprintf(data + len, "FPS: %lu", rtspServer.rtpFps); 104 | #if INCLUDE_TELEM 105 | // add telemetry data 106 | if (teleUse) { 107 | storeSensorData(true); 108 | if (srtBytes) len += sprintf(data + len, "%s", (const char*)srtBuffer); 109 | srtBytes = 0; 110 | } 111 | #endif 112 | rtspServer.sendRTSPSubtitles(data, len); 113 | } 114 | 115 | static void startRTSPSubtitles(void* arg) { 116 | rtspServer.startSubtitlesTimer(sendRTSPSubtitles); // 1-second period 117 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 118 | vTaskDelete(NULL); // not reached 119 | } 120 | 121 | #endif 122 | 123 | static void sendRTSPAudio(void* p) { 124 | #if INCLUDE_AUDIO 125 | // send audio chunks via RTSP 126 | audioBytes = 0; 127 | while (true) { 128 | if (micGain && audioBytes && rtspServer.readyToSendAudio()) { 129 | rtspServer.sendRTSPAudio((int16_t*)audioBuffer, audioBytes); 130 | audioBytes = 0; 131 | } 132 | delay(20); 133 | } 134 | #endif 135 | vTaskDelete(NULL); 136 | } 137 | 138 | static void initRTSP() { 139 | #ifdef ISVC 140 | // Initialize the RTSP server for VC using constants 141 | rtspVideo = rtspSubtitles = false; 142 | rtspAudio = true; 143 | strcpy(RTP_ip, "239.255.0.1"); 144 | rtspPort = 554; 145 | rtpAudioPort = 5432; 146 | rtpVideoPort = 0; 147 | rtpSubtitlesPort = 0; 148 | rtspMaxClients = 1; 149 | rtpTTL = 1; 150 | #endif 151 | } 152 | 153 | void prepRTSP() { 154 | initRTSP(); 155 | useAuth = rtspServer.setCredentials(RTSP_Name, RTSP_Pass); // Set RTSP authentication 156 | RTSPServer::TransportType transport = determineTransportType(); 157 | rtpIp.fromString(RTP_ip); 158 | rtspServer.transport = transport; 159 | #if INCLUDE_AUDIO 160 | rtspServer.sampleRate = SAMPLE_RATE; 161 | #endif 162 | rtspServer.rtspPort = rtspPort; 163 | rtspServer.rtpVideoPort = rtpVideoPort; 164 | rtspServer.rtpAudioPort = rtpAudioPort; 165 | rtspServer.rtpSubtitlesPort = rtpSubtitlesPort; 166 | rtspServer.rtpIp = rtpIp; 167 | rtspServer.maxRTSPClients = rtspMaxClients; 168 | rtspServer.rtpTTL = rtpTTL; 169 | 170 | if (transport != RTSPServer::NONE) { 171 | if (rtspServer.init()) { 172 | LOG_INF("RTSP server started successfully with transport%s", transportStr); 173 | LOG_INF("Connect to: rtsp://%s%s:%d%s", useAuth ? ":@" : "", WiFi.localIP().toString().c_str(), 174 | rtspServer.rtspPort, useAuth ? " (credentials not shown for security reasons)" : ""); 175 | 176 | // start RTSP tasks, need bigger stack for video 177 | #ifdef ISCAM 178 | if (rtspVideo) xTaskCreate(sendRTSPVideo, "sendRTSPVideo", 1024 * 5, NULL, SUSTAIN_PRI, &sustainHandle[1]); 179 | if (rtspAudio) xTaskCreate(sendRTSPAudio, "sendRTSPAudio", 1024 * 5, NULL, SUSTAIN_PRI, &sustainHandle[2]); 180 | if (rtspSubtitles) xTaskCreate(startRTSPSubtitles, "startRTSPSubtitles", 1024 * 1, NULL, SUSTAIN_PRI, &sustainHandle[3]); 181 | #endif 182 | #ifdef ISVC 183 | xTaskCreate(sendRTSPAudio, "sendRTSPAudio", 1024 * 5, NULL, 5, NULL); 184 | #endif 185 | } else LOG_ERR("Failed to start RTSP server"); 186 | } else LOG_WRN("RTSP server not started, no transport selected"); 187 | } 188 | 189 | #endif 190 | -------------------------------------------------------------------------------- /smtp.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Simple SMTP client for sending email message with attachment 3 | // 4 | // Only tested with Gmail sender account 5 | // 6 | // Prereqs for Gmail sender account: 7 | // - recommended to create a dedicated email account 8 | // - create an app password - https://support.google.com/accounts/answer/185833 9 | // - set smtpUse in web config page to true, and enter account details on web page 10 | // 11 | // s60sc 2022 12 | 13 | #include "appGlobals.h" 14 | 15 | #if INCLUDE_SMTP 16 | #if (!INCLUDE_CERTS) 17 | const char* smtp_rootCACertificate = ""; 18 | #endif 19 | 20 | // SMTP connection params, setup via web page 21 | char smtp_login[MAX_HOST_LEN]; // sender email account 22 | char SMTP_Pass[MAX_PWD_LEN]; // 16 digit app password, not account password 23 | char smtp_email[MAX_HOST_LEN]; // receiver, can be same as smtp_login, or be any other email account 24 | char smtp_server[MAX_HOST_LEN]; // the email service provider, eg smtp.gmail.com" 25 | uint16_t smtp_port; // gmail SSL port 465; 26 | 27 | #define MIME_TYPE "image/jpg" 28 | #define ATTACH_NAME "frame.jpg" 29 | 30 | // SMTP control 31 | // Calling function has to populate SMTPbuffer and set smtpBufferSize for attachment data 32 | TaskHandle_t emailHandle = NULL; 33 | static char rspBuf[256]; // smtp response buffer 34 | static char respCodeRx[4]; // smtp response code 35 | static char subject[50]; 36 | static char message[100]; 37 | 38 | bool smtpUse = false; // whether or not to send email alerts 39 | int emailCount = 0; 40 | int alertMax = 10; // only applied to emails 41 | 42 | static bool sendSmtpCommand(NetworkClientSecure& client, const char* cmd, const char* respCode) { 43 | // wait from smtp server response, check response code and extract response data 44 | LOG_VRB("Cmd: %s", cmd); 45 | if (strlen(cmd)) client.println(cmd); 46 | 47 | uint32_t start = millis(); 48 | while (!client.available() && millis() < start + (responseTimeoutSecs * 1000)) delay(1); 49 | if (!client.available()) { 50 | LOG_WRN("SMTP server response timeout"); 51 | return false; 52 | } 53 | 54 | // read in response code and message 55 | client.read((uint8_t*)respCodeRx, 3); 56 | respCodeRx[3] = 0; // terminator 57 | int readLen = client.read((uint8_t*)rspBuf, 255); 58 | rspBuf[readLen] = 0; 59 | while (client.available()) client.read(); // bin the rest of response 60 | 61 | // check response code with expected 62 | LOG_VRB("Rx code: %s, resp: %s", respCodeRx, rspBuf); 63 | if (strcmp(respCodeRx, respCode) != 0) { 64 | // incorrect response code 65 | LOG_ERR("Command %s got wrong response: %s", cmd, rspBuf); 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | static bool emailSend(const char* mimeType = MIME_TYPE, const char* fileName = ATTACH_NAME) { 72 | 73 | // send email to defined smtp server 74 | char content[100]; 75 | 76 | NetworkClientSecure client; 77 | bool res = remoteServerConnect(client, smtp_server, smtp_port, smtp_rootCACertificate, EMAILCONN); 78 | if (!res) return false; 79 | 80 | while (true) { // fake non loop to enable breaks 81 | res = false; 82 | if (!sendSmtpCommand(client, "", "220")) break; 83 | 84 | sprintf(content, "HELO %s: ", APP_NAME); 85 | if (!sendSmtpCommand(client, content, "250")) break; 86 | 87 | if (!sendSmtpCommand(client, "AUTH LOGIN", "334")) break; 88 | if (!sendSmtpCommand(client, encode64(smtp_login), "334")) break; 89 | if (!sendSmtpCommand(client, encode64(SMTP_Pass), "235")) break; 90 | 91 | // send email header 92 | sprintf(content, "MAIL FROM: <%s>", APP_NAME); 93 | if (!sendSmtpCommand(client, content, "250")) break; 94 | sprintf(content, "RCPT TO: <%s>", smtp_email); 95 | if (!sendSmtpCommand(client, content, "250")) break; 96 | 97 | // send message body header 98 | if (!sendSmtpCommand(client, "DATA", "354")) break; 99 | sprintf(content, "From: \"%s\" <%s>", APP_NAME, smtp_login); 100 | client.println(content); 101 | sprintf(content, "To: <%s>", smtp_email); 102 | client.println(content); 103 | sprintf(content, "Subject: %s", subject); 104 | client.println(content); 105 | 106 | // send message 107 | client.println("MIME-Version: 1.0"); 108 | sprintf(content, "Content-Type: Multipart/mixed; boundary=%s", BOUNDARY_VAL); 109 | client.println(content); 110 | sprintf(content, "--%s", BOUNDARY_VAL); 111 | client.println(content); 112 | client.println("Content-Type: text/plain; charset=UTF-8"); 113 | client.println("Content-Transfer-Encoding: quoted-printable"); 114 | client.println("Content-Disposition: inline"); 115 | client.println(); 116 | client.println(message); 117 | client.println(); 118 | 119 | if (alertBufferSize) { 120 | // send attachment 121 | client.println(content); // boundary 122 | sprintf(content, "Content-Type: %s", mimeType); 123 | client.println(content); 124 | client.println("Content-Transfer-Encoding: base64"); 125 | sprintf(content, "Content-Disposition: attachment; filename=\"%s\"", fileName); 126 | client.println(content); 127 | // base64 encode attachment and send out in chunks 128 | size_t chunkSize = 3; 129 | for (size_t i = 0; i < alertBufferSize; i += chunkSize) 130 | client.write(encode64chunk(alertBuffer + i, min(alertBufferSize - i, chunkSize)), 4); 131 | } 132 | client.println("\n"); // two lines to finish header 133 | 134 | // close message data and quit 135 | if (!sendSmtpCommand(client, ".", "250")) break; 136 | if (!sendSmtpCommand(client, "QUIT", "221")) break; 137 | res = true; 138 | break; 139 | } 140 | // cleanly terminate connection 141 | remoteServerClose(client); 142 | alertBufferSize = 0; 143 | return res; 144 | } 145 | 146 | static void emailTask(void* parameter) { 147 | // send email 148 | if (emailCount < alertMax) { 149 | // send email if under daily limit 150 | if (emailSend()) LOG_ALT("Sent daily email %u", emailCount + 1); 151 | else LOG_WRN("Failed to send email"); 152 | } 153 | if (++emailCount >= alertMax) LOG_WRN("Daily email limit %u reached", alertMax); 154 | emailHandle = NULL; 155 | vTaskDelete(NULL); 156 | } 157 | 158 | void emailAlert(const char* _subject, const char* _message) { 159 | // send email to alert on required event 160 | if (smtpUse) { 161 | if (alertBuffer != NULL) { 162 | if (emailHandle == NULL) { 163 | strncpy(subject, _subject, sizeof(subject)-1); 164 | snprintf(subject+strlen(subject), sizeof(subject)-strlen(subject), " from %s", hostName); 165 | strncpy(message, _message, sizeof(message)-1); 166 | xTaskCreate(&emailTask, "emailTask", EMAIL_STACK_SIZE, NULL, EMAIL_PRI, &emailHandle); 167 | debugMemory("emailAlert"); 168 | } else LOG_WRN("Email alert already in progress"); 169 | } else LOG_WRN("Need to restart to setup email"); 170 | } 171 | } 172 | 173 | void prepSMTP() { 174 | if (smtpUse) { 175 | emailCount = 0; 176 | if (alertBuffer == NULL) alertBuffer = (byte*)ps_malloc(maxFrameBuffSize); 177 | LOG_INF("Email alerts active"); 178 | } 179 | } 180 | 181 | #endif 182 | -------------------------------------------------------------------------------- /streamServer.cpp: -------------------------------------------------------------------------------- 1 | // streamServer handles streaming, playback, file downloads 2 | // each sustained activity uses a separate task if available 3 | // - web streaming, playback, file downloads use task 0 4 | // - video streaming uses task 1 5 | // - audio streaming uses task 2 6 | // - subtitle streaming uses task 3 7 | // 8 | // s60sc 2022 - 2025 9 | 10 | #include "appGlobals.h" 11 | 12 | #define AUX_STRUCT_SIZE 2048 // size of http request aux data - sizeof(struct httpd_req_aux) = 1108 in esp_http_server 13 | // stream separator 14 | #define STREAM_CONTENT_TYPE "multipart/x-mixed-replace;boundary=" BOUNDARY_VAL 15 | #define JPEG_BOUNDARY "\r\n--" BOUNDARY_VAL "\r\n" 16 | #define JPEG_TYPE "Content-Type: image/jpeg\r\nContent-Length: %10u\r\n\r\n" 17 | #define HDR_BUF_LEN 64 18 | #define END_WAIT 100 19 | 20 | static fs::FS fpv = STORAGE; 21 | bool forcePlayback = false; // browser playback status 22 | bool streamVid = false; 23 | bool streamAud = false; 24 | bool streamSrt = false; 25 | static bool isStreaming[MAX_STREAMS] = {false}; 26 | size_t streamBufferSize[MAX_STREAMS] = {0}; 27 | byte* streamBuffer[MAX_STREAMS] = {NULL}; // buffer for stream frame 28 | static char variable[FILE_NAME_LEN]; 29 | static char value[FILE_NAME_LEN]; 30 | uint16_t sustainId = 0; 31 | uint8_t numStreams = 1; 32 | uint8_t vidStreams = 1; 33 | int srtInterval = 1; // subtitle interval in secs 34 | 35 | 36 | TaskHandle_t sustainHandle[MAX_STREAMS]; 37 | struct httpd_sustain_req_t { 38 | httpd_req_t* req = NULL; 39 | uint8_t taskNum; 40 | char activity[16]; 41 | bool inUse = false; 42 | }; 43 | httpd_sustain_req_t sustainReq[MAX_STREAMS]; 44 | 45 | #if INCLUDE_RTSP 46 | static const bool includeRTSP = true; 47 | #else 48 | static const bool includeRTSP = false; 49 | #endif 50 | 51 | static void showPlayback(httpd_req_t* req) { 52 | // output playback file to browser 53 | esp_err_t res = ESP_OK; 54 | stopPlaying(); 55 | forcePlayback = true; 56 | if (fpv.exists(inFileName)) { 57 | if (stopPlayback) LOG_WRN("Playback refused - capture in progress"); 58 | else { 59 | LOG_INF("Playback enabled (SD file selected)"); 60 | doPlayback = true; 61 | } 62 | } else LOG_WRN("File %s doesn't exist when Playback requested", inFileName); 63 | 64 | if (doPlayback) { 65 | // playback mjpeg from SD 66 | mjpegStruct mjpegData; 67 | // output header for playback request 68 | httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); 69 | httpd_resp_set_type(req, STREAM_CONTENT_TYPE); 70 | char hdrBuf[HDR_BUF_LEN]; 71 | openSDfile(inFileName); 72 | mjpegData = getNextFrame(true); 73 | while (doPlayback) { 74 | size_t jpgLen = mjpegData.buffLen; 75 | size_t buffOffset = mjpegData.buffOffset; 76 | if (!jpgLen && !buffOffset) { 77 | // complete mjpeg playback streaming 78 | res = httpd_resp_sendstr_chunk(req, JPEG_BOUNDARY); 79 | doPlayback = false; 80 | } else { 81 | if (jpgLen) { 82 | if (mjpegData.jpegSize) { // start of frame 83 | // send mjpeg header 84 | if (res == ESP_OK) res = httpd_resp_sendstr_chunk(req, JPEG_BOUNDARY); 85 | snprintf(hdrBuf, HDR_BUF_LEN-1, JPEG_TYPE, mjpegData.jpegSize); 86 | if (res == ESP_OK) res = httpd_resp_sendstr_chunk(req, hdrBuf); 87 | } 88 | // send buffer 89 | if (res == ESP_OK) res = httpd_resp_send_chunk(req, (const char*)iSDbuffer+buffOffset, jpgLen); 90 | } 91 | if (res == ESP_OK) mjpegData = getNextFrame(); 92 | else { 93 | // when browser closes playback get send error 94 | LOG_VRB("Playback aborted due to error: %s", espErrMsg(res)); 95 | stopPlaying(); 96 | } 97 | } 98 | } 99 | if (res == ESP_OK) httpd_resp_sendstr_chunk(req, NULL); 100 | sustainId = currEpoch; 101 | } 102 | } 103 | 104 | static void showStream(httpd_req_t* req, uint8_t taskNum) { 105 | // start live streaming to browser 106 | esp_err_t res = ESP_OK; 107 | size_t jpgLen = 0; 108 | uint8_t* jpgBuf = NULL; 109 | uint32_t startTime = millis(); 110 | uint32_t frameCnt = 0; 111 | uint32_t mjpegLen = 0; 112 | isStreaming[taskNum] = true; 113 | streamBufferSize[taskNum] = 0; 114 | if (!taskNum) motionJpegLen = 0; 115 | // output header for streaming request 116 | httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); 117 | httpd_resp_set_type(req, STREAM_CONTENT_TYPE); 118 | char hdrBuf[HDR_BUF_LEN]; 119 | while (isStreaming[taskNum]) { 120 | // stream from camera at current frame rate 121 | if (xSemaphoreTake(frameSemaphore[taskNum], pdMS_TO_TICKS(MAX_FRAME_WAIT)) == pdFAIL) { 122 | // failed to take semaphore, allow retry 123 | streamBufferSize[taskNum] = 0; 124 | continue; 125 | } 126 | if (dbgMotion && !taskNum) { 127 | // motion tracking stream on task 0 only, wait for new move mapping image 128 | if (xSemaphoreTake(motionSemaphore, pdMS_TO_TICKS(MAX_FRAME_WAIT)) == pdFAIL) continue; 129 | // use image created by checkMotion() 130 | jpgLen = motionJpegLen; 131 | if (!jpgLen) continue; 132 | jpgBuf = motionJpeg; 133 | } else { 134 | // live stream 135 | if (!streamBufferSize[taskNum]) continue; 136 | jpgLen = streamBufferSize[taskNum]; 137 | // use frame stored by processFrame() 138 | jpgBuf = streamBuffer[taskNum]; 139 | } 140 | if (res == ESP_OK) { 141 | // send next frame in stream 142 | res = httpd_resp_sendstr_chunk(req, JPEG_BOUNDARY); 143 | snprintf(hdrBuf, HDR_BUF_LEN-1, JPEG_TYPE, jpgLen); 144 | if (res == ESP_OK) res = httpd_resp_sendstr_chunk(req, hdrBuf); 145 | if (res == ESP_OK) res = httpd_resp_send_chunk(req, (const char*)jpgBuf, jpgLen); 146 | frameCnt++; 147 | } 148 | mjpegLen += jpgLen; 149 | jpgLen = streamBufferSize[taskNum] = 0; 150 | if (dbgMotion && !taskNum) motionJpegLen = 0; 151 | if (res != ESP_OK) { 152 | // get send error when browser closes stream 153 | LOG_VRB("Streaming aborted due to error: %s", espErrMsg(res)); 154 | isStreaming[taskNum] = false; 155 | } 156 | } 157 | if (res == ESP_OK) httpd_resp_sendstr_chunk(req, NULL); 158 | uint32_t mjpegTime = millis() - startTime; 159 | float mjpegTimeF = float(mjpegTime) / 1000; // secs 160 | LOG_INF("MJPEG: %u frames, total %s in %0.1fs @ %0.1ffps", frameCnt, fmtSize(mjpegLen), mjpegTimeF, (float)(frameCnt) / mjpegTimeF); 161 | } 162 | 163 | static void audioStream(httpd_req_t* req, uint8_t taskNum) { 164 | // output WAV audio stream to remote NVR 165 | #if INCLUDE_AUDIO 166 | if (micGain) { 167 | esp_err_t res = ESP_OK; 168 | httpd_resp_set_type(req, "audio/wav"); 169 | httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); 170 | isStreaming[taskNum] = true; 171 | uint32_t totalSamples = 0; 172 | audioBytes = WAV_HDR_LEN; 173 | updateWavHeader(); 174 | while (isStreaming[taskNum]) { 175 | if (audioBytes) { 176 | res = httpd_resp_send_chunk(req, (const char*)audioBuffer, audioBytes); 177 | audioBytes = 0; 178 | } else delay(20); // allow time for buffer to load 179 | if (res != ESP_OK) isStreaming[taskNum] = false; // client connection closed 180 | else totalSamples += audioBytes / 2; // 16 bit samples 181 | } 182 | audioBytes = 1; // stop loading of buffer 183 | if (res == ESP_OK) httpd_resp_sendstr_chunk(req, NULL); 184 | LOG_INF("WAV: sent %lu samples", totalSamples); 185 | } else LOG_WRN("No ESP mic defined or mic is off"); 186 | #else 187 | httpd_resp_sendstr(req, NULL); 188 | #endif 189 | } 190 | 191 | static void srtStream(httpd_req_t* req, uint8_t taskNum) { 192 | // generate subtitle entries for streaming, consisting of timestamp 193 | // plus telemetry data if telemetry enabled 194 | esp_err_t res = ESP_OK; 195 | isStreaming[taskNum] = true; 196 | httpd_resp_set_type(req, "text/plain"); 197 | httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); 198 | int srtSeqNo = 0; 199 | uint32_t srtTime = 0; 200 | const uint32_t sampleInterval = 1000 * (srtInterval < 1 ? 1 : srtInterval); 201 | char srtHdr[100]; 202 | char timeStr[10]; 203 | while (isStreaming[taskNum]) { 204 | srtSeqNo++; 205 | uint32_t startTime = millis(); 206 | formatElapsedTime(timeStr, srtTime, true); 207 | size_t srtPtr = sprintf(srtHdr, "%d\n%s --> ", srtSeqNo, timeStr); 208 | srtTime += sampleInterval; 209 | formatElapsedTime(timeStr, srtTime, true); 210 | srtPtr += sprintf(srtHdr + srtPtr, "%s\n", timeStr); 211 | time_t currEpoch = getEpoch(); 212 | srtPtr += strftime(srtHdr + srtPtr, 12, "%H:%M:%S ", localtime(&currEpoch)); 213 | httpd_resp_send_chunk(req, (const char*)srtHdr, srtPtr); 214 | #if INCLUDE_TELEM 215 | // add telemetry data 216 | if (teleUse) { 217 | storeSensorData(true); 218 | if (srtBytes) res = httpd_resp_send_chunk(req, (const char*)srtBuffer, srtBytes); 219 | srtBytes = 0; 220 | } 221 | #endif 222 | if (res == ESP_OK) res = httpd_resp_sendstr_chunk(req, "\n\n"); 223 | if (res != ESP_OK) isStreaming[taskNum] = false; // client connection closed 224 | else while (isStreaming[taskNum] && millis() - sampleInterval < startTime) delay(50); 225 | } 226 | if (res == ESP_OK) httpd_resp_sendstr_chunk(req, NULL); 227 | LOG_INF("SRT: sent %d subtitles", srtSeqNo); 228 | } 229 | 230 | void stopSustainTask(int taskId) { 231 | isStreaming[taskId] = false; 232 | } 233 | 234 | static void sustainTask(void* p) { 235 | // process sustained http(s) requests as a separate task 236 | while (true) { 237 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 238 | uint8_t i = *(uint8_t*)p; // identify task number 239 | if (i == 0) { 240 | if (!strcmp(sustainReq[i].activity, "download")) fileHandler(sustainReq[i].req, true); 241 | else if (!strcmp(sustainReq[i].activity, "playback")) showPlayback(sustainReq[i].req); 242 | else if (!strcmp(sustainReq[i].activity, "stream")) showStream(sustainReq[i].req, i); 243 | } 244 | else if (i == 1) showStream(sustainReq[i].req, i); 245 | else if (i == 2) audioStream(sustainReq[i].req, i); 246 | else if (i == 3) srtStream(sustainReq[i].req, i); 247 | // cleanup as request now complete on return 248 | killSocket(httpd_req_to_sockfd(sustainReq[i].req)); 249 | delay(END_WAIT); 250 | free(sustainReq[i].req->aux); 251 | sustainReq[i].req->~httpd_req_t(); 252 | free(sustainReq[i].req); 253 | sustainReq[i].req = NULL; 254 | sustainReq[i].inUse = false; 255 | } 256 | vTaskDelete(NULL); 257 | } 258 | 259 | void startSustainTasks() { 260 | // start httpd sustain tasks 261 | if (streamVid) numStreams = vidStreams = 2; 262 | if (streamAud) numStreams = 3; 263 | if (streamSrt) numStreams = 4; 264 | if (numStreams > MAX_STREAMS) { 265 | LOG_WRN("numStreams %d exceeds MAX_STREAMS %d", numStreams, MAX_STREAMS); 266 | numStreams = MAX_STREAMS; 267 | } 268 | if (maxFrameBuffSize * (vidStreams + 1) > ESP.getFreePsram()) { 269 | LOG_WRN("Insufficient PSRAM for NVR streams"); 270 | vidStreams = 1; 271 | streamVid = streamAud = streamSrt = false; 272 | } 273 | for (int i = 0; i < vidStreams; i++) 274 | if (streamBuffer[i] == NULL) streamBuffer[i] = (byte*)ps_malloc(maxFrameBuffSize); 275 | 276 | for (int i = 0; i < numStreams; i++) { 277 | sustainReq[i].taskNum = i; // so task knows its number 278 | if (includeRTSP && i > 0) continue; // as RTSP tasks created in rtsp.cpp 279 | xTaskCreate(sustainTask, "sustainTask", SUSTAIN_STACK_SIZE, &sustainReq[i].taskNum, SUSTAIN_PRI, &sustainHandle[i]); 280 | } 281 | 282 | LOG_INF("Started %d sustain tasks", numStreams); 283 | debugMemory("startSustainTasks"); 284 | } 285 | 286 | esp_err_t appSpecificSustainHandler(httpd_req_t* req) { 287 | // first check if authentication is required & passed 288 | esp_err_t res = ESP_FAIL; 289 | if (checkAuth(req)) { 290 | // handle long running request as separate task 291 | // obtain details from query string 292 | if (extractQueryKeyVal(req, variable, value) == ESP_OK) { 293 | // playback, download, web streaming uses task 0 294 | // remote streaming eg video uses task 1, audio task 2, srt task 3 295 | uint8_t taskNum = 99; 296 | if (!strcmp(variable, "download")) taskNum = 0; 297 | else if (!strcmp(variable, "playback")) taskNum = 0; 298 | else if (!strcmp(variable, "stream")) taskNum = 0; 299 | else if (!strcmp(variable, "video")) taskNum = 1; 300 | else if (!strcmp(variable, "audio")) taskNum = 2; 301 | else if (!strcmp(variable, "srt")) taskNum = 3; 302 | // http(s) streams not available if RTSP being used 303 | if (includeRTSP && taskNum > 0) taskNum = 99; 304 | if (taskNum < numStreams) { 305 | if (taskNum == 0) { 306 | if (req->method == HTTP_HEAD) { 307 | // task check request from app web page 308 | if (sustainReq[taskNum].inUse) { 309 | // task not free, try stopping it for new stream 310 | if (!strcmp(variable, "stream")) { 311 | isStreaming[taskNum] = false; 312 | if (!taskNum) doPlayback = false; // only for task 0 313 | delay(END_WAIT + 100); 314 | } 315 | } 316 | if (sustainReq[taskNum].inUse) { 317 | LOG_WRN("Task %d not free", taskNum); 318 | httpd_resp_set_status(req, "500 No free task"); 319 | } 320 | else { 321 | sustainId = currEpoch; // task available 322 | res = ESP_OK; 323 | } 324 | httpd_resp_sendstr(req, NULL); 325 | return res; 326 | } 327 | } else { 328 | // stop remote streaming if currently active 329 | if (taskNum < MAX_STREAMS) { 330 | if (sustainReq[taskNum].inUse) { 331 | isStreaming[taskNum] = false; 332 | delay(END_WAIT + 100); 333 | } 334 | } 335 | } 336 | 337 | // action request if task available 338 | if (!sustainReq[taskNum].inUse) { 339 | // make copy of request data and pass request to task indexed by request 340 | uint8_t i = taskNum; 341 | sustainReq[i].inUse = true; 342 | sustainReq[i].req = static_cast(malloc(sizeof(httpd_req_t))); 343 | new (sustainReq[i].req) httpd_req_t(*req); 344 | sustainReq[i].req->aux = psramFound() ? ps_malloc(AUX_STRUCT_SIZE) : malloc(AUX_STRUCT_SIZE); 345 | memcpy(sustainReq[i].req->aux, req->aux, AUX_STRUCT_SIZE); 346 | strncpy(sustainReq[i].activity, variable, sizeof(sustainReq[i].activity) - 1); 347 | // activate relevant task 348 | xTaskNotifyGive(sustainHandle[i]); 349 | return ESP_OK; 350 | } else httpd_resp_set_status(req, "500 No free task"); 351 | } else { 352 | if (taskNum < MAX_STREAMS) LOG_WRN("Task not created for stream: %s, numStreams %d", variable, numStreams); 353 | else LOG_WRN("Invalid task id: %s", variable); 354 | httpd_resp_set_status(req, "400 Invalid url"); 355 | } 356 | } else httpd_resp_set_status(req, "400 Bad URL"); 357 | httpd_resp_sendstr(req, NULL); 358 | } 359 | return res; 360 | } 361 | -------------------------------------------------------------------------------- /telegram.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Simple generic Telegram bot supporting: 3 | // - message interaction 4 | // - photo upload 5 | // - file upload (avoid using simultaneously with file upload via ftp, smtp, or browser) 6 | // Add custom processing to appSetupTelegram() in appSpecific.cpp 7 | // 8 | // Using ideas from: 9 | // - https://github.com/jameszah/ESP32-CAM-Video-Telegram 10 | // - https://github.com/cotestatnt/AsyncTelegram2 11 | // 12 | // 13 | // s60sc 2023 14 | 15 | #include "appGlobals.h" 16 | 17 | #if INCLUDE_TGRAM 18 | #define TELEGRAM_HOST "api.telegram.org" 19 | #define LONG_POLL 60 // how long in secs to keep connection open without reply 20 | #define MAX_HTTP_MSG 2048 // max size of buffer for HTTP request or response body 21 | #define FORM_OFFSET 256 // offset in tgramBuff to prepare form data 22 | #define MAX_TGRAM_SIZE (50 * ONEMEG) // max size for Telegram file upload 23 | 24 | #define HTTP_VER "HTTP/1.1" 25 | #define HTTP_CODE HTTP_VER " %d %*s\r" 26 | #define POST_HDR "POST /bot%s/%s " HTTP_VER "\r\nHost: " TELEGRAM_HOST "\r\nContent-Length: %u\r\nContent-Type: " 27 | #define FORM_DATA "--" BOUNDARY_VAL "\r\nContent-disposition: form-data; name=\"" 28 | #define CONTENT_TYPE "\"; filename=\"%s\"\r\nContent-Type: \"%s\"\r\n\r\n" 29 | #define MULTI_TYPE "multipart/form-data; boundary=" BOUNDARY_VAL 30 | #define JSON_TYPE "application/json" 31 | #define GETUP_JSON "{\"limit\":1,\"timeout\":%d,\"offset\":%ld}" 32 | #define POST_JSON "{\"chat_id\":%s,\"text\":\"%s\n\n%s%s\n\"}" 33 | #define PARSE_MODE ",\"parse_mode\":\"%s\"}" 34 | #define END_BOUNDARY "\r\n--" BOUNDARY_VAL "--\r\n" 35 | 36 | #if (!INCLUDE_CERTS) 37 | const char* telegram_rootCACertificate = ""; 38 | #endif 39 | 40 | // set via web interface 41 | bool tgramUse = false; 42 | char tgramToken[MAX_PWD_LEN] = ""; 43 | char tgramChatId[MAX_IP_LEN] = ""; 44 | 45 | char tgramHdr[FILE_NAME_LEN]; 46 | static char keyValue[60] = ""; // holds value for searched key in JSON response 47 | static char* tgramBuff = NULL; // holds sent then received data 48 | static int32_t lastUpdate = 0; 49 | 50 | TaskHandle_t telegramHandle = NULL; 51 | NetworkClientSecure tclient; 52 | 53 | static inline bool connectTelegram() { 54 | // Connect to Telegram server if not already connected 55 | return remoteServerConnect(tclient, TELEGRAM_HOST, HTTPS_PORT, telegram_rootCACertificate, TGRAMCONN); 56 | } 57 | 58 | static bool searchJsonResponse(const char* keyName) { 59 | // search json to extract value for given key, must end with a colon 60 | char* keyPtr = strstr(tgramBuff, keyName); 61 | if (keyPtr == NULL) return false; 62 | char* startItem = keyPtr + strlen(keyName); 63 | char* endItem = strchr(startItem, ','); 64 | int valSize = endItem - startItem; 65 | if (valSize > sizeof(keyValue) - 1) { 66 | LOG_WRN("Telegram JSON value too long %d", valSize); 67 | valSize = sizeof(keyValue - 1); 68 | } 69 | strncpy(keyValue, startItem, valSize); 70 | keyValue[valSize] = 0; 71 | return true; 72 | } 73 | 74 | size_t getResponseHeader(NetworkClientSecure& sclient, const char* host, int waitSecs) { 75 | // get response header from remote server if available 76 | if (!waitSecs) waitSecs = responseTimeoutSecs; 77 | bool endOfHeader = false; 78 | size_t contentLen = 0; 79 | int httpCode = 0; 80 | uint32_t startTime = millis(); 81 | if (sclient.available()) { 82 | while (!endOfHeader && millis() - startTime < waitSecs * 1000) { 83 | if (sclient.available()) { 84 | String tline = sclient.readStringUntil('\n'); 85 | //printf("Res: %s\n", tline.c_str()); 86 | endOfHeader = tline.length() > 1 ? false : true; // blank line ends header 87 | if (!httpCode) sscanf(tline.c_str(), HTTP_CODE, &httpCode); 88 | // get contentLength from header 89 | if (!contentLen) sscanf(tline.c_str(), "Content-Length: %d\r", &contentLen); 90 | } else delay(100); 91 | } 92 | if (!endOfHeader) { 93 | LOG_WRN("Timed out waiting for response from %s", host); 94 | return 0; 95 | } 96 | } 97 | return contentLen; 98 | } 99 | 100 | static bool getTgramResponse() { 101 | // receive response from Telegram if available and check if ok 102 | bool haveResponse = false; 103 | size_t readLen = 0; 104 | size_t contentLen = getResponseHeader(tclient, TELEGRAM_HOST, LONG_POLL); 105 | if (contentLen) { 106 | if (contentLen >= MAX_HTTP_MSG - 1) { 107 | LOG_WRN("contentLen %d exceeds buffer size", contentLen); 108 | contentLen = MAX_HTTP_MSG - 1; 109 | } 110 | while (contentLen - readLen > 0) { 111 | // retrieve response content 112 | size_t availLen = tclient.available(); 113 | if (availLen) readLen += tclient.readBytes((uint8_t*)tgramBuff + readLen, availLen); 114 | delay(50); 115 | } 116 | // format tgramBuff for searchJsonResponse() 117 | if (readLen != contentLen) LOG_WRN("Telegram data %d not equal to contentLength %d", readLen, contentLen); 118 | tgramBuff[contentLen] = 0; 119 | removeChar(tgramBuff, '"'); 120 | replaceChar(tgramBuff, '}', ','); 121 | // check if response from Telegram has ok'd request 122 | if (searchJsonResponse("ok:")) { 123 | if (strcmp(keyValue, "true")) { 124 | // get error description 125 | if (searchJsonResponse("description:")) LOG_WRN("Telegram error: %s", keyValue); 126 | else LOG_WRN("Telegram error, but description not retrieved"); 127 | } else if (searchJsonResponse("result:")) { 128 | // have response if result contains data, else just an ack 129 | if (strcmp(keyValue, "[]")) haveResponse = true; 130 | } 131 | } 132 | //printf("Cnt: %s\n", tgramBuff); 133 | remoteServerClose(tclient); // end of transaction 134 | } // else nothing received, so leave connection open 135 | return haveResponse; 136 | } 137 | 138 | static bool sendTgramHeader(const char* tmethod, const char* contentType, const char* dataType, 139 | size_t fileSize, const char* fileName, const char* caption) { 140 | if (connectTelegram()) { 141 | // create http post header 142 | char* p = tgramBuff + FORM_OFFSET; // leave space for http request data 143 | bool isFile = dataType != NULL ? true : false; 144 | if (isFile) { 145 | p += sprintf(p, FORM_DATA "chat_id\"\r\n\r\n%s", tgramChatId); 146 | if (caption != NULL) p += sprintf(p, "\r\n" FORM_DATA "caption\"\r\n\r\n%s", caption); 147 | p += sprintf(p, "\r\n" FORM_DATA "%s", dataType); 148 | p += sprintf(p, CONTENT_TYPE, fileName, contentType); 149 | } // else JSON data already loaded by sendTgramMessage 150 | size_t formLen = strlen(tgramBuff + FORM_OFFSET); 151 | // create http request header 152 | p = tgramBuff; 153 | if (isFile) fileSize += formLen + strlen(END_BOUNDARY); 154 | p += sprintf(p, POST_HDR, tgramToken, tmethod, fileSize); 155 | isFile ? strcat(p, MULTI_TYPE) : strcat(p, JSON_TYPE); 156 | strcat(p, "\r\n\r\n"); 157 | size_t reqLen = strlen(tgramBuff); 158 | // concatenate request and form data 159 | if (formLen) { 160 | memmove(tgramBuff + reqLen, tgramBuff + FORM_OFFSET, formLen); 161 | tgramBuff[reqLen + formLen] = 0; 162 | } 163 | tclient.print(tgramBuff); // http header 164 | //printf("header:\n%s\n", tgramBuff); 165 | return true; 166 | } 167 | return false; 168 | } 169 | 170 | static bool sendTgramBuff(uint8_t* buffData, size_t buffSize) { 171 | // generic for any post message sending buffer content, eg photo 172 | if (connectTelegram()) { 173 | // send as chunks 174 | for (size_t i = 0; i < buffSize; i += CHUNKSIZE) tclient.write(buffData + i, min((int)(buffSize - i), CHUNKSIZE)); 175 | tclient.println(END_BOUNDARY); 176 | return true; 177 | } 178 | return false; 179 | } 180 | 181 | bool prepTelegram() { 182 | // setup and check access to Telegram if required 183 | if (tgramUse) { 184 | if (strlen(tgramToken)) { 185 | if (tgramBuff == NULL) tgramBuff = psramFound() ? (char*)ps_malloc(MAX_HTTP_MSG) : (char*)malloc(MAX_HTTP_MSG); 186 | // check connection with getme request 187 | bool res = false; 188 | sendTgramHeader("getMe", NULL, NULL, 0, NULL, NULL); 189 | uint32_t startTime = millis(); 190 | while (!res && (millis() - startTime < responseTimeoutSecs * 1000)) { 191 | if (getTgramResponse()) res = true; 192 | delay(200); 193 | } 194 | if (res) { 195 | // response loaded into tgramBuff 196 | if (searchJsonResponse("username:")) { 197 | LOG_INF("Connected to Telegram Bot Handle: %s", keyValue); 198 | xTaskCreate(appSpecificTelegramTask, "telegramTask", TGRAM_STACK_SIZE, NULL, TGRAM_PRI, &telegramHandle); 199 | debugMemory("setupTelegramTask"); 200 | return true; 201 | } else LOG_WRN("getMe response not parsed %s", tgramBuff); 202 | } else LOG_WRN("Failed to communicate with Telegram server"); 203 | } else LOG_WRN("No Telegram Bot token supplied"); 204 | } else LOG_INF("Telegram not being used"); 205 | return false; 206 | } 207 | 208 | bool getTgramUpdate(char* responseText) { 209 | // get and process message from Telegram 210 | if (tclient.connected()) { 211 | // check for incoming message 212 | if (getTgramResponse()) { 213 | // process response and extract command if present 214 | if (searchJsonResponse("update_id:")) { 215 | int32_t update_id = atoi(keyValue); 216 | if (lastUpdate < update_id) { 217 | // new message, ok to process 218 | lastUpdate = update_id; 219 | if (searchJsonResponse("chat:{id:")) { 220 | if (!strcmp(tgramChatId, keyValue)) { 221 | if (searchJsonResponse("text:")) { 222 | strncpy(responseText, keyValue, FILE_NAME_LEN - 1); 223 | return true; // user request for app to process 224 | } // No text, ignore 225 | } else LOG_WRN("Message from unknown chat id: %s", keyValue); 226 | } else LOG_WRN("No chat id found"); 227 | } else LOG_WRN("Old update_id: %d", update_id); 228 | } // no update_id, ignore 229 | } 230 | } else { 231 | // send getUpdates request as not connected 232 | char* t = tgramBuff + FORM_OFFSET; 233 | t += sprintf(t, GETUP_JSON, LONG_POLL, lastUpdate + 1); 234 | sendTgramHeader("getUpdates", NULL, NULL, strlen(tgramBuff + FORM_OFFSET), NULL, NULL); 235 | } 236 | return false; // no response for app to process 237 | } 238 | 239 | bool sendTgramMessage(const char* info, const char* item, const char* parseMode) { 240 | // format message data as json, append to http header (buff overflow unlikely) 241 | char* t = tgramBuff + FORM_OFFSET; 242 | t += sprintf(t, POST_JSON, tgramChatId, tgramHdr, info, item); 243 | if (strlen(parseMode)) t += sprintf(t - 1, PARSE_MODE, parseMode); // overwrite previous '}' 244 | return sendTgramHeader("sendMessage", NULL, NULL, strlen(tgramBuff + FORM_OFFSET), NULL, NULL); 245 | } 246 | 247 | bool sendTgramPhoto(uint8_t* photoData, size_t photoSize, const char* caption) { 248 | // send photo stored in buffer to Telegram 249 | // max size of photo upload to Telegram is 10MB, bigger than ESP camera maximum 250 | if (sendTgramHeader("sendPhoto", "image/jpeg", "photo", photoSize, "frame.jpg", caption)) 251 | return sendTgramBuff(photoData, photoSize); 252 | return false; 253 | } 254 | 255 | bool sendTgramFile(const char* fileName, const char* contentType, const char* caption) { 256 | // retrieve identified file from selected storage and send to Telegram 257 | if (connectTelegram()) { 258 | fs::FS fp = STORAGE; 259 | File df = fp.open(fileName); 260 | char errMsg[100] = ""; 261 | if (df) { 262 | if (df.size() < MAX_TGRAM_SIZE) { 263 | sendTgramHeader("sendDocument", contentType, "document", df.size(), fileName, caption); 264 | // upload file content in chunks 265 | uint8_t percentLoaded = 0; 266 | size_t chunksize = 0, totalSent = 0; 267 | while ((chunksize = df.read((uint8_t*)tgramBuff, MAX_HTTP_MSG))) { 268 | tclient.write((uint8_t*)tgramBuff, chunksize); 269 | totalSent += chunksize; 270 | if (calcProgress(totalSent, df.size(), 5, percentLoaded)) LOG_INF("Downloaded %u%%", percentLoaded); 271 | } 272 | df.close(); 273 | tclient.println(END_BOUNDARY); 274 | } else snprintf(errMsg, sizeof(errMsg) - 1, "File size too large: %s", fmtSize(df.size())); 275 | } else snprintf(errMsg, sizeof(errMsg) - 1, "File does not exist or cannot be opened: %s", fileName); 276 | if (strlen(errMsg)) { 277 | LOG_WRN("%s", errMsg); 278 | sendTgramMessage("ERROR: ", errMsg, ""); 279 | } 280 | } else return false; 281 | return true; 282 | } 283 | 284 | #endif 285 | -------------------------------------------------------------------------------- /telemetry.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Telemetry data recorded to storage during camera recording 3 | // Formatted as CSV file for presentation in spreadsheet 4 | // and as a SRT file to provide video subtitles when used with a media player 5 | // Sensor data obtained from user supplied libraries and code 6 | // Need to check 'Use telemetry recording' under Peripherals button on Edit Config web page 7 | // and have downloaded relevant device libraries. 8 | // Best used on ESP32S3, not tested on ESP32 9 | 10 | // s60sc 2023, 2024 11 | 12 | #include "appGlobals.h" 13 | 14 | #if INCLUDE_TELEM 15 | #if !INCLUDE_I2C 16 | #error "Need INCLUDE_I2C true" 17 | #endif 18 | 19 | // If separate I2C pins are not defined, then the telemetry I2C devices 20 | // share the camera I2C pins: SIOD_GPIO_NUM and SIOC_GPIO_NUM in camera_pins.h are shared 21 | 22 | #define NUM_BUFF 2 // CSV, SRT 23 | #define MAX_LINE_LEN 128 // adjust to be max size of formatted telemetry row 24 | 25 | TaskHandle_t telemetryHandle = NULL; 26 | bool teleUse = false; 27 | static int teleInterval = 1; 28 | static char* teleBuf[NUM_BUFF]; // csv and srt telemetry data buffers 29 | size_t highPoint[NUM_BUFF]; // indexes to buffers 30 | static bool capturing = false; 31 | static char teleFileName[FILE_NAME_LEN]; 32 | char srtBuffer[MAX_LINE_LEN]; // store each srt entry, for subtitle streaming 33 | char csvHeader[MAX_LINE_LEN]; // column headers for CSV file 34 | size_t srtBytes = 0; 35 | 36 | /*************** USER TO MODIFY CODE BELOW for REQUIRED SENSORS ******************/ 37 | 38 | // example code for BMx280 and MPU9250 I2C sensors 39 | // if using GY-91 board (combination BMP280 + MPU9250), 40 | // then in periphsI2C.cpp, set both USE_BMx280 and USE_MPU9250 to true 41 | // GY-91 best powered via 5V to VIN (using internal LDO) than direct 3V3 42 | 43 | // user defined CSV header row per device used, must start with a comma 44 | #define BME_CSV ",Temperature (C),Humidity (%),Pressure (mb),Altitude (m)" 45 | #define BMP_CSV ",Temperature (C),Pressure (mb),Altitude (m)" 46 | #define MPU_CSV ",Heading,Pitch,Roll" 47 | // user defined SRT content line per device used, must start with 2 spaces 48 | #define BME_SRT " %0.1fC %0.1fRH %0.1fmb %0.1fm" 49 | #define BMP_SRT " %0.1fC %0.1fmb %0.1fm" 50 | #define MPU_SRT " %0.1f %0.1f %0.1f" 51 | 52 | static bool isBME = false; 53 | static bool haveBMX = false; 54 | static bool haveMPU = false; 55 | 56 | static bool setupSensors() { 57 | // setup required sensors 58 | bool res = false; 59 | #if USE_BMx280 60 | if (checkI2Cdevice("BMx280")) { 61 | bool isBME = identifyBMx(); 62 | LOG_INF("%s available", isBME ? "BME280" : "BMP280"); 63 | if (isBME) strncat(csvHeader, BME_CSV, MAX_LINE_LEN - strlen(csvHeader) - 1); 64 | else strncat(csvHeader, BMP_CSV, MAX_LINE_LEN - strlen(csvHeader) - 1); 65 | haveBMX = res = true; 66 | } else LOG_WRN("%s not available", isBME ? "BME280" : "BMP280"); 67 | #endif 68 | 69 | #if USE_MPU9250 70 | if (checkI2Cdevice("MPU9250")) { 71 | LOG_INF("MPU9250 available"); 72 | strncat(csvHeader, MPU_CSV, MAX_LINE_LEN - strlen(csvHeader) - 1); 73 | haveMPU = res = true; 74 | } else LOG_WRN("MPU9250 not available"); 75 | #endif 76 | return res; 77 | } 78 | 79 | static void getSensorData() { 80 | // get sensor data and format as csv row & srt entry in buffers 81 | #if USE_BMx280 82 | if (haveBMX) { 83 | float* bmxData = getBMx280(); 84 | if (isBME) { 85 | highPoint[0] += sprintf(teleBuf[0] + highPoint[0], ",%0.1f,%0.1f,%0.1f,%0.1f", bmxData[0], bmxData[3], bmxData[1], bmxData[2]); 86 | highPoint[1] += sprintf(teleBuf[1] + highPoint[1], BME_SRT, bmxData[0], bmxData[3], bmxData[1], bmxData[2]); 87 | } else { 88 | highPoint[0] += sprintf(teleBuf[0] + highPoint[0], ",%0.1f,%0.1f,%0.1f", bmxData[0], bmxData[1], bmxData[2]); 89 | highPoint[1] += sprintf(teleBuf[1] + highPoint[1], BMP_SRT, bmxData[0], bmxData[1], bmxData[2]); 90 | } 91 | #if INCLUDE_MQTT 92 | if (mqtt_active) { 93 | sprintf(jsonBuff, "{\"Temp\":\"%0.1f\", \"TIME\":\"%s\"}", bmxData[0], esp_log_system_timestamp()); 94 | mqttPublish(jsonBuff); 95 | } 96 | #endif 97 | } 98 | #endif 99 | 100 | #if USE_MPU9250 101 | if (haveMPU) { 102 | float* mpuData = getMPU9250(); 103 | highPoint[0] += sprintf(teleBuf[0] + highPoint[0], ",%0.1f,%0.1f,%0.1f", mpuData[0], mpuData[1], mpuData[2]); 104 | highPoint[1] += sprintf(teleBuf[1] + highPoint[1], MPU_SRT, mpuData[0], mpuData[1], mpuData[2]); 105 | } 106 | #endif 107 | } 108 | 109 | /*************** LEAVE CODE BELOW AS IS UNLESS YOU KNOW WHAT YOUR DOING ******************/ 110 | 111 | void storeSensorData(bool fromStream) { 112 | // can be called from telemetry task or streaming task 113 | if (fromStream) { 114 | // called fron streaming task 115 | if (capturing) return; // as being stored by telemetry task 116 | else highPoint[0] = highPoint[1] = 0; 117 | } 118 | size_t startData = highPoint[1]; 119 | getSensorData(); 120 | if (!srtBytes) { 121 | srtBytes = min(highPoint[1] - startData, (size_t)MAX_LINE_LEN); 122 | memcpy(srtBuffer, teleBuf[1] + startData, srtBytes); 123 | } 124 | } 125 | 126 | static void telemetryTask(void* pvParameters) { 127 | while (true) { 128 | ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 129 | capturing = true; 130 | int srtSeqNo = 1; 131 | uint32_t srtTime = 0; 132 | char timeStr[10]; 133 | uint32_t sampleInterval = 1000 * (teleInterval < 1 ? 1 : teleInterval); 134 | // open storage file 135 | if (STORAGE.exists(TELETEMP)) STORAGE.remove(TELETEMP); 136 | if (STORAGE.exists(SRTTEMP)) STORAGE.remove(SRTTEMP); 137 | File teleFile = STORAGE.open(TELETEMP, FILE_WRITE); 138 | File srtFile = STORAGE.open(SRTTEMP, FILE_WRITE); 139 | // write CSV header row to buffer 140 | highPoint[0] = sprintf(teleBuf[0], "Time%s\n", csvHeader); 141 | highPoint[1] = 0; 142 | 143 | // loop while camera recording 144 | while (capturing) { 145 | uint32_t startTime = millis(); 146 | // write header for this subtitle 147 | formatElapsedTime(timeStr, srtTime, true); 148 | highPoint[1] += sprintf(teleBuf[1] + highPoint[1], "%d\n%s,000 --> ", srtSeqNo++, timeStr); 149 | srtTime += sampleInterval; 150 | formatElapsedTime(timeStr, srtTime, true); 151 | highPoint[1] += sprintf(teleBuf[1] + highPoint[1], "%s,000\n", timeStr); 152 | // write current time for csv row and srt entry 153 | time_t currEpoch = getEpoch(); 154 | for (int i = 0; i < NUM_BUFF; i++) highPoint[i] += strftime(teleBuf[i] + highPoint[i], 10, "%H:%M:%S", localtime(&currEpoch)); 155 | // get and store data from sensors 156 | storeSensorData(false); 157 | // add newline to finish row 158 | highPoint[0] += sprintf(teleBuf[0] + highPoint[0], "\n"); 159 | highPoint[1] += sprintf(teleBuf[1] + highPoint[1], "\n\n"); 160 | 161 | // if marker overflows buffer, write to storage 162 | for (int i = 0; i < NUM_BUFF; i++) { 163 | if (highPoint[i] >= RAMSIZE) { 164 | highPoint[i] -= RAMSIZE; 165 | if (i) srtFile.write((uint8_t*)teleBuf[i], RAMSIZE); 166 | else teleFile.write((uint8_t*)teleBuf[i], RAMSIZE); 167 | // push overflow to buffer start 168 | memcpy(teleBuf[i], teleBuf[i]+RAMSIZE, highPoint[i]); 169 | } 170 | } 171 | // wait for next collection interval 172 | while (millis() - sampleInterval < startTime) delay(10); 173 | } 174 | 175 | // capture finished, write remaining buff to storage 176 | if (highPoint[0]) teleFile.write((uint8_t*)teleBuf[0], highPoint[0]); 177 | if (highPoint[1]) srtFile.write((uint8_t*)teleBuf[1], highPoint[1]); 178 | teleFile.close(); 179 | srtFile.close(); 180 | // rename temp files to specific file names using avi file name with relevant extension 181 | changeExtension(teleFileName, CSV_EXT); 182 | STORAGE.rename(TELETEMP, teleFileName); 183 | changeExtension(teleFileName, SRT_EXT); 184 | STORAGE.rename(SRTTEMP, teleFileName); 185 | LOG_INF("Saved %d entries in telemetry files", srtSeqNo); 186 | } 187 | } 188 | 189 | void prepTelemetry() { 190 | // called by app initialisation 191 | if (teleUse) { 192 | teleInterval = srtInterval; 193 | for (int i=0; i < NUM_BUFF; i++) teleBuf[i] = psramFound() ? (char*)ps_malloc(RAMSIZE + MAX_LINE_LEN) : (char*)malloc(RAMSIZE + MAX_LINE_LEN); 194 | if (setupSensors()) xTaskCreate(&telemetryTask, "telemetryTask", TELEM_STACK_SIZE, NULL, TELEM_PRI, &telemetryHandle); 195 | else teleUse = false; 196 | LOG_INF("Telemetry recording %s available", teleUse ? "is" : "NOT"); 197 | debugMemory("prepTelemetry"); 198 | } 199 | } 200 | 201 | bool startTelemetry() { 202 | // called when camera recording started 203 | bool res = true; 204 | if (teleUse && telemetryHandle != NULL) xTaskNotifyGive(telemetryHandle); // wake up task 205 | else res = false; 206 | return res; 207 | } 208 | 209 | void stopTelemetry(const char* fileName) { 210 | // called when camera recording stopped 211 | if (teleUse) strcpy(teleFileName, fileName); 212 | capturing = false; // stop task 213 | } 214 | 215 | #endif 216 | -------------------------------------------------------------------------------- /uart.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Use Uart interface to communicate between client ESP and auxiliary ESP 3 | // to support peripherals that cannot be hosted by client 4 | // 5 | // Connect auxiliary UART_TXD_PIN pin to client UART_RXD_PIN pin 6 | // Connect auxiliary UART_RXD_PIN pin to client UART_TXD_PIN pin 7 | // Also connect a common GND 8 | // The UART id and pins used are defined using the web page 9 | // 10 | // The data exchanged consists of 8 bytes: 11 | // - 2 byte fixed header 12 | // - 1 byte command char 13 | // - 4 bytes are data of any type that fits in 32 bits or less 14 | // - 1 byte checksum 15 | // 16 | // Callbacks: 17 | // - setOutputPeripheral(): on Auxiliary, convert uint32_t data read from uart into appropriate output peripheral data type and write to peripheral 18 | // - getInputPeripheral(): on Auxiliary, read input peripheral and convert input data type to uint32_t to send over uart 19 | // - setInputPeripheral(): on client, convert uint32_t read from UART to input status data type 20 | // 21 | // s60sc 2022, 2024 22 | 23 | #include "appGlobals.h" 24 | 25 | #if INCLUDE_UART 26 | #if ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3, 1, 0) 27 | #error uart.cpp must be compiled with arduino-esp32 core v3.1.0 or higher 28 | #endif 29 | #include "driver/uart.h" 30 | 31 | // UART pins 32 | #define UART_RTS UART_PIN_NO_CHANGE 33 | #define UART_CTS UART_PIN_NO_CHANGE 34 | 35 | #define UART_BAUD_RATE 115200 36 | #define BUFF_LEN (UART_FIFO_LEN * 2) 37 | #define MSG_LEN 8 38 | 39 | // UART connection for Auxiliary 40 | int uartTxdPin; 41 | int uartRxdPin; 42 | 43 | TaskHandle_t uartRxHandle = NULL; 44 | static QueueHandle_t uartQueue = NULL; 45 | static SemaphoreHandle_t responseMutex = NULL; 46 | static SemaphoreHandle_t writeMutex = NULL; 47 | static uart_event_t uartEvent; 48 | static byte uartBuffTx[BUFF_LEN]; 49 | static byte uartBuffRx[BUFF_LEN]; 50 | static const char* uartErr[] = {"FRAME_ERR", "PARITY_ERR", "UART_BREAK", "DATA_BREAK", 51 | "BUFFER_FULL", "FIFO_OVF", "UART_DATA", "PATTERN_DET", "EVENT_MAX"}; 52 | static const uint16_t header = 0x55aa; 53 | static uart_port_t uartId; 54 | 55 | static bool readUart() { 56 | // Read data from the UART when available 57 | // wait until event occurs 58 | if (xQueueReceive(uartQueue, (void*)&uartEvent, (TickType_t)portMAX_DELAY)) { 59 | if (uartEvent.type != UART_DATA) { 60 | xQueueReset(uartQueue); 61 | uart_flush_input(uartId); 62 | LOG_WRN("Unexpected uart event type: %s", uartErr[uartEvent.type]); 63 | delay(1000); 64 | return false; 65 | } else { 66 | // uart rx data available, wait till have full message 67 | int msgLen = 0; 68 | while (msgLen < MSG_LEN) { 69 | uart_get_buffered_data_len(uartId, (size_t*)&msgLen); 70 | delay(10); 71 | } 72 | heartBeatDone = true; // implied heartbeat 73 | msgLen = uart_read_bytes(uartId, uartBuffRx, msgLen, pdMS_TO_TICKS(20)); 74 | uint16_t* rxPtr = (uint16_t*)uartBuffRx; 75 | if (rxPtr[0] != header) { 76 | // ignore data that received from client when it reboots if using UART0 77 | return false; 78 | } 79 | // valid message header, check if content ok 80 | byte checkSum = 0; // checksum is modulo 256 of data content summation 81 | for (int i = 0; i < MSG_LEN - 1; i++) checkSum += uartBuffRx[i]; 82 | if (checkSum != uartBuffRx[MSG_LEN - 1]) { 83 | LOG_WRN("Invalid message ignored, got checksum %02x, expected %02x", uartBuffRx[MSG_LEN - 1], checkSum); 84 | return false; 85 | } 86 | } 87 | } 88 | return true; 89 | } 90 | 91 | bool writeUart(uint8_t cmd, uint32_t outputData) { 92 | // prep and write request to uart 93 | xSemaphoreTake(writeMutex, portMAX_DELAY); 94 | // load uart TX buffer with peripheral data to send 95 | memcpy(uartBuffTx, &header, 2); 96 | uartBuffTx[2] = cmd; 97 | memcpy(uartBuffTx + 3, &outputData, 4); 98 | uartBuffTx[MSG_LEN - 1] = 0; // checksum is modulo 256 of data content summation 99 | for (int i = 0; i < MSG_LEN - 1; i++) uartBuffTx[MSG_LEN - 1] += uartBuffTx[i]; 100 | bool res = uart_write_bytes(uartId, uartBuffTx, MSG_LEN) > 0 ? true : false; 101 | xSemaphoreGive(writeMutex); 102 | return res; 103 | } 104 | 105 | static bool configureUart() { 106 | // Configure parameters of UART driver 107 | uart_config_t uart_config = { 108 | .baud_rate = UART_BAUD_RATE, 109 | .data_bits = UART_DATA_8_BITS, 110 | .parity = UART_PARITY_DISABLE, 111 | .stop_bits = UART_STOP_BITS_1, 112 | .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, 113 | .rx_flow_ctrl_thresh = 122, 114 | #if CONFIG_IDF_TARGET_ESP32 115 | .source_clk = UART_SCLK_REF_TICK, 116 | #endif 117 | }; 118 | 119 | // install the driver and configure pins 120 | #if CONFIG_IDF_TARGET_ESP32C3 121 | uartId = UART_NUM_1; 122 | #else // ESP32, ESP32S3 123 | uartId = UART_NUM_2; 124 | #endif 125 | esp_err_t res = uart_driver_install(uartId, BUFF_LEN, BUFF_LEN, 20, &uartQueue, 0); 126 | if (res == ESP_OK) res = uart_param_config(uartId, &uart_config); 127 | if (res == ESP_OK) res = uart_set_pin(uartId, uartTxdPin, uartRxdPin, UART_RTS, UART_CTS); 128 | if (res != ESP_OK) LOG_WRN("UART config failed: %s", espErrMsg(res)); 129 | return (res == ESP_OK) ? true : false; 130 | } 131 | 132 | static void uartRxTask(void *arg) { 133 | // used by auxiliary to receive data from uart 134 | while (true) { 135 | // wait for response to previous request to be processed 136 | xSemaphoreTake(responseMutex, portMAX_DELAY); 137 | if (readUart()) { 138 | // update given peripheral status 139 | uint32_t receivedData; 140 | memcpy(&receivedData, uartBuffRx + 3, 4); // response data (if relevant) 141 | #ifdef AUXILIARY 142 | // try output request 143 | if (!setOutputPeripheral(uartBuffRx[2], receivedData)) { 144 | // try input request 145 | int receivedData = getInputPeripheral(uartBuffRx[2]); // cmd 146 | // write response to client 147 | if (receivedData >= 0) writeUart(uartBuffRx[2], (uint32_t)receivedData); // cmd, data 148 | } 149 | #else 150 | // client, process received input 151 | setInputPeripheral(uartBuffRx[2], receivedData); 152 | #endif 153 | } 154 | xSemaphoreGive(responseMutex); 155 | } 156 | } 157 | 158 | void prepUart() { 159 | // setup uart if Auxiliary being used 160 | if (useUart) { 161 | if (uartTxdPin && uartRxdPin) { 162 | LOG_INF("Prepare UART on pins Tx %d, Rx %d", uartTxdPin, uartRxdPin); 163 | responseMutex = xSemaphoreCreateMutex(); 164 | writeMutex = xSemaphoreCreateMutex(); 165 | if (configureUart()) { 166 | #ifdef USE_UARTTASK 167 | xSemaphoreTake(responseMutex, portMAX_DELAY); 168 | xTaskCreate(uartRxTask, "uartRxTask", UART_STACK_SIZE, NULL, UART_PRI, &uartRxHandle); 169 | #endif 170 | xSemaphoreGive(responseMutex); 171 | xSemaphoreGive(writeMutex); 172 | } 173 | } else LOG_WRN("At least one uart pin not defined"); 174 | } 175 | } 176 | 177 | #endif 178 | -------------------------------------------------------------------------------- /webDav.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Using the WebDAV server: 4 | Windows 10: 5 | - Windows file explorer, in address bar enter: /webdav 6 | - Map Network Drive, connect to: \\\webdav 7 | Windows 11: 8 | - Map Network Drive: 9 | - connect to: \\\webdav 10 | - Click on the link “Connect to a web site that you can use to store your documents and pictures.” 11 | - Click “Next” and then “Choose a custom network location.” 12 | - Re-enter \\\webdav 13 | 14 | Android: 15 | - Solid Explorer, enter for Remote host name, webdav for Path 16 | 17 | Not tested: 18 | MacOS: 19 | - Finder: command-K > http:///webdav (do not select anonymous to have write access) 20 | - cmdline: mkdir -p /tmp/esp; mount_webdav -S -i -v esp32 /webdav /tmp/esp && echo OK 21 | 22 | linux: 23 | - mount -t davs2 http:///webdav /mnt/ 24 | - gio/gvfs/nautilus/YourFileExplorer http:///webdav 25 | 26 | Uses ideas from https://github.com/d-a-v/ESPWebDAV 27 | 28 | s60sc 2024 29 | */ 30 | 31 | #include "appGlobals.h" 32 | 33 | #if INCLUDE_WEBDAV 34 | #define ALLOW "PROPPATCH,PROPFIND,OPTIONS,DELETE,MOVE,COPY,HEAD,POST,PUT,GET" 35 | #define XML1 "" 36 | #define XML2 "" 37 | #define XML3 "HTTP/1.1 200 OK" 38 | #define XML4 "" 39 | #define XML5 "" 40 | #define XML6 "" 41 | 42 | static char pathName[IN_FILE_NAME_LEN]; 43 | static httpd_req_t* req; 44 | static char formattedTime[80]; 45 | static const char* extensions[] = {"dummy", ".htm", ".css", ".txt", ".js", ".json", ".png", ".gif", ".jpg", ".ico", ".svg", ".xml", ".pdf", ".zip", ".gz"}; 46 | static const char* mimeTypes[] = {"application/octet-stream", "text/html", "text/html", "text/css", "text/plain", "application/javascript", "application/json", "image/png", "image/gif", "image/jpeg", "image/x-icon", "image/svg+xml", "text/xml", "application/pdf", "application/zip", "application/x-gzip"}; 47 | 48 | static int getMimeType(const char* path) { 49 | // determine mime type for give file extension 50 | int mimePtr = 1; 51 | size_t len = strlen(path); 52 | for (const char* ext : extensions) { 53 | size_t slen = strlen(ext); 54 | if (!strncmp(path + len - slen, ext, slen)) return mimePtr; 55 | mimePtr++; 56 | } 57 | return 0; // default mime type 58 | } 59 | 60 | static void formatTime(time_t t) { 61 | // format time for XML property values 62 | tm* timeinfo = gmtime(&t); 63 | strftime(formattedTime, sizeof(formattedTime), "%a, %d %b %Y %H:%M:%S %Z", timeinfo); 64 | } 65 | 66 | static bool haveResource(bool ignore = false) { 67 | // check if file or folder exists 68 | if (STORAGE.exists(pathName)) return true; 69 | else if (!ignore) httpd_resp_send_404(req); 70 | return false; 71 | } 72 | 73 | static bool isFolder() { 74 | // identify if resource is file of folder 75 | File root = STORAGE.open(pathName); 76 | bool res = root.isDirectory(); 77 | root.close(); 78 | return res; 79 | } 80 | 81 | static void sendContentProp(const char* prop, const char* value) { 82 | // set individual XML properties in response 83 | char propStr[strlen(prop) * 2 + strlen(value) + 15]; 84 | sprintf(propStr, "%s", prop, value, prop); 85 | httpd_resp_sendstr_chunk(req, propStr); 86 | LOG_VRB("propStr %s", propStr); 87 | } 88 | 89 | static void sendPropResponse(File& file, const char* payload) { 90 | // send SD properties details to PC 91 | size_t encodeLen = 3 + strlen(file.path()) * 2; 92 | size_t maxLen = strlen(XML2) + encodeLen + strlen(XML3); 93 | char resp[maxLen + 1]; 94 | snprintf(resp, maxLen, "%s%s%s", XML2, file.path(), XML3); 95 | httpd_resp_sendstr_chunk(req, resp); 96 | LOG_VRB("resp xml: %s", resp); 97 | 98 | formatTime(file.getLastWrite()); 99 | sendContentProp("getlastmodified", formattedTime); 100 | sendContentProp("creationdate", formattedTime); 101 | 102 | if (file.isDirectory()) sendContentProp("resourcetype", ""); 103 | else { 104 | char fsizeStr[15]; 105 | sprintf(fsizeStr, "%u", file.size()); 106 | sendContentProp("getcontentlength", fsizeStr); 107 | sendContentProp("getcontenttype", mimeTypes[getMimeType(file.path())]); 108 | httpd_resp_sendstr_chunk(req, ""); 109 | } 110 | sendContentProp("displayname", file.name()); 111 | 112 | if (strlen(payload)) { 113 | // return quota data if requested 114 | if (strstr(payload, "quota-available-bytes") != NULL || strstr(payload, "quota-used-bytes") != NULL) { 115 | char numberStr[15]; 116 | sprintf(numberStr, "%llu", (uint64_t)STORAGE.totalBytes() - (uint64_t)STORAGE.usedBytes()); 117 | sendContentProp("quota-available-bytes", numberStr); 118 | sprintf(numberStr, "%llu", (uint64_t)STORAGE.usedBytes()); 119 | sendContentProp("quota-used-bytes", numberStr); 120 | } 121 | } 122 | httpd_resp_sendstr_chunk(req, XML4); 123 | } 124 | 125 | static bool getPayload(char* payload) { 126 | // get payload in PROPFIND message 127 | int bytesRead = -1; 128 | size_t offset = 0; 129 | size_t psize = req->content_len; 130 | if (psize) { 131 | do { 132 | bytesRead = httpd_req_recv(req, payload + offset, psize - offset); 133 | if (bytesRead < 0) { 134 | if (bytesRead == HTTPD_SOCK_ERR_TIMEOUT) { 135 | delay(10); 136 | continue; 137 | } else { 138 | LOG_WRN("Transfer request failed with status %i", bytesRead); 139 | psize = 0; 140 | break; 141 | } 142 | } else offset += bytesRead; 143 | } while (bytesRead > 0); 144 | payload[psize] = 0; 145 | LOG_VRB("payload: %s\n", payload); 146 | } 147 | return bytesRead < 0 ? false : true; 148 | } 149 | 150 | static bool handleProp() { 151 | // provide details of SD content to PC 152 | if (!haveResource()) return false; 153 | // get depth header 154 | bool depth = false; 155 | char value[10]; 156 | if (extractHeaderVal(req, "Depth", value) == ESP_OK) depth = (!strcmp(value, "0")) ? false : true; 157 | 158 | // get request payload content if present 159 | char payload[req->content_len + 1] = {0}; 160 | if (req->content_len) getPayload(payload); 161 | 162 | // common header 163 | httpd_resp_set_status(req, "207 Multi-Status"); 164 | httpd_resp_set_type(req, "application/xml;charset=utf-8"); 165 | httpd_resp_sendstr_chunk(req, XML1); 166 | 167 | // return details of selected folder 168 | File root = STORAGE.open(pathName); 169 | sendPropResponse(root, payload); 170 | if (depth && root.isDirectory()) { 171 | // if requested return details of each resource in folder 172 | File entry = root.openNextFile(); 173 | while (entry) { 174 | sendPropResponse(entry, ""); 175 | entry.close(); 176 | entry = root.openNextFile(); 177 | } 178 | } 179 | root.close(); 180 | httpd_resp_sendstr_chunk(req, ""); 181 | httpd_resp_sendstr_chunk(req, NULL); 182 | return true; 183 | } 184 | 185 | static bool handleOptions() { 186 | httpd_resp_sendstr(req, NULL); 187 | return true; 188 | } 189 | 190 | static bool handleGet() { 191 | // transfer file to PC 192 | if (!haveResource()) return false; 193 | if (isFolder()) { 194 | httpd_resp_send_404(req); 195 | return false; 196 | } else { 197 | httpd_resp_set_type(req, mimeTypes[getMimeType(pathName)]); 198 | strcpy(inFileName, pathName); 199 | esp_err_t res = fileHandler(req); // file content 200 | return res == ESP_OK ? true : false; 201 | } 202 | return true; 203 | } 204 | 205 | static bool handleHead() { 206 | if (!haveResource()) return false; 207 | httpd_resp_sendstr(req, NULL); 208 | return true; 209 | } 210 | 211 | static bool handleLock() { 212 | // provide (dummy) lock while file open 213 | const char* lockToken = "0123456789012345"; 214 | httpd_resp_set_hdr(req, "Lock-Token", lockToken); 215 | char resp[strlen(XML5) + strlen(lockToken) + strlen(XML6) + 1]; 216 | sprintf(resp, "%s%s%s", XML5, lockToken, XML6); 217 | httpd_resp_set_type(req, "application/xml;charset=utf-8"); 218 | httpd_resp_sendstr(req, resp); 219 | return true; 220 | } 221 | 222 | static bool handleUnlock() { 223 | // unlock file when closed 224 | httpd_resp_set_status(req, "204 No Content"); 225 | httpd_resp_sendstr(req, NULL); 226 | return true; 227 | } 228 | 229 | static bool handlePut() { 230 | // transfer file from PC 231 | if (isFolder()) return false; 232 | if (!haveResource(true) || !req->content_len) { 233 | // if no content, create file entry only 234 | File file = STORAGE.open(pathName, FILE_WRITE); 235 | file.close(); 236 | httpd_resp_set_status(req, "201 Created"); 237 | httpd_resp_sendstr(req, NULL); 238 | } 239 | if (req->content_len) { 240 | // transfer file content to SD 241 | strcpy(inFileName, pathName); 242 | esp_err_t res = uploadHandler(req); 243 | return res == ESP_OK ? true : false; 244 | } 245 | return true; 246 | } 247 | 248 | static bool handleDelete() { 249 | // delete file or folder 250 | if (!haveResource()) return false; 251 | // for this app, single folder level only 252 | deleteFolderOrFile(pathName); 253 | httpd_resp_sendstr(req, NULL); 254 | return true; 255 | } 256 | 257 | static bool handleMkdir() { 258 | // create new folder 259 | if (haveResource(true)) return false; // already exists 260 | bool res = STORAGE.mkdir(pathName); 261 | if (res) httpd_resp_set_status(req, "201 Created"); 262 | else httpd_resp_set_status(req, "500 Internal Server Error"); 263 | httpd_resp_sendstr(req, NULL); 264 | return res; 265 | } 266 | 267 | static bool checkSamePath(const char *source_path, const char *dest_path) { 268 | // Compare paths, excluding filenames 269 | char source_dir[strlen(source_path) + 1]; 270 | char dest_dir[strlen(dest_path) + 1]; 271 | strncpy(source_dir, source_path, strrchr(source_path, '/') - source_path); 272 | source_dir[strrchr(source_path, '/') - source_path] = 0; 273 | strncpy(dest_dir, dest_path, strrchr(dest_path, '/') - dest_path); 274 | dest_dir[strrchr(dest_path, '/') - dest_path] = 0; 275 | return strcmp(source_dir, dest_dir) == 0; 276 | } 277 | 278 | static bool handleMove() { 279 | // rename file or folder, or change file location 280 | bool res = false; 281 | char dest[100]; 282 | if (extractHeaderVal(req, "Destination", dest) == ESP_OK) { 283 | // obtain destination filename 284 | res = true; 285 | urlDecode(dest); 286 | char* pos = strstr(dest, WEBDAV); 287 | memmove(dest, pos + strlen(WEBDAV), strlen(dest)); 288 | 289 | // only allow renaming if a folder 290 | if (isFolder()) res = checkSamePath(pathName, dest); 291 | if (res) { 292 | res = STORAGE.rename(pathName, dest); 293 | if (res) httpd_resp_set_status(req, "201 Created"); 294 | else httpd_resp_set_status(req, "500 Internal Server Error"); 295 | httpd_resp_sendstr(req, NULL); 296 | return true; 297 | } 298 | } 299 | httpd_resp_send_404(req); 300 | return false; 301 | } 302 | 303 | static bool handleCopy() { 304 | // copy folder - not implemented 305 | // files can be copied by copy / paste actions 306 | httpd_resp_send_404(req); 307 | return false; 308 | } 309 | 310 | bool handleWebDav(httpd_req_t* rreq) { 311 | // extract method to determine which WebDAV action to take 312 | //showHttpHeaders(rreq); 313 | req = rreq; 314 | sprintf(pathName, "%s", req->uri + strlen(WEBDAV)); // strip out "/webdav" 315 | if (pathName[strlen(pathName) - 1] == '/') pathName[strlen(pathName) - 1] = 0; // remove final / if present 316 | if (!strlen(pathName)) strcpy(pathName, "/"); // if pathname empty, use single / 317 | urlDecode(pathName); 318 | // common response header 319 | httpd_resp_set_hdr(req, "DAV", "1"); 320 | httpd_resp_set_hdr(req, "Allow", ALLOW); 321 | 322 | switch(req->method) { 323 | case HTTP_PUT: return handlePut(); // file create/uploads 324 | case HTTP_PROPFIND: return handleProp(); // get file or directory properties 325 | case HTTP_PROPPATCH: return handleProp(); // set file or directory properties 326 | case HTTP_GET: return handleGet(); // file downloads 327 | case HTTP_HEAD: return handleHead(); // file properties 328 | case HTTP_OPTIONS: return handleOptions(); // supported options 329 | case HTTP_LOCK: return handleLock(); // open file lock 330 | case HTTP_UNLOCK: return handleUnlock(); // close file lock 331 | case HTTP_MKCOL: return handleMkdir(); // folder creation 332 | case HTTP_MOVE: return handleMove(); // rename or move file or directory 333 | case HTTP_DELETE: return handleDelete(); // delete a file or directory 334 | case HTTP_COPY: return handleCopy(); // copy a file or directory 335 | default: { 336 | LOG_ERR("Unhandled method %s", HTTP_METHOD_STRING(req->method)); 337 | httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Unhandled method"); 338 | return false; 339 | } 340 | } 341 | return true; 342 | } 343 | 344 | #endif 345 | --------------------------------------------------------------------------------