Introduction
In this second part of our chat application tutorial, we will start serving the application frontend files from the ESP32. We will do this by storing the files on the ESP32 SPIFFS file system and adding endpoints to the HTTP server to serve them.
Although in the previous tutorial we had just a single file with all the HTML and JavaScript of our application, we will now split it in two files: one containing the HTML and another containing the JavaScript. This process was already covered in greater detail here.
If we take a look at the original frontend code, we can clearly see that the HTML part is related with the content of the interface and the JavaScript part is related with its behavior. So, having a separation of the code in two different files makes our application cleaner and the development process easier.
Furthermore, if we were working in a more complex application, it was likely that we had multiple HTML pages that could reuse some parts of the JavaScript code. If that happens, replicating the same JavaScript code in all the HTML pages is definitely not a good ideia for maintenance.
With this in mind, we can simply split the JS and the HTML code and import the JS file from the HTML, like we are going to do below. Although we are not adding any CSS (style and appearance) yet, please keep in mind that if we did, it would also be a good idea to have it on a separated file. We will do that later in part 3.
You can consult the following tutorials for more detailed explanations on how to serve files from the ESP32:
- Serving HTML from the ESP32 file system: How to serve HTML files via HTTP from the ESP32 file system, using the Async HTTP web server.
- Serving JavaScript from the ESP32 file system: How to serve JS files from the ESP32 file system, using HTTP. It also explains how a HTML file can reference the JS file to be included.
The tests shown below were performed on a ESP32-E FireBeetle board from DFRobot. The Arduino core version used was 2.0.0 and the Arduino IDE version was 1.8.15, working on Windows 8.1. The code was also tested on Platformio.
The HTML code
Our HTML code will be pretty much the same as part 1, except that we are no longer having the JavaScript code directly here.
Like we have seen in the previous tutorial, the script tag can be used to embed JavaScript code, which was where we defined all the behavior of our application (websocket connection, enable and disable of elements, etc..). This time, instead of having all the JS code inside the script tag, we are going to import it from an external JavaScript file, using the src attribute.
Note that we should assign the URL of the file containing the JS code to this src attribute. This URL can either be absolute (pointing to another host where the JavaScript file is being server) or relative (pointing to the same host that is serving the HTML file). In our case, we are going with a relative URL, since the ESP32 will not only serve the HTML file but also the JavaScript file.
We will assume that the file is called chat.js and it will be served in a route with the same exact name. Consequently, the script tag should look like the following:
<head>
<script src="chat.js"></script>
</head>
Naturally, we will need to ensure that the ESP32 will serve the JS file on the “/chat.js” route, so it can be imported in this HTML file.
The rest of the HTML code should be exactly like the one we wrote in part 1. As a quick reminder, it has 4 divs:
- One for the connection elements (input text for the name and connect button).
- One that starts empty but will be where the messages of the chat will be added.
- One with the elements to send a new message (input text for the message and send button).
- One with the disconnect button.
The full code, which includes these 4 divs in the body, can be seen below.
<!DOCTYPE html>
<html>
<head>
<script src="chat.js"></script>
</head>
<body>
<div>
<input type = "text" id = "nameText"></input>
<button onclick = "OpenWebsocket()" id = "connectButton">CONNECT</button>
</div>
<div id = "chatDiv">
</div>
<div>
<input type = "text" disabled id = "inputText"></input>
<button onclick = "SendData()" disabled id = "sendButton">SEND</button>
</div>
<div>
<button onclick = "CloseWebsocket()" disabled id = "disconnectButton">DISCONNECT</button>
</div>
</body>
</html>
We will assume that this HTML file will be called “chat.html“.
The JavaScript code
Since we removed the JS code from the HTML, we need to place it in another file, so it can be imported. As already mentioned, we are assuming that the file is called “chat.js“.
Like for the HTML, this file will contain pretty much the same JavaScript code from the previous tutorial, with a small exception. On that tutorial, we were running the code from a computer, meaning that we had to know beforehand what was the IP address of the ESP32 in the network (recall that we had it hardcoded in the websocket URL).
Now, we are going to be serving the JS and the HTML file from the same ESP32 that is hosting the websocket endpoint. So, we can simply use the hostname property from the location object, which should allow us to access the host that served the file. In our case, since we are not using any domain name, this host will simply be the IP address of the ESP32.
Using this approach, we no longer have to hardcode the IP address of the ESP32 on the JS code. The portion of the code that is new is shown below (it executes inside the OpenWebsocket function).
const url = `ws://${location.hostname}/chat`;
ws = new WebSocket(url);
The full JavaScript code, already with this modification, is shown below.
var ws = null;
var name = null;
function OpenWebsocket() {
const nameTextElement = document.getElementById("nameText");
name = nameTextElement.value;
nameTextElement.value = '';
const url = `ws://${location.hostname}/chat`;
ws = new WebSocket(url);
ws.onopen = function() {
document.getElementById("inputText").disabled = false;
document.getElementById("sendButton").disabled = false;
document.getElementById("disconnectButton").disabled = false;
document.getElementById("connectButton").disabled = true;
document.getElementById("nameText").disabled = true;
};
ws.onclose = function() {
document.getElementById("inputText").disabled = true;
document.getElementById("sendButton").disabled = true;
document.getElementById("disconnectButton").disabled = true;
document.getElementById("connectButton").disabled = false;
document.getElementById("nameText").disabled = false;
document.getElementById("chatDiv").innerHTML = '';
};
ws.onmessage = function(event) {
const receivedObj = JSON.parse(event.data);
const textToDisplay = `${receivedObj.name}: ${receivedObj.msg}`;
const newChatEntryElement = document.createElement('p');
newChatEntryElement.textContent = textToDisplay;
const chatDiv = document.getElementById("chatDiv");
chatDiv.appendChild(newChatEntryElement);
};
}
function CloseWebsocket(){
ws.close();
}
function SendData(){
const inputTextElement = document.getElementById("inputText");
const msg = inputTextElement.value;
inputTextElement.value = '';
const objToSend = {
msg: msg,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
}
Uploading the code to the ESP32 file system
In order for the ESP32 to be able to access the previous two files (“chat.html” and “chat.js“), we need to upload them to the file system of the device. The easiest way to do so is using the Arduino IDE file system upload plugin. The usage of this plugin was detailed in this previous tutorial.
In short, we need to have an Arduino sketch opened in the Arduino IDE and access its location in the computer file system. Once we are on the sketch folder, we need to create a folder called “data“. Inside, we place the files we want to upload (in our case, the “chat.html” and the “chat.js” files, like shown below in figure 1).
After having the “data” folder ready, we simply need to go back to the Arduino IDE and, under the “Tools” menu, select “ESP32 Sketch Data Upload”. This should initiate the process to upload the file to the SPIFFS file system of the ESP32.
Note that the files will keep the same name in the ESP32 file system and, since they were located in the data folder, they will be on the root of the file system. Consequently, they are located on the “/chat.html” and “/chat.js” paths.
The ESP32 code
The ESP32 code will also not differ much from what we have covered in the previous tutorial. In this case, we will need to add two new endpoints to serve our HTML and JS files, and interact with the file system to obtain them.
Since we will need to interact with the file system of the ESP32 (SPIFFS), we will need to include the SPIFFS.h library, in addition to the includes we already had.
#include <SPIFFS.h>
Moving on to the Arduino setup function, we need to initialize the SPIFFS file system. Only after that we will be able to access its files. To perform the initialization, we simply need to call the begin method on the SPIFFS extern variable that gets available from the previously mentioned include.
Note that this method call will return a Boolean indicating if the initialization was correctly done (true) or not (false). We will use this value for error checking, since if it fails we cannot proceed with the execution of the rest of the code, as it wouldn’t work.
if(!SPIFFS.begin()){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
After the initialization of SPIFFS, we should now be able to access files in the file system. So, we will first configure a route that will serve the HTML of our application. This route will be called “/chat” and answer to HTTP GET requests.
server.on("/chat", HTTP_GET, [](AsyncWebServerRequest *request){
// function implementation
});
On its implementation, the route handling function will access to the “chat.html” file and serve it to the client. We can do so by calling the send method on the AsyncWebServerRequest object to which we receive a pointer as input of the route handling function. We pass as first input the SPIFFS extern variable, as second the path to the file in the SPIFFS file system (it should be “/chat.html“, since we uploaded it to the root of the file system), and as third the content-type (which should be “application/html“, so the client knows how to interpret its content).
request->send(SPIFFS, "/chat.html", "text/html");
The full code for the route configuration is shown below.
server.on("/chat", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/chat.html", "text/html");
});
We will also need to configure a route to serve the “chat.js” file. From the HTML code, we can recall that it expects this file to be served on the “/chat.js” endpoint. The implementation of the route handling function will be similar to the previous one, except that we are going to access the “/chat.js” file in the SPIFFS file system, and the content-type is “text/javascript“.
server.on("/chat.js", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/chat.js", "text/javascript");
});
The complete code with these changes is shown below.
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";
AsyncWebServer server(80);
AsyncWebSocket ws("/chat");
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");
} else if(type == WS_EVT_DISCONNECT){
Serial.println("Client disconnected");
} else if(type == WS_EVT_DATA){
ws.textAll(data, len);
Serial.print("Data received: ");
for(int i=0; i < len; i++) {
Serial.print((char) data[i]);
}
Serial.println();
}
}
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("/chat", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/chat.html", "text/html");
});
server.on("/chat.js", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/chat.js", "text/javascript");
});
server.begin();
}
void loop(){}
Testing the code
To test the system, simply compile and upload the Arduino code from the previous section to the ESP32. Once the procedure is finished, open a serial monitor tool of your choice, to obtain the outputs from the ESP32.
Once the connection to the WiFi network is finished, you should get the IP address assigned to the ESP32 on that network. Copy that address. Then, open a web browser of your choice in a computer connected to the same network and type the following, changing #yourEsp32Ip# by the IP address you have just copied:
http://#yourEsp32Ip#/chat
Like shown on figure 2, you should get exactly the same page from the previous part.
Then, to test everything is working correctly, simply open more tabs to the same URL and test the chat application like we have done before. If you have more devices connected to the network, you can also test that the chatting app works the same (ex: using a smartphone to talk with the computer).