ESP8266 NodeMCU Web Server using SPIFFS (Beginner Guide)

This tutorial shows how to create a web server that serves HTML and CSS files stored on the ESP8266 NodeMCU filesystem (SPIFFS) using the Arduino IDE. We'll generate separate HTML and CSS files rather than having to write the HTML and CSS text inside the Arduino sketch.

The web server we'll create shows how to control the ESP8266 outputs and display sensor readings. We'll control an LED and display sensor readings from a BME280 sensor as an example.

The concepts learned in this tutorial may be used to control or display sensor readings from additional sensors.

Project Overview

It's important to outline what our web server will accomplish so that it's easier to comprehend before going straight to the project.

ESP8266 Web Server using SPIFFS page HTML CSS Demonstration smartphone
  • The web server controls an LED connected to ESP8266 GPIO 2. This is the ESP8266 on-board LED. You can control any other GPIO.
  • The web server page shows two buttons: ON and OFF to turn GPIO 2 on and off.
  • The web server page also shows the current GPIO state.
  • You’ll also use a BME280 sensor to display sensor readings (temperature, humidity, and pressure).

The following figure shows a simplified diagram to demonstrate how things work.

ESP8266 NodeMCU SPIFFS Web Server Project Overview

Prerequisites

Make sure you check all the following criteria before proceeding with this project:

1) Install ESP8266 Board in Arduino IDE

You must have the ESP8266 add-on installed since we'll be using the Arduino IDE to program the ESP8266. To install it, adhere to the next tutorial:

2) Filesystem Uploader Plugin

We'll use the Filesystem Uploader Plugin to upload files to the ESP8266 SPI Flash Filesystem (SPIFFS). The Arduino IDE plugin should now be installed.

3) Installing Libraries

Using the ESPAsyncWebServer library is one of the easiest ways to build a web server using files from the file system.

Installing the ESPAsyncWebServer library

This library cannot be downloaded through the Arduino IDE library manager. To install the library, you need to follow the next steps:

  1. Click here to download the ESPAsyncWebServer library. You should have a .zip folder in your Downloads folder.
  2. Unzip the .zip folder, and you should get the ESPAsyncWebServer-master folder.
  3. Rename your folder from ESPAsyncWebServer-master to ESPAsyncWebServer.
  4. Move the ESPAsyncWebServer folder to your Arduino IDE installation library folder.

You may also go to Sketch > Include Library > .zip Library and select the previously downloaded library.

Installing the ESPAsyncTCP

To operate properly, the ESPAsyncWebServer library also needs the ESPAsyncTCP library. Install the ESPAsyncTCP library by following the next steps:

  1. Click here to download the ESPAsyncTCP library. You should have a .zip folder in your Downloads folder.
  2. Unzip the .zip folder, and you should get the ESPAsyncTCP-master folder.
  3. Rename your folder from ESPAsyncTCP-master to ESPAsyncTCP.
  4. Move the ESPAsyncTCP folder to your Arduino IDE installation library folder.
  5. Finally, re-open your Arduino IDE.

You may also go to Sketch > Include Library > .zip Library and select the previously downloaded library.

Installing BME280 libraries

We'll display readings from a BME280 sensor in this tutorial. You will need to install the following libraries:

These libraries may be installed using the Arduino IDE Libraries Manager. Go to Sketch Include Libraries > Manage Libraries to begin. Then, to install the libraries, search for their names.

Parts Required

ESP8266 NodeMCU BME280 Temperature Humidity Pressure Sensor LED Circuit

You will need the following items to proceed with this project:

You can use the preceding links to find all the parts for your projects at the best price!

Schematic Diagram

Connect all of the components by following the next schematic diagram.

ESP8266 NodeMCU BME280 LED Wiring Schematic Diagram
BME280ESP8266
Vin3.3V
GNDGND
SCLGPIO 5
SDAGPIO 4

Organizing Your Files

You'll need three different files to build the web server. The Arduino sketch, as well as the HTML and CSS files The HTML and CSS files should be saved inside the Arduino sketch folder in a folder named “data“, as shown below:

ESP8266 NodeMCU SPIFFS Web Server Files Directories

Creating the HTML File

Create an index.html file with the following content, or go here to download all project files:

<!DOCTYPE html>
<!-- 
  LEDEdit PRO
  Complete project details at https://lededitpro.com  
