In this esp32 tutorial we will check how to setup a HTTP web server on the ESP32, which will have a websocket endpoint and will serve a HTML page. The HTML page will run a simple JavaScript application that will connect to the server using websockets and periodically receive simulated temperature measurements from the server.
For simplicity, we will assume that only one client can be connected at most at each time, so we don’t need to deal with multiple connections.
We will also simplify the periodic sending of data to the client by taking advantage of the Arduino main loop and some delays. Nonetheless, a more robust and scalable implementation can be achieved using timer interrutps and semaphores to synchronize with a dedicated FreeRTOS task responsible for handling the sending of data.
In order to avoid dealing with big code strings and escaping characters, we will serve the HTML file from the ESP32 SPIFFS file system. For a tutorial on how to serve HTML from the file system, please check here.
We will be assuming the use of the Arduino IDE plugin that allows to upload data to the ESP32 SPIFFS file system. You can check in this tutorial how to use it. I’m assuming the HTML file that will be served is called ws.html, which means its full path on the ESP32 file system will be “/ws.html“. You can use other name if you want, as long as you set the correct path when developing the Arduino core, which we will analyze in detail below.
Also, to keep the HTML code simple, please take in consideration that we won’t be following all the best practices. Naturally, in a final application, you should take them in consideration.
The tests from this tutorial were performed using a DFRobot’s ESP32 module integrated in a ESP32 development board.
Our code will have two main sections: the head, where we will place the JavaScript code, and the body, which will have a HTML element to display the measurements.
In terms of JavaScript, we will start our code by instantiating an object of class WebSocket, which receives as input the complete websocket endpoint from our server.
If you don’t know the IP address of your ESP32 on your network, please run the Arduino code below and use the IP address that gets printed to the serial port when the device finishes connecting to the WiFi network.
If you want a more optimized approach and don’t want to depend on a hardcoded IP address, you can explore the template processing features of the HTTP async web server libraries in order to set this value at run time, when serving the page. Alternatively, you can use the mDNS features for domain name resolution.
Additionally, as can be seen in the code below, we are assuming that the websocket endpoint will be “/ws”, as we will configure later in the Arduino code. From this point onward, we will deal with websocket events, which means configuring callback functions that will handle those events.
var ws = new WebSocket("ws://192.168.1.78/ws");
So, now that we have initialized the websocket, we need to setup a handling function for the connection established event. In this example, we will simply open an alert message so the user knows that the connection to the server was completed.
We setup the handling function by assigning a function to the onopen property of our WebSocket object, as shown below. Then, in the implementation of the handling function, we display our message to the user using the alert method of the window object, passing as input the message to display.
ws.onopen = function() { window.alert("Connected"); };
Finally, we will need to setup the handling function that will be executed when a message received event occurs. We set this handling function by assigning a function to the onmessage property of the WebSocket object.
Note that this handling function receives as input a parameter that we will use to access the data. We will call this parameter evt.
Inside the handling function, we will simply update the text of the HTML element that will show to the user the last temperature received. We are assuming there will be an element with an ID equal do “display“, as we will see in the final HTML code.
So, we use the getElementById method of the document object and update its inner HTML with the new measurement.
As mentioned, we will have access to a parameter we called evt, which is passed to our handling function. This will be an object of class MessageEvent, which has a property called data that contains the data sent by the server.
We will use the value received to concatenate with some strings and build the final text to display to the user.
ws.onmessage = function(evt) { document.getElementById("display").innerHTML = "temperature: " + evt.data + " C"; };
You can check below the full HTML code containing the JavaScript and the mentioned element with ID equal to “display”. It will be a simple paragraph since we are not focusing on design. That paragraph will start with a default “not connected” message, to be later substituted by the temperature message.
<!DOCTYPE html> <html> <head> <script type = "text/javascript"> var ws = new WebSocket("ws://192.168.1.78/ws"); ws.onopen = function() { window.alert("Connected"); }; ws.onmessage = function(evt) { document.getElementById("display").innerHTML = "temperature: " + evt.data + " C"; }; </script> </head> <body> <div> <p id = "display">Not connected</p> </div> </body> </html>
After finishing this code, we should upload it to the ESP32 file system using the Arduino IDE plugin, as already mentioned in the introductory section.
As usual, we will start the code by the needed library includes. We need the WiFi.h library to be able to connect the ESP32 to a WiFi network, the ESPAsyncWebServer.h to setup the HTTP server and the SPIFFS.h to be able to serve files from the file system (our HTML will be stored in the ESP32 SPIFFS file system).
We will also need the credentials of the WiFi network, more precisely the network name and password, so the ESP32 can connect to it.
In order to setup the server and the websocket endpoint, we will need an object of class AsyncWebServer and AsyncWebSocket, respectively.
As covered in the previous tutorials, the constructor of the AsyncWebServer class receives as input the port where the server will be listening and the constructor of the AsyncWebSocket class receives a string with the websocket endpoint. So, accordingly to the code below, our server will be listening on port 80 and we will have a websocket endpoint on “/ws“.
#include "WiFi.h" #include "SPIFFS.h" #include "ESPAsyncWebServer.h" const char* ssid = "yourNetwornName"; const char* password = "yourPassword"; AsyncWebServer server(80); AsyncWebSocket ws("/ws");
Additionally, since we are going to need to access the client object to periodically send it some data, we will declare a pointer to an object of class AsyncWebSocketClient.
As explained in more detail on this previous tutorial, the websocket events handling function will receive a pointer to an object of this class when an event occurs. In order to send data back to the client, we need to use that object pointer.
So, what we will do later on the websocket handling function is storing that pointer in this global variable, so we can send data to the client from outside the event handling function.
For now, when the program starts, we know that no client is connected, so we will initialize the global pointer explicitly as NULL. As long as it is NULL, we know that no client is connected and that we should not try to send data.
AsyncWebSocketClient * globalClient = NULL;
In the setup function we will take care of all the initialization that needs to be performed before the web server starts working properly. We start by opening a serial connection, initializing the SPIFFS file system and then connecting the ESP32 to a WiFi network.
Serial.begin(115200); if(!SPIFFS.begin()){ Serial.println("An Error has occurred while mounting SPIFFS"); return; } WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println(WiFi.localIP());
After that, we will bind the websocket endpoint to the corresponding handling function (we will analyze the function implementation below) and register the websocket object on the HTTP web server.
ws.onEvent(onWsEvent); server.addHandler(&ws);
Then, we will declare the route that will be serving the HTML with the websocket code. We will call the endpoint “/html” and listen only to incoming HTTP GET requests.
As explained in detail here, in order to serve the HTML back to the client as response to the request, we need to call the send method AsyncWebServerRequest object, passing as input the SPIFFS variable (which will be used under the hood to interact with the file system), the complete path to the HTML file on the ESP32 file system (as we have seen in the introductory section, the file was named “ws.html” and will be on the root folder) and the content-type (it will be “text/html” since we are going to serve a HTML page).
server.on("/html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/ws.html", "text/html"); });
To finalize, we need to call the begin method on our server object, so it starts listening to HTTP requests. The final setup function is shown below and already includes this method call.
void setup(){ Serial.begin(115200); if(!SPIFFS.begin()){ Serial.println("An Error has occurred while mounting SPIFFS"); return; } WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println(WiFi.localIP()); ws.onEvent(onWsEvent); server.addHandler(&ws); server.on("/html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/ws.html", "text/html"); }); server.begin(); }
In this tutorial, we assume that the ESP32 is not going to receive any data but rather sending it periodically to the client. So, we will only listen to the client connection and disconnection events. For simplicity, we will assume that no more than one client will be connected at each time.
So, our handling function will be very simple. When a connection event is detected, we will receive as one of the inputs of the handling function the pointer to the client object (it will be an object of class AsyncWebSocketClient, as already mentioned).
So, we will assign that value to the global AsyncWebSocketClient pointer we have declared previously, so we can later send data to the client, outside the scope of this websocket event callback function.
In case we detect a disconnection event, then we should no longer try to send data to the client, so we will set the global pointer back to NULL.
The full handling function can be seen below. It includes some extra prints so we can get a message in the Arduino serial monitor when the events occur, making it easier to debug eventual problems.
void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ if(type == WS_EVT_CONNECT){ Serial.println("Websocket client connection received"); globalClient = client; } else if(type == WS_EVT_DISCONNECT){ Serial.println("Websocket client connection finished"); globalClient = NULL; } }
If you want to know more about the signature that this handling function needs to follow, please check this previous tutorial.
We will make use of the Arduino main loop to send data periodically to the client, when a connection exists. Please note that this is not the most optimized approach since we are going to periodically poll the global pointer to check if a client is available. Also, we are going use the Arduino delay function to wait between the iterations of the loop.
For a more optimized alternative, we could have used timer interrupts, semaphores and a dedicated FreeRTOS task. Nonetheless, in order to focus on the websocket communication, we are following the simplest approach.
So, in our main loop, we will check if the global client pointer is different from NULL. Additionally, as a safeguard, we will also call the status method and check if the client is connected by comparing the returned value with the WS_CONNECTED enumerated value.
if(globalClient != NULL && globalClient->status() == WS_CONNECTED){ // Sending data to client }
If both conditions are met, it means the client is connected and we can send data to it. Since we are not using any real sensor and only simulating the interaction, we will returning a random number between 0 and 20 to the client, simulating a possible temperature reading. Please check here more about random number generation on the ESP32.
To send the data to the client, we need to convert it to a string and then call the text method on our AsyncWebSocketClient object. Since we have a pointer to the object, we need to use the -> operator.
String randomNumber = String(random(0,20)); globalClient->text(randomNumber);
After that, we will do a 4 seconds delay. You can try to use other values if you want to change the refreshing rate in the HTML page. You can check the full loop function below.
void loop(){ if(globalClient != NULL && globalClient->status() == WS_CONNECTED){ String randomNumber = String(random(0,20)); globalClient->text(randomNumber); } delay(4000); }
The final Arduino code can be seen below.
#include "WiFi.h" #include "SPIFFS.h" #include "ESPAsyncWebServer.h" const char* ssid = "yourNetworkName"; const char* password = "yourPassword"; AsyncWebServer server(80); AsyncWebSocket ws("/ws"); AsyncWebSocketClient * globalClient = NULL; void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ if(type == WS_EVT_CONNECT){ Serial.println("Websocket client connection received"); globalClient = client; } else if(type == WS_EVT_DISCONNECT){ Serial.println("Websocket client connection finished"); globalClient = NULL; } } void setup(){ Serial.begin(115200); if(!SPIFFS.begin()){ Serial.println("An Error has occurred while mounting SPIFFS"); return; } WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println(WiFi.localIP()); ws.onEvent(onWsEvent); server.addHandler(&ws); server.on("/html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/ws.html", "text/html"); }); server.begin(); } void loop(){ if(globalClient != NULL && globalClient->status() == WS_CONNECTED){ String randomNumber = String(random(0,20)); globalClient->text(randomNumber); } delay(4000); }
Assuming that you have already uploaded the HTML file to the ESP32 file system, simply compile and upload the Arduino code to your device.
Once it finishes, open the Arduino IDE serial monitor and wait for the connection to the WiFi network. When it is done, copy the IP address that gets printed. Then, open a web browser of your choice and write the following on the address bar, changing #yourIP# for the IP you have just copied
http://#yourIP#/html
You should get to the HTML page we have been developing and after a while, it should show an alert indicating the connection to the server was established. After that, as shown in figure 1, it should periodically update the temperature with the random values returned by the server.
Figure 1 – HTML page with the temperature messages.