ESP32 CYD с ESP-NOW: прием и отображение данных от нескольких плат
В этом проекте вы узнаете, как использовать протокол связи ESP-NOW с платой ESP32 CYD (Cheap Yellow Display) для приема и отображения данных от нескольких плат-отправителей ESP32. Две платы ESP32 будут считывать данные с датчиков BME280 и отправлять их на плату CYD. Плата CYD, выступая в роли приемника ESP-NOW, будет отображать полученные данные в отдельных таблицах — по одной для каждого отправителя — каждая на отдельной вкладке на экране.
Впервые работаете с ESP32 Cheap Yellow Display? Начните здесь: Getting Started with ESP32 Cheap Yellow Display Board – CYD (ESP32-2432S028R).
Обзор проекта
Вот обзор того, как работает этот проект.
В этом проекте плата ESP32 CYD будет выступать в роли приемника ESP-NOW, который получает данные от других плат ESP32.
Другие платы ESP32 будут периодически отправлять данные датчиков (BME280) на плату CYD через ESP-NOW.
Плата CYD отображает две вкладки, по одной для каждой платы. Каждая вкладка содержит таблицу с данными, полученными от других плат.
Таблицы обновляются сразу же, как только CYD получает новые данные.
Впервые работаете с протоколом связи ESP-NOW? Начните здесь: Getting Started with ESP-NOW (ESP32 with Arduino IDE).
Предварительные требования
Перед тем как продолжить, убедитесь, что вы выполнили следующие предварительные требования. Вы должны выполнить все шаги, иначе ваш проект не будет работать.
1) Необходимые компоненты
Для этого проекта вам понадобятся следующие компоненты:
2x платы ESP32 на ваш выбор (мы используем DOIT DevKIT V1 Board)
2x BME280
Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!
2) Установка плат ESP32 в Arduino IDE
Мы будем программировать ESP32 с помощью Arduino IDE. Убедитесь, что у вас установлены платы ESP32. Следуйте следующему руководству:
3) Знакомство с ESP32 Cheap Yellow Display
Плата разработки ESP32-2432S028R стала известна в сообществе мейкеров как «Cheap Yellow Display» или сокращенно CYD. Эта плата разработки, основным чипом которой является модуль ESP32-WROOM-32, поставляется с 2.8-дюймовым TFT-сенсорным ЖК-дисплеем, интерфейсом для карты microSD, RGB-светодиодом и всей необходимой схемотехникой для программирования и питания платы.
Если вы впервые используете ESP32 Cheap Yellow Display, обязательно следуйте нашему руководству по началу работы:
4) Установка библиотек TFT и LVGL
LVGL (Light and Versatile Graphics Library) — это бесплатная графическая библиотека с открытым исходным кодом, которая предоставляет широкий спектр удобных графических элементов для ваших проектов на микроконтроллерах, требующих графического пользовательского интерфейса (GUI).
Следуйте следующему руководству для установки и настройки необходимых библиотек для использования LVGL с ESP32 Cheap Yellow Display в Arduino IDE.
5) Установка библиотек BME280
Для этого проекта мы будем использовать библиотеку Adafruit BME280 для получения данных с датчика BME280 на других платах ESP32. В Arduino IDE перейдите в Sketch > Include Library > Manage Libraries. Найдите Adafruit BME280 Library в строке поиска и установите библиотеку. Также установите все зависимости, которые в данный момент не установлены (обычно это библиотеки Adafruit Bus IO и Adafruit Unified Sensor).
Получение MAC-адреса платы CYD
Сначала вам нужно узнать MAC-адрес платы CYD, чтобы платы-отправители могли отправлять ей сообщения.
Загрузите следующий код на плату CYD.
/*
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(){
}
После загрузки откройте Serial Monitor на скорости 115200 бод. Нажмите встроенную кнопку RST. MAC-адрес платы будет выведен в Serial Monitor.
Подготовка плат-отправителей
Для этого руководства мы будем отправлять данные на CYD с двух разных плат. Вы можете модифицировать этот проект для отображения данных с большего количества плат.
Каждая плата будет идентифицирована по ID (номер, который мы присвоим каждой плате):
ID=1 для платы 1
ID=2 для платы 2
Подключение датчика BME280
Каждая плата-отправитель будет отправлять данные об окружающей среде с датчика BME280. Подключите датчик BME280 к каждой из ваших плат. Мы будем использовать стандартные пины I2C для ESP32.
Узнайте больше об I2C с ESP32: ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE).
Не знакомы с BME280 и ESP32? Прочитайте это руководство: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).
Если вы используете другую модель платы, стандартные пины I2C могут отличаться.
Код отправителя ESP32 ESP-NOW
Следующий код считывает данные с датчика BME280 и отправляет их через ESP-NOW на плату CYD. Не забудьте изменить код, указав MAC-адрес вашей платы CYD. Кроме того, не забудьте изменить ID платы для каждой из ваших плат.
/*********
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 wifi_tx_info_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(esp_now_send_cb_t(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");
}
}
Как работает код?
Давайте посмотрим, как работает код. Если вы новичок в ESP-NOW, мы рекомендуем начать с этого руководства по ESP-NOW, чтобы лучше понять, как все работает.
Подключение библиотек
Начнем с подключения необходимых библиотек для этого проекта. Библиотеки esp_now и WiFi необходимы для использования связи ESP-NOW, а Adafruit_BME280 и Adafruit_Sensor нужны для работы с датчиком BME280.
#include <esp_now.h>
#include <WiFi.h>
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
Установка ID платы
Присвойте уникальный ID вашей плате, чтобы приемник мог легко ее идентифицировать. Здесь мы просто нумеруем платы, но вы можете использовать другой метод, например, дать им имя или просто идентифицировать по MAC-адресу.
// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 2
Убедитесь, что вы присвоили разный ID каждой плате.
Экземпляр BME280
Создайте экземпляр библиотеки Adafruit_BME280 с именем bme, который будет использоваться для работы с датчиком.
Adafruit_BME280 bme;
MAC-адрес платы-приемника
Введите MAC-адрес вашей платы-приемника (платы CYD). Например, MAC-адрес моей платы CYD — 24:dc:c3:49:6a:14. Я должен добавить его в код в следующем формате.
// REPLACE WITH YOUR ESP RECEIVER'S MAC ADDRESS
uint8_t broadcastAddress[] = {0x24, 0xDC, 0xC3, 0x49, 0x6A, 0x14};
Создание структуры данных
Создайте новую структуру с именем struct_message, которая будет содержать все данные, которые вы хотите отправить. id — это идентификатор платы, температура и влажность соответствуют данным, которые мы получаем с датчика BME280, а readingId — это просто номер для отслеживания количества отправленных показаний.
typedef struct struct_message {
int id;
float temp;
float hum;
int readingId;
} struct_message;
Затем мы создаем новую переменную структуры с именем myData на основе структуры struct_message.
struct_message myData;
Инициализация и получение данных с датчика BME280
Мы создаем несколько функций, связанных с датчиком BME280.
Функция initBME() инициализирует датчик BME280 на стандартных пинах I2C ESP32.
void initBME() {
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
Функция readTemperature() возвращает текущее значение температуры в виде числа с плавающей точкой (float).
float readTemperature() {
float t = bme.readTemperature();
return t;
}
Функция readHumidity() возвращает текущее значение влажности, также в виде числа с плавающей точкой (float).
float readHumidity() {
float h = bme.readHumidity();
return h;
}
Переменная информации о пире
Создайте новую глобальную переменную типа esp_now_peer_info_t с именем peerInfo, которая будет использоваться позже для сохранения данных о пирах ESP-NOW.
esp_now_peer_info_t peerInfo;
OnDataSent()
Функция onDataSent() — это функция обратного вызова, которая будет вызываться при отправке сообщения через ESP-NOW. Эта функция выводит MAC-адрес отправителя и информацию о том, было ли сообщение успешно доставлено или нет.
// 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()
В функции setup() инициализируем Serial Monitor и датчик BME280.
Serial.begin(115200);
initBME();
Также необходимо инициализировать интерфейс Wi-Fi для использования ESP-NOW.
WiFi.mode(WIFI_STA);
Инициализация протокола ESP-NOW.
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
Регистрация функции обратного вызова, которая будет выполняться при отправке данных через ESP-NOW.
esp_now_register_send_cb(OnDataSent);
Регистрация пира ESP-NOW (платы-приемника).
// 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;
}
В этом примере мы не будем шифровать сообщения. Если вы хотите узнать больше о шифровании сообщений ESP-NOW, вы можете прочитать это руководство: ESP32: ESP-NOW Encrypted Messages.
loop()
Наконец, в функции loop() каждые 10 секунд (установлено ранее в переменной interval) мы получаем новые данные и устанавливаем значения структуры myData на эти новые данные.
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() для отправки данных пиру.
// Send data and check for errors
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
Мы выводим показания датчиков и информацию о процессе отправки в 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");
Тестирование кода отправителя
После загрузки кода на плату откройте Serial Monitor на скорости 115200 бод.
Вы увидите, что плата начнет отправлять данные через ESP-NOW. На данный момент доставка будет неуспешной, потому что мы еще не подготовили приемник — именно этим мы займемся далее.
Не забудьте загрузить этот код на каждую плату и изменить ID платы.
Плата-приемник ESP32 CYD ESP-NOW
Плата CYD будет выступать в роли приемника ESP-NOW, который получает данные от двух других плат.
Данные от каждой платы будут отображаться на экране. Мы создадим две разных вкладки: по одной для каждой платы.
/* 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 240x320 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);
}
}
Мы рассказываем, как создавать вкладки, текстовые поля, кнопки и многое другое в нашей книге LVGL для ESP32. Ознакомьтесь с ней здесь: Learn LVGL: Build GUIs for ESP32 Projects (eBook).
Как работает код?
Теперь давайте кратко рассмотрим, как работает код, или перейдите к разделу демонстрации.
Подключение библиотек
Во всех ваших скетчах LVGL необходимо подключить библиотеки lvgl.h и TFT_eSPI.h для отображения на экране.
#include <lvgl.h>
#include <TFT_eSPI.h>
Мы также используем сенсорный экран для переключения между вкладками. Поэтому нам нужно подключить библиотеку сенсорного экрана.
#include <XPT2046_Touchscreen.h>
Для использования ESP-NOW необходимо подключить библиотеки WiFi и esp_now.
#include <esp_now.h>
#include <WiFi.h>
В нашем коде мы будем использовать очереди для получения данных через ESP-NOW и одновременного обновления дисплея.
#include <freertos/queue.h>
Инициализация сенсорного экрана
Следующие строки устанавливают распиновку сенсорного экрана:
#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
#define XPT2046_MISO 39
#define XPT2046_CLK 25
#define XPT2046_CS 33
Создаем экземпляры touchscreenSPI и touchscreen:
SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);
Определение ширины и высоты дисплея
Вам нужно определить ширину и высоту дисплея во всех ваших скетчах, использующих LVGL. Если вы используете рекомендуемый дисплей, размер составляет 240x320.
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
Переменные X, Y и Z сенсорного экрана
Определите переменные для хранения координат касания и давления.
// 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];
Создание очереди
Создайте новую очередь с именем esp_now_queue, которую мы будем использовать позже для обработки задач получения сообщений ESP-NOW и обновления дисплея.
// 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;
Объекты LVGL для таблиц
В этом проекте мы создадим две таблицы. Одну для отображения данных с каждой платы. Мы создаем два глобальных объекта LVGL, которые затем будут использоваться для обращения к таблицам.
static lv_obj_t * table1;
static lv_obj_t * table2;
Функция обратного вызова onDataRecv() ESP-NOW
Функция onDataRecv() будет выполняться, когда плата получает новые данные через 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("-------------------");
}
В этой функции мы получаем принятые данные и сохраняем их в переменную myData.
// Copy incoming data to myData structure
struct_message myData;
memcpy(&myData, incomingData, sizeof(myData));
Затем мы отправляем переменную myData в очередь безопасным способом (неблокирующим).
xQueueSendFromISR(esp_now_queue, &myData, NULL); // Send to queue from ISR
По сути, функция xQueueSendFromISR(esp_now_queue, &myData, NULL) отправляет данные (myData) в очередь FreeRTOS (esp_now_queue). Затем мы будем использовать эти данные в функции, которая отображает показания датчиков на экране.
После этого мы выводим полученные данные в 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("-------------------");
Функция отладки
Для отладки с библиотекой LVGL следует использовать функцию log_print(). Она определена ниже. Включайте ее во все ваши скетчи перед функцией 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_read() будет вызываться при обнаружении касания на сенсорном экране.
// 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 – рисование GUI
Функция lv_create_main_gui() будет рисовать графический интерфейс на дисплее. Эта функция будет вызвана позже в функции setup.
Сначала мы создаем tabview, в который добавим наши вкладки — по одной вкладке для каждой платы.
// Create a Tab view object
lv_obj_t * tabview;
tabview = lv_tabview_create(lv_screen_active());
lv_tabview_set_tab_bar_size(tabview, 40);
Затем мы создаем вкладки, которые хотим отобразить, и добавляем их в tabview.
lv_obj_t * tab1 = lv_tabview_add_tab(tabview, "BOARD #1");
lv_obj_t * tab2 = lv_tabview_add_tab(tabview, "BOARD #2");
Далее мы создаем таблицу с именем table1 и добавляем ее в tab1.
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);
Обновление таблицы
Для обновления значений в таблице мы позже вызовем функцию update_table_values().
void update_table_values(struct_message *myData) {
Сначала мы проверяем ID платы, чтобы определить, от какой платы получены данные, а затем обновляем соответствующую таблицу.
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());
}
Чтобы узнать больше о создании и работе с таблицами в LVGL, вы можете ознакомиться с этим проектом:
setup()
В функции setup() включите следующие строки для отладки. Они выведут версию используемой библиотеки LVGL. Вы должны использовать версию 9.
String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.begin(115200);
Serial.println(LVGL_Arduino);
Инициализация ESP-NOW
Активируйте интерфейс Wi-Fi и инициализируйте 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;
}
Инициализация библиотеки LVGL
Инициализируйте библиотеку LVGL, вызвав функцию lv_init() в setup().
// Start LVGL
lv_init();
Регистрация функции отладки
Зарегистрируйте ранее объявленную функцию log_print() как функцию, связанную с отладкой LVGL.
// Register print function for debugging
lv_log_register_print_cb(log_print);
Запуск сенсорного экрана
Запустите SPI для сенсорного экрана и инициализируйте его.
touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
touchscreen.begin(touchscreenSPI);
touchscreen.setRotation(2);
Примечание: на некоторых дисплеях сенсорный экран может быть перевернут, поэтому вам может потребоваться установить поворот на 0: touchscreen.setRotation(0);
Создание объекта дисплея
Создайте объект дисплея и инициализируйте TFT-дисплей с помощью библиотеки TFT_eSPI.
// 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);
Инициализируйте объект устройства ввода LVGL (сенсорный экран) и установите функцию обратного вызова, которая будет срабатывать при нажатии на сенсорный экран — функцию touchscreen_read(), которую мы создали ранее.
// 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);
Рисование GUI
Вызовите функцию lv_create_main_gui() для отрисовки графического интерфейса на вашем сенсорном экране:
lv_create_main_gui();
Функция обратного вызова ESP-NOW
Установите, какая функция будет запускаться при получении данных через ESP-NOW. В данном случае будет выполняться функция OnDataRecv(), которую мы уже создали ранее.
// Register callback function to handle received data
esp_now_register_recv_cb(OnDataRecv);
Создание очереди
Код esp_now_queue = xQueueCreate(10, sizeof(struct_message)) создает очередь FreeRTOS для хранения до 10 сообщений ESP-NOW типа struct_message для связи между ISR (функцией обратного вызова ESP-NOW) и задачей.
esp_now_queue = xQueueCreate(10, sizeof(struct_message)); // Queue for 10 messages
if (esp_now_queue == NULL) {
Serial.println("Error creating queue");
return;
}
Обновление значений таблицы в loop
Эта строка if (xQueueReceive(esp_now_queue, &myData, 0) == pdTRUE) проверяет, доступно ли сообщение в esp_now_queue без ожидания, сохраняя его в myData в случае успеха. Если сообщение получено, вызывается update_table_values(&myData) для обработки и отображения данных на экране.
if (xQueueReceive(esp_now_queue, &myData, 0) == pdTRUE) {
// Process and display the data
update_table_values(&myData);
}
Этот метод гарантирует, что мы не блокируем функцию обратного вызова ESP-NOW при записи на дисплей.
Демонстрация
Загрузите код на вашу плату CYD. Перейдите в Tools > Board и выберите ESP32 > ESP32 Dev Module. Затем выберите правильный COM-порт в Tools > Port.
Перейдите в Tools > Partition scheme > выберите любой вариант с более чем 1.4 МБ APP, например: «Huge APP (3MB No OTA/1MB SPIFFS)».
Наконец, нажмите кнопку загрузки.
Через несколько минут плата CYD начнет получать сообщения от плат-отправителей через ESP-NOW.
Данные, полученные от каждой платы, будут отображаться в таблице. Мы разделили экран на две вкладки. Каждая вкладка отображает таблицу для каждой платы.
Для переключения между вкладками можно провести пальцем влево/вправо или нажать на название вкладки вверху.
Заключение
В этом руководстве вы узнали, как использовать ESP-NOW с платой CYD для приема данных от нескольких плат ESP32.
Аналогичным образом вы также можете отправлять данные на несколько плат ESP32. Плата CYD может выступать в роли пульта дистанционного управления с кнопками, которые управляют другими платами по беспроводной связи.
Надеемся, что это руководство было для вас полезным. Если вы хотите узнать больше о создании графических пользовательских интерфейсов с помощью библиотеки LVGL с ESP32, ознакомьтесь с нашей последней книгой:
Другие руководства, которые могут вам понравиться:
Getting Started with ESP32 Cheap Yellow Display Board – CYD (ESP32-2432S028R)
ESP32 Touchscreen On/Off Button – Cheap Yellow Display (ESP32-2432S028R)
LVGL with ESP32 Cheap Yellow Display Board (ESP32-2432S028R)
Чтобы узнать больше об ESP32, обязательно ознакомьтесь с нашими ресурсами:
Спасибо за чтение.
Источник: ESP32 CYD with ESP-NOW: Receive and Display Data From Multiple Boards