How to update Web page using ESP32 and Server-Sent Event (SSE)

This tutorial covers how to update a Web page using ESP32 and SSE. It happens many times that it is necessary to send sensor readings to a web page and we want to update the web page pushing the data acquired by the sensors.

ESP32 Server-Sent Event (SSE) is a mechanism to push updates to the client asynchronously. This tutorial covers how to implement Server-Sent Event (SSE) using ESP32. Moreover, we will cover how to develop a Web page that receives events. Using SSE, the Web page can receive updates from the ESP32 through the HTTP protocol asynchronously. To explain how it works, we will suppose we want to update the Web page with the temperature and the humidity values acquired by the DHT11 connected to the ESP32.

In the previous post, we have covered how to use WebSocket protocol using ESP32, now we focus our attention on a different way to notify events to a Web client.

The main difference between the SSE and the Websocket is that the WebSocket is bi-directional, or in other words, the Web page and the ESP32 can send events. In the Server-Sent Event, the Web client can receive updates from the ESP32 but it can’t send updates to the ESP32. The final result is shown below:

ESP32 SSE: Update Web page using Server-Sent event using ESP32

How the Server-Sent Event works

Before diving into the details of SSE, it is useful to understand how it works. The image below describes how the Web page client and the ESP32 exchange data using SSE:

Server-Sent Event (SSE) with ESP32

The process starts with an HTTP request coming from the Web page to the ESP32. From now on, the ESP32 can send events to the Web page as they happen. In this tutorial, we have supposed to send events that holds sensor readings but it is possible to send different kind of messages. As stated before, only the ESP32 can send events toward the Web page.

ESP32 SSE code

Now we know how SSE works we can focus our attention on the code to implement the Web server on the ESP32 that supports Server-Sent Event:

#include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "DHT.h" // WiFi config const char *SSID = "ssid"; const char *PWD = "wifi_password"; #define DHTPIN 16 #define DHTTYPE DHT11 DHT dht(DHTPIN, DHTTYPE); const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP32 SSE</title> <script src="https://cdn.amcharts.com/lib/4/core.js"></script> <script src="https://cdn.amcharts.com/lib/4/charts.js"></script> <script src="https://cdn.amcharts.com/lib/4/themes/animated.js"></script> <script language="javascript"> var hand; var handHum; am4core.ready(function() { am4core.useTheme(am4themes_animated); // create chart var chart = am4core.create("chart_div_temp", am4charts.GaugeChart); chart.innerRadius = -15; var axis = chart.xAxes.push(new am4charts.ValueAxis()); axis.min = 0; axis.max = 50; axis.strictMinMax = true; var colorSet = new am4core.ColorSet(); var gradient = new am4core.LinearGradient(); gradient.addColor(am4core.color("blue")); gradient.addColor(am4core.color("red")); axis.renderer.line.stroke = gradient; axis.renderer.line.strokeWidth = 15; axis.renderer.line.strokeOpacity = 1; axis.renderer.labels.template.adapter.add("text", function(text) { return text + "C"; }) hand = chart.hands.push(new am4charts.ClockHand()); hand.radius = am4core.percent(90); var chartHum = am4core.create("chart_div_hum", am4charts.GaugeChart); chartHum.innerRadius = -15; var axisHum = chartHum.xAxes.push(new am4charts.ValueAxis()); axisHum.min = 0; axisHum.max = 100; axisHum.strictMinMax = true; var colorSetHum = new am4core.ColorSet(); var gradientHum = new am4core.LinearGradient(); gradientHum.stops.push({color:am4core.color("white")}) gradientHum.stops.push({color:am4core.color("light_blue")}) gradientHum.stops.push({color:am4core.color("blue")}) axisHum.renderer.line.stroke = gradientHum; axisHum.renderer.line.strokeWidth = 15; axisHum.renderer.line.strokeOpacity = 1; axisHum.renderer.labels.template.adapter.add("text", function(text) { return text + "%"; }) axisHum.renderer.grid.template.disabled = true; handHum = chartHum.hands.push(new am4charts.ClockHand()); handHum.radius = am4core.percent(90); }); if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temp', function(e) { setTempValue(e.data); }, false); source.addEventListener('hum', function(e) { setHumValue(e.data); }, false); function setTempValue(temp) { hand.showValue(Math.round(temp)); } function setHumValue(hum) { handHum.showValue(Math.round(hum)); } } </script> <style> .card { box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); transition: 0.3s; border-radius: 5px; /* 5px rounded corners */ } /* On mouse-over, add a deeper shadow */ .card:hover { box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); } /* Add some padding inside the card container */ .container { padding: 2px 16px; } </style> </head> <body> <h2>ESP32 SSE</h2> <div class="content"> <div class="card"> <div class="container"> <h4><b>Environment</b></h4> <div id="chart_div_temp" style="width: 300px; height: 250px;"></div> <div id="chart_div_hum" style="width: 300px; height: 250px;"></div> </div> </div> </div> </body> </html> )rawliteral"; // Web server running on port 80 AsyncWebServer server(80); // Async Events AsyncEventSource events("/events"); void connectToWiFi() { Serial.print("Connecting to "); Serial.println(SSID); WiFi.begin(SSID, PWD); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); // we can even make the ESP32 to sleep } Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); } void configureEvents() { events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client connections. Id: %u\n", client->lastId()); } // and set reconnect delay to 1 second client->send("hello from ESP32",NULL,millis(),1000); }); server.addHandler(&events); } void setup() { Serial.begin(9600); connectToWiFi(); server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, NULL); }); configureEvents(); server.begin(); dht.begin(); } void loop() { float temp = dht.readTemperature(); float hum = dht.readHumidity(); // Send event to the client events.send(String(temp).c_str(), "temp", millis()); events.send(String(hum).c_str(), "hum", millis()); //Serial.print("Temp:"); //Serial.println(temp); delay(2000); }
Code language: C++ (cpp)

As you can notice in the code there is a Web page that receives events. We will cover it later. Let us focus our attention on the ESP32 source code.

Adding libraries using Platform.io

This is the platformio.ini files to add the required libraries:

[env:esp32dev] platform = espressif32 board = esp32dev framework = arduino upload_port = /dev/tty.SLAB_USBtoUART lib_deps = ESPAsyncTCP ESP Async WebServer adafruit/DHT sensor library @ ^1.4.0 adafruit/Adafruit Unified Sensor @ ^1.1.4
Code language: JavaScript (javascript)

How to send events from ESP32

The first step is implementing the Web server that receives the HTTP connection as shown in the schema:

// Web server running on port 80 AsyncWebServer server(80); // Async Events AsyncEventSource events("/events");
Code language: C++ (cpp)

Moreover, we define a new event source named /events. You can change this name if you want. Next, we have to handle the incoming HTTP client request:

void configureEvents() { events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client connections. Id: %u\n", client->lastId()); } // and set reconnect delay to 1 second client->send("hello from ESP32",NULL,millis(),1000); }); server.addHandler(&events); }
Code language: C# (cs)

As you can notice, as soon as the Web page connects to the Web server running on the ESP32, it responds with a simple hello message. Finally, the code attaches the event source to the Web server so that it can handle it.

Other interesting articles:

Sending the Web page to the browser

Once we have configured everything, we can handle the HTTP request made by the browser when it connects for the first time providing the HTML page:

void setup() { Serial.begin(9600); connectToWiFi(); server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, NULL); }); configureEvents(); server.begin(); ... }
Code language: PHP (php)

ESP32 Web page to receive events

Now we can focus our attention on the HTML Web page that receives Server-Sent Events from the ESP32. This page is provided by the ESP32 as described before. This the Web page:

<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP32 SSE</title> <script src="https://cdn.amcharts.com/lib/4/core.js"></script> <script src="https://cdn.amcharts.com/lib/4/charts.js"></script> <script src="https://cdn.amcharts.com/lib/4/themes/animated.js"></script> <script language="javascript"> var hand; var handHum; am4core.ready(function() { am4core.useTheme(am4themes_animated); // create chart var chart = am4core.create("chart_div_temp", am4charts.GaugeChart); chart.innerRadius = -15; var axis = chart.xAxes.push(new am4charts.ValueAxis()); axis.min = 0; axis.max = 50; axis.strictMinMax = true; var colorSet = new am4core.ColorSet(); var gradient = new am4core.LinearGradient(); gradient.addColor(am4core.color("blue")); gradient.addColor(am4core.color("red")); axis.renderer.line.stroke = gradient; axis.renderer.line.strokeWidth = 15; axis.renderer.line.strokeOpacity = 1; axis.renderer.labels.template.adapter.add("text", function(text) { return text + "C"; }) hand = chart.hands.push(new am4charts.ClockHand()); hand.radius = am4core.percent(90); var chartHum = am4core.create("chart_div_hum", am4charts.GaugeChart); chartHum.innerRadius = -15; var axisHum = chartHum.xAxes.push(new am4charts.ValueAxis()); axisHum.min = 0; axisHum.max = 100; axisHum.strictMinMax = true; var colorSetHum = new am4core.ColorSet(); var gradientHum = new am4core.LinearGradient(); gradientHum.stops.push({color:am4core.color("white")}) gradientHum.stops.push({color:am4core.color("light_blue")}) gradientHum.stops.push({color:am4core.color("blue")}) axisHum.renderer.line.stroke = gradientHum; axisHum.renderer.line.strokeWidth = 15; axisHum.renderer.line.strokeOpacity = 1; axisHum.renderer.labels.template.adapter.add("text", function(text) { return text + "%"; }) axisHum.renderer.grid.template.disabled = true; handHum = chartHum.hands.push(new am4charts.ClockHand()); handHum.radius = am4core.percent(90); }); if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temp', function(e) { setTempValue(e.data); }, false); source.addEventListener('hum', function(e) { setHumValue(e.data); }, false); function setTempValue(temp) { hand.showValue(Math.round(temp)); } function setHumValue(hum) { handHum.showValue(Math.round(hum)); } } </script> <style> .card { box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); transition: 0.3s; border-radius: 5px; /* 5px rounded corners */ } /* On mouse-over, add a deeper shadow */ .card:hover { box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); } /* Add some padding inside the card container */ .container { padding: 2px 16px; } </style> </head> <body> <h2>ESP32 SSE</h2> <div class="content"> <div class="card"> <div class="container"> <h4><b>Environment</b></h4> <div id="chart_div_temp" style="width: 300px; height: 250px;"></div> <div id="chart_div_hum" style="width: 300px; height: 250px;"></div> </div> </div> </div> </body> </html>
Code language: HTML, XML (xml)

How to receive SSE Events

The Web page handles the SSE using Javascript. It handles different kinds of events:

  • connection
  • disconnection
  • messages sent by ESP32
  • events

The first step is defining an event source (or path):

var source = new EventSource('/events');
Code language: JavaScript (javascript)

Next, it adds some listener to listen to incoming events. This is the listener for incoming messages:

source.addEventListener('message', function(e) { console.log("message", e.data); }, false);
Code language: JavaScript (javascript)

Finally, the two most important listeners: one for the temperature update and the other one for humidity update:

source.addEventListener('temp', function(e) { // update chart }, false); source.addEventListener('hum', function(e) { // update chart }, false);
Code language: JavaScript (javascript)

When the page receives these two kinds of events, it updates the values.

Visualizing Server sent events

The last step is visualizing the events received. To this goal, the page uses AMCharts, a very simple and light-weight javascript library:

<script src="https://cdn.amcharts.com/lib/4/core.js"></script> <script src="https://cdn.amcharts.com/lib/4/charts.js"></script> <script src="https://cdn.amcharts.com/lib/4/themes/animated.js"></script> <script language="javascript"> var hand; var handHum; am4core.ready(function() { am4core.useTheme(am4themes_animated); // create chart var chart = am4core.create("chart_div_temp", am4charts.GaugeChart); chart.innerRadius = -15; var axis = chart.xAxes.push(new am4charts.ValueAxis()); axis.min = 0; axis.max = 50; axis.strictMinMax = true; var colorSet = new am4core.ColorSet(); var gradient = new am4core.LinearGradient(); gradient.addColor(am4core.color("blue")); gradient.addColor(am4core.color("red")); axis.renderer.line.stroke = gradient; axis.renderer.line.strokeWidth = 15; axis.renderer.line.strokeOpacity = 1; axis.renderer.labels.template.adapter.add("text", function(text) { return text + "C"; }) hand = chart.hands.push(new am4charts.ClockHand()); hand.radius = am4core.percent(90); var chartHum = am4core.create("chart_div_hum", am4charts.GaugeChart); chartHum.innerRadius = -15; var axisHum = chartHum.xAxes.push(new am4charts.ValueAxis()); axisHum.min = 0; axisHum.max = 100; axisHum.strictMinMax = true; var colorSetHum = new am4core.ColorSet(); var gradientHum = new am4core.LinearGradient(); gradientHum.stops.push({color:am4core.color("white")}) gradientHum.stops.push({color:am4core.color("light_blue")}) gradientHum.stops.push({color:am4core.color("blue")}) axisHum.renderer.line.stroke = gradientHum; axisHum.renderer.line.strokeWidth = 15; axisHum.renderer.line.strokeOpacity = 1; axisHum.renderer.labels.template.adapter.add("text", function(text) { return text + "%"; }) axisHum.renderer.grid.template.disabled = true; handHum = chartHum.hands.push(new am4charts.ClockHand()); handHum.radius = am4core.percent(90); }); if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temp', function(e) { setTempValue(e.data); }, false); source.addEventListener('hum', function(e) { setHumValue(e.data); }, false); function setTempValue(temp) { hand.showValue(Math.round(temp)); } function setHumValue(hum) { handHum.showValue(Math.round(hum)); } } </script>
Code language: JavaScript (javascript)

The fnal result is shown below:

ESP32 dashboard using Server Sent Events

Wrapping up

In this tutorial, we have covered how to use Server-Sent Event to update a Web page with sensor readings with ESP32. We have discovered that using ESP32 and SSE it is possible to update asynchronously a Web page with the data coming from sensors. You can further extend this example by implementing a complete Dashboard using HTML and javascript that visualizes the data read by sensors connected to the ESP32.