From 87c1241f0e6311413da0926ed920d1ca9bb24ab5 Mon Sep 17 00:00:00 2001 From: jameszah <36938190+jameszah@users.noreply.github.com> Date: Wed, 26 Feb 2020 15:25:49 -0700 Subject: [PATCH] Add files via upload --- v60/ESP32FtpServer.cpp | 1169 +++++++++++++++++++++++++++++ v60/ESP32FtpServer.h | 112 +++ v60/TimeLapseAvi60x.ino | 1542 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 2823 insertions(+) create mode 100644 v60/ESP32FtpServer.cpp create mode 100644 v60/ESP32FtpServer.h create mode 100644 v60/TimeLapseAvi60x.ino diff --git a/v60/ESP32FtpServer.cpp b/v60/ESP32FtpServer.cpp new file mode 100644 index 0000000..e720163 --- /dev/null +++ b/v60/ESP32FtpServer.cpp @@ -0,0 +1,1169 @@ +/* + * FTP Serveur for ESP8266 + * based on FTP Serveur for Arduino Due and Ethernet shield (W5100) or WIZ820io (W5200) + * based on Jean-Michel Gallego's work + * modified to work with esp8266 SPIFFS by David Paiva david@nailbuster.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// 2017: modified by @robo8080 +// 2019: modified by @fa1ke5 + +#include "ESP32FtpServer.h" + +#include +//#include +#include +#include "SD_MMC.h" +//#include "SPI.h" + +#include / //jz feb2020 + + +WiFiServer ftpServer( FTP_CTRL_PORT ); +WiFiServer dataServer( FTP_DATA_PORT_PASV ); + +void FtpServer::begin(String uname, String pword) +{ + // Tells the ftp server to begin listening for incoming connection + _FTP_USER=uname; + _FTP_PASS = pword; + + ftpServer.begin(); + delay(10); + dataServer.begin(); + delay(10); + millisTimeOut = (uint32_t)FTP_TIME_OUT * 60 * 1000; + millisDelay = 0; + cmdStatus = 0; + iniVariables(); +} + +void FtpServer::iniVariables() +{ + // Default for data port + dataPort = FTP_DATA_PORT_PASV; + + // Default Data connection is Active + dataPassiveConn = true; + + // Set the root directory + strcpy( cwdName, "/" ); + + rnfrCmd = false; + transferStatus = 0; + +} + +void FtpServer::handleFTP() +{ + if((int32_t) ( millisDelay - millis() ) > 0 ) + return; + + if (ftpServer.hasClient()) { +// if (ftpServer.available()) { + client.stop(); + client = ftpServer.available(); + } + + if( cmdStatus == 0 ) + { + if( client.connected()) + disconnectClient(); + cmdStatus = 1; + } + else if( cmdStatus == 1 ) // Ftp server waiting for connection + { + abortTransfer(); + iniVariables(); + #ifdef FTP_DEBUG + Serial.println("Ftp server waiting for connection on port "+ String(FTP_CTRL_PORT)); + #endif + cmdStatus = 2; + } + else if( cmdStatus == 2 ) // Ftp server idle + { + + if( client.connected() ) // A client connected + { + clientConnected(); + millisEndConnection = millis() + 10 * 1000 ; // wait client id during 10 s. + cmdStatus = 3; + } + } + else if( readChar() > 0 ) // got response + { + if( cmdStatus == 3 ) // Ftp server waiting for user identity + if( userIdentity() ) + cmdStatus = 4; + else + cmdStatus = 0; + else if( cmdStatus == 4 ) // Ftp server waiting for user registration + if( userPassword() ) + { + cmdStatus = 5; + millisEndConnection = millis() + millisTimeOut; + } + else + cmdStatus = 0; + else if( cmdStatus == 5 ) // Ftp server waiting for user command + if( ! processCommand()) + cmdStatus = 0; + else + millisEndConnection = millis() + millisTimeOut; + } + else if (!client.connected() || !client) + { + cmdStatus = 1; + #ifdef FTP_DEBUG + Serial.println("client disconnected"); + #endif + } + + if( transferStatus == 1 ) // Retrieve data + { + if( ! doRetrieve()) + transferStatus = 0; + } + else if( transferStatus == 2 ) // Store data + { + if( ! doStore()) + transferStatus = 0; + } + else if( cmdStatus > 2 && ! ((int32_t) ( millisEndConnection - millis() ) > 0 )) + { + client.println("530 Timeout"); + millisDelay = millis() + 200; // delay of 200 ms + cmdStatus = 0; + } +} + +void FtpServer::clientConnected() +{ + #ifdef FTP_DEBUG + Serial.println("Client connected!"); + #endif + client.println( "220--- Welcome to FTP for ESP8266 ---"); + client.println( "220--- By David Paiva ---"); + client.println( "220 -- Version "+ String(FTP_SERVER_VERSION) +" --"); + iCL = 0; +} + +void FtpServer::disconnectClient() +{ + #ifdef FTP_DEBUG + Serial.println(" Disconnecting client"); + #endif + abortTransfer(); + client.println("221 Goodbye"); + client.stop(); +} + +boolean FtpServer::userIdentity() +{ + if( strcmp( command, "USER" )) + client.println( "500 Syntax error"); + if( strcmp( parameters, _FTP_USER.c_str() )) + client.println( "530 user not found"); + else + { + client.println( "331 OK. Password required"); + strcpy( cwdName, "/" ); + return true; + } + millisDelay = millis() + 100; // delay of 100 ms + return false; +} + +boolean FtpServer::userPassword() +{ + if( strcmp( command, "PASS" )) + client.println( "500 Syntax error"); + else if( strcmp( parameters, _FTP_PASS.c_str() )) + client.println( "530 "); + else + { + #ifdef FTP_DEBUG + Serial.println( "OK. Waiting for commands."); + #endif + client.println( "230 OK."); + return true; + } + millisDelay = millis() + 100; // delay of 100 ms + return false; +} + +boolean FtpServer::processCommand() +{ + /////////////////////////////////////// + // // + // ACCESS CONTROL COMMANDS // + // // + /////////////////////////////////////// + + // + // CDUP - Change to Parent Directory + // + if( ! strcmp( command, "CDUP" ) || ( ! strcmp( command, "CWD" ) && ! strcmp( parameters, ".." ))) + { + bool ok = false; + if( strlen( cwdName ) > 1 ) // do nothing if cwdName is root + { + // if cwdName ends with '/', remove it (must not append) + if( cwdName[ strlen( cwdName ) - 1 ] == '/' ) + cwdName[ strlen( cwdName ) - 1 ] = 0; + // search last '/' + char * pSep = strrchr( cwdName, '/' ); + ok = pSep > cwdName; + // if found, ends the string on its position + if( ok ) + { + * pSep = 0; + ok = SD_MMC.exists( cwdName ); + } + } + // if an error appends, move to root + if( ! ok ) + strcpy( cwdName, "/" ); + // client << F("250 Ok. Current directory is ") << cwdName << eol; + + client.println("250 Ok. Current directory is " + String(cwdName)); + } + // + // CWD - Change Working Directory + // + else if( ! strcmp( command, "CWD" )) + { + + + char path[ FTP_CWD_SIZE ]; + if( haveParameter() && makeExistsPath( path )) + { + strcpy( cwdName, path ); + client.println( "250 Ok. Current directory is " + String(cwdName) ); + } + + + //Serial.print("********************************************cwdName: ");Serial.println(String(cwdName)); + /* + char path[ FTP_CWD_SIZE ]; + if( strcmp( parameters, "." ) == 0 ){ // 'CWD .' is the same as PWD command + + client.println( "257 \"" + String(cwdName) + "\" is your current directory"); + */ + // Serial.print("********************************************cwdName: ");Serial.println(String(cwdName)); + /* + } + else + { + if( haveParameter() && makeExistsPath( path )){ + strcpy( cwdName, path ); + Serial.print("************************parameters: ");Serial.println(parameters); + + client.println( "250 Ok. Current directory is " + String(cwdName) ); + Serial.print("********************************************cwdName: ");Serial.println(String(cwdName)); + } + } + */ + } + // + // PWD - Print Directory + // + else if( ! strcmp( command, "PWD" )) + client.println( "257 \"" + String(cwdName) + "\" is your current directory"); + // + // QUIT + // + else if( ! strcmp( command, "QUIT" )) + { + disconnectClient(); + return false; + } + + /////////////////////////////////////// + // // + // TRANSFER PARAMETER COMMANDS // + // // + /////////////////////////////////////// + + // + // MODE - Transfer Mode + // + else if( ! strcmp( command, "MODE" )) + { + if( ! strcmp( parameters, "S" )) + client.println( "200 S Ok"); + // else if( ! strcmp( parameters, "B" )) + // client.println( "200 B Ok\r\n"; + else + client.println( "504 Only S(tream) is suported"); + } + // + // PASV - Passive Connection management + // + else if( ! strcmp( command, "PASV" )) + { + if (data.connected()) data.stop(); + //dataServer.begin(); + //dataIp = Ethernet.localIP(); + dataIp = WiFi.localIP(); + dataPort = FTP_DATA_PORT_PASV; + //data.connect( dataIp, dataPort ); + //data = dataServer.available(); + #ifdef FTP_DEBUG + Serial.println("Connection management set to passive"); + Serial.println( "Data port set to " + String(dataPort)); + #endif + client.println( "227 Entering Passive Mode ("+ String(dataIp[0]) + "," + String(dataIp[1])+","+ String(dataIp[2])+","+ String(dataIp[3])+","+String( dataPort >> 8 ) +","+String ( dataPort & 255 )+")."); + dataPassiveConn = true; + } + // + // PORT - Data Port + // + else if( ! strcmp( command, "PORT" )) + { + if (data) data.stop(); + // get IP of data client + dataIp[ 0 ] = atoi( parameters ); + char * p = strchr( parameters, ',' ); + for( uint8_t i = 1; i < 4; i ++ ) + { + dataIp[ i ] = atoi( ++ p ); + p = strchr( p, ',' ); + } + // get port of data client + dataPort = 256 * atoi( ++ p ); + p = strchr( p, ',' ); + dataPort += atoi( ++ p ); + if( p == NULL ) + client.println( "501 Can't interpret parameters"); + else + { + + client.println("200 PORT command successful"); + dataPassiveConn = false; + } + } + // + // STRU - File Structure + // + else if( ! strcmp( command, "STRU" )) + { + if( ! strcmp( parameters, "F" )) + client.println( "200 F Ok"); + // else if( ! strcmp( parameters, "R" )) + // client.println( "200 B Ok\r\n"; + else + client.println( "504 Only F(ile) is suported"); + } + // + // TYPE - Data Type + // + else if( ! strcmp( command, "TYPE" )) + { + if( ! strcmp( parameters, "A" )) + client.println( "200 TYPE is now ASII"); + else if( ! strcmp( parameters, "I" )) + client.println( "200 TYPE is now 8-bit binary"); + else + client.println( "504 Unknow TYPE"); + } + + /////////////////////////////////////// + // // + // FTP SERVICE COMMANDS // + // // + /////////////////////////////////////// + + // + // ABOR - Abort + // + else if( ! strcmp( command, "ABOR" )) + { + abortTransfer(); + client.println( "226 Data connection closed"); + } + // + // DELE - Delete a File + // + else if( ! strcmp( command, "DELE" )) + { + char path[ FTP_CWD_SIZE ]; + if( strlen( parameters ) == 0 ) + client.println( "501 No file name"); + else if( makePath( path )) + { + if( ! SD_MMC.exists( path )) + client.println( "550 File " + String(parameters) + " not found"); + else + { + if( SD_MMC.remove( path )) + client.println( "250 Deleted " + String(parameters) ); + else + client.println( "450 Can't delete " + String(parameters)); + } + } + } + // + // LIST - List + // + + + + else if( ! strcmp( command, "LIST" )) + { + if(dataConnect()){ + client.println( "150 Accepted data connection"); + uint16_t nm = 0; + File dir=SD_MMC.open(cwdName); + if((!dir)||(!dir.isDirectory())) + client.println( "550 Can't open directory " + String(cwdName) ); + else + { + File file = dir.openNextFile(); + while( file == 1) + { + String fn, fs; + fn = file.name(); + int i = fn.lastIndexOf("/")+1; + fn.remove(0, i); + #ifdef FTP_DEBUG + Serial.println("File Name = "+ fn); + #endif + fs = String(file.size()); + + /* jz feb2020 code from https://github.com/espressif/arduino-esp32/blob/master/libraries/SD_MMC/examples/SDMMC_time/SDMMC_time.ino + * to implement file dates and times for the esp32 ftp + + + Serial.print(" FILE: "); + Serial.print(file.name()); + Serial.print(" SIZE: "); + Serial.print(file.size()); + time_t t= file.getLastWrite(); + struct tm * tmstruct = localtime(&t); + Serial.printf(" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct->tm_year)+1900,( tmstruct->tm_mon)+1, tmstruct->tm_mday,tmstruct->tm_hour , tmstruct->tm_min, tmstruct->tm_sec); + + */ + + time_t t= file.getLastWrite(); //jz + struct tm * tmstruct = localtime(&t); //jz + + if(file.isDirectory()){ + // jz start + char the_date[26]; + sprintf(the_date, "%02d-%02d-%04d %02d:%02dAM ",( tmstruct->tm_mon)+1, tmstruct->tm_mday, (tmstruct->tm_year)+1900,tmstruct->tm_hour, tmstruct->tm_min); + data.println(the_date + fn); + //jz end + + //jz data.println( "01-01-2000 00:00AM " + fn); + //Serial.println( "01-01-2000 00:00AM " + fn); + } else { + // jz start + char the_date[26]; + sprintf(the_date, "%02d-%02d-%04d %02d:%02d ",( tmstruct->tm_mon)+1, tmstruct->tm_mday, (tmstruct->tm_year)+1900,tmstruct->tm_hour, tmstruct->tm_min); + data.println(the_date + fs + " " + fn); + //jz end + + //jz data.println( "01-01-2000 00:00AM " + fs + " " + fn); + //Serial.println( "01-01-2000 00:00AM " + fs + " " + fn); +// data.println( " " + fn ); + } + nm ++; + //Serial.println("... opening next"); + file = dir.openNextFile(); + //Serial.println(file); + if (file < 1 ) { + //Serial.println("BREAK!"); + break; + + } + } + client.println( "226 " + String(nm) + " matches total"); + Serial.println( "226 " + String(nm) + " matches total"); + data.stop(); //jz aug2019 + } + + } + else{ + client.println( "425 No data connection"); + data.stop(); + } + /* + + if( ! dataConnect()) + client.println( "425 No data connection"); + else + { + client.println( "150 Accepted data connection"); + uint16_t nm = 0; + File dir=SD_MMC.open(cwdName); + if((!dir)||(!dir.isDirectory())) + client.println( "550 Can't open directory " + String(cwdName) ); + else + { + File file = dir.openNextFile(); + while( file) + { + String fn, fs; + fn = file.name(); + int i = fn.lastIndexOf("/")+1; + fn.remove(0, i); + #ifdef FTP_DEBUG + Serial.println("File Name = "+ fn); + #endif + fs = String(file.size()); + if(file.isDirectory()){ + data.println( "01-01-2000 00:00AM " + fn); + } else { + data.println( "01-01-2000 00:00AM " + fs + " " + fn); +// data.println( " " + fn ); + } + nm ++; + file = dir.openNextFile(); + } + client.println( "226 " + String(nm) + " matches total"); + } + data.stop(); + } +*/ + + } + + // + // MLSD - Listing for Machine Processing (see RFC 3659) + // + else if( ! strcmp( command, "MLSD" )) + { + if( ! dataConnect()) + client.println( "425 No data connection MLSD"); + else + { + client.println( "150 Accepted data connection"); + uint16_t nm = 0; +// Dir dir= SD.openDir(cwdName); + File dir= SD_MMC.open(cwdName); + char dtStr[ 15 ]; + // if(!SD.exists(cwdName)) + if((!dir)||(!dir.isDirectory())) + client.println( "550 Can't open directory " +String(cwdName) ); +// client.println( "550 Can't open directory " +String(parameters) ); + else + { +// while( dir.next()) + File file = dir.openNextFile(); +// while( dir.openNextFile()) + while( file) + { + + String fn,fs; + fn = file.name(); + int pos = fn.lastIndexOf("/"); //ищем начало файла по последнему "/" + fn.remove(0, pos+1); //Удаляем все до имени файла включительно + fs = String(file.size()); + if(file.isDirectory()){ + + data.println(fn); +// data.println( "Type=dir;Size=" + fs + ";"+"modify=20000101000000;" +" " + fn); +// data.println( "Type=dir;modify=20000101000000; " + fn); + } else { + data.println( fs + " " + fn); + //data.println( "Type=file;Size=" + fs + ";"+"modify=20000101160656;" +" " + fn); + //data.println( "Type=file;Size=" + fs + ";"+"modify=20000101000000;" +" " + fn); + + } + nm ++; + file = dir.openNextFile(); + } + client.println( "226-options: -a -l"); + client.println( "226 " + String(nm) + " matches total"); + } + data.stop(); + } + } + // + // NLST - Name List + // + else if( ! strcmp( command, "NLST" )) + { + if( ! dataConnect()) + client.println( "425 No data connection"); + else + { + client.println( "150 Accepted data connection"); + uint16_t nm = 0; +// Dir dir=SD.openDir(cwdName); + File dir= SD_MMC.open(cwdName); + if( !SD_MMC.exists( cwdName )) + client.println( "550 Can't open directory " + String(parameters)); + else + { + File file = dir.openNextFile(); +// while( dir.next()) + while( file) + { +// data.println( dir.fileName()); + data.println( file.name()); + nm ++; + file = dir.openNextFile(); + } + client.println( "226 " + String(nm) + " matches total"); + } + data.stop(); + } + } + // + // NOOP + // + else if( ! strcmp( command, "NOOP" )) + { + // dataPort = 0; + client.println( "200 Zzz..."); + } + // + // RETR - Retrieve + // + else if( ! strcmp( command, "RETR" )) + { + char path[ FTP_CWD_SIZE ]; + if( strlen( parameters ) == 0 ) + client.println( "501 No file name"); + else if( makePath( path )) + { + file = SD_MMC.open(path, "r"); + //Serial.println("open the file"); + if( !file) { + client.println( "550 File " +String(parameters)+ " not found"); + Serial.println("550"); + } + else if( !file ) { + client.println( "450 Can't open " +String(parameters)); + Serial.println("450"); + } + else if( ! dataConnect()) { + client.println( "425 No data connection"); + Serial.println("425"); + + } + else + { + #ifdef FTP_DEBUG + Serial.println("Sending " + String(parameters)); + #endif + client.println( "150-Connected to port "+ String(dataPort)); + client.println( "150 " + String(file.size()) + " bytes to download"); + millisBeginTrans = millis(); + bytesTransfered = 0; + transferStatus = 1; + } + } + } + // + // STOR - Store + // + else if( ! strcmp( command, "STOR" )) + { + char path[ FTP_CWD_SIZE ]; + if( strlen( parameters ) == 0 ) + client.println( "501 No file name"); + else if( makePath( path )) + { + file = SD_MMC.open(path, "w"); + if( !file) + client.println( "451 Can't open/create " +String(parameters) ); + else if( ! dataConnect()) + { + client.println( "425 No data connection"); + file.close(); + } + else + { + #ifdef FTP_DEBUG + Serial.println( "Receiving " +String(parameters)); + #endif + client.println( "150 Connected to port " + String(dataPort)); + millisBeginTrans = millis(); + bytesTransfered = 0; + transferStatus = 2; + } + } + } + // + // MKD - Make Directory + // + + else if( ! strcmp( command, "MKD" )) + { + char path[ FTP_CWD_SIZE ]; + if( haveParameter() && makePath( path )){ + if (SD_MMC.exists( path )){ + client.println( "521 Can't create \"" + String(parameters) + ", Directory exists"); + } + else + { + if( SD_MMC.mkdir( path )){ + client.println( "257 \"" + String(parameters) + "\" created"); + } + else{ + client.println( "550 Can't create \"" + String(parameters)); + } + } + + } + + } + // + // RMD - Remove a Directory + // + else if( ! strcmp( command, "RMD" )) + { + char path[ FTP_CWD_SIZE ]; + if( haveParameter() && makePath( path )){ + if( SD_MMC.rmdir( path )){ + #ifdef FTP_DEBUG + Serial.println( " Deleting " +String(parameters)); + + #endif + client.println( "250 \"" + String(parameters) + "\" deleted"); + + } + else + { + client.println( "550 Can't remove \"" + String(parameters) + "\". Directory not empty?"); + } + } + + } + // + // RNFR - Rename From + // + else if( ! strcmp( command, "RNFR" )) + { + buf[ 0 ] = 0; + if( strlen( parameters ) == 0 ) + client.println( "501 No file name"); + else if( makePath( buf )) + { + if( ! SD_MMC.exists( buf )) + client.println( "550 File " +String(parameters)+ " not found"); + else + { + #ifdef FTP_DEBUG + Serial.println("Renaming " + String(buf)); + #endif + client.println( "350 RNFR accepted - file exists, ready for destination"); + rnfrCmd = true; + } + } + } + // + // RNTO - Rename To + // + else if( ! strcmp( command, "RNTO" )) + { + char path[ FTP_CWD_SIZE ]; + char dir[ FTP_FIL_SIZE ]; + if( strlen( buf ) == 0 || ! rnfrCmd ) + client.println( "503 Need RNFR before RNTO"); + else if( strlen( parameters ) == 0 ) + client.println( "501 No file name"); + else if( makePath( path )) + { + if( SD_MMC.exists( path )) + client.println( "553 " +String(parameters)+ " already exists"); + else + { + #ifdef FTP_DEBUG + Serial.println("Renaming " + String(buf) + " to " + String(path)); + #endif + if( SD_MMC.rename( buf, path )) + client.println( "250 File successfully renamed or moved"); + else + client.println( "451 Rename/move failure"); + + } + } + rnfrCmd = false; + } + + /////////////////////////////////////// + // // + // EXTENSIONS COMMANDS (RFC 3659) // + // // + /////////////////////////////////////// + + // + // FEAT - New Features + // + else if( ! strcmp( command, "FEAT" )) + { + client.println( "500 Unknow command"); + //client.println( "211-Extensions suported:"); // recommendation by gendron for WinSCP - jz sep0519 + //client.println( " MLSD"); + //client.println( "211 End."); + } + // + // MDTM - File Modification Time (see RFC 3659) + // + else if (!strcmp(command, "MDTM")) + { + client.println("550 Unable to retrieve time"); + } + + // + // SIZE - Size of the file + // + else if( ! strcmp( command, "SIZE" )) + { + char path[ FTP_CWD_SIZE ]; + if( strlen( parameters ) == 0 ) + client.println( "501 No file name"); + else if( makePath( path )) + { + file = SD_MMC.open(path, "r"); + if(!file) + client.println( "450 Can't open " +String(parameters) ); + else + { + client.println( "213 " + String(file.size())); + file.close(); + } + } + } + // + // SITE - System command + // + else if( ! strcmp( command, "SITE" )) + { + client.println( "500 Unknow SITE command " +String(parameters) ); + } + // + // OPTS - OPTS UTF8 ON - jz sep152019 - handle this by doing nothing + // - didnt work - windows 10 ftp sends command before login + // + else if( ! strcmp( command, "OPTS" )) + { + client.println( "200 Zzz..."); + } + // + // Unrecognized commands ... + // + else + client.println( "500 Unknow command"); + + return true; +} + +boolean FtpServer::dataConnect() +{ + unsigned long startTime = millis(); + //wait 5 seconds for a data connection + if (!data.connected()) + { + + while (!dataServer.hasClient() && millis() - startTime < 10000) + + +// while (!dataServer.available() && millis() - startTime < 10000) + { + //Serial.println("start while");Serial.println("before yield"); + //Serial.print("hasClient / available "); Serial.print(dataServer.hasClient()); Serial.print("/"); Serial.println(dataServer.available()); + +// delay(100); //jz sep152019 + + yield(); + if (dataServer.hasClient() == 1){ + Serial.println("Break in dataConnect"); + break; + } + } + if (dataServer.hasClient()) { +// if (dataServer.available()) { +// Serial.println("before stop"); + data.stop(); + data = dataServer.available(); + #ifdef FTP_DEBUG + Serial.println("ftpdataserver client...."); + #endif + + } + } + + return data.connected(); + +} + +boolean FtpServer::doRetrieve() +{ + //Serial.println("doRetreive"); +if (data.connected()) +{ + int16_t nb = file.readBytes(buf, FTP_BUF_SIZE); + if (nb > 0) + { + data.write((uint8_t*)buf, nb); + bytesTransfered += nb; + //Serial.print(" - ");Serial.print(bytesTransfered); + return true; + } +} +closeTransfer(); +return false; +} + + +boolean FtpServer::doStore() +{ + if( data.connected() ) + { + int16_t nb = data.readBytes((uint8_t*) buf, FTP_BUF_SIZE ); + if( nb > 0 ) + { + // Serial.println( millis() << " " << nb << endl; + file.write((uint8_t*) buf, nb ); + bytesTransfered += nb; + } + return true; + } + closeTransfer(); + return false; +} + +void FtpServer::closeTransfer() +{ + uint32_t deltaT = (int32_t) ( millis() - millisBeginTrans ); + if( deltaT > 0 && bytesTransfered > 0 ) + { + client.println( "226-File successfully transferred"); + client.println( "226 " + String(deltaT) + " ms, "+ String(bytesTransfered / deltaT) + " kbytes/s"); + } + else + client.println( "226 File successfully transferred"); + + file.close(); + data.stop(); +} + +void FtpServer::abortTransfer() +{ + if( transferStatus > 0 ) + { + file.close(); + data.stop(); + client.println( "426 Transfer aborted" ); + #ifdef FTP_DEBUG + Serial.println( "Transfer aborted!") ; + #endif + } + transferStatus = 0; +} + +// Read a char from client connected to ftp server +// +// update cmdLine and command buffers, iCL and parameters pointers +// +// return: +// -2 if buffer cmdLine is full +// -1 if line not completed +// 0 if empty line received +// length of cmdLine (positive) if no empty line received + +int8_t FtpServer::readChar() +{ + int8_t rc = -1; + + if( client.available()) + { + char c = client.read(); + // char c; + // client.readBytes((uint8_t*) c, 1); + #ifdef FTP_DEBUG + Serial.print( c); + #endif + if( c == '\\' ) + c = '/'; + if( c != '\r' ) + if( c != '\n' ) + { + if( iCL < FTP_CMD_SIZE ) + cmdLine[ iCL ++ ] = c; + else + rc = -2; // Line too long + } + else + { + cmdLine[ iCL ] = 0; + command[ 0 ] = 0; + parameters = NULL; + // empty line? + if( iCL == 0 ) + rc = 0; + else + { + rc = iCL; + // search for space between command and parameters + parameters = strchr( cmdLine, ' ' ); + if( parameters != NULL ) + { + if( parameters - cmdLine > 4 ) + rc = -2; // Syntax error + else + { + strncpy( command, cmdLine, parameters - cmdLine ); + command[ parameters - cmdLine ] = 0; + + while( * ( ++ parameters ) == ' ' ) + ; + } + } + else if( strlen( cmdLine ) > 4 ) + rc = -2; // Syntax error. + else + strcpy( command, cmdLine ); + iCL = 0; + } + } + if( rc > 0 ) + for( uint8_t i = 0 ; i < strlen( command ); i ++ ) + command[ i ] = toupper( command[ i ] ); + if( rc == -2 ) + { + iCL = 0; + client.println( "500 Syntax error"); + } + } + return rc; +} + +// Make complete path/name from cwdName and parameters +// +// 3 possible cases: parameters can be absolute path, relative path or only the name +// +// parameters: +// fullName : where to store the path/name +// +// return: +// true, if done + +boolean FtpServer::makePath( char * fullName ) +{ + return makePath( fullName, parameters ); +} + +boolean FtpServer::makePath( char * fullName, char * param ) +{ + if( param == NULL ) + param = parameters; + + // Root or empty? + if( strcmp( param, "/" ) == 0 || strlen( param ) == 0 ) + { + strcpy( fullName, "/" ); + return true; + } + // If relative path, concatenate with current dir + if( param[0] != '/' ) + { + strcpy( fullName, cwdName ); + if( fullName[ strlen( fullName ) - 1 ] != '/' ) + strncat( fullName, "/", FTP_CWD_SIZE ); + strncat( fullName, param, FTP_CWD_SIZE ); + } + else + strcpy( fullName, param ); + // If ends with '/', remove it + uint16_t strl = strlen( fullName ) - 1; + if( fullName[ strl ] == '/' && strl > 1 ) + fullName[ strl ] = 0; + if( strlen( fullName ) < FTP_CWD_SIZE ) + return true; + + client.println( "500 Command line too long"); + return false; +} + +// Calculate year, month, day, hour, minute and second +// from first parameter sent by MDTM command (YYYYMMDDHHMMSS) +// +// parameters: +// pyear, pmonth, pday, phour, pminute and psecond: pointer of +// variables where to store data +// +// return: +// 0 if parameter is not YYYYMMDDHHMMSS +// length of parameter + space + +uint8_t FtpServer::getDateTime( uint16_t * pyear, uint8_t * pmonth, uint8_t * pday, + uint8_t * phour, uint8_t * pminute, uint8_t * psecond ) +{ + char dt[ 15 ]; + + // Date/time are expressed as a 14 digits long string + // terminated by a space and followed by name of file + if( strlen( parameters ) < 15 || parameters[ 14 ] != ' ' ) + return 0; + for( uint8_t i = 0; i < 14; i++ ) + if( ! isdigit( parameters[ i ])) + return 0; + + strncpy( dt, parameters, 14 ); + dt[ 14 ] = 0; + * psecond = atoi( dt + 12 ); + dt[ 12 ] = 0; + * pminute = atoi( dt + 10 ); + dt[ 10 ] = 0; + * phour = atoi( dt + 8 ); + dt[ 8 ] = 0; + * pday = atoi( dt + 6 ); + dt[ 6 ] = 0 ; + * pmonth = atoi( dt + 4 ); + dt[ 4 ] = 0 ; + * pyear = atoi( dt ); + return 15; +} + +// Create string YYYYMMDDHHMMSS from date and time +// +// parameters: +// date, time +// tstr: where to store the string. Must be at least 15 characters long +// +// return: +// pointer to tstr + +char * FtpServer::makeDateTimeStr( char * tstr, uint16_t date, uint16_t time ) +{ + sprintf( tstr, "%04u%02u%02u%02u%02u%02u", + (( date & 0xFE00 ) >> 9 ) + 1980, ( date & 0x01E0 ) >> 5, date & 0x001F, + ( time & 0xF800 ) >> 11, ( time & 0x07E0 ) >> 5, ( time & 0x001F ) << 1 ); + return tstr; +} + +bool FtpServer::haveParameter() +{ + if( parameters != NULL && strlen( parameters ) > 0 ) + return true; + client.println ("501 No file name"); + return false; +} +bool FtpServer::makeExistsPath( char * path, char * param ) +{ + if( ! makePath( path, param )) + return false; + if( SD_MMC.exists( path )) + return true; + client.println("550 " + String(path) + " not found."); + + return false; +} diff --git a/v60/ESP32FtpServer.h b/v60/ESP32FtpServer.h new file mode 100644 index 0000000..a40186f --- /dev/null +++ b/v60/ESP32FtpServer.h @@ -0,0 +1,112 @@ + +/* +* FTP SERVER FOR ESP8266 + * based on FTP Serveur for Arduino Due and Ethernet shield (W5100) or WIZ820io (W5200) + * based on Jean-Michel Gallego's work + * modified to work with esp8266 SPIFFS by David Paiva (david@nailbuster.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// 2017: modified by @robo8080 +// 2019: modified by @fa1ke5 + +/******************************************************************************* + ** ** + ** DEFINITIONS FOR FTP SERVER ** + ** ** + *******************************************************************************/ + +// Uncomment to print debugging info to console attached to ESP8266 +//#define FTP_DEBUG + +#ifndef FTP_SERVERESP_H +#define FTP_SERVERESP_H + +//#include "Streaming.h" +#include "SD_MMC.h" +#include +#include + +#define FTP_SERVER_VERSION "FTP-2016-01-14" + +#define FTP_CTRL_PORT 21 // Command port on wich server is listening +#define FTP_DATA_PORT_PASV 50009 // Data port in passive mode + +#define FTP_TIME_OUT 5 // Disconnect client after 5 minutes of inactivity +#define FTP_CMD_SIZE 255 + 8 // max size of a command +#define FTP_CWD_SIZE 255 + 8 // max size of a directory name +#define FTP_FIL_SIZE 255 // max size of a file name + +//#define FTP_BUF_SIZE 512 //512 // size of file buffer for read/write +//#define FTP_BUF_SIZE 2*1460 //512 // size of file buffer for read/write +#define FTP_BUF_SIZE 4096 //512 // 700 KByte/s download in AP mode, direct connection. + + +class FtpServer +{ +public: + void begin(String uname, String pword); + void handleFTP(); + +private: + bool haveParameter(); +bool makeExistsPath( char * path, char * param = NULL ); + void iniVariables(); + void clientConnected(); + void disconnectClient(); + boolean userIdentity(); + boolean userPassword(); + boolean processCommand(); + boolean dataConnect(); + boolean doRetrieve(); + boolean doStore(); + void closeTransfer(); + void abortTransfer(); + boolean makePath( char * fullname ); + boolean makePath( char * fullName, char * param ); + uint8_t getDateTime( uint16_t * pyear, uint8_t * pmonth, uint8_t * pday, + uint8_t * phour, uint8_t * pminute, uint8_t * second ); + char * makeDateTimeStr( char * tstr, uint16_t date, uint16_t time ); + int8_t readChar(); + + IPAddress dataIp; // IP address of client for data + WiFiClient client; + WiFiClient data; + + File file; + + boolean dataPassiveConn; + uint16_t dataPort; + char buf[ FTP_BUF_SIZE ]; // data buffer for transfers + char cmdLine[ FTP_CMD_SIZE ]; // where to store incoming char from client + char cwdName[ FTP_CWD_SIZE ]; // name of current directory + char command[ 5 ]; // command sent by client + boolean rnfrCmd; // previous command was RNFR + char * parameters; // point to begin of parameters sent by client + uint16_t iCL; // pointer to cmdLine next incoming char + int8_t cmdStatus, // status of ftp command connexion + transferStatus; // status of ftp data transfer + uint32_t millisTimeOut, // disconnect after 5 min of inactivity + millisDelay, + millisEndConnection, // + millisBeginTrans, // store time of beginning of a transaction + bytesTransfered; // + String _FTP_USER; + String _FTP_PASS; + + + +}; + +#endif // FTP_SERVERESP_H diff --git a/v60/TimeLapseAvi60x.ino b/v60/TimeLapseAvi60x.ino new file mode 100644 index 0000000..8b8064e --- /dev/null +++ b/v60/TimeLapseAvi60x.ino @@ -0,0 +1,1542 @@ + + +#include + +/* + + TimeLapseAvi + + ESP32-CAM Video Recorder + + This program records an AVI video on the SD Card of an ESP32-CAM. + + by James Zahary July 20, 2019 TimeLapseAvi23x.ino + jamzah.plc@gmail.com + + https://github.com/jameszah/ESP32-CAM-Video-Recorder + + jameszah/ESP32-CAM-Video-Recorder is licensed under the + GNU General Public License v3.0 + + The is Arduino code, with standard setup for ESP32-CAM + - Board ESP32 Wrover Module + - Partition Scheme Huge APP (3MB No OTA) + +*/ + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// edit these parameters for your needs + +static const char vernum[] = "v60"; // version 60 Feb 26, 2020 - added dates/times to ftp + // version 59 Feb 23, 2020 - switch to 1 bit system, and pir +static const char devname[] = "desklens"; // name of your camera for mDNS, Router, and filenames + + +#define TIMEZONE "GMT0BST,M3.5.0/01,M10.5.0/02" // your timezone - this is GMT + + +// 1 for blink red led with every sd card write, at your frame rate +// 0 for blink only for skipping frames and SOS if camera or sd is broken +#define BlinkWithWrite 0 + +// EDIT ssid and password +const char ssid[] = "wifiRouter"; // your wireless network name (SSID) +const char password[] = "wifipassword"; // your Wi-Fi network password + +// startup defaults for first recording + +// here are the recording options from the "retart web page" +// VGA 10 fps for 30 min, repeat, realtime http://192.168.0.117/start?framesize=VGA&length=1800&interval=100&quality=10&repeat=100&speed=1&gray=0 +// VGA 2 fps, for 30 minutes repeat, 30x playback http://192.168.0.117/start?framesize=VGA&length=1800&interval=500&quality=10&repeat=300&speed=30&gray=0 +// UXGA 1 sec per frame, for 30 minutes repeat, 30x playback http://192.168.0.117/start?framesize=UXGA&length=1800&interval=1000&quality=10&repeat=100&speed=30&gray=0 +// UXGA 2 fps for 30 minutes repeat, 15x playback http://192.168.0.117/start?framesize=UXGA&length=1800&interval=500&quality=10&repeat=100&speed=30&gray=0 +// CIF 20 fps second for 30 minutes repeat http://192.168.0.117/start?framesize=CIF&length=1800&interval=50&quality=10&repeat=100&speed=1&gray=0 + +// reboot startup parameters here + +int record_on_reboot = 1; // set to 1 to record, or 0 to NOT record on reboot +int framesize = 6; // vga (10 UXGA, 7 SVGA, 6 VGA, 5 CIF) +int repeat = 100; // 100 files +int xspeed = 1; // 1x playback speed (realtime is 1) +int gray = 0; // not gray +int quality = 10; // 10 on the 0..64 scale, or 10..50 subscale - 10 is good, 20 is grainy and smaller files +int capture_interval = 100; // 100 ms or 10 frames per second +int total_frames = 18000; // 18000 frames = 10 fps * 60 seconds * 30 minutes = half hour + +int PIRpin = 12; + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +int new_config = 5; // this system abandoned ! +int xlength = total_frames * capture_interval / 1000; +int recording = 0; +int PIRstatus = 0; +int PIRrecording = 0; +int ready = 0; + +//#define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE +#include "esp_log.h" +#include "esp_http_server.h" +#include "esp_camera.h" + +#include + +#include "ESP32FtpServer.h" +#include + +FtpServer ftpSrv; //set #define FTP_DEBUG in ESP32FtpServer.h to see ftp verbose on serial + +// Time +#include "time.h" + +// MicroSD +#include "driver/sdmmc_host.h" +#include "driver/sdmmc_defs.h" +#include "sdmmc_cmd.h" +#include "esp_vfs_fat.h" +#include + +long current_millis; +long last_capture_millis = 0; +static esp_err_t cam_err; +static esp_err_t card_err; +char strftime_buf[64]; +int file_number = 0; +bool internet_connected = false; +struct tm timeinfo; +time_t now; + +char *filename ; +char *stream ; +int newfile = 0; +int frames_so_far = 0; +FILE *myfile; +long bp; +long ap; +long bw; +long aw; +long totalp; +long totalw; +float avgp; +float avgw; +int overtime_count = 0; + +// CAMERA_MODEL_AI_THINKER +#define PWDN_GPIO_NUM 32 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 0 +#define SIOD_GPIO_NUM 26 +#define SIOC_GPIO_NUM 27 +#define Y9_GPIO_NUM 35 +#define Y8_GPIO_NUM 34 +#define Y7_GPIO_NUM 39 +#define Y6_GPIO_NUM 36 +#define Y5_GPIO_NUM 21 +#define Y4_GPIO_NUM 19 +#define Y3_GPIO_NUM 18 +#define Y2_GPIO_NUM 5 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 + + +// GLOBALS +#define BUFFSIZE 512 + +// global variable used by these pieces + +char str[20]; +uint16_t n; +uint8_t buf[BUFFSIZE]; + +static int i = 0; +uint8_t temp = 0, temp_last = 0; +unsigned long fileposition = 0; +uint16_t frame_cnt = 0; +uint16_t remnant = 0; +uint32_t length = 0; +uint32_t startms; +uint32_t elapsedms; +uint32_t uVideoLen = 0; +bool is_header = false; +long bigdelta = 0; +int other_cpu_active = 0; +int skipping = 0; +int skipped = 0; + +int fb_max = 12; + +camera_fb_t * fb_q[30]; +int fb_in = 0; +int fb_out = 0; + +camera_fb_t * fb = NULL; + +FILE *avifile = NULL; +FILE *idxfile = NULL; + + +#define AVIOFFSET 240 // AVI main header length + +unsigned long movi_size = 0; +unsigned long jpeg_size = 0; +unsigned long idx_offset = 0; + +uint8_t zero_buf[4] = {0x00, 0x00, 0x00, 0x00}; +uint8_t dc_buf[4] = {0x30, 0x30, 0x64, 0x63}; // "00dc" +uint8_t avi1_buf[4] = {0x41, 0x56, 0x49, 0x31}; // "AVI1" +uint8_t idx1_buf[4] = {0x69, 0x64, 0x78, 0x31}; // "idx1" + +uint8_t vga_w[2] = {0x80, 0x02}; // 640 +uint8_t vga_h[2] = {0xE0, 0x01}; // 480 +uint8_t cif_w[2] = {0x90, 0x01}; // 400 +uint8_t cif_h[2] = {0x28, 0x01}; // 296 +uint8_t svga_w[2] = {0x20, 0x03}; // 800 +uint8_t svga_h[2] = {0x58, 0x02}; // 600 +uint8_t uxga_w[2] = {0x40, 0x06}; // 1600 +uint8_t uxga_h[2] = {0xB0, 0x04}; // 1200 + + +const int avi_header[AVIOFFSET] PROGMEM = { + 0x52, 0x49, 0x46, 0x46, 0xD8, 0x01, 0x0E, 0x00, 0x41, 0x56, 0x49, 0x20, 0x4C, 0x49, 0x53, 0x54, + 0xD0, 0x00, 0x00, 0x00, 0x68, 0x64, 0x72, 0x6C, 0x61, 0x76, 0x69, 0x68, 0x38, 0x00, 0x00, 0x00, + 0xA0, 0x86, 0x01, 0x00, 0x80, 0x66, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x02, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x84, 0x00, 0x00, 0x00, + 0x73, 0x74, 0x72, 0x6C, 0x73, 0x74, 0x72, 0x68, 0x30, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x73, + 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x66, + 0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x18, 0x00, 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x4E, 0x46, 0x4F, + 0x10, 0x00, 0x00, 0x00, 0x6A, 0x61, 0x6D, 0x65, 0x73, 0x7A, 0x61, 0x68, 0x61, 0x72, 0x79, 0x20, + 0x76, 0x36, 0x30, 0x20, 0x4C, 0x49, 0x53, 0x54, 0x00, 0x01, 0x0E, 0x00, 0x6D, 0x6F, 0x76, 0x69, +}; + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// AviWriterTask runs on cpu 1 to write the avi file +// + +TaskHandle_t CameraTask, AviWriterTask; +SemaphoreHandle_t baton; +int counter = 0; + +void codeForAviWriterTask( void * parameter ) +{ + + for (;;) { + if (ready) { + make_avi(); + } + delay(1); + } +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// CameraTask runs on cpu 0 to take pictures and drop them in a queue +// + +void codeForCameraTask( void * parameter ) +{ + + for (;;) { + + if (other_cpu_active == 1 ) { + current_millis = millis(); + if (current_millis - last_capture_millis > capture_interval) { + + last_capture_millis = millis(); + + xSemaphoreTake( baton, portMAX_DELAY ); + + if ( ( (fb_in + fb_max - fb_out) % fb_max) + 1 == fb_max ) { + xSemaphoreGive( baton ); + + Serial.print(" Queue Full, Skipping ... "); // the queue is full + skipped++; + skipping = 1; + + } + + if (skipping > 0 ) { + + + if (!BlinkWithWrite) { + digitalWrite(33, LOW); + } + + if (skipping % 2 == 0) { // skip every other frame until queue is cleared + + frames_so_far = frames_so_far + 1; + frame_cnt++; + + fb_in = (fb_in + 1) % fb_max; + bp = millis(); + fb_q[fb_in] = esp_camera_fb_get(); + totalp = totalp - bp + millis(); + + } else { + Serial.print(((fb_in + fb_max - fb_out) % fb_max)); // skip an extra frame to empty the queue + skipped++; + } + skipping = skipping + 1; + if (((fb_in + fb_max - fb_out) % fb_max) == 0 ) { + skipping = 0; + Serial.println(" Queue cleared. "); + } + + xSemaphoreGive( baton ); + + } else { + + skipping = 0; + frames_so_far = frames_so_far + 1; + frame_cnt++; + + fb_in = (fb_in + 1) % fb_max; + bp = millis(); + fb_q[fb_in] = esp_camera_fb_get(); + totalp = totalp - bp + millis(); + xSemaphoreGive( baton ); + + } + } + } + delay(1); + } +} + + +// +// Writes an uint32_t in Big Endian at current file position +// +static void inline print_quartet(unsigned long i, FILE * fd) +{ + uint8_t x[1]; + + x[0] = i % 0x100; + size_t i1_err = fwrite(x , 1, 1, fd); + i = i >> 8; x[0] = i % 0x100; + size_t i2_err = fwrite(x , 1, 1, fd); + i = i >> 8; x[0] = i % 0x100; + size_t i3_err = fwrite(x , 1, 1, fd); + i = i >> 8; x[0] = i % 0x100; + size_t i4_err = fwrite(x , 1, 1, fd); +} + + +void startCameraServer(); +httpd_handle_t camera_httpd = NULL; + +char the_page[3000]; + +char localip[20]; +WiFiEventId_t eventID; + +#include "soc/soc.h" +#include "soc/rtc_cntl_reg.h" + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// setup() runs on cpu 1 +// + +void setup() { + //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector // creates other problems + + Serial.begin(115200); + + Serial.setDebugOutput(true); + + // zzz + Serial.println(" "); + Serial.println("-------------------------------------"); + Serial.printf("ESP-CAM Video Recorder %s\n", vernum); + Serial.printf(" http://%s.local - to access the camera\n", devname); + Serial.println("-------------------------------------"); + + pinMode(33, OUTPUT); // little red led on back of chip + digitalWrite(33, LOW); // turn on the red LED on the back of chip + + if (init_wifi()) { // Connected to WiFi + internet_connected = true; + } + + if (!psramFound()) { + Serial.println("paraFound wrong - major fail"); + major_fail(); + } + + // SD camera init + card_err = init_sdcard(); + if (card_err != ESP_OK) { + Serial.printf("SD Card init failed with error 0x%x", card_err); + major_fail(); + return; + } + + startCameraServer(); + + // zzz username and password for ftp server + + ftpSrv.begin("esp", "esp"); + + Serial.printf("Total space: %lluMB\n", SD_MMC.totalBytes() / (1024 * 1024)); + Serial.printf("Used space: %lluMB\n", SD_MMC.usedBytes() / (1024 * 1024)); + + digitalWrite(33, HIGH); // red light turns off when setup is complete + + baton = xSemaphoreCreateMutex(); + + xTaskCreatePinnedToCore( + codeForCameraTask, + "CameraTask", + 10000, + NULL, + 1, + &CameraTask, + 0); + + delay(50); + + xTaskCreatePinnedToCore( + codeForAviWriterTask, + "AviWriterTask", + 10000, + NULL, + 2, + &AviWriterTask, + 1); + + delay(50); + + + recording = 0; // we are NOT recording + config_camera(); + + + pinMode(4, OUTPUT); // using 1 bit mode, shut off the Blinding Disk-Active Light + digitalWrite(4, LOW); + + pinMode(PIRpin, INPUT_PULLDOWN); // or PULLDOWN for active high + + newfile = 0; // no file is open // don't fiddle with this! + + recording = record_on_reboot; + + ready = 1; + + Serial.print("Camera Ready! Use 'http://"); + Serial.print(WiFi.localIP()); + Serial.println("' to connect"); + + +} + + +// +// if we have no camera, or sd card, then flash rear led on and off to warn the human SOS - SOS +// +void major_fail() { + + Serial.println(" "); + + for (int i = 0; i < 10; i++) { // 10 loops or about 100 seconds then reboot + digitalWrite(33, LOW); delay(150); + digitalWrite(33, HIGH); delay(150); + digitalWrite(33, LOW); delay(150); + digitalWrite(33, HIGH); delay(150); + digitalWrite(33, LOW); delay(150); + digitalWrite(33, HIGH); delay(150); + + delay(1000); + + digitalWrite(33, LOW); delay(500); + digitalWrite(33, HIGH); delay(500); + digitalWrite(33, LOW); delay(500); + digitalWrite(33, HIGH); delay(500); + digitalWrite(33, LOW); delay(500); + digitalWrite(33, HIGH); delay(500); + + delay(1000); + Serial.print("Major Fail "); Serial.print(i); Serial.print(" / "); Serial.println(10); + } + + ESP.restart(); + +} + + +bool init_wifi() +{ + int connAttempts = 0; + + WiFi.disconnect(true); + WiFi.mode(WIFI_STA); + + WiFi.setHostname(devname); + //WiFi.printDiag(Serial); + WiFi.begin(ssid, password); + delay(1000); + while (WiFi.status() != WL_CONNECTED ) { + delay(500); + Serial.print("."); + if (connAttempts == 10) { + Serial.println("Cannot connect - try again"); + WiFi.begin(ssid, password); + WiFi.printDiag(Serial); + } + if (connAttempts == 20) { + Serial.println("Cannot connect - fail"); + + WiFi.printDiag(Serial); + return false; + } + connAttempts++; + } + + Serial.println("Internet connected"); + + //WiFi.printDiag(Serial); + + if (!MDNS.begin(devname)) { + Serial.println("Error setting up MDNS responder!"); + } else { + Serial.printf("mDNS responder started '%s'\n", devname); + } + + configTime(0, 0, "pool.ntp.org"); + setenv("TZ", TIMEZONE, 1); // mountain time zone from #define at top + tzset(); + + time_t now ; + timeinfo = { 0 }; + int retry = 0; + const int retry_count = 10; + delay(1000); + time(&now); + localtime_r(&now, &timeinfo); + + while (timeinfo.tm_year < (2016 - 1900) && ++retry < retry_count) { + Serial.printf("Waiting for system time to be set... (%d/%d) -- %d\n", retry, retry_count, timeinfo.tm_year); + delay(1000); + time(&now); + localtime_r(&now, &timeinfo); + } + + Serial.println(ctime(&now)); + sprintf(localip, "%s", WiFi.localIP().toString().c_str()); + + return true; + +} + + +static esp_err_t init_sdcard() +{ + esp_err_t ret = ESP_FAIL; + sdmmc_host_t host = SDMMC_HOST_DEFAULT(); + host.flags = SDMMC_HOST_FLAG_1BIT; // using 1 bit mode + host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + slot_config.width = 1; // using 1 bit mode + //Serial.print("Slot config width should be 4 width: "); Serial.println(slot_config.width); + esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + }; + + //pinMode(4, OUTPUT); // using 1 bit mode, shut off the Blinding Disk-Active Light + //digitalWrite(4, LOW); + + sdmmc_card_t *card; + + Serial.println("Mounting SD card..."); + ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); + + if (ret == ESP_OK) { + Serial.println("SD card mount successfully!"); + } else { + Serial.printf("Failed to mount SD card VFAT filesystem. Error: %s", esp_err_to_name(ret)); + major_fail(); + } + sdmmc_card_print_info(stdout, card); + Serial.print("SD_MMC Begin: "); Serial.println(SD_MMC.begin()); // required by ftp system ?? +} + + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Make the avi move in 4 pieces +// +// make_avi() called in every loop, which calls below, depending on conditions +// start_avi() - open the file and write headers +// another_pic_avi() - write one more frame of movie +// end_avi() - write the final parameters and close the file + +void make_avi( ) { + + + PIRstatus = digitalRead(PIRpin); + + + if (PIRstatus == 1) { + + if (PIRrecording == 1) { + // keep recording for 15 more seconds + if ( (millis() - startms) > (total_frames * capture_interval - 5000) ) { + + total_frames = total_frames + 10000 / capture_interval ; + Serial.println("Add another 10 seconds"); + } + + } else { + + if ( recording == 0 && newfile == 0) { + + //start a pir recording with current parameters, except no repeat and 15 seconds + Serial.println("Start a PIR"); + PIRrecording = 1; + repeat = 0; + total_frames = 15000 / capture_interval; + xlength = total_frames * capture_interval / 1000; + recording = 1; + } + } + } + + + // we are recording, but no file is open + + if (newfile == 0 && recording == 1) { // open the file + + digitalWrite(33, HIGH); + newfile = 1; + start_avi(); + + } else { + + // we have a file open, but not recording + + if (newfile == 1 && recording == 0) { // got command to close file + + digitalWrite(33, LOW); + end_avi(); + + Serial.println("Done capture due to command"); + + frames_so_far = total_frames; + + newfile = 0; // file is closed + recording = 0; // DO NOT start another recording + PIRrecording = 0; + + } else { + + if (newfile == 1 && recording == 1) { // regular recording + + if (frames_so_far >= total_frames) { // we are done the recording + + Serial.println("Done capture for total frames!"); + + digitalWrite(33, LOW); // close the file + end_avi(); + + frames_so_far = 0; + newfile = 0; // file is closed + + if (repeat > 0) { + recording = 1; // start another recording + repeat = repeat - 1; + } else { + recording = 0; + PIRrecording = 0; + } + + } else if ((millis() - startms) > (total_frames * capture_interval)) { // time is up, even though we have not done all the frames + + Serial.println (" "); Serial.println("Done capture for time"); + Serial.print("Time Elapsed: "); Serial.print(millis() - startms); Serial.print(" Frames: "); Serial.println(frame_cnt); + Serial.print("Config: "); Serial.print(total_frames * capture_interval ) ; Serial.print(" ("); + Serial.print(total_frames); Serial.print(" x "); Serial.print(capture_interval); Serial.println(")"); + + digitalWrite(33, LOW); // close the file + + end_avi(); + + frames_so_far = 0; + newfile = 0; // file is closed + if (repeat > 0) { + recording = 1; // start another recording + repeat = repeat - 1; + } else { + recording = 0; + PIRrecording = 0; + } + + } else { // regular + + another_save_avi(); + + } + } + } + } +} + +static esp_err_t config_camera() { + + camera_config_t config; + + //Serial.println("config camera"); + + if (new_config == 5) { + + config.ledc_channel = LEDC_CHANNEL_0; + config.ledc_timer = LEDC_TIMER_0; + config.pin_d0 = Y2_GPIO_NUM; + config.pin_d1 = Y3_GPIO_NUM; + config.pin_d2 = Y4_GPIO_NUM; + config.pin_d3 = Y5_GPIO_NUM; + config.pin_d4 = Y6_GPIO_NUM; + config.pin_d5 = Y7_GPIO_NUM; + config.pin_d6 = Y8_GPIO_NUM; + config.pin_d7 = Y9_GPIO_NUM; + config.pin_xclk = XCLK_GPIO_NUM; + config.pin_pclk = PCLK_GPIO_NUM; + config.pin_vsync = VSYNC_GPIO_NUM; + config.pin_href = HREF_GPIO_NUM; + config.pin_sscb_sda = SIOD_GPIO_NUM; + config.pin_sscb_scl = SIOC_GPIO_NUM; + config.pin_pwdn = PWDN_GPIO_NUM; + config.pin_reset = RESET_GPIO_NUM; + config.xclk_freq_hz = 20000000; + config.pixel_format = PIXFORMAT_JPEG; + + config.frame_size = FRAMESIZE_UXGA; + + fb_max = 7; // for vga and uxga + config.jpeg_quality = 8; + config.fb_count = fb_max + 1; + + // camera init + cam_err = esp_camera_init(&config); + if (cam_err != ESP_OK) { + Serial.printf("Camera init failed with error 0x%x", cam_err); + major_fail(); + } + + new_config = 2; + + } + + delay(100); + + sensor_t * ss = esp_camera_sensor_get(); + ss->set_quality(ss, quality); + ss->set_framesize(ss, (framesize_t)framesize); + if (gray == 1) { + ss->set_special_effect(ss, 2); // 0 regular, 2 grayscale + } else { + ss->set_special_effect(ss, 0); // 0 regular, 2 grayscale + } + + for (int j = 0; j < 3; j++) { + do_fb(); // start the camera ... warm it up + delay(1); + } +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// start_avi - open the files and write in headers +// + +static esp_err_t start_avi() { + + Serial.println("Starting an avi "); + + config_camera(); + + time(&now); + localtime_r(&now, &timeinfo); + + strftime(strftime_buf, sizeof(strftime_buf), "%F_%H.%M.%S", &timeinfo); + + char fname[100]; + + if (framesize == 6) { + sprintf(fname, "/sdcard/%s_%s_vga_Q%d_I%d_L%d_S%d.avi", devname, strftime_buf, quality, capture_interval, xlength, xspeed); + } else if (framesize == 7) { + sprintf(fname, "/sdcard/%s_%s_svga_Q%d_I%d_L%d_S%d.avi", devname, strftime_buf, quality, capture_interval, xlength, xspeed); + } else if (framesize == 10) { + sprintf(fname, "/sdcard/%s_%s_uxga_Q%d_I%d_L%d_S%d.avi", devname, strftime_buf, quality, capture_interval, xlength, xspeed); + } else if (framesize == 5) { + sprintf(fname, "/sdcard/%s_%s_cif_Q%d_I%d_L%d_S%d.avi", devname, strftime_buf, quality, capture_interval, xlength, xspeed); + } else { + Serial.println("Wrong framesize"); + sprintf(fname, "/sdcard/%s_%s_xxx_Q%d_I%d_L%d_S%d.avi", devname, strftime_buf, quality, capture_interval, xlength, xspeed); + } + + Serial.print("\nFile name will be >"); Serial.print(fname); Serial.println("<"); + + avifile = fopen(fname, "w"); + idxfile = fopen("/sdcard/idx.tmp", "w"); + + if (avifile != NULL) { + + //Serial.printf("File open: %s\n", fname); + + } else { + Serial.println("Could not open file"); + major_fail(); + } + + if (idxfile != NULL) { + + //Serial.printf("File open: %s\n", "/sdcard/idx.tmp"); + + } else { + Serial.println("Could not open file"); + major_fail(); + } + + + for ( i = 0; i < AVIOFFSET; i++) + { + char ch = pgm_read_byte(&avi_header[i]); + buf[i] = ch; + } + + size_t err = fwrite(buf, 1, AVIOFFSET, avifile); + + if (framesize == 6) { + + fseek(avifile, 0x40, SEEK_SET); + err = fwrite(vga_w, 1, 2, avifile); + fseek(avifile, 0xA8, SEEK_SET); + err = fwrite(vga_w, 1, 2, avifile); + fseek(avifile, 0x44, SEEK_SET); + err = fwrite(vga_h, 1, 2, avifile); + fseek(avifile, 0xAC, SEEK_SET); + err = fwrite(vga_h, 1, 2, avifile); + + } else if (framesize == 10) { + + fseek(avifile, 0x40, SEEK_SET); + err = fwrite(uxga_w, 1, 2, avifile); + fseek(avifile, 0xA8, SEEK_SET); + err = fwrite(uxga_w, 1, 2, avifile); + fseek(avifile, 0x44, SEEK_SET); + err = fwrite(uxga_h, 1, 2, avifile); + fseek(avifile, 0xAC, SEEK_SET); + err = fwrite(uxga_h, 1, 2, avifile); + + } else if (framesize == 7) { + + fseek(avifile, 0x40, SEEK_SET); + err = fwrite(svga_w, 1, 2, avifile); + fseek(avifile, 0xA8, SEEK_SET); + err = fwrite(svga_w, 1, 2, avifile); + fseek(avifile, 0x44, SEEK_SET); + err = fwrite(svga_h, 1, 2, avifile); + fseek(avifile, 0xAC, SEEK_SET); + err = fwrite(svga_h, 1, 2, avifile); + + } else if (framesize == 5) { + + fseek(avifile, 0x40, SEEK_SET); + err = fwrite(cif_w, 1, 2, avifile); + fseek(avifile, 0xA8, SEEK_SET); + err = fwrite(cif_w, 1, 2, avifile); + fseek(avifile, 0x44, SEEK_SET); + err = fwrite(cif_h, 1, 2, avifile); + fseek(avifile, 0xAC, SEEK_SET); + err = fwrite(cif_h, 1, 2, avifile); + } + + fseek(avifile, AVIOFFSET, SEEK_SET); + + Serial.print(F("\nRecording ")); + Serial.print(total_frames); + Serial.println(F(" video frames ...\n")); + + startms = millis(); + bigdelta = millis(); + totalp = 0; + totalw = 0; + overtime_count = 0; + jpeg_size = 0; + movi_size = 0; + uVideoLen = 0; + idx_offset = 4; + + + frame_cnt = 0; + frames_so_far = 0; + + skipping = 0; + skipped = 0; + + newfile = 1; + + other_cpu_active = 1; + +} // end of start avi + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// another_save_avi runs on cpu 1, saves another frame to the avi file +// +// the "baton" semaphore makes sure that only one cpu is using the camera subsystem at a time +// + +static esp_err_t another_save_avi() { + + xSemaphoreTake( baton, portMAX_DELAY ); + + if (fb_in == fb_out) { // nothing to do + + xSemaphoreGive( baton ); + + } else { + + fb_out = (fb_out + 1) % fb_max; + + int fblen; + fblen = fb_q[fb_out]->len; + + //xSemaphoreGive( baton ); + + if (BlinkWithWrite) { + digitalWrite(33, LOW); + } + + jpeg_size = fblen; + movi_size += jpeg_size; + uVideoLen += jpeg_size; + + bw = millis(); + size_t dc_err = fwrite(dc_buf, 1, 4, avifile); + size_t ze_err = fwrite(zero_buf, 1, 4, avifile); + + bw = millis(); + size_t err = fwrite(fb_q[fb_out]->buf, 1, fb_q[fb_out]->len, avifile); + if (err == 0 ) { + Serial.println("Error on avi write"); + major_fail(); + } + totalw = totalw + millis() - bw; + + //xSemaphoreTake( baton, portMAX_DELAY ); + esp_camera_fb_return(fb_q[fb_out]); // release that buffer back to the camera system + xSemaphoreGive( baton ); + + remnant = (4 - (jpeg_size & 0x00000003)) & 0x00000003; + + print_quartet(idx_offset, idxfile); + print_quartet(jpeg_size, idxfile); + + idx_offset = idx_offset + jpeg_size + remnant + 8; + + jpeg_size = jpeg_size + remnant; + movi_size = movi_size + remnant; + if (remnant > 0) { + size_t rem_err = fwrite(zero_buf, 1, remnant, avifile); + } + + fileposition = ftell (avifile); // Here, we are at end of chunk (after padding) + fseek(avifile, fileposition - jpeg_size - 4, SEEK_SET); // Here we are the the 4-bytes blank placeholder + + print_quartet(jpeg_size, avifile); // Overwrite placeholder with actual frame size (without padding) + + fileposition = ftell (avifile); + + fseek(avifile, fileposition + 6, SEEK_SET); // Here is the FOURCC "JFIF" (JPEG header) + // Overwrite "JFIF" (still images) with more appropriate "AVI1" + + size_t av_err = fwrite(avi1_buf, 1, 4, avifile); + + fileposition = ftell (avifile); + fseek(avifile, fileposition + jpeg_size - 10 , SEEK_SET); + //Serial.println("Write done"); + //41 totalw = totalw + millis() - bw; + + //if (((fb_in + fb_max - fb_out) % fb_max) > 0 ) { + // Serial.print(((fb_in + fb_max - fb_out) % fb_max)); Serial.print(" "); + //} + + digitalWrite(33, HIGH); + } +} // end of another_pic_avi + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// end_avi runs on cpu 1, empties the queue of frames, writes the index, and closes the files +// + +static esp_err_t end_avi() { + + unsigned long current_end = 0; + + other_cpu_active = 0 ; // shuts down the picture taking program + + //Serial.print(" Write Q: "); Serial.print((fb_in + fb_max - fb_out) % fb_max); Serial.print(" in/out "); Serial.print(fb_in); Serial.print(" / "); Serial.println(fb_out); + + for (int i = 0; i < fb_max; i++) { // clear the queue + another_save_avi(); + } + + //Serial.print(" Write Q: "); Serial.print((fb_in + fb_max - fb_out) % fb_max); Serial.print(" in/out "); Serial.print(fb_in); Serial.print(" / "); Serial.println(fb_out); + + current_end = ftell (avifile); + + Serial.println("End of avi - closing the files"); + + elapsedms = millis() - startms; + float fRealFPS = (1000.0f * (float)frame_cnt) / ((float)elapsedms) * xspeed; + float fmicroseconds_per_frame = 1000000.0f / fRealFPS; + uint8_t iAttainedFPS = round(fRealFPS); + uint32_t us_per_frame = round(fmicroseconds_per_frame); + + + //Modify the MJPEG header from the beginning of the file, overwriting various placeholders + + fseek(avifile, 4 , SEEK_SET); + print_quartet(movi_size + 240 + 16 * frame_cnt + 8 * frame_cnt, avifile); + + fseek(avifile, 0x20 , SEEK_SET); + print_quartet(us_per_frame, avifile); + + unsigned long max_bytes_per_sec = movi_size * iAttainedFPS / frame_cnt; + + fseek(avifile, 0x24 , SEEK_SET); + print_quartet(max_bytes_per_sec, avifile); + + fseek(avifile, 0x30 , SEEK_SET); + print_quartet(frame_cnt, avifile); + + fseek(avifile, 0x8c , SEEK_SET); + print_quartet(frame_cnt, avifile); + + fseek(avifile, 0x84 , SEEK_SET); + print_quartet((int)iAttainedFPS, avifile); + + fseek(avifile, 0xe8 , SEEK_SET); + print_quartet(movi_size + frame_cnt * 8 + 4, avifile); + + Serial.println(F("\n*** Video recorded and saved ***\n")); + Serial.print(F("Recorded ")); + Serial.print(elapsedms / 1000); + Serial.print(F("s in ")); + Serial.print(frame_cnt); + Serial.print(F(" frames\nFile size is ")); + Serial.print(movi_size + 12 * frame_cnt + 4); + Serial.print(F(" bytes\nActual FPS is ")); + Serial.print(fRealFPS, 2); + Serial.print(F("\nMax data rate is ")); + Serial.print(max_bytes_per_sec); + Serial.print(F(" byte/s\nFrame duration is ")); Serial.print(us_per_frame); Serial.println(F(" us")); + Serial.print(F("Average frame length is ")); Serial.print(uVideoLen / frame_cnt); Serial.println(F(" bytes")); + Serial.print("Average picture time (ms) "); Serial.println( totalp / frame_cnt ); + Serial.print("Average write time (ms) "); Serial.println( totalw / frame_cnt ); + Serial.print("Frames Skipped % "); Serial.println( 100.0 * skipped / total_frames, 1 ); + + Serial.println("Writing the index"); + + fseek(avifile, current_end, SEEK_SET); + + fclose(idxfile); + + size_t i1_err = fwrite(idx1_buf, 1, 4, avifile); + + print_quartet(frame_cnt * 16, avifile); + + idxfile = fopen("/sdcard/idx.tmp", "r"); + + if (idxfile != NULL) { + + //Serial.printf("File open: %s\n", "/sdcard/idx.tmp"); + + } else { + Serial.println("Could not open file"); + //major_fail(); + } + + char * AteBytes; + AteBytes = (char*) malloc (8); + + for (int i = 0; i < frame_cnt; i++) { + size_t res = fread ( AteBytes, 1, 8, idxfile); + size_t i1_err = fwrite(dc_buf, 1, 4, avifile); + size_t i2_err = fwrite(zero_buf, 1, 4, avifile); + size_t i3_err = fwrite(AteBytes, 1, 8, avifile); + } + + free(AteBytes); + fclose(idxfile); + fclose(avifile); + int xx = remove("/sdcard/idx.tmp"); + + Serial.println("---"); + +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// do_fb - just takes a picture and discards it +// + +static esp_err_t do_fb() { + xSemaphoreTake( baton, portMAX_DELAY ); + camera_fb_t * fb = esp_camera_fb_get(); + + Serial.print("Pic, len="); Serial.println(fb->len); + + esp_camera_fb_return(fb); + xSemaphoreGive( baton ); +} + +void do_time() { + + if (WiFi.status() != WL_CONNECTED) { + + Serial.println("***** WiFi reconnect *****"); + WiFi.reconnect(); + delay(5000); + + if (WiFi.status() != WL_CONNECTED) { + Serial.println("***** WiFi rerestart *****"); + init_wifi(); + } + + sprintf(localip, "%s", WiFi.localIP().toString().c_str()); + } + +} + +//////////////////////////////////////////////////////////////////////////////////// +// +// some globals for the loop() +// + +long wakeup; +long last_wakeup = 0; + + +void loop() +{ + + wakeup = millis(); + if (wakeup - last_wakeup > (14 * 60 * 1000) ) { // 14 minutes + last_wakeup = millis(); + + do_time(); + } + + ftpSrv.handleFTP(); + + //delay(1); + +} + + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// + +static esp_err_t capture_handler(httpd_req_t *req) { + + camera_fb_t * fb = NULL; + esp_err_t res = ESP_OK; + char fname[100]; + //xSemaphoreTake( baton, portMAX_DELAY ); + fb = esp_camera_fb_get(); + + if (!fb) { + Serial.println("Camera capture failed"); + httpd_resp_send_500(req); + xSemaphoreGive( baton ); + return ESP_FAIL; + } + + file_number++; + + sprintf(fname, "inline; filename=capture_%d.jpg", file_number); + + httpd_resp_set_type(req, "image/jpeg"); + httpd_resp_set_hdr(req, "Content-Disposition", fname); + + size_t out_len, out_width, out_height; + size_t fb_len = 0; + fb_len = fb->len; + res = httpd_resp_send(req, (const char *)fb->buf, fb->len); + esp_camera_fb_return(fb); + //xSemaphoreGive( baton ); + return res; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// +static esp_err_t stop_handler(httpd_req_t *req) { + + esp_err_t res = ESP_OK; + + recording = 0; + Serial.println("stopping recording"); + + do_stop("Stopping previous recording"); + + httpd_resp_send(req, the_page, strlen(the_page)); + return ESP_OK; + +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// +static esp_err_t start_handler(httpd_req_t *req) { + + esp_err_t res = ESP_OK; + + char buf[80]; + size_t buf_len; + char new_res[20]; + + if (recording == 1) { + const char* resp = "You must Stop recording, before starting a new one. Start over ..."; + httpd_resp_send(req, resp, strlen(resp)); + + return ESP_OK; + return res; + + } else { + //recording = 1; + Serial.println("starting recording"); + + sensor_t * s = esp_camera_sensor_get(); + + int new_interval = capture_interval; + int new_length = capture_interval * total_frames; + + int new_framesize = s->status.framesize; + int new_quality = s->status.quality; + int new_repeat = 0; + int new_xspeed = 1; + int new_xlength = 3; + int new_gray = 0; + + /* + Serial.println(""); + Serial.println("Current Parameters :"); + Serial.print(" Capture Interval = "); Serial.print(capture_interval); Serial.println(" ms"); + Serial.print(" Length = "); Serial.print(capture_interval * total_frames / 1000); Serial.println(" s"); + Serial.print(" Quality = "); Serial.println(new_quality); + Serial.print(" Framesize = "); Serial.println(new_framesize); + Serial.print(" Repeat = "); Serial.println(repeat); + Serial.print(" Speed = "); Serial.println(xspeed); + */ + + buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) { + if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) { + ESP_LOGI(TAG, "Found URL query => %s", buf); + char param[32]; + /* Get value of expected key from query string */ + //Serial.println(" ... parameters"); + if (httpd_query_key_value(buf, "length", param, sizeof(param)) == ESP_OK) { + + int x = atoi(param); + if (x >= 5 && x <= 3600 * 24 ) { // 5 sec to 24 hours + new_length = x; + } + + ESP_LOGI(TAG, "Found URL query parameter => length=%s", param); + + } + if (httpd_query_key_value(buf, "repeat", param, sizeof(param)) == ESP_OK) { + int x = atoi(param); + if (x >= 0 ) { + new_repeat = x; + } + + ESP_LOGI(TAG, "Found URL query parameter => repeat=%s", param); + } + if (httpd_query_key_value(buf, "framesize", new_res, sizeof(new_res)) == ESP_OK) { + if (strcmp(new_res, "UXGA") == 0) { + new_framesize = 10; + } else if (strcmp(new_res, "SVGA") == 0) { + new_framesize = 7; + } else if (strcmp(new_res, "VGA") == 0) { + new_framesize = 6; + } else if (strcmp(new_res, "CIF") == 0) { + new_framesize = 5; + } else { + Serial.println("Only UXGA, SVGA, VGA, and CIF are valid!"); + + } + ESP_LOGI(TAG, "Found URL query parameter => framesize=%s", new_res); + } + if (httpd_query_key_value(buf, "quality", param, sizeof(param)) == ESP_OK) { + + int x = atoi(param); + if (x >= 10 && x <= 50) { // MINIMUM QUALITY 10 to save memory + new_quality = x; + } + + ESP_LOGI(TAG, "Found URL query parameter => quality=%s", param); + } + + if (httpd_query_key_value(buf, "speed", param, sizeof(param)) == ESP_OK) { + + int x = atoi(param); + if (x >= 1 && x <= 100) { + new_xspeed = x; + } + + ESP_LOGI(TAG, "Found URL query parameter => speed=%s", param); + } + + if (httpd_query_key_value(buf, "gray", param, sizeof(param)) == ESP_OK) { + + int x = atoi(param); + if (x == 1 ) { + new_gray = x; + } + + ESP_LOGI(TAG, "Found URL query parameter => gray=%s", param); + } + + if (httpd_query_key_value(buf, "interval", param, sizeof(param)) == ESP_OK) { + + int x = atoi(param); + if (x >= 1 && x <= 180000) { // 180,000 ms = 3 min + new_interval = x; + } + ESP_LOGI(TAG, "Found URL query parameter => interval=%s", param); + } + } + } + + framesize = new_framesize; + capture_interval = new_interval; + xlength = new_length; + total_frames = new_length * 1000 / capture_interval; + repeat = new_repeat; + quality = new_quality; + xspeed = new_xspeed; + gray = new_gray; + + do_start("Starting a new AVI"); + httpd_resp_send(req, the_page, strlen(the_page)); + + recording = 1; + return ESP_OK; + } +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// +void do_start(char *the_message) { + + Serial.print("do_start "); Serial.println(the_message); + + const char msg[] PROGMEM = R"rawliteral( + + + + +%s ESP32-CAM Video Recorder + + +

