Caméra Wi-Fi ESP32-CAM avec Arduino

Création du serveur de flux vidéo

Dans cette partie nous avons 2 éléments à gérer:

  • La création du serveur internet
  • La génération du flux vidéo

La création du serveur internet

Pour la création du serveur internet, nous choisissons d’utiliser la bibliothèque « WebServer » déjà incluse par défaut avec les librairies par défaut de l’ESP32 et qui est très simple à manipuler. Cette librairie se déclare par l’instruction suivante:

#include <WebServer.h>

Nous déclarons ensuite l’objet ServeurFluxVideo de classe « WebServer » en écoute sur le port 80. Nous utiliserons par la suite les méthodes de cet objet « ServeurFluxVideo » pour configurer notre serveur Web de flux vidéo.

WebServer ServeurFluxVideo_G(80);  // Serveur Web sur le port 80

Dans cet article Arduino, j’ai choisi de vous présenter l’utilisation d’une caméra Wi-Fi ESP32-CAM de façon très simple. L’objectif est que vous puissiez comprendre son fonctionnement. Aussi, le serveur Web réalisé n’a qu’une page principale qui renvoie vers la fonction de traitement baptisée « FluxVideo ». L’instruction qui réalise cette association entre la page web principale et la fonction de traitement est la suivante:

ServeurFluxVideo_G.on("/", HTTP_GET, FluxVideo); // La racine du site Web est associée à la fonction "FluxVideo"

Nous déclarons également le corps de notre fonction de traitement:

void FluxVideo()
{
}

La méthode  « handleClient » demande au serveur Web de traiter les requêtes HTML émises par les navigateurs clients :

ServeurFluxVideo_G.handleClient(); // Traitement des requêtes des clients du serveur Web

Il ne reste plus qu’à démarrer notre serveur par l’instruction:

ServeurFluxVideo_G.begin(); // Démarrage du serveur

Nous pouvons maintenant passer à la génération du flux vidéo.

La génération du flux vidéo

Pour la diffusion du flux vidéo vers le navigateur consommateur, nous allons utiliser la technique  « HTTP server push » disponible depuis la version 2 du protocole HTTP. Cette technique permet à un serveur Web de transmettre non pas une seule trame de réponse à la requête HTTP d’un navigateur Web client,  mais plusieurs trames de réponse de façon régulière sans que le navigateur Web client n’ait besoin d’envoyer de nouvelles requêtes HTTP.

Concrètement en réponse à la requête du navigateur internet, la fonction FluxVideo de notre programme va d’abord envoyer la trame d’entête initiale suivante :

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=FinDeLaSection

Puis l’entête HTML intermédiaire …

--FinDeLaSection
Content-Type: image/jpeg

… ensuite la première image au format JPEG:

Exemple de première image

Puis l’entête HTML intermédiaire …

--FinDeLaSection
Content-Type: image/jpeg

… ensuite la deuxième image au format JPEG.

Exemple de 2ème image

Et ainsi de suite.

Maintenant que le principe est expliqué, il est temps de compléter notre fonction FluxVideo.

Nous commençons par définir nos trames d’entête HTML:

String EnteteHtmlInitiale_L = "HTTP/1.1 200 OK\r\n" \
    "Access-Control-Allow-Origin: *\r\n"; \
    "Content-Type: multipart/x-mixed-replace; boundary=FinDeLaSection\r\n\r\n;";
String EnteteHtmlIntermediaire_L = "--FinDeLaSection\r\n" \
    "Content-Type: image/jpeg\r\n\r\n";

La méthode « sendContent » de notre objet ServeurFluxVideo permet d’envoyer un contenu HTTP de type « texte » ou « image ». Nous l’utilisons pour envoyer la trame d’entête initiale : 

ServeurFluxVideo_G.sendContent(EnteteHtmlInitiale_L);

Ensuite on utilise la propriété « client » de notre objet ServeurFluxVideo pour accéder aux propriétés et méthodes relatives au navigateur Web actuellement connecté à notre serveur:

WiFiClient Client_L;

Client_L = ServeurFluxVideo_G.client();

Tant que le navigateur Web client est connecté, on lui envoie la trame d’entête intermédiaire :

while (Client_L.connected())
{   
    // Le navigateur Web client est toujours connecté
    ServeurFluxVideo_G.sendContent(EnteteHtmlIntermediaire_L);
}

Concernant les images de la caméra, nous commençons par créer une variable tampon permettant de les stocker avant leur envoi.

camera_fb_t *TamponVideo_L = NULL; 

L’acquisition des images de la caméra s’effectue en utilisant la fonction esp_camera_fb_get() :

TamponVideo_L = esp_camera_fb_get(); // Récupère une image depuis la caméra  

