Outils pour utilisateurs

Outils du site


projets:esp32-cam_timelaps_code
/*
  ESP32-CAM l'oeil clignotemps
  TimeLapseAvi

  ESP32-CAM enregistreur Video pour CAMERA_MODEL_AI_THINKER

  by James Zahary July 20, 2019  TimeLapseAvi23x.ino
  jamzah.plc@gmail.com  https://github.com/jameszah/ESP32-CAM-Video-Recorder
  This program records an AVI video on the SD Card of an ESP32-CAM.
    jameszah/ESP32-CAM-Video-Recorder is licensed under the
    GNU General Public License v3.0

  modif gepeto@du-libre.org https://snhack.org/doku.php?id=projets:esp32-cam_ftp_avi_timelaps

  The is Arduino code, with standard setup for ESP32-CAM
    - Board ESP32 Wrover Module
    - Partition Scheme Huge APP (3MB No OTA)

  //server.on("/", index_handler);
  server.on("/capture",    capture_handler);
  server.on("/start",      start_handler);
  server.on("/stop",       stop_handler);
  server.on("/saveconfig", saveSPIFFSConfigFile);
  server.on("/list",       listDir_handler);
  server.on("/reset",      reset_handler);

  Accet http://esp32-cam.local (devname dans le fichier config.jsn)
  /sdcard/config.jsn:
  {"devname":"esp32-cam",
   "framesize":10,
   "repeat":0,
   "xspeed":1,
   "gray":0,
   "quality":10,
   "capture_interval":10000,
   "length":1800,
   "ssid":"wifissid",
   "pass":"trucpass",
   "horaire":"6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21"
   }
*/
//
// WARNING!!! Make sure that you have either selected ESP32 Wrover Module,
//            or another board which has PSRAM enabled
//

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//  edit these parameters for your needs

static const char vernum[] = "v10Gepeto"; // modif AccesPoint and http filesystem
// plage horaire
#include <dummy.h>
// 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
static const char devname_conf[] = "epscam";   // nom de secour
char devname[20] = "espcam";
// EDIT ssid and pass
char ssid[40] = "";
char pass[20] = "";
const char ssid_ap[] = "ESPCAM";
const char pass_ap[] = "1234567890";
int invisible      = 0;
int canal_wifi     = 6;
int max_connexion  = 4;
int numero_fichier = 1; // si pas de serveur ntp retour a un index dans /sdcard/nofile.txt
int connAttempts = 0; // si >10 mode AP
bool bouton_init_ok = false;
bool sleep_mode = false;
long sleep_duree =  10 ;
char horaire[80]  = {0};// heures d'enregistrement
char conf_horaire[]  = "6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21";// heures d'enregistrement
#define TIMEZONE "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" // de https://remotemonitoringsystems.ca/time-zone-abbreviations.php
// startup defaults for first recordin
// here are the recording options from the "retart web page"
// framesize=UXGA&length=1800&interval=10000&quality=10&repeat=100&speed=1&gray=0
// 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 ResetPin = 13; // pin pour ouverture de la config
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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 ESP_getChipId()   ((uint32_t)ESP.getEfuseMac())

//#define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE
#include "esp_log.h"
#include "esp_camera.h"

#include <ESPmDNS.h>


#include <FS.h>
#include "SPIFFS.h"

// Now support ArduinoJson 6.0.0+ ( tested with v6.14.1 )
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>

//#include <ESP32WebServer.h> // a changer dans src de ESP_WiFiManager
//#include <ESP_WiFiManager.h>              //https://github.com/khoih-prog/ESP_WiFiManager
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager
//default custom static IP
char static_ip[16] = "10.0.1.56";
char static_gw[16] = "10.0.1.1";
char static_sn[16] = "255.255.255.0";
//FtpServer ftpSrv;   //set #define FTP_DEBUG in ESP32FtpServer.h to see ftp verbose on serial
//#define FTP_DEBUG in ESP32FtpServer.h to see ftp verbose on serial

File fsUploadFile;
File list_file;
File f_log;

// 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 <SD_MMC.h>

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;
int internet_connected = 0; // 0 non connecté , 1 mode sta , 2 freewifi, 3 mode AP
struct tm timeinfo;
time_t now;
char localip[20] = {};
char buf_config[513] = {0};
size_t buftaille = 0;

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;
FILE *nofile = NULL;
FILE *configSD = NULL;
FILE *ficnofic = 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,
};

#define LED_BUILTIN       2
#define LED_ON            HIGH
#define LED_OFF           LOW

// Pin D2 mapped to pin GPIO2/ADC12 of ESP32, or GPIO2/TXD1 of NodeMCU control on-board LED
#define PIN_LED       LED_BUILTIN

////////////// test horaire ////////////////////

