In this article, we'll teach you how to build an ESP32 web server that serves HTML and CSS files saved on the filesystem. We'll generate separate HTML and CSS files rather than having to write the HTML and CSS text inside the Arduino code.
The web server we'll build will control an ESP32 output for demonstration reasons, but it's easily adaptable for other uses like displaying sensor readings.
Recommended reading: ESP8266 Web Server using SPIFFS
ESP32 Filesystem Uploader Plugin
You must have the ESP32 Filesystem Uploader plugin installed in your Arduino IDE to follow this tutorial. To install it first, if you haven't already, follow the instructions in the next tutorial:
- Install ESP32 Board in Arduino IDE in less than 1 minute
- How to Install ESP32 Boards in Arduino IDE 2.0
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 you’ll build controls an LED connected to ESP32 GPIO 2. This is the ESP32 onboard 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.
To demonstrate how everything works, the following figure shows a simplified diagram.
- The ESP32 runs web server code based on the ESPAsyncWebServer library.
- The HTML and CSS files are stored on the ESP32 SPIFFS (Serial Peripheral Interface Flash File System).
- When you request a specific URL using your browser, the ESP32 responds with the requested files.
- When you click the ON button, you are redirected to the root URL followed by /on, and the LED is turned on.
- When you click the OFF button, you are redirected to the root URL followed by /off, and the LED is turned off.
- On the web page, there is a placeholder for the GPIO state. The placeholder for the GPIO state is written directly in the HTML file between % signs, for example, %STATE%.
Installing Libraries
In the majority of our projects, the HTML and CSS files for the web server have typically been produced as strings right on the Arduino sketch You may write the HTML and CSS separately using SPIFFS and store them on the ESP32 filesystem.
The ESPAsyncWebServer library is one of the simplest ways to create a web server using files from the filesystem. On its GitHub website, the ESPAsyncWebServer library is well described. Visit the following website for further details about that library:
Installing the ESPAsyncWebServer library
Install the ESPAsyncWebServer library by following 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.
Installing the Async TCP Library for ESP32
The ESPAsyncWebServer library requires the AsyncTCP library to work. To install that library, follow the next steps:
- Click here to download the AsyncTCP library. You should have a .zip folder in your Downloads folder.
- Unzip the .zip folder, and you should get the AsyncTCP-master folder.
- Rename your folder from
AsyncTCP-masterto AsyncTCP. - Move the AsyncTCP folder to your Arduino IDE installation libraries folder.
- Finally, re-open your Arduino IDE.
Organizing your Files
Three different files are required to construct the web server. The HTML file, CSS file, and Arduino sketch As shown below, the HTML and CSS files have to be saved inside the Arduino sketch folder in the data folder:
Creating the HTML File
This project's HTML is quite simple. Just a heading, a paragraph displaying the GPIO status, and two buttons are needed for the web page.
Alternatively, you can download all the project files here and create an index.html file with the following content:
<!DOCTYPE html>
<html>
<head>
<title>ESP32 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>ESP32 Web Server</h1>
<p>GPIO state: <strong> %STATE%</strong></p>
<p><a href="/on"><button class="button">ON</button></a></p>
<p><a href="/off"><button class="button button2">OFF</button></a></p>
</body>
</html>
We need to reference the CSS file in the HTML text since we're using CSS and HTML in distinct files. It is necessary to insert the following line between the head> and /head> tags:
<link rel="stylesheet" type="text/css" href="style.css">
The link> tag tells the HTML file that you are using an external style sheet to format the appearance of the page. In this situation, the external file is a stylesheet—the CSS file—which will be used to change the look of the page; the rel attribute specifies the kind of external file.
You can see that you're using a CSS file for the styling by looking at the type attribute, which is set to “text/css“. Since the CSS and HTML files will be in the same folder, you just need to reference the file name: style.css. The href attribute identifies the file location.
We write the first heading of our web page on the line that follows. An “ESP32 Web Server” is what we have here. The heading may be changed to whatever text you like.
<h1>ESP32 Web Server</h1>
Following the GPIO state, we add a paragraph with the text “GPIO state:” We may add a placeholder that will be replaced with any value we set on the Arduino sketch since the GPIO state varies depending on the state of the GPIO.
We use % signs to add placeholders. We may, for instance, use %STATE%
to create a placeholder for the state.
<p>GPIO state: <strong>%STATE%</strong></p>
The Arduino sketch assigns a value to the placeholder for STATE
.
We then create an ON button and an OFF button. When you click on a button, we follow the web page to root, followed by /on
URL. You are redirected to the /off
URL when you click the off button.
<p><a href="/on"><button class="button">ON</button></a></p>
<p><a href="/off"><button class="button button2">OFF</button></a></p>
Creating the CSS file
Create the following CSS style.css file, or download the whole project files here:
html {
font-family: Helvetica;
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;
}
This is just a simple CSS file to orient the page and set the text size, style, and color of the buttons. CSS won't be described in detail here. The W3Schools website is a good place to learn about CSS.
Arduino Sketch
Download all the project files here, or copy the following code to the Arduino IDE. Then, to make it work, you need to type your network credentials (SSID and password).
/*********
LEDEdit PRO
Complete project details at https://lededitpro.com
*********/
// Import required libraries
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "SPIFFS.h"
// 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);
// 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;
}
return String();
}
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
// Initialize SPIFFS
if(!SPIFFS.begin(true)){
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);
});
// Start server
server.begin();
}
void loop(){
}
How the Code Works
First, include the necessary libraries:
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "SPIFFS.h"
The following variables need your network credentials typed in:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Next, create a variable named ledPin
that refers to GPIO 2 and a String variable named ledState
that holds the led state.
const int ledPin = 2;
String ledState;
Create an AsynWebServer
object called server
that is listening on port 80.
AsyncWebServer server(80);
processor()
The processor()
function will create an attribute for the placeholder value we've added to 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;
}
return String();
}
The STATE
we created in the HTML file is the first thing this function checks to see whether the placeholder is that.
if(var == "STATE"){
If so, we set the ledstate
variable to either ON or OFF according to the LED state.
if(digitalRead(ledPin)){
ledState = "ON";
}
else{
ledState = "OFF";
}
Finally, we return the ledState
variable. This replaces the placeholder with the ledState
string value.
return ledState;
setup()
In the setup()
, start by initializing the Serial Monitor and setting the GPIO as an output.
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
Initialize SPIFFS:
if(!SPIFFS.begin(true)){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
Wi-Fi connection
Connect to Wi-Fi and print the ESP32 IP address:
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println(WiFi.localIP());
Async Web Server
We can set the routes where the server will be listening for incoming HTTP requests and execute functions when a request is received on those routes using the ESPAsyncWebServer library. Use the server object's on()
method as follows to do that:
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 whenever it receives a request on the root “/” URL. Since the processor is the send()
function's final input, we may replace the placeholder with whatever value we like—in this example, ledState
.
The client will request the CSS file since we have referenced it in the HTML file. When this happens, the CSS file is sent to the client.
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/style.css","text/css");
});
Finally, you'll need to specify what happens on the /on
and /off
routes. When a request is made through certain routes, the LED is 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 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 other code to the loop()
.
Uploading Code and Files
You can either download all the project files here or save the code as Async_ESP32_Web_Server. Create a folder named “data” by selecting Sketch > Show Sketch Folder. The HTML and CSS files should be saved in that folder.
The code should then be uploaded to your ESP32 board. Be sure you choose the right board and COM port. Also, make sure your network credentials are included in the code.
You need to upload the files after uploading the code. Wait for the upload of the files by going to Tools > ESP32 Data Sketch Upload.
Open the Serial Monitor at a baud rate of 115200 after everything has been successfully uploaded. When you press the ESP32 “ENABLE” button, the ESP32 IP address should print.
Demonstration
Type the ESP32 IP address into your browser once it's open. To control the on-board LED of the ESP32, press the ON and OFF buttons. Additionally, make sure the GPIO state is being updated properly.
Conclusion
Instead of needing to write all the code inside the Arduino sketch, the SPI Flash File System (SPIFFS) is particularly useful to store HTML and CSS files to serve to a client.
By running a specific function in response to a specific request, the ESPAsyncWebServer library enables you to create a web server. Additionally, placeholders that may be replaced with variables, such as sensor readings or GPIO states, can be included in the HTML file.
If you like ESP32, you may also like:
- Send Email from ESP32 via SMTP Server (Arduino IDE)
- ESP32 with WiFiMulti: Connect to the Strongest Wi-Fi Network
- Use ESP32 with VS Code & Platformio IDE to Upload Files to Filesystem (SPIFFS)
- How to Install ESP32 Boards in Arduino IDE 2.0
We hope you find this tutorial useful. Thanks for reading.