Une caméra web ESP32 pour surveiller son environnement

Magazine
Marque
Hackable
Numéro
33
Mois de parution
avril 2020
Spécialité(s)


Résumé

L'ESP32-CAM permet de très simplement et très économiquement créer une simple webcam Wi-Fi. Ceci est très pratique, mais ne présente pas réellement d'intérêt lorsqu'on compare cela à la myriade de produits clé en main similaires et disponibles à bas prix. Ce qui est plus intéressant en revanche, c’est l'opportunité d'avoir totalement la main sur les fonctionnalités embarquées et donc de pouvoir se confectionner un système répondant à un cahier des charges totalement arbitraire. Chose difficile, sinon impossible avec un produit manufacturé. Que diriez-vous de créer une webcam connectée qui affiche, en plus de l'image, des données environnementales comme la température ou l'hygrométrie ?


Body

Dans un précédent article sur l'ESP32-CAM, un petit module à base d'ESP32 de chez Espressif embarquant une caméra OV2640, nous avons découvert le matériel et « dégraissé » le code exemple intégré au support pour Arduino. En plus de nous permettre de libérer de la mémoire flash pour supporter la programmation OTA (via Wi-Fi), nous avons ainsi pu faire un peu connaissance avec différents éléments de code, dont une partie est toutefois restée mystérieuse. Il est grand temps de dépasser la simple phase de réutilisation/adaptation et de nous pencher sur la manière de développer un projet à partir de zéro.