void Test_horaires() {   // cherche si heure maintenant contenue dans horaires
  Serial.print ( timeinfo.tm_year) ;
  if ( timeinfo.tm_year < 110)  { // date year deouis 1900
    return;
  } else {
    Serial.println(F(" test horaire"));
    int heure = 0 ;
    int taille_plages = strlen(horaire);
    if (taille_plages > 3 ) {
      char buf[3] = "00";
      for (int i = 0; i < taille_plages; i++) {
        buf[0] = horaire[i++];
        if (horaire[i] != ',' && horaire[i] != ';' && horaire[i] != ' ') buf[1] = horaire[i++];
        else buf[1] = '\0';
        buf[2] = '\0';
        heure = atoi(buf);
        time(&now);
        localtime_r(&now, &timeinfo);
        //Serial.print(F("heures "));
        //Serial.println(heure);
        if ((int)timeinfo.tm_hour  == heure) {
          //Serial.print(F("  "));
          //Serial.println(strftime_buf);
          heure = 99;
          break;
        }
      }
      int reste = 58 - (int)timeinfo.tm_min;
      if (reste > 3 && heure < 90) {
        //end_avi();
        Serial.print(F("Sleep "));
        Serial.println(reste);
        esp_sleep_enable_timer_wakeup(1000000 * 60 * (uint64_t)reste);
        esp_deep_sleep_start();
        delay(1000);
      }
    } else Serial.println(F("ok"));
  }
}
///
/// SPIFFS/SD config
///
bool readFileconfig() {
  Serial.println(F("/sdcard/config.jsn"));
  configSD = fopen("/sdcard/config.jsn", "r");
  if (configSD == NULL) {
    Serial.println(F("pas sur la SD"));
    return false;
  }
  //Serial.write(configSD.read());
  fseek(configSD, 0, SEEK_END);
  int bufftaille = ftell(configSD);
  fseek(configSD, 0, SEEK_SET);
  while (!feof(configSD)) {
    fread(buf_config, bufftaille + 1, 1, configSD);
  }
  fclose(configSD);
  Serial.println(buf_config);
  return true;
}
void saveFileconfigSD() {
  configSD = fopen("/sdcard/config.jsn", "w");
  if (configSD == NULL) {
    Serial.println(F("Failed to open /sdcard/config.jsn for writing"));
  }
  DynamicJsonDocument json(512);
  json["devname"]            = devname;
  json["quality"]            = quality;
  json["framesize"]          = framesize;
  json["capture_interval"]   = capture_interval;
  json["repeat"]             = repeat;
  json["xspeed"]             = xspeed;
  json["length"]             = xlength;
  json["gray"]               = gray;
  json["ssid"]               = ssid;
  json["pass"]               = pass;
  json["horaire"]            = horaire;
  serializeJson(json, buf_config);
  fwrite(buf_config, sizeof(buf_config), 1, configSD);
  fclose(configSD);
}

bool loadSPIFFSConfigFile(void)
{
  //clean FS, for testing
  //SPIFFS.format();
  if (SPIFFS.begin())
  {
    //read configuration from FS json
    Serial.println(F("Mounting FS..."));
    if (readFileconfig() == true) {
      //Serial.print(F("flash config "));
      //Serial.println(buf_config);
      File configFile = SPIFFS.open("/config.jsn", "w");
      configFile.print(buf_config);
      configFile.flush();
      configFile.close();
      delay(100);
    }
    if (SPIFFS.exists("/config.jsn"))
    {
      Serial.println(F("Reading config file"));
      File configFile = SPIFFS.open("/config.jsn", "r");
      if (configFile)
      {
        Serial.print(F("Opened config file, size = "));
        size_t configFileSize = configFile.size();
        Serial.println(configFileSize);

        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[configFileSize + 1]);

        configFile.readBytes(buf.get(), configFileSize);

        Serial.print(F("\nJSON parseObject() result : "));

        DynamicJsonDocument json(512);
        auto deserializeError = deserializeJson(json, buf.get(), configFileSize);

        if ( deserializeError )
        {
          Serial.println(F("failed"));
          return false;
        }
        else
        {
          if (json["framesize"])        framesize = json["framesize"] | framesize;
          if (json["blynk_server"])     repeat    = json["repeat"] | repeat;
          if (json["xspeed"])           xspeed    = json["blynk_port"] | xspeed;
          if (json["gray"])             gray      = json["gray"] | gray;
          if (json["quality"])          quality   = json["quality"] | quality ;
          if (json["capture_interval"]) capture_interval =  json["capture_interval"] | capture_interval;
          if (json["length"])           xlength =  json["length"] | xlength;
          if (json["ssid"])             strlcpy (ssid , json["ssid"], sizeof(ssid));
          else strlcpy (ssid , ssid_ap, sizeof(ssid));
          if (json["pass"])             strlcpy (pass , json["pass"], sizeof(pass));
          else strlcpy (pass , pass_ap, sizeof(pass));
          if (json["devname"])          strlcpy (devname , json["devname"], sizeof(devname));
          else strlcpy (devname , devname_conf, sizeof(devname));
          if (json["horaire"])          strlcpy (horaire , json["horaire"], sizeof(horaire));
          else strlcpy (horaire , conf_horaire, sizeof(horaire));
        }

        //serializeJson(json, Serial);
        Serial.println(F("json :"));
        serializeJsonPretty(json, Serial);
        configFile.close();
        Serial.println(F("OK"));
        total_frames = xlength * 1000 / capture_interval;

      }
    }
  }
  else
  {
    Serial.println(F("failed to mount FS"));
    return false;
  }
  return true;
}
////
////
////
void heartBeatPrint(void)
{
  static int num = 1;

  if (WiFi.status() == WL_CONNECTED)
    Serial.print(F("H"));        // H means connected to WiFi
  else
    Serial.print(F("F"));        // F means not connected to WiFi

  if (num == 80)
  {
    Serial.println();
    num = 1;
  }
  else if (num++ % 10 == 0)
  {
    Serial.print(F(" "));
  }
}

