ESP32 CYD with ESP-NOW: Receive and Display Data From Multiple Boards

In this project, you’ll learn how to use the ESP-NOW communication protocol with the ESP32 CYD Board (Cheap Yellow Display) to receive and display data from multiple ESP32 sender boards. Two ESP32 boards will each read sensor data from a BME280 and send it to the CYD board. The CYD board, acting as the ESP-NOW receiver, will display the received data in separate tables—one for each sender—each shown on a different tab on the screen.

ESP32 CYD with ESP-NOW: Receive and Display Data From Multiple Boards

New to the ESP32 Cheap Yellow Display? Start here: Getting Started with ESP32 Cheap Yellow Display Board – CYD (ESP32-2432S028R).

Project Overview

Here’s an overview of how this project works.

ESP32 CYD Board Receive Data from Multiple ESP32 Boards via ESP-NOW - project overview
  • In this project, the ESP32 CYD board will act as a ESP-NOW receiver that receives data from other ESP32 boards.
  • The other ESP32 boards will be sending sensor data (BME280) to the CYD board periodically, via ESP-NOW.
  • The CYD board displays two tabs, each one for each board. Each tab contains a table with the data received from the other boards.
  • The tables are updated as soon as the CYD receives new data.

New to ESP-NOW communication protocol? Get started here: Getting Started with ESP-NOW (ESP32 with Arduino IDE).

Prerequisites

Before proceeding, make sure you follow the next prerequisites. You must follow all steps, otherwise, your project will not work.

1) Parts Required

For this project, 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!

2) Install ESP32 Boards in Arduino IDE

Arduino IDE 2 Logo

We’ll program the ESP32 using Arduino IDE. Make sure you have the ESP32 boards installed. Follow the next tutorial:

3) Get familiar with the ESP32 Cheap Yellow Display

The ESP32-2432S028R development board has become known in the maker community as the “Cheap Yellow Display” or CYD for short. This development board, whose main chip is an ESP32-WROOM-32 module, comes with a 2.8-inch TFT touchscreen LCD, a microSD card interface, an RGB LED, and all the required circuitry to program and apply power to the board.

ESP32 Cheap Yellow Display CYD Board ESP32-2432S028R front

If this is your first time using the ESP32 Cheap Yellow Display, make sure to follow our getting started guide:

4) Install TFT and LVGL Libraries

LVGL (Light and Versatile Graphics Library) is a free and open-source graphics library that provides a wide range of easy-to-use graphical elements for your microcontroller projects that require a graphical user interface (GUI).

LVGL new logo

Follow the next tutorial to install and configure the required libraries to use LVGL for the ESP32 Cheap Yellow Display using Arduino IDE.

5) Install BME280 Libraries

For this project, we’ll use the Adafruit BME280 library to get data from the BME280 on the other ESP32 boards. In the Arduino IDE, go to Sketch > Include Library > Manage Libraries. Search for Adafruit BME280 Library in the search box and install the library. Also, install any dependencies that are currently not installed (usually the Adafruit Bus IO and the Adafruit Unified Sensor libraries).

Installing Adafruit BME280 Sensor Library Arduino IDE

Getting the CYD Board MAC Address

First, you need to know the MAC address of the CYD board so that the sender boards can send messages to it.

ESP32 Cheap Yellow Display CYD Board ESP32-2432S028R front

Upload the following code to the CYD Board.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/get-change-esp32-esp8266-mac-address-arduino/
  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 <esp_wifi.h>

void readMacAddress(){
  uint8_t baseMac[6];
  esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);
  if (ret == ESP_OK) {
    Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
                  baseMac[0], baseMac[1], baseMac[2],
                  baseMac[3], baseMac[4], baseMac[5]);
  } else {
    Serial.println("Failed to read MAC address");
  }
}

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

  WiFi.mode(WIFI_STA);
  WiFi.STA.begin();

  Serial.print("[DEFAULT] ESP32 Board MAC Address: ");
  readMacAddress();
}
 
void loop(){

}

View raw code

After uploading, open the Serial Monitor at a baud rate of 115200. Press the onboard RST button. The board’s MAC Address will be printed in the Serial Monitor.

Getting the ESP32 CYD Board Mac Address on the Serial Monitor

Preparing the Sender Boards

For this tutorial, we’ll send data to the CYD from two different boards. You can modify this project to display data from more boards.

Each board will be identified by an ID (a number that we’ll attribute to each board):

  • ID=1 for board1
  • ID=2 for board2
Two ESP32 Boards with a BME280 Sensor

Wiring the BME280 Sensor

Each sender board will send environmental data from a BME280 sensor. Wire a BME280 sensor to each of your boards. We’ll use the ESP32 default I2C pins.

Learn more about I2C with the ESP32: ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE).

ESP32 BME280 Sensor Temperature Humidity Pressure Wiring Diagram Circuit

Not familiar with the BME280 with the ESP32? Read this tutorial: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).

If you’re using a different board model, the default I2C pins migh different.

ESP32 ESP-NOW Sender Code

The following code reads the data from the BME280 sensor and sends it via ESP-NOW to the CYD board. Don’t forget to modify the code with your CYD board’s MAC address. Additionally, don’t forget to change the board ID for each of your boards.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-cyd-esp-now-receive-data/
  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 <esp_now.h>
#include <WiFi.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 1

Adafruit_BME280 bme; 

// REPLACE WITH YOUR ESP RECEIVER'S MAC ADDRESS
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Structure 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;

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

esp_now_peer_info_t peerInfo;

// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  Serial.print("Packet to: ");
  // Copies the receiver mac address to a string
  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.print(macStr);
  Serial.print(" send status:\t");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}
 
void setup() {
  Serial.begin(115200);
  initBME(); 

  WiFi.mode(WIFI_STA);
 
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  
  esp_now_register_send_cb(OnDataSent);
   
  // Register peer
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  // Copy receiver's MAC address to peerInfo
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    Serial.println("Failed to add peer");
    return;
  }
}
 
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++;
     
    // Send data and check for errors
    esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
    Serial.print("Sending data (Reading ID: ");
    Serial.print(myData.readingId);
    Serial.print(", Temp: ");
    Serial.print(myData.temp);
    Serial.print("C, Hum: ");
    Serial.print(myData.hum);
    Serial.print("%): ");
    Serial.println(result == ESP_OK ? "Sent" : "Failed to send");
  }
}

View raw code

How Does the Code Work?

Let’s take a look at how the code works. If you’re new to ESP-NOW, we recommend getting started with this ESP-NOW guide to better understand how things work.

Including Libraries

Start to include the required libraries for this project. The esp_now and WiFi are required to use ESP-NOW communication, and the Adafruit_BME280 and Adafruit_Sensor are needed to interface with the BME280 sensor.

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

Set Your Board ID

Give a unique ID to your board so that the receiver can easily identify it. Here, we’re just numbering the boards, but you can use a different method, like giving them a name, or simply identifying them by the MAC address.

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

Make sure you give a different ID to each board.

BME280 Instance

Create an instance of the Adafruit_BME280 library called bme that will be used to interface with the sensor.

Adafruit_BME280 bme; 

The Receiver Board’s MAC Address

Insert your receiver board (the CYD board) MAC address. For example, my CYD board MAC address is 24:dc:c3:49:6a:14. I must add it to the code in the following format.

// REPLACE WITH YOUR ESP RECEIVER'S MAC ADDRESS
uint8_t broadcastAddress[] = {0x24, 0xDC, 0xC3, 0x49, 0x6A, 0x14};

Creating a Data Structure

Create a new structure called struct_message that will contain all the data you want to send. The id is the ID of the board, the temperature and humidity correspond to the data we get from the BME280 sensor, and the readingId is just a number to keep track of the number of readings sent.

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

Then, we create a new structure variable called myData based on the struct_message structure.

struct_message myData;

Initializing and Getting Data from the BME280 Sensor

We create several functions related to the BME280 sensor.

The initBME() initializes the BME280 sensor on the ESP32’s default I2C pins.

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

The readTemperature() function returns the current temperature value as a float.

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

The readHumidity() function returns the current humidity value, also as a float.

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

Peer Info Variable

Create a new global variable of type esp_now_peer_info_t called peerInfo that will be used later on to save data about ESP-NOW peers.

esp_now_peer_info_t peerInfo;

OnDataSent()

The onDataSent() is a callback function that will be called when we send a message via ESP-NOW. This function will print the sender MAC address and whether the message was successfully delivered or not.

// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  Serial.print("Packet to: ");
  // Copies the receiver mac address to a string
  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.print(macStr);
  Serial.print(" send status:\t");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

setup()

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

Serial.begin(115200);
initBME();

You also need to initialize the Wi-Fi interface to be able to use ESP-NOW.

WiFi.mode(WIFI_STA);

Initialize ESP-NOW protocol.

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

Register a callback function that will run when we send data via ESP-NOW.

esp_now_register_send_cb(OnDataSent);

Register the ESP-NOW peer (the receiver board).

// Register peer
peerInfo.channel = 0;  
peerInfo.encrypt = false;
// Copy receiver's MAC address to peerInfo
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
  Serial.println("Failed to add peer");
  return;
}

In this example, we won’t encrypt the messages. If you want to learn more about encrypting ESP-NOW messages, you can read this tutorial: ESP32: ESP-NOW Encrypted Messages.

loop()

Finally, in the loop(), every 10 seconds (set previously on the interval variable), we get new data and set the myData structure values to that new data.

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

In the end, we use the esp_now_send() function to send the data to the peer.

// Send data and check for errors
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));

We print the sensor readings and information about the sending process in the Serial Monitor.

esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
Serial.print("Sending data (Reading ID: ");
Serial.print(myData.readingId);
Serial.print(", Temp: ");
Serial.print(myData.temp);
Serial.print("C, Hum: ");
Serial.print(myData.hum);
Serial.print("%): ");
Serial.println(result == ESP_OK ? "Sent" : "Failed to send");

Testing the Sender Code

After uploading the code to the board, open the Serial Monitor at a baud rate of 115200.

Serial Monitor for the ESP32 ESP-NOW Sender Board - showing the message was sent, but delivery failed

You’ll see that the board will start sending data via ESP-NOW. At the moment, the delivery is failing because we haven’t prepared the receiver yet—that’s what we’re going to do next.

Don’t forget to upload that code to each board and change the board ID.

ESP32 CYD ESP-NOW Receiver Board

The CYD board will act as a ESP-NOW receiver that will get the data from the other two boards.

ESP32 CYD Board Displaying data on a table - data received from other ESP32 via ESP-NOW
ESP32 CYD Board Displaying data on a table - data received from other ESP32 via ESP-NOW

The data from each board will be displayed on the screen. We’ll create two different tabs: one for each board.

/*  Rui Santos & Sara Santos - Random Nerd Tutorials - https://RandomNerdTutorials.com/esp32-cyd-esp-now-receive-data/
    THIS EXAMPLE WAS TESTED WITH THE FOLLOWING HARDWARE:
    1) ESP32-2432S028R 2.8 inch 240Ă—320 also known as the Cheap Yellow Display (CYD): https://makeradvisor.com/tools/cyd-cheap-yellow-display-esp32-2432s028r/
      SET UP INSTRUCTIONS: https://RandomNerdTutorials.com/cyd-lvgl/
    2) REGULAR ESP32 Dev Board + 2.8 inch 240x320 TFT Display: https://makeradvisor.com/tools/2-8-inch-ili9341-tft-240x320/ and https://makeradvisor.com/tools/esp32-dev-board-wi-fi-bluetooth/
      SET UP INSTRUCTIONS: https://RandomNerdTutorials.com/esp32-tft-lvgl/
    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.
*/

/*  Install the "lvgl" library version 9.X by kisvegabor to interface with the TFT Display - https://lvgl.io/
    *** IMPORTANT: lv_conf.h available on the internet will probably NOT work with the examples available at Random Nerd Tutorials ***
    *** YOU MUST USE THE lv_conf.h FILE PROVIDED IN THE LINK BELOW IN ORDER TO USE THE EXAMPLES FROM RANDOM NERD TUTORIALS ***
    FULL INSTRUCTIONS AVAILABLE ON HOW CONFIGURE THE LIBRARY: https://RandomNerdTutorials.com/cyd-lvgl/ or https://RandomNerdTutorials.com/esp32-tft-lvgl/   */
#include <lvgl.h>

/*  Install the "TFT_eSPI" library by Bodmer to interface with the TFT Display - https://github.com/Bodmer/TFT_eSPI
    *** IMPORTANT: User_Setup.h available on the internet will probably NOT work with the examples available at Random Nerd Tutorials ***
    *** YOU MUST USE THE User_Setup.h FILE PROVIDED IN THE LINK BELOW IN ORDER TO USE THE EXAMPLES FROM RANDOM NERD TUTORIALS ***
    FULL INSTRUCTIONS AVAILABLE ON HOW CONFIGURE THE LIBRARY: https://RandomNerdTutorials.com/cyd-lvgl/ or https://RandomNerdTutorials.com/esp32-tft-lvgl/   */
#include <TFT_eSPI.h>

// Install the "XPT2046_Touchscreen" library by Paul Stoffregen to use the Touchscreen - https://github.com/PaulStoffregen/XPT2046_Touchscreen - Note: this library doesn't require further configuration
#include <XPT2046_Touchscreen.h>

#include <esp_now.h>
#include <WiFi.h>
#include <freertos/queue.h>

// Touchscreen pins
#define XPT2046_IRQ 36   // T_IRQ
#define XPT2046_MOSI 32  // T_DIN
#define XPT2046_MISO 39  // T_OUT
#define XPT2046_CLK 25   // T_CLK
#define XPT2046_CS 33    // T_CS

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320

// SET VARIABLE TO 0 FOR THE TEMPERATURE DEGREES SYMBOL IN FAHRENHEIT
#define TEMP_CELSIUS 1  

// Touchscreen coordinates: (x, y) and pressure (z)
int x, y, z;

#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

// Define a queue handle
QueueHandle_t esp_now_queue;

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

static lv_obj_t * table1;
static lv_obj_t * table2;

// Callback function executed when data is received
void OnDataRecv(const esp_now_recv_info *recv_info, const uint8_t *incomingData, int len) {
  // Copy incoming data to myData structure
  struct_message myData;
  memcpy(&myData, incomingData, sizeof(myData));
  xQueueSendFromISR(esp_now_queue, &myData, NULL); // Send to queue from ISR

  // Get sender's MAC address from recv_info
  const uint8_t *mac_addr = recv_info->src_addr;
  
  // Convert sender's MAC address to a string for display
  char macStr[18];
  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]);
  
  // Print received data to Serial Monitor
  Serial.println("Received data:");
  Serial.print("  Sender MAC: ");
  Serial.println(macStr);
  Serial.print("  Board ID: ");
  Serial.println(myData.id);
  Serial.print("  Temperature: ");
  Serial.print(myData.temp);
  Serial.println(" °C");
  Serial.print("  Humidity: ");
  Serial.print(myData.hum);
  Serial.println(" %");
  Serial.print("  Reading ID: ");
  Serial.println(myData.readingId);
  Serial.println("-------------------");
}

// If logging is enabled, it will inform the user about what is happening in the library
void log_print(lv_log_level_t level, const char * buf) {
  LV_UNUSED(level);
  Serial.println(buf);
  Serial.flush();
}

// Get the Touchscreen data
void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) {
  // Checks if Touchscreen was touched, and prints X, Y and Pressure (Z)
  if(touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();

    // Advanced Touchscreen calibration, LEARN MORE » https://RandomNerdTutorials.com/touchscreen-calibration/
    float alpha_x, beta_x, alpha_y, beta_y, delta_x, delta_y;

    // REPLACE WITH YOUR OWN CALIBRATION VALUES » https://RandomNerdTutorials.com/touchscreen-calibration/
    alpha_x = -0.000;
    beta_x = 0.090;
    delta_x = -33.771;
    alpha_y = 0.066;
    beta_y = 0.000;
    delta_y = -14.632;

    x = alpha_y * p.x + beta_y * p.y + delta_y;
    // clamp x between 0 and SCREEN_WIDTH - 1
    x = max(0, x);
    x = min(SCREEN_WIDTH - 1, x);

    y = alpha_x * p.x + beta_x * p.y + delta_x;
    // clamp y between 0 and SCREEN_HEIGHT - 1
    y = max(0, y);
    y = min(SCREEN_HEIGHT - 1, y);

    // Basic Touchscreen calibration points with map function to the correct width and height
    //x = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
    //y = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);

    z = p.z;

    data->state = LV_INDEV_STATE_PRESSED;

    // Set the coordinates
    data->point.x = x;
    data->point.y = y;

    // Print Touchscreen info about X, Y and Pressure (Z) on the Serial Monitor
    Serial.print("X = ");
    Serial.print(x);
    Serial.print(" | Y = ");
    Serial.print(y);
    Serial.print(" | Pressure = ");
    Serial.print(z);
    Serial.println();
  }
  else {
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

void lv_create_main_gui(void) {
  // Create a Tab view object
  lv_obj_t * tabview;
  tabview = lv_tabview_create(lv_screen_active());
  lv_tabview_set_tab_bar_size(tabview, 40);
  
  // Add 3 tabs (the tabs are page (lv_page) and can be scrolled
  lv_obj_t * tab1 = lv_tabview_add_tab(tabview, "BOARD #1");
  lv_obj_t * tab2 = lv_tabview_add_tab(tabview, "BOARD #2");

  table1 = lv_table_create(tab1);
  lv_table_set_cell_value(table1, 0, 0, "Temperature");
  lv_table_set_cell_value(table1, 1, 0, "Humidity");
  lv_table_set_cell_value(table1, 2, 0, "Reading ID");
  lv_table_set_cell_value(table1, 0, 1, "--");
  lv_table_set_cell_value(table1, 1, 1, "--");
  lv_table_set_cell_value(table1, 2, 1, "--");

  table2 = lv_table_create(tab2);
  lv_table_set_cell_value(table2, 0, 0, "Temperature");
  lv_table_set_cell_value(table2, 1, 0, "Humidity");
  lv_table_set_cell_value(table2, 2, 0, "Reading ID");
  lv_table_set_cell_value(table2, 0, 1, "--");
  lv_table_set_cell_value(table2, 1, 1, "--");
  lv_table_set_cell_value(table2, 2, 1, "--");

  // Center the tables
  lv_obj_center(table1);
  lv_obj_center(table2);
}

void update_table_values(struct_message *myData) {
  #if TEMP_CELSIUS
    const char degree_symbol[] = "\u00B0C";
  #else
    const char degree_symbol[] = "\u00B0F";
  #endif

  String temp_value = String(myData->temp) + degree_symbol;
  String humi_value = String(myData->hum) + "%";

  if(myData->id == 1) {
    lv_table_set_cell_value(table1, 0, 1, temp_value.c_str());
    lv_table_set_cell_value(table1, 1, 1, humi_value.c_str());
    lv_table_set_cell_value(table1, 2, 1, String(myData->readingId).c_str());
  }
  else if(myData->id == 2) {
    lv_table_set_cell_value(table2, 0, 1, temp_value.c_str());
    lv_table_set_cell_value(table2, 1, 1, humi_value.c_str());
    lv_table_set_cell_value(table2, 2, 1, String(myData->readingId).c_str());
  }
}

void setup() {
  String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
  Serial.begin(115200);
  Serial.println(LVGL_Arduino);

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

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

  // Start LVGL
  lv_init();
  // Register print function for debugging
  lv_log_register_print_cb(log_print);

  // Start the SPI for the touchscreen and init the touchscreen
  touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  touchscreen.begin(touchscreenSPI);
  // Set the Touchscreen rotation in landscape mode
  // Note: in some displays, the touchscreen might be upside down, so you might need to set the rotation to 0: touchscreen.setRotation(0);
  touchscreen.setRotation(2);

  // Create a display object
  lv_display_t * disp;
  // Initialize the TFT display using the TFT_eSPI library
  disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
  lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
  
  // Initialize an LVGL input device object (Touchscreen)
  lv_indev_t * indev = lv_indev_create();
  lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
  // Set the callback function to read Touchscreen input
  lv_indev_set_read_cb(indev, touchscreen_read);

  // Function to draw the GUI
  lv_create_main_gui();

  // Register callback function to handle received data
  esp_now_register_recv_cb(OnDataRecv);
  
  esp_now_queue = xQueueCreate(10, sizeof(struct_message)); // Queue for 10 messages
  if (esp_now_queue == NULL) {
    Serial.println("Error creating queue");
    return;
  }
}

void loop() {
  lv_task_handler();  // let the GUI do its work
  lv_tick_inc(5);     // tell LVGL how much time has passed
  delay(5);           // let this time pass
  
  struct_message myData;
  if (xQueueReceive(esp_now_queue, &myData, 0) == pdTRUE) {
    // Process and display the data
    update_table_values(&myData);
  }
}

View raw code

We cover how to create tabs, text fields, buttons, and much more in our LVGL for the ESP32 eBook. Check it out here: Learn LVGL: Build GUIs for ESP32 Projects (eBook).

How Does the Code Work?

Now, let’s take a quick look at how the code works, or skip to the demonstration section.

Including Libraries

In all your LVGL sketches, you need to include the lvgl.h and the TFT_eSPI.h libraries to display on the screen.

#include <lvgl.h>
#include <TFT_eSPI.h>

We also use the touchscreen to choose between tabs. So, we need to include the touchscreen library.

#include <XPT2046_Touchscreen.h>

You need to include the WiFi and esp_now libraries to use ESP-NOW.

#include <esp_now.h>
#include <WiFi.h>

In our code, we’ll use queues to receive data via ESP-NOW while updating the display simultaneously.

#include <freertos/queue.h>

Initialize Touchscreen

The following lines set the touchscreen pinout:

#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
#define XPT2046_MISO 39
#define XPT2046_CLK 25
#define XPT2046_CS 33

Create a touchscreenSPI and touchscreen instances:

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

Defining the Display Width and Height

You need to define your display width and height in all your sketches that use LVGL. If you’re using the recommended display, the size is 240×320.

#define SCREEN_WIDTH 241
#define SCREEN_HEIGHT 320

X, Y, and Z Touchscreen Variables

Define the variables to hold the touch coordinates and touch pressure.

// Touchscreen coordinates: (x, y) and pressure (z)
int x, y, z;

Creating a Drawing Buffer

You need to create a buffer to draw on the display as follows:

#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

Create a Queue

Create a new queue called esp_now_queue that we’ll use later for handling the tasks for receiving ESP-NOW messages and updating the display.

// Define a queue handle
QueueHandle_t esp_now_queue;

Data Structure

Create a data structure to receive the data from the sender. This must be the same sent by the sender board.

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

LVGL Objects for the Tables

In this project, we’ll create two tables. One to display the data from each board. We’re creating two LVGL objects globally that will then be used to refer to the tables.

static lv_obj_t * table1;
static lv_obj_t * table2;

onDataRecv() ESP-NOW Callback Function

The onDataRecv() function will run when the board receives new data via ESP-NOW.

// Callback function executed when data is received
void OnDataRecv(const esp_now_recv_info *recv_info, const uint8_t *incomingData, int len) {
  // Copy incoming data to myData structure
  struct_message myData;
  memcpy(&myData, incomingData, sizeof(myData));
  xQueueSendFromISR(esp_now_queue, &myData, NULL); // Send to queue from ISR

  // Get sender's MAC address from recv_info
  const uint8_t *mac_addr = recv_info->src_addr;
  
  // Convert sender's MAC address to a string for display
  char macStr[18];
  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]);
  
  // Print received data to Serial Monitor
  Serial.println("Received data:");
  Serial.print("  Sender MAC: ");
  Serial.println(macStr);
  Serial.print("  Board ID: ");
  Serial.println(myData.id);
  Serial.print("  Temperature: ");
  Serial.print(myData.temp);
  Serial.println(" °C");
  Serial.print("  Humidity: ");
  Serial.print(myData.hum);
  Serial.println(" %");
  Serial.print("  Reading ID: ");
  Serial.println(myData.readingId);
  Serial.println("-------------------");
}

In this function, we get the received data and save it in the myData variable.

// Copy incoming data to myData structure
struct_message myData;
memcpy(&myData, incomingData, sizeof(myData));

Then, we send the myData variable to the queue in a safe way (non-blocking).

xQueueSendFromISR(esp_now_queue, &myData, NULL); // Send to queue from ISR

Basically, the function xQueueSendFromISR(esp_now_queue, &myData, NULL) sends data (myData) to a FreeRTOS queue (esp_now_queue). Then, we’ll use this data on a function that display the sensor readings on the screen.

Then, we print the received data in the Serial Monitor.

Serial.println("Received data:");
Serial.print("  Sender MAC: ");
Serial.println(macStr);
Serial.print("  Board ID: ");
Serial.println(myData.id);
Serial.print("  Temperature: ");
Serial.print(myData.temp);
Serial.println(" °C");
Serial.print("  Humidity: ");
Serial.print(myData.hum);
Serial.println(" %");
Serial.print("  Reading ID: ");
Serial.println(myData.readingId);
Serial.println("-------------------");

Debugging Function

For debugging with the LVGL library, you should use the log_print() function. It is defined below. Include it in all your sketches before the setup().

// If logging is enabled, it will inform the user about what is happening in the library
void log_print(lv_log_level_t level, const char * buf) {
  LV_UNUSED(level);
  Serial.println(buf);
  Serial.flush();
}

Touchscreen Callback Function

The touchscreen_read() callback function will be called when touch is detected on the touchscreen.

// Get the Touchscreen data
void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) {
  // Checks if Touchscreen was touched, and prints X, Y and Pressure (Z)
  if(touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();


(....)
  }
}

LVGL Main Function – Drawing the GUI

The lv_create_main_gui() function will draw the GUI on the display. This function will be called later in the setup.

First, we create a tabview where we’ll add our tabs—one tab for each board.

// Create a Tab view object
lv_obj_t * tabview;
tabview = lv_tabview_create(lv_screen_active());
lv_tabview_set_tab_bar_size(tabview, 40);

Then, we create the tabs we want to display and add them to the tabview.

lv_obj_t * tab1 = lv_tabview_add_tab(tabview, "BOARD #1");
lv_obj_t * tab2 = lv_tabview_add_tab(tabview, "BOARD #2");

Then, we create a table called table1 and add it to tab1.

table1 = lv_table_create(tab1);

After that, we create the cells for the table.

lv_table_set_cell_value(table1, 0, 0, "Temperature");
lv_table_set_cell_value(table1, 1, 0, "Humidity");
lv_table_set_cell_value(table1, 2, 0, "Reading ID");
lv_table_set_cell_value(table1, 0, 1, "--");
lv_table_set_cell_value(table1, 1, 1, "--");
lv_table_set_cell_value(table1, 2, 1, "--");

We follow a similar procedure for the other table.

table2 = lv_table_create(tab2);
lv_table_set_cell_value(table2, 0, 0, "Temperature");
lv_table_set_cell_value(table2, 1, 0, "Humidity");
lv_table_set_cell_value(table2, 2, 0, "Reading ID");
lv_table_set_cell_value(table2, 0, 1, "--");
lv_table_set_cell_value(table2, 1, 1, "--");
lv_table_set_cell_value(table2, 2, 1, "--");

After creating the tables, we center them in the middle of the screen.

// Center the tables
lv_obj_center(table1);
lv_obj_center(table2);

Update the Table

To update the values on the table, we’ll later call the update_table_values() function.

void update_table_values(struct_message *myData) {

We first check the board ID to determine from which board we received the data, and then update the corresponding table accordingly.

if(myData->id == 1) {
  lv_table_set_cell_value(table1, 0, 1, temp_value.c_str());
  lv_table_set_cell_value(table1, 1, 1, humi_value.c_str());
  lv_table_set_cell_value(table1, 2, 1, String(myData->readingId).c_str());
}
else if(myData->id == 2) {
  lv_table_set_cell_value(table2, 0, 1, temp_value.c_str());
  lv_table_set_cell_value(table2, 1, 1, humi_value.c_str());
  lv_table_set_cell_value(table2, 2, 1, String(myData->readingId).c_str());
}

To learn more about creating and handling tables in LVGL, you can check this project:

setup()

In the setup(), include the following lines for debugging. These will print the version of LVGL that you’re using. You must be using version 9.

String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.begin(115200);
Serial.println(LVGL_Arduino);

Initialize ESP-NOW

Activate the Wi-Fi interface and initialize ESP-NOW.

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

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

Initialize the LVGL Library

Initialize the LVGL Library by calling the lv_init() function in the setup().

// Start LVGL
lv_init();

Register Debugging Function

Register your log_print() function declared previously as a function associated with debugging LVGL.

// Register print function for debugging
lv_log_register_print_cb(log_print);

Start the Touchscreen

Start the SPI for the touchscreen and initialize the touchscreen.

touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
touchscreen.begin(touchscreenSPI);
touchscreen.setRotation(2);

Note: in some displays, the touchscreen might be upside down, so you might need to set the rotation to 0: touchscreen.setRotation(0);

Creating a Display Object

Create the display object and initialize the TFT display using the TFT_eSPI library.

  // Create a display object
  lv_display_t * disp;
  // Initialize the TFT display using the TFT_eSPI library
  disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
  lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);

Initialize an LVGL input device object (touchscreen) and set the callback function that will be triggered when you click the touchscreen—the touchscreen_read() function we created previously.

// Initialize an LVGL input device object (Touchscreen)
lv_indev_t * indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
// Set the callback function to read Touchscreen input
lv_indev_set_read_cb(indev, touchscreen_read);

Drawing the GUI

Call the lv_create_main_gui() function to draw the GUI for your touchscreen:

lv_create_main_gui();

ESP-NOW Callback Function

Set which function to run when the board receives data via ESP-NOW. In this case, it will run the OnDataRecv() function that we’ve already created previously.

// Register callback function to handle received data
esp_now_register_recv_cb(OnDataRecv);

Creating a Queue

The code esp_now_queue = xQueueCreate(10, sizeof(struct_message)) creates a FreeRTOS queue to hold up to 10 ESP-NOW messages of type struct_message for communication between an ISR (the ESP-NOW callback function) and a task.

esp_now_queue = xQueueCreate(10, sizeof(struct_message)); // Queue for 10 messages
if (esp_now_queue == NULL) {
  Serial.println("Error creating queue");
  return;
}

Updating the Table Values in the Loop

This line if (xQueueReceive(esp_now_queue, &myData, 0) == pdTRUE) checks if a message is available in esp_now_queue without waiting, storing it in myData if successful. If a message is received, it calls update_table_values(&myData) to process and display the data on the screen.

if (xQueueReceive(esp_now_queue, &myData, 0) == pdTRUE) {
  // Process and display the data
  update_table_values(&myData);
}

This method ensures that we don’t block the ESP-NOW callback function while writing to the display.

Demonstration

Upload the code to your CYD board. Go to Tools Board and select ESP32 ESP32 Dev Module. Then, select the right COM port in Tools Port.

Go to Tools > Partition scheme > choose anything that has more than 1.4MB APP, for example: “Huge APP (3MB No OTA/1MB SPIFFS“.

Select Huge App Partion Scheme Arduino IDE Tools Menu

Finally, click the upload button.

Arduino IDE 2 Upload Button

After a few minutes, the CYD board will start receiving the messages from the sender boards via ESP-NOW.

ESP32 CYD Board Displaying Data received via ESP-NOW on a table

The data received from each board will be displayed on a table. We divided the screen into two tabs. Each tab displays a table for each board.

To change between tabs, you can swipe left/right, or tap on the tab name at the top.

ESP32 CYD Board - Swiping between tabs

Wrapping Up

In this tutorial, you learned how to use ESP-NOW with the CYD board to receive data from multiple ESP32 boards.

In a similar way, you can also send data to multiple ESP32 boards. The CYD board can act like a remote control with buttons that control other boards wirelessly.

We hope you found this tutorial useful. If you would like to learn more about creating graphical user interfaces using the LVGL library with the ESP32, check out our latest eBook:

Other guides you might like reading:

To learn more about the ESP32, make sure to take a look at our resources:

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

2 thoughts on “ESP32 CYD with ESP-NOW: Receive and Display Data From Multiple Boards”

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.