L'objectif ici sera de créer une caméra accessible via un navigateur web, fournissant non seulement une image avec une résolution acceptable, mais également des informations en provenance d'un capteur BME280 (température, humidité relative et pression atmosphérique). Ayant naturellement un esprit tordu, mais pas au point d'implémenter cela en JavaScript, l'idée n'est pas d'ajouter ces informations autour de l'image ou sur l'image en manipulant une page web. Non, l'idée est d'intégrer ces informations (et d'autres) directement dans l'image, avec comme évolution possible le fait de déployer une flottille d'ESP32-CAM où chacune d'elles se contente de fournir un simple JPEG (et non une page web à intégrer dans une FRAME). L'objectif est de tout intégrer dans l'image, à sa source.

Cette approche radicale impliquera donc de non seulement lire l'image depuis la caméra OV2640, mais également de la modifier à la volée. Nous allons donc devoir explorer en détail la façon dont les données sont tirées du matériel et la forme qu'elles prennent en mémoire.

v-ESP32CAMpinout

L’ESP32-CAM de DIYmore semble plus intéressant que l'ESP-EYE d'Espressif en raison des quelque 16 broches présentes, mais en réalité, celles-ci sont parfaitement inutilisables sans sacrifier une fonctionnalité présente sur le module. Dans notre cas, pour utiliser deux GPIO en guise d'interface I2C, nous devons nous passer du support microSD.

Précisons, d'entrée de jeu, qu'une des plus grandes difficultés ici consistera à faire des sacrifices. En effet, l'ESP32-CAM est assez pauvre en GPIO, malgré les 16 broches présentes sur le circuit imprimé. En retirant les broches liées à l'alimentation, il ne nous en reste plus que 10. Si nous écartons également les broches dédiées à la communication série (U0R et U0T) et à la programmation (IO0), ce nombre tombe à 7. Sachant que les lignes IO4, IO2, IO14, IO15, IO13 et IO12 sont également connectées à la microSD, il ne nous reste plus qu'IO16 (alias U2RXD ou U1R) pour connecter un éventuel capteur. On pourrait s'imaginer se satisfaire de cela et utiliser, par exemple, un capteur de température Dallas 18B20 interfacé en 1-wire, mais IO16 est également connectée à la broche /CE de la PSRAM SPI embarquée qui permet d'étendre la mémoire vive. Or, les 4 Mo de SRAM supplémentaires sont indispensables dès lors que l'on souhaite utiliser la caméra avec une résolution acceptable.

En résumé : si vous voulez utiliser un capteur ou tout autre montage satellite avec l'ESP32-CAM, vous allez devoir sacrifier le support microSD. De plus, il faut également savoir que la LED blanche de forte puissance, connectée au GPIO4 est également utilisée pour la microSD. Celle-ci va donc flasher à chaque accès au support, ce qui, dans certaines situations, est loin d'être discret. La seule solution serait de dessouder la LED et/ou le MOSFET qui la contrôle.

v-espcam2

Voici le résultat obtenu dans le navigateur avec l'image de base présentant une créature étrange doutant du bon fonctionnement du code, les croix vertes en surimpression et surtout les informations réseau et environnementales. Intégrer cette image et d'autres sur une page web serait un jeu d'enfant, car après tout, il ne s'agit que d'un JPEG.

Notre projet du moment n'utilisera pas le support microSD, puisque les images sont destinées à s'afficher dans un navigateur web. Mais pour d'autres projets, l'utilisation d'un capteur I2C, 1-wire ou même SPI, de concert avec un support de stockage, risque d'être un vrai casse-tête.

1. Le projet dans les grandes lignes

Vous l'avez compris, nous allons mettre en œuvre un capteur interfacé en I2C et plus spécifiquement, un BME280 de Bosch permettant une mesure relativement précise de la température, de l'humidité et de la pression atmosphérique. Ces informations seront affichées en surimpression dans l'image, accompagnées du nom d'hôte de l'ESP32, de son adresse IP et d'une indication de la force du signal Wi-Fi (RSSI pour Received Signal Strength Indication).

Pour utiliser ce module, nous ferons usage d'une bibliothèque écrite par Tyler Glenn et disponible sur https://github.com/finitespace/BME280. Pourquoi ne pas utiliser la classique bibliothèque Adafruit BME280 ? Tout simplement parce qu'elle provoque un conflit avec le code de gestion de la caméra dans le support ESP32. Les deux codes utilisent des structures nommées sensor_t, mais totalement différentes. C'est assez curieux, car Adafruit semble avoir l'habitude d'intégrer systématiquement le nom de la société partout (y compris dans des séquences d'initialisation d'écran OLED), sans doute pour s'assurer une grande visibilité, mais ne daigne pas préfixer ses structures de la même manière pour éviter les problèmes... Notez que si sensor_t existe dans Adafruit BME280, il n'est pas impossible que ce nom soit également utilisé dans d'autres bibliothèques Adafruit.

Nous ferons fonctionner un serveur web HTTP sur l'ESP32 afin de fournir au navigateur à la fois l'image retraitée de la caméra, mais également une page web très basique affichant cette même image de façon répétitive, toutes les deux secondes. Le code exemple de l'ESP32-CAM montre qu'il est possible de capturer des images fixes, mais également un flux MJPEG pour un affichage « en live ». Cette possibilité a été testée pour ce projet et est plus ou moins fonctionnelle, mais ne présente que peu d'intérêt en vérité. En effet, comme nous voulons travailler avec une image d'assez grande résolution (SVGA en 800×600) et que nous la manipulons en temps réel, le nombre d'images par seconde est drastiquement réduit, ce qui annule tout avantage d'un flux en lieu et place d'une succession de captures (puisque le débit d'images sera plus ou moins le même dans un cas comme dans l'autre). L'idée n'est pas d'avoir une vidéo, mais simplement le visuel d'une situation, accompagné d'informations complémentaires.

À propos des informations provenant du BME280, il y a deux façons d'envisager la situation :

  • obtenir ces informations et donc lire le capteur à chaque fois qu'une image est demandée et donc traitée ;
  • ou lire ces données avec une fréquence moindre de façon indépendante du traitement d'image.

Partant du principe que les données environnementales comme la température ne changent pas très rapidement, du moins pas aussi rapidement que ce qui est montré sur une succession de photos prises avec un intervalle de deux secondes, il n'est pas nécessaire de perdre du temps de traitement (et donc de réponse de la part du serveur web) en procédant à des transactions I2C. Le fait de détacher la fréquence d'affichage de l'image de celle de relève des mesures nous offre également une certaine liberté de configuration. Rien ne nous empêche, au final, d'utiliser des fréquences proches, mais un tel changement n'impactera qu'une seule ligne du croquis et non toute une fonction plus complexe.

Enfin, étant donné la quantité de code à traiter pour ce projet, je me concentrerai ici sur les fonctionnalités spécifiques au sujet. Comme tous mes projets concernant un ESP8266 ou un ESP32, je ferai usage de l'OTA permettant de programmer la plateforme directement en Wi-Fi et non via une liaison série qui, dans le cas de l'ESP32-CAM, nécessite du matériel complémentaire. Le précédent article à propos de ce module était, entre autres, dédié à l'ajout de cette fonctionnalité ainsi qu'à la configuration du client Wi-Fi (et l'usage de l'EEPROM émulée pour stocker la configuration). Nous n'en reparlerons pas ici.

2. Préparer le terrain : créons des fonctions

Généralement, lorsque je sens qu'un code risque de prendre une certaine ampleur rapidement, j'évite de tout stocker dans setup() et/ou loop() et préfère découper les fonctionnalités en fonctions. Ceci est d'autant plus vrai ici, sachant que le travail à réaliser est assez important. Nous devons en effet :

  • configurer le réseau Wi-Fi ;
  • initialiser le BME280 ;
  • initialiser/configurer la caméra ;
  • configurer un serveur HTTP ;
  • capturer l'image de la caméra ;
  • convertir le format de données obtenu en quelque chose de manipulable ;
  • apporter des changements à l'image ;
  • reconvertir les données en un format acceptable pour un client web ;
  • et servir les données au client.

Mieux vaut donc découper tout cela, même si certaines étapes sont séquentielles, afin de pouvoir travailler indépendamment sur chacune d'elles par la suite. Ne perdez jamais de vu qu'un code n'est jamais réellement fini et que vous aurez toujours des améliorations à apporter. Inutile donc de vous précipiter et mettre des bâtons dans les roues de votre futur vous.

v-espcam ds3231 0

Une horloge temps-réel en I2C est généralement un ajout pertinent, puisqu'elle permet d'horodater les images. Malheureusement, le seul module qu'il me reste sous la main est celui-ci, construit autour d'un DS3231, avec une pile en fin de vie qu'il va me falloir remplacer et donc dessouder. Sans oublier que l'ESP32 étant connecté au Wi-Fi, il lui est possible de récupérer date et heure via le réseau, si nécessaire.

Comme d'habitude, nous allons commencer par inclure les bibliothèques et déclarer quelques macros :

#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
 
// macros dessin image
#define COLOR_WHITE 0x00FFFFFF
#define COLOR_BLACK 0x00000000
#define COLOR_RED    0x000000FF
#define COLOR_GREEN 0x0000FF00
#define COLOR_BLUE   0x00FF0000
#define COLOR_YELLOW (COLOR_RED | COLOR_GREEN)
#define COLOR_CYAN   (COLOR_BLUE | COLOR_GREEN)
#define COLOR_PURPLE (COLOR_BLUE | COLOR_RED)
#define ESPX 100
#define ESPY 100
#define TAILLE 10

Je vous fais grâce ici d'une partie de tout cela, que vous retrouverez dans le croquis final mis à disposition dans le dépôt GitHub du numéro. Le premier bloc de définitions concerne la configuration des connexions entre la caméra et l'ESP32. Ceci est tiré du fichier camera_pins.h de l'exemple livré avec le support ESP32. Nous nous limitons cependant aux macros originellement prévues pour le support du modèle de caméra CAMERA_MODEL_AI_THINKER que nous savons compatible avec notre ESP32-CAM. Le second bloc est spécifique à la fonction que nous allons écrire pour modifier les images et que nous verrons plus loin.

Nous aurons également besoin de quelques variables globales :

// configuration du Wi-Fi/EEPROM
EEconf readconf;
 
// accès à notre serveur http
httpd_handle_t cam_httpd = NULL;
 
// capteur BME280
BME280I2C bme;
 
// variable pour stocker les valeurs du BME280
float temp, hum, pres;
 
// gestion millis pour relève mesures
unsigned long previousMillis = 0;

readconf est spécifique à ma façon de gérer la connexion Wi-Fi et est présent ici pour rendre cohérent une partie du code que nous verrons plus tard. cam_httpd est très classique dans l'utilisation du serveur HTTP sur ESP8266/ESP32. C'est une sorte de point d'accès nous permettant de contrôler le serveur. bme représente notre capteur Bosch et la série de variables qui suit, temp, hum et pres, permet de stocker respectivement les valeurs de température, humidité et pression. L'idée est ici de lire le capteur dans loop() et d’affecter les valeurs lues à ces variables pour qu'elles puissent être utilisées ailleurs (sans lecture effective, donc). Enfin, le très classique previousMillis nous permettra justement de déclencher les mesures sans bloquer le croquis avec delay().

Le décor étant maintenant planté, passons directement à notre première fonction avec l'initialisation et la configuration de la caméra :

int configcam() {
  camera_config_t config;
  
  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.jpeg_quality = 10;
  config.fb_count = 2;
 
  // init caméra
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Erreur init camera 0x%x", err);
    return 1;
  }
 
  // config caméra
  sensor_t *s = esp_camera_sensor_get();
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);
  s->set_framesize(s, FRAMESIZE_SVGA);    // 800x600
 
  return 0;
}

