ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically)

This tutorial shows how to use Server-Sent Events (SSE) in an ESP32 Web Server programmed with Arduino IDE. SSE allows the browser to receive automatic updates from a server via HTTP connection. This is useful to send updated sensor readings to the browser, for example. Whenever a new reading is available, the ESP32 sends it to the client and the web page can be updated automatically without the need to make additional requests.

ESP32 Web Server using Server-Sent Events (SSE) Update Sensor Readings Automatically Arduino

As an example, we’ll build a web server that displays sensor readings from a BME280 temperature, humidity and pressure sensor. To learn more about the BME280, read our guide:

We also have a similar Server-Sent Events guide for the ESP8266.

Introducing Server-Sent Events (SSE)

Server-Sent Events (SSE) allows the client to receive automatic updates from a server via HTTP connection.

ESP32 ESP8266 Server-Sent Events Web Server How it Works

The client initiates the SSE connection and the server uses the event source protocol to send updates to the client. The client will receive updates from the server, but it can’t send any data to the server after the initial handshake.

This is useful to send updated sensor readings to the browser. Whenever a new reading is available, the ESP32 sends it to the client and the web page can be updated automatically without the need for further requests. Instead of sensor readings, you can send any data that might be useful for your project like GPIO states, notifications when motion is detected, etc.

Important: Server-Sent Events (SSE) are not supported on Internet Explorer.

Project Overview

Here’s the web page we’ll build for this project.

BME280 Web Server using Server-Sent Events (SSE) ESP32 Arduino
  • The ESP32 web server displays three cards with BME280 temperature, humidity and pressure readings;
  • The ESP32 gets new readings from the sensor every 30 seconds;
  • Whenever a new reading is available, the board (server) sends it to the client using server-sent events;
  • The client receives the data and updates the web page accordingly;
  • This allows the web page to be updated automatically whenever new readings are available.

How it Works?

The following diagram summarizes how Server-Sent Events work to update the web page.

ESP32 BME280 Web Server with Server Sent Events (SSE) How it Works
  1. The client initiates the SSE connection and the server uses the event source protocol on the /events URL to send updates to the client;
  2. The ESP32 gets new sensor readings;
  3. It sends the readings as events with the following names to the client: ‘temperature’, ‘humidity’ and ‘pressure’;
  4. The client has event listeners for the events sent by the server and receives the updated sensor readings on those events;
  5. It updates the web page with the newest readings.

Preparing Arduino IDE

We’ll program the ESP32 board using Arduino IDE, so make sure you have it installed in your Arduino IDE.

Installing Libraries – Async Web Server

To build the web server we’ll use the ESPAsyncWebServer library. This library needs the AsyncTCP library to work properly. Click the links below to download the libraries.

These libraries aren’t available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch Include Library > Add .zip Library and select the libraries you’ve just downloaded.

Installing Libraries – BME280 Sensor

To get readings from the BME280 sensor module, we’ll use the Adafruit_BME280 library. You also need to install the Adafruit_Sensor library. Follow the next steps to install the libraries in your Arduino IDE:

1. Open your Arduino IDE and go to Sketch Include Library > Manage Libraries. The Library Manager should open.

2. Search for “adafruit bme280 ” on the Search box and install the library.

Installing BME280 library in Arduino IDE

To use the BME280 library, you also need to install the Adafruit Unified Sensor. Follow the next steps to install the library in your Arduino IDE:

3. Search for “Adafruit Unified Sensor“in the search box. Scroll all the way down to find the library and install it.

Installing Adafruit Unified Sensor Driver library

After installing the libraries, restart your Arduino IDE.

To learn more about the BME280 sensor, read our guide: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).

Building the Circuit

To exemplify how to use server-sent events with the ESP32, we’ll send sensor readings from a BME280 sensor to the browser. So, you need to wire a BME280 sensor to your ESP32.

Parts Required

