J'ai apporté quelques modifications à mes sketches précédents afin de produire des vidéos time-lapse au moyen de mon ESP32-CAM (programmée avec l'IDE Arduino, comme d'habitude).
Comme dans mes projets précédents, l'image captée par la caméra est présentée en temps réel sur une page web. Mais cette page comporte, en plus, un champ texte permettant à l'utilisateur d'écrire le nombre de secondes désiré entre deux images consécutives ainsi qu'un bouton permettant de démarrer et d'interrompre l'enregistrement des images.
Les images sont enregistrées en format jpeg sur une carte micro-SD insérée dans l'ESP32-CAM. On peut ensuite combiner ces images pour en faire une vidéo (pour réaliser la vidéo ci-dessous, j'ai utilisé Movie Maker).
Sketch
Mon point de départ à été mon sketch qui enregistre des photos sur une carte microSD.
Les principales modifications sont:
- dans la routine web_handler(), aux lignes 127 à 134: ajout d'un champ de texte pour choisir le nombre de secondes entre 2 images successives.
- dans la routine clic_handler(), aux lignes 189 à 225: récupération du délai entré par l'utilisateur, et activation (ou désactivation) de l'enregistrement des photos.
- dans la routine loop(), aux lignes 360 à 365: vérification si c'est le moment de prendre une photo, compte tenu du délai choisi par l'utilisateur.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Timelapse avec ESP32-CAM. Les photos s'enregistrent sur une carte SD. | |
On démarre la prise de vue et on contrôle le délai entre 2 images | |
consécutives grâce à une page web. | |
Plus d'infos: | |
https://electroniqueamateur.blogspot.com/2020/03/time-lapse-avec-lesp32-cam.html | |
*/ | |
// inclusion des bibliothèques utile | |
#include "esp_camera.h" | |
#include <WiFi.h> | |
#include "esp_http_server.h" | |
#include "FS.h" // manipulation de fichiers | |
#include "SD_MMC.h" // carte SD | |
// paramètres de votre réseau WiFi | |
const char* ssid = "**********"; | |
const char* password = "**********"; | |
#define PART_BOUNDARY "123456789000000000000987654321" | |
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; | |
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; | |
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; | |
httpd_handle_t stream_httpd = NULL; | |
httpd_handle_t camera_httpd = NULL; | |
int numero_port; // numéro du port du stream server | |
bool carte_presente = 0; // présence d'une carte micro SD | |
int numero_fichier = 10000; // numéro initial de la photo | |
unsigned long photoPrecedente = 0; // prise de la dernière photo | |
long intervalle = 10000; // nombre de millisecondes entre 2 photos. | |
bool timeLapseActif = 0; // devient vrai pendant l'enregistrement de la séquence | |
// ******************************************************** | |
// stream_handler: routine qui gère le streaming vidéo | |
static esp_err_t stream_handler(httpd_req_t *req) { | |
camera_fb_t * fb = NULL; | |
esp_err_t res = ESP_OK; | |
size_t _jpg_buf_len = 0; | |
uint8_t * _jpg_buf = NULL; | |
char * part_buf[64]; | |
static int64_t last_frame = 0; | |
if (!last_frame) { | |
last_frame = esp_timer_get_time(); | |
} | |
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE); | |
if (res != ESP_OK) { | |
return res; | |
} | |
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); | |
while (true) { | |
fb = esp_camera_fb_get(); | |
if (!fb) { | |
Serial.println("Echec de la capture de camera"); | |
res = ESP_FAIL; | |
} else { | |
if (fb->width > 400) { | |
if (fb->format != PIXFORMAT_JPEG) { | |
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); | |
esp_camera_fb_return(fb); | |
fb = NULL; | |
if (!jpeg_converted) { | |
Serial.println("Echec de la compression JPEG"); | |
res = ESP_FAIL; | |
} | |
} else { | |
_jpg_buf_len = fb->len; | |
_jpg_buf = fb->buf; | |
} | |
} | |
} | |
if (res == ESP_OK) { | |
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len); | |
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen); | |
} | |
if (res == ESP_OK) { | |
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len); | |
} | |
if (res == ESP_OK) { | |
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); | |
} | |
if (fb) { | |
esp_camera_fb_return(fb); | |
fb = NULL; | |
_jpg_buf = NULL; | |
} else if (_jpg_buf) { | |
free(_jpg_buf); | |
_jpg_buf = NULL; | |
} | |
if (res != ESP_OK) { | |
break; | |
} | |
} | |
last_frame = 0; | |
return res; | |
} | |
// ******************************************************** | |
// web_handler: affichage de la page web | |
static esp_err_t web_handler(httpd_req_t *req) { | |
httpd_resp_set_type(req, "text/html"); | |
httpd_resp_set_hdr(req, "Content-Encoding", "identity"); | |
sensor_t * s = esp_camera_sensor_get(); | |
char pageWeb[450] = ""; | |
strcat(pageWeb, "<!doctype html> <html> <head> <title id='title'>ESP32-CAM</title> </head> <body> <img id='stream' src='http://"); | |
// l'adresse du stream server (exemple: 192.168.0.145:81): | |
char adresse[20] = ""; | |
sprintf (adresse, "%d.%d.%d.%d:%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3], numero_port); | |
strcat(pageWeb, adresse); | |
strcat(pageWeb, "/stream'> <br> <br>"); | |
if (carte_presente) { | |
// on ajoute le bouton | |
if (timeLapseActif) { | |
strcat(pageWeb, "<form action='/clic' method='GET'> <label for='intervalle'> Intervalle (en secondes):</label> <br> <input type='text' id='intervalle' name='intervalle' value = '10'><br><br><INPUT type='submit' value='Arrêter'></form> "); | |
} | |
else { | |
strcat(pageWeb, "<form action='/clic' method='GET'> <label for='intervalle'> Intervalle (en secondes):</label> <br> <input type='text' id='intervalle' name='intervalle' value = '10'><br><br><INPUT type='submit' value='Démarrer'></form> "); | |
} | |
} | |
else { | |
// on indique l'absence de la carte | |
strcat(pageWeb, "<p> Pas de carte microSD.</p>"); | |
} | |
strcat(pageWeb, "</body> </html>"); | |
int taillePage = strlen(pageWeb); | |
return httpd_resp_send(req, (const char *)pageWeb, taillePage); | |
} | |
// ******************************************************** | |
/* enregistrer_photo: prise de la photo et création du fichier jpeg */ | |
void enregistrer_photo (void) | |
{ | |
char adresse[20] = ""; // chemin d'accès du fichier .jpeg | |
camera_fb_t * fb = NULL; // frame buffer | |
// prise de la photo | |
fb = esp_camera_fb_get(); | |
if (!fb) { | |
Serial.println("Echec de la prise de photo."); | |
return; | |
} | |
numero_fichier = numero_fichier + 1; | |
// enregitrement du fichier sur la carte SD | |
sprintf (adresse, "/photo%d.jpg", numero_fichier); | |
fs::FS &fs = SD_MMC; | |
File file = fs.open(adresse, FILE_WRITE); | |
if (!file) { | |
Serial.println("Echec lors de la creation du fichier."); | |
} | |
else { | |
file.write(fb->buf, fb->len); // payload (image), payload length | |
Serial.printf("Fichier enregistre: %s\n", adresse); | |
} | |
file.close(); | |
esp_camera_fb_return(fb); | |
} | |
// ******************************************************** | |
/* clic_handler: Gestion du clic sur le bouton */ | |
static esp_err_t clic_handler(httpd_req_t *req) { | |
// récupérons le contenu du champ texte (temps entre 2 poses successives) | |
char* buf; | |
size_t buf_len; | |
char valeur[32] = {0,}; | |
buf_len = httpd_req_get_url_query_len(req) + 1; | |
if (buf_len > 1) { | |
buf = (char*)malloc(buf_len); | |
if (!buf) { | |
httpd_resp_send_500(req); | |
return ESP_FAIL; | |
} | |
if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) { | |
if (httpd_query_key_value(buf, "intervalle", valeur, sizeof(valeur)) == ESP_OK ) { | |
} else { | |
free(buf); | |
httpd_resp_send_404(req); | |
return ESP_FAIL; | |
} | |
} else { | |
free(buf); | |
httpd_resp_send_404(req); | |
return ESP_FAIL; | |
} | |
free(buf); | |
} else { | |
httpd_resp_send_404(req); | |
return ESP_FAIL; | |
} | |
intervalle = atoi(valeur) * 1000; | |
timeLapseActif = !(timeLapseActif); | |
return (web_handler(req)); | |
} | |
// ******************************************************** | |
// startCameraServer: démarrage du web server et du stream server | |
void startCameraServer() { | |
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | |
httpd_uri_t index_uri = { | |
.uri = "/", | |
.method = HTTP_GET, | |
.handler = web_handler, | |
.user_ctx = NULL | |
}; | |
httpd_uri_t stream_uri = { | |
.uri = "/stream", | |
.method = HTTP_GET, | |
.handler = stream_handler, | |
.user_ctx = NULL | |
}; | |
httpd_uri_t clic_uri = { | |
.uri = "/clic", | |
.method = HTTP_GET, | |
.handler = clic_handler, | |
.user_ctx = NULL | |
}; | |
Serial.printf("Demarrage du web server sur le port: '%d'\n", config.server_port); | |
if (httpd_start(&camera_httpd, &config) == ESP_OK) { | |
httpd_register_uri_handler(camera_httpd, &index_uri); | |
httpd_register_uri_handler(camera_httpd, &clic_uri); | |
} | |
config.server_port += 1; | |
config.ctrl_port += 1; | |
Serial.printf("Demarrage du stream server sur le port: '%d'\n", config.server_port); | |
if (httpd_start(&stream_httpd, &config) == ESP_OK) { | |
httpd_register_uri_handler(stream_httpd, &stream_uri); | |
} | |
numero_port = config.server_port; | |
} | |
// ******************************************************** | |
// initialisation de la caméra, connexion au réseau WiFi. | |
void setup() { | |
Serial.begin(115200); | |
Serial.println(); | |
Serial.println("===="); | |
// définition des broches pour le modèle AI Thinker - ESP32-CAM | |
camera_config_t config; | |
config.ledc_channel = LEDC_CHANNEL_0; | |
config.ledc_timer = LEDC_TIMER_0; | |
config.pin_d0 = 5; | |
config.pin_d1 = 18; | |
config.pin_d2 = 19; | |
config.pin_d3 = 21; | |
config.pin_d4 = 36; | |
config.pin_d5 = 39; | |
config.pin_d6 = 34; | |
config.pin_d7 = 35; | |
config.pin_xclk = 0; | |
config.pin_pclk = 22; | |
config.pin_vsync = 25; | |
config.pin_href = 23; | |
config.pin_sscb_sda = 26; | |
config.pin_sscb_scl = 27; | |
config.pin_pwdn = 32; | |
config.pin_reset = -1; | |
config.xclk_freq_hz = 20000000; | |
config.pixel_format = PIXFORMAT_JPEG; //YUV422|GRAYSCALE|RGB565|JPEG | |
config.frame_size = FRAMESIZE_VGA; // QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA | |
config.jpeg_quality = 10; // 0-63 ; plus bas = meilleure qualité | |
config.fb_count = 2; // nombre de frame buffers | |
// initialisation de la carte micro SD | |
if (SD_MMC.begin()) { | |
uint8_t cardType = SD_MMC.cardType(); | |
if (cardType != CARD_NONE) { | |
carte_presente = 1; | |
} | |
} | |
if (!carte_presente) { | |
Serial.println("Pas de carte microSD dans l'ESP32-CAM"); | |
} | |
// initialisation de la caméra | |
esp_err_t err = esp_camera_init(&config); | |
if (err != ESP_OK) { | |
Serial.printf("Echec de l'initialisation de la camera, erreur 0x%x", err); | |
return; | |
} | |
sensor_t * s = esp_camera_sensor_get(); | |
Serial.println(""); | |
Serial.print("Connexion au reseau WiFi: "); | |
Serial.println(ssid); | |
delay(100); | |
WiFi.begin(ssid, password); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(250); | |
} | |
Serial.println("WiFi connecte"); | |
Serial.println(""); | |
delay(100); | |
startCameraServer(); | |
Serial.print("La camera est prete. Utilisez 'http://"); | |
Serial.print(WiFi.localIP()); | |
Serial.println("' pour vous connecter."); | |
} | |
// ******************************************************** | |
void loop() { | |
// enregistrement d'une photo, si c'est le bon moment | |
unsigned long maintenant = millis(); | |
if (maintenant - photoPrecedente >= intervalle) { | |
photoPrecedente = maintenant; | |
if (timeLapseActif) { | |
enregistrer_photo(); | |
} | |
} | |
} |
À lire également
Dans le passé, j'avais fait du time-lapse avec un vieil appareil photo numérique commandé par un Arduino (on peut aussi utiliser la camera du Raspberry Pi, mais je n'ai pas écrit d'article sur ce sujet spécifique).
De plus, j'ai publié plusieurs articles concernant l'ESP32-CAM: première utilisation, un web server minimaliste , enregistrer des photos sur la carte microSD, les LEDs de l'ESP32-CAM, mouvement panoramique de l'ESP32-CAM avec un servomoteur.
Yves Pelletier (Twitter, Facebook)
Bonjour,
RépondreSupprimerMerci pour votre partage et pour sa clarté.
Est-il possible de détecter un changement dans l'image et de l'enregistrer avec un intervalle prédéfini ?
Merci.
Bonjour,
RépondreSupprimerImpeccable ton programme, c'est tout à fait ce que je cherchais.
Par contre je vois le stream sur le serveur web quand il n'y a pas de carte SD insérée, et plus lorsqu'il y a une carte SD. J'ai en haut de la page le petit icone vert qui m'indique que la source n'existe pas, cependant j'ai bien la prise de photo automatique, est-ce un fonctionnement normal ? J’imagine que non, avoir le timelaps et le retour vidéo me parait plus "sympa", une piste à me donner pour résoudre ce problème?
Merci pour le coup de main.
Et bonne continuation, j'ai trouvé plein d'article passionnant sur ce site.
Bonjour,
RépondreSupprimerJ'ai le même problème (pas de stream lorsque carte SD mise).
Il y a t'il une solution ?
Merci d'avance pour la réponse.
une solution a vos problème depuis ou toujours en attente?
RépondreSupprimerBonjour,
RépondreSupprimerJe rencontre le même souci : si carte SD présente, alors rien ne s'affiche ...Quelqu'un a-t-il la solution ?
Merci pour vos réponses.
Je me reponds : il faut utiliser la version 1.0.2 de ESP32 Espressif Systems (j'avais la 1.0.6 !)
SupprimerCa marche tres bien.
Encore merci pour tous ces tutos.
Bonjour Christian, peut-tu développer la procédure à suivre ?
Supprimer