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.



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!

20 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

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.