Introduction
In this first part of the chat application series of tutorials we will take care of creating the foundation of our app. At the end of this tutorial, you should have a very simple working prototype.
We will setup a HTTP server to run on the ESP32, using the async HTTP webserver library. For a getting started tutorial on the async HTTP server, please consult this post. On this HTTP server, we will define a websocket endpoint to which the chat clients (users) can connect. If you haven’t yet used the websockets feature on the async HTTP server, you can consult this post for a detailed explanation.
Whenever a client sends a chat message to the ESP32, the device will be responsible for broadcasting it back to all the websocket clients, so they can all display the user that sent the message and its content. For a detailed explanation on how to broadcast messages to all the websocket clients, please go here.
Besides implementing the ESP32 side, we will also write the HTML and JavaScript code for the initial version of our chat. The UI of our application will be very simple (we won’t take care of the CSS on this part) and will allow a user to specify his name, connect to the chat, send messages and disconnect.
To avoid enlarging this part too much, we won’t serve the frontend files from the ESP32 yet. Instead, when testing the code below, we will simply run it directly from a computer in the same network as the ESP32, which will allow to test the basic functionalities.
It is also important to mention that the websockets messages that will be exchanged between the clients and the server will be in JSON format. For now, the ESP32 won’t need to actually parse the messages but rather just broadcast whatever it receives. Naturally, this is a simplification to keep the code simple and, in a real application scenario, it would be a good ideia to actually parse the messages and check if all the necessary content is sent. Later, in part 4, we will need to do the JSON parsing to timestamp the messages.
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 ESP32 Chat Server Code
We will start by the library includes. We will need the WiFi.h, to connect the ESP32 to a WiFi network, and the ESPAsyncWebServer.h, to be able to setup a HTTP server that supports websocket connections.
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
After that we will define the credentials of the WiFi network on two global variables, so we can later connect the ESP32 to the network.
const char* ssid = "YourWiFiNetworkName";
const char* password = "YourWiFiNetworkPass";
Then, we will create an object of class AsyncWebServer, which will be used in the Arduino setup to configure our HTTP server. The constructor of this class receives as input the number of the port where the server will be listening for incoming requests. We will set it to port 80, which is the default HTTP port.
AsyncWebServer server(80);
We also need to create an object of class AsyncWebSocket, to configure the websocket endpoint we want to expose on our HTTP server. We pass as input of the constructor, as a string, this endpoint. Since we are developing a chat application, we will call it “/chat”.
AsyncWebSocket ws("/chat");
Moving on to the Arduino setup, we will start by opening a serial connection and then connecting the ESP32 to the WiFi network. After the connection is established, we will print the IP address assigned to the ESP32 on the network, since we will need to use it in our client code.
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println(WiFi.localIP());
Then we will assign a function to handle the websocket related events, such as new clients connecting, disconnecting or messages being received. We will call our function onWsEvent and analyze its implementation in greater detail below.
ws.onEvent(onWsEvent);
Next we will register the websocket endpoint on our HTTP server. We do this with a call to the addHandler method on our AsyncWebServer object, passing as input the address of our AsyncWebSocket object.
server.addHandler(&ws);
To finalize, we will call the begin method on our AsyncWebServer object, so it starts listening to incoming requests.
server.begin();
The full Arduino setup is available in the code snippet below.
void setup(){
Serial.begin(115200);
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.begin();
}
Since we are working with the async HTTP web server solution and we will react in response to message received events, there’s nothing we need to do on the Arduino main loop. Consequentely, we will leave it empty.
void loop(){}
To finalize, we will check the implementation of the onWsEvent function. Note that this function needs to follow the signature defined by the AwsEventHandler type. In our case, we will make use of the following input parameters:
- 3º input: an enumerated value of type AwsEventType, which indicates what was the websocket event that triggered the execution of the handling function. Note that, as can be seen in the enum value, there are different websocket events that can occur.
- 5º input: an array of data bytes which contains the payload of the message, in case the event corresponds to data received.
- 6º input: the length of the previous array.
void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
// Handle event
}
We will start by handling the client connect (WS_EVT_CONNECT enum value) and client disconnected (WS_EVT_DISCONNECT enum value) events. For these, we will simply print a message to the serial port, to inform that a client as either connected or disconnected. These messages are for debugging purposes.
if(type == WS_EVT_CONNECT){
Serial.println("Websocket client connection received");
} else if(type == WS_EVT_DISCONNECT){
Serial.println("Client disconnected");
}
In case we receive data (WS_EVT_DATA enum value), then we will broadcast that same data to all the connected clients. We do so with a call to the textAll method on our AsyncWebSocket object. As first input we pass the data buffer and as second we pass its length.
ws.textAll(data, len);
For debugging purposes, we will also print this same data to the serial port, so we can quickly identify if something is wrong while testing.
Serial.print("Data received: ");
for(int i=0; i < len; i++) {
Serial.print((char) data[i]);
}
Serial.println();
Note that, although this data is supposed to be a JSON payload, we are not doing any type of parsing but rather just broadcast it to all the connected clients. Naturally, on a real application scenario, it would be good idea to validate that the data is formatted as a JSON and all the required fields are present. Also, like already mentioned, in part 4 we will need to parse the data to be able to timestamp the message before broadcasting it back to all the clients.
The full callback function is available below.
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();
}
}
The complete code can be seen below.
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
const char* ssid = "YourWiFiNetworkName";
const char* password = "YourWiFiNetworkPass";
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);
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.begin();
}
void loop(){}
The client HTML / JavaScript code
We will start by the HTML layout and later cover the JS code. We will have two main sections: the head and the body. We will place the JavaScript script in the head section and the HTML elements that compose our interface in the body section.
<!DOCTYPE html>
<html>
<head>
<script type = "text/javascript">
<!-- JS code-->
</script>
</head>
<body>
<!-- Body elements-->
</body>
</html>
Our body will have four div elements, in the order below:
- A div with the elements to establish the connection: a text input to specify the name of the chat user and a button to start the connection.
- A div where the chat will be displayed. It will start empty but, as messages are received, new paragraph elements will be added.
- A div with the elements to send a new message to the chat: a text input to place the message and a button to send it.
- A div with the elements to disconnect from the chat: a button.
Like mentioned, the first div will have an input element of type text, where the user must input the name before establishing the connection. We will assign it an identifier (as we will do with most elements of our UI), so we can easily access it later from the JS code to get its content.
We are going to keep our UI code very simple, so we are not going to enforce any restrictions in the elements. Nonetheless, I recommend you to take a look at the documentation of the attributes of the input element, which allow to add restrictions such as a maximum or minimum length of the content.
<input type = "text" id = "nameText"></input>
After the input text, we need the connect button. Naturally, we will use a button element. For the onclick attribute of the button, we will specify the execution of a function called OpenWebsocket, which we will define later in the JavaScript code. This function will basically take care of establishing a connection to the server.
<button onclick = "OpenWebsocket()" id = "connectButton">CONNECT</button>
The first div is shown complete below.
<div>
<input type = "text" id = "nameText"></input>
<button onclick = "OpenWebsocket()" id = "connectButton">CONNECT</button>
</div>
The second div will be an empty div that we will populate once messages are received. We will assign it an identifier, so we can easily locate it to later add the received content.
<div id = "chatDiv">
</div>
The third div will be very similar to the first in terms of structure: it has a text input and a button. The text input is used for the user to put the message and the button to send it. Nonetheless, since we can only send messages after being connected to the server, both these elements will start on a disabled state, and only later will be enabled, after a successful connection to the ESP32 server.
In this case, when the button is clicked, a function called SendData will be invoked. This function, which will also be defined in the JS code, will take care of sending the message to the ESP32.
<div>
<input type = "text" disabled id = "inputText"></input>
<button onclick = "SendData()" disabled id = "sendButton">SEND</button>
</div>
The fourth and last div will have a button, to disconnect from the server. It will invoke a function called CloseWebsocket when clicked. Note that it will start disabled and it will only be enabled later, after a successful connection to the ESP32 websocket server.
<div>
<button onclick = "CloseWebsocket()" disabled id = "disconnectButton">DISCONNECT</button>
</div>
The full HTML part of the code can be seen below.
<!DOCTYPE html>
<html>
<head>
<script type = "text/javascript">
<!-- JS code-->
</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>
Now we will focus on the JavaScript code. First, we will define a globally scoped variable that will later hold the WebSocket object. For now we will initialize it with the null value.
var ws = null;
We will also define a variable to hold the name of the chat participant. This will be later assigned and we will keep it also with a value of null for now.
var name = null;
Next we will take care of defining the JS functions that we have previously used in some of the HTML elements that compose our UI. We will start by the OpenWebsocket function, which will be responsible for initializing the connection to the ESP32 websocket server.
function OpenWebsocket() {
// Implementation
}
We will start by obtaining the text input element where the user is supposed to introduce the name to be used in the chat. To do so, we can simply call the getElementById method on the document object. As input of the method we need to pass the identifier of the element we want to obtain. In our case, we assigned the id “nameText“, which is the one we will use.
const nameTextElement = document.getElementById("nameText");
Now that we have the element, we will assign its value to the name variable we defined earlier. We will use this name later when sending messages to the server. Note that we are not doing any validation to confirm that there is something typed in the text input, but in a real application scenario we should do that. Other approach would be to only enable the connect button after a name is typed.
name = nameTextElement.value;
After obtaining the name, we will clean the content of the text input. Once again, we are doing this immediately under the assumption that the connection to the ESP32 will be successful. From a usability perspective, on a real scenario, it would be better to only clean this input after the connection was successful, and leave it in case of error, so the user could retry without re-typing the name.
nameTextElement.value = '';
Next we will create a WebSocket object, passing as input of the constructor the websocket endpoint of the ESP32 server.
The format of the URL is shown below. Note that you should change #yourEspIp# by the IP of your ESP32 (recall from the Arduino code that this IP address will be printed to the serial port after it connects to the WiFi network).
ws://#yourEspIp#/chat
For illustration purposes, in the code segment below, I’m passing the URL with the IP address assigned to my ESP32 on my WiFi network. Yours will most likely be different.
ws = new WebSocket("ws://192.168.1.75/chat");
The websocket API works based on events, to which we need to assign handling functions. We will start by assigning a handling function for the connection established event. So, the handling function for this event must be assigned to the onopen property.
Since this function is triggered once we are connected to the server, we will enable all the UI elements that allow to send messages and also to disconnect from the server. On the other hand, we will disable the UI elements related with the connection.
Like we have done before, we can access the elements with a call to the getElementById method, passing as input a string with the identifier of the element. After that, we only need to set the disabled attribute to the Boolean value we intend: true if we want to disable the element and false otherwise.
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;
};
The handling of the connection closed event (onclose property) will be similar, except that we enable the connection elements and we disable the messaging elements. Additionally, we are going to clean any content from the chat div. Since we will add each message as a paragraph inside this div, we can clean the div simply by setting its innerHTML property to an empty string.
We are cleaning the chat div for simplicity. In a real application scenario, the history of the messages is usually preserved.
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 = '';
};
After this, we will take care of defining the handling function to treat a received message (onmessage property). This handling function will receive as input a MessageEvent when a new websocket message is received (in the signature of the handling function we will call it “event“. This MessageEvent object has a property called data that contains the actual websocket message.
ws.onmessage = function(event) {
// Handle the received message
}
As we will see later when implementing the function to send messages to the server, our messages will be formatted as a JSON object that contains both the name of the sender and the actual chat message. The name will be on a property called “name” and the message in a property called “msg“.
So, the first thing we will do when we receive new data is to parse it to a JavaScript object. For a detailed tutorial on how to parse JSON payloads to JS objects, please consult this post. In short, we simply need to call the parse method on the JSON object (the JSON object is a standard built-in object).
So, we will pass the message data as input of the parse method and, as output, we will obtain the parsed object.
const receivedObj = JSON.parse(event.data);
Then we will use the received data to build the text content of the new entry in the chat. We will simply create a string that has the name of the user and the chat message. Recall that we have that information in the “name” and “msg” fields of our parsed object.
Note that we will be using a template string to build the final string from the values held in our object.
const textToDisplay = `${receivedObj.name}: ${receivedObj.msg}`;
Next we will create a new element with a call to the createElement method on the document object. As input, we pass the name of the tag of the element and, as output, we receive the created element. Since we want to add the messages to the chat div as paragraphs, the tag name is “p“.
const newChatEntryElement = document.createElement('p');
Then, we will set the textContent property of our paragraph to the string we just built.
newChatEntryElement.textContent = textToDisplay;
To finish this handling function, we will append this new paragraph to the list of elements of the chat div. We first get the chat div by its identifier and then call the appendChild method, passing as input the paragraph.
const chatDiv = document.getElementById("chatDiv");
chatDiv.appendChild(newChatEntryElement);
The full handling function is shown below.
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);
};
Recall that we are defining all these websocket event handling functions still in the scope of the OpenWebsocket function. As such, the full function is displayed in the snippet below.
function OpenWebsocket() {
const nameTextElement = document.getElementById("nameText");
name = nameTextElement.value;
nameTextElement.value = '';
ws = new WebSocket("ws://192.168.1.75/chat");
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);
};
}
After defining the OpenWebsocket function, we will now take care of the CloseWebsocket function, which will be much simpler. Basically, we only need to call the close method on our WebSocket object.
Note that we are not doing anything else because we already defined an event handling function that will take care of the event that is triggered after a successful closing of the websocket connection. Here we are basically calling the function that will start the close process, which will then trigger the event.
function CloseWebsocket(){
ws.close();
}
To finalize our JS code, we still need to analyze the SendData function. This function takes care of sending the messsage to the ESP32 server when the client clicks the send button.
function SendData(){
// Send message to ESP32 server
}
We start by obtaining the element where the user will write the message.
const inputTextElement = document.getElementById("inputText");
Then we will obtain the text from this element and clean it afterwards. Once again, take in consideration that we are not validating if the input is empty, but we should do so in a real scenario.
const msg = inputTextElement.value;
inputTextElement.value = '';
Like mentioned before, we are going to send the information to the server in JSON format. We will send both the name of the user that wrote the message (we have it stored in the name variable) and the chat text message we just obtained. As such, we will start by creating a JavaScript object with these two fields.
const objToSend = {
msg: msg,
name: name
}
Now we will serialize this object to a JSON string, so we can send it using our websocket connection. For a detailed tutorial on how to serialize a JS object to JSON, string please go here. In short, we once again take advantage of the JSON built-in object, using the stringify method. This method receives as input the JS object we want to serialize and returns as output the serialized JSON string.
const serializedObj = JSON.stringify(objToSend);
Finally, we will send the message with a call to the send method on our WebSocket object.
ws.send(serializedObj);
The complete SendData function can be seen below.
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);
}
As a recap, the whole JavaScript code of our client can be obtained in the snippet below.
var ws = null;
var name = null;
function OpenWebsocket() {
const nameTextElement = document.getElementById("nameText");
name = nameTextElement.value;
nameTextElement.value = '';
ws = new WebSocket("ws://192.168.1.75/chat");
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);
}
The final client code (JavaScript and HTML) can be seen below.
<!DOCTYPE html>
<html>
<head>
<script type = "text/javascript">
var ws = null;
var name = null;
function OpenWebsocket() {
const nameTextElement = document.getElementById("nameText");
name = nameTextElement.value;
nameTextElement.value = '';
ws = new WebSocket("ws://192.168.1.75/chat");
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);
}
</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>
Testing the code
To test the end-to-end system, first compile and upload the ESP32 code to your device, using the Arduino IDE or platformio (or another tool of your choice). Don’t forget to replace the WiFi credential placeholders by the actual credentials of your network.
Once the procedure is finished, open a serial monitor tool of your choice, so you can check the outputs from the ESP32 program. After a successful connection to the WiFi network, the local IP address assigned to the device should get printed. Use that IP address in the HTML/JavaScript code, on the URL passed as input of the WebSocket class constructor.
Once you have the HTML/JS code ready, simply save the file as .html in a computer connected to the same WiFi network that the ESP32 is connected to. Then, open at least two instances of the file in a web browser of your choice.
In each one of the tabs put a different name on the UI and click the “Connect” button, like indicated in figure 1.
After you click the “Connect” button, if everything is successful, the elements of the first div should get disabled and the remaining ones should get enabled. After that, you should be able to put messages in the text input and click the “Send” button.
If you do so, both users should see the sent messages, like illustrated in figure 2. Note that the chat div, which was previously empty, starts now displaying the newly received messages.
Note that this will work if we add more users to the chat. For example, if we open a third instance of the file and connect a third user, that user won’t see the old messages (we don’t have any chat history functionality) but will be notified of the new ones, like shown in figure 3.
Now if we click the “Disconnect” button on one of these users, that user will get disconnected from the chat and the chat div for that user will be cleaned. After disconnecting one of the clients, the others remain connected and can keep chatting.