Vous remarquerez qu'il y a ici une petite économie de lignes à faire, puisque nous initialisons config avec les macros précédemment définies. Nous aurions tout aussi bien pu utiliser les valeurs directement, mais l'avantage n'est que cosmétique. Les macros ne finissent pas dans le code et n'occupent donc pas de place, il s'agit de simples substitutions opérées par le préprocesseur, alors que le gain est évident : ceci nous permet d'adapter facilement le code à une autre plateforme, comme la pathétique carte de développement ESP-EYE (avec un port USB certes, mais pas une seule E/S disponible).

En dehors de l'initialisation de config, directement utilisé avec esp_camera_init(), nous ajustons quelques éléments de configuration. esp_camera_sensor_get() nous permet de récupérer les paramètres actuels qui deviennent accessibles via un pointeur sur un objet de type sensor_t (celui-là même qui rend la bibliothèque Adafruit incompatible). Nous utilisons alors les méthodes disponibles pour modifier l'orientation de l'image et spécifier la résolution de la capture. FRAMESIZE_SVGA provient directement de esp32-camera/sensor.h et permet de spécifier l'une des 12 résolutions supportées, de 160×120 (QQVGA) a 2048×1536 (QXGA). Notez qu'on parle ici de « supporté par la caméra », mais pas nécessairement par l'ESP32 et en particulier sa mémoire disponible. Dans ce même fichier d'en-tête, vous trouverez également la structure sensor_t contenant les paramètres modifiables (plus exactement, les pointeurs vers les méthodes disponibles pour ajuster ces paramètres). Ceci concerne le format d'image, mais aussi des choses comme le contraste, la balance automatique des blancs, un éventuel effet sur l'image, etc.

Avant d'attaquer le plat de résistance, penchons-nous un instant sur la configuration du serveur HTTP qui prend, bien entendu, également la forme d'une fonction :

void confServeurWeb() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
 
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx = NULL
  };
  
  httpd_uri_t capture_uri = {
    .uri       = "/jpeg",
    .method    = HTTP_GET,
    .handler   = jpg_handler,
    .user_ctx = NULL
  };
 
  Serial.printf("Démarrage serveur HTTP port %d\r\n", config.server_port);
  if (httpd_start(&cam_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(cam_httpd, &index_uri);
    httpd_register_uri_handler(cam_httpd, &capture_uri);
    Serial.println("Serveur HTTP ok.");
  } else {
    Serial.println("Erreur démarrage serveur HTTP");
  }
}