-->
<html>
<head>
  <title>ESP8266 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
  <h1>ESP8266 Web Server</h1>
  <p>GPIO state<strong> %STATE%</strong></p>
  <p>
    <a href="/on"><button class="button">ON</button></a>
    <a href="/off"><button class="button button2">OFF</button></a>
  </p>
  <p>
    <span class="sensor-labels">Temperature</span>
    <span id="temperature">%TEMPERATURE%</span>
    <sup class="units">°C</sup>
  </p>
  <p>
    <span class="sensor-labels">Humidity</span>
    <span id="humidity">%HUMIDITY%</span>
    <sup class="units">%</sup>
  </p>
  <p>
    <span class="sensor-labels">Pressure</span>
    <span id="pressure">%PRESSURE%</span>
    <sup class="units">hPa</sup>
  </p>
</body>
<script>
  setInterval(function ( ) {
    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        document.getElementById("temperature").innerHTML = this.responseText;
      }
    };
    xhttp.open("GET", "/temperature", true);
    xhttp.send();
  }, 10000 ) ;

  setInterval(function ( ) {
    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        document.getElementById("humidity").innerHTML = this.responseText;
      }
    };
    xhttp.open("GET", "/humidity", true);
    xhttp.send();
  }, 10000 ) ;

  setInterval(function ( ) {
    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        document.getElementById("pressure").innerHTML = this.responseText;
      }
    };
    xhttp.open("GET", "/pressure", true);
    xhttp.send();
  }, 10000 ) ;
</script>
</html>

We need to reference the CSS file in the HTML text since we're using CSS and HTML in different files.

<link rel="stylesheet" type="text/css" href="style.css">

The <link> tag tells the HTML file that you're using an external style sheet to prepare the page's appearance. The rel attribute specifies the kind of external file; in this case, a stylesheet—the CSS file—will be used to change the look of the page.

The type attribute is set to "text/css" to indicate that the styles are being applied using a CSS file. The href attribute specifies the file location; since both the CSS and HTML files will be in the same folder, you just need to reference the filename: style.css.

We write the first heading of our web page on the following line: We have an “ESP8266 Web Server” in this case. You may change the heading to any text you want.

<h1>ESP8266 Web Server</h1>

Then add a paragraph with the text “GPIO state: ” followed by the GPIO state. Because the GPIO state changes accordingly on the GPIO state, we may add a placeholder that will be replaced with whatever value we set on the Arduino sketch.

Use % signs to add placeholders. You may use %STATE%, for example, to create a placeholder for the state.

<p>GPIO state<strong> %STATE%</strong></p>

In the Arduino sketch, you assign an attribute to the STATE placeholder value.

Create an ON button and an OFF button next. When you click on a button, the web page is sent to the root, followed by the /on URL. You are redirected to the /off URL when you click the off button.

<a href="/on"><button class="button">ON</button></a>
<a href="/off"><button class="button button2">OFF</button></a>

Finally, create three paragraphs to display the temperature, humidity, and pressure.

<p>
  <span class="sensor-labels">Temperature</span>
  <span id="temperature">%TEMPERATURE%</span>
  <sup class="units">°C</sup>
</p>
<p>
  <span class="sensor-labels">Pressure</span>
  <span id="pressure">%PRESSURE%</span>
  <sup class="units">hPa</sup>
</p>
<p>
  <span class="sensor-labels">Humidity</span>
  <span id="humidity">%HUMIDITY%</span>
  <sup class="units">%</sup>
</p>

We use the %TEMPERATURE%%HUMIDITY%, and %PRESSURE% placeholders. These will then be replaced by the actual temperature readings in the Arduino sketch.

Automatic Updates

In addition, we include some JavaScript in our HTML file that is responsible for updating the temperature readings without the need to refresh the web page.

The following snippet of code is responsible for the temperature:

setInterval(function ( ) {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("temperature").innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/temperature", true);
  xhttp.send();
}, 10000 ) ;

To update the temperature, we have a setInterval() function that runs every 10 seconds.

Basically, it makes a request at the /temperature URL to get the latest temperature reading.

  xhttp.open("GET", "/temperature", true);
  xhttp.send();
}, 10000 ) ;

When it receives that value, it updates the HTML element with the temperature ID.

if (this.readyState == 4 && this.status == 200) {
  document.getElementById("temperature").innerHTML = this.responseText;
}

In conclusion, the previous section is responsible for asynchronously updating the temperature. The process is repeated for both the pressure and humidity readings.

Creating the CSS File

Create the style.css file with the following content, or download all project files here:

/***
   LEDEdit PRO
   Complete project details at https://lededitpro.com
***/

html {
  font-family: Arial;
  display: inline-block;
  margin: 0px auto;
  text-align: center;
}
h1 {
  color: #0F3376;
  padding: 2vh;
}
p {
  font-size: 1.5rem;
}
.button {
  display: inline-block;
  background-color: #008CBA;
  border: none;
  border-radius: 4px;
  color: white;
  padding: 16px 40px;
  text-decoration: none;
  font-size: 30px;
  margin: 2px;
  cursor: pointer;
}
.button2 {
  background-color: #f44336;
}
.units {
  font-size: 1.2rem;
 }
.sensor-labels {
  font-size: 1.5rem;
  vertical-align:middle;
  padding-bottom: 15px;
}

This is just a simple CSS file that sets the font size, style, and color of the buttons as well as aligns the page. We will not go into detail about how CSS works. The W3Schools website is a great place to learn about CSS.

ESP8266 Asynchronous Web Server Sketch

Copy the following code into the Arduino IDE, or download all project files from this page. To connect the ESP8266 to your local network, you need to type in your network credentials (SSID and password).

/*
  LEDEdit PRO
  Complete project details at https://lededitpro.com
  
  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.
*/

// Import required libraries
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 bme; // I2C
//Adafruit_BME280 bme(BME_CS); // hardware SPI
//Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI

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

// Set LED GPIO
const int ledPin = 2;
// Stores LED state
String ledState;

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

String getTemperature() {
  float temperature = bme.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  //float temperature = 1.8 * bme.readTemperature() + 32;
  Serial.println(temperature);
  return String(temperature);
}
  
String getHumidity() {
  float humidity = bme.readHumidity();
  Serial.println(humidity);
  return String(humidity);
}

String getPressure() {
  float pressure = bme.readPressure()/ 100.0F;
  Serial.println(pressure);
  return String(pressure);
}

// Replaces placeholder with LED state value
String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if(digitalRead(ledPin)){
      ledState = "ON";
    }
    else{
      ledState = "OFF";
    }
    Serial.print(ledState);
    return ledState;
  }
  else if (var == "TEMPERATURE"){
    return getTemperature();
  }
  else if (var == "HUMIDITY"){
    return getHumidity();
  }
  else if (var == "PRESSURE"){
    return getPressure();
  }
  return String();
}
 
void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);

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

  // Initialize SPIFFS
  if(!SPIFFS.begin()){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP32 Local IP Address
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  
  // Route to load style.css file
  server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/style.css", "text/css");
  });

  // Route to set GPIO to HIGH
  server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){
    digitalWrite(ledPin, HIGH);    
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  
  // Route to set GPIO to LOW
  server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){
    digitalWrite(ledPin, LOW);    
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });

  server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", getTemperature().c_str());
  });
  
  server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", getHumidity().c_str());
  });
  
  server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", getPressure().c_str());
  });

  // Start server
  server.begin();
}
 
void loop(){
  
}

How the code works

Continue reading to learn how the code works, or skip to the next section.

First, include the necessary libraries:

#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

You need to type your network credentials in the following variables:

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

Create an instance that refers to the BME280 sensor called bme:

Adafruit_BME280 bme; // I2C

Next, create a variable that refers to GPIO 2 called ledPin and a string variable to hold the led state: ledState.

const int ledPin = 2;
String ledState;

Create an AsynWebServer object called server that is listening on port 80.

AsyncWebServer server(80);

Get Sensor Readings

We create three functions that return sensor readings as strings: getTemperature(), getHumidity(), and getPressure().

Here’s how the getTemperature() function looks (the other functions are similar).

String getTemperature() {
  float temperature = bme.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  //float temperature = 1.8 * bme.readTemperature() + 32;
  Serial.println(temperature);
  return String(temperature);
}

You just need to uncomment the relevant line in the getTemperature() method if you want  to display temperature in Fahrenheit degrees:

float temperature = 1.8 * bme.readTemperature() + 32;

processor()

The process() function attributes a value to the placeholders we've established in the HTML file. It takes the placeholder as input and should return a String that replaces the placeholder. The following structure should be used in the processor() function:

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if(digitalRead(ledPin)){
      ledState = "ON";
    }
    else{
      ledState = "OFF";
    }
    Serial.print(ledState);
    return ledState;
  }
  else if (var == "TEMPERATURE"){
    return getTemperature();
  }
  else if (var == "HUMIDITY"){
    return getHumidity();
  }
  else if (var == "PRESSURE"){
    return getPressure();
  }
}

This function first checks if the placeholder is the STATE one we’ve created in the HTML file.

if(var == "STATE"){

If it is, then, accordingly to the LED state, we set the ledState variable to either ON or OFF.

if(digitalRead(ledPin)){
  ledState = "ON";
}
else{
  ledState = "OFF";
}

Finally, we return the ledState variable. This replaces the STATE placeholder with the ledState string value.

return ledState;

If it finds the %TEMPERATURE% placeholder, we return the temperature by calling the getTemperature() function created previously.

else if (var == "TEMPERATURE"){
  return getTemperature();
}

The same happens for the %HUMIDITY% and %PRESSURE% placeholders by calling the corresponding functions:

else if (var == "TEMPERATURE"){
  return getTemperature();
}
else if (var == "HUMIDITY"){
  return getHumidity();
}
else if (var == "PRESSURE"){
  return getPressure();
}  

setup()

In the setup(), start by initializing the Serial Monitor and setting the GPIO as an output.

Serial.begin(115200);
pinMode(ledPin, OUTPUT);

Initialize the BME280 sensor:

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

Initialize SPIFFS:

if(!SPIFFS.begin()){
  Serial.println("An Error has occurred while mounting SPIFFS");
  return;
}

Wi-Fi connection

Connect to Wi-Fi and print the ESP8266 address:

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Connecting to WiFi..");
}
Serial.println(WiFi.localIP());

Async Web Server

The ESPAsyncWebServer library enables us to set the routes where the server will be listening for incoming HTTP requests and executing functions when a request is received on that route. For that, use the on method on the server object as follows:

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(SPIFFS, "/index.html", String(), false, processor);
});

The server will send the index.html file to the client when it receives a request on the root “/” URL. The processor is the send() function's last processor, so we may replace the placeholder with the value we want—in this case, the ledState.

Because we’ve referenced the CSS file in the HTML file, the client will request the CSS file. When that happens, the CSS file is sent to the client.

server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(SPIFFS, "/style.css","text/css");
});

You also need to define what happens on the /on and /off routes. When a request is made on those routes, the LED is either turned on or off, and the ESP32 serves the HTML file.

server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){
  digitalWrite(ledPin, HIGH);
  request->send(SPIFFS, "/index.html", String(),false, processor);
});
server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){
  digitalWrite(ledPin, LOW);
  request->send(SPIFFS, "/index.html", String(),false, processor);
});

In the HTML file, we’ve written JavaScript code that requests the temperature, humidity, and pressure on the /temperature, /humidity, and /pressure routes, respectively, every 10 seconds. So, we also need to handle what happens when we receive a request on those routes.

We simply need to send the updated sensor readings. The updated sensor readings are returned by the getTemperature()getHumidity(), and getPressure() functions we’ve created previously.

The readings are plain text and should be sent as a char, so we use the c_str() method.

server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/plain", getTemperature().c_str());
});
  
server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/plain", getHumidity().c_str());
});
  
server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/plain", getPressure().c_str());
});

In the end, we use the begin() method on the server object so that the server starts listening for incoming clients.

server.begin();

Because this is an asynchronous web server, all requests may be defined in setup(). Then, while the server is listening for incoming clients, you may add further code to the loop().

Uploading Code and Files

Save the Arduino sketch as “ESP8266_SPIFFS_Web_Server“, or download all project files here.

  • Go to Sketch > Show Sketch folder, and create a folder called data. Save the HTML and CSS files inside that folder.
  • In Tools Board, select the ESP8266 board you’re using.
  • Then, go to Tools Flash Size and select 4M (1M SPIFFS).
Select ESP8266 NodeMCU board with SPIFFS Arduino IDE

Finally, upload the files to your board. Go to Tools > ESP8266 Data Sketch Upload and wait for the uploaded files.

Upload Files to ESP8266 NodeMCU SPIFFS ESP8266 Sketch Data Upload

Then, in the Arduino IDE, press the upload button to send the code to the ESP8266.

Upload Code Arduino IDE to ESP8266 NodeMCU

When everything has been successfully uploaded, open the Serial Monitor at a 115200 baud rate. When you press the ESP8266 on-board RST button, the ESP8266 IP address should be printed.

ESP8266 NodeMCU IP address Serial Monitor Arduino IDE

Demonstration

Open a browser and type in the IP address of your ESP8266. The following web page should be shown:

ESP8266 Web Server using SPIFFS page HTML CSS Demonstration

To control the ESP8266 onboard LED, press the ON and OFF buttons. You may also see the most recent sensor readings. The sensor readings are automatically updated, so there is no need to refresh the web page.

Conclusion

Instead of needing to write all the code in the Arduino sketch, it is particularly useful to use the ESP8266 NodeMCU web server using SPIFFS to store HTML and CSS files to serve a client.

If you like ESP8266, you may also like:

We hope you find this tutorial useful. Thanks for reading.

Oh hi there It’s nice to meet you.

Sign up to receive awesome content in your inbox, every month.

We don’t spam! Read our privacy policy for more info.

Leave a Reply

Your email address will not be published. Required fields are marked *

ESP8266 Home Automation Projects

Leverage the power of this tiny WiFi chip to build exciting smart home projects