Le projet que je vais décrire dans cet article consiste à faire en sorte que l'ESP32-CAM prenne automatiquement des photos (et les enregistre sur une carte microSD) chaque fois qu'un capteur infrarouge passif (PIR) détecte une présence. Ça peut faire partie d'un système d'alarme (prendre les photos d'un intrus dans une pièce) , constituer un dispositif qui photographie des animaux sauvages, etc.
Il y quelques années, j'avais rédigé un article assez détaillé sur les capteurs PIR. Le modèle que j'utilise est muni d'un régulateur de tension: on doit l'alimenter avec 5 V pour qu'il fonctionne correctement, mais son signal de sortie passe de 0 V à 3,3 V lorsque la présence d'un humain ou d'un animal est détectée (N.B.: ce n'est probablement pas le cas pour tous les modules PIR disponibles sur le marché).
Une pénurie de broches
La caméra de l'ESP32-CAM utilise déjà un très grand nombre d'entrées/sorties de l'ESP32, ce qui explique pourquoi un module ESP32-CAM comporte beaucoup moins de broches qu'on module ESP32 conventionnel. Mais lorsque vous utilisez le lecteur de cartes microSD intégré au module, la situation devient vraiment délicate: à lui-seul, le lecteur de cartes microSD accapare 6 broches (GPIO 2, 4, 12, 13, 14 et 15). Puisque GPIO 0 , 1 et 3 sont utilisés pour la programmation de la carte, il ne reste que GPIO 16; mais en plus d'être reliée à une résistance pull up qui la tient par défaut au niveau logique haut, cette broche est liée à la mémoire PSRAM.
La solution que j'ai choisie consiste à utiliser le module de carte SD en mode 1 bit plutôt qu'en mode 4 bits. C'est en principe un peu plus lent, mais ça permet de rendre disponibles les broches GPIO 4, 12 et 13.
J'ai donc branché la sortie du capteur PIR à la broche D13 de l'ESP32-CAM.
Des faux positifs?
Si votre capteur PIR détecte une présence même lorsqu'il n'y a personne, vous pouvez diminuer sa sensibilité en ajustant le potentiomètre "Sensitivity". Dans mon cas, il s'est avéré nécessaire d'alimenter le capteur PIR et l'ESP32-CAM de façon indépendante. Lorsqu'ils partageaient tous les deux la même alimentation de 5 V, ils se perturbaient mutuellement: des photos étaient prises sans raison apparente, et l'ESP32-CAM redémarrait parfois de façon impromptue.
Sketch
(En cas de besoin, vous pouvez consulter cet article pour plus de détails concernant la façon de programmer l'ESP32-CAM avec l'IDE Arduino.)
Voici un premier sketch plutôt simple qui n'exploite pas les capacités WIFI de l'ESP32-CAM. Lorsque la broche D13 de l'ESP32-CAM se trouve au niveau logique "Haut" (parce que le capteur PIR a détecté une présence), des photos sont prises et enregistrées sur la carte microSD. Vous devez ensuite éjecter la carte et l'insérer dans un autre appareil pour voir son contenu.
-
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
/* | |
* ESP32-CAM PIR | |
* | |
* Capteur de mouvement PIR branché à une ESP32-CAM. | |
* | |
* Des photos sont prises lorsqu'un mouvement est détecté. | |
* Version sans WIFI | |
* | |
* Pour plus d'infos: | |
* https://electroniqueamateur.blogspot.com/2020/07/esp32-cam-et-capteur-infrarouge-passif.html | |
* | |
*/ | |
#include "FS.h" // manipulation de fichiers | |
#include "SD_MMC.h" // carte SD | |
#include "esp_camera.h" // caméra! | |
static bool cartePresente = false; | |
int numero_fichier = 0; // numéro de la photo (nom du fichier) | |
int brochePIR = 13; // capteur PIR branché à la broche GPIO13. | |
// 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, "/%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); | |
} | |
void setup(void) { | |
// définition des broches de la caméra 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 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(); | |
// initialisation de la carte micro SD | |
// en mode 1 bit: plus lent, mais libère des broches | |
if (SD_MMC.begin("/sdcard",true)) { | |
uint8_t cardType = SD_MMC.cardType(); | |
if (cardType != CARD_NONE) { | |
Serial.println("Carte SD Initialisee."); | |
cartePresente = true; | |
} | |
} | |
pinMode(brochePIR,INPUT); | |
} | |
void loop(void) { | |
if (digitalRead(brochePIR)){ | |
enregistrer_photo(); | |
Serial.print("Nouvelle photo! "); | |
Serial.println(numero_fichier); | |
delay(300); | |
} | |
} |
-
Voici un deuxième exemple, plus élaboré, qui permet de voir le contenu de la carte à distance, grâce à une page web générée par l'ESP32-CAM (voir ce précédent article pour plus d'informations).
-
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
/* | |
ESP32-CAM PIR WIFI | |
Capteur de mouvement PIR branché à une ESP32-CAM. | |
Lorsqu'un mouvement est détectée, des photos sont prises. | |
Le contenu de la carte SD peut être consulté par WIFI. | |
Plus d'infos sur le blog: | |
https://electroniqueamateur.blogspot.com/2020/07/esp32-cam-et-capteur-infrarouge-passif.html | |
*/ | |
#include <WiFi.h> | |
#include <WiFiClient.h> | |
#include <WebServer.h> | |
#include "FS.h" // manipulation de fichiers | |
#include "SD_MMC.h" // carte SD | |
#include "esp_camera.h" // caméra! | |
// écrivez le nom et le mot de passe de votre réseau WIFI | |
const char* ssid = "**********"; | |
const char* password = "**********"; | |
WebServer server(80); | |
static bool cartePresente = false; | |
int numero_fichier = 0; // numéro de la photo (nom du fichier) | |
int brochePIR = 13; // capteur PIR branché à la broche GPIO13. | |
// 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, "/%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); | |
} | |
void returnOK() { | |
server.send(200, "text/plain", ""); | |
} | |
void returnFail(String msg) { | |
server.send(500, "text/plain", msg + "\r\n"); | |
} | |
// Affichage d'un fichier présent sur la carte | |
bool loadFromSdCard(String path) { | |
String dataType = "text/plain"; | |
if (path == "/") { | |
printDirectory(); | |
} | |
else { | |
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"; | |
} | |
fs::FS &fs = SD_MMC; | |
File 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("Sent less data than expected!"); | |
} | |
dataFile.close(); | |
} | |
return true; | |
} | |
// utilisé lors de la suppression d'un fichier | |
void deleteRecursive(String path) { | |
fs::FS &fs = SD_MMC; | |
File file = fs.open((char *)path.c_str()); | |
if (!file.isDirectory()) { | |
file.close(); | |
fs.remove((char *)path.c_str()); | |
return; | |
} | |
file.rewindDirectory(); | |
while (true) { | |
File entry = file.openNextFile(); | |
if (!entry) { | |
break; | |
} | |
String entryPath = path + "/" + entry.name(); | |
if (entry.isDirectory()) { | |
entry.close(); | |
deleteRecursive(entryPath); | |
} else { | |
entry.close(); | |
fs.remove((char *)entryPath.c_str()); | |
} | |
yield(); | |
} | |
fs.rmdir((char *)path.c_str()); | |
file.close(); | |
} | |
// suppression d'un fichier | |
void handleDelete() { | |
fs::FS &fs = SD_MMC; | |
if (server.args() == 0) { | |
return returnFail("Mauvais arguments?"); | |
} | |
String path = server.arg(0); | |
if (path == "/" || !fs.exists((char *)path.c_str())) { | |
returnFail("BAD PATH"); | |
return; | |
} | |
deleteRecursive(path); | |
// on affiche un message de confirmation | |
server.setContentLength(CONTENT_LENGTH_UNKNOWN); | |
server.send(200, "text/html", ""); | |
WiFiClient client = server.client(); | |
server.sendContent("<h1>Le fichier a été supprimé</h1>"); | |
server.sendContent("<p><a href = / > Retour à la liste des fichiers </a></p>"); | |
} | |
// Affichage du contenu de la carte | |
void printDirectory() { | |
fs::FS &fs = SD_MMC; | |
String path = "/"; | |
File dir = fs.open((char *)path.c_str()); | |
path = String(); | |
if (!dir.isDirectory()) { | |
dir.close(); | |
return returnFail("PAS UN REPERTOIRE"); | |
} | |
dir.rewindDirectory(); | |
server.setContentLength(CONTENT_LENGTH_UNKNOWN); | |
server.send(200, "text/html", ""); | |
WiFiClient client = server.client(); | |
server.sendContent("<h1>Contenu de la carte SD</h1>"); | |
for (int cnt = 0; true; ++cnt) { | |
File entry = dir.openNextFile(); | |
if (!entry) { | |
break; | |
} | |
String output; | |
output += "<a href = "; | |
output += entry.name(); | |
output += "> "; | |
output += entry.name(); | |
// on ajoute un bouton delete: | |
output += "</a> <a href = /delete?url="; | |
output += entry.name(); | |
output += "> [Supprimer] </a> <br>"; | |
server.sendContent(output); | |
entry.close(); | |
} | |
dir.close(); | |
} | |
// on tente d'afficher le fichier demandé. Sinon, message d'erreur | |
void handleNotFound() { | |
if (cartePresente && loadFromSdCard(server.uri())) { | |
return; | |
} | |
String message = "Carte SD non detectee ou action imprevue\n\n"; | |
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.print(message); | |
} | |
void setup(void) { | |
// définition des broches de la caméra 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 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(); | |
// connexion au WIFI | |
Serial.begin(115200); | |
WiFi.mode(WIFI_STA); | |
WiFi.begin(ssid, password); | |
Serial.print("Connexion au reseau Wifi "); | |
Serial.println(ssid); | |
uint8_t i = 0; | |
while (WiFi.status() != WL_CONNECTED && i++ < 20) {//wait 10 seconds | |
delay(500); | |
} | |
if (i == 21) { | |
Serial.print("Impossible de se connecter au reseau "); | |
Serial.println(ssid); | |
while (1) { | |
delay(500); | |
} | |
} | |
Serial.print("Connecte a l'adresse IP: "); | |
Serial.println(WiFi.localIP()); | |
// initialisation du web server | |
server.on("/delete", HTTP_GET, handleDelete); | |
server.onNotFound(handleNotFound); | |
server.begin(); | |
Serial.println("Serveur HTTP en fonction."); | |
// initialisation de la carte micro SD | |
// en mode 1 bit: plus lent, mais libère des broches | |
if (SD_MMC.begin("/sdcard", true)) { | |
uint8_t cardType = SD_MMC.cardType(); | |
if (cardType != CARD_NONE) { | |
Serial.println("Carte SD Initialisee."); | |
cartePresente = true; | |
} | |
} | |
pinMode(brochePIR, INPUT); | |
} | |
void loop(void) { | |
server.handleClient(); | |
if (digitalRead(brochePIR)) { | |
enregistrer_photo(); | |
Serial.print("Nouvelle photo! "); | |
Serial.println(numero_fichier); | |
delay(300); | |
} | |
} |
-
À lire également:
Mes autres publications concernant l'ESP32-CAM:
- ESP32-CAM: Première utilisation avec l'IDE Arduino
- ESP32-CAM: un web server minimaliste
- ESP32-CAM: enregistrer des photos dans Google Drive
- ESP32-CAM: enregistrer des photos sur la carte microSD
- ESP32-CAM: gestion à distance de la carte microSD
- Time-lapse avec l'ESP32-CAM
- Mouvement panoramique avec ESP32-CAM et servomoteur
- Les LEDs de l'ESP32-CAM