Nous avons là quelque chose de relativement classique qu'on retrouve dans la plupart des exemples accompagnant le support ESP32 et ESP8266 de l'environnement Arduino. Chaque URL à traiter prend la forme d'une variable contenant l'URI (identifiant de ressource), la méthode HTTP utilisée (ici, GET) et la fonction chargée de traiter la requête. On utilise ensuite httpd_start() pour démarrer le serveur, puis enregistrer ces URI. Ici, nous traitons « / » qui est la page racine/index du serveur et « /jpeg » qui retournera l'image capturée au format JPEG.

v-espcam i2c

Le gros avantage du bus I2C face au SPI, par exemple, est précisément le fait qu'il s'agisse d'un bus. Avec deux simples lignes, il est donc possible de s'interfacer avec une myriade de capteurs ayant chacun leur adresse. La plupart des modules permettent de personnaliser cette information et donc d'utiliser plusieurs exemplaires du même composant sur un unique bus.

confServeurWeb() sera appelée au dernier moment à la fin de setup() lorsque tout aura été configuré et correctement initialisé.

3. Occupons-nous des images

La fonction jpg_handler(), chargée de répondre aux requêtes concernant /jpeg, doit envoyer des données JPEG en retour au navigateur web. Ça tombe bien, c'est déjà le format utilisé pour obtenir une image du capteur optique. Ce qui tombe moins bien, en revanche, est le fait que nous voulons apporter des modifications à cette image, chose qu'il n'est pas possible de faire directement, puisque tout est compressé. Nous n'avons donc pas directement accès aux pixels et devrons passer par une étape de conversion, avant de modifier quoi que ce soit.

Heureusement pour nous, la bibliothèque supportant la caméra offre également des fonctions de conversion des données ainsi que d'autres fournissant des primitives graphiques simplistes. Toutes ces manipulations vont consommer de la mémoire en masse et impliqueront de jouer avec des tableaux. Voilà une excellente occasion de réviser l'un des éléments les plus amusants du C/C++ : les pointeurs !

Commençons par déclarer les variables dont nous allons avoir besoin :

