ESP8266 (Web Server – Part 2): Interaction between Arduino code and HTML interface

Here is the second part of the tutorials devoted to the creation of an HTML interface stored in the SPIFFS zone of an ESP8266 functioning like a web server. In this tutorial, we will add the code needed to implement the interactions between the Web interface and the Arduino code. We will implement automatic updating of the measurement table and displays. We will manage the commands to enable and disable GPIOs. Finally, some resources will be placed in the SPIFFS zone (Bootstrap and jquery CSS and JS files) to reduce the loading time.

A little JSON before you start

JSON is a method for structuring data for storage and transfer. It replaces very advantageously the XML especially in the Javascript language or it is supported natively. It is also very simple to create a JSON structure in the Arduino code. Without going into too much detail, here’s how it works:

  • The data is stored as key: value
  • Each pair must be separated by a comma, except the last pair
  • Several types of values ​​exist
    • string
    • array []
    • A free data structure object respecting the JSON formalism => key: value, key: {}, key: []

For example,

{
  "cle1":"valeur1",
  "cle2":"[1,2,3,4]",
  "cle3":{
    "clea":"valeura",
    "cleb":"['a','b','c']"
  }
}

Or an array

[{
	"temperature": "18",
	"humidite": "52"
}, {
	"temperature": "19",
	"humidite": "52"
}]

To test your JSON, I advise you http://jsonlint.com/. It is enough to carry out many projects, but to know more, you can consult this article in French of our friends of alsacreation.

Automatic updating of the table and measurement displays

As much as let the browser regularly update the measurements made using the ESP8266. We will take the code from the interface developed in the previous tutorial and make it evolve so that all the interactions are managed on the client side (on the Internet browser).

HTML code

Let’s resume the code on the Measures tab. Three navigation buttons have been added to create 3 displays. Each button has a badge marked with an identifier (#temperature, #humidite and #pa) that will allow us to update the measurement.

Then a table consisting of three columns displays the measurement type, the current value and a previous value. Each column is associated with a data field that will be associated with a JSON sent by the Arduino code. The contents of each column are formatted by a function specified in the data-formatter attribute.

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
table(id='tab_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

Everything is ready on the side of the web interface

Javascript code

The Bootstrap-table plugin will simplify our lives to update the data. Indeed, it will suffice to call the method refresh and to indicate the url of return of the data (all the functions are presented here). However, we must make sure that the table exists before we can start, so we must include this call in a $ (document) .ready () test. It will also be used to call a function that will update the displays. Here is the code

$(document).ready(function(){
  var Timer_UdpateMesures;
  // Actualise les données dès que la page est chargée - Update data when the page is ready  
  $('#tab_mesures').bootstrapTable('refresh',{silent: true, showLoading: false, url: '/tabmesures.json'})
  updateMesures();
})

Small problem, this solution does not work in case the table is integrated into a tab (div tab-pane). Fortunately, the solution was presented in issue 1760 on Github. On the shown.bs.tab class of the tag a [data-toggle = ‘tab’], we test the presence of the tab_mesures attribute, which corresponds to the tab containing the measure table. If this is the case, the refresh of the table and the 3 displays are started.

var Timer_UdpateMesures;
$('a[data-toggle=\"tab\"]').on('shown.bs.tab', function (e) {   
  //On supprime tous les timers lorsqu'on change d'onglet
  clearTimeout(Timer_UdpateMesures);  
  var target = $(e.target).attr("href")  
  console.log('activated ' + target );  
  
  // IE10, Firefox, Chrome, etc.
  if (history.pushState) {
    window.history.pushState(null, null, target);
  } else { 
    window.location.hash = target;
  }        
  if (target=='#tab_mesures')  {
    $('#table_mesures').bootstrapTable('refresh',{silent:true, url:'/tabmesures.json'}); 
    updatesMesures();
  }  
});

To update the data at regular intervals, we will create a Timer which will execute a function after one minute (6000ms) for example. In order for the timer to be invoked recursively, it is sufficient to restart it each time the measurement table has just been updated (on.(‘load-success.bs.table’)). We will also test that the tab of the table is active. The following code is obtained:

// 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");
  Timer_UdpateMesures=setTimeout(function(){
    $('#tab_mesures').bootstrapTable('refresh',{silent: true, showLoading: false, url: '/tabmesures.json'});
    updateMesures();
  },6000);                
});