et leur transmission en utilisant la méthode « write » de notre objet Client_L. Cette méthode permet en effet d’envoyer des données binaires:

 Client_L.write((char *)TamponVideo_L->buf, TamponVideo_L->len);

Pour compléter cet envoi intermédiaire, il est nécessaire d’envoyer des retours à la ligne:

ServeurFluxVideo_G.sendContent("\r\n");

Concernant la variable tampon permettent de stocker les images vidéo, il est nécessaire de la réamorcer avant de pouvoir la réutiliser. Ceci s’effectue à l’aide des instructions suivantes:

// Réamorcage du tampon video, indispensable pour sa réutilisation
if (TamponVideo_L)
{
    esp_camera_fb_return(TamponVideo_L);
    TamponVideo_L = NULL;
}

Voilà ! C’était la dernière ligne de code !

Le programme complet:

// Déclaration des bibliothèques utilisées
#include <WiFi.h>
#include <ESP32_MailClient.h>
#include <esp_camera.h>
#include <WebServer.h>

// Définition des constantes globales
#define PORT_LED_FLASH      4   // Numéro de port auquel est branchée la LED servant de flash.
 
// Déclaration globales
IPAddress AdresseIpLocale_G; // Permet de mémoriser l'adresse IP de la carte ESP32-CAM
WebServer ServeurFluxVideo_G(80);  // Serveur Web sur le port 80

// Fonction de démarrage, s'exécute une seule fois:
void setup()
{
    // Constantes de la fonction
    const char* SSID_L = "LiveBox_1324"; // Nom du réseau Wi-Fi
    const char* MOT_DE_PASSE_L = "12345?ABCDE"; // Mot De Passe du réseau

    // Variables de la fonction
    wl_status_t StatutConnexion_L; // Pour mémoriser l'état de la connexion
    esp_err_t   Retour_L;

    // Variables de la fonction
    char Buffer_L[200];

    pinMode(PORT_LED_FLASH, OUTPUT); // Initialisation en "sortie" de la broche d'E/S connectée au flash
    WiFi.begin(SSID_L, MOT_DE_PASSE_L); // Tentative de connexion au point d'accès Wi-Fi
    StatutConnexion_L = WiFi.status(); // Lecture de l'état de la connexion et mémorisation dans la variable "StatutConnexion_L"
    while ((StatutConnexion_L != WL_NO_SSID_AVAIL)&&(StatutConnexion_L != WL_CONNECTED)&&(StatutConnexion_L != WL_CONNECT_FAILED))
    {
        digitalWrite(PORT_LED_FLASH, HIGH);
        delay(100);
        digitalWrite(PORT_LED_FLASH, LOW);
        delay(500);
        StatutConnexion_L = WiFi.status(); // Lecture de l'état de la connexion et mémorisation dans la variable "StatutConnexion_L"
    }

    Serial.begin(115200); // Ouverture du port série à 115200 bauds

    // Affichage du résultat de la tentative de connexion
    if (StatutConnexion_L == WL_CONNECTED)
    {
        Serial.println("Connection OK");
        AdresseIpLocale_G = WiFi.localIP(); // Mémorisation de l'adresse actuelle
        Retour_L = InitialiserCamera();
        if(Retour_L == ESP_OK)
        {
          sprintf(Buffer_L,"<p><strong>La caméra a démarré avec succès !</strong></p><p>Cliquez sur le lien \"http://%u.%u.%u.%u\" pour vous connecter.</p>", AdresseIpLocale_G[0], AdresseIpLocale_G[1], AdresseIpLocale_G[2], AdresseIpLocale_G[3]);
        }
        else
        {
          sprintf(Buffer_L,"<p><strong>Erreur d'initialisation de la caméra !</strong></p><p>L'erreur 0x%x a été rencontrée.</p>", Retour_L);
        }
        EnvoyerEmail("Démarrage de la caméra Wi-Fi", Buffer_L);
        ServeurFluxVideo_G.on("/", HTTP_GET, FluxVideo); // La racine du site Web est associée à la fonction "FluxVideo"
        ServeurFluxVideo_G.begin(); // Démarrage du serveur
    }
    else if (StatutConnexion_L == WL_NO_SSID_AVAIL)
    {
        Serial.println("SSID introuvable");
    }
    else if (StatutConnexion_L == WL_CONNECT_FAILED)
    {
        Serial.println("Mot de passe KO");
    }
    else
    {
        Serial.println("Autre erreur");
    }
}

