ESP8266 (Web Serveur – Partie 5) : tracer les données sous forme de jauge et graphique Google Charts

Maintenant que nous disposons d’un historique de mesures, nous allons ajouter des graphiques à l’interface Web de l’ESP8266 à l’aide de la librairie Google Charts. Il existe de très nombreuses librairies pour créer des graphiques et des jauges en Javascript / HTML5. Google Charts est une librairie très riche qui propose 28 modèles différents (ligne, jauge, histogramme horizontal ou vertical, gant, camembert, bulle, carte, radar…) suffisante pour débuter et créer une belle interface. Nous verrons dans un prochain tutoriel d’autres librairies avec un affichage plus moderne.

Le code source complet du projet est également disponible sur GitHub ici

Lire les autres articles du projet

Partie Sujets abordés Dépôt GitHub Article
Partie 1
  • Comment préparer le code HTML de interface Web
    • Pour cela, nous utiliserons le langage Pug (auparavant appelé Jade) qui permet de simplifier l’écriture
    • Nouvelles notions d’HTML : 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
Partie 2 Comment interagir avec le code Arduino. Lire l’article

  • 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. Lire l’article
Partie 4 Comment créer un historique de mesures. Lire l’article

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

Préparer les données pour l’histogramme en barre

Nous allons commencer par créer un petit histogramme qui nous permettra de voir l’évolution de la température moyenne (et l’humidité) durant les 7 dernières heures (la période d’enregistrement définie dans le tutoriel précédent). Sauf erreur de ma part, il n’y a pas de librairie pour faire de la classification de mesures sur Arduino (dites le moi dans les commentaires sinon). Je vous propose un petit algorithme rapide qui permet de calculer la température et l’humidité moyenne pour 7 classes (1 classe par heure).

Cet algorithme n’est probablement pas le meilleur pour ça mais l’objectif de ce tutoriel est la mise en place de graphiques Google Charts et l’envoi des données depuis le code Arduino vers l’interface Web.

Comment ça marche ?

  1. On initialise deux tableaux (statTemp et statHumi) ayant comme valeur initiale -999 (ou toute autre valeur arbitraire).
  2. On calcul la taille d’une classe sizeClass = sizeHist / nbClass
  3. On parcours le tableau des données (uniquement lorsqu’il est plein, il faudra optimiser ça). la variable k donne la position dans le tableau
  4. Si la cellule est vide (-999) alors on lui attribue la valeur courante. Sinon on calcul la valeur moyenne (valeur actuelle + valeur précédente / 2)
  5. On test si on doit changer de classe ( k + 1 ) > sizeClass * ( currentClass + 1 )
  6. On met à jour les deux tableaux dans le JSON (bart et barh).
void calcStat(){
  float statTemp[7] = {-999,-999,-999,-999,-999,-999,-999};
  float statHumi[7] = {-999,-999,-999,-999,-999,-999,-999};
  int nbClass = 7;  // Nombre de classes - Number of classes                         
  int currentClass = 0;
  int sizeClass = sizeHist / nbClass;  // 2
  double temp;

  if ( hist_t.size() >= sizeHist ) {
    for ( int k = 0 ; k < sizeHist ; k++ ) {
      temp = root["t"][k];
      if ( statTemp[currentClass] == -999 ) {
        statTemp[ currentClass ] = temp;
      } else {
        statTemp[ currentClass ] = ( statTemp[ currentClass ] + temp ) / 2;
      }
      temp = root["h"][k];
      if ( statHumi[currentClass] == -999 ) {
        statHumi[ currentClass ] = temp;
      } else {
        statHumi[ currentClass ] = ( statHumi[ currentClass ] + temp ) / 2;
      }
         
      if ( ( k + 1 ) > sizeClass * ( currentClass + 1 ) ) {
        Serial.print("k ");Serial.print(k + 1);Serial.print(" Cellule statTemp = ");Serial.println(statTemp[ currentClass ]);
        currentClass++;
      } else {
        Serial.print("k ");Serial.print(k + 1);Serial.print(" < ");Serial.println(sizeClass * currentClass);
      }
    }
    
    // Pour la mise au point - For debug
    Serial.println("Histogramme Temperature"); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statTemp[i]);Serial.print('|');
    }
    Serial.println("Histogramme Humidite "); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statHumi[i]);Serial.print('|');
    }
    Serial.print("");
    // Met à jour le JSON - update JSON object
    if ( bart.size() == 0 ) {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.add(statTemp[k]);
        barh.add(statHumi[k]);
      }  
    } else {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.set(k, statTemp[k]);
        barh.set(k, statHumi[k]);
      }  
    }
  }
}

Ajouter des graphiques Google Charts à une interface Web ESP8266

Passons maintenant aux choses sérieuses.

La librairie Google Charts

Google est décidément partout. La librairie Google Charts est une librairie qui existe depuis 2008. Ce n’est pas la librairie la plus belle qui existe mais elle est assez simple à mettre en oeuvre et surtout il est assez facile de créer des graphiques temps réel (ou du moins qu’on va mettre à jour très souvent). Nous testerons d’autres librairies ultérieurement. La documentation (uniquement en anglais) et assez bien faite. Seul bémol, actuellement certains graphiques sont disponibles dans l’ancienne version et dans la nouvelle (Material Design). C’est assez facile de le repérer, par exemple pour tracer un graphique en ligne (Line Chart), on peut faire

