mercredi 29 janvier 2020

ESP32-CAM: un web server minimaliste


Lors du premier essai de ma ESP32-CAM, j'ai utilisé l'exemple officiel "CameraWebServer", conçu par l'équipe d'Espressif et fourni avec l'IDE Arduino.

Cet exemple a visiblement été créé pour en mettre plein la vue: il créé une page web sophistiquée qui comporte une grande quantité de boutons et de glissières permettant à l'utilisateur de régler tous les paramètres de l'image, d'activer la reconnaissance faciale, etc.  Il s'agit d'un programme assez complexe (1400 lignes de code réparties en 4 fichiers) et souvent nébuleux (les commentaires sont rares!).

Lorsque j'ai voulu apporter quelques modifications à la page web générée par cet exemple, j'ai eu la surprise de constater que son code html a été compressé (gzip) et présenté par une longue suite d'octets dans le fichier camera_index.h: vraiment pas pratique si vous désirez apporter des modifications!


Owen (alias easytarget) a apporté une solution à ce problème: dans sa version améliorée de l'exemple CameraWebServer, le code html de la page web est directement accessible dans le fichier "camera_index_ov2640.h": c'est beaucoup plus facile de comprendre comment la page web est générée, et d'y apporter des modifications. Owen a pris soin de préserver toutes les fonctionnalités de l'exemple officiel, et en a même ajouté quelques-unes (contrôle du flash et de la LED indicatrice, par exemple).

La version easytarget est donc devenue mon nouveau point de départ. Mais le programme demeure lourd: qu'est-ce qui fait quoi, exactement? Pas facile de s'y retrouver.

Je me suis donc donné comme mission de produire un sketch qui ne fera qu'une seule chose: afficher dans une page web l'image vidéo de l'ESP32-CAM. Ça a donné le sketch de 225 lignes présenté ci-dessous.

Le sketch

Dans ma version simplifiée, la page web générée par l'ESP32-CAM ne présente rien d'autre que l'image vidéo: l'utilisateur ne dispose d'aucun bouton qui lui permettrait de modifier le contraste, la luminosité, etc.

Je me suis également permis, très égoïstement de ne conserver que ce qui concerne le modèle d'ESP32-CAM que je possède (AI Thinker). Certaines modifications devront être apportées au sketch si vous désirez l'utiliser sur un autre modèle.

La routine "stream_handler()" s'occupe du "stream server": elle gère l'affichage en direct de l'image vidéo. La version officielle comportait certaines instructions liées à la reconnaissance faciale, qui ont été retirées.

La routine "web_handler()" construit la page web. Cette modeste page web est logée dans un tableau de 175 caractères.

La routine "startCameraServer()" démarre le web server et le stream server.

setUp() initialise la caméra et établit la connexion au réseau WiFi, et loop() n'a rien à faire puisque tout sera géré par stream_handler() et web_handler().

-
/*
Affichage dans une page web de l'image vidéo
d'une ESP32-CAM.
Version légère, sans reconnaissance faciale,
réglage de paramètres, page web sophistiquée, etc.
Pour plus d'infos:
https://electroniqueamateur.blogspot.com/2020/01/esp32-cam-un-web-server-minimaliste.html
*/
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"
// 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
// ********************************************************
// 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: construction 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[175] = "";
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'> </body> </html>");
int taillePage = strlen(pageWeb);
return httpd_resp_send(req, (const char *)pageWeb, taillePage);
}
// ********************************************************
// 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
};
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);
}
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 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.");
}
// ********************************************************
// loop ne fait rien!
void loop() {
delay(10000);
}
-

Utilisation

Au démarrage, l'adresse IP de la caméra est affichée dans le moniteur série.


On colle cette adresse dans un navigateur web pour accéder en direct à la vidéo.



À lire également

Première utilisation de l'ESP32-CAM avec l'IDE Arduino ,  enregistrer des photos sur la carte microSD, time-lapse avec l'ESP32-CAMles LEDs de l'ESP32-CAM, mouvement panoramique avec servomoteur.

Yves Pelletier (TwitterFacebook)


6 commentaires:

  1. Bonjour,
    Merci pour votre partage et pour sa clarté.
    Sur un sonoff à base d'ESP8266, j'avais fait une routine qui indiquait l'ipv4 en faisant clignoter la led à l'initialisation, bien pratique pour s'y connecter quand on n'a plus la possibilité du moniteur série.
    La voulez-vous ?
    Bonne continuation.

    RépondreSupprimer
  2. Bonjour,
    Merci pour cette page !
    Je m'en suis servi pour créer une petite caméra arrière afin de faciliter l'attelage d'une remorque et de faire la vérification des feux quand je suis seul. Bien pratique !
    Cordialement,
    Eric

    RépondreSupprimer
  3. Bonsoir,
    Afin que mes enfants puissent suivre l'évolution des graines qu'ils plantent, j'ai eu l'idée d'en faire une photo chaque nuit à 1h. Pour l'instant j'ai un sonoff qui récupère l'heure nft sur la toile et allume 5 minutes les plantes. Je n'arrive pas à programmer la prise d'une photo avec esp32-cam à un moment donné (la dérive de millis() est trop grande). Dans la foulée, ça permettrait de dater les photos et pour aller plus loin, d'incruster la date sur la photo. Mais je n'arrive pas à avancer, des suggestions ou un tuto seront les bienvenus. Merci. En attendant c'est le sonoff qui déclenche la photo avec une perche selphie connectée en bluetooth à mon smartphone : trop gourmand en énergie, trop moche comme montage, il faut que chaque soir je pense à repositionner précisément mon smartphone, (mais le cadrage n'est jamais le même). Merci de votre attention.

    RépondreSupprimer
  4. Bonjour, afin d'avoir une heure juste, il faut demander à l'ESP32 de récupérer l'heure sur fr.pool.ntp.org par exemple. Moi je le fais tous les jours afin de rester précis (peux-tu est-ce top mais bon ...)
    si l'anglais ne vous rebute pas : https://randomnerdtutorials.com/esp32-date-time-ntp-client-server-arduino/

    RépondreSupprimer
  5. Bonjour
    Bravo pour votre serveur mini.
    La ligne 140 http_register_uri_handler(camera_httpd,&index_uri);
    ne doit-elle pas être remplacée par:
    http_register_uri_handler(camera_httpd,&web_uri);
    ?

    RépondreSupprimer
  6. Bns, est-il possible de stocker, en plus du streaming, les images (ou la video) sur le SDcard ?

    RépondreSupprimer