Introduction
In this tutorial we are going to learn how to dynamically add and remove a card from a web dashboard served by the ESP32. We will be using the ESP-DASH library and the Arduino core.
In previous tutorials we have already explored how to setup a real-time web dashboard to be served by an ESP32, to display sensor measurements to a user. Nonetheless, in all the examples we have covered so far, we had a predefined number of cards in our dashboard.
Although this scenario works great for many simple applications, there might be use cases where we don’t know beforehand how many sensor measurements we want to display. One such example could be building a sensor network where ESP32 nodes can be added and removed dynamically and one of the nodes of the network needs to serve the dashboard and keep the cards matching the nodes that are part of the network at each moment.
Naturally, this is just one of the use cases where we may require such dynamic behavior. As such, in this tutorial, we will learn the basics on how to dynamically add and remove a card from the dashboard. The objective is to keep the code simple, so after understanding the basics you can use it in any specific case where you might need this behavior.
In the code below, to be able to create the card dynamically, we will need to allocate an object in the Heap memory (you can read here an interesting comparison between Stack and Heap memory). As such, we will need to understand some C++ concepts related with object creation and scopes. This will be explained in more detail when analyzing the code.
In order for us to focus on the actual code to add and remove the cards, we are not going to connect the ESP32 to a real sensor but rather generate the measurements with random numbers, just for illustration purposes. If you want to test with real sensor measurements, you can check here an example where a DHT22 sensor module was used to obtain both temperature and humidity measurements. If you are interested in other types of measurements that don’t fit the standard cards, you can also check the Generic Card.
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.
Dynamic dashboard code
As usual, we will start our code with the library includes:
- WiFi.h: Exposes to us the WiFi extern variable, which allows to connect the ESP32 to a WiFi network.
- ESPAsyncWebServer.h: Allows to setup an async HTTP web server on the ESP32. For our particular use case, this will be used under the hood by the ESP-DASH library to serve the dashboard and to receive a websocket connection from the frontend.
- ESPDash.h: Allows us to setup and manage a real-time dashboard, served by the ESP32.
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESPDash.h>
Then we will define two global variables to hold the credentials of the WiFi network. More precisely, we will need the network name (SSID) and the network password. Don’t forget to replace the placeholders I’m using below with the actual credentials of your network.
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPass";
Then we will create an object of class AsyncWebServer, which will be used under the hood by the ESP-DASH lib. As input of the constructor we need to pass the port where the server will be listening to incoming requests. We will be using port 80, since it is the default HTTP port.
AsyncWebServer server(80);
Then we will create an object of class ESPDash, passing as input of the constructor the address of our server object.
ESPDash dashboard(&server);
After this we will declare a pointer to an object of class Card. This will be later used for us to dynamically add and remove cards from the dashboard.
Card *tempCard;
We will also define a Boolean variable that indicates, at any moment, if the card is displayed in the dashboard (true) or not (false). Naturally, we will change its value in our program, as we add and remove the card. We will initialize it with the value false since, when our program starts running, there won’t be any card in the dashboard.
bool isCardVisible = false;
Moving on to the Arduino setup, we will start by opening a serial connection. We will use it to print the IP address assigned to the ESP32 on the WiFi network, which we will need to later access the dashboard when testing the code.
Serial.begin(115200);
After this we will connect to the WiFi network using the previously defined credentials. We do this with a call to the begin method on the WiFi extern variable, passing as input the network credentials. After this we will poll the connection state until it is equal to WL_CONNECTED. Finally, we print the IP address assigned to the ESP32 with a call to the localIP method on the WiFi extern variable.
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println(WiFi.localIP());
To complete the Arduino setup, we will call the begin method on our server object. This will make the server start listening to incoming HTTP requests.
server.begin();
The complete setup code is available 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());
server.begin();
}
Moving on to the Arduino loop, where we will write the rest of our code, we will start by checking the value of the isCardVisible variable, in an IF ELSE statement.
if(!isCardVisible){
// Add card to dashboard
}else{
// Remove card from dashboard
}
In case the card is currently not visible, we will create a new Card object. We will use the new operator, which will allocate memory for the Card object on the Heap [1] and return a pointer to our object. If we did not use the new operator, then the object would have been created on the Stack.
In our case, the problem of creating this object on the stack would be that, once the loop function exits, the objects allocated on the stack go out of scope and are freed (to be more precise, in our particular case, the object would go out of scope as soon as the IF block finishes). This is the standard behavior in C++for when a stack allocated object goes out of scope [1], and the Arduino loop is just a function.
When the objects go out of scope, their destructor is called. If we look into the destructor of the Card class (here), it basically removes itself from the ESPDash object cards by calling the remove method. If we analyze this remove method from the ESPDash class, we will see that it will remove the Card object from a list it contains internally and update the frontend dashboard layout to remove the corresponding card.
Consequently, if we just created a Card object without the new operator, it would be added to the dashboard upon its creation but removed immediately after it goes out of scope. As such, we would never see the new card in the frontend.
It’s important to mention that, when we allocate memory in the heap, it is our responsibility to free it later (if we are not careful in our programs, we can have a memory leak situation).
Now that we have a basic idea about why we are using an object pointer and the new operator for this particular use case, we will analyze the inputs we will pass to the constructor:
- The address of our dashboard object.
- The constant TEMPERATURE_CARD, which indicates we want a temperature card.
- The name of the card, to be displayed in the dashboard.
- The unit of the measurements.
tempCard = new Card(&dashboard, TEMPERATURE_CARD, "Temperature", "°C");
Then we will generate a fake measurement by calling the Arduino random function. Naturally, if you want to use a real sensor to test this example, you simply need to call some method or function to obtain that measurement, instead of the random function.
int temperature = random(0, 100);
After this, we will update the Card object with a call to the update method, passing as input the measurement we just obtained. Since we have a pointer to the object, we need to use the “->” operator instead of the “.” operator.
Note however that this update method won’t send the new value to the dashboard. To do so, we still need to call the sendUpdates method on our dashboard object.
tempCard->update(temperature);
dashboard.sendUpdates();
The whole IF block is shown below.
if(!isCardVisible){
tempCard = new Card(&dashboard, TEMPERATURE_CARD, "Temperature", "°C");
int temperature = random(0, 100);
tempCard->update(temperature);
dashboard.sendUpdates();
}else{
// Remove card
}
In the ELSE block, we will take care of removing the card from the dashboard. We have already mentioned the fact that, when the destructor of the Card object is called, it will remove itself from the dashboard.
Nonetheless, we also know that we have allocated the object on the Heap, meaning that it is our responsibility to free that memory once we no longer need the object.
As such, we will call the delete operator on our object pointer. This will ensure that the destructor is called and the allocated memory is freed afterwards [2].
delete tempCard;
The complete IF ELSE block is shown below.
if(!isCardVisible){
tempCard = new Card(&dashboard, TEMPERATURE_CARD, "Temperature", "°C");
int temperature = random(0, 100);
tempCard->update(temperature);
dashboard.sendUpdates();
}else{
delete tempCard;
}
To finalize the loop, we will toggle the value of the flag and add a small delay.
isCardVisible = !isCardVisible;
delay(5000);
The full Arduino main loop can be seen below.
void loop() {
if(!isCardVisible){
tempCard = new Card(&dashboard, TEMPERATURE_CARD, "Temperature", "°C");
int temperature = random(0, 100);
tempCard->update(temperature);
dashboard.sendUpdates();
}else{
delete tempCard;
}
isCardVisible = !isCardVisible;
delay(5000);
}
The full code is available on the snippet below.
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESPDash.h>
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPass";
AsyncWebServer server(80);
ESPDash dashboard(&server);
Card *tempCard;
bool isCardVisible = false;
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());
server.begin();
}
void loop() {
if(!isCardVisible){
tempCard = new Card(&dashboard, TEMPERATURE_CARD, "Temperature", "°C");
int temperature = random(0, 100);
tempCard->update(temperature);
dashboard.sendUpdates();
}else{
delete tempCard;
}
isCardVisible = !isCardVisible;
delay(5000);
}
Testing the code
To test the code, simply compile it and upload it to your ESP32. Once the procedure is finished, open the IDE serial monitor (or any other similar tool). After the ESP32 connects to the WiFi network, it should print its IP address. Copy it, since we need to use it to reach the dashboard.
Then, open a web browser of your choice (I’ve used Google Chrome). On the address bar type the following, changing #yourDeviceIP# by the IP you just copied from the serial monitor:
http://#yourDeviceIP#/
After clicking enter, you should see the dashboard, like shown in figure 1. In this case, I’ve taken the snippet in a moment when the card was removed. Nonetheless, you should see the card appearing and disappearing with the periodicity we defined in the code. Note also that the “Loading” icon and message will be displayed every time that there is no element in the dashboard.
As an interesting coincidence, the dashboard has a tab called “Statistics” that displays some statistics from the ESP32. As can be seen below in figure 2, one of these is the free Heap. So, if we leave the dashboard running for a while (the Card object will be created and deleted many times), we should not see this value go down (it will fluctuate due to some other memory allocations, but should remain on a stable range).
Also, if you want to see the effects of not deleting the object in the heap memory, you can comment the following line of code and let the code run for a while:
else{
//delete tempCard;
}
You will see that cards are no longer going to be removed (they will just accumulate in the dashboard) because new objects are allocated every time we call the new operator and are never deallocated. Consequently their destructor is never called (recall that only stack allocated objects have the destructor called automatically when they go out of scope) and they are never removed from the dashboard.
If you check the statistics on that case, you should see the free Heap going down (you can reduce the delay in the main loop if you want to see this happening faster).
Other interesting exercise you can do is creating the Card object without using the new operator (snippet below). In this case, it will be allocated on the stack and go out of scope after the IF block ends. Consequently, from a user’s perspective, you won’t ever see the card in the dashboard because the destructor is called soon after the constructor.
if(!isCardVisible){
Card stackAllocatedCard = Card(&dashboard, TEMPERATURE_CARD, "Temperature", "°C");
int temperature = random(0, 100);
stackAllocatedCard.update(temperature);
dashboard.sendUpdates();
}
Note that, for this case, if you inspect the data sent through the websocket on your browser, you will see the ESP32 informing the frontend that a new card was added, but soon after you will see another command to remove it. This means that, effectively, the card was added when the object was created, but it was removed right after when it went out of scope.
If you are on Google Chrome, you can check the websocket communication flow by going to the “Network” tab in the developer tools, as shown below in figure 3.
Final notes
With this tutorial, we learned a simple way of dynamically adding and removing cards from the dashboard. As could be seen, it is very important to understand how to allocate the new Card objects and to later free the memory when they are no longer needed.
Without doing this properly and understanding how memory allocation works, it is very easy to get a program that compiles but has hard to debug issues.
It is also important to take in consideration that what we have learned on this tutorial extends far beyond the scope of the ESP-DASH library, and could be useful for other scenarios.
To finalize, I would like to mention that, although we can implement a lot of nice applications with Arduino without ever worrying about the C++ details, taking that step of learning C++ opens up a new level of understanding and possibilities. Although C++ can be complex to learn (I’m not an expert on the language and I’m also still learning every day) it is really fun and interesting and can enhance a lot what you can build.
Suggested Readings
Stack vs Heap memory Allocation
References
[1] https://cs.stanford.edu/people/eroberts/courses/cs106b/handouts/22-DynamicAllocation.pdf
[2] https://docs.microsoft.com/en-us/cpp/cpp/delete-operator-cpp?view=msvc-160