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 ?
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.
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.
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.
Comme d'habitude, nous allons commencer par inclure les bibliothèques et déclarer quelques macros :
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 :
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 :
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 :
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.
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 :
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 :
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 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 :
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 :
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) :
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.
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 :
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 :
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 :
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...
Arrêtons là les explications, car le reste est relativement classique. La configuration du capteur BME280 dans setup() se résume à quelques lignes :
et il suffit d'appeler nos fonctions dans le bon ordre :
Dans loop(), nous nous contentons de procéder aux relevés de valeurs du capteur régulièrement :
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 :
Notre HTML sera composé à côté, sur un système GNU/Linux (distribution x86, Raspberry Pi ou Windows 10 WSL) :
puis compressé et transformé en déclaration/initialisation de variable avec xxd :
Le contenu sera alors sensiblement modifié et intégré au croquis :
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 ».
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.
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...