static esp_err_t jpg_handler(httpd_req_t *req){
  camera_fb_t *fb = NULL;  // framebuffer cam
  esp_err_t res = ESP_OK;  // réponse/return
  uint8_t *rgb;            // data RGB
  size_t rgb_len;          // taille data RGB
  fb_data_t rgbfb;         // framebuffer RGB
  uint8_t *jpg;            // data JPG
  size_t jpg_len;          // taille data JPG

Le support de la caméra utilise des framebuffers, des blocs de mémoire contenant une représentation d'une trame complète de l'image capturée par la caméra. Le support de la caméra nous permet d'obtenir un camera_fb_t contenant non seulement un pointeur vers les données, mais également des « métadonnées » comme les dimensions de l'image. Nous allons devoir convertir l'image dans un autre format et la placer dans un simple tableau de uint8_t (rgb) qui nous servira ensuite de base pour construire un fb_data_t que nous compléterons avec les mêmes « métadonnées ». Ce framebuffer (rgbfb) pourra ensuite être utilisé par les primitives graphiques pour ajouter des éléments et du texte, pour enfin reconvertir le tout en JPEG (jpg) et l'envoyer au navigateur.

Notez que certains de ces tableaux, ou buffers, existent déjà, car créés lors de l'initialisation de la caméra alors que d'autres sont créés par les fonctions de conversion de format ou demandent à être créés manuellement par nos soins. Voilà une sympathique opportunité de jouer avec l'allocation mémoire, des pointeurs et la façon de gérer la PSRAM.

Mais commençons par le commencement, en récupérant tout d'abord image du capteur de la caméra :

  fb = esp_camera_fb_get();
  if(!fb) {
    Serial.println("Erreur capture caméra !");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }
 
  httpd_resp_set_type(req, "image/jpeg");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");

Si tout se passe bien, fb est initialisé avec un pointeur vers toutes les informations utiles, parmi lesquelles un pointeur vers le tableau contenant les données de l'image au format JPEG. Si esp_camera_fb_get() retourne NULL, c'est qu'un problème est survenu et nous réagissons en conséquence en stoppant tout et en retournant une erreur HTTP. Dans le cas contraire, nous avons une image et nous pouvons envoyer l'en-tête HTTP adéquat.

Il nous faut maintenant convertir les données et la fonction fmt2rgb888() permet précisément cela. rgb888 fait référence au format souhaité, où les données sont constituées de trois valeurs de 8 bits par pixel : rouge, vert et bleu. Mais cette fonction n'alloue pas la mémoire pour y placer les données, nous devons avoir un tableau à disposition, dont l'adresse sera passée en argument. Nous commençons donc par allouer de la mémoire :

  rgb_len = (fb->width)*(fb->height)*3;
  if((rgb = (uint8_t*)ps_malloc(rgb_len)) == NULL) {
    Serial.println("Erreur ps_malloc !");
    esp_camera_fb_return(fb);
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

rgb_len contient la taille des données : 1 octet par couleur « primaire », fois la hauteur, fois la largeur de l'image. Ici, avec la caméra réglée sur une résolution SVGA, nous parlons de : 800 x 600 x 3, soit 1440000 octets, ou environ 1,37 Mo. On comprend immédiatement la raison d'être des 4 Mo de PSRAM accompagnant l'ESP32 et la baisse de débit à mesure que la résolution augmente, etc. Pour rappel, un ESP32 sans PSRAM ne possède que 520 Ko de SRAM, déjà partiellement consommés par FreeRTOS, les caches et la gestion réseau, ce qui ne laisse en réalité que quelque 320 Ko pour vos codes.

À présent que rgb pointe sur une zone mémoire allouée correspondante à la taille des données à convertir, nous pouvons procéder à l'opération :

  if(!fmt2rgb888(fb->buf, fb->len, fb->format, rgb)) {
    Serial.println("Erreur fmt2rgb888 !");
    free(rgb);
    esp_camera_fb_return(fb);
    httpd_resp_send_500(req);
    return ESP_FAIL;    
  }

Nous utilisons le buffer contenant l'image initiale, sa taille et le format de départ tel que décrit dans fb et, en dernier argument, nous spécifions le pointeur vers le buffer cible que nous venons de créer. Suite à l'appel de cette fonction, rgb pointera vers les 1,37 Mo de données au format RGB888. Ce format est très simple, puisqu'il s'agit d'une suite d'octets précisant les valeurs de rouge, vert et bleu pour chaque pixel.

Nous pourrions donc changer manuellement ces valeurs pour modifier l'image, mais une meilleure approche est possible. Un certain nombre de fonctions sont à notre disposition pour dessiner dans une image, mais celles-ci ne prennent pas en argument un pointeur vers une grosse masse d'octets (uint8_t). En lieu et place, elles utilisent un pointeur vers un fb_data_t, reprenant un pointeur vers les données, mais aussi des « métadonnées » sur l'image. Il est relativement facile de créer ce nouveau framebuffer :

  rgbfb.width = fb->width;
  rgbfb.height = fb->height;
  rgbfb.data = rgb;
  rgbfb.bytes_per_pixel = 3;
  rgbfb.format = FB_BGR888;
 
  ajoutOSD(&rgbfb);

Laissons pour l'instant de côté cet appel à ajoutOSD() que nous verrons plus loin. Il s'agit de la fonction permettant d'ajouter nos informations sur l'image, mais ne nous interrompons pas dans notre élan. En effet, une fois le nouveau framebuffer créé et utilisé pour les modifications, nous devons le reconvertir en JPEG. Encore une fois, une fonction est disponible pour cette opération (cf. esp32-camera/img_converters.h) :

  if(!fmt2jpg(rgb, rgb_len, rgbfb.width, rgbfb.height,
              PIXFORMAT_RGB888, 80, &jpg, &jpg_len)) {
    Serial.println("Erreur fmt2jpg !");
    free(rgb);
    esp_camera_fb_return(fb);
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

fmt2jpg() est un peu plus « riche » que fmt2rgb888() et prend respectivement un pointeur sur le buffer RGB, sa taille, la largeur de l'image, la hauteur de l'image, le format d'origine, le niveau de qualité JPEG (différent de celui spécifié à la configuration de la caméra), un pointeur vers un pointeur sur les données JPEG à créer et un pointeur vers la taille de ces données.

v-espcam4

En ayant un accès aux valeurs RGB de chaque pixel, il est est possible d'ajouter plus ou moins n'importe quoi à l'image : comme ici une grille, mais, pourquoi pas, un graphique, un logo, l'heure courante, une barre de progression...

Notez qu'il est bien question ici d'un pointeur vers un pointeur sur les données et d'un pointeur vers un entier 32 bits (size_t) indiquant la taille des données, d'où le &rgb alors que rgb est déclaré comme un pointeur vers un uint8_t. Ceci est obligatoire, car contrairement à fmt2rgb888(), la fonction fmt2jpg() va allouer la mémoire pour les données JPEG. Ce qui est parfaitement logique, car au format RGB888 la taille est invariable, alors qu'avec des données compressées, cette taille dépend totalement de la complexité de l'image et de la qualité choisie pour le JPEG.

Une fois les données JPEG produites, il nous suffit de les envoyer au client :

  res = httpd_resp_send(req, (const char *)jpg, jpg_len);
  Serial.printf("Réponse HTTP, %zu\r\n", jpg_len);
 
  free(rgb);
  free(jpg);
  esp_camera_fb_return(fb);
  return res;
}

Sans oublier, bien entendu, de faire un brin de ménage pour libérer la mémoire utilisée avant de conclure la fonction. En phase de test, il est fortement recommandé de surveiller l'utilisation de la mémoire PSRAM à différents endroits d'une telle fonction, afin de s'assurer qu'il n'y a pas de fuite mémoire. Ceci est relativement simple à faire en criblant son code de Serial.println(ESP.getPsramSize()) aux endroits stratégiques. Le but est d'avoir exactement autant de PSRAM libre avant et après les manipulations, allocations, libérations, etc.

4. Ajoutons des informations à l'image

En l'absence de l'invocation de ajoutOSD() dans la fonction précédente, nous ne faisons que convertir des données pour rien. De base, comme les images sont par défaut en JPEG, il nous aurait suffit de passer pointeur fb->buf à httpd_resp_send(). La raison d'être de ces opérations est précisément de modifier l'image « à la volée ». Chose que nous nous empressons de faire.

Nous allons diviser cette fonction en deux parties, car nous allons ajouter deux types d'éléments graphiques : des rectangles pleins et du texte. Le premier type n'a absolument aucune utilité pratique autre que didactique dans le contexte de cet article. L'idée est d'ajouter un effet « sonde martienne » en plaçant de petites croix vertes à intervalles réguliers sur l'image :

void ajoutOSD(fb_data_t *rgbfb) {
  char infostr[255];       // buffer texte OSD
 
  for(int x=ESPX; x<=(rgbfb->width)-ESPX; x+=ESPX) {
    for(int y=ESPY; y<=(rgbfb->height)-ESPY; y+=ESPY) {
      fb_gfx_fillRect(rgbfb, x-1, y-TAILLE, 2, TAILLE*2, COLOR_GREEN);
      fb_gfx_fillRect(rgbfb, x-TAILLE, y-1, TAILLE*2, 2, COLOR_GREEN);
    }
  }

La fonction prend en argument un pointeur vers notre framebuffer RGB et se contente pour l'instant de dessiner des rectangles horizontaux et verticaux via une double boucle for. Notez qu'il est très important de rester dans le système de coordonnées correspondant à la géométrie de l'image, sous peine de se retrouver avec une corruption de mémoire. En effet, la fonction fb_gfx_fillRect() ne procède à aucune vérification et dessiner hors de l'image revient à écrire hors de la mémoire allouée (corruption du tas). Les macros utilisées ici, comme TAILLE, ESPX ou encore COLOR_GREEN sont celles précisées en tout début d'article et de croquis.

Une fois la « grille » de petites croix dessinée, nous pouvons nous pencher sur l'ajout de texte. Là encore, une fonction dédiée est fournie :

  snprintf(infostr, 255, " %s.local (%d.%d.%d.%d) RSSI: %d dBm ",
    readconf.myhostname,
    WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3],
    WiFi.RSSI());
  fb_gfx_fillRect(rgbfb, 10, (rgbfb->height)-10-24, 14*strlen(infostr), 24, COLOR_BLACK);
  fb_gfx_print(rgbfb, 10, (rgbfb->height)-10-24, COLOR_WHITE, infostr);
 
  memset(infostr, 0, sizeof(infostr));
  snprintf(infostr, 255, " Temp: %.2fC Hum: %.0f%% Pres: %.2f hPa ", temp, hum, pres/100);
  fb_gfx_fillRect(rgbfb, 10, 10, 14*strlen(infostr), 24, COLOR_BLACK);
  fb_gfx_print(rgbfb, 10, 10, COLOR_WHITE, infostr);
}

Cette primitive graphique est très basique et ne permet pas de choisir la police ou sa taille. Après maints essais et tâtonnements, il s'avère qu'un caractère semble faire 14 pixels de large sur 24 de haut, et ce, indépendamment de la résolution de l'image. Ceci semble confirmé par un simple coup d’œil au fichier libfb_gfx.a qui mentionne une police FreeMonoBold12pt7b. Tenter d'utiliser cette fonction avec une image en QVGA est donc une perte de temps, à moins de se limiter à un ou deux caractères tout au plus.

Ici, nous avons amplement de place avec nos 800 par 600 pixels et pouvons ajouter, en haut de l'image, les informations environnementales et en bas, celles concernant la connexion Wi-Fi/IP. Notez qu'il existe une déclinaison de la fonction fb_gfx_print(), nommée fb_gfx_printf() et permettant d'utiliser des instructions de formatage (%u, %x, %f, etc.). J'ai cependant préféré utiliser la bonne vieille technique du snprintf()/sprintf afin de composer préalablement une chaîne pour ensuite la passer à fb_gfx_print().

Ces primitives graphiques sont deux des six disponibles et dont les prototypes sont précisés dans fb_gfx/fb_gfx.h :

  • fb_gfx_fillRect() : rectangle plein ;
  • fb_gfx_drawFastHLine() : ligne horizontale ;
  • fb_gfx_drawFastVLine() : ligne verticale ;
  • fb_gfx_putc() : impression d'un caractère ;
  • fb_gfx_print() : impression d'une chaîne ;
  • fb_gfx_printf() : impression d'une chaîne avec descripteur de format.

Ceci est très spartiate et il serait intéressant de pouvoir compléter ces fonctions avec des primitives plus avancées comme des disques, des cercles et des arcs de cercle. Malheureusement, les sources de libfb_gfx.a ne semblent pas disponibles (??). Il est toutefois possible d'implémenter de telles fonctions de manière indépendante, puisque le type fb_data_t est connu et que le format des données du buffer est simple. Après tout, ce n'est l'affaire que d'un peu de trigonométrie...

v-espcam3

Comme l'ensemble des données RGB888 peut être manipulé, retirer (mettre à zéro) des composantes de couleurs revient à simplement parcourir le buffer et changer des valeurs. En retirant ainsi le vert et le bleu, nous voici avec une version très « Terminator » du résultat. Il ne manque presque plus qu'une touche d'assembleur 6502 pour renforcer l'aspect authentique de l'ensemble (hé oui, le HUD du T-800 affiche du code ASM Apple-II).

Arrêtons là les explications, car le reste est relativement classique. La configuration du capteur BME280 dans setup() se résume à quelques lignes :

Wire.begin(12, 13); // sda, scl
if (!bme.begin()){
  Serial.println("Erreur BME280");
} else {
  if(bme.chipModel() == BME280::ChipModel_BME280)
    temp = bme.temp();
    hum = bme.hum();
    pres = bme.pres();
    Serial.println("Modèle BME280");
}

et il suffit d'appeler nos fonctions dans le bon ordre :

  if(configcam()) {
    Serial.println(F("\r\nErreur configuration cam !"));
    return;
  } else {
    Serial.println(F("\r\nConfiguration cam ok."));
  }
 
  confServeurWeb();
}