new google.visualization.LineChart

ou pour la nouvelle version (Material Design)

new google.charts.Line

Toutes les options ne sont pas encore disponibles dans la version Material Design, ce qui rend assez fastidieux la mise au point. Si l’aspect esthétique n’est pas primordial pour vous, je vous conseille de rester sur l’ancienne version.

esp8266 web server google charts graphiques

Code HTML

Pour charger la librairie, il suffit d’ajouter dans la section Head cette référence

script(src='https://www.gstatic.com/charts/loader.js')

Ajout d’un Panel Bootstrap

La classe panel de Bootstrap permet de présenter des données sous la forme d’une carte avec une entête (documentation Bootstrap). Nous allons nous en servir pour créer une entête contenant les mesures actuelles renvoyées par les capteurs (DHT22 et BMP180).

Le Panel est constitué d’une entête, classe panel-heading. Si on veut ajouter un titre, il faudra ajouter à la classe h1 (ou hx) la classe panel-title. Mais rien ne nous empêche de mettre d’autres choses dans le heading. Ici on va simplement ajouter une row qui sera découpée en 3 colonnes égales (col-md-4). Pour la rendre adaptable (responsive) sur les écrans de petite taille, on lui ajoute la classe col-xs-4 (par exemple). Dans chaque colonne, on place simplement un identifiant qui nous servira à afficher le contenu dès que des valeurs sont disponibles.

Ce qui donne le code Pug suivant

div.panel.panel-default
  div.panel-heading
    .row.panel-title 
      .col-xs-4.col-md-4
        #labelTemp
      .col-xs-4.col-md-4
        #labelHumi
      .col-xs-4.col-md-4
        #labelPa

Voici ce qu’on va obtenir

esp8266 web server bootstrap panel title header

Ajout des graphiques (ligne, histogramme vertical, jauge) sur 2 colonnes

Les graphiques vont prendre place dans une autre div de classe panel-body. On va simplement ajouter 2 lignes découpées en 2 colonnes. Soit 4 cellules. Dans chaque cellule, on viendra y placer un Google Chart. Dan la code HTML, il n’y a rien a définir à part un style. Ici, on prend toute la largeur disponible (width: 100%) et on fixe une hauteur, par exemple height:300px. Pour la jauge, j’ai simplement ajouté une marge à gauche (margin-left:25%) car par défaut elle est collée sur le bord gauche.

Enfin vous avez du remarquer un titre h2 que l’on rendra visible s’il n’y a pas d’histogramme à afficher et inversement.

div.panel.body
  .row
    .col-xs-6.col-md-6
      .div#chartTemp(style="width: 100%; height: 300px;")
    .col-xs-6.col-md-6
      .div#chartPA(style="width: 100%; height: 300px;")
  .row
    .col-xs-6.col-md-6
      h2#zeroDataTemp.label.label-info Pas encore de données
      .div#barTemp(style="width: 100%; height: 300px;")
      .col-xs-6.col-md-6
    .div#jaugePA(style="width: 100%; height: 300px; margin-left: 25%")

Code Javascript

En fonction du type de graphique désiré, on doit charger des packages complémentaires en plus du corechart.

google.charts.load('current', {packages: ['corechart', 'line', 'bar', 'gauge']});

Ensuite on indique la fonction qui sera lancée dès que la page aura été chargée et que les ressources auront été récupérées et chargées.

google.charts.setOnLoadCallback(drawChart);

Pour créer un graphique, on doit déjà créer un objet en lui indiquant son type. On récupère sa destination avec la fonction document.getElementById (par exemple).

var chartTemp = new google.visualization.AreaChart(document.getElementById('chartTemp'));

Ensuite, il existe plusieurs méthodes pour attribuer des options et des données, en voici une. On créé un tableau de données.

dataChartTemp = new google.visualization.DataTable();

Puis on ajoute les colonnes qui contiendrons les valeurs du graphique. On doit indiquer le type de donnée pour chaque colonne. Il faut se reporter à la documentation pour connaitre les formats disponibles (data format) pour le graphique souhaité, ici par exemple pour un AreaChart. Ici le temps est de la forme timeofday (heure tout simplement !).

dataChartTemp.addColumn('timeofday', 'Temps');
dataChartTemp.addColumn('number', 'Température');
dataChartTemp.addColumn('number', 'Humidité');

On peut aussi définir des options, par exemple la position de la légende (position:”bottom” pour la placer en dessous, “none” pour désactiver son affichage), le libellé des axes, etc… Un exemple pour l’AreaChart

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é (%)'}
      }
    }
  }

Maintenant, il ne reste plus qu’à créer une fonction chargée de récupérer des données depuis l’ESP8266 et les afficher à l’écran. Pour cela, on va simplement utiliser la fonction jquery $.getJSON déjà utilisée dans le tutoriel précédent. Pour éviter de gaspiller les ressources de l’ordinateur, smartphone ou tablette et ESP8266, on vérifie que le panneau des graphiques est bien actif avant de faire une actualisation

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){
      // Actualisation des graphiques - update charts
    }).fail(function(err){
      // Avertir d'une erreur - display error message
  })
}

Traitement du JSON envoyé par le Web Server ESP8266

On va récupérer les données sous la forme d’un JSON. Par exemple