To complete this tutorial you need the following parts:

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

Schematic Diagram

We’re going to use I2C communication with the BME280 sensor module. For that, wire the sensor to the default ESP32 SCL (GPIO 22) and SDA (GPIO 21) pins, as shown in the following schematic diagram.

ESP32 Wiring to BME280 Schematic Diagram

Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?

Code for ESP32 Web Server using Server-Sent Events (SSE)

Copy the following code to your Arduino IDE.

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-web-server-sent-events-sse/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// Create an Event Source on /events
AsyncEventSource events("/events");

// Timer variables
unsigned long lastTime = 0;  
unsigned long timerDelay = 30000;

// Create a sensor object
Adafruit_BME280 bme;         // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)

float temperature;
float humidity;
float pressure;

// Init BME280
void initBME(){
    if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

void getSensorReadings(){
  temperature = bme.readTemperature();
  // Convert temperature to Fahrenheit
  //temperature = 1.8 * bme.readTemperature() + 32;
  humidity = bme.readHumidity();
  pressure = bme.readPressure()/ 100.0F;
}

// Initialize WiFi
void initWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    Serial.print("Connecting to WiFi ..");
    while (WiFi.status() != WL_CONNECTED) {
        Serial.print('.');
        delay(1000);
    }
    Serial.println(WiFi.localIP());
}

String processor(const String& var){
  getSensorReadings();
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return String(temperature);
  }
  else if(var == "HUMIDITY"){
    return String(humidity);
  }
  else if(var == "PRESSURE"){
    return String(pressure);
  }
  return String();
}

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <link rel="icon" href="data:,">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    p { font-size: 1.2rem;}
    body {  margin: 0;}
    .topnav { overflow: hidden; background-color: #50B8B4; color: white; font-size: 1rem; }
    .content { padding: 20px; }
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
    .cards { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
    .reading { font-size: 1.4rem; }
  </style>
</head>
<body>
  <div class="topnav">
    <h1>BME280 WEB SERVER (SSE)</h1>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card">
        <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p><p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>
      </div>
      <div class="card">
        <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p><p><span class="reading"><span id="hum">%HUMIDITY%</span> &percnt;</span></p>
      </div>
      <div class="card">
        <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p>
      </div>
    </div>
  </div>
<script>
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('temperature', function(e) {
  console.log("temperature", e.data);
  document.getElementById("temp").innerHTML = e.data;
 }, false);
 
 source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("hum").innerHTML = e.data;
 }, false);
 
 source.addEventListener('pressure', function(e) {
  console.log("pressure", e.data);
  document.getElementById("pres").innerHTML = e.data;
 }, false);
}
</script>
</body>
</html>)rawliteral";

void setup() {
  Serial.begin(115200);
  initWiFi();
  initBME();


  // Handle Web Server
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Handle Web Server Events
  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);
  server.begin();
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
    getSensorReadings();
    Serial.printf("Temperature = %.2f ºC \n", temperature);
    Serial.printf("Humidity = %.2f \n", humidity);
    Serial.printf("Pressure = %.2f hPa \n", pressure);
    Serial.println();

    // Send Events to the Web Server with the Sensor Readings
    events.send("ping",NULL,millis());
    events.send(String(temperature).c_str(),"temperature",millis());
    events.send(String(humidity).c_str(),"humidity",millis());
    events.send(String(pressure).c_str(),"pressure",millis());
    
    lastTime = millis();
  }
}

View raw code

Insert your network credentials in the following variables and the code will work straight away.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

How the Code Works

Continue reading to learn how the code works or skip to the Demonstration section.

Including Libraries

The Adafruit_Sensor and Adafruit_BME280 libraries are needed to interface with the BME280 sensor.

#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

The WiFi, ESPAsyncWebServer and AsyncTCP libraries are used to create the web server.

#include <WiFi.h>
#include "ESPAsyncWebServer.h"

Network Credentials

Insert your network credentials in the following variables, so that the ESP32 can connect to your local network using Wi-Fi.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

AsyncWebServer and AsyncEventSource

Create an AsyncWebServer object on port 80.

AsyncWebServer server(80);

The following line creates a new event source on /events.

AsyncEventSource events("/events");

Declaring Variables

The lastTime and the timerDelay variables will be used to update sensor readings every X number of seconds. As an example, we’ll get new sensor readings every 30 seconds (30000 milliseconds). You can change that delay time in the timerDelay variable.

unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

Create an Adafruit_BME280 object called bme on the default ESP32 I2C pins.

Adafruit_BME280 bme;

The temperature, humidity and pressure float variables will be used to hold BME280 sensor readings.

float temperature;
float humidity;
float pressure;

Initialize BME280

The following function can be called to initialize the BME280 sensor.

void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

Get BME280 Readings

The getSensorReading() function gets temperature, humidity and pressure readings from the BME280 sensor and saves them on the temperature, humidity and pressure variables.

void getSensorReadings(){
  temperature = bme.readTemperature();
  // Convert temperature to Fahrenheit
  //temperature = 1.8 * bme.readTemperature() + 32;
  humidity = bme.readHumidity();
  pressure = bme.readPressure()/ 100.0F;
}

Initialize Wi-Fi

The following function sets the ESP32 as a Wi-Fi station and connects to your router using the ssid and password defined earlier.

void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

Processor

The processor() function replaces any placeholders on the HTML text used to build the web page with the current sensor readings before sending it to the browser.

String processor(const String& var){
  getSensorReadings();
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return String(temperature);
  }
  else if(var == "HUMIDITY"){
    return String(humidity);
  }
  else if(var == "PRESSURE"){
    return String(pressure);
  }
}

This allows us to display the current sensor readings on the web page when you access it for the first time. Otherwise, you would see a blank space until new readings were available (which can take some time depending on the delay time you’ve defined on the code).

Building the Web Page

The index_html variable contains all the HTML, CSS and JavaScript to build the web page.

Note: for the simplicity of this tutorial, we’re placing everything needed to build the web page on the index_html variable that we use on the Arduino sketch. Note that it may be more practical to have separated HTML, CSS and JavaScript files that then you upload to the ESP32 filesystem and reference them on the code.

Here’s the content index_html variable:

<!DOCTYPE HTML>
<html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <link rel="icon" href="data:,">
  <style>
    html {
      font-family: Arial; 
      display: inline-block; 
      text-align: center;
    }
    p { 
      font-size: 1.2rem;
    }
    body {  
      margin: 0;
    }
    .topnav { 
      overflow: hidden; 
      background-color: #50B8B4; 
      color: white; 
      font-size: 1rem; 
    }
    .content { 
      padding: 20px; 
    }
    .card { 
      background-color: white; 
      box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); 
    }
    .cards { 
      max-width: 800px; 
      margin: 0 auto; 
      display: grid; 
      grid-gap: 2rem; 
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
    .reading { 
      font-size: 1.4rem;  
    }
  </style>
</head>
<body>
  <div class="topnav">
    <h1>BME280 WEB SERVER (SSE)</h1>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card">
        <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p>
        <p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>
      </div>
      <div class="card">
        <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p>
        <p><span class="reading"><span id="hum">%HUMIDITY%</span> &percnt;</span></p>
      </div>
      <div class="card">
        <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p>
      </div>
    </div>
  </div>
<script>
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('temperature', function(e) {
    console.log("temperature", e.data);
    document.getElementById("temp").innerHTML = e.data;
  }, false);

  source.addEventListener('humidity', function(e) {
    console.log("humidity", e.data);
    document.getElementById("hum").innerHTML = e.data;
  }, false);

  source.addEventListener('pressure', function(e) {
    console.log("pressure", e.data);
    document.getElementById("pres").innerHTML = e.data;
  }, false);
}
</script>
</body>
</html>

We won’t go into detail on how the HTML and CSS works. We’ll just take a look at how to handle the events sent by the server.

CSS

Between the <style> </style> tags we include the styles to style the web page using CSS. Feel free to change it to make the web page look as you wish. We won’t explain how the CSS for this web page works because it is not relevant for this tutorial.

<style>
  html {
    font-family: Arial; 
    display: inline-block; 
    text-align: center;
  }
  p { 
    font-size: 1.2rem;
  }
  body {  
    margin: 0;
  }
  .topnav { 
    overflow: hidden; 
    background-color: #50B8B4; 
    color: white; 
    font-size: 1rem; 
  }
  .content { 
    padding: 20px; 
  }
  .card { 
    background-color: white; 
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); 
  }
  .cards { 
    max-width: 800px; 
    margin: 0 auto; 
    display: grid; 
    grid-gap: 2rem; 
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
  .reading { 
    font-size: 1.4rem;  
  }
</style>

HTML

Between the <body> </body> tags we add the web page content that is visible to the user.

<body>
  <div class="topnav">
    <h1>BME280 WEB SERVER (SSE)</h1>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card">
        <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p>
        <p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>
      </div>
      <div class="card">
        <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p>
        <p><span class="reading"><span id="hum">%HUMIDITY%</span> &percnt;</span></p>
      </div>
      <div class="card">
        <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p>
      </div>
    </div>
  </div>

There’s a heading 1 with the content “BME280 WEB SERVER (SSE)”. That’s the text that shows up on the top bar. Feel free to modify that text.

<h1>BME280 WEB SERVER (SSE)</h1>

Then, we display the sensor readings in separated div tags. Let’s take a quick look at the paragraphs that displays the temperature:

<p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p>
<p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>

The first paragraph simply displays the “TEMPERATURE” text. We define the color and also an icon.

On the second paragraph, you can see that the %TEMPERATURE% placeholder is surrounded by <span id=”temp”> </span> tags. The HTML id attribute is used to specify a unique id for an HTML element.

It is used to point to a specific style or it can be used by JavaScript to access and manipulate the element with that specific id. That’s what we’re going to do. For instance, when the client receives a new event with the latest temperature reading, it updates the HTML element with the id “temp” with the new reading.

A similar process is done to update the other readings.

JavaScript – EventSource

The JavaScript goes between the <script> </script> tags. It is responsible for initializing an EventSource connection with the server and handle the events received from the server.

<script>
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('temperature', function(e) {
    console.log("temperature", e.data);
    document.getElementById("temp").innerHTML = e.data;
  }, false);

  source.addEventListener('humidity', function(e) {
    console.log("humidity", e.data);
    document.getElementById("hum").innerHTML = e.data;
  }, false);

  source.addEventListener('pressure', function(e) {
    console.log("pressure", e.data);
    document.getElementById("pres").innerHTML = e.data;
  }, false);
}
</script>

Let’s take a look at how this works.

Handle Events

Create a new EventSource object and specify the URL of the page sending the updates. In our case, it’s /events.

if (!!window.EventSource) {
  var source = new EventSource('/events');

Once you’ve instantiated an event source, you can start listening for messages from the server with addEventListener().

These are the default event listeners, as shown here in the AsyncWebServer documentation.

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

Then, add the event listener for “temperature”.

source.addEventListener('temperature', function(e) {

When a new temperature reading is available, the ESP32 sends an event (“temperature”) to the client. The following lines handle what happens when the browser receives that event.

console.log("temperature", e.data);
document.getElementById("temp").innerHTML = e.data;

Basically, print the new readings on the browser console, and put the received data into the element with the corresponding id (“temp“) on the web page.

A similar processor is done for humidity and pressure.

source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("hum").innerHTML = e.data;
}, false);
 
source.addEventListener('pressure', function(e) {
  console.log("pressure", e.data);
  document.getElementById("pres").innerHTML = e.data;
}, false);
 
source.addEventListener('gas', function(e) {
  console.log("gas", e.data);
  document.getElementById("gas").innerHTML = e.data;
}, false);

setup()

In the setup(), initialize the Serial Monitor, initialize Wi-Fi and the BME280 sensor.

Serial.begin(115200);
initWiFi();
initBME();

Handle Requests

When you access the ESP32 IP address on the root / URL, send the text that is stored on the index_html variable to build the web page and pass the processor as argument, so that all placeholders are replaced with the latest sensor readings.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processor);
});

Server Event Source

Set up the event source on the server.

// Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);

Finally, start the server.

server.begin();

loop()

In the loop(), get new sensor readings:

getSensorReadings();

Print the new readings in the Serial Monitor.

Serial.printf("Temperature = %.2f ºC \n", temperature);
Serial.printf("Humidity = %.2f % \n", humidity);
Serial.printf("Pressure = %.2f hPa \n", pressure);
Serial.println();

Finally, send events to the browser with the newest sensor readings to update the web page.

// Send Events to the Web Server with the Sensor Readings
events.send("ping",NULL,millis());
events.send(String(temperature).c_str(),"temperature",millis());
events.send(String(humidity).c_str(),"humidity",millis());
events.send(String(pressure).c_str(),"pressure",millis());

Demonstration

After inserting your network credentials on the ssid and password variables, you can upload the code to your board. Don’t forget to check if you have the right board and COM port selected.

After uploading the code, open the Serial Monitor at a baud rate of 115200 and press the on-board EN/RST button. The ESP IP address should be printed.

ESP32 Web Server using Server Sent Events Serial Monitor Demonstration

Open a browser on your local network and insert the ESP32 IP address. You should get access to the web page to monitor the sensor readings.

BME280 Web Server using Server-Sent Events ESP32

The readings are updated automatically every 30 seconds.

At the same time, you should get new sensor readings on the Serial Monitor as shown in the previous print screen. Additionally, you can check if the client is receiving the events. On your browser, open the console by pressing Ctrl+Shift+J.

Server-Sent Events Browser Console ESP2 Web Server Demonstration

Wrapping Up

In this tutorial you’ve learned how to use Server-Sent Events with the ESP32. Server-Sent Events allow a web page (client) to get updates from a server. This can be used to automatically display new sensor readings on the web server page when they are available.

We have a similar tutorial but using the BME680 environmental sensor. You can check the tutorial on the following link:

Instead of Server-Sent Events, you can also use the WebSocket protocol to keep the web page updated. Take a look at the following tutorial to learn how to set up a WebSocket server with the ESP32:

Learn more about the ESP32 with our resources:



Learn how to program and build projects with the ESP32 and ESP8266 using MicroPython firmware DOWNLOAD »

Learn how to program and build projects with the ESP32 and ESP8266 using MicroPython firmware DOWNLOAD »


Enjoyed this project? Stay updated by subscribing our weekly newsletter!

26 thoughts on “ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically)”

  1. Excellent tutorial however I would like to know the advantages/disadvantages of Web Sent Events as opposed to using the Web Sockets protocol like you have described in a previous project.

    thanks and best regards,
    Carl

    Reply
    • Completely agree, great article and answers very neatly how I can introduce live behaviour to my ESP served pages easily.

      I am also interested in the pros and cons of SSE vs WS for this sort of application, as I’ve not used either yet.

      Reply
    • SSE has a messaging protocol so you can have a general UTF-8 message string, or labelled message like “temperature” with a UTF-8 string value (as in the software above). Typically json is used to format more complex data, so we could have used “TPH”:{“temperature”:25.2, “pressure”:1012, “humidity”:76.8} to send 3 readings in one message. With websockets, messages are a byte string and you have to come up with your own protocol.

      The SSE server implementation is simpler– it’s similar to a normal HTTP reply except you don’t give a length, then just keep the socket open to output a few more lines (with a flush) for each message. With websockets the client/server needs to implement a protocol to convert the http socket to separately managed websocket. But when changed to a websocket, the TCP connection doesn’t count against the limit per server. With libraries though, there might not be that much difference in top level software.

      Websockets are bidirectional, so the client can send messages to the server while receiving push data. You can see the difference in https://randomnerdtutorials.com/esp32-websocket-server-arduino/ which combines commands from the client with data (status) from the server. I believe browsers reissue a new request when the connection is broken, whereas websockets need to handle errors as you can see in the RNT tutorial above.

      Reply
  2. Thanks for the tutorial– I didn’t know about SSE before and it’s a good comparison with the prior websocket implementation.

    I don’t understand how the float value gets converted into a %.2f (apparently) string when sending events. Seems like sprintf() should have been used instead of String().c_str(). Maybe the libraries return sensor readings with an int to 1/100 decimal value?

    But in engineering school we were taught to not show more precision than you have, so if the BME has +-1degC, +-3% RH, and +-1hPa, then seems like you want to display temperature and pressure with with %.1f, and humidity rounded to an int().

    Reply
  3. Great tutorial!
    but how would one send calibration offsets to fix small errors in the temperature/humidity and differentiate from 2 sensor boards?

    ie temperature measured by sensor 1 is 23.5 sensor 2 reads 23.00 when both in the same location – the actual temperature is 22.8 -” read from a calibrated thermometer.

    How does the SSE know which board is which if you had 2 sensors in a room?. Can we use the sensor boards mac address as an id or do we need to another method.

    Reply
    • Hi.
      You can send a JSON string with the two temperature values.
      Or you can create an event for each reading. For example “sensor1” and “sensor2” like in this tutorial. We have three events for each reading.
      Regards,
      Sara

      Reply
  4. Great write up. I took the liberty to adapt your code for an esp8266. Also tailored it a bit for my needs (added a reading, made a static connection) Maybe it is of use to someone
    gitlab.com/diy_bloke/esp8266webserversse

    Reply
  5. A few notes on my experience with this sketch:

    The BME280 board I received from Adafruit was part # P2652. This board has a few differences from the one you pictured. I’m not sure they still offer your style board. If the esp32 pin 21 is connected to SDI pin of the Adafruit board and SDO is connected to ground everything works fine. Don’t leave SDO floating! Connecting it to 3.3volt will change the boards address to 77h.

    Thanks.

    Reply
  6. My compile aborts with an error that I don’t understand:

    /Users/imac2019/Documents/Arduino/sketch_oct23a/sketch_oct23a.ino: In function ‘void loop()’:
    sketch_oct23a:180:51: error: unknown conversion type character 0xa in format [-Werror=format=]
    Serial.printf(“Humidity = %.2f % \n”, humidity);

    Arduino: 1.8.13 (Mac OS X), Board: “NodeMCU-32S, 80MHz, 921600”

    Presumably I’ve got something wrong in my IDE environment – any suggestions?
    Thanks.

    Reply
    • Hi.
      I’ve just tested the code for your specific board and it compiles fine.
      Are you try to run the code as it is or did you change anything?
      Regards,
      Sara

      Reply
    • It is croaking on the % at the end. character 0xa is the \n, and it sees %\n like %i %s %d, etc., and doesn’t recognize the ” \n”. To include a % inside a printf, use %%. [A space is a valid flag after a % meaning reserve a column for optional -, then it looks at the next character for type.]

      Use: Serial.printf(“Humidity = %.2f %%\n”, humidity);

      Reply
  7. Many thanks Sara. I’ve tried again, copying the code directly from ‘Raw Code’, as well as from where you say ‘copy the following code …’ and still get the same error result.
    Your other server examples still compile with no problems on my iMac. However, I have had some recent problems where some of my Arduino files located themselves onto my iCloud drive, and I have been trying to untangle them. I suspect I have upset something whilst doing that but it’s strange that all else works fine.

    Reply
  8. An update on my comment of 40 mins ago …

    I can get round the above compile error by changing:
    Serial.printf(“Humidity = %.2f % \n”, humidity);
    to:
    Serial.printf(“Humidity = %.2f %% \n”, humidity);
    But then I get another compile error;
    /Users/imac2019/Documents/Arduino/sketch_oct30a/sketch_oct30a.ino: In function ‘String processor(const String&)’:
    sketch_oct30a:79:1: error: control reaches end of non-void function [-Werror=return-type]
    }

    and I can get round that error by adding the line:
    return String(“Happy Christmas”);
    before the last curly bracket of the function:
    String processor(const String& var).

    Have to say I don’t fully understand what I am doing, but it compiles now. Am I the only one to have these hiccups?

    Many thanks for all these really useful web pages. I am trying to make a temperature sensor to put in my fridge and communicate to the fridge pump controller using WiFi or BLE but I am having problems with large amount of current needed by the ESP32 board. Have you done any tutorials on making the board go to sleep for a period of time between sending temperature readings?

    Reply
  9. I tried the “ESP32 Web Server using Server-Sent Events (SSE) program that gives me satisfaction. Nevertheless, I would like to do a local server version with “SoftAP”. A quick search for information seems to prove incompatibilities with this process. Do you think this is possible and how?

    Reply
  10. Hi Ruis, Hi Sara
    I downloaded the needed libraries and installed them as add zip-library
    then I tried to compile the code.
    But I get a lot of compiler-errors

    here some of them
    C:\Users\Stefan\Documents\Arduino\libraries\AsyncTCP-master\src\AsyncTCP.cpp:291:65: error: invalid conversion from ‘err_t ()(tcpip_api_call) {aka signed char ()(tcpip_api_call)}’ to ‘tcpip_api_call_fn {aka signed char ()(tcpip_api_call_data)}’ [-fpermissive]

    tcpip_api_call(_tcp_output_api, (struct tcpip_api_call*)&msg);

    ^

    C:\Users\Stefan\Documents\Arduino\libraries\AsyncTCP-master\src\AsyncTCP.cpp:291:65: error: cannot convert ‘tcpip_api_call‘ to ‘tcpip_api_call_data‘ for argument ‘2’ to ‘err_t tcpip_api_call(tcpip_api_call_fn, tcpip_api_call_data*)’

    C:\Users\Stefan\Documents\Arduino\libraries\AsyncTCP-master\src\AsyncTCP.cpp:259:12: note: class type ‘tcpip_api_call’ is incomplete

    struct tcpip_api_call call;

    ^

    C:\Users\Stefan\Documents\Arduino\libraries\AsyncTCP-master\src\AsyncTCP.cpp: In function ‘esp_err_t _tcp_write(tcp_pcb*, const char*, size_t, uint8_t)’:

    C:\Users\Stefan\Documents\Arduino\libraries\AsyncTCP-master\src\AsyncTCP.cpp:307:64: error: invalid conversion from ‘err_t ()(tcpip_api_call) {aka signed char ()(tcpip_api_call)}’ to ‘tcpip_api_call_fn {aka signed char ()(tcpip_api_call_data)}’ [-fpermissive]

    I guess this is caused by a invalid combination of libraries
    How can I setup up a second Arduino-IDE that works independent from the first?
    I guess it will be a big hassle to sort out all the installed libraries I have
    and i don’t want to install the Arduino-IDE new

    Just installing another Arduino-IDE does NOT work! It uses the same sketch and library-folders as the main IDE

    Reply
  11. finally found a solution. I had an older version of AsyncTCP installed.
    This caused the compiler-error-messages.
    So I deleted all asyncTCP-folders on my harddisk and installed them new

    Reply

Leave a Comment

Download our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.