Dans loop(), nous nous contentons de procéder aux relevés de valeurs du capteur régulièrement :

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= 30000) {
    temp = bme.temp();
    hum = bme.hum();
    pres = bme.pres();
    previousMillis = currentMillis;
  }
 
  // gestion OTA
  ArduinoOTA.handle();
}

5. Et la page index, alors ?

L'idée originale présente dans l'exemple livré avec le support ESP32 m'a tellement conquise que j'ai simplement décidé de sauvagement et honteusement la copier. Rappelez-vous, plutôt que de stocker un fichier HTML sur un espace de stockage (SD ou SPIFFS), le croquis d'exemple utilisait une version compressée stockée dans un tableau de uint8_t. En reprenant le concept, nous pouvons écrire la courte fonction suivante chargée de répondre aux requêtes vers la page index :

static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_set_type(req, "text/html");
  httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
  return httpd_resp_send(req, (const char *)index_html_gz, index_html_gz_len);
}

Notre HTML sera composé à côté, sur un système GNU/Linux (distribution x86, Raspberry Pi ou Windows 10 WSL) :

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Page Caméra</title>
</head>
<body>
<div>
<img src="/jpeg" id="monImage" />
</div>
<script>
setInterval(function() {
    var myImageElement = document.getElementById('monImage');
    myImageElement.src = '/jpeg?rand=' + Math.random();
}, 2000);
</script>
</body>
</html>