{
  "timestamp": [1485273937, 1485273938, 1485273939, 1485273940, 1485273941, 1485273942, 1485273943, 1485273944, 1485273945, 1485273946, 1485273947, 1485273948, 1485273949, 1485273950],
  "t": [23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3],
  "h": [35.6, 35.6, 35.6, 35.6, 35.6, 35.5, 35.5, 35.4, 35.4, 35.5, 35.5, 35.5, 35.5, 35.5],
  "pa": [987.7, 987.7, 987.7, 987.8, 987.7, 987.7, 987.7, 987.7, 987.7, 987.8, 987.7, 987.7, 987.7, 987.7],
  "bart": [23.30, 23.30, 23.30, 23.30, 23.30, 23.30, 23.30],
  "barh": [35.60, 35.60, 35.50, 35.40, 35.50, 35.50, 35.50]
}

On va devoir faire une petite boucle qui parcours le tableau timestamp pour ajouter à chaque fois une nouvelle ligne au tableau des données du graphique. Par exemple ici on prépare les données pour le graphique température / humidité du DHT22. La première colonne doit contenir une heure. Pour cela on va utiliser la fonction javascript new Date qui est capable de convertir un timestamp unix en une date. Il faut multiplier par 1000 car la fonction attend des milli-secondes, le timestamp est en secondes dans ce cas. Enfin, on indique la date sous la forme d’un tableau [HH,MM,SS]. Désolé, mais faut faire avec l’API Google ! Le traitement du temps est toujours un problème finalement !

var _dataT;
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]
  ])
}

On ajoute ce bloc de valeurs d’un coup avec la fonction addRows. Attention, la fonction addRow existe aussi (pour une seule ligne).

dataChartTemp.addRows(_dataT);

Ici, j’ai choisi de ne pas conserver les anciennes valeurs, on va donc les supprimer avec la fonction removeRows qui prend comme paramètre l’index et le nombre de lignes à supprimer.

var nbRec = dataChartTemp.getNumberOfRows() - json.timestamp.length;
if ( dataChartTemp.getNumberOfRows() > json.timestamp.length ) {
  dataChartTemp.removeRows(0, nbRec );
  dataChartPA.removeRows(0, nbRec );
}

Affichage et actualisation automatique des graphiques

Tout est prêt, on peut demander au navigateur d’afficher le graphique chartTemp.

chartTemp.draw(dataChartTemp, options1);

On peut masquer un graphique s’il est vide et afficher un message d’information à la place. Il suffit de tester le nombre de ligne dans le tableau de données avec la méthode getNumberOfRows(). Ensuite un show() ou hide() sur l’élément concerné pour le rendre visible ou le masquer. Le tour est joué.

if ( dataBarTemp.getNumberOfRows() == 0 ) {
  $("#zeroDataTemp").show();
  $("#barTemp").hide();
} else {
  $("#zeroDataTemp").hide();
  $("#barTemp").show();
}

Il ne reste plus qu’à laisser au navigateur le soin d’actualiser régulièrement les graphiques en créant un timer à l’aide de la fonction setInterval(). On place le timer juste avant la fonction updateGraph().

setInterval(updateGraphs, 60000); //60000 MS == 1 minute

Code Arduino

On va ajouter un appel à la fonction sendHistory() lorsque le serveur Web intercepte un appel sur la page /graph_temp.json.

server.on("/graph_temp.json", sendHistory);

Toutes les mesures sont stockées dans un objet JSON à l’aide de la librairie ArduinoJSON (tutoriel précédent). Il est très simple d’en générer une chaine de caractères à l’aide de la fonction printTo. qui stocke le résultat dans un buffer. Il faudra donc disposer d’assez de mémoire sinon l’export sera impossible. On envoi au client le JSON sérialisé de manière classique avec server.send().

void sendHistory(){  
  root.printTo(json, sizeof(json));             // Export du JSON dans une chaine - Export JSON object as a string
  server.send(200, "application/json", json);   // Envoi l'historique au client Web - Send history data to the web client
  Serial.println("Historique envoye");   
}

Attention à rester raisonnable et ne pas demander une actualisation des graphiques chaque seconde. Avec peu de données, l’ESP8266 va encaisser, mais pour combien de temps… Pour connaitre la fréquence minimale à ne pas dépasser, regardez le temps pour obtenir une réponse avec les outils de développement d’un navigateur.

Code final du projet

Le code source complet du projet est également disponible sur GitHub ici

Template Pug

Le Template Pug (ancien Jade) plus facile à comprendre, modifier, corriger que du code HTML.

