Maintenant que nous disposons d’un historique de mesures, nous allons pouvoir tracer des graphiques et des jauges sur l’interface HTML de notre mini station météo ESP8266 à l’aide de la librairie Google Charts. Il existe de très nombreuses librairies pour créer des graphiques et des gauges en Javascript / HTML5, celle développée par Google est très simple d’utilisation.
Projet actualisé le 29 juillet 2020
Google Charts est une librairie très riche qui propose 28 modèles différents (ligne, gauge, 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.
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).
On utilisera un petit algorithme rapide qui permet de calculer la température et l’humidité moyenne pour 7 classes. Chaque classe correspondra à une heure écoulée.
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 ?
- On initialise deux tableaux statTemp et statHumi ayant comme valeur initiale -999 (ou toute autre valeur arbitraire).
- On calcul la taille de la classe sizeClass = sizeHist / nbClass
- 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
- 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)
- On test si on doit changer de classe ( k + 1 ) > sizeClass * ( currentClass + 1 )
- 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]);
}
}
}
}
Comment ajouter des graphiques Google Charts à une interface Web
Passons maintenant aux choses sérieuses.
Comment fonctionne 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.
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 autre) 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
Ajout des graphiques (ligne, histogramme vertical, gauge) 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 gauge, 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#gaugePA(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 de l’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.
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 la méthode 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 complet et final du projet de station météo
Voilà, il ne reste plus qu’à modifier le code précédent
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="http://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js") link(rel="stylesheet" href="http://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étéo (DHT22 + BMP180) ul.nav.nav-pills li.active a(href='#') #temperature.span.badge.pull-right - | Température li a(href='#') #humidite.span.badge.pull-right - | Humidité li a(href='#') #pa.span.badge.pull-right - | Pression atmosphé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écé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='http://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(); // 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 + "<span class='glyphicon " + row.glyph + " pull-left'></span>"; $("#labelTemp").html(" " + value + " " + "<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(" " + value + " " + "<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(" " + value + " " + "<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è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 généré automatiquement
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="http://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js"></script> <link rel="stylesheet" href="http://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étéo (DHT22 + BMP180)</h2> <ul class="nav nav-pills"> <li class="active"><a href="#"> <div class="span badge pull-right" id="temperature">-</div> Température</a></li> <li><a href="#"> <div class="span badge pull-right" id="humidite">-</div> Humidité</a></li> <li><a href="#"> <div class="span badge pull-right" id="pa">-</div> Pression atmosphé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écé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="http://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(); // 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 + "<span class='glyphicon " + row.glyph + " pull-left'></span>"; $("#labelTemp").html(" " + value + " " + "<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(" " + value + " " + "<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(" " + value + " " + "<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è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 compatible ESP8266
Et le code Arduino à téléverser dans l’ESP8266. Le code est prévu pour une LoLin (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 http://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 : http://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 HTML obtenue
Connectez-vous sur l’ESP8266 depuis un navigateur. Attendez quelques secondes pour obtenir les premières mesures.
Les deux graphiques du 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 gauge. 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.
L’histogramme permet de suivre l’évolution moyenne de la température et de l’humidité. Ici, chaque barre représente 6 minutes environ.
Voilà, notre projet de mini station météo est terminé.
Tous les articles et projets sur programmation Web Server des ESP8266
Si le sujet vous intéresse, voici un récapitulatif de tous les articles portant sur programmation Web Server des ESP8266
- Projet de station météo avec interface HTML sur ESP8266 (DHT22 + BMP180)
- Projet station météo ESP8266 (Partie 3). Récupérer l’heure avec NTPClient et stockage SPIFFS
- ESP8266. Comment utiliser la librairie WiFiManager pour gérer les connexions WiFi
- Piloter le GPIO de l’ESP8266 (Serveur Web) depuis Jeedom en TCP/IP sans fil – Partie 2
- Piloter le GPIO de l’ESP8266 (2) depuis Domoticz en TCP/IP sans fil. Serveur Web et requête HTTP
- Projet station météo ESP8266 (Partie 5). Afficher des jauges et graphiques Google Charts
- ESP8266. Développer et tester l’interface HTML avec Node.js et Pug (ancien Jade)
- Projet station météo ESP8266 (Partie 4). ArduinoJson, charger, enregistrer des fichiers (SPIFFS)
- Projet station météo ESP8266 (Partie 2). Piloter le code Arduino depuis l’interface HTML
- Projet station météo ESP8266 (Partie 1). Créer l’interface HTML, stockage SPIFFS
Accéder rapidement aux autres parties du projet
Voici les liens pour accéder aux autres parties du projet
- Stocker des données sur une carte micro SD. Code Arduino compatible ESP32, ESP8266
- Débuter Arduino. Recevoir des commandes depuis le port série (compatible ESP32 ESP8266)
- Fonctions C++ print•println•printf•sprintf pour Arduino ESP32 ESP8266. Combiner•formater → port série
- String C++. concat•c_srt•indexOf•replace•subString… pour Arduino ESP32 ESP8266
- ESP01. Débuter avec l’IDE Arduino ou PlatformIO. Quel module choisir ? Repérage des broches
Bonjour,
c’est un excellent tuto, malheureusement j’ai aussi “not found :/” quand je me connecte à l’ESP8266 via Firefox…
Je vois que les mauvais élèves précédents n’ont même pas eu le droit à une petite réponse de votre part…
Pourriez vous me donner une piste s’il vous plait?
Merci par avance,
Olivier
Bonsoir j’ai essayée ce code avec ma carte NODEMCU v0.9 mais j’ai trouvée un problème de connexion au wifi, la carte ce connecte au réseau wifi pendant quelque seconde et se déconnecte automatiquement pouvez vous me proposez une solution je travaille avec une capteur de température DHT11, la capacité de la mémoire
flash est 4MB, j’ai trouvé ce problème juste avec ce code pour les autres codes la carte reste connecté.
Bonjour Fatiha. Qu’utilisez vous pour coder ? code Arduino, micropython… ?
Bonjour,
Merci encore pour ce tutoriel très intéressant et bien décrit.
Je n’ai en revanche pas réussi à le faire fonctionner… si le moniteur m’informe que tout semble fonctionner, la page web affiche cependant ‘Not found : /”. Auriez-vous une idée ?
Par ailleurs, je voudrais vous demander de quelle façon je pourrais sauvegarder les données enregistrées pour les exploiter par exemple sous Excel ou autre ? J’imagine qu’il existe plusieurs stratégies possibles, par exemple si je souhaite les stocker dans une base de données ou encore dans un fichier.. je suis preneur de liens vers d’autres tuto si jamais vous en connaissez.
Encore merci !
Bonjour,
avez vous trouvez une solution à ce problème svp?
Merci
Olivier
Bonjour,
pour les impatients…il faut faire la partie 1 du tuto lol…
https://projetsdiy.fr/esp8266-web-server-partie1-stockage-spiffs-interface-web/
avec le point super important du firmware a mettre à jour… : ESP8266 Sketch Data Upload.
très beau programme en tout cas, pas tout compris mais très esthétique et efficace, merci Projets DIY
Merci beaucoup Oliver
dem j’obtiens la même chose
Bonjour,
avez vous trouvez le problème svp?
Merci
Olivier
Bonjour, Merci pour ce très bon tuto.
Je me pose la question de partir sur des petits modules indépendants à base d’ESP8266, d’une batterie Lipo et de capteurs (temperature, luminosité…).
J’aime assez l’idée que chaque ESP8266 héberge son propre serveur web.
J’ai commandé des ESP8266 avec 16Mo de mémoire pour avoir plus de place.
– Tu me conseilles quoi pour générer des pages interactives ? Pour faire des graphiques ?
– tu penses qu’a terme on pourrait heberger du php ? ou alors partir sur des mini orange pi avec une lipo ?
Merci
Content de te revoir Gandolfi. Oui c’est une très bonne idée. Tu peux toujours publier les mesures sur un serveur domotique en HTTP ou MQTT très simplement pour construire un dashboard qui regroupe toutes les mesures et commandes dans un 2ème temps. Pour construire tes pages HTML, c’est toujours un problème effectivement. Je ne suis pas développeur Web alors je ne connais pas toutes les solutions disponibles. Il faut aussi prendre en compte le coût car tout n’est pas Open Source. Cependant, sur un ESP8266, on ne va pas faire un site marchand. Il y a assez peu d’affichages et de boutons. Je te conseille de regarder les épisodes précédents
– https://projetsdiy.fr/bootstrap-esp8266-webserver-interface/ : maquette HTML en ligne avec layoutit
– https://projetsdiy.fr/esp8266-web-serveur-developpement-rapide-code-htmljs-node-js-pug/ : comment moins coder en HTML en Pug (Jade).
Pour les graphiques, ceux de google fonctionnent assez bien. Il y en a d’autres mais je n’ai encore rien rédigé dessus. Pour le php, je dois regarder, je ne sais pas. Non, l’orange Pi ne me semble pas très bien adapté pour un fonctionnement en continu sur batterie. Pour de la robotique et du radio commandé (ou WiFi / Bluetooth), ça fonctionne. Par contre pour un appareil qui dort dans un coin, il faut gérer la consommation d’énergie. Dans l’idéal, mesurer l’énergie restante, envoyer un alerte, alarme puis arrêter proprement l’OS…sinon gros risque pour la carte SD. Ensuite, il faut maintenir l’OS pour corriger les failles de sécurité et éviter de créer une porte ouverte à des hackers peux scrupuleux. A très bientôt.
Bonjour. Content de te parler moi aussi. Je ne t’ai pas répondu avant car j’étais en train de faire le tour des possibilités. Je ne sais pas par ou commencer.
– Je voudrais utiliser le moins de matériel ou de logiciels possibles pour mes projets car je compte l’amener en classe avec mes élèves.
A ma disposition, j’ai mon ordinateur portable sous windows que j’amène les jours de classes, mon portable sous android, des wemos, des arduino mini et pro, des plaques d’essais, des émetteurs recepteurs en 433 ou 2.4ghz , des lipos, des powerbank, des raspberry 2 et 3, des sondes diverses, des relais…..
La classe est équipé d’internet et surement d’un vidéo projecteur avec Tableau numérique tactile.
– Je voudrais avoir des dispositifs indépendants avec des sondes que je puisse laisser dans la classe ou que je transporte de temps en temps pour des relevés en sciences (température, humidité, pression, luminosité…). Ainsi on pourrait suivre les températures, pressions et humidité sur la journée et l’année en classe.
Dans un deuxième temps pouvoir aussi consulter ces relevés de valeur sur un site internet et commander des gpio pour actionner des choses par le net également.
J’ai des compétences en php et je penser concevoir un site avec des pages dédiées aux relevés et aux commandes de GPIO. Ainsi je peux ouvrir ce site en classe (sur le réseau interne) ou de l’extérieur par internet.
LES POSSIBILITÉS QUE J’AI VUES.
———————————————-
– J’ai sois ESPeasy qui permet de connecter un tas de sondes, commander des gpio apparement (https://www.letscontrolit.com/wiki/index.php/GPIO#PWM_control).
Mais je ne sais pas si je dois mettre en place des rules pour l’envoi de données ? a qui j’envoie les données (serveur jeedom, roboticz ou mon site perso) ? ou si je peux les stocker sur l’esp8266 pour les consulter à la volée depuis un navigateur web ?
– Installer un serveur sur l’ESP8266 grace aux tutos que tu m’indiques. Avec les memes questions que pour l’ESP8266.
– J’ai node-red mais je trouve que c’est un peu usine à gaz bien que très sympa à utiliser. J’ai d’ailleurs fait mon premier montage grace à ton tuto.
Comme tu le vois c’est un peu fouillis car il y a plein de possibilités mais je ne sais pas sur quoi bifurquer pour utiliser le moins de logiciels et de matériels. L’idée du site internet perso m’interesse beaucoup car j’ai un serveur dédié et je maitrise assez bien la conception de site sous wordpress par exemple. En plus cela permettrait d’ajouter d’autres choses par la suite (webcam, serveur d’échanges….). S’il le faut je pourrais laisser en classe un orange pi pas cher que je “planquerais” sous mon bureau si la solution du serveur est la plus pratique.
je te remercie
Super Tuto. Sur cette partie du projet, la compilation du code arduino ne se fait pas car il me signale l’erreur suivante: ‘HISTORY_FILE’ was not declared in this scope. Merci de m’indiquer comment y remédier .
Bonjour Didier. Pouvez vous vérifier si la ligne #define HISTORY_FILE “/history.json” n’a pas été effacée dans votre code (ligne 44). Cette variable permet de définir le chemin du fichier JSON dans lequel l’historique de mesure est enregistré.