void toggleLED()
{
  //toggle state
  digitalWrite(PIN_LED, !digitalRead(PIN_LED));
}

void check_status()
{
  static ulong checkstatus_timeout  = 0;
  static ulong LEDstatus_timeout    = 0;
  static ulong currentMillis;

#define HEARTBEAT_INTERVAL    10000L
#define LED_INTERVAL          2000L

  currentMillis = millis();

  if ((currentMillis > LEDstatus_timeout) || (LEDstatus_timeout == 0))
  {
    // Toggle LED at LED_INTERVAL = 2s
    toggleLED();
    LEDstatus_timeout = currentMillis + LED_INTERVAL;
  }

  // Print hearbeat every HEARTBEAT_INTERVAL (10) seconds.
  if ((currentMillis > checkstatus_timeout) || (checkstatus_timeout == 0))
  {
    heartBeatPrint();
    checkstatus_timeout = currentMillis + HEARTBEAT_INTERVAL;
  }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// 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(F(" 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. ");
            delay(100);
          }

          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);
}

WebServer server(80);
void startCameraServer();
//httpd_handle_t camera_httpd = NULL;

char the_page[3000];

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);
  Serial.println("                                    ");
  Serial.printf("ESP-CAM Video Recorder %s\n", vernum);

  // SD camera init
  card_err = init_sdcard();
  if (card_err != true) {
    Serial.printf("init sd error 0x%x", card_err);
    major_fail();
  }
  loadSPIFFSConfigFile();

  //Serial.printf(" http://%s.local - to access the camera\n", devname);

  pinMode(33, OUTPUT);    // little red led on back of chip
  digitalWrite(33, LOW);           // turn on the red LED on the back of chip
  if (!psramFound()) {
    Serial.println("paraFound wrong - major fail");
    //major_fail();
  }
  init_wifi();
  Test_horaires();
  startCameraServer();

  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(F("Camera Ready! Use http://"));
  Serial.print(WiFi.localIP());
  Serial.print(F(": "));
  Serial.print(localip);
  Serial.print(F(": "));
  Serial.println(WiFi.softAPIP());
  Serial.println(F(" to connect"));
}
bool init_wifi()
{
  if (internet_connected < 3) { // que si pas deja en AP
    WiFi.disconnect(true);
    WiFi.mode(WIFI_STA);
    WiFi.setHostname(devname);
    WiFi.begin(ssid, pass);
    delay(1000);
    while (WiFi.status() != WL_CONNECTED ) {
      delay(1000);
      Serial.print(F("."));
      if (connAttempts == 10) {
        Serial.println(F("tentative FreeWifi ntp")); // juste pour ntp
        WiFi.begin("FreeWifi");
        delay(2000);
        if (WiFi.SSID() == (String)"FreeWifi") {
          Serial.println(F("Temps FreeWifi"));
          internet_connected = 2;
        }
        delay(1000);
      } 
      if(connAttempts > 13) { // laisse tomber, passage AP
        internet_connected = 3;
        break;
      }
      connAttempts++;
    }
  }
  if (internet_connected == 0) internet_connected = 1;
  if (internet_connected < 3) {
    Serial.println(WiFi.SSID());
    char pool[16] = "";
    if ( internet_connected == 2) {
      sprintf(pool, "%s", WiFi.gatewayIP().toString().c_str());
    } else {
      sprintf(pool, "pool.ntp.org");
    }
    Serial.print(F("Attente ntp de "));
    Serial.print(pool);
    configTime(0, 0, pool);
    setenv("TZ", TIMEZONE, 1);  // Paris time zone from #define at top
    tzset();
    time_t now ;
    timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    delay(2000);
    time(&now);
    localtime_r(&now, &timeinfo);
    Serial.print(F(" : "));
    Serial.println(ctime(&now));
    Serial.print(F(" - "));

    while (!time(nullptr) && ++retry < retry_count) {
      Serial.printf("... (%d/%d) -- %d\n", retry, retry_count, timeinfo.tm_year);
      delay(1000);
      time(&now);
      localtime_r(&now, &timeinfo);
      Serial.println(ctime(&now));
      Serial.print(F(" ok ntp "));
    }
    sprintf(localip, "%s", WiFi.localIP().toString().c_str());
  }
  if (internet_connected >= 2) {
    Serial.print(F("\nPassage en Point d acces..."));
    WiFi.disconnect();
    delay(500);
    WiFi.mode(WIFI_AP);
    delay(1000);
    WiFi.softAP(ssid_ap, pass_ap, canal_wifi, invisible, max_connexion);
    delay(1000);
    Serial.print(F("ouverture paswd de "));
    Serial.print(ssid_ap);
    Serial.print(F(" "));
    Serial.println(pass_ap);
    internet_connected = 3;
    delay(1000);
    sprintf(localip, "%s", WiFi.softAPIP().toString().c_str());
    Serial.println(WiFi.softAPIP());
    Serial.println(WiFi.localIP());
    if (!MDNS.begin(devname)) {
      Serial.println(F("Error MDNS !"));
    } else {
      Serial.printf("mDNS started '%s'\n", devname);
    }
  }
  Serial.println(localip);
  return true;
}