html(charset='UTF-8')
    head
      meta( name='viewport')
      script(src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js')
      script(src='https://www.gstatic.com/charts/loader.js')
      script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')
      script(src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js")
      link(rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css")
      link(href='https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/superhero/bootstrap.min.css', rel='stylesheet' title="main")
      title Demo ESP8266 SPIFFS + Boostrap - www.projetsdiy.fr
    body
      .container-fluid
        h1 ESP8266 Web Server + SPIFFS + Bootstrap + Google Charts 
        ul#tab.nav.nav-tabs
          li.active
            a(href="#tab_mesures" data-toggle="tab") Mesures
          li
            a(href="#tab_graphs" data-toggle="tab") Graphiques
          li
            a(href="#tab_gpio" data-toggle="tab") GPIO  
          li
            a(href="#tab_configuration" data-toggle="tab") Configuration
        div.tab-content
          div#tab_mesures.tab-pane.fade.in.active         
            h2 Mini station m&eacute;t&eacute;o (DHT22 + BMP180)
            ul.nav.nav-pills
              li.active
                a(href='#')
                  #temperature.span.badge.pull-right -
                  |  Temp&eacute;rature
              li
                a(href='#')
                  #humidite.span.badge.pull-right -
                  |  Humidit&eacute;
              li
                a(href='#')
                  #pa.span.badge.pull-right -
                  |  Pression atmosph&eacute;rique
            br
            table(id='table_mesures' data-toggle='table' data-show-colunns='true')
              thead
                tr
                  th(data-field='mesure' data-align='left' data-sortable='true' data-formatter='labelFormatter') Mesure
                  th(data-field='valeur' data-align='left' data-sortable='true' data-formatter='valueFormatter') Valeur
                  th(data-field='precedente' data-align='left' data-sortable='true' data-formatter='vpFormatter') Valeur Pr&eacute;c&eacute;dente

          div#tab_graphs.tab-pane.fade
            div.panel.panel-default
              div.panel-heading
                .row.panel-title 
                  .col-xs-4.col-md-4
                    #labelTemp
                  .col-xs-4.col-md-4
                    #labelHumi
                  .col-xs-4.col-md-4
                    #labelPa
                  
              div.panel.body
                .row
                  .col-xs-6.col-md-6
                    .div#chartTemp(style="width: 100%; height: 300px;")
                  .col-xs-6.col-md-6
                    .div#chartPA(style="width: 100%; height: 300px;")
                .row
                  .col-xs-6.col-md-6
                    h2#zeroDataTemp.label.label-info Pas encore de données
                    .div#barTemp(style="width: 100%; height: 300px;")
                  .col-xs-6.col-md-6
                    .div#gaugePA(style="width: 100%; height: 300px; margin-left: 25%")     
          div#tab_gpio.tab-pane.fade
            h2 GPIO
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D5
                  #D5_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D5_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D5_Off.button.btn.btn-danger.btn-lg(type='button') OFF
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D6
                  #D6_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D6_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D6_Off.button.btn.btn-danger.btn-lg(type='button') OFF
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D7
                  #D7_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D7_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D7_Off.button.btn.btn-danger.btn-lg(type='button') OFF
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D8
                  #D8_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D8_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D8_Off.button.btn.btn-danger.btn-lg(type='button') OFF
          div#tab_configuration.tab-pane.fade
            h2 Configuration        

            .btn-group
              button#labelTheme.btn.btn-default Theme
              button.btn.btn-default.dropdown-toggle(data-toggle='dropdown')
                span.caret
              ul.dropdown-menu
                li
                    a.change-style-menu-item(href='#' rel='bootstrap') Boostrap
                li
                    a.change-style-menu-item(href='#' rel='cerulean') Cerulean
                li
                    a.change-style-menu-item(href='#' rel='cosmo') Cosmo
                li
                    a.change-style-menu-item(href='#' rel='cyborg') Cyborg
                li
                    a.change-style-menu-item(href='#' rel='darkly') Darkly
                li
                    a.change-style-menu-item(href='#' rel='flatly') Flatly
                li
                    a.change-style-menu-item(href='#' rel='journal') Journal
                li
                    a.change-style-menu-item(href='#' rel='lumen') Lumen
                li
                    a.change-style-menu-item(href='#' rel='paper') Paper
                li
                    a.change-style-menu-item(href='#' rel='readable') Readable
                li
                    a.change-style-menu-item(href='#' rel='sandstone') Sandstone
                li
                    a.change-style-menu-item(href='#' rel='simplex') Simplex
                li
                    a.change-style-menu-item(href='#' rel='slate') Slate
                li
                    a.change-style-menu-item(href='#' rel='spacelab') Spacelab
                li
                    a.change-style-menu-item(href='#' rel='superhero') Superhero
                li
                    a.change-style-menu-item(href='#' rel='united') United
                li
                    a.change-style-menu-item(href='#' rel='yeti') Yeti  
        .row(style="position:absolute; bottom:0; width:100%")
          .col-xs-2.col-md-2
            img(src="img/logo.png" width="30" height="30")
          .col-xs-5.col-md-5
            p
              a(href='https://www.projetsdiy.fr') Version francaise : www.projetsdiy.fr
          .col-xs-5.col-md-5
            p
              a(href='http://www.diyprojects.io') English version : www.diyprojects.io
    
      //script(src='js/script.js')
    
      script().
        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();
          
          // Jauge 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 + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
            $("#labelTemp").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
          } else if ( value === "Humidité" ) {
            label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
            $("#labelHumi").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
          } else if ( value === "Pression Atmosphérique" ) {
            label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
            $("#labelPa").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
          } else {
            label = value;
          } 
          return label;
        }
        function valueFormatter(value, row){
          //console.log("valueFormatter");
          var label = "";
          if ( row.valeur > row.precedente ) {
            label = value + row.unite + "<span class='glyphicon glyphicon-chevron-up pull-right'></span>";
          } else { 
            label = value + row.unite + "<span class='glyphicon glyphicon-chevron-down pull-right'></span>";
          }
          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&egrave;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;
          }
        }

