ESP32: Chat Application (Part 4)

Introduction

In this part we will add the timestamping functionality, so we can add a timestamp to all the messages sent by users. Although messages are displayed in the UI in sequence, it is useful to have the actual time they were sent.

We will be doing the timestamping on the ESP32, to avoid depending on the clients’ clock. To do so, we will configure the system time on the ESP32 using the SNTP protocol and later access its value to timestamp the messages. For a detailed tutorial on this functionality, please consult here.

Additionally, we will need to parse the websocket JSON messages, since they originally contain no timestamp when the client sends them to the server. Then, after parsing, we add the new timestamp field, serialize the message back to text and broadcast to all the clients.

We will be using the Nlohmann/json library to take care of the deserialization and serialization of the messages. For a detailed tutorial on how to install and get started with this library, please go here.

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 JavaScript code

We will need to do a minor change to our JS code, in order to include the timestamp also as part of the message line in the chat div. We will need to change the following line of code, in the onmessage handler of the websocket:

newTag.textContent = receivedObj.name;

So, instead of just displaying the name, we are going to display the received timestamp and only after the name. All will still be on the same tag of the UI. Note that the field in the JSON should be called “timestamp“, as we will also see in the Arduino code.

newTag.textContent = `${receivedObj.timestamp} ${receivedObj.name}`;

As a refresher from the previous tutorials, the complete JavaScript code can be seen 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 newChatEntryElement = document.createElement('div');
		   const newTag = document.createElement('span');
		   const newMsg = document.createElement('span');				

		   newTag.textContent = `${receivedObj.timestamp} ${receivedObj.name}`;
		   newMsg.textContent = receivedObj.msg;

		   newTag.classList.add('chat-tag');

		   newChatEntryElement.appendChild(newTag);
		   newChatEntryElement.appendChild(newMsg);

		   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);
}

If you need a refresher on how to upload this new version of the “chat.js” file to the SPIFFS file system, please check part 2.

The Arduino code

We will start the Arduino code by the library includes:

  • WiFi.h: Connecting the ESP32 to the WiFi network.
  • ESPAsyncWebServer.h: Setting up the Async web server on the ESP32, with the websocket endpoint.
  • SPIFFS.h: Initializing the SPIFFS file system and serving the files needed for the chat application
  • json.hpp: Parsing the JSON objects sent by the clients, adding the timestamp and serializing it back to be broadcasted.

After this we will define two global variables to hold the network credentials (name and password).

const char* ssid = "yourNetworkName";
const char* password =  "yourNetworkPass";

We will also define a string with the time server URL that will be used by the ESP32 to retrieve the time. I’ll be using Google’s server, but you can check here a list of available public time servers if you want to choose another.

const char* ntpServer = "time.google.com";

To finalize the global variable definitions, we will create an AsyncWebServer object, which we will use to setup the HTTP server, and an AsyncWebSocket object, which we will use to configure the websocket endpoint for the chat application.

AsyncWebServer server(80);
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, using the previously defined credentials.

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 this we will take care of defining the time zone of the ESP32 and then set the SNTP server. We do this with a call to the configTime function, which receives the following arguments:

  • GMT offset, in seconds. The Greenwich Mean Time (GMT) offset corresponds to the time difference between a given zone and the Greenwich Mean Time. You can read more about GMT here.
  • Daylight Saving Time offset, in seconds. Daylight Saving Time (DST) is the practice of advancing clocks, typically one hour, during warmer months, so that darkness falls at a later clock time.
  • NTP server address.

In my case, since I’m located in Portugal, I’m in GMT (also called GMT+0). Consequently, I will pass the value 0 as first argument of the configTime function. You can check here a list of countries and find your time zone. Don’t forget the value must be converted to seconds.

Regarding the second argument, since Portugal has a DST period, I should pass a value of 3600 seconds. You can check also here the countries that have DST periods.

For the third and last argument, we will pass the global variable we have defined before, which contains the URL for the server.

configTime(0, 3600, ntpServer);

To finish the Arduino setup, we will configure the HTTP server endpoints, to serve the 3 files (JavaScript, HTML and CSS), and to register the Websocket endpoint. The complete Arduino setup, already with all these configurations, can be seen below.

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());
 
  configTime(0, 3600, ntpServer);

  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.on("/chat.css", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/chat.css", "text/css");
  });

  server.begin();
}

We will finalize by analyzing the Websocket event handling function. We will focus our attention on the message received handler, since it is on this event that we will need to parse the received message to a json object, add the timestamp and serialize it back to a string.

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){
 
    // Process received data and broadcast result to all chat clients
  }
}

We will start by declaring a struct of type tm.

struct tm time;

Then we will call the getLocalTime function. As input, we will pass the address of our previously declared struct. The getLocalTime function will take care of populating the tm struct with the time information. As output, this function returns a Boolean indicating if it was possible to retrieve the time information (true) or not (false). We will use the returning value for error checking and print a message in case of error.

if(!getLocalTime(&time)){
   Serial.println("Could not obtain time info");
   return;
}

Now that we have our tm struct populated, we will use it to create a string with a user readable format that can be used directly in the frontend. Note that we could have delegated the formatting of the timestamp to the frontend instead, but I’ve opted to do it here. We will make use of the strftime function to format the contents of the tm struct accordingly to a specification and write the result in a string. You can check in detail how to use the strftime function here. We will use only the hour, minute and second in our timestamp.

char buffer[80];
strftime(buffer, sizeof(buffer), "%H:%M:%S", &time);

After that, we will parse the content received on the websocket to a json object. We do so with a call to the parse static method from the nlohmann/json library.

nlohmann::json obj = nlohmann::json::parse(data, data+len);

After that, we will add a new field called “timestamp” to the JSON object. We will assign our string with the formatted time to this field.

obj["timestamp"] = buffer;

Finally, we will serialize the object back to a string and broadcast it to all the websocket clients.

std::string serializedObject = obj.dump();

ws.textAll(serializedObject.c_str(), serializedObject.length());

The complete event handling function can be seen 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){
 
    struct tm time;
   
    if(!getLocalTime(&time)){
      Serial.println("Could not obtain time info");
      return;
    }

    char buffer[80];
    strftime(buffer, sizeof(buffer), "%H:%M:%S", &time);

    nlohmann::json obj = nlohmann::json::parse(data, data+len);
    obj["timestamp"] = buffer;

    std::string serializedObject = obj.dump();

    ws.textAll(serializedObject.c_str(), serializedObject.length());
  }
}

The complete code can be seen below.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <json.hpp>

const char* ssid = "yourNetworkName";
const char* password =  "yourNetworkPass";

const char* ntpServer = "time.google.com";

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){
 
    struct tm time;
   
    if(!getLocalTime(&time)){
      Serial.println("Could not obtain time info");
      return;
    }

    char buffer[80];
    strftime(buffer, sizeof(buffer), "%H:%M:%S", &time);

    nlohmann::json obj = nlohmann::json::parse(data, data+len);
    obj["timestamp"] = buffer;

    std::string serializedObject = obj.dump();

    ws.textAll(serializedObject.c_str(), serializedObject.length());
  }
}
 
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());
 
  configTime(0, 3600, ntpServer);

  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.on("/chat.css", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/chat.css", "text/css");
  });

  server.begin();
}
 
void loop(){}

Testing the code

Like we have been doing in the previous tutorials, simply compile and upload the code to your ESP32. Also, make sure to upload the modified version of the “chat.js” to its file system.

Then, access the chat application in two different browser windows (either in the same device or different devices) and start sending messages. You should get an output similar to figure 1. As can be seen, now the tags have the name of the user and also the timestamp of the message.

ESP32 chat with messages timestamp.
Figure 1 – ESP32 chat with messages timestamp.

Related Links

Leave a Reply