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.
- 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.
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:
- Click here to download the ESPAsyncWebServer library. You should have a .zip folder in your Downloads folder.
- Unzip the .zip folder, and you should get the ESPAsyncWebServer-master folder.
- Rename your folder from
ESPAsyncWebServer-masterto ESPAsyncWebServer. - 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:
- Click here to download the ESPAsyncTCP library. You should have a .zip folder in your Downloads folder.
- Unzip the .zip folder, and you should get the ESPAsyncTCP-master folder.
- Rename your folder from
ESPAsyncTCP-masterto ESPAsyncTCP. - Move the ESPAsyncTCP folder to your Arduino IDE installation library folder.
- 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
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.
BME280 | ESP8266 |
Vin | 3.3V |
GND | GND |
SCL | GPIO 5 |
SDA | GPIO 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:
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).
Finally, upload the files to your board. Go to Tools > ESP8266 Data Sketch Upload and wait for the uploaded files.
Then, in the Arduino IDE, press the upload button to send the code to the ESP8266.
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.
Demonstration
Open a browser and type in the IP address of your ESP8266. The following web page should be shown:
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:
- Install ESP8266 SPIFFS Filesystem Uploader in Arduino IDE
- Install ESP8266 LittleFS Filesystem Uploader in Arduino IDE
- ESP8266 NodeMCU Static/Fixed IP Address – Working & Testing
- How to Wire an OLED Display with ESP8266 NodeMCU
We hope you find this tutorial useful. Thanks for reading.