Code HTML

Le code HTML généré à partir du template Pug ci-dessus.

<!DOCTYPE html>
<html charset="UTF-8">
  <head>
    <meta name="viewport">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css">
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/superhero/bootstrap.min.css" rel="stylesheet" title="main">
    <title>Demo ESP8266 SPIFFS + Boostrap - www.projetsdiy.fr</title>
  </head>
  <body>
    <div class="container-fluid">
      <h1>ESP8266 Web Server + SPIFFS + Bootstrap + Google Charts </h1>
      <ul class="nav nav-tabs" id="tab">
        <li class="active"><a href="#tab_mesures" data-toggle="tab">Mesures</a></li>
        <li><a href="#tab_graphs" data-toggle="tab">Graphiques</a></li>
        <li><a href="#tab_gpio" data-toggle="tab">GPIO  </a></li>
        <li><a href="#tab_configuration" data-toggle="tab">Configuration</a></li>
      </ul>
      <div class="tab-content">
        <div class="tab-pane fade in active" id="tab_mesures">        
          <h2>Mini station m&eacute;t&eacute;o (DHT22 + BMP180)</h2>
          <ul class="nav nav-pills">
            <li class="active"><a href="#">
                <div class="span badge pull-right" id="temperature">-</div> Temp&eacute;rature</a></li>
            <li><a href="#">
                <div class="span badge pull-right" id="humidite">-</div> Humidit&eacute;</a></li>
            <li><a href="#">
                <div class="span badge pull-right" id="pa">-</div> Pression atmosph&eacute;rique</a></li>
          </ul><br>
          <table id="table_mesures" data-toggle="table" data-show-colunns="true">
            <thead>
              <tr>
                <th data-field="mesure" data-align="left" data-sortable="true" data-formatter="labelFormatter">Mesure</th>
                <th data-field="valeur" data-align="left" data-sortable="true" data-formatter="valueFormatter">Valeur</th>
                <th data-field="precedente" data-align="left" data-sortable="true" data-formatter="vpFormatter">Valeur Pr&eacute;c&eacute;dente</th>
              </tr>
            </thead>
          </table>
        </div>
        <div class="tab-pane fade" id="tab_graphs">
          <div class="panel panel-default">
            <div class="panel-heading">
              <div class="row panel-title"> 
                <div class="col-xs-4 col-md-4">
                  <div id="labelTemp"></div>
                </div>
                <div class="col-xs-4 col-md-4">
                  <div id="labelHumi"></div>
                </div>
                <div class="col-xs-4 col-md-4">
                  <div id="labelPa"></div>
                </div>
              </div>
            </div>
            <div class="panel body">
              <div class="row">
                <div class="col-xs-6 col-md-6">
                  <div class="div" id="chartTemp" style="width: 100%; height: 300px;"></div>
                </div>
                <div class="col-xs-6 col-md-6">
                  <div class="div" id="chartPA" style="width: 100%; height: 300px;"></div>
                </div>
              </div>
              <div class="row">
                <div class="col-xs-6 col-md-6">
                  <h2 class="label label-info" id="zeroDataTemp">Pas encore de données</h2>
                  <div class="div" id="barTemp" style="width: 100%; height: 300px;"></div>
                </div>
                <div class="col-xs-6 col-md-6">
                  <div class="div" id="gaugePA" style="width: 100%; height: 300px; margin-left: 25%;">    </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="tab-pane fade" id="tab_gpio">
          <h2>GPIO</h2>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D5
                <div class="span badge" id="D5_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D5_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D5_Off" type="button">OFF</div>
            </div>
          </div>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D6
                <div class="span badge" id="D6_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D6_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D6_Off" type="button">OFF</div>
            </div>
          </div>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D7
                <div class="span badge" id="D7_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D7_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D7_Off" type="button">OFF</div>
            </div>
          </div>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D8
                <div class="span badge" id="D8_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D8_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D8_Off" type="button">OFF</div>
            </div>
          </div>
        </div>
        <div class="tab-pane fade" id="tab_configuration">
          <h2>Configuration        </h2>
          <div class="btn-group">
            <button class="btn btn-default" id="labelTheme">Theme</button>
            <button class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
            <ul class="dropdown-menu">
              <li><a class="change-style-menu-item" href="#" rel="bootstrap">Boostrap</a></li>
              <li><a class="change-style-menu-item" href="#" rel="cerulean">Cerulean</a></li>
              <li><a class="change-style-menu-item" href="#" rel="cosmo">Cosmo</a></li>
              <li><a class="change-style-menu-item" href="#" rel="cyborg">Cyborg</a></li>
              <li><a class="change-style-menu-item" href="#" rel="darkly">Darkly</a></li>
              <li><a class="change-style-menu-item" href="#" rel="flatly">Flatly</a></li>
              <li><a class="change-style-menu-item" href="#" rel="journal">Journal</a></li>
              <li><a class="change-style-menu-item" href="#" rel="lumen">Lumen</a></li>
              <li><a class="change-style-menu-item" href="#" rel="paper">Paper</a></li>
              <li><a class="change-style-menu-item" href="#" rel="readable">Readable</a></li>
              <li><a class="change-style-menu-item" href="#" rel="sandstone">Sandstone</a></li>
              <li><a class="change-style-menu-item" href="#" rel="simplex">Simplex</a></li>
              <li><a class="change-style-menu-item" href="#" rel="slate">Slate</a></li>
              <li><a class="change-style-menu-item" href="#" rel="spacelab">Spacelab</a></li>
              <li><a class="change-style-menu-item" href="#" rel="superhero">Superhero</a></li>
              <li><a class="change-style-menu-item" href="#" rel="united">United</a></li>
              <li><a class="change-style-menu-item" href="#" rel="yeti">Yeti  </a></li>
            </ul>
          </div>
        </div>
      </div>
      <div class="row" style="position:absolute; bottom:0; width:100%;">
        <div class="col-xs-2 col-md-2"><img src="img/logo.png" width="30" height="30"></div>
        <div class="col-xs-5 col-md-5">
          <p><a href="https://www.projetsdiy.fr">Version francaise : www.projetsdiy.fr</a></p>
        </div>
        <div class="col-xs-5 col-md-5">
          <p><a href="http://www.diyprojects.io">English version : www.diyprojects.io</a></p>
        </div>
      </div>
    </div>
    <!--script(src='js/script.js')-->
    <script>
      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();
        
        // Jauge 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 + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          $("#labelTemp").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
        } else if ( value === "Humidité" ) {
          label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          $("#labelHumi").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
        } else if ( value === "Pression Atmosphérique" ) {
          label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          $("#labelPa").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
        } else {
          label = value;
        } 
        return label;
      }
      function valueFormatter(value, row){
        //console.log("valueFormatter");
        var label = "";
        if ( row.valeur > row.precedente ) {
          label = value + row.unite + "<span class='glyphicon glyphicon-chevron-up pull-right'></span>";
        } else { 
          label = value + row.unite + "<span class='glyphicon glyphicon-chevron-down pull-right'></span>";
        }
        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&egrave;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;
        }
      }
    </script>
  </body>