// Fonction principale du programme, s'exécute en boucle:
void loop()
{
    // Variables de la fonction
    char Buffer_L[200];

    if(WiFi.status()==WL_CONNECTED)
    {  
        // Internet est disponible
        if (WiFi.localIP()!=AdresseIpLocale_G)
        {
            // L'ESP32-CAM vient d'obtenir une nouvelle adresse IP
            AdresseIpLocale_G = WiFi.localIP(); // Mémorisation de l'adresse actuelle
            sprintf(Buffer_L,"<p><strong>La caméra a changé d'adresse IP</strong></p><p>Cliquez sur le lien \"http://%u.%u.%u.%u\" pour vous connecter.</p>", AdresseIpLocale_G[0], AdresseIpLocale_G[1], AdresseIpLocale_G[2], AdresseIpLocale_G[3]);
            EnvoyerEmail("Changement d'adresse IP de la caméra Wi-Fi", Buffer_L);
        }
        ServeurFluxVideo_G.handleClient(); // Traitement des requêtes des clients du serveur Web
    }
    else
    {
        // Pas de connexion internet
        digitalWrite(PORT_LED_FLASH, HIGH);
        delay(100);
        digitalWrite(PORT_LED_FLASH, LOW);
        delay(500);
    }
}

void EnvoyerEmail(const char *pObjet_P, const char *pMessage_P)
{
    const char* ADRESSE_EMAIL_EMISSION_L = "votremaildetest@gmail.com";
    const char* MOT_DE_PASSE_EMAIL_EMISSION_L = "12345MoteDePasseMail";
    const char* ADRESSE_SERVEUR_SMTP_L = "smtp.gmail.com";
    int PORT_SERVEUR_SMTP_L = 465; // Port SSL utilisé
    const char* ADRESSE_EMAIL_DESTINATAIRE_L = "votremail@gmail.com";

    // Variables locales de la fonction
    SMTPData DonneesEmail_L;  // Données de l'e-mail à envoyer
    
    // Les caractéristique de l'e-mail sont renseignées
    DonneesEmail_L.setLogin(ADRESSE_SERVEUR_SMTP_L, PORT_SERVEUR_SMTP_L, ADRESSE_EMAIL_EMISSION_L, MOT_DE_PASSE_EMAIL_EMISSION_L); // Identifiants de connexion au serveur SMTP
    DonneesEmail_L.setSender("Camera Wi-Fi", ADRESSE_EMAIL_EMISSION_L); // Nom et adresse email de l'emetteur
    DonneesEmail_L.setPriority("Normal"); // Niveau d'importance du mail
    DonneesEmail_L.setSubject(pObjet_P);  // Objet du mail
    DonneesEmail_L.setMessage(pMessage_P, true); // Contenu du message au format HTML
    DonneesEmail_L.addRecipient(ADRESSE_EMAIL_DESTINATAIRE_L);  // Adresse du destinataire
    
    //Emission de l'e-mail
    if (!MailClient.sendMail(DonneesEmail_L))
    {
      Serial.println("Erreur lors de l'envoi d'e-mail:" + MailClient.smtpErrorReason());
    }
    DonneesEmail_L.empty(); // Nettoyage des données d'envoi d'e-mail
}

esp_err_t InitialiserCamera()
{
    // Variables locales de la fonction
    esp_err_t       Retour_L;
    camera_config_t ConfigurationCamera_L;

    // Cablage de la caméra sur l'ESP32-CAM du fabricant AI-Thinker
    ConfigurationCamera_L.pin_d0 = 5;
    ConfigurationCamera_L.pin_d1 = 18;
    ConfigurationCamera_L.pin_d2 = 19;
    ConfigurationCamera_L.pin_d3 = 21;
    ConfigurationCamera_L.pin_d4 = 36;
    ConfigurationCamera_L.pin_d5 = 39;
    ConfigurationCamera_L.pin_d6 = 34;
    ConfigurationCamera_L.pin_d7 = 35;
    ConfigurationCamera_L.pin_xclk = 0;
    ConfigurationCamera_L.pin_pclk = 22;
    ConfigurationCamera_L.pin_vsync = 25;
    ConfigurationCamera_L.pin_href = 23;
    ConfigurationCamera_L.pin_sscb_sda = 26;
    ConfigurationCamera_L.pin_sscb_scl = 27;
    ConfigurationCamera_L.pin_pwdn = 32;
    ConfigurationCamera_L.pin_reset = -1;

    // La génération du signal d'horloge
    ConfigurationCamera_L.ledc_channel = LEDC_CHANNEL_0;
    ConfigurationCamera_L.ledc_timer = LEDC_TIMER_0;
    ConfigurationCamera_L.xclk_freq_hz = 20000000;

    // Compression jpeg
    ConfigurationCamera_L.pixel_format = PIXFORMAT_JPEG;
    ConfigurationCamera_L.fb_count = 2;
    ConfigurationCamera_L.jpeg_quality = 10;

    // Résolution de l'image
    ConfigurationCamera_L.frame_size = FRAMESIZE_SVGA;

    // Lancement de l'initialisation de la caméra
    Retour_L=esp_camera_init(&ConfigurationCamera_L);
    if (Retour_L == ESP_OK)
    {
        Serial.printf("La camera est initialisee\n");
    }
    else
    {
        Serial.printf("Erreur 0x%x lors de l'initialisation de la camera\n", Retour_L);
    }
    return(Retour_L);
}