%s
ESP32-CAM Video Recorder %s


+

Message is %s


+ Recording = %d (1 is active)
+ Capture Interval = %d ms
+ Length = %d seconds
+ Quality = %d (10 best to 50 worst)
+ Framesize = %d (10 UXGA, 7 SVGA, 6 VGA, 5 CIF)
+ Repeat = %d
+ Speed = %d
+ Gray = %d

+ +
+
+ + +)rawliteral"; + + + sprintf(the_page, msg, devname, devname, vernum, the_message, recording, capture_interval, capture_interval * total_frames / 1000, quality, framesize, repeat, xspeed, gray); + //Serial.println(strlen(msg)); + +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// +void do_stop(char *the_message) { + + Serial.print("do_stop "); Serial.println(the_message); + + const char msg[] PROGMEM = R"rawliteral( + + + + +%s ESP32-CAM Video Recorder + + +

%s
ESP32-CAM Video Recorder %s


+

Message is %s


+

http://%s/

+

http://%s/start?framesize=VGA&length=1800&interval=100&quality=10&repeat=100&speed=1&gray=0

+

VGA 2 fps, for 30 minutes repeat, 30x playback

+

UXGA 1 sec per frame, for 30 minutes repeat, 30x playback

+

UXGA 2 fps for 30 minutes repeat, 15x playback

+

CIF 20 fps second for 30 minutes repeat

+
+ +)rawliteral"; + + sprintf(the_page, msg, devname, devname, vernum, the_message, localip, localip, localip, localip, localip, localip, localip, localip); + +} + + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// +void do_status(char *the_message) { + + Serial.print("do_status "); Serial.println(the_message); + + const char msg[] PROGMEM = R"rawliteral( + + + + +%s ESP32-CAM Video Recorder + + +

%s
ESP32-CAM Video Recorder %s
%s


+

Message is %s


+ Total SD Space is %d MB, Used SD Space is %d MB
+ Recording = %d (1 is active)
+ Frame %d of %d, Skipped %d

+ Capture Interval = %d ms
+ Length = %d seconds
+ Quality = %d (10 best to 50 worst)
+ Framesize = %d (10 UXGA, 7 SVGA, 6 VGA, 5 CIF)
+ Repeat = %d
+ Playback Speed = %d
+ Gray = %d

+ Commands as follows for your ESP's ip address:
+

http://%s/

+

http://%s/stop ... and restart
You must be stopped before restart or PIR

+

ftp://%s/

+ Username: esp, Password: esp ... to download the files

+ Red LED on back of ESP will flash with every frame (or skipped frames), and flash SOS if camera or sd card not working.
+ +
+Check camera position with the frame below
+Refresh page for more.
+
+
+ +)rawliteral"; + + time(&now); + const char *strdate = ctime(&now); + + //Serial.printf("Total space: %lluMB\n", SD_MMC.totalBytes() / (1024 * 1024)); + //Serial.printf("Used space: %lluMB\n", SD_MMC.usedBytes() / (1024 * 1024)); + + int tot = SD_MMC.totalBytes() / (1024 * 1024); + int use = SD_MMC.usedBytes() / (1024 * 1024); + + //Serial.print(strlen(msg)); Serial.print(" "); + + sprintf(the_page, msg, devname, devname, vernum, strdate, the_message, tot, use, recording, frames_so_far, total_frames, skipped, capture_interval, capture_interval * total_frames / 1000, quality, framesize, repeat, xspeed, gray, localip, localip, localip, localip, localip, localip); + + //Serial.println(strlen(the_page)); +} + + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// +static esp_err_t index_handler(httpd_req_t *req) { + + do_status("Refresh Status"); + httpd_resp_send(req, the_page, strlen(the_page)); + return ESP_OK; +} + +void startCameraServer() { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + httpd_uri_t index_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = index_handler, + .user_ctx = NULL + }; + httpd_uri_t capture_uri = { + .uri = "/capture", + .method = HTTP_GET, + .handler = capture_handler, + .user_ctx = NULL + }; + + httpd_uri_t file_stop = { + .uri = "/stop", + .method = HTTP_GET, + .handler = stop_handler, + .user_ctx = NULL + }; + + httpd_uri_t file_start = { + .uri = "/start", + .method = HTTP_GET, + .handler = start_handler, + .user_ctx = NULL + }; + + if (httpd_start(&camera_httpd, &config) == ESP_OK) { + httpd_register_uri_handler(camera_httpd, &index_uri); + httpd_register_uri_handler(camera_httpd, &capture_uri); + httpd_register_uri_handler(camera_httpd, &file_start); + httpd_register_uri_handler(camera_httpd, &file_stop); + } + + Serial.println("Camera http started"); +}