</html>

Code Arduino

Et le code Arduino à téléverser dans l’ESP8266. Le code est prévu pour une Wemos D1 Mini équipée d’un Shield DHT22 ainsi qu’un BMP180 sur bus I2C.

/*
 * ESP8266 + DHT22 + BMP180 + BOOTSTRAP + SPIFFS + GOOGLE CHARTS
 * Copyright (C) 2017 https://www.projetsdiy.fr - http://www.diyprojects.io
 * 
 * Part 5 : 
 *  - Create History based ArduinoJSON
 *  - Prepare the histogram graph data for the average temperature and humidity measurements
 *  - Add Google Charts to the web page
 *  - Full tutorial here : http://www.diyprojects.io/esp8266-web-server-part-5-add-google-charts-gauges-and-charts/
 *  
 *  Partie 5 :
 *  - Enregistre un historique de mesure à l'aide d'ArduinoJSON
 *  - Prepare les données du graphique en histogramme des mesures moyennes de température et d'humidité
 *  - Ajout des graphiques Google Charts à la page web
 *  - Tutoriel complet :  https://www.projetsdiy.fr/esp8266-web-serveur-partie5-gauges-graphiques-google-charts/
 *  
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <DHT.h>
#include <Adafruit_BMP085.h>
#include <FS.h>
#include <TimeLib.h>
#include <NtpClientLib.h>
#include <ArduinoJson.h>

#define ssid      "yourSSID"        // WiFi SSID
#define password  "yourPASSWORD"    // WiFi password
#define DHTTYPE   DHT22             // DHT type (DHT11, DHT22)
#define DHTPIN    D4                // Broche du DHT / DHT Pin
#define HISTORY_FILE "/history.json"
const uint8_t GPIOPIN[4] = {D5,D6,D7,D8};  // Led
float   t = 0 ;
float   h = 0 ;
float   pa = 0;
int     sizeHist = 84 ;        // Taille historique (7h x 12pts) - History size 

const long intervalHist = 1000 * 60 * 5;  // 5 mesures / heure - 5 measures / hours
unsigned long previousMillis = intervalHist;  // Dernier point enregistré dans l'historique - time of last point added

// Création des objets / create Objects
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP085 bmp;
ESP8266WebServer server ( 80 );

StaticJsonBuffer<10000> jsonBuffer;                 // Buffer static contenant le JSON courant - Current JSON static buffer
JsonObject& root = jsonBuffer.createObject();
JsonArray& timestamp = root.createNestedArray("timestamp");
JsonArray& hist_t = root.createNestedArray("t");
JsonArray& hist_h = root.createNestedArray("h");
JsonArray& hist_pa = root.createNestedArray("pa");
JsonArray& bart = root.createNestedArray("bart");   // Clé historgramme (temp/humidité) - Key histogramm (temp/humidity)
JsonArray& barh = root.createNestedArray("barh");   // Clé historgramme (temp/humidité) - Key histogramm (temp/humidity)

char json[10000];                                   // Buffer pour export du JSON - JSON export buffer

void updateGpio(){
  String gpio = server.arg("id");
  String etat = server.arg("etat");
  String success = "1";
  int pin = D5;
 if ( gpio == "D5" ) {
      pin = D5;
 } else if ( gpio == "D7" ) {
     pin = D7;
 } else if ( gpio == "D8" ) {
     pin = D8;  
 } else {   
      pin = D5;
  }
  Serial.println(pin);
  if ( etat == "1" ) {
    digitalWrite(pin, HIGH);
  } else if ( etat == "0" ) {
    digitalWrite(pin, LOW);
  } else {
    success = "1";
    Serial.println("Err Led Value");
  }
  
  String json = "{\"gpio\":\"" + String(gpio) + "\",";
  json += "\"etat\":\"" + String(etat) + "\",";
  json += "\"success\":\"" + String(success) + "\"}";
    
  server.send(200, "application/json", json);
  Serial.println("GPIO updated");
}

void sendMesures() {
  String json = "{\"t\":\"" + String(t) + "\",";
  json += "\"h\":\"" + String(h) + "\",";
  json += "\"pa\":\"" + String(pa) + "\"}";

  server.send(200, "application/json", json);
  Serial.println("Send measures");
}

void calcStat(){
  float statTemp[7] = {-999,-999,-999,-999,-999,-999,-999};
  float statHumi[7] = {-999,-999,-999,-999,-999,-999,-999};
  int nbClass = 7;  // Nombre de classes - Number of classes                         
  int currentClass = 0;
  int sizeClass = hist_t.size() / nbClass;  // 2
  double temp;
  //
  if ( hist_t.size() >= sizeHist ) {
    //Serial.print("taille classe ");Serial.println(sizeClass);
    //Serial.print("taille historique ");Serial.println(hist_t.size());
    for ( int k = 0 ; k < hist_t.size() ; k++ ) {
      temp = root["t"][k];
      if ( statTemp[currentClass] == -999 ) {
        statTemp[ currentClass ] = temp;
      } else {
        statTemp[ currentClass ] = ( statTemp[ currentClass ] + temp ) / 2;
      }
      temp = root["h"][k];
      if ( statHumi[currentClass] == -999 ) {
        statHumi[ currentClass ] = temp;
      } else {
        statHumi[ currentClass ] = ( statHumi[ currentClass ] + temp ) / 2;
      }
         
      if ( ( k + 1 ) > sizeClass * ( currentClass + 1 ) ) {
        //Serial.print("k ");Serial.print(k + 1);Serial.print(" Cellule statTemp = ");Serial.println(statTemp[ currentClass ]);
        currentClass++;
      } else {
        //Serial.print("k ");Serial.print(k + 1);Serial.print(" < ");Serial.println(sizeClass * currentClass);
      }
    }
    
    Serial.println("Histogram - Temperature"); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statTemp[i]);Serial.print('|');
    }
    Serial.println("Histogram - Humidity "); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statHumi[i]);Serial.print('|');
    }
    Serial.print("");
    if ( bart.size() == 0 ) {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.add(statTemp[k]);
        barh.add(statHumi[k]);
      }  
    } else {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.set(k, statTemp[k]);
        barh.set(k, statHumi[k]);
      }  
    }
  }
}

void sendTabMesures() {
  double temp = root["t"][0];      // Récupère la plus ancienne mesure (temperature) - get oldest record (temperature)
  String json = "[";
  json += "{\"mesure\":\"Température\",\"valeur\":\"" + String(t) + "\",\"unite\":\"°C\",\"glyph\":\"glyphicon-indent-left\",\"precedente\":\"" + String(temp) + "\"},";
  temp = root["h"][0];             // Récupère la plus ancienne mesure (humidite) - get oldest record (humidity)
  json += "{\"mesure\":\"Humidité\",\"valeur\":\"" + String(h) + "\",\"unite\":\"%\",\"glyph\":\"glyphicon-tint\",\"precedente\":\"" + String(temp) + "\"},";
  temp = root["pa"][0];             // Récupère la plus ancienne mesure (pression atmospherique) - get oldest record (Atmospheric Pressure)
  json += "{\"mesure\":\"Pression Atmosphérique\",\"valeur\":\"" + String(pa) + "\",\"unite\":\"mbar\",\"glyph\":\"glyphicon-dashboard\",\"precedente\":\"" + String(temp) + "\"}";
  json += "]";
  server.send(200, "application/json", json);
  Serial.println("Send data tab");
}

void sendHistory(){  
  root.printTo(json, sizeof(json));             // Export du JSON dans une chaine - Export JSON object as a string
  server.send(200, "application/json", json);   // Envoi l'historique au client Web - Send history data to the web client
  Serial.println("Send History");   
}

void loadHistory(){
  File file = SPIFFS.open(HISTORY_FILE, "r");
  if (!file){
    Serial.println("Aucun historique existe - No History Exist");
  } else {
    size_t size = file.size();
    if ( size == 0 ) {
      Serial.println("Fichier historique vide - History file empty !");
    } else {
      std::unique_ptr<char[]> buf (new char[size]);
      file.readBytes(buf.get(), size);
      JsonObject& root = jsonBuffer.parseObject(buf.get());
      if (!root.success()) {
        Serial.println("Impossible de lire le JSON - Impossible to read JSON file");
      } else {
        Serial.println("Historique charge - History loaded");
        root.prettyPrintTo(Serial);  
      }
    }
    file.close();
  }
}

void saveHistory(){
  Serial.println("Save History");            
  File historyFile = SPIFFS.open(HISTORY_FILE, "w");
  root.printTo(historyFile); // Exporte et enregistre le JSON dans la zone SPIFFS - Export and save JSON object to SPIFFS area
  historyFile.close();  
}

void setup() {
  NTP.onNTPSyncEvent([](NTPSyncEvent_t error) {
    if (error) {
      Serial.print("Time Sync error: ");
      if (error == noResponse)
        Serial.println("NTP server not reachable");
      else if (error == invalidAddress)
        Serial.println("Invalid NTP server address");
      }
    else {
      Serial.print("Got NTP time: ");
      Serial.println(NTP.getTimeDateString(NTP.getLastNTPSync()));
    }
  });
  // Serveur NTP, decalage horaire, heure été - NTP Server, time offset, daylight 
  NTP.begin("pool.ntp.org", 0, true); 
  NTP.setInterval(60000);
  delay(500);
     
  for ( int x = 0 ; x < 5 ; x++ ) {
    pinMode(GPIOPIN[x], OUTPUT);
  }
  
  Serial.begin ( 115200 );
  // Initialisation du BMP180 / Init BMP180
  if ( !bmp.begin() ) {
    Serial.println("BMP180 KO!");
    while (1);
  } else {
    Serial.println("BMP180 OK");
  }

  WiFi.begin ( ssid, password );
  int tentativeWiFi = 0;
  // Attente de la connexion au réseau WiFi / Wait for connection
  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 ); Serial.print ( "." );
    tentativeWiFi++;
    if ( tentativeWiFi > 20 ) {
      ESP.reset();
      while(true)
        delay(1);
    }
  }
  // Connexion WiFi établie / WiFi connexion is OK
  Serial.println ( "" );
  Serial.print ( "Connected to " ); Serial.println ( ssid );
  Serial.print ( "IP address: " ); Serial.println ( WiFi.localIP() );
  
  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS Mount failed");        // Problème avec le stockage SPIFFS - Serious problem with SPIFFS 
  } else { 
    Serial.println("SPIFFS Mount succesfull");
    loadHistory();
  }
  delay(50);
  
  server.on("/tabmesures.json", sendTabMesures);
  server.on("/mesures.json", sendMesures);
  server.on("/gpio", updateGpio);
  server.on("/graph_temp.json", sendHistory);

  server.serveStatic("/js", SPIFFS, "/js");
  server.serveStatic("/css", SPIFFS, "/css");
  server.serveStatic("/img", SPIFFS, "/img");
  server.serveStatic("/", SPIFFS, "/index.html");

  server.begin();
  Serial.println ( "HTTP server started" );

  Serial.print("Uptime :");
  Serial.println(NTP.getUptime());
  Serial.print("LastBootTime :");
  Serial.println(NTP.getLastBootTime());
}

void loop() {
  // put your main code here, to run repeatedly:
  server.handleClient();
  t = dht.readTemperature();
  h = dht.readHumidity();
  pa = bmp.readPressure() / 100.0F;
  if ( isnan(t) || isnan(h) ) {
    //Erreur, aucune valeur valide - Error, no valid value
  } else {
    addPtToHist();
  }
  //delay(5);
}

void addPtToHist(){
  unsigned long currentMillis = millis();
  
  //Serial.println(currentMillis - previousMillis);
  if ( currentMillis - previousMillis > intervalHist ) {
    long int tps = NTP.getTime();
    previousMillis = currentMillis;
    //Serial.println(NTP.getTime());
    if ( tps > 0 ) {
      timestamp.add(tps);
      hist_t.add(double_with_n_digits(t, 1));
      hist_h.add(double_with_n_digits(h, 1));
      hist_pa.add(double_with_n_digits(pa, 1));

      //root.printTo(Serial);
      if ( hist_t.size() > sizeHist ) {
        //Serial.println("efface anciennes mesures");
        timestamp.removeAt(0);
        hist_t.removeAt(0);
        hist_h.removeAt(0);
        hist_pa.removeAt(0);
      }
      //Serial.print("size hist_t ");Serial.println(hist_t.size());
      calcStat();
      delay(100);
      saveHistory();
      //root.printTo(Serial);  
    }  
  }
}

Interface obtenue

Connectez-vous sur l’ESP8266 depuis un navigateur. Attendez quelques secondes pour obtenir les premières mesures. Le deux graphiques en haut permettent de suivre l’évolution de la température, de l’humidité et de la pression atmosphérique sur la période d’enregistrement (ici 40 minutes). La pression atmosphérique est également reportée sur une jauge. Les couleurs sont réelles (rouge pour risque de tempête et pluie), orange (temps variable), vert (beau temp). Il n’est pas possible de définir d’autres zones, ce qui est dommage. Enfin, l’histogramme permet de suivre l’évolution moyenne de la température et de l’humidité. Ici, chaque barre représente 6 minutes environ.

esp8266 web server spiffs bootstrap dht22 bmp180 google charts

Voilà, notre série de tutoriel sur la programmation Web Server des ESP8266 touche bientôt à sa fin. Dans le prochain tutoriel, nous verrons comment mettre à jour “dans les airs” (OTA) le programme Arduino.

Print Friendly, PDF & Email

Inscrivez-vous à la newsletter hebdomadaire

Aucun spam et aucun autre usage ne sera fait de votre email. Vous pouvez vous désinscrire à tout moment.

Comparateur de prix

Bons plans

Les offres suivantes se terminent bientôt. Utilisez le coupon indiqué pour profiter du prix promo

Domotique et objets connectés à faire soi-même