puis compressé et transformé en déclaration/initialisation de variable avec xxd :

$ gzip -c index.html > index.html.gz
$ xxd -c 8 -i index.html.gz
unsigned char index_html_gz[] = {
  0x1f, 0x8b, 0x08, 0x08, 0xd1, 0x30, 0xfb, 0x5d,
  0x00, 0x03, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2e,
[...]
  0xb7, 0x27, 0xf2, 0xf5, 0x1b, 0xff, 0x00, 0xc6,
  0xac, 0xeb, 0xe9, 0x57, 0x01, 0x00, 0x00
};
unsigned int index_html_gz_len = 271;

Le contenu sera alors sensiblement modifié et intégré au croquis :

#define index_html_gz_len 271
const uint8_t index_html_gz[] = {
  0x1f, 0x8b, 0x08, 0x08, 0xd1, 0x30, 0xfb, 0x5d,
  0x00, 0x03, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2e,
[...]
  0xb7, 0x27, 0xf2, 0xf5, 0x1b, 0xff, 0x00, 0xc6,
  0xac, 0xeb, 0xe9, 0x57, 0x01, 0x00, 0x00
};

Je vous laisse le soin de vous acoquiner avec les deux malheureuses lignes de JavaScript rafraîchissant l'image ou, comme on le dit généralement : « l'étude du code JavaScript intégré dans la page HTML est laissé en exercice au lecteur ».

v-espcam insitu

Voici le montage en phase de test reposant sur un support hautement technologique de conception révolutionnaire également appelé... un pot à crayons avec une platine à essais en équilibre.

Conclusion

Le lecteur assidu aura sans doute remarqué que nous configurons la caméra avec un format PIXFORMAT_JPEG. Pourquoi ne pas directement demander un format PIXFORMAT_RGB888 et ainsi sauter l'étape de conversion de JPEG en RGB888 ? Ceci a été testé et ne semble pas fonctionner. Une erreur concernant un temps de capture trop important est déclenchée lors de l'appel à esp_camera_fb_get(). Une sorte de watchdog semble être configuré afin d'éviter de rendre l'appel bloquant. Il n'est pas impossible qu'il existe une solution, en allongeant d'une façon ou d'une autre ce délai limite, mais je n'ai rien trouvé dans les fonctions et paramètres utilisables qui aille dans ce sens.Quoi qu'il en soit, le projet est arrivé à son terme et permet d'un coup d’œil d'avoir à la fois une image du lieu surveillé et des informations sur l'environnement. Le croquis final avec OTA occupe moins de la moitié de la flash disponible sur l'ESP32 et il reste donc énormément de place pour ajouter d'autres fonctionnalités. L'usage du bus I2C pour le capteur permet également d'envisager, sans surcoût en GPIO, l'ajout d'autres mesures (luxmètre, capteur de gaz, UV, etc.).Mais je crois que la voie la plus intéressante, en ce qui me concerne, est celle consistant à chercher à pallier aux faiblesses actuelles et donc de fournir à ce projet, et ceux qui suivront, une bibliothèque graphique plus complète. Des primitives graphiques supplémentaires incluant des courbes et une gestion plus complète du texte consisteraient en une avancée importante. Il devrait être possible, pour cela, de s'inspirer d'autres bibliothèques comme la fantastique U8g2 (https://github.com/olikraus/u8g2) originellement destinée à la gestion d'écrans monochromes. En adaptant quelques fonctions simples, comme u8g2_DrawPixel(), on s'ouvre alors toute une collection de primitives plus avancées, dont l'écriture de texte avec un choix de police.