void FluxVideo()
{
    // Variables locales de la fonction
    String EnteteHtmlInitiale_L = "HTTP/1.1 200 OK\r\n" \
        "Access-Control-Allow-Origin: *\r\n" \
        "Content-Type: multipart/x-mixed-replace; boundary=FinDeLaSection\r\n\r\n;";
    String EnteteHtmlIntermediaire_L = "--FinDeLaSection\r\n" \
        "Content-Type: image/jpeg\r\n\r\n";
    WiFiClient Client_L;
    camera_fb_t *TamponVideo_L = NULL; 
  
    ServeurFluxVideo_G.sendContent(EnteteHtmlInitiale_L);
    Client_L = ServeurFluxVideo_G.client();
    while (Client_L.connected())
    {   
        // Le navigateur Web client est toujours connecté
        TamponVideo_L = esp_camera_fb_get(); // Récupère une image depuis la caméra      
        ServeurFluxVideo_G.sendContent(EnteteHtmlIntermediaire_L);
        Client_L.write((char *)TamponVideo_L->buf, TamponVideo_L->len);
        ServeurFluxVideo_G.sendContent("\r\n");
         
        // Réamorcage du tampon video, indispensable pour sa réutilisation
        if (TamponVideo_L)
        {
            esp_camera_fb_return(TamponVideo_L);
            TamponVideo_L = NULL;
        }
    }
}

Compilation et tests:

Pour rappel, dans le code logiciel ci-dessus pensez à remplacer par vos propres identifiants les éléments suivants:

  • Nom du réseau Wi-Fi,
  • Mot De Passe du réseau,
  • les adresses e-mail d’émission et de réception,
  • le mot de passe de l’adresse e-mail d’émission.

Maintenant après avoir compilé puis transféré ce programme, vous devez obtenir le comportement suivant lorsque vous remettez ensuite l’ESP32-CAM en mode « exécution de programme »:

  1. Au démarrage le flash clignote quelques seconde puis s’éteint.
  2. Un nouvel e-mail apparait dans votre messagerie en vous fournissant le lien internet.
  3. Une fois cliqué sur ce lien internet, votre navigateur s’ouvre sur l’adresse IP locale du module ESP32-CAM
  4. Au bout d’un 30aine de seconde vous voyez sur votre navigateur ce que filme votre caméra.

Réalisation d’une alimentation portative:

Maintenant que nous avons vérifié que notre programme fonctionne bien, il est temps de passer à la version portative.

Liste des composants utilisés pour la caméra Wi-Fi ESP32-CAM autonome

J’ai utilisé:

  • 4 piles rechargeable,
  • un boitier 4 piles avec connecteur à clip,
  • un connecteur à clip,
  • une mini carte de prototypage,
  • et un gros élastique pour tenir le tout.
Caméra Wi-Fi ESP32-CAM autonome avec Arduino

J’ai posé la caméra Wi-Fi ESP32-CAM que nous avons programmée en Arduino sur un lieu stratégique pour vérifier les visiteurs non-annoncés.

Caméra Wi-Fi ESP32-CAM autonome avec Arduino popur espionner les chats

Voici mon tableau de chasse de l’après-midi:

Un visiteur bien poilu:

et un passage éclair:

J’espère que ce tutoriel de Caméra Wi-Fi ESP32-CAM avec Arduino vous a plus, j’attends avec impatience vos commentaires ou vos questions.

Ce contenu a été publié dans Arduino, ESP32-CAM. Vous pouvez le mettre en favoris avec ce permalien.

2 réponses à Caméra Wi-Fi ESP32-CAM avec Arduino

  1. bariod dit :

    Bonjour et félicitations pour ce tuto.
    Pourrait-il être utilisé pour détecter la sortie de pelouse (plus de vert) par un robot tondeuse ?
    Merci d’avance si vous me répondez.

    • Nico dit :

      Bonjour,
      Merci pour ce retour

      Effectivement, en disposant la caméra verticalement à quelques centimètres du sol, l’image sera plus ou moins remplie de vert en fonction de la hauteur de pousse de l’herbe.
      Une solution de programme de « détection de sortie de tondeuse » réalisé avec l’ESP32-CAM est d’effectuer les actions suivantes :
      • Prendre 1 photo par jour
      • Analyser la quantité de pixels verts de l’image à partir d’un algorithme
      • Déclencher le robot tondeuse au-delà d’un certain niveau de « verdure » de la photo
      Et voilà 😉

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *