ESP8266 NodeMCU: ESP-NOW Web Server Sensor Dashboard (ESP-NOW + Wi-Fi)

In this project you’ll learn how to host a web server with the ESP8266 NodeMCU board and use ESP-NOW communication protocol at the same time. You can have several ESP8266 boards sending sensor readings via ESP-NOW to one ESP8266 receiver that displays all readings on a web server. The boards will be programmed using Arduino IDE.

ESP8266 NodeMCU: ESP-NOW Web Server Sensor Dashboard using Arduino IDE (ESP-NOW and Wi-Fi simultaneously)

We have other guides related to ESP-NOW that you might be interested in:

Using ESP-NOW and Wi-Fi Simultaneously

Using ESP-NOW and Wi-Fi Simultaneously: ESP-NOW Receiver Web Server and ESP-NOW Sender boards with ESP8266 NodeMCU

There are a few things you need to take into account if you want to use Wi-Fi to host a web server and use ESP-NOW simultaneously to receive sensor readings from other boards:

  • The ESP8266 sender boards must use the same Wi-Fi channel of the receiver board.
  • The Wi-Fi channel of the receiver board is automatically assigned by your Wi-Fi router.
  • The Wi-Fi mode of the receiver board must be access point and station (WIFI_AP_STA).
  • You can set up the same Wi-Fi channel manually, or you can add a simple spinet of code on the sender to set its Wi-Fi channel to the same of the receiver board.

Project Overview

The following diagram shows a high-level overview of the project we’ll build.

ESP-NOW Receiver Web Server and ESP32 boards sending temperature humidity readings with ESP-NOW using ESP8266 NodeMCU boards
  • There are two ESP8266 sender boards that send BME280 temperature and humidity readings via ESP-NOW to one ESP8266 receiver board (ESP-NOW many to one configuration);
  • The ESP8266 receiver board receives the packets and displays the readings on a web server;
  • The web page is updated automatically every time it receives a new reading using Server-Sent Events (SSE).
  • The web page also shows the last time the readings were updated using JavaScript.

Prerequisites

Before proceeding with this project, make sure you check the following prerequisites.

Arduino IDE

We’ll program the ESP8266 boards using Arduino IDE, so before proceeding with this tutorial, make sure you have the ESP8266 board installed in your Arduino IDE.

BME280 Libraries

The ESP8266 sender board will send temperature and humidity readings from a BME280 sensor.

To read from the BME280 sensor, we’ll use the Adafruit_BME280 library. To use this library you also need to install the Adafruit Unified Sensor library. Follow the next steps to install those libraries.

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

Install BME280 sensor library in Arduino IDE

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

Go to Sketch Include Library > Manage Libraries and type “Adafruit Unified Sensor” in the search box. Scroll all the way down to find the library and install it.

Install Adafruit unified sensor library in Arduino IDE

To learn more about the BME280 temperature, humidity and pressure sensor, read our guide: ESP8266 with BME280 using Arduino IDE (Pressure, Temperature, Humidity).

Async Web Server Libraries

To build the web server you need to install the following 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.

Arduino_JSON Library

You need to install the Arduino_JSON library. You can install this library in the Arduino IDE Library Manager. Just go to Sketch Include Library > Manage Libraries and search for the library name as follows:

Install Arduino JSON library Arduino IDE

Parts Required

To follow this tutorial, you need multiple ESP8266 boards. We’ll use three ESP8266 boards. You also need:

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!

Getting the Receiver Board MAC Address

To send messages via ESP-NOW, you need to know the receiver board’s MAC address. Each board has a unique MAC address (learn how to Get and Change the ESP8266 MAC Address).

Upload the following code to your ESP8266 receiver board to get its MAC address.

// Complete Instructions to Get and Change ESP MAC Address: https://RandomNerdTutorials.com/get-change-esp32-esp8266-mac-address-arduino/

#ifdef ESP32
  #include <WiFi.h>
#else
  #include <ESP8266WiFi.h>
#endif

void setup(){
  Serial.begin(115200);
  Serial.println();
  Serial.print("ESP Board MAC Address:  ");
  Serial.println(WiFi.macAddress());
}
 
void loop(){

}

View raw code

After uploading the code, press the RST/EN button, and the MAC address should be displayed on the Serial Monitor.

ESP32 board MAC Address with Arduino IDE Serial Monitor

ESP8266 Receiver (ESP-NOW + Web Server)

The ESP8266 NodeMCU receiver board receives the packets from the sender boards and hosts a web server to display the latest received readings.

Upload the following code to your receiver board – the code is prepared to receive readings from two different boards.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp8266-esp-now-wi-fi-web-server/
  
  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 <espnow.h>
#include <ESP8266WiFi.h>
#include "ESPAsyncWebServer.h"
#include "ESPAsyncTCP.h"
#include <Arduino_JSON.h>

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

// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
  int id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

struct_message incomingReadings;

JSONVar board;

AsyncWebServer server(80);
AsyncEventSource events("/events");

// callback function that will be executed when data is received
void OnDataRecv(uint8_t * mac_addr, uint8_t *incomingData, uint8_t len) { 
  // Copies the sender mac address to a string
  char macStr[18];
  Serial.print("Packet received from: ");
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.println(macStr);
  memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));
  
  board["id"] = incomingReadings.id;
  board["temperature"] = incomingReadings.temp;
  board["humidity"] = incomingReadings.hum;
  board["readingId"] = String(incomingReadings.readingId);
  String jsonString = JSON.stringify(board);
  events.send(jsonString.c_str(), "new_readings", millis());
  
  Serial.printf("Board ID %u: %u bytes\n", incomingReadings.id, len);
  Serial.printf("t value: %4.2f \n", incomingReadings.temp);
  Serial.printf("h value: %4.2f \n", incomingReadings.hum);
  Serial.printf("readingID value: %d \n", incomingReadings.readingId);
  Serial.println();
}

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP-NOW DASHBOARD</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;}
    h1 {  font-size: 2rem;}
    body {  margin: 0;}
    .topnav { overflow: hidden; background-color: #2f4468; color: white; font-size: 1.7rem; }
    .content { padding: 20px; }
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
    .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
    .reading { font-size: 2.8rem; }
    .timestamp { color: #bebebe; font-size: 1rem; }
    .card-title{ font-size: 1.2rem; font-weight : bold; }
    .card.temperature { color: #B10F2E; }
    .card.humidity { color: #50B8B4; }
  </style>
</head>
<body>
  <div class="topnav">
    <h1>ESP-NOW DASHBOARD</h1>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card temperature">
        <p class="card-title"><i class="fas fa-thermometer-half"></i> BOARD #1 - TEMPERATURE</p><p><span class="reading"><span id="t1"></span> &deg;C</span></p><p class="timestamp">Last Reading: <span id="rt1"></span></p>
      </div>
      <div class="card humidity">
        <p class="card-title"><i class="fas fa-tint"></i> BOARD #1 - HUMIDITY</p><p><span class="reading"><span id="h1"></span> &percnt;</span></p><p class="timestamp">Last Reading: <span id="rh1"></span></p>
      </div>
      <div class="card temperature">
        <p class="card-title"><i class="fas fa-thermometer-half"></i> BOARD #2 - TEMPERATURE</p><p><span class="reading"><span id="t2"></span> &deg;C</span></p><p class="timestamp">Last Reading: <span id="rt2"></span></p>
      </div>
      <div class="card humidity">
        <p class="card-title"><i class="fas fa-tint"></i> BOARD #2 - HUMIDITY</p><p><span class="reading"><span id="h2"></span> &percnt;</span></p><p class="timestamp">Last Reading: <span id="rh2"></span></p>
      </div>
    </div>
  </div>
<script>
function getDateTime() {
  var currentdate = new Date();
  var datetime = currentdate.getDate() + "/"
  + (currentdate.getMonth()+1) + "/"
  + currentdate.getFullYear() + " at "
  + currentdate.getHours() + ":"
  + currentdate.getMinutes() + ":"
  + currentdate.getSeconds();
  return datetime;
}
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('new_readings', function(e) {
  console.log("new_readings", e.data);
  var obj = JSON.parse(e.data);
  document.getElementById("t"+obj.id).innerHTML = obj.temperature.toFixed(2);
  document.getElementById("h"+obj.id).innerHTML = obj.humidity.toFixed(2);
  document.getElementById("rt"+obj.id).innerHTML = getDateTime();
  document.getElementById("rh"+obj.id).innerHTML = getDateTime();
 }, false);
}
</script>
</body>
</html>)rawliteral";

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);

  // Set the device as a Station and Soft Access Point simultaneously
  WiFi.mode(WIFI_AP_STA);
  
  // Set device as a Wi-Fi Station
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Setting as a Wi-Fi Station..");
  }
  Serial.print("Station IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.print("Wi-Fi Channel: ");
  Serial.println(WiFi.channel());

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info
  esp_now_register_recv_cb(OnDataRecv);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });
   
  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() {
  static unsigned long lastEventTime = millis();
  static const unsigned long EVENT_INTERVAL_MS = 5000;
  if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
    events.send("ping",NULL,millis());
    lastEventTime = millis();
  }
}

View raw code

How the Code Works

First, include the necessary libraries.

#include <espnow.h>
#include <ESP8266WiFi.h>
#include "ESPAsyncWebServer.h"
#include "ESPAsyncTCP.h"
#include <Arduino_JSON.h>

The Arduino_JSON library is needed because we’ll create a JSON variable with the data received from each board. This JSON variable will be used to send all the needed information to the web page as you’ll see later in this project.

Insert your network credentials on the following lines so that the ESP8266 can connect to your local network.

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

Data Structure

Then, create a structure that contains the data we’ll receive. We called this structure struct_message and it contains the board ID, temperature and humidity readings, and the reading ID.

typedef struct struct_message {
    int id;
    float temp;
    float hum;
    int readingId;
} struct_message;

Create a new variable of type struct_message that is called incomingReadings that will store the variables values.

struct_message incomingReadings;

Create a JSON variable called board.

JSONVar board;

Create an Async Web Server on port 80.

AsyncWebServer server(80);

Create Event Source

To automatically display the information on the web server when a new reading arrives, we’ll use Server-Sent Events (SSE).

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

AsyncEventSource events("/events");

Server-Sent Events allow a web page (client) to get updates from a server. We’ll use this to automatically display new readings on the web server page when a new ESP-NOW packet arrives.

Important: Server-sent events are not supported on Internet Explorer.

OnDataRecv() function

The OnDataRecv() function will be executed when you receive a new ESP-NOW packet.

void OnDataRecv(uint8_t * mac_addr, uint8_t *incomingData, uint8_t len) { 

Inside that function, print the sender’s MAC address:

char macStr[18];
Serial.print("Packet received from: ");
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
         mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.println(macStr);

Copy the information in the incomingData variable into the incomingReadings structure variable.

memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));

Then, create a JSON String variable with the received information (jsonString variable):

board["id"] = incomingReadings.id;
board["temperature"] = incomingReadings.temp;
board["humidity"] = incomingReadings.hum;
board["readingId"] = String(incomingReadings.readingId);
String jsonString = JSON.stringify(board);

Here’s an example on how the jsonString variable may look like after receiving the readings:

board = {
  "id": "1",
  "temperature": "24.32",
  "humidity" = "65.85",
  "readingId" = "2"
}

After gathering all the received data on the jsonString variable, send that information to the browser as an event (“new_readings”).

events.send(jsonString.c_str(), "new_readings", millis());

Later, we’ll see how to handle these events on the client side.

Finally, print the received information on the Arduino IDE Serial Monitor for debugging purposes:

Serial.printf("Board ID %u: %u bytes\n", incomingReadings.id, len);
Serial.printf("t value: %4.2f \n", incomingReadings.temp);
Serial.printf("h value: %4.2f \n", incomingReadings.hum);
Serial.printf("readingID value: %d \n", incomingReadings.readingId);
Serial.println();

Building the Web Page

The index_html variable contains all the HTML, CSS and JavaScript to build the web page. We won’t go into details on how the HTML and CSS works. We’ll just take a look at how to handle the events sent by the server.

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 “new_readings”.

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

When the ESP8266 receives a new packet, it sends a JSON string with the readings as an event (“new_readings”) to the client. The following lines handle what happens when the browser receives that event.

console.log("new_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("t"+obj.id).innerHTML = obj.temperature.toFixed(2);
document.getElementById("h"+obj.id).innerHTML = obj.humidity.toFixed(2);
document.getElementById("rt"+obj.id).innerHTML = getDateTime();
document.getElementById("rh"+obj.id).innerHTML = getDateTime();

Basically, print the new readings on the browser console, and put the received data into the elements with the corresponding id on the web page. We also update the date and time the readings were received by calling the getDateTime() JavaScript function.

function getDateTime() {
  var currentdate = new Date();
  var datetime = currentdate.getDate() + "/"
  + (currentdate.getMonth()+1) + "/"
  + currentdate.getFullYear() + " at "
  + currentdate.getHours() + ":"
  + currentdate.getMinutes() + ":"
  + currentdate.getSeconds();
  return datetime;
}

setup()

In the setup(), set the ESP8266 receiver as an access point and Wi-Fi station:

WiFi.mode(WIFI_AP_STA);

The following lines connect the ESP8266 to your local network and print the IP address and the Wi-Fi channel:

// Set device as a Wi-Fi Station
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Setting as a Wi-Fi Station..");
}
Serial.print("Station IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Wi-Fi Channel: ");
Serial.println(WiFi.channel());

Initialize ESP-NOW.

if (esp_now_init() != 0) {
  Serial.println("Error initializing ESP-NOW");
  return;
}

Register for the OnDataRecv callback function, so that it is executed when a new ESP-NOW packet arrives.

esp_now_register_recv_cb(OnDataRecv);

Handle Requests

When you access the ESP8266 IP address on the root / URL, send the text that is stored on the index_html variable to build the web page.

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

Server Event Source

Set up the event source on the server.

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(), send a ping every 5 seconds. This is used to check on the client side, if the server is still running (these lines are not mandatory).

static unsigned long lastEventTime = millis();
static const unsigned long EVENT_INTERVAL_MS = 5000;
if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
  events.send("ping",NULL,millis());
  lastEventTime = millis();
}

The following diagram summarizes how the Server-sent Events work on this project and how it updates the values without refreshing the web page.

ESP8266 ESP-NOW Web Server Sensor Dashboard Project Overview

After uploading the code to the receiver board, press the on-board EN/RST button. The ESP8266 IP address should be printed on the Serial Monitor as well as the Wi-Fi channel.

ESP-NOW get ESP32 IP address and Wi-Fi channel

ESP8266 Sender Circuit

The ESP8266 sender boards are connected to a BME280 sensor. Wire the sensor to the default ESP8266 I2C pins:

  • GPIO 5 (D1) -> SCL
  • GPIO 4 (D2) -> SDA
ESP8266 NodeMCU BME280 Sensor Temperature Humidity Pressure Wiring Diagram Circuit

ESP8266 Sender Code (ESP-NOW)

Each sender board will send a structure via ESP-NOW that contains the board ID (so that you can identify which board sent the readings), the temperature, the humidity and the reading ID. The reading ID is an int number to know how many messages were sent.

ESP8266 Sender Receiver Board with ESP-NOW using Arduino IDE

Upload the following code to each of your sender boards. Don’t forget to increment the id number for each sender board and insert your SSID in the WIFI_SSID variable.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp8266-esp-now-wi-fi-web-server/
  
  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 <espnow.h>
#include <ESP8266WiFi.h>
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>

// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 2

Adafruit_BME280 bme; 

//MAC Address of the receiver 
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

//Structure example to send data
//Must match the receiver structure
typedef struct struct_message {
    int id;
    float temp;
    float hum;
    int readingId;
} struct_message;

//Create a struct_message called myData
struct_message myData;

unsigned long previousMillis = 0;   // Stores last time temperature was published
const long interval = 10000;        // Interval at which to publish sensor readings

unsigned int readingId = 0;

// Insert your SSID
constexpr char WIFI_SSID[] = "MEO-D32A40";

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
    for (uint8_t i=0; i<n; i++) {
      if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
        return WiFi.channel(i);
      }
    }
  }
  return 0;
}

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

float readTemperature() {
  float t = bme.readTemperature();
  return t;
}

float readHumidity() {
  float h = bme.readHumidity();
  return h;
}

// Callback when data is sent
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) {
  Serial.print("Last Packet Send Status: ");
  if (sendStatus == 0){
    Serial.println("Delivery success");
  }
  else{
    Serial.println("Delivery fail");
  }
}
 
void setup() {
  //Init Serial Monitor
  Serial.begin(115200);
  initBME(); 

  // Set device as a Wi-Fi Station and set channel
  WiFi.mode(WIFI_STA);

  int32_t channel = getWiFiChannel(WIFI_SSID);

  WiFi.printDiag(Serial); // Uncomment to verify channel number before
  wifi_promiscuous_enable(1);
  wifi_set_channel(channel);
  wifi_promiscuous_enable(0);
  WiFi.printDiag(Serial); // Uncomment to verify channel change after

  // Init ESP-NOW
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
   esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);

  esp_now_register_send_cb(OnDataSent);
  
  esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);
}
 
void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    // Save the last time a new reading was published
    previousMillis = currentMillis;
    //Set values to send
    myData.id = BOARD_ID;
    myData.temp = readTemperature();
    myData.hum = readHumidity();
    myData.readingId = readingId++;
     
    esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

    Serial.print("loop");
  }
}

View raw code

How the Code Works

Start by importing the required libraries:

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

Set Board ID

Define the ESP8266 sender board ID, for example set BOARD_ID 1 for ESP8266 Sender #1, etc…

#define BOARD_ID 1

BME280 Sensor

Create an Adafruit_BME280 object called bme.

Adafruit_BME280 bme;

Receiver’s MAC Address

Insert the receiver’s MAC address on the next line (for example):

uint8_t broadcastAddress[] = {0x30, 0xAE, 0xA4, 0x15, 0xC7, 0xFC};

Data Structure

Then, create a structure that contains the data we want to send. The struct_message contains the board ID, temperature reading, humidity reading, and the reading ID.

typedef struct struct_message {
    int id;
    float temp;
    float hum;
    int readingId;
} struct_message;

Create a new variable of type struct_message that is called myData that stores the variables’ values.

struct_message myData;

Timer Interval

Create some auxiliary timer variables to publish the readings every 10 seconds. You can change the delay time on the interval variable.

unsigned long previousMillis = 0; // Stores last time temperature was published
const long interval = 10000; // Interval at which to publish sensor readings

Initialize the readingId variable – it keeps track of the number of readings sent.

unsigned int readingId = 0;

Changing Wi-Fi channel

Now, we’ll get the receiver’s Wi-Fi channel. This is useful because it allows us to automatically assign the same Wi-Fi channel to the sender board.

To do that, you must insert your SSID in the following line:

constexpr char WIFI_SSID[] = "REPLACE_WITH_YOUR_SSID";

Then, the getWiFiChannel() function scans for your network and gets its channel.

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
    for (uint8_t i=0; i<n; i++) {
      if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
        return WiFi.channel(i);
      }
    }
  }
  return 0;
}

This snippet of code was proposed by Stephane (one of our readers). You can see his complete example here.

Initialize BME280 Sensor

The initBME() function initializes the BME280 sensor.

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

Reading Temperature

The readTemperature() function reads and returns the temperature from the BME280 sensor.

float readTemperature() {
  float t = bme.readTemperature();
  return t;
}

Reading Humidity

The readHumidity() function reads and returns the humidity from the BME280 sensor.

float readHumidity() {
  float h = bme.readHumidity();
  return h;
}

Note: to learn more about getting temperature and humidity from the BME280 sensor, read: ESP8266 with BME280 using Arduino IDE (Pressure, Temperature, Humidity).

OnDataSent Callback Function

The OnDataSent() callback function will be executed when a message is sent. In this case, this function prints if the message was successfully delivered or not.

void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) {
  Serial.print("Last Packet Send Status: ");
  if (sendStatus == 0){
    Serial.println("Delivery success");
  }
  else{
    Serial.println("Delivery fail");
  }
}

setup()

Initialize the Serial Monitor.

Serial.begin(115200);

Initialize the BME280 sensor:

initBME();

Set the ESP8266 as a Wi-Fi station.

WiFi.mode(WIFI_STA);

Set its channel to match the receiver’s Wi-Fi channel:

int32_t channel = getWiFiChannel(WIFI_SSID);

WiFi.printDiag(Serial); // Uncomment to verify channel number before
wifi_promiscuous_enable(1);
wifi_set_channel(channel);
wifi_promiscuous_enable(0);
WiFi.printDiag(Serial); // Uncomment to verify channel change after

Initialize ESP-NOW.

// Init ESP-NOW
if (esp_now_init() != 0) {
  Serial.println("Error initializing ESP-NOW");
  return;
}

Set the ESP8266 role:

esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);

After successfully initializing ESP-NOW, register the callback function that will be called when a message is sent. In this case, register for the OnDataSent() function created previously.

esp_now_register_send_cb(OnDataSent);

Add peer

To send data to another board (the receiver), you need to pair it as a peer. The following lines register and add the receiver as a peer.

esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);

loop()

In the loop(), check if it is time to get and send new readings.

unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
  // Save the last time a new reading was published
  previousMillis = currentMillis;

Send ESP-NOW Message

Finally, send the message structure via ESP-NOW.

esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

Recommended reading: Getting Started with ESP-NOW (ESP8266 with Arduino IDE)

Upload the code to your sender boards. You should notice that the boards change its Wi-Fi channel to the channel of the receiver board.

Demonstration

After uploading the code to all the boards and if everything is going as expected, the ESP8266 receiver board should start receiving sensor readings from the other boards.

Open a browser on your local network and type the ESP8266 IP address.

ESP8266 NodeMCU ESP-NOW Web Server Sensor Dashboard ESP-NOW and Wi-Fi Demonstration Sensor Readings

It should load the temperature, humidity and the last time the readings were updated on the web page for each board. Upon receiving a new packet, your web page updates automatically without refreshing the web page.

ESP8266 NodeMCU ESP-NOW Web Server Sensor Dashboard Demo

Wrapping Up

In this tutorial you’ve learned how to use ESP-NOW and Wi-Fi to setup a web server to receive ESP-NOW packets from multiple boards (many-to-one configuration) using the ESP8266 NodeMCU board. We have a similar project using the ESP32:

Additionally, you also used Server-Sent Events to automatically update the web page every time a new packet is received without refreshing the web page. We have a dedicated guide to Server-Sent Events:

If you like ESP8266, you might consider enrolling in our eBook “Home Automation using ESP8266“. You can also access our free ESP8266 resources here.

Thanks for reading.



Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »

Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »


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

33 thoughts on “ESP8266 NodeMCU: ESP-NOW Web Server Sensor Dashboard (ESP-NOW + Wi-Fi)”

  1. Hi
    What tool do you guys use to create the dashboard.

    It is really difficult to manage / edit them with just a text editor.
    Also to see the display without doing a whole code build and program
    Ps nice project

    Reply
    • Hi.
      The dashboard is built using HTML + CSS.
      You can use any text editor to write your HTML and CSS to build your web page.
      We use VS Code.
      Regards,
      Sara

      Reply
  2. Hi,

    very interesting project. This is something I have looked for a long time. If my family gives me the time; – this project is my top prio.

    Thanks for your ideas.

    By the way: For what do you need 2x 4.7k Ohm resistor; listed?

    Konrad

    Reply
    • Hi.
      Accordingly to the documentation:
      Limited encrypted peers. 10 encrypted peers at the most are supported in Station mode; 6 at the most in SoftAP or SoftAP + Station mode;
      Multiple unencrypted peers are supported, however, their total number should be less than 20, including encrypted peers;
      Regards,
      Sara

      Reply
  3. Hi again,

    just a stupid question: Is it possible to realise this project also with different ESPs, like one ESP32 and two ESP8266; – and visa verso?

    I just saw, that I have two ESP8266 left (of which one I wanted to take as receiver) and there was just another ESP32 left.

    Hope this is not too stupid to ask.

    Thanks

    Konrad

    Reply
    • Hi,

      For your info: I have tried it (together with both tutorials (ESP32 and ESP8266) and it worked perfectly.

      Therefore ignore the question above.

      Again: This tutorial has solved some of my problems with MQTT / Raspi / Nodered connection. Big, big thanks to you.

      Konrad

      Reply
      • Shit…. Murphy’s law. It stopped working correctly.

        Now I get the following message:

        Station IP Address: 192.168.178.34
        Wi-Fi Channel: 6

        Client reconnected! Last message ID that it got is: 671831

        And then nothing happens. I also think the Wi-Fi Channal was 1 when it worked well.

        What did I wrong? Sorry; – I really feel now like a complete fool.

        Konrad

        I

        Reply
    • It’s not a stupid question…I’m having a problem using 2 ESP8266 as Senders and 1 ESP32 as Receiver. Different mix works (ESP32 Sender and ESP826 Receiver).
      I think that it’s a matter of channel assigment since the ESP is probably still working with the default channel (1) instead of 9 of the router. I raise the question to Rui… waiting.

      Regards

      Fiorenzo

      Reply
  4. Hi,

    Concerning my issue above: I think having found the issue and solved it. Or let me say; – it works since 5 days without complete stop.

    Let me first explain the situation:
    a.) I used 3 ESP8266 (I excluded the ESP32 to avoid further potential other issues; – although I think I know how to solve it, if the ESP32 is a sender).
    b.) I started as discribed above with the senders -> no issues
    c.) Then the receiver as discribed above. -> no issues

    After a while; I found out in the browser client: No data sent! But Why?
    It took me some time, but I could see, that the WiFi-Class seems to be different- But Why?

    I could see, that this happened on 2 occaisons:
    1.) Sometimes I had to adapt the receivers sketch. Suddenly there was this issue. Not always on all Senders and not always every time. Obviously the senders had different classes to the receiver.
    2.) It happened often, when I switched off all browser clients (for example at night). After re-opening the browser the above mentionned issue happened once in a while, but regularly.

    I decided to program a ESP Restart in the callback function. Very stupid like this, with a counter to avoid that it restarts to early:

    // Callback when data is sent
    void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)
    {
    Serial.print(“Last Packet Send Status: “);
    if (sendStatus == 0)
    {
    Serial.println(“Delivery success”);
    }
    else
    {
    Serial.println(“Delivery fail”);
    if (zaehler < 5)
    {
    zaehler = zaehler + 1;
    }
    else
    {
    zaehler = 0;
    //somit klappt die Verbindung nicht—> Restart des ESP8266
    ESP.restart();
    }
    }
    }

    Since then no issues. This means, that if the sender does not find the receiver it restarts and checks again the WiFi class, etc.

    I am sure that the same is applicable for the ESP32 (not tested yet).

    Hope that helps others. If not just ignore it.

    Neverthelss Sara and Rui –> you are so wonderfull with all your ideas for projects.
    Thanks.

    Konrad

    Reply
  5. Hello,
    congrats for all of those tutorials, especially for this one. I was looking for that kind of thinks for such a long time! I’ve got a poor programmation level, as far as my english level so, I succed to use DHT 22 instead of BMP80, but I didn’t succed to have 3 and more Sending ESPs. My Receiver is a ESP 12E, and the senders are ESP01. No trouble with that so far.
    Even if I succed to change the home page with 3 differents “Boards”, the Board#3 never upload with the temperature and humidity. But on the receiver’s serial, all of the 3 boards data are well loaded. I you can highlight my, I’ll appreciate! Anyway, congratulations again!

    Reply
    • it’s OK now. I understood it was a problem of HTML. I made the comparisson line after line and found the problem. So happy! Thanks again!

      Reply
  6. Instead of the slave being a webserver I set the slave to communicate with Telegram

    I have to slow the rate at which the sender sends data to the slave. If I do not slow the sender, new data will arrive before Telegram acts and I will not get any motion sensor notification that is connected to the sender. Even the data arrives corrrectly at the slave.

    I also got the slave (the AP board that is connected to the router aand theerfore to Telegram) to send commands to the other board(to the sendor board)

    So both boards send and recive but only one is access point.

    The conclusion is that the board that has the telegram code must recive data at slow rate. Othherwise I will not get a messagee from telegram

    If any one knows why please tell me

    All the modified code is based on examples from this sight

    Thanks Sara and Rui

    PhilipTheArab

    Reply
  7. Is it possible to display the min and max values of the temp and humidity readings over a 24hr period? Perhaps where is says Last reading under the Temp and Humidity?.

    Reply
  8. Hi
    This is a very interesting project. Thank you Sara and Rui.
    Although my knowledge is very low, I made some changes and was able to simultaneously use 3 senders (ESP8266-01 with DS18B20) on the receiver (ESP8266-12F).
    Now I am trying to add the possibility of connecting 3 relays on the receiver to control them using a threshold field (already added to each of the sender cards). That is, if the received temperature is below the threshold, the relay should be ON but if the temperature is above this value, the relay should go OFF. This is where I am now lost because I cannot figure out how to use in the relays the temperature values received.
    For the output (to control the relays), I am using this project as a reference: https://randomnerdtutorials.com/esp32-esp8266-thermostat-web-server/
    Is there anyone who can give me some ideas on how I can move forward?
    Thank you.
    Paulo

    Reply
      • Hello Sara,
        Well, that tutorial I was able to do, too.
        I think I do not explain well myself. What I am not able to understand is how I can use the values received from the sender boards to make the receiver board control the relays.
        In your tutorial https://randomnerdtutorials.com/esp32-esp8266-thermostat-web-server/ I replace the LED with a RELAY, and it works well. Relay opens and closes according to the temperature read by the sensor.
        In this https://randomnerdtutorials.com/esp8266-esp-now-wi-fi-web-server/, I built 3 “ESP-NOW Sender” cards, where I replace BME280 sensor with a DS18B20.
        I also built an “ESP-NOW Receiver” and eliminated the moisture cards. On each of the three temperature cards I placed a field for Threshold, a “Submit” button, and an ON/OFF flag (as in thermostat-web-server). In the DASHBOARD the three cards work well and change the desired temperature (Threshold), also works well.
        My problem now is how to use the temperature value that is represented on each card to activate the corresponding relay. When using:
        float temperature01 = incomingReadings.temp; //From sender “board#1” to Relay01
        And
        float temperature02 = incomingReadings.temp; //From sender “board#2” to Relay02
        the relays open and close depending on the last temperature received, regardless of which was the sender board, of course.
        Now, what I am trying to do is create something like:
        float temperature01 = incomingReadingsBoard#1.temp; //From sender “board#1” to Relay01
        And
        float temperature02 = incomingReadingsBoard#2.temp; //From sender “board#2” to Relay02
        And
        float temperature03 = incomingReadingsBoard#3.temp; //From sender “board#3″ to Relay03

        Then I will use something similar to this (from the other project):
        float temperature02 = incomingReadingsBoard#2.temp; //sensors.getTempCByIndex(0);
        Serial.print(temperature02);
        Serial.println(” *C”);

        lastTemperature02 = String(temperature02);

        // Check if temperature is above threshold and if it needs to trigger Relay
        if(temperature02 > inputMessageA02.toFloat() && inputMessageB02 == "true" && !triggerActive){
        String message = String("Temperature above threshold. Current temperature: ") +
        String(temperature01) + String("C");
        Serial.println(message);
        triggerActive = true;
        digitalWrite(Relay02, HIGH);
        }
        // Check if temperature is below threshold and if it needs to trigger Relay
        else if((temperature02 < inputMessageA02.toFloat()) && inputMessageB02 == "true" && triggerActive) {
        String message = String("Temperature below threshold. Current temperature: ") +
        String(temperature01) + String(" C");
        Serial.println(message);
        triggerActive = false;
        digitalWrite(Relay02, LOW);

        I will do some more research… 🙂
        Thank you.
        Paulo

        Reply
        • Hi.
          You need to keep in mind that if each relay is controlled by a specific temperature sensor, then, you need to have different variables for each relay.
          Regards,
          Sara

          Reply
  9. First, I love your tutorials!
    Second, this one is spot on for something I am trying to do – this is a perfect foundational element
    Third, it complied and worked great using the Arduino IDE.

    So foremost, thank you very much!!!

    However, I am migrating my projects to VS Code for multiple reasons and haven’t been able to get this to compile using the PlatformIO extension even though every other one of your tutorials I tried compiled and ran fine.

    The (first) error I am getting is:

    C:\users\bs-ma.platformio\packages\framework-arduinoespressif8266\tools\sdk\include/espnow.h:41:35: error: typedef ‘esp_now_recv_cb_t’ is initialized (use decltype instead)

    (There are subsequent ones, but I think it is a cascading problem.)
    Clearly this is my problem not yours, but I would appreciate any insights you may have about how to get my PlatformIO configuration straightened out or a tip for a resource to investigate?

    Reply
    • Hi Robert.
      I’ve searched for a while, but unfortunately, I couldn’t find any solution.
      If you find something, please share it. It may be useful for other readers too.
      I’m sorry that I can’t help much.
      Regards,
      Sara

      Reply
      • Thank you for your efforts!

        Originally I was going to use the techniques in your excellent Websockets tutorial for the information sharing (which compiles fine under PlatformIO); but NOW is actually a little more attractive.

        The ESP-NOW tutorial also compiles fine, so there must be something conflicting with AsyncWebserver libraries and NOW in my installation.

        If / when I get it sorted out, I’ll let you know.

        Thanks again!

        Reply
  10. Hello how are you? I hope my English can be understood. I’ve searched, but can’t find information about the usefulness of the third boolean argument in source.addEventListener (). Could someone clarify this question? Thank you very much in advance for posting such useful information.

    Reply
  11. Great tutorial! Exactly what I was looking for. I just put some lines before esp_now_send() to emulate two BME devices for testing the sender code:

    long rand = random(0, 2000);
    myData.id = rand % 2 + 1;
    myData.temp = 10.0 + (float) rand / 100.0;
    myData.hum = 40.0 + (float) rand / 50.0;;
    myData.readingId = readingId++;

    Not searhing for the WiFi channel reduces the connection time of the sender to below 100ms! Therfore the ESP Now method is ideal for battery powered ESP applications where the sender goes to deepSleep after sending the data. Before I was using a REST like HTTP POST request which took about 400 to 1200ms to complete when not issuing a WiFi.begin() but relying o the persistent WiFi autoconnect instead. Ignoring the sleep current, a device should last at least 4 times longer with one set of batteries using ESP Now!

    Btw. nice looking and simple to use Web page implementation for the server part, I like it.

    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.