In the updateMesures() function, we are going to make a call $.getJSON which takes in parameter a URL, here /mesures.json .

Note. We could extract the data when formatting the table but I prefer to introduce a new jquery function.

As soon as the data is received, the function is executed. The jquery html method updates the contents of the badge. Since this is a JSON object, the value is accessed by data.name_element. To facilitate the development, the function fail can also be called. The JSON object is sent to the console. To be able to read it, one uses the Javascript function JSON.stringify.

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));
  });
};

It only remains to format the display of the columns of the table. To understand what is happening, here is the JSON returned by ESP8266. The Bootstrap-table plugin expects a table of data. If this is not the case, you will get an error returned by the function .fail()

[{
	"mesure": "Température",
	"valeur": "21.40",
	"unite": "°C",
	"glyph": "glyphicon-indent-left",
	"precedente": "0.00"
}, {
	"mesure": "Humidité",
	"valeur": "32.00",
	"unite": "%",
	"glyph": "glyphicon-tint",
	"precedente": "0.00"
}, {
	"mesure": "Pression Atmosphérique",
	"valeur": "990.48",
	"unite": "mbar",
	"glyph": "glyphicon-dashboard",
	"precedente": "0.00"
}]

For each line, we will recover:

  • The label of the measure
  • The current value
  • The unit
  • The symbol avoids a test client side (browser) and Arduino code is downloaded more quickly if modified than SPIFFS files.
  • A previous value

We will now create a function that will take care of creating the label that will correspond to each column. For the Measure column, the labelFormatter() function is called.

Note. The type of measure could be coded so that the label is fetched in a table or a JSON file for each language. Here it is just an example to show the architecture of the system.

We test the label and build a string that contains the label. We add a span tag that will add just before the pull-left a glyphicon icon contained in the JSON (row.glyph). You will find all the glyphicons offered by Bootstrap here. Alternatively, you can use the Fontawesome library.

function labelFormatter(value, row){
  var label = "";
  if ( value === "Température" ) {
    label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
  } else if ( value === "Humidité" ) {
    label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
  } else if ( value === "Pression Atmosphérique" ) {
    label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
  } else {
    label = value;
  } 
    return label;
}

Let’s move on to the current measure that is formatted by the valueFormatter() function. Since the content of the line is available, a test can be made to determine whether the current value is greater or lower than the previous one. One can thus construct a label of the form: value + unit + icon (up / down).

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;
}

Finally for the previous value, we simply assemble value + unit.

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;
}

Everything is ready on the side of the web interface, let’s move to the code Arduino listed ESP8266.

Arduino Code

We will add functions that will be called when the server receives a request on /mesures.json and /tabmesures.json (just before starting the server).

server.on("/tabmesures.json", sendTabMesures);
server.on("/mesures.json", sendMesures);

For both functions, we will create a string text respecting the formalist JSON. For the sendMesures() function, this will be a simple object.

{“t”: “21.50”, “h”: “31.80”, “pa”: “990.53”}

Or in a more readable form

{
  "t":"21.50",
  "h":"31.80",
  "pa":"990.53"
}

As you can see in the code, each key and value must be enclosed by a double quotation mark (“), so you must precede it with the escape character so that the string can be compiled (\”). The server.send() method is then called to send the JSON object to the client.

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

  server.send(200, "application/json", json);
  Serial.println("Mesures envoyees");
}

For the table, an object consisting of a table must be sent, each line being an object containing the necessary information (label, current value, unit, symbol, previous value). The chain is more tricky to build. For the moment we send 0 for the previous value.

void sendTabMesures() {
  double temp = 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 = 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 = 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("Tableau mesures envoyees");
}

Little advice. Feel free to cut out your channel and go to the browser to retrieve the JSON sent. An extra or missing comma and nothing works. Also consider jsonlint to check your JSON.

esp8266 web server http post json console google chrome bootstrap

Driving the GPIO from the Web Interface

There is nothing more complicated to drive the GPIO from the WEB interface, we will send a request of HTTP type POST using the command jsquery $ .post (). In return, the display of the GPIO (On or Off) will be updated according to the response sent by the server.

HTML code

We will take advantage of the power of the jquery library to intercept a click on the command button (ON, OFF) of each GPIO. The HTML code becomes very simple.

.col-xs-6.col-md-4
  #D5_On.button.btn.btn-success.btn-lg(type='button') ON
.col-xs-6.col-md-4
  #D5_Off.button.btn.btn-danger.btn-lg(type='button') OFF

Javascript code

The Javascript code will run in two steps. First we intercept a click on the button using the command jquery $(‘#Dx_State’). On a click, the setButton function is called and the parameters (GPIO, State) are passed to it. The state will be set to 1 to enable output and 0 to stop!

$('#D5_On').click(function(){ setBouton('D5','1'); });
$('#D5_Off').click(function(){ setBouton('D5','0'); });

Then the setButton function sends an HTTP request of type POST. To do this, use the $ .post function of jquery (official documentation). Here are how it works:

  • The URL contains parameters and values. We could also pass a JSON but it’s easier to manage it on the Arduino side with the server.arg (“xx”) method. The URL will take the form gpio?id=id_gpio&state=state_gpio
  • The done function is used to execute code as soon as a valid response is received from the server
  • The fail function is used to perform a processing in the event of an error. Practice for the development

The processing is very simple, by convention, the Arduino code returns the request completed by a success flag which takes the value 1 if the ESP8266 has correctly performed the requested request, 0 if it is not. One could imagine other states and modify the display accordingly or make appear a notification …

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(id_gpio);
    if ( 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));
  });
}

Arduino Code

This is exactly the same as for measurements. The updateGpio() function is called when the server receives a request to /gpio.

server.on("/gpio", updateGpio);

We will retrieve the parameters passed to the server (gpio, state) using the server.arg(“id”) method. Then it’s classic cooking. We just need to be careful because we get a character string, so we need to make a small conversion to point to the correct pin (Pin). Regardless of the treatment, what matters is to respond to the client, as he awaits his response impatiently. We build a JSON in which we copy (for example) the request and add a success (success) which could take 1s if OK, 0 if KO, another value in case of error … and we send it all with A server.send().

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 mis a jour");
}

For example, to activate pin D7 the following JSON object

{"gpio":"D7","etat":"1","success":"1"}

Adding resources to the SPIFFS file system

It is possible to avoid fetching resources on the Internet (jquery, bootstrap, bootstrap-table …) by embarking all the resources in the Data directory. After doing some tests, there is not really any gain in terms of performance. The ESP8266 has a fairly limited power, so it does not deliver resources to the browser faster than the CDN.

On the other hand, if you do not want the ESP8266 to be able to access the internet and access it in AP mode (access point), it is necessary to take everything into the SPIFFS zone

esp8266 firefow temps chargement ressources webserver

To retrieve the resources, paste the URL into a browser and paste the resulting text into a text file. It is best to place the files in separate folders to facilitate the now. Be careful that the full path (directory / file.ext) does not exceed 31 characters (SPIFFS limitation).

Ressource Folder File name
 bootstrap.min.css css bootstrap.min.css
 bootstrap-table.min.css css bootstrap-table.min.css

It only remains to modify the paths in the Head, which gives for example.

script(src="js/jquery.min.js")
script(src="js/bootstrap.min.js")
script(src="js/bootstrap-table.min.js")
link(rel="stylesheet" href="css/bootstrap-table.min.css")

Assemble everything

As usual, here is the Pug code to generate the HTML interface file. You can modify it according to your needs and to do your tests.

Interface Pug Template

html(charset='UTF-8')
    head
      meta( name='viewport')
      script(src='js/jquery.min.js')
      script(src='js/bootstrap.min.js')
      script(src='js/bootstrap-table.min.js')
      link(rel="stylesheet" href="css/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 Demo Webserver ESP8266 + Bootstrap
        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
            h2 Graphs
            
          div#tab_gpio.tab-pane.fade
            h2 GPIO
            .row
                .col-xs-6.col-md-4
                  h4.text-left
                    | D5
                    #D5_etat.span.badge OFF
                .col-xs-6.col-md-4
                  #D5_On.button.btn.btn-success.btn-lg(type='button') ON
                .col-xs-6.col-md-4
                  #D5_Off.button.btn.btn-danger.btn-lg(type='button') OFF
                .col-xs-6.col-md-4
                  h4.text-left
                    | D6
                    #D6_etat.span.badge OFF
                .col-xs-6.col-md-4
                  #D6_On.button.btn.btn-success.btn-lg(type='button') ON
                .col-xs-6.col-md-4
                  #D6_Off.button.btn.btn-danger.btn-lg(type='button') OFF
                .col-xs-6.col-md-4
                  h4.text-left
                    | D7
                    #D7_etat.span.badge OFF
                .col-xs-6.col-md-4
                  #D7_On.button.btn.btn-success.btn-lg(type='button') ON
                .col-xs-6.col-md-4
                  #D7_Off.button.btn.btn-danger.btn-lg(type='button') OFF
                .col-xs-6.col-md-4
                  h4.text-left
                    | D8
                    #D8_etat.span.badge OFF
                .col-xs-6.col-md-4
                  #D8_On.button.btn.btn-success.btn-lg(type='button') ON
                .col-xs-6.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='https://www.diyprojects.io') English version : www.diyprojects.io
    
      //script(src='js/script.js')
    
      script().
        var Timer_UdpateMesures;
        
        $('a[data-toggle=\"tab\"]').on('shown.bs.tab', function (e) {   
          //On supprime tous les timers lorsqu'on change d'onglet
          clearTimeout(Timer_UdpateMesures);  
          var target = $(e.target).attr("href")  
          console.log('activated ' + target );  

          // IE10, Firefox, Chrome, etc.
          if (history.pushState) 
            window.history.pushState(null, null, target);
          else 
            window.location.hash = target;
          
          if (target=='#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();
            },5000);
          }                 
        });   
            
        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){
          //console.log("labelFormatter");
          //console.log(value);
          console.log(row);
          var label = "";
          if ( value === "Température" ) {
            label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          } else if ( value === "Humidité" ) {
            label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          } else if ( value === "Pression Atmosphérique" ) {
            label = value + "<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(id_gpio);
            if ( 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 thème - 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));
          }
        }
        
        // Un nouveau thème est sélectionné - New theme selected
        jQuery(function($){
          $('body').on('click', '.change-style-menu-item', function() {
            var theme_name = $(this).attr('rel');
            console.log("Changement de 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;
          }
        }

HTML code generated

Otherwise here is the HTML file

<!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 src="js/jquery.min.js"></script>
    <!--script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')-->
    <script src="js/bootstrap.min.js"></script>
    <!--script(src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js")-->
    <script src="js/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 rel="stylesheet" href="css/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>Demo Webserver ESP8266 + Bootstrap</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">
          <h2>Graphs</h2>
        </div>
        <div class="tab-pane fade" id="tab_gpio">
          <h2>GPIO</h2>
          <div class="row">
            <div class="col-xs-6 col-md-4">
              <h4 class="text-left">D5
                <div class="span badge" id="D5_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-success btn-lg" id="D5_On" type="button">ON</div>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D5_Off" type="button">OFF</div>
            </div>
            <div class="col-xs-6 col-md-4">
              <h4 class="text-left">D6
                <div class="span badge" id="D6_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-success btn-lg" id="D6_On" type="button">ON</div>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D6_Off" type="button">OFF</div>
            </div>
            <div class="col-xs-6 col-md-4">
              <h4 class="text-left">D7
                <div class="span badge" id="D7_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-success btn-lg" id="D7_On" type="button">ON</div>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D7_Off" type="button">OFF</div>
            </div>
            <div class="col-xs-6 col-md-4">
              <h4 class="text-left">D8
                <div class="span badge" id="D8_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-6 col-md-4">
              <div class="button btn btn-success btn-lg" id="D8_On" type="button">ON</div>
            </div>
            <div class="col-xs-6 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="https://www.diyprojects.io">English version : www.diyprojects.io</a></p>
        </div>
      </div>
    </div>
    <!--script(src='js/script.js')-->
    <script>
      var Timer_UdpateMesures;
      
      $('a[data-toggle=\"tab\"]').on('shown.bs.tab', function (e) {   
        //On supprime tous les timers lorsqu'on change d'onglet
        clearTimeout(Timer_UdpateMesures);  
        var target = $(e.target).attr("href")  
        console.log('activated ' + target );  
      
        // IE10, Firefox, Chrome, etc.
        if (history.pushState) 
          window.history.pushState(null, null, target);
        else 
          window.location.hash = target;
        
        if (target=='#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();
          },5000);
        }                 
      });   
          
      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){
        //console.log("labelFormatter");
        //console.log(value);
        console.log(row);
        var label = "";
        if ( value === "Température" ) {
          label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
        } else if ( value === "Humidité" ) {
          label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
        } else if ( value === "Pression Atmosphérique" ) {
          label = value + "<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(id_gpio);
          if ( 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 thème - 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));
        }
      }
      
      // Un nouveau thème est sélectionné - New theme selected
      jQuery(function($){
        $('body').on('click', '.change-style-menu-item', function() {
          var theme_name = $(this).attr('rel');
          console.log("Changement de 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>

Arduino code

And finally the Arduino code

#include <ESP8266WebServer.h>
#include <DHT.h>
#include <Adafruit_BMP085.h>
#include <FS.h>

#define ssid      "xxxx"      // WiFi SSID
#define password  "xxxx"      // WiFi password
#define DHTTYPE   DHT22       // DHT type (DHT11, DHT22)
#define DHTPIN    D4          // Broche du DHT / DHT Pin
const uint8_t GPIOPIN[4] = {D5,D6,D7,D8};  // Led
float   t = 0 ;
float   h = 0 ;
float   pa = 0;
int     sizeHist = 100;        // Nombre de points dans l'historique - History size

// Création des objets / create Objects
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP085 bmp;
ESP8266WebServer server ( 80 );
 
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 mis a jour");
}

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

  server.send(200, "application/json", json);
  Serial.println("Mesures envoyees");
}

void sendTabMesures() {
  double temp = 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 = 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 = 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("Tableau mesures envoyees");
}

void setup() {
  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 );
  // Attente de la connexion au réseau WiFi / Wait for connection
  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 ); Serial.print ( "." );
  }
  // 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())
  {
    // Serious problem
    Serial.println("SPIFFS Mount failed");
  } else {

    Serial.println("SPIFFS Mount succesfull");
  }

  server.on("/tabmesures.json", sendTabMesures);
  server.on("/mesures.json", sendMesures);
  server.on("/gpio", updateGpio);
  /*HTTP_POST, []() {
    updateGpio();
  });
  */
  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" );

}

void loop() {
  // put your main code here, to run repeatedly:
  server.handleClient();
  t = dht.readTemperature();
  h = dht.readHumidity();
  pa = bmp.readPressure() / 100.0F;
  delay(100);
}

The resulting interface

By accessing the ESP8266, you now have measurements that are displayed

esp8266 webserver dht22 bmp180 bootstrap

As well as full interaction with the GPIO. The label (On / Off) is updated after the ESP8266 has executed the command.

esp8266 webserver dht22 bmp180 bootstrap gpio

In the next tutorial we will see how to save measurements to create a history and create graphs and gauges.

Subscribe to the weekly newsletter

No spam and no other use will be made of your email. You can unsubscribe anytime.

  • tiago

    Congratulations big project, I did not understand the name of the json file and the html file to salver in arduino IDE.

DIY Projects