v-espcam oled

La bibliothèque U8g2 prend en charge des écrans monochromes interfacés en I2C ou SPI et intègre un grand nombre de primitives graphiques incluant une gestion de polices de caractères. Une partie du code de cette bibliothèque, sous licence BSD, pourrait être réutilisée pour améliorer le traitement des images par l'ESP32. Une autre approche pourrait être de soumettre la fonctionnalité inverse aux développeurs d'U8g2 : prendre en charge un buffer RGB888 comme celui de l'ESP32-CAM comme s'il s'agissait d'un écran.

Dans un tout autre registre, il est possible de travailler sur l'interactivité en se penchant sur l'aspect HTML/JS. Là encore, on pourra s'inspirer de l'exemple de base, mais également envisager des choses plus audacieuses, comme l'utilisation de la capture en guise d'image-map (balise HTML MAP) pour déclencher des actions. Mais je préfère laisser cela aux personnes ayant davantage d'affinités avec les langages du Web que votre humble serviteur...En guise de mot de la fin, un petit conseil : lorsque vous reprogrammez votre ESP32 en OTA, assurez-vous que la page web ne soit pas ouverte par un navigateur. Sans que cela pose réellement problème, une double communication Wi-Fi OTA+HTTP ralentit de manière intermittente la programmation et on en vient à douter de la qualité/stabilité de la liaison. Fermez donc simplement la page avant de lancer la mise à jour de votre code...



Article rédigé par

Par le(s) même(s) auteur(s)

Cross-compilation d'OpenBSD : c'est mal (tm), mais c’est pas grave...

Magazine
Marque
Hackable
Numéro
57
Mois de parution
novembre 2024
Spécialité(s)
Résumé

OpenBSD est un système spécial, adorablement spécial même, et parmi ses spécificités, on trouve par exemple le fait de privilégier la compilation native, par opposition à la compilation croisée. Je n'invente rien, c'est dans la documentation [1]. Ceci dit, même si les raisons évoquées sont parfaitement compréhensibles et légitimes, ce n'est pas parce que quelque chose n'est pas recommandé qu'il ne faut pas le faire. Cross-compilons donc un noyau OpenBSD pour arm64 depuis amd64...

Outil en ligne de commande : pourquoi pas l'assembleur ?

Magazine
Marque
GNU/Linux Magazine
Numéro
272
Mois de parution
novembre 2024
Spécialité(s)
Résumé

Le C est sur la sellette ces derniers temps, car, voyez-vous, c'est un langage peu sûr, qui laisse faire tout et n'importe quoi au programmeur. Et comme on ne peut pas faire confiance aux programmeurs pour s'occuper proprement de la mémoire qui leur est gracieusement confiée, il est important de promptement leur confisquer ce langage dangereux, permissif, et disons-le franchement, totalement maléfique. Si, si ! J'adhère totalement à cette vision et, moi aussi, je vais changer de langage préféré pour assurer ma sécurité, protéger les chatons et sauver les petits enfants. Au revoir le méchant C, bonjour le gentil assembleur !

RPi et I2P : anonymiser son trafic avec l'Internet invisible

Magazine
Marque
Hackable
Numéro
57
Mois de parution
novembre 2024
Spécialité(s)
Résumé

La surveillance de masse, la censure et les nombreuses restrictions qui pèsent sur Internet, et les communications en général, représentent un énorme problème pour la vie privée et la liberté d'expression. Bien entendu, en Europe, nous ne sommes certainement pas les moins bien lotis, en particulier en comparaison avec des pays aux régimes totalitaires. Mais le fait de dissimuler ses communications et ses échanges de données n’est pas l'apanage des journalistes, des lanceurs d'alerte, des freedom fighters ou même, à l'autre extrême, des groupes mafieux ou terroristes et des réseaux pédophiles. L'anonymisation, le chiffrement et la dissimilation sont devenus l'affaire de tout un chacun désormais.

Les derniers articles Premiums

Les derniers articles Premium

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Les listes de lecture

7 article(s) - ajoutée le 01/07/2020
La SDR permet désormais de toucher du doigt un domaine qui était jusqu'alors inaccessible : la réception et l'interprétation de signaux venus de l'espace. Découvrez ici différentes techniques utilisables, de la plus simple à la plus avancée...
8 article(s) - ajoutée le 01/07/2020
Au-delà de l'aspect nostalgique, le rétrocomputing est l'opportunité unique de renouer avec les concepts de base dans leur plus simple expression. Vous trouverez ici quelques-unes des technologies qui ont fait de l'informatique ce qu'elle est aujourd'hui.
9 article(s) - ajoutée le 01/07/2020
S'initier à la SDR est une activité financièrement très accessible, mais devant l'offre matérielle il est parfois difficile de faire ses premiers pas. Découvrez ici les options à votre disposition et les bases pour aborder cette thématique sereinement.
Voir les 33 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous