Projet de station météo avec interface HTML sur ESP8266 (DHT22 + BMP180) • Domotique et objets connectés à faire soi-même

mbexlixklg10kibkr5tp-8898298

Code source

Nous allons développer une mini station météo avec une interface HMTL accessible depuis un ordinateur ou smartphone en WiFi. Dans la série d’articles précédents, nous avons découvert comment programmer une interface HTML pour ESP8266 avec du code Arduino.

Voici une série d’article pour débuter avec la programmation de l’interface HTML des ESP8266.

Nous allons aller encore plus loin avec ce nouveau projet en apprenant comment gérer séparément le code de l’interface Web qui sera entièrement stocké dans la zone mémoire de l’ESP8266 à l’aide du système de fichier SPIFFS. Nous verrons également comment stocker les mesures (température, humidité, pression atmosphérique) au format JSON dans un fichier dans la zone mémoire. Ces données seront utilisées pour tracer un historique de mesure à l’aide de la librairie Google Charts.

Présentation du projet de station météo avec un ESP8266

Nous allons aborder de nombreuses notions dans ce projet de station météo connectée réalisée à l’aide d’un ESP8266.

Mais tout d’abord, voici à quoi ressemblera l’interface de la station météo.

q986hrpoqp7jjrt57zl8-7245904

L’interface HTML sera développée à l’aide de Bootstrap. Les informations sont accessibles via 4 onglets :

  • Mesures affiche les mesures des capteurs DHT22 et BMP180
  • Graphiques historique des mesures à l’aide de la librairie Google Charts
  • GPIO pour tester le pilotage du GPIO de l’ESP8266 depuis l’interface WEB. On pourrait par exemple piloter l’ouverture/fermeture des stores, des volets…
  • Configuration

Il y a énormément de notions et d’aspects techniques abordés dans ce projet, il a donc été découpé en 5 parties.

mbexlixklg10kibkr5tp-8898298

Code source

Partie 1 Comment séparer et développer l’interface HTML de la station météo du code Arduino

  • Comment développer le code HTML de interface Web
    • Pour cela, nous utiliserons le langage Pug (auparavant appelé Jade) qui permet de simplifier l’écriture
    • Notions d’HTML abordées : menu de navigation, image, fixer le bas de page (footer), meilleure gestion “responsive” pour les petits écrans
  • Comment changer le thème de l’interface
    • Comment stocker le choix pour recharger le thème au prochain chargement de l’interface
  • Comment préparer et utiliser la zone SPIFFS pour stocker les fichiers HTML, JS, CSS, images
    • et les envoyer sur l’ESP8266
    • Premier test de l’interface WEB sur l’ESP8266

jqa9m4diyhrjr5gfxfwy-7046383

Code source

Partie 2 Comment interagir avec le code Arduino

  • Intercepter les actions sur les boutons de l’interface et actualiser les affichages lorsque l’action a été réalisée (Javascript + jQuery)
    • Récupérer les requêtes sur l’Arduino, exécuter la demande et envoyer la réponse
  • Actualiser régulièrement (et automatiquement) le tableau de mesures et les afficheurs (Javascript)
    • Mettre à jour les symboles si la valeur actuelle est supérieure ou inférieure à la précédente

Partie 3 Comment récupérer l’heure depuis internet sur un serveur de temps à l’aide de la librairie NtpClientLib

Nous verrons comment stocker la date et l’heure dans un fichier sur la zone mémoire à l’aide du système de fichier SPIFFS

Partie 4 Comment collecter les mesures et créer un historique de mesures

  • Manipuler les données à l’aide de la librairie ArduinoJSON
  • Enregistrer des données (historique de mesure) dans un fichier au format JSON sur la zone SPIFFS
  • Recharger le fichier historique au démarrage de l’ESP8266

jqa9m4diyhrjr5gfxfwy-7046383

Code source

Partie 5 Comment ajouter des graphiques et des jauges sur l’interface HTML à l’aide de la librairie Google Charts

  • Evolution de la température et de l’humidité moyenne sur les 7 dernières heures (histogramme en barre)
  • Affichage de la mesure courante sous la forme d’une gauge thématique : thermomètre (température), goutte d’eau (humidité), gauge (pression atmosphérique)

Logiciels nécessaires

Comme vous le savez, il est possible de programmer l’ESP8266 en C++ depuis l’IDE Arduino. C’est une très bonne solution pour débuter car on trouve beaucoup plus facilement des exemples, informations, librairies, forums et l’accès aux débutants et plus facile.

Pour ceux d’entre vous qui êtes plus avancés, vous pouvez vous tourner vers PlatformIO sur VSCode, une solution plus flexible et professionnelle

Il est possible d’écrire directement le code HTML dans le code Arduino mais pour des projets de grande envergure (tel que cette station météo), cela devient vite difficile à maintenir.

Pour contourner ce problème, il est préférable de séparer le code de l’interface HTML ainsi que les feuilles de style CSS et le code Javascript du code Arduino.

Il est également possible d’écriture du code HTML en développant le code de l’interface avec un pseudo-langage. Je vous conseille d’utiliser le langage Pug (ancien Jade). Pug permet de simplifier la mise au point du code car on ne gère plus la fermeture des balises.

N’importe quel éditeur de code fera l’affaire. Si vous êtes sur Raspberry Pi OS (nouveau nom de Raspbian), vous pouvez utiliser Geany. Visual Studio Code de Microsoft reste toutefois une référence et dispose d’un très grand nombre d’extensions qui facilitent la vie.

Matériel nécessaire

Coté matériel, nous allons rester dans le classique, l’objectif et d’avoir à disposition des données à afficher. Le code Arduino du projet est adapté au DHT22 qui permettra de récupérer la température et l’humidité environnante. Le BMP180 permettra de récupérer la pression atmosphérique.

Coté ESP8266, nous utiliserons une LoLin – WeMos d1 mini disposant de 3Mo de mémoire flash attribuable au système de fichier SPIFFS.

Stockage des fichiers

Les fichiers de l’interface HTML (fichiers HTML, feuilles de styles CSS, images, fichiers javascripts…) seront stockés sur la mémoire flash de l’ESP8266 dans la zone réservée au système de fichier

Il existe deux librairies qui permettent de manipuler des fichiers sur un ESP8266 (presque) comme sur un ordinateur. La librairie LittleFS et FS.h qui est une implémentation de SPIFFS pour l’ESP8266. Pour ce projet, nous utiliserons la librairie FS.h.

Comment choisir la taille de la zone mémoire ?

Les modules ESP8266 disposent d’une mémoire flash (similaire aux clés USB). La mémoire flash est découpée et chaque espace est réservée pour chaque fonction :

  • Sketch code C++ du projet
  • OTA update zone tampon utilisée pour télécharger une nouvelle mise à jour du sketch
  • File System (FS) c’est ce qui nous intéresse ici
  • EEPROM une petite zone mémoire qui simule l’EEPROM de l’Arduino. On pourra y stocker des paramètres
  • WiFi config est une zone réservée pour le stockage des paramètres de connexion lorsqu’on développe directement en C++ avec le SDK ESP-IDF
|--------------|-------|---------------|--|--|--|--|--|
^ ^ ^ ^ ^
Sketch OTA update File system EEPROM WiFi config (SDK)

La taille de la mémoire flash varie d’un fabricant à un autre mais la norme est de 4Mo.

Sélectionner votre carte ESP8266 depuis le menu Outils -> Carte de développement

Puis vous pourrez choisir la quantité de mémoire flash allouée au système de fichier depuis le menu Outils -> Flash Size

Par exemple pour la LoLin – WeMos d1 Mini, il existe 4 choix possibles : aucune allocation 1Mo 2Mo ou 3Mo.

ei1fshxwmfb1swelnr2p-7928057

Comment organiser les fichiers puis le transférer dans la zone SPIFFS depuis l’IDE Arduino ?

Tous les fichiers de l’interface seront stockés dans un dossier data situé à la racine du projet (au même niveau que le fichier principal du projet). Voici un exemple d’organisation.

Il faudra téléverser manuellement les fichiers avant le premier démarrage et à chaque modification depuis l’IDE Arduino. Pour cela, il faudra installer le plugin ESP8266fs (ou ESP8266 Sketch Data Upload) qui ajoute une options au menu outil de l’IDE Arduino. Lisez ce tutoriel pour en savoir plus

Si vous débutez avec le système de fichier SPIFFS, commencez par lire ce tutoriel

Code complet du projet

Le code complet du projet est entièrement disponible sur le dépôt GitHub du blog sur cette page.

Voici toutefois le code final du projet pour gagner du temps

Code HTML de l’interface WEB

Le code HTML de la page.

Suivez ce tutoriel qui explique comment téléverser le fichier HTML de la page dans la zone SPIFFS de l’ESP8266


  
    
    
    
    
    
    
    
    Demo ESP8266 SPIFFS + Boostrap - www.projetsdiy.fr
  
  
    
      

ESP8266 Web Server + SPIFFS + Bootstrap + Google Charts

      
      
                
          

Mini station météo (DHT22 + BMP180)

          

Mesure Valeur Valeur Précédente

Pas encore de données

GPIO

D5 OFF

ON OFF

D6 OFF

ON OFF

D7 OFF

ON OFF

D8 OFF

ON OFF

Configuration

Theme

Version francaise : www.projetsdiy.fr

English version : www.diyprojects.io

var Timer_UdpateMesures; var tab_pane; google.charts.load(‘current’, {packages: [‘corechart’, ‘line’, ‘bar’, ‘gauge’]}); google.charts.setOnLoadCallback(drawChart); function drawChart(){ // https://developers.google.com/chart/interactive/docs/reference?csw=1#datatable-class var options1 = { title: ‘Température et humidité – DHT22’, legend: ‘bottom’, series: { // Gives each series an axis name that matches the Y-axis below. 0: {axis: ‘temperature’}, 1: {axis: ‘humidite’} }, axes: { // Adds labels to each axis; they don’t have to match the axis names. y: { temperature: {label: ‘Température (°C)’}, humidite: {label: ‘Humidité (%)’} } } } var options2 = { title: ‘Pression Atmosphérique – BMP180’, legend: {position: ‘none’}, } var optionsGauge = { redFrom: 960, redTo: 990, yellowFrom: 990, yellowTo: 1030, greenFrom: 1030, greenTo: 1080, minorTicks: 10, min: 960, max: 1080, animation: { duration: 400, easing: ‘out’, }, }; // Objets graphiques – Charts objects var chartTemp = new google.visualization.AreaChart(document.getElementById(‘chartTemp’)); var barTemp = new google.charts.Bar(document.getElementById(‘barTemp’)); var chartPA = new google.visualization.AreaChart(document.getElementById(‘chartPA’)); var gaugePA = new google.visualization.Gauge(document.getElementById(‘gaugePA’)); // Données – Data dataGaugePA = new google.visualization.DataTable(); dataChartTemp = new google.visualization.DataTable(); dataBarTemp = new google.visualization.DataTable(); dataChartPA = new google.visualization.DataTable(); // Gauge Pression Atmospherique – Gauge Atmosph. pressure dataGaugePA.addColumn(‘string’, ‘Label’); dataGaugePA.addColumn(‘number’, ‘Value’); dataGaugePA.addRows(1); // Line chart temp/humidity dataChartTemp.addColumn(‘timeofday’, ‘Temps’); dataChartTemp.addColumn(‘number’, ‘Température’); dataChartTemp.addColumn(‘number’, ‘Humidité’); // Bar temp/humidity dataBarTemp.addColumn(‘string’, ‘Moyennes’); dataBarTemp.addColumn(‘number’, ‘Température’); dataBarTemp.addColumn(‘number’, ‘Humidité’); // Line Chart PA dataChartPA.addColumn(‘timeofday’, ‘Temps’); dataChartPA.addColumn(‘number’, ‘Pression Atmosphérique’); // Force l’actualisation du graphique au 1er lancement – Force chart update first launch var firstStart = true; updateGraphs(); // Actualise à intervalle régulier les graphiques – auto-update charts setInterval(updateGraphs, 60000); //60000 MS == 1 minutes function updateGraphs(){ // Uniquement si le panneau des graphs est actif – only if chart panel is active if (tab_pane==’#tab_graphs’ | firstStart ){ firstStart = false; $.getJSON(‘/graph_temp.json’, function(json){ //console.log(« Mesures envoyees :  » + JSON.stringify(data) + « | » + data.t + « | » + data.h + « | » + data.pa) ; var _dataT = []; var _dataPA = []; var _dataBarTemp = []; var _dataBarPA = []; // Data line chart for ( var i = 0; i < json.timestamp.length; i++ ) { var d = new Date(json.timestamp[i] * 1000); _dataT.push( [ [d.getHours(), d.getMinutes(), d.getSeconds()], json.t[i], json.h[i] ] ) _dataPA.push( [ [d.getHours(), d.getMinutes(), d.getSeconds()], json.pa[i] ] ) } for ( var i = 0; i < json.bart.length; i++ ) { _dataBarTemp.push( [ i – 7 + « h »,, json.bart[i], json.barh[i] ] ) } dataGaugePA.setValue(0, 0, ‘mbar’); dataGaugePA.setValue(0, 1, json.pa[0]); dataChartTemp.addRows(_dataT); dataChartPA.addRows(_dataPA); dataBarTemp.addRows(_dataBarTemp); // Efface les anciennes valeurs – Erase old data var nbRec = dataChartTemp.getNumberOfRows() – json.timestamp.length; if ( dataChartTemp.getNumberOfRows() > json.timestamp.length ) { dataChartTemp.removeRows(0, nbRec ); dataChartPA.removeRows(0, nbRec ); } nbRec = dataBarTemp.getNumberOfRows() – json.bart.length; if ( dataBarTemp.getNumberOfRows() > json.bart.length ) { dataBarTemp.removeRows(0, nbRec ); } // Masque ou affiche l’histogramme – hide or sho bar graph if ( dataBarTemp.getNumberOfRows() == 0 ) { $(« #zeroDataTemp »).show(); $(« #barTemp »).hide(); } else { $(« #zeroDataTemp »).hide(); $(« #barTemp »).show(); } // Affiche les graphiques – display charts gaugePA.draw(dataGaugePA,optionsGauge); chartTemp.draw(dataChartTemp, options1); barTemp.draw(dataBarTemp, options1); chartPA.draw(dataChartPA, options2); }).fail(function(err){ console.log(« err getJSON graph_temp.json « +JSON.stringify(err)); }); } } } $(‘a[data-toggle=\ »tab\ »]’).on(‘shown.bs.tab’, function (e) { //On supprime tous les timers lorsqu’on change d’onglet clearTimeout(Timer_UdpateMesures); tab_pane = $(e.target).attr(« href ») console.log(‘activated ‘ + tab_pane ); // IE10, Firefox, Chrome, etc. if (history.pushState) window.history.pushState(null, null, tab_pane); else window.location.hash = tab_pane; if (tab_pane==’#tab_mesures’) { $(‘#table_mesures’).bootstrapTable(‘refresh’,{silent:true, url:’/tabmesures.json’}); } }); // Créé un timer qui actualise les données régulièrement – Create a timer than update data every n secondes $(‘#tab_mesures’).on(‘load-success.bs.table’,function (e,data){ console.log(« tab_mesures loaded »); if ($(‘.nav-tabs .active > a’).attr(‘href’)==’#tab_mesures’) { Timer_UdpateMesures=setTimeout(function(){ $(‘#table_mesures’).bootstrapTable(‘refresh’,{silent: true, showLoading: false, url: ‘/tabmesures.json’}); updateMesures(); },10000); } }); function updateMesures(){ $.getJSON(‘/mesures.json’, function(data){ //console.log(« Mesures envoyees :  » + JSON.stringify(data) + « | » + data.t + « | » + data.h + « | » + data.pa) ; $(‘#temperature’).html(data.t); $(‘#humidite’).html(data.h); $(‘#pa’).html(data.pa); }).fail(function(err){ console.log(« err getJSON mesures.json « +JSON.stringify(err)); }); }; function labelFormatter(value, row){ var label = «  »; if ( value === « Température » ) { label = value + «  »; $(« #labelTemp »).html( »  » + value +  »  » +  »  » + row.valeur + row.unite + «  »); } else if ( value === « Humidité » ) { label = value + «  »; $(« #labelHumi »).html( »  » + value +  »  » +  »  » + row.valeur + row.unite + «  »); } else if ( value === « Pression Atmosphérique » ) { label = value + «  »; $(« #labelPa »).html( »  » + value +  »  » +  »  » + row.valeur + row.unite + «  »); } else { label = value; } return label; } function valueFormatter(value, row){ //console.log(« valueFormatter »); var label = «  »; if ( row.valeur > row.precedente ) { label = value + row.unite + «  »; } else { label = value + row.unite + «  »; } return label; } function vpFormatter(value, row){ //console.log(« valueFormatter »); var label = «  »; if ( row.valeur > row.precedente ) { label = value + row.unite } else { label = value + row.unite } return label; } // Commandes sur le GPIO – GPIO change $(‘#D5_On’).click(function(){ setBouton(‘D5′,’1’); }); $(‘#D5_Off’).click(function(){ setBouton(‘D5′,’0’); }); $(‘#D6_On’).click(function(){ setBouton(‘D6′,’1’); }); $(‘#D6_Off’).click(function(){ setBouton(‘D6′,’0’); }); $(‘#D7_On’).click(function(){ setBouton(‘D7′,’1’); }); $(‘#D7_Off’).click(function(){ setBouton(‘D7′,’0’); }); $(‘#D8_On’).click(function(){ setBouton(‘D8′,’1’); }); $(‘#D8_Off’).click(function(){ setBouton(‘D8′,’0’); }); function setBouton(id, etat){ $.post(« gpio?id= » + id + « &etat= » + etat).done(function(data){ //console.log(« Retour setBouton  » + JSON.stringify(data)); var id_gpio = « # » + id + « _etat »; //console.log(data); if ( data.success === « 1 » | data.success === 1 ) { if ( data.etat === « 1 » ) { $(id_gpio).html(« ON »); } else { $(id_gpio).html(« OFF »); } } else { $(id_gpio).html(‘!’); } }).fail(function(err){ console.log(« err setButton  » + JSON.stringify(err)); }); } // Changement du theme – Change current theme // Adapté de – Adapted from : https://wdtz.org/bootswatch-theme-selector.html var supports_storage = supports_html5_storage(); if (supports_storage) { var theme = localStorage.theme; console.log(« Recharge le theme  » + theme); if (theme) { set_theme(get_themeUrl(theme)); } } // Nouveau theme sélectionne – New theme selected jQuery(function($){ $(‘body’).on(‘click’, ‘.change-style-menu-item’, function() { var theme_name = $(this).attr(‘rel’); console.log(« Change theme  » + theme_name); var theme_url = get_themeUrl(theme_name); console.log(« URL theme :  » + theme_url); set_theme(theme_url); }); }); // Recupere l’adresse du theme – Get theme URL function get_themeUrl(theme_name){ $(‘#labelTheme’).html(« Thème :  » + theme_name); var url_theme = «  »; if ( theme_name === « bootstrap » ) { url_theme = « https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css »; } else { url_theme = « https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/ » + theme_name + « /bootstrap.min.css »; } if (supports_storage) { // Enregistre le theme sélectionné en local – save into the local database the selected theme localStorage.theme = theme_name; } return url_theme; } // Applique le thème – Apply theme function set_theme(theme_url) { $(‘link[title= »main »]’).attr(‘href’, theme_url); } // Stockage local disponible ? – local storage available ? function supports_html5_storage(){ try { return ‘localStorage’ in window && window[‘localStorage’] !== null; } catch (e) { return false; } }

Accéder rapidement aux autres parties du projet

Voici les liens pour accéder aux autres parties du projet

Avez-vous aimé cet article ?