bool 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(F("Mounting SD card..."));
  ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card);

  if (ret == ESP_OK) {
    Serial.println(F("SD mount ok!"));
  }  else  {
    Serial.printf("err mount SD card VFAT Error: %s", esp_err_to_name(ret));
    major_fail();
    return false;
  }
  sdmmc_card_print_info(stdout, card);
  Serial.print("SD_MMC Begin: "); Serial.println(SD_MMC.begin());   // required by ftp system ??

  listDirSD(SD_MMC, "/", 0);
  return true;
}

void listDirSD(fs::FS & fs, const char * dirname, uint8_t levels) {
  Serial.printf("Listing directory: %s\n", dirname);

  File root = fs.open(dirname);
  if (!root) {
    Serial.println(F("err open directory"));
    return;
  }
  if (!root.isDirectory()) {
    Serial.println(F("Not a dir"));
    return;
  }
  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      Serial.print(F("  DIR : "));
      Serial.println(file.name());
      if (levels) {
        listDirSD(fs, file.name(), levels - 1);
      }
    } else {
      Serial.print(F("  FILE: "));
      Serial.print(file.name());
      Serial.print(F("  SIZE: "));
      Serial.println(file.size());
    }
    file = root.openNextFile();
  }
  return;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// 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(F("Add 10 s"));
      }
    } else {
      if ( recording == 0 && newfile == 0) {

        //start a pir recording with current parameters, except no repeat and 15 seconds
        Serial.println(F("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(F("ok capture 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(F("ok 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 (F(" ")); Serial.println(F("Done capture for time"));
          Serial.print(F("Time Elapsed: ")); Serial.print(millis() - startms); Serial.print(F(" Frames: ")); Serial.println(frame_cnt);
          Serial.print(F("Config:       ")); Serial.print(total_frames * capture_interval ) ; Serial.print(F(" ("));
          Serial.print(total_frames); Serial.print(F(" x ")); Serial.print(capture_interval);  Serial.println(F(")"));

          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;
          }
          //// Sleep si necessaire si personne n'est connecté entre 2 fichiers
          if (sleep_mode == true && WiFi.softAPgetStationNum() < 1) {
            Serial.print(F("Sleep "));
            Serial.println(sleep_duree);
            esp_sleep_enable_timer_wakeup(1000000 * 60 * (uint64_t)sleep_duree);
            esp_deep_sleep_start();
            delay(1000);
          }
        } 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 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);
  }
}
void nofichiersave(fs::FS & fs, const char * nomfic) {
  File ficnofic = fs.open(nomfic, FILE_WRITE);
  if (!ficnofic) {
    Serial.println(F("err open /nofic.jsn writing"));
  }
  DynamicJsonDocument json(32);
  json["nofichier"]  = numero_fichier;
  serializeJson(json, ficnofic);
}
int nofichierread(fs::FS & fs, const char * nomfic) {
  File ficnofic = fs.open(nomfic, FILE_READ);
  if (!ficnofic) {
    Serial.println(F("err open /nofic.jsn reading"));
  }
  DynamicJsonDocument json(32);
  DeserializationError error = deserializeJson(json, ficnofic);
  if (error) {
    Serial.println(F("err json /nofic.jsn"));
    ficnofic.println();
    return (numero_fichier);
  }
  numero_fichier = json["nofichier"];
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// start_avi - open the files and write in headers
//

static esp_err_t start_avi() {
  Serial.println(F("Starting an avi "));
  config_camera();
  if ( (int)timeinfo.tm_year > 110 ) {
    time(&now);
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%F_%H.%M.%S", &timeinfo);
  } else {
    nofichierread(SD_MMC, "/nofic.jsn");
    sprintf(strftime_buf, "%d", numero_fichier);
  }

  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(F("\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("err open file");
    major_fail();
  }
  if (idxfile != NULL)  {
    //Serial.printf("File open: %s\n", "/sdcard/idx.tmp");
  }  else  {
    Serial.println(F("err 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(F("Err 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(F(" ");
    //}
    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(F("End of avi - close 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 ok,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(F("Average picture time (ms) ")); Serial.println( totalp / frame_cnt );
  Serial.print(F("Average write time (ms)   ")); Serial.println( totalw / frame_cnt );
  Serial.print(F("Frames Skipped % "));  Serial.println( 100.0 * skipped / total_frames, 1 );

  Serial.println(F("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(F("err 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(F("---"));

  // incremente le numero_fichier
  if ( (int)timeinfo.tm_year < 110) {
    numero_fichier++;
    nofichiersave(SD_MMC, "/nofic.jsn");
    sprintf(strftime_buf, "%d", numero_fichier);
  }
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//  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(F("Pic, len=")); Serial.println(fb->len);
  esp_camera_fb_return(fb);
  xSemaphoreGive( baton );
}
//
// if we have no camera, or sd card, then flash rear led on and off to warn the human SOS - SOS
//
void major_fail() {
  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(F("Major Fail  ")); Serial.print(i); Serial.print(F(" / ")); Serial.println(10);
  }
  ESP.restart();
}

void do_time() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println(F("** WiFi reconnect **"));
    WiFi.reconnect();
    delay(5000);
    if (WiFi.status() != WL_CONNECTED) {
      Serial.println(F("** WiFi rerestart **"));
      init_wifi();
    }
  }
  Test_horaires();
}

////////////////////////////////////////////////////////////////////////////////////
//
// 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();
  server.handleClient();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
// save photo stuff not currently used
static esp_err_t save_photo()
{
  Serial.print(F("Taking picture: /capture.jpg"));
  camera_fb_t *fb = esp_camera_fb_get();

  char *filename = (char*)malloc(21 + sizeof(int));
  sprintf(filename, "/sdcard/capture.jpg");

  Serial.println(filename);
  FILE *file = fopen(filename, "w");
  if (file != NULL)  {
    size_t err = fwrite(fb->buf, 1, fb->len, file);
    Serial.printf("File saved: %s\n", filename);
  }  else  {
    Serial.println(F("err open file"));
  }
  fclose(file);
  esp_camera_fb_return(fb);
  free(filename);
}
void capture_handler() {
  save_photo();

  time(&now);
  const char *strdate = ctime(&now);
  const char msg[] PROGMEM = R"rawliteral(<!doctype html>
  <head>
    <title>ESP32 Photo</title>
    <style>
      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
    </style>
  </head>
  <body>
    <h1>Capture</h1>
    <p>Date: %s</p>
    <img src="/capture.jpg" />
    <br>
    <h3><a href="http://%s/">http://%s/</a></h3>
  </body>
</html>)rawliteral";
  sprintf(the_page, msg,  strdate, localip, localip);
  server.send(200, "text/html", the_page);

  delay(1000); // pour eviter les prise en rafale
}

static esp_err_t save_photo_numbered()
{
  file_number++;
  Serial.print(F("Taking picture: "));
  Serial.print(file_number);
  camera_fb_t *fb = esp_camera_fb_get();

  char *filename = (char*)malloc(21 + sizeof(int));
  sprintf(filename, "/sdcard/capture_%d.jpg", file_number);

  Serial.println(filename);
  FILE *file = fopen(filename, "w");
  if (file != NULL)  {
    size_t err = fwrite(fb->buf, 1, fb->len, file);
    Serial.printf("File saved: %s\n", filename);
  }  else  {
    Serial.println(F("err open file"));
  }
  fclose(file);
  esp_camera_fb_return(fb);
  free(filename);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
//
void do_start(char *the_message) {

  Serial.print(F("do_start ")); Serial.println(the_message);

  const char msg[] PROGMEM = R"rawliteral(<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s ESP32-CAM start</title>
</head>
<body>
<h1>%s<br>ESP32-CAM Video Recorder %s </h1><br>
 <h3>Message is <font color="red">%s</font></h3><br>
 Recording = %d (1 is active)<br>
 Capture Interval = %d ms<br>
 Length = %d seconds<br>
 Quality = %d (10 best to 50 worst)<br>
 Framesize = %d (10 UXGA, 7 SVGA, 6 VGA, 5 CIF)<br>
 Repeat = %d<br>
 Speed = %d<br>
 Gray = %d<br><br>
<br>
<br><div id="image-container"></div>
<h3><a href="http://%s/">http://%s/ retour au status</a></h3>

</body>
</html>)rawliteral";

  sprintf(the_page, msg, devname, devname, vernum, the_message, recording, capture_interval, capture_interval * total_frames / 1000, quality, framesize, repeat, xspeed, gray, localip, localip);
  //Serial.println(strlen(msg));

}
bool start_handler() {
  char  buf[80];
  size_t buf_len;
  char  new_res[20];

  if (recording == 1) {
    server.send(200, "text/html", "You must Stop recording, before starting a new one.  Start over ...");
    return false;

  } else {
    //recording = 1;
    Serial.println(F("start 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);
    */

    for (int i = 0; i < server.args(); i++) {
      Serial.print(server.argName(i));
      Serial.print(F(" "));
      Serial.println(server.arg(i));
      int len = server.arg(i).length() + 1;
      char retour[20] = {0};
      server.arg(i).toCharArray(retour, len);

      if (server.argName(i) == "length") {
        int x = atoi(retour);
        if (x >= 1 && x <= 3600 * 24 ) {   // 1 sec to 24 hours
          new_length = x;
        }
      }
      else if (server.argName(i) == "repeat") {
        int x = atoi(retour);
        if (x >= 0  ) {
          new_repeat = x;
        }
      }
      else if (server.argName(i) == "framesize") {
        if (strcmp(retour, "UXGA") == 0) {
          new_framesize = 10;
        } else if (strcmp(retour, "SVGA") == 0) {
          new_framesize = 7;
        } else if (strcmp(retour, "VGA") == 0) {
          new_framesize = 6;
        } else if (strcmp(retour, "CIF") == 0) {
          new_framesize = 5;
        } else {
          Serial.println(F("Only UXGA, SVGA, VGA, and CIF are valid!"));

        }
      }
      else if (server.argName(i) == "quality") {
        int x = atoi(retour);
        if (x >= 10 && x <= 50) {                 // MINIMUM QUALITY 10 to save memory
          new_quality = x;
        }
      }
      else if (server.argName(i) == "speed") {

        int x = atoi(retour);
        if (x >= 1 && x <= 100) {
          new_xspeed = x;
        }
      }

      else if (server.argName(i) == "interval") {
        int x = atoi(retour);
        if (x <= 1 ) {
          new_interval = capture_interval;
        } else {
          new_interval = x;
        }
      }
      else if (server.argName(i) == "sleep") { // en minute
        sleep_mode = true;
        if (len > 1) sleep_duree = atoi(retour);
      }
      else if (server.argName(i) == "gray") {
        int x = atoi(retour);
        if (x == 1 ) {
          new_gray = x;
        } else {
          new_gray = 0;
        }
      }
      else if (server.argName(i) == "ssid") {
        strncpy(ssid, retour, len);
      }
      else if (server.argName(i) == "pass") {
        strncpy(pass, retour, len);
      }
      else if (server.argName(i) == "horaire") {
        strncpy(horaire, retour, len);
      }
    }

    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");
    server.send(200, "text/html", the_page);

    recording = 1;
    return true;
  }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
void do_stop(char *the_message) {

  Serial.print(F("do_stop ")); Serial.println(the_message);

  const char msg[] PROGMEM = R"rawliteral(<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s ESP32-CAM stop</title>
</head>
<body>
<h1>%s<br>ESP32-CAM configurations %s </h1><br>
 <h3>Message is <font color="red">%s</font></h3><br>
 <h3><a href="http://%s/">http://%s/ retour au status</a></h3>

<h3><a href="http://%s/start?framesize=VGA&length=1800&interval=100&quality=10&repeat=100&speed=1&gray=0">VGA  2 fps for 30 minutes repeat, 1x playback</a></h3> 
<h3><a href="http://%s/start?framesize=VGA&length=1800&interval=500&quality=10&repeat=300&speed=30&gray=0">VGA 2 fps, for 30 minutes repeat, 30x playback</a></h3>
<h3><a href="http://%s/start?framesize=UXGA&length=1800&interval=1000&quality=10&repeat=100&speed=30&gray=0">UXGA 1 sec per frame, for 30 minutes repeat, 30x playback</a></h3>
<h3><a href="http://%s/start?framesize=UXGA&length=1800&interval=500&quality=10&repeat=100&speed=30&gray=0">UXGA 2 fps for 30 minutes repeat, 15x playback</a></h3>
<h3><a href="http://%s/start?framesize=CIF&length=1800&interval=50&quality=10&repeat=100&speed=1&gray=0">CIF 20 fps second for 30 minutes repeat</a></h3>

<h3><a href="http://%s/start?framesize=UXGA&length=1800&interval=10000&quality=10&repeat=100&speed=1&gray=0">UXGA 10 second for 30 minutes repeat</a></h3>
<h3><a href="http://%s/start?framesize=UXGA&length=1800&interval=600000&quality=10&repeat=100&speed=1&gray=0&sleep">UXGA 10 min for 30 minutes repeati sleep</a></h3>
<br>
Capture Interval en ms<br>
Length en seconds<br>
Quality   = 10 meilleurs a 50 pire<br>
Framesize = UXGA  SVGA VGA CIF<br>
Repeat    = nombre de fois<br>
Speed     = vitesse<br>
Gray      = 1 ou 0<br>


<br>mis a jour dela config initiale par 
<h3><a href="http://%s/saveconfig"> saveconfig</a></h3>

Fichier sur la microSD /config.jsn <br>
%s
</body>
</html>)rawliteral";
  //ex: {"devname":"esp32-cam","framesize":6,"repeat":100,"xspeed":1,"gray":0,"quality":10,"capture_interval":10000,"length":180,"ssid":"SNHACK","pass":"1234567890","horaire":"10 11,12,13,14,15,16,17,18,19,20"}

  sprintf(the_page, msg, devname, devname, vernum, the_message, localip, localip, localip, localip, localip, localip, localip, localip, localip, localip, buf_config);
  Serial.println(the_page);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
bool stop_handler() {

  recording = 0;
  Serial.println(F("stop recording"));

  do_stop("Stopping previous recording");
  server.send(200, "text/html", the_page);
  delay(1000);
  return true;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
void do_status(char *the_message) {

  Serial.print(F("do_status ")); Serial.println(the_message);

  const char msg[] PROGMEM = R"rawliteral(<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s ESP32-CAM index</title>
</head>
<body>
<h1>%s<br>ESP32-CAM Video Recorder %s <br><font color="red">%s</font></h1><br>
 <h3>Page de <font color="red">%s</font></h3><br>
 Total SD Space is %d MB, Used SD Space is %d MB<br>
 Recording = %d (1 is active)<br>
 Frame %d of %d, Skipped %d<br><br>
 Capture Interval = %d ms<br>
 Length = %d seconds<br>
 Quality = %d (10 best to 50 worst)<br>
 Framesize = %d (10 UXGA, 7 SVGA, 6 VGA, 5 CIF)<br>
 Repeat = %d<br>
 Playback Speed = %d<br>
 Gray = %d<br><br>
 Commandes possibles sur cette adresse ip:<br>
 <h3><a href="http://%s/">http://%s/</a></h3>
 <h3><a href="http://%s/stop">http://%s/stop<font color="red"> ... configuration et restart
 <br>Vous devez faire stop pour modifier la configuration</font></a></h3>
 <h3><a href="http://%s/list">http://%s/list> list </a></h3>
 <h3><a href="http://%s/capture">http://%s/capture> photo </a></h3>
 <h3><a href="http://%s/saveconfig"> saveconfig</a></h3>

Fichier sur la microSD /config.json ex: {"framesize":6,"repeat":100,"xspeed":1,"gray":0,"quality":10,"capture_interval":10000,"length":180},
cf list<br>
 LED flash sur une frame, SOS flash si pas de microSD<br>
<br>
</body>
</html>)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, localip, localip, localip);

  //Serial.println(strlen(the_page));
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//  LIST ///
void listDir(fs::FS & fs, const char * dirname, uint8_t levels) {
  Serial.printf("Listing directory: %s\n", dirname);
  String output = "";
  output += "<html><head><meta charset=\"utf-8\">";
  output += "<title> ESP32-CAM list</title>";
  output += "</head><body>";
  output += "<h3> Fichiers    Tailles</h3>";
  File root = fs.open(dirname);
  if (!root) {
    Serial.println(F("err open dir"));
    return;
  }
  if (!root.isDirectory()) {
    Serial.println(F("Not a dir"));
    return;
  }
  File file = root.openNextFile();

  while (file) {
    delay(100);
    if (file.isDirectory()) {
      output += "<br><h2><a href=\"http://";
      output += (String)localip;
      output += (String)file.name();
      output += "\">http://";
      output += (String)localip;
      output += (String)file.name();
      output += "</a></h2>";
      output += "\n";
      //Serial.println((String)file.name());
      if (levels) {
        listDir(fs, file.name(), levels - 1);
      }
    } else {
      output += "<br><a href=\"http://";
      output += (String)localip;
      output += (String)file.name();
      output += "\">http://";
      output += (String)localip;
      output += (String)file.name();
      output += "</a>";
      output += "  : ";
      output += file.size();
      //Serial.println((String)file.name());
    }
    file = root.openNextFile();
  }
  output += "<br>";
  output += "<br><a href=\"http://";
  output += (String)localip;
  output += "\">http://";
  output += (String)localip;
  output += " Retour aux status</a>";

  output += "</body></html>";
  server.send(200, "text/html", output);
  //Serial.println(F("fin"));
  return;
}

bool listDir_handler() {
  listDir(SD_MMC, "/", 0);
  //Serial.println(F("listDir fin"));

  return true;
}
///
/// SDCARD
///
bool loadFromSdCard(fs::FS & fs, String path) {
  String dataType = "text/plain";
  if (path.endsWith("/")) {
    do_status("Refresh Status");
    server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    server.sendHeader("Pragma", "no-cache");
    server.sendHeader("Expires", "-1");
    server.send(200, "text/html", the_page);
    return true;
  }
  if (path.endsWith(".src")) {
    path = path.substring(0, path.lastIndexOf("."));
  } else if (path.endsWith(".htm")) {
    dataType = "text/html";
  } else if (path.endsWith(".css")) {
    dataType = "text/css";
  } else if (path.endsWith(".js")) {
    dataType = "application/javascript";
  } else if (path.endsWith(".png")) {
    dataType = "image/png";
  } else if (path.endsWith(".gif")) {
    dataType = "image/gif";
  } else if (path.endsWith(".jpg")) {
    dataType = "image/jpeg";
  } else if (path.endsWith(".ico")) {
    dataType = "image/x-icon";
  } else if (path.endsWith(".xml")) {
    dataType = "text/xml";
  } else if (path.endsWith(".pdf")) {
    dataType = "application/pdf";
  } else if (path.endsWith(".zip")) {
    dataType = "application/zip";
  }
  File dataFile = fs.open(path.c_str());
  if (dataFile.isDirectory()) {
    path += "/index.htm";
    dataType = "text/html";
    dataFile = fs.open(path.c_str());
  }
  if (!dataFile) {
    return false;
  }
  if (server.hasArg("download")) {
    dataType = "application/octet-stream";
  }
  if (server.streamFile(dataFile, dataType) != dataFile.size()) {
    Serial.println(F("Sent less data than expected!"));
  }
  dataFile.close();
  return true;
}
///
/// DEFAULT LECTURE SD
///
void handleNotFound() {
  if (loadFromSdCard(SD_MMC, server.uri())) {
    return;
  }
  String message = "";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " NAME:" + server.argName(i) + "\n VALUE:" + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
  Serial.println(message);
}
///
/// SPIFFS  SAVE config
///
bool saveSPIFFSConfigFile(void)
{
  Serial.println(F("Save config"));
  recording = 0;
  DynamicJsonDocument json(512);
  json["devname"]            = devname;
  json["quality"]            = quality;
  json["framesize"]          = framesize;
  json["capture_interval"]   = capture_interval;
  json["repeat"]             = repeat;
  json["xspeed"]             = xspeed;
  json["length"]             = xlength;
  json["gray"]               = gray;
  json["ssid"]               = ssid;
  json["pass"]               = pass;
  json["horaire"]            = horaire;

  File configFile = SPIFFS.open("/config.jsn", "w");
  if (!configFile)
  {
    Serial.println(F("Failed to open config file for writing"));
  }
  //serializeJson(json, Serial);
  serializeJsonPretty(json, Serial);
  String output = "";
  output += "<html><head><meta charset=\"utf-8\">";
  output += "<title> ESP32-CAM sauvegarde</title>";
  output += "</head><body>";
  output += "<h3>Sauvegarde du fichier json sur la microSD</h3><br>";
  serializeJson(json, output);
  output += " <br> OK <br>";
  output += " Attendre 10s avant de retourner sur la page d'avant!<br>" ;
  output += " RESET  en cours !</body></html>";

  server.send(200, "text/html", output);

  // sauv flash
  serializeJson(json, configFile);
  configFile.close();
  saveFileconfigSD();
  //end save
  delay(1000);
  ESP.restart();
}
///
/// RESET
///
void reset_handler() {
  String output = "";
  output += "<html><head><meta charset=\"utf-8\">";
  output += "<title> ESP32-CAM reset</title>";
  output += "</head><body>";
  output += "<h3>Reset</h3><br>";
  output += " Attendre 10s avant de retourner sur la page d'avant!<br>" ;
  output += " RESET  en cours !</body></html>";
  server.send(200, "text/html", output);
  delay(1000);
  ESP.restart();
}
////
//// START CAMERA
////
void startCameraServer() {

  //server.on("/", index_handler);
  server.on("/capture",    capture_handler);
  server.on("/start",      start_handler);
  server.on("/stop",       stop_handler);
  server.on("/saveconfig", saveSPIFFSConfigFile);
  server.on("/list",       listDir_handler);
  server.on("/reset",      reset_handler);
  server.onNotFound(handleNotFound);

  server.begin();

  Serial.println(F("Camera http started"));
}
projets/esp32-cam_timelaps_code.txt · Dernière modification: 2020/06/10 16:26 par gepeto