ESP8266 NodeMCU Web Server: Display Sensor Readings in Gauges

Learn how to build a web server with the ESP8266 NodeMCU to display sensor readings in gauges. As an example, we’ll display temperature and humidity from a BME280 sensor in two different gauges: linear and radial. You can easily modify the project to plot any other data. To build the gauges, we’ll use the canvas-gauges JavaScript library.

ESP8266 NodeMCU Web Server Display Sensor Readings in Gauges

We have a similar tutorial for the ESP32 board: Web Server – Display Sensor Readings in Gauges

Project Overview

This project will build a web server with the ESP8266 that displays temperature and humidity readings from a BME280 sensor. We’ll create a linear gauge that looks like a thermometer to display the temperature, and a radial gauge to display the humidity.

ESP8266 NodeMCU Gauges Web Server Overview

Server-Sent Events

The readings are updated automatically on the web page using Server-Sent Events (SSE).

ESP8266 NodeMCU Gauges Web Server Overview Server-Sent Events

To learn more about SSE, you can read:

Files Saved on the Filesystem

To keep our project better organized and easier to understand, we’ll save the HTML, CSS, and JavaScript files to build the web page on the board’s filesystem (LittleFS).

Prerequisites

Make sure you check all the prerequisites in this section before continuing with the project.

1. Install ESP8266 Board in Arduino IDE

We’ll program the ESP8266 using Arduino IDE. So, you must have the ESP8266 add-on installed. Follow the next tutorial if you haven’t already:

If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP8266:

2. Filesystem Uploader Plugin

To upload the HTML, CSS, and JavaScript files to the ESP8266 filesystem (LittleFS), we’ll use a plugin for Arduino IDE: LittleFS Filesystem Uploader. Follow the next tutorial to install the filesystem uploader plugin:

If you’re using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:

3. Installing Libraries

To build this project, you need to install the following libraries:

You can install the first three libraries using the Arduino Library Manager. Go to Sketch Include Library > Manage Libraries and search for the libraries’ names.

The ESPAsyncWebServer and ESPAsynTCP libraries aren’t available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, download the libraries’ .zip folders, and then, in your Arduino IDE, go to Sketch Include Library > Add .zip Library and select the libraries you’ve just downloaded.

Installing Libraries (VS Code + PlatformIO)

If you’re programming the ESP8266 using PlatformIO, you should add the following lines to the platformio.ini file to include the libraries, change the Serial Monitor speed to 115200, and use LittleFS for the filesystem:

monitor_speed = 115200
lib_deps = ESP Async WebServer
  arduino-libraries/Arduino_JSON @ 0.1.0
  adafruit/Adafruit BME280 Library @ ^2.1.0
  adafruit/Adafruit Unified Sensor @ ^1.1.4
board_build.filesystem = littlefs

Parts Required

To follow this tutorial you need the following parts:

You can use any other sensor, or display any other values that are useful for your project. If you don’t have the sensor, you can also experiment with random values to learn how the project works.

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

Schematic Diagram

We’ll send temperature and humidity readings from a BME280 sensor. We’re going to use I2C communication with the BME280 sensor module. For that, wire the sensor to the default ESP8266 SCL (GPIO 5) and SDA (GPIO 4) pins, as shown in the following schematic diagram.

ESP8266 NodeMCU BME280 Sensor Temperature Humidity Pressure Wiring Diagram Circuit

Recommended reading: ESP8266 Pinout Reference: Which GPIO pins should you use?

Organizing Your Files

To keep the project organized and make it easier to understand, we’ll create four files to build the web server:

  • Arduino sketch that handles the web server;
  • index.html: to define the content of the web page;
  • sytle.css: to style the web page;
  • script.js: to program the behavior of the web page—handle web server responses, events, create the gauges, etc.
ESP8266 organizing your Files arduino sketch index html style css script js

You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We’ll upload these files to the ESP8266 filesystem (LittleFS).

You can download all project files:

HTML File

Copy the following to the index.html file.

<!DOCTYPE html>
<html>
  <head>
    <title>ESP IOT DASHBOARD</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/png" href="favicon.png">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="http://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.7/all/gauge.min.js"></script>
  </head>
  <body>
    <div class="topnav">
      <h1>ESP WEB SERVER GAUGES</h1>
    </div>
    <div class="content">
      <div class="card-grid">
        <div class="card">
          <p class="card-title">Temperature</p>
          <canvas id="gauge-temperature"></canvas>
        </div>
        <div class="card">
          <p class="card-title">Humidity</p>
          <canvas id="gauge-humidity"></canvas>
        </div>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

View raw code

The HTML file for this project is very simple. It includes the JavaScript canvas-gauges library in the head of the HTML file:

<script src="https://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.7/all/gauge.min.js"></script>

There is a <canvas> tag with the id gauge-temperature where we’ll render the temperature gauge later on.

<canvas id="gauge-temperature"></canvas>

There is also another <canvas> tag with the id gauge-humidity, where we’ll render the humidity gauge later on.

<canvas id="gauge-humidity"></canvas>

CSS File

Copy the following styles to your style.css file. It styles the web page with simple colors and styles.

html {
  font-family: Arial, Helvetica, sans-serif; 
  display: inline-block; 
  text-align: center;
}
h1 {
  font-size: 1.8rem; 
  color: white;
}
p { 
  font-size: 1.4rem;
}
.topnav { 
  overflow: hidden; 
  background-color: #0A1128;
}
body {  
  margin: 0;
}
.content { 
  padding: 5%;
}
.card-grid { 
  max-width: 1200px; 
  margin: 0 auto; 
  display: grid; 
  grid-gap: 2rem; 
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card { 
  background-color: white; 
  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title { 
  font-size: 1.2rem;
  font-weight: bold;
  color: #034078
}

View raw code

JavaScript File (creating the gauges)

Copy the following to the script.js file.

// Get current sensor readings when the page loads  
window.addEventListener('load', getReadings);

// Create Temperature Gauge
var gaugeTemp = new LinearGauge({
  renderTo: 'gauge-temperature',
  width: 120,
  height: 400,
  units: "Temperature C",
  minValue: 0,
  startAngle: 90,
  ticksAngle: 180,
  maxValue: 40,
  colorValueBoxRect: "#049faa",
  colorValueBoxRectEnd: "#049faa",
  colorValueBoxBackground: "#f1fbfc",
  valueDec: 2,
  valueInt: 2,
  majorTicks: [
      "0",
      "5",
      "10",
      "15",
      "20",
      "25",
      "30",
      "35",
      "40"
  ],
  minorTicks: 4,
  strokeTicks: true,
  highlights: [
      {
          "from": 30,
          "to": 40,
          "color": "rgba(200, 50, 50, .75)"
      }
  ],
  colorPlate: "#fff",
  colorBarProgress: "#CC2936",
  colorBarProgressEnd: "#049faa",
  borderShadowWidth: 0,
  borders: false,
  needleType: "arrow",
  needleWidth: 2,
  needleCircleSize: 7,
  needleCircleOuter: true,
  needleCircleInner: false,
  animationDuration: 1500,
  animationRule: "linear",
  barWidth: 10,
}).draw();
  
// Create Humidity Gauge
var gaugeHum = new RadialGauge({
  renderTo: 'gauge-humidity',
  width: 300,
  height: 300,
  units: "Humidity (%)",
  minValue: 0,
  maxValue: 100,
  colorValueBoxRect: "#049faa",
  colorValueBoxRectEnd: "#049faa",
  colorValueBoxBackground: "#f1fbfc",
  valueInt: 2,
  majorTicks: [
      "0",
      "20",
      "40",
      "60",
      "80",
      "100"

  ],
  minorTicks: 4,
  strokeTicks: true,
  highlights: [
      {
          "from": 80,
          "to": 100,
          "color": "#03C0C1"
      }
  ],
  colorPlate: "#fff",
  borderShadowWidth: 0,
  borders: false,
  needleType: "line",
  colorNeedle: "#007F80",
  colorNeedleEnd: "#007F80",
  needleWidth: 2,
  needleCircleSize: 3,
  colorNeedleCircleOuter: "#007F80",
  needleCircleOuter: true,
  needleCircleInner: false,
  animationDuration: 1500,
  animationRule: "linear"
}).draw();

// Function to get current readings on the webpage when it loads for the first time
function getReadings(){
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var myObj = JSON.parse(this.responseText);
      console.log(myObj);
      var temp = myObj.temperature;
      var hum = myObj.humidity;
      gaugeTemp.value = temp;
      gaugeHum.value = hum;
    }
  }; 
  xhr.open("GET", "/readings", true);
  xhr.send();
}

if (!!window.EventSource) {
  var source = new EventSource('/events');
  
  source.addEventListener('open', function(e) {
    console.log("Events Connected");
  }, false);

  source.addEventListener('error', function(e) {
    if (e.target.readyState != EventSource.OPEN) {
      console.log("Events Disconnected");
    }
  }, false);
  
  source.addEventListener('message', function(e) {
    console.log("message", e.data);
  }, false);
  
  source.addEventListener('new_readings', function(e) {
    console.log("new_readings", e.data);
    var myObj = JSON.parse(e.data);
    console.log(myObj);
    gaugeTemp.value = myObj.temperature;
    gaugeHum.value = myObj.humidity;
  }, false);
}

View raw code

Here’s a summary of what this code does:

  • initializing the event source protocol;
  • adding an event listener for the new_readings event;
  • creating the gauges;
  • getting the latest sensor readings from the new_readings event and display them in the corresponding gauges;
  • making an HTTP GET request for the current sensor readings when you access the web page for the first time.

Get Readings

When you access the web page for the first time, we’ll request the server to get the current sensor readings. Otherwise, we would have to wait for new sensor readings to arrive (via Server-Sent Events), which can take some time depending on the interval that you set on the server.

Add an event listener that calls the getReadings function when the web page loads.

// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);

The window object represents an open window in a browser. The addEventListener() method sets up a function to be called when a certain event happens. In this case, we’ll call the getReadings function when the page loads (‘load’) to get the current sensor readings.

Now, let’s take a look at the getReadings function. Create a new XMLHttpRequest object. Then, send a GET request to the server on the /readings URL using the open() and send() methods.

function getReadings() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/readings", true);
  xhr.send();
}

When we send that request, the ESP will send a response with the required information. So, we need to handle what happens when we receive the response. We’ll use the onreadystatechange property that defines a function to be executed when the readyState property changes. The readyState property holds the status of the XMLHttpRequest. The response of the request is ready when the readyState is 4, and the status is 200.

  • readyState = 4 means that the request finished and the response is ready;
  • status = 200 means “OK”

So, the request should look something like this:

function getStates(){
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      … DO WHATEVER YOU WANT WITH THE RESPONSE …
    }
  };
  xhr.open("GET", "/states", true);
  xhr.send();
}

The response sent by the ESP is the following text in JSON format (those are just arbitrary values).

{
  "temperature" : "25.02",
  "humidity" : "64.01",
}

We need to convert the JSON string into a JSON object using the parse() method. The result is saved on the myObj variable.

var myObj = JSON.parse(this.responseText);

The myObj variable is a JSON object that contains the temperature and humidity readings. We want to update the gauges values with the corresponding readings.

Updating the value of a gauge is straightforward. For example, our temperature gauge is called gaugeTemp (as we’ll see later on), to update a value, we can simply call: gaugeTemp.value = NEW_VALUE. In our case, the new value is the temperature reading saved on the myObj JSON object.

gaugeTemp.value = myObj.temperature;

It is similar for the humidity (our humidity gauge is called gaugeHum).

gaugeHum.value = myObj.humidity;

Here’s the complete getReadings() function.

function getReadings(){
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var myObj = JSON.parse(this.responseText);
      console.log(myObj);
      var temp = myObj.temperature;
      var hum = myObj.humidity;
      gaugeTemp.value = temp;
      gaugeHum.value = hum;
    }
  }; 
  xhr.open("GET", "/readings", true);
  xhr.send();
}

Creating the Gauges

The canvas-charts library allows you to build linear and radial gauges to display your readings. It provides several examples, and it is very simple to use. We recommend taking a look at the documentation and exploring all the gauges functionalities:

Temperature Gauge

The following lines create the gauge to display the temperature.

// Create Temperature Gauge
var gaugeTemp = new LinearGauge({
  renderTo: 'gauge-temperature',
  width: 120,
  height: 400,
  units: "Temperature C",
  minValue: 0,
  startAngle: 90,
  ticksAngle: 180,
  maxValue: 40,
  colorValueBoxRect: "#049faa",
  colorValueBoxRectEnd: "#049faa",
  colorValueBoxBackground: "#f1fbfc",
  valueDec: 2,
  valueInt: 2,
  majorTicks: [
      "0",
      "5",
      "10",
      "15",
      "20",
      "25",
      "30",
      "35",
      "40"
  ],
  minorTicks: 4,
  strokeTicks: true,
  highlights: [
      {
          "from": 30,
          "to": 40,
          "color": "rgba(200, 50, 50, .75)"
      }
  ],
  colorPlate: "#fff",
  colorBarProgress: "#CC2936",
  colorBarProgressEnd: "#049faa",
  borderShadowWidth: 0,
  borders: false,
  needleType: "arrow",
  needleWidth: 2,
  needleCircleSize: 7,
  needleCircleOuter: true,
  needleCircleInner: false,
  animationDuration: 1500,
  animationRule: "linear",
  barWidth: 10,
}).draw();

To create a new linear gauge, use the new LinearGauge() method and pass as an argument the properties of the gauge.

var gaugeTemp = new LinearGauge({

In the next line, define where you want to put the chart (it must be a <canvas> element). In our example, we want to place it in the <canvas> HTML element with the gauge-temperature id—see the HTML file section.

renderTo: 'gauge-temperature',

Then, we define other properties to customize our gauge. The names are self-explanatory, but we recommend taking a look at all possible configurations and changing the gauge to meet your needs.

In the end, you need to apply the draw() method to actually display the gauge on the canvas.

}).draw();

Special attention that if you need to change the gauge range, you need to change the minValue and maxValue properties:

minValue: 0,
maxValue: 40,

You also need to adjust the majorTicks values for the values displayed on the axis.

majorTicks: [
    "0",
    "5",
    "10",
    "15",
    "20",
    "25",
    "30",
    "35",
    "40"
],

Humidity Gauge

Creating the humidity gauge is similar, but we use the new RadialGauge() function instead and it is rendered to the <canvas> with the gauge-humidity id. Notice that we apply the draw() method on the gauge so that it is drawn on the canvas.

// Create Humidity Gauge
var gaugeHum = new RadialGauge({
  renderTo: 'gauge-humidity',
  width: 300,
  height: 300,
  units: "Humidity (%)",
  minValue: 0,
  maxValue: 100,
  colorValueBoxRect: "#049faa",
  colorValueBoxRectEnd: "#049faa",
  colorValueBoxBackground: "#f1fbfc",
  valueInt: 2,
  majorTicks: [
      "0",
      "20",
      "40",
      "60",
      "80",
      "100"

  ],
  minorTicks: 4,
  strokeTicks: true,
  highlights: [
      {
          "from": 80,
          "to": 100,
          "color": "#03C0C1"
      }
  ],
  colorPlate: "#fff",
  borderShadowWidth: 0,
  borders: false,
  needleType: "line",
  colorNeedle: "#007F80",
  colorNeedleEnd: "#007F80",
  needleWidth: 2,
  needleCircleSize: 3,
  colorNeedleCircleOuter: "#007F80",
  needleCircleOuter: true,
  needleCircleInner: false,
  animationDuration: 1500,
  animationRule: "linear"
}).draw();

Handle events

Update the readings on the gauge when the client receives the readings on the new_readings event

Create a new EventSource object and specify the URL of the page sending the updates. In our case, it’s /events.

if (!!window.EventSource) {
  var source = new EventSource('/events');

Once you’ve instantiated an event source, you can start listening for messages from the server with addEventListener().

These are the default event listeners, as shown here in the AsyncWebServer documentation.

source.addEventListener('open', function(e) {
  console.log("Events Connected");
}, false);

source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
}, false);

source.addEventListener('message', function(e) {
  console.log("message", e.data);
}, false);

Then, add the event listener for new_readings.

source.addEventListener('new_readings', function(e) {

When new readings are available, the ESP8266 sends an event (new_readings) to the client. The following lines handle what happens when the browser receives that event.

source.addEventListener('new_readings', function(e) {
  console.log("new_readings", e.data);
  var myObj = JSON.parse(e.data);
  console.log(myObj);
  gaugeTemp.value = myObj.temperature;
  gaugeHum.value = myObj.humidity;
}, false);

Basically, print the new readings on the browser console, convert the data into a JSON object and display the readings on the corresponding gauges.

Arduino Sketch

Copy the following code to your Arduino IDE or to the main.cpp file if you’re using PlatformIO.

You can also download all the files here.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp8266-web-server-gauges/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "LittleFS.h"
#include <Arduino_JSON.h>
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// Create an Event Source on /events
AsyncEventSource events("/events");

// Json Variable to Hold Sensor Readings
JSONVar readings;

// Timer variables
unsigned long lastTime = 0;  
unsigned long timerDelay = 30000;

// Create a sensor object
Adafruit_BME280 bme;         // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)

// Init BME280
void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

// Get Sensor Readings and return JSON object
String getSensorReadings(){
  readings["temperature"] = String(bme.readTemperature());
  readings["humidity"] =  String(bme.readHumidity());
  String jsonString = JSON.stringify(readings);
  return jsonString;
}

// Initialize LittleFS
void initFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  Serial.println("LittleFS mounted successfully");
}

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

void setup() {
  Serial.begin(115200);
  initBME();
  initWiFi();
  initFS();

  // Web Server Root URL
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", "text/html");
  });

  server.serveStatic("/", LittleFS, "/");
  
  // Request for the latest sensor readings
  server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
    String json = getSensorReadings();
    request->send(200, "application/json", json);
    json = String();
  });

  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);

  // Start server
  server.begin();
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
    // Send Events to the client with the Sensor Readings Every 30 seconds
    events.send("ping",NULL,millis());
    events.send(getSensorReadings().c_str(),"new_readings" ,millis());
    lastTime = millis();
  }
}

View raw code

How the code works

Let’s take a look at the code and see how it works to send readings to the client using server-sent events.

Including Libraries

The Adafruit_Sensor and Adafruit_BME280 libraries are needed to interface with the BME280 sensor.

#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

The ESP8266WiFi, ESPAsyncWebServer, and ESPAsyncTCP libraries are used to create the web server.

#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

We’ll use LittleFS to save the files to build the web server.

#include "LittleFS.h"

You also need to include the Arduino_JSON library to make it easier to handle JSON strings.

#include <Arduino_JSON.h>

Network Credentials

Insert your network credentials in the following variables, so that the ESP8266 can connect to your local network using Wi-Fi.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

AsyncWebServer and AsyncEventSource

Create an AsyncWebServer object on port 80.

AsyncWebServer server(80);

The following line creates a new event source on /events.

AsyncEventSource events("/events");

Declaring Variables

The readings variable is a JSON variable to hold the sensor readings in JSON format.

JSONVar readings;

The lastTime and the timerDelay variables will be used to update sensor readings every X number of seconds. As an example, we’ll get new sensor readings every 30 seconds (30000 milliseconds). You can change that delay time in the timerDelay variable.

unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

Create an Adafruit_BME280 object called bme on the default ESP I2C pins.

Adafruit_BME280 bme;

Initialize BME280 Sensor

The following function can be called to initialize the BME280 sensor.

// Init BME280
void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

Get BME280 Readings

To get temperature and humidity from the BME280 temperature, use the following methods on the bme object:

  • bme.readTemperature()
  • bme.readHumidity()

The getSensorReadings() function gets the sensor readings and saves them on the readings JSON array.

// Get Sensor Readings and return JSON object
String getSensorReadings(){
  readings["temperature"] = String(bme.readTemperature());
  readings["humidity"] =  String(bme.readHumidity());
  String jsonString = JSON.stringify(readings);
  return jsonString;
}

The readings array is then converted into a JSON string variable using the stringify() method and saved on the jsonString variable.

The function returns the jsonString variable with the current sensor readings. The JSON string has the following format (the values are just arbitrary numbers for explanation purposes).

{
  "temperature" : "25",
  "humidity" : "50"
}

setup()

In the setup(), initialize the Serial Monitor, Wi-Fi, filesystem, and the BME280 sensor.

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);
  initBME();
  initWiFi();
  initFS();

Handle Requests

When you access the ESP8266 IP address on the root / URL, send the text that is stored on the index.html file to build the web page.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(LittleFS, "/index.html", "text/html");
});

Serve the other static files requested by the client (style.css and script.js).

server.serveStatic("/", LittleFS, "/");

Send the JSON string with the current sensor readings when you receive a request on the /readings URL.

// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
  String json = getSensorReadings();
  request->send(200, "application/json", json);
  json = String();
});

The json variable holds the return from the getSensorReadings() function. To send a JSON string as response, the send() method accepts as first argument the response code (200), the second is the content type (“application/json”) and finally the content (json variable).

Server Event Source

Set up the event source on the server.

events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);

Finally, start the server.

server.begin();

loop()

In the loop(), send events to the browser with the newest sensor readings to update the web page every 30 seconds.

events.send("ping",NULL,millis());
events.send(getSensorReadings().c_str(),"new_readings" ,millis());

Use the send() method on the events object and pass as an argument the content you want to send and the name of the event. In this case, we want to send the JSON string returned by the getSensorReadings() function. The send() method accepts a variable of type char, so we need to use the c_str() method to convert the variable. The name of the events is new_readings.

Usually, we also send a ping message every X number of seconds. That line is not mandatory. It is used to check on the client side that the server is alive.

events.send("ping",NULL,millis());

Uploading Code and Files

After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.

Arduino IDE Open Sketch Folder to create data folder

Inside that folder, you should save the HTML, CSS, and JavaScript files.

Then, upload the code to your ESP8266 board. Make sure you have the right board and COM port selected. Also, make sure you’ve added your network credentials.

Upload Arduino code

After uploading the code, you need to upload the files. Go to Tools ESP8266 LittleFS Data Upload and wait for the files to be uploaded.

ESP8266 Tools LittleFS Data Upload Arduino IDE

When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP8266 EN/RST button, and it should print the ESP8266 IP address.

Demonstration

Open your browser and type the ESP8266 IP address. You should get access to the web page that shows the gauges with the latest sensor readings.

Gauges Web Server ESP32 ESP8266 Demonstration

You can also check your gauges using your smartphone (the web page is mobile responsive).

Gauges Web Server ESP32 ESP8266 Demonstration Smartphone

Wrapping Up

In this tutorial you’ve learned how to create a web server to display sensor readings in linear and radial gauges. As an example, we displayed temperature and humidity from a BME280 sensor. You can use those gauges to display any other values that may make sense for your project.

You might also like reading:

Learn more about the ESP8266 with our resources:

Thank you for reading.



Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »

Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »

Recommended Resources

Build a Home Automation System from Scratch » With Raspberry Pi, ESP8266, Arduino, and Node-RED.

Home Automation using ESP8266 eBook and video course » Build IoT and home automation projects.

Arduino Step-by-Step Projects » Build 25 Arduino projects with our course, even with no prior experience!

What to Read Next…


Enjoyed this project? Stay updated by subscribing our newsletter!

53 thoughts on “ESP8266 NodeMCU Web Server: Display Sensor Readings in Gauges”

  1. I just was done converting the esp32 code tp esp8266 …and then your article popped up 🙂
    Anyway great project and an interesting library. I need to check that library out a bit more as for now it seems to have problems with negative value. Yes, it is possible to add a ‘major tick’ of say “-10”, but then a value of +3 degrees, starts counting from ‘-10’ and is shown on the thermometer at the “-7” tick. I am pretty sure it can be done but I need to dive in it a bit more

    Reply
  2. Not all BME280 sensors work for me with this sketch.
    With the BME280I2C.h lib they are recognized on the 8266 or esp32 but
    with Adafruit_BME280.h not.
    All sensors I have have the same i2c address (0x76) but still
    the 6 pin BME280 sensor is not recognized ( VIN-GND-SCL-SDA-CSB-SDO).
    But they do work on an Arduino uno, for example.
    In your sketch only the model with ( VIN-GND-SCL-SDA) connections works.

    Reply
    • That is kinda odd. Does the adafruit libraru maybe accedentally expect an SPI device or is your BME SPI only perhaps?
      On a different note, the adafruit BME280 library is in fact very inefficient. It reads the temperature, humidity and pressure with 3 seperate readings,but in order to read humidity and pressure one needs to start a conversion by reading the temperature. So if you read all 3, the library does 5 readings and discards 2. There are some advantages in that coz if you just want the humidity, one always has a fresh reading but imho it would be better to have a ‘start conversion’ command and then pick whatever fresh value you want from the results

      Reply
  3. Thank you for another great tutorial. I live in North America where we do not use the metric system. Are these the lines I need to change for frahenheit readings? Are there any other lines that need changing?

    Thanks

    Reply
    • // Get Sensor Readings and return JSON object
      String getSensorReadings(){
      readings[“temperature”] = String(bme.readTemperature());
      readings[“humidity”] = String(bme.readHumidity());
      String jsonString = JSON.stringify(readings);
      return jsonString;
      }

      Reply
      • Hi.
        Yes, you need to convert to Fahrenheit as follows:
        readings[“temperature”] = String(1.8*bme.readTemperature()+32);

        Then, you need to change the script.js file to adjust the temperature range. It is explained in the tutorial how to change the range.
        Regards,
        Sara

        Reply
  4. // Get Sensor Readings and return JSON object
    String getSensorReadings(){
    readings[“temperature”] = String(bme.readTemperature());
    readings[“humidity”] = String(bme.readHumidity());
    String jsonString = JSON.stringify(readings);
    return jsonString;
    }

    Reply
  5. I would like to use AHT10 instead of the expensive BME, but I get lost in the “Get Sensor Readings and return JSON object” section. Could I count on help?

    Reply
  6. I am ot sure what library you use for the AHT10 but it will probably do a reading like:
    hum=aht10.readHumidity(AHTXX_USE_READ_DATA).
    The BME280 has a similar instruction:
    hum=BME280.readHumidity, so it is just a matter of substitution

    Reply
    • Thank you! For those in need:

      String getSensorReadings(){
      sensors_event_t humidity, temp;
      aht.getEvent(&humidity, &temp);
      readings[“temperature”] = String(temp.temperature);
      readings[“humidity”] = String(humidity.relative_humidity);
      String jsonString = JSON.stringify(readings);
      return jsonString;

      Reply
  7. For me it was impossibble di see any data in Webserver, while I see in the serial monitor.
    Now i followed the suggestion of Wijnand November 22, 2019 and downgraded ESP8266 to the 2.6.0 version.
    Now I see data in Webserber, on the smatphone too, but I dont see guages.
    Any suggestion.
    Thanks
    Renzo

    Reply
  8. thanks for the amazing project
    whether a switch can be connected to this project so that I can control it through the page, say turn on the led.

    thank you,

    Reply
  9. Hello, thanks for the tutorial. I hope I get a really fast response… I’m using 4 ultrasonic sensors, on arduino uno board and trying to send the mean value to node mcu which I would love to display in place of the humidity gauge above, but its been a week without luck. Need help please…. The data displays on my nodemcu serial monitor but the gauge doesn’t respond on the webpage.

    heres what my code looks like;
    String getSensorReadings(){
    readings[“Distance”] = String(Serial.read());
    String jsonString = JSON.stringify(readings);
    return jsonString;

    Reply
    • Hi.
      Don’t forget that you need to change the function getReadings() on the Javascript file to get the distance, as well as the addEventListener.
      Regards,
      Sara

      Reply
      • Thank you very much… That worked.
        Now, the new problem is that… I’m getting either “-1” or flunctuating readings like “13 to 58 to 10 to 208” and so on reading on my gauge.

        On my arduino sketch, using software serial I have;
        SoftwareSerial espSerial(2, 3);
        Void setup(){
        espSerial.begin(115200);
        }
        Vood loop(){
        distance = ultrasonic.read();
        espSerial.println(distance);
        }

        While nodemcu has;
        SoftwareSerial readerSerial(D6, D5)
        String getSensorReadings(){
        readings[“Distance”] = String(readerSerial.read());
        String jsonString = JSON.stringify(readings);
        return jsonString;

        Reply
  10. Hi! I have a ESP01 with 512KB. When I try to upload any file on data folder, clicking on “ESP8266 LittleFS Data Upload”, I have this report “LittleFS Not Defined for Generic ESP8266 Module, 80 MHz, Flash, Disabled (new aborts on oom), Disabled, All SSL ciphers (most compatible), 32KB cache + 32KB IRAM (balanced), Use pgm_read macros for IRAM/PROGMEM, dtr (aka nodemcu), 26 MHz, 40MHz, DOUT (compatible), 512KB (FS:none OTA:~246KB), 0, nonos-sdk 2.2.1+100 (190703), v2 Lower Memory, Disabled, None, Only Sketch, 115200

    Reply
    • Hi.
      In that error, you can see that the board doesn’t allocate any memory for the filesystem: (FS:none
      Go to Tools > Flash size and select one of the 512KB options with some memory for FS.
      I hope this helps.
      Regards,
      Sara

      Reply
      • Hummm. Thank you very much! I have read something about that, but do not understood fully. In “Flash size”, we select how the space be divided between FileSystem and Over The Air. I lost 5 days with that…How that is the simple thing, I not finded the answer. Thank you very much Sara!
        Hummm. Muito obrigado! Li algo sobre isso, mas não entendi completamente. Em “Flash Size”, selecionamos como o espaço será dividido entre FileSystem e Over The Air. Perdi 5 dias com isso… Como isso é básico, não encontrei a resposta. Muito obrigado Sara!

        Reply
  11. Hi there,

    this is a very good example for enviroment-testings. We use a MQ-2 and a SHT-30 to get 3 values with temp, humidity and gas. All works fine, startup ist OK and LittleFS working well with all 3 files from your tutorial.

    When we startup the system it will booting perfect and start reading the sensor-values. We opened the serial-monitor for checking and placed few serial.print() to check if sensor readings are good. Up to this point there comes a Error as an exeption error “Panic core_esp8266_main.cpp:137 __yield” with a near endless stack-listing. Every time we reboot the system it starts up correctly. At the moment we start a browser (PC, Tablet or SmartPhone) an type in the given IP of the board (or type F5 refreshing) the whole system will crash again.

    The only way we found in endless tries is as follow:
    – First start up the browser
    – then startup the board – plugin power-supply
    – startup will be OK – then F5 on Browser – crasing again

    A realy sporadic situation. Only (often plugin/plugout) the power-supply will sometimes (maybe all 20-30 tries) produce a startup with reaction in the opened browser. This is a realy big and frustating situation.

    Does anyone have an idea or solution for this problem? How could the Browser-Start produce such a exeption on a 8266-12F?

    Have some screenprints to show Arduino-IDE an Serial-Monitor outputs but can’t added to this comment.

    Summary:

    System will work fine in Arduino-IDE and serial-monitor. Specific serial.prints() show that sht-30 and mq-2 works well an supply values that could sown in serial-output.

    This time i start up a browser (fireox, edge, chrome) and log in to ID-Adress of the board the system will crash and startup again. But at this time there a no longer values from the sensors. Only at reconnect power-supply will produce a clear startup in serial-monitor.

    With an opened browser and many tries to plugi/plugout power will produce in a random case a situation where the opened browser shows up the sensor-values. If then is a second client-connect e.g. with smartphone on the same ip the system creases again.

    Thanks a lot for readings this and we hope there is some help on the line.

    Thanks, best regrads and stay healthy,
    Manfred

    Reply
    • Hi.
      It may be the case that the libraries you use to read from your sensors are messing up with the AsyncWebServer library because of delays in the library.
      So, what’s probably messing up your project are the following lines:

      server.on(“/readings”, HTTP_GET, [](AsyncWebServerRequest *request){
      String json = getSensorReadings();
      request->send(200, “application/json”, json);
      json = String();
      });

      The easiest workaround is to remove that part.

      Regards,
      Sara

      Reply
  12. Hi Sara,

    thanks for this fast reply. I tried to comment out this code-part (server.on….) but dosent effect in any way. I think this is not a time-problem with the sensor librarys. A few days ago i played arround with a project on your tut-site that using “one” SendHTML()-Method to build up a webpage to show up some sensor-values (but i cant find it again – please help to find it). In this method is some ajax-scripting refreshing the

    – Elements for temperature and humidity. This example works realy well and straight.

    Now i stay in trouble with the SSE-Version of reading the SHT-30 and the MQ-2. If i work “only” with the serial monitor all parts acting straight and correct. The output shows all sensor-values at any time. When i open a browser and type the ip-adress to start gauges-view the system crashes with exception, restart and no more values shown in serial-monitor and gauges webview.

    What the hell breaks the server down to reboot in case of an exception? In some cases the system act correct but this is not reproduceable (after 50 sheets a4-paper with notices ;-). I hope there is a chance to solve this problem. Any suggestions to test the getSensorReading() or the setup() befor calling server.begin()? Maybe the fault is in loop().

    The only question left is: What breaks down the server and throw an exception which breaks down the whole system and cut off any further sensor readings. I thought the readings should work in any way.

    Thanks for further thoughts and help….

    Best regards,
    Manfred

    Reply
  13. Hello, I have an encoder on interrupt pins of one ESP8266.
    It correctly give me back the angle values form 0 to 360. I store it in a variable int called pos.

    I draw a gauge by myself using canvas, I have this code for drawing the hand:

    function drawHand(ctx, pos, length, width) {
    ctx.beginPath();
    ctx.lineWidth = width;
    ctx.lineCap = “round”;
    ctx.moveTo(0,0);
    ctx.rotate(pos);
    ctx.lineTo(0, -100);
    ctx.stroke();
    ctx.rotate(-pos);
    }

    I can’t pass the pos value from esp8266 code to script.js.
    I tryed with this tutorial but you use

    gaugeTemp.value = temp;

    how can I set drawHand.pos for my hand?
    Thank you
    Massimo

    Reply
  14. Hi there,

    your workshops are realy great and some of them could be installed with no problems – thanks for that good stuff.

    We have one great question to WiFi-System on the ESP8266 / ESP32 as follow:

    To use WiFi the SOC uses the 2.4GHz Band to operate. The question is if this 2.4GHZ-Frequency could be changed? We think that this frequency is generated by PLL-Oscillator in the SOC and driven by operating-system values.

    Could this be changed to a lower frequency band width for example with an OS-Update?

    Thanks for reply to this question.

    Best regards,
    Manfred

    Reply
  15. Hi again,

    as we ask for the timer-problem few days ago we now found the problem. In setup() there is a function-call getSensorReadings() to update the values for the first time. This call is embedded in the server.on() call within an request.send() method.

    This method-call crashes the function call and the whole setup()-call. By the way we think this is not a timing-problem at all, only the function call (for testing we cleared the getsensorreadings-method to deliver a simle “Hello” so that it couldn’t have timing-delays) kill the setup.

    Our solution is to initiate the ESP setup() with hardcoded startup-values that updated with the first (and following) loop() call at all. Mess but simple and clear.

    Thanks,
    Manfred

    Reply
    • Hi Manfred,
      I’ve got the same problem: The thing crashes as soon as the Website request an update.
      It seem that you found the solution. Could you post the piece of code you changed / inserted ?
      Would be a great help!

      Reply
      • Hi.
        Can you better describe your issue?
        What happens exactly to the board? Do you get any errors on the Serial Monitor?
        Regards,
        Sara

        Reply
        • Hi Sara, sorry for the delay in answering your post.
          I did get the usual messages after an ESP restart and a very long stack list.
          Restart gave 2 as Reset cause and 3,6 as boot mode.
          Worked fine when the following lines were commented out:
          // Request for the latest sensor readings
          // server.on(“/readings”, HTTP_GET, [](AsyncWebServerRequest * request) {
          // String json = getSensorReadings();
          // request->send(200, “application/json”, json);
          // json = String();
          // });

          One had to wait for the first values to be display for max. 30 seconds but that would be okay.

          I did try to reproduce the error yesterday. But, funny thing, now it works fine with the above lines uncommented. Maybe one of the many recent updates fixed the problem?

          There is still a minor thing. After some time (about an hour or so), the gauges are displayed with an offset (still within there little windows) to the top and the left and are only partially visible. Only the gauge with the offset is updated. After F5 / refresh display everything is fine again for a while. Tested with Firefox (Windows), might try others browsers (on Windows and Android) later. I’d like to add a screenshot, but now idea how 🙁

          Reply
          • Hi.
            To share an image, upload the image to google drive, Imgur, or dropbox, and then, share a link to the image source.
            Regards,
            Sara

          • Hi Sara, I tried again over the weekend and the crash-effect is back. I have not changed a thing but to add a few serial outputs to get infos about the SDK, reset reason etc.
            As long as one doesn’t refresh the browser window (F5), everything is fine. But after a refresh there is this output :

            User exception (panic/abort/assert)
            ————— CUT HERE FOR EXCEPTION DECODER —————

            Panic core_esp8266_main.cpp:137 __yield

            stack>>>

            ctx: sys
            sp: 3fffec70 end: 3fffffb0 offset: 0000
            3fffec70: 00000000 3ffe85d4 3fff0d64 40205c98
            3fffec80: 00000001 000000fe 00000000 00000000
            3fffec90: 00000000 00000000 00000000 00000000
            3fffeca0: 00000000 00000000 4bc6a7f0 00000000
            3fffecb0: 000002ed 0000be0c 3ffef20c 40214516
            3fffecc0: 0000be0c 00000000 00000100 40214583
            3fffecd0: 3f000039 0000be0c 3ffef20c 40213d55
            3fffece0: 000002ed 0000be0c 3ffef20c 4021151d
            3fffecf0: 3fffed54 3ffef20c 3ffef20c 40211570
            3fffed00: 3fffed54 3ffef0f0 3ffef13c 4020662a
            … more stack listings…
            ————— CUT HERE FOR EXCEPTION DECODER —————

            ets Jan 8 2013,rst cause:2, boot mode:(3,6)

            load 0x4010f000, len 3460, room 16
            tail 4
            chksum 0xcc
            load 0x3fff20b8, len 40, room 4
            tail 4
            chksum 0xc9
            csum 0xc9
            v00059660
            ~ld

            Sdk version: 2.2.2-dev(38a443e)
            Core Version: 3.0.2
            Boot Version: 31
            Boot Mode: 1
            CPU Frequency: 80 MHz
            Reset reason: Software/System restart

          • Hi again.
            What sensor are you using?
            Instead of getting the readings inside the on() method, call the getSensorReadings outside the function and save the results in a global variable. Then, use that variable inside the on() method instead of calling the function. I believe it’s that that is crashing your board.
            Regards,
            Sara

  16. Hi Greece2001,

    your description of the boot-message (Reset-code 2 – BootMode 3,6) is still OK an means in what case the ESP restarted. Best way is to use directly ESP.restart() before using any messaging-loops or -calls.

    The visual mess with the gauges are also on our project. At this time we have no solution where this “visual error” comes from. This could be a problem from the gauges code itself. With all browsers (firefox, chrome, edge …) we have the same problem. We think this is a timing-problem in drawing-routines at the gauges-lib. Seems that the redraw-refresh-time is longer as the message-respone-time and therefore comes the destroyed drawing.

    Hope someone find the problem soon. This time the gauges-drawing-fault is not for production systems.

    Best regards,
    Manfred

    Reply
  17. Hello
    Is there a way to replace the BNE280 with DS18B20 to measure the temperature of a swimming pool and stay only with the web page that has the thermometer?
    I know you have a project with the DS18B20, but this website is nicer and I wanted to keep the image of the thermometer

    Reply
    • Hi.
      Yes, you can use only the thermometer part.
      Make sure you replace all BME280 parts with the DS18B20.
      In the JSON string, send only the temperature reading.
      Regards,
      Sara

      Reply
  18. Hello
    All codes on this site and other sites are designed for on and off switches.
    I want a code that can control the output instantly with a button.
    This means that when my finger is on the button, the output is active and when I remove my finger from the button, the output is disabled.
    Please guide me.

    Reply

Leave a Reply to Manfred Cancel reply

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.