دریافت و نمایش دادهها از چندین برد ESP-NOW
در این پروژه، شما یاد خواهید گرفت که چگونه از پروتکل ارتباطی ESP-NOW با برد ESP32 CYD (نمایشگر هوشمند) برای دریافت و نمایش دادهها از چندین برد فرستنده ESP32 استفاده کنید. دو برد ESP32 هر کدام دادههای حسگر BME280 را میخوانند و به برد CYD ارسال میکنند. برد CYD، که بهعنوان گیرنده ESP-NOW عمل میکند، دادههای دریافتشده را در جدولهای جداگانهای برای هر فرستنده نمایش میدهد که هر کدام در تبهای مختلفی روی صفحه نمایش داده میشوند.
مرور پروژه
در اینجا توضیحی کلی از نحوه عملکرد این پروژه ارائه شده است.
در این پروژه، برد ESP32 CYD بهعنوان گیرنده ESP-NOW عمل میکند و دادهها را از سایر بردهای ESP32 دریافت میکند.
سایر بردهای ESP32 دادههای حسگر (BME280) را بهصورت دورهای از طریق ESP-NOW به برد CYD ارسال میکنند.
برد CYD دو تب نمایش میدهد، هر تب برای یکی از بردها. هر تب شامل یک جدول با دادههای دریافتشده از بردهای دیگر است. جدولها بهمحض دریافت دادههای جدید توسط برد CYD بهروزرسانی میشوند.
پیشنیازها
قبل از ادامه، مطمئن شوید که پیشنیازهای زیر را دنبال کردهاید. باید تمام مراحل را انجام دهید، در غیر این صورت پروژه شما کار نخواهد کرد.
۱) قطعات مورد نیاز
برای این پروژه، به قطعات زیر نیاز دارید:
-ماژول نمایشگر 2.8 اینچی لمسی مخصوص هوشمند سازی بر اساس ESP32
– ۲ عدد برد ESP32 به انتخاب شما (ما از برد DOIT DevKIT V1 استفاده میکنیم)
– ۲ عدد حسگر BME280
– سیم جامپر
۲) نصب بردهای ESP32 در Arduino IDE
ما برد ESP32 را با استفاده از Arduino IDE برنامهریزی می کنیم. مطمئن شوید که بردهای ESP32 را نصب کردهاید.
۳) آشنایی با ماژول نمایشگر 2.8 اینچی لمسی مخصوص هوشمند سازی بر اساس ESP32
برد توسعه ESP32-2432S028R در جامعه سازندگان به نام «نمایشگر هوشمند» یا به اختصار CYD شناخته شده است. این برد توسعه، که تراشه اصلی آن ماژول ESP32-WROOM-32است، دارای یک صفحه نمایش لمسی TFT 2.8 اینچی ،رابط کارت microSD، یک LED RGB و تمام مدار های مورد نیاز برای برنامهریزی و تأمین توان برد است.
۴) نصب کتابخانههای TFT و LVGL
LVGL (کتابخانه گرافیکی سبک و چندمنظوره) یک کتابخانه گرافیکی رایگان و متنباز است که مجموعهای گسترده از عناصر گرافیکی کاربرپسند را برای پروژههای میکروکنترلری که نیاز به رابط کاربری گرافیکی دارند فراهم میکند
برای این پروژه، از کتابخانه 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 آپلود کنید.
#include
#include
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(){
}
پس از آپلود، سریال مانیتور را با نرخ باود 115200 باز کنید. دکمه RST روی برد را فشار دهید. آدرس MAC برد در سریال مانیتور نمایش داده خواهد شد.
آمادهسازی بردهای فرستنده
در این آموزش، دادهها از دو برد مختلف به CYD ارسال میشوند. میتوانید این پروژه را برای نمایش دادهها از بردهای بیشتری اصلاح کنید.
هر برد با یک شناسه (ID) که یک شماره است و به هر برد اختصاص داده میشود، شناسایی خواهد شد:
– ID=1 برای برد شماره ۱
– ID=2 برای برد شماره ۲
هر برد فرستنده دادههای محیطی را از یک حسگر BME280 ارسال خواهد کرد. یک حسگر BME280 را به هر یک از بردها متصل کنید. ما از پینهای پیشفرض I2C برد ESP32 استفاده خواهیم کرد.
اگر از مدل برد متفاوتی استفاده میکنید، پینهای پیشفرض I2C ممکن است متفاوت باشند.
کد زیر دادهها را از حسگر BME280 میخواند و از طریق ESP-NOW به برد CYD ارسال میکند. فراموش نکنید که کد را با آدرس MAC برد CYD خود اصلاح کنید. همچنین، حتماً شناسه برد (Board ID) را برای هر یک از بردها تغییر دهید.
#include
#include
#include
#include
// 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");
}
}
ابتدا کتابخانههای مورد نیاز برای این پروژه را فراخوانی کنید. کتابخانههای esp_now و WiFi برای استفاده از ارتباط ESP-NOW لازم هستند، و کتابخانههای Adafruit_BME280 و Adafruit_Sensor برای ارتباط با حسگر BME280 مورد نیازند.
#include
#include
#include
#include
تنظیم شناسه برد
به برد خود یک شناسه منحصربهفرد اختصاص دهید تا گیرنده بتواند بهراحتی آن را شناسایی کند. در اینجا، ما فقط بردها را شمارهگذاری میکنیم، اما میتوانید از روش دیگری مانند اختصاص دادن یک نام یا شناسایی آنها از طریق آدرس MAC استفاده کنید.
// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 2
مطمئن شوید که به هر برد یک شناسه متفاوت اختصاص میدهید.
نمونه 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 ایجاد کنید که شامل تمام دادههایی باشد که میخواهید ارسال کنید.
1- id نشاندهنده شناسه (ID) برد است.
2- temperature (دما) و humidity (رطوبت) مربوط به دادههایی هستند که از سنسور BME280 دریافت میشود.
3- 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;
}
متغیر اطلاعات همتا (Peer Info)
یک متغیر سراسری جدید از نوع esp_now_peer_info_t با نام peerInfo ایجاد کنید که بعداً برای ذخیره اطلاعات همتایان (peers) در پروتکل ESP-NOW مورد استفاده قرار خواهد گرفت.
esp_now_peer_info_t peerInfo;
تابع ()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 را مقداردهی اولیه (initialize) کنید.
Serial.begin(115200);
initBME();
برای استفاده از ESP-NOW، باید اینترفیس وایفای (Wi-Fi interface) را نیز مقداردهی اولیه (initialize) کنید.
WiFi.mode(WIFI_STA);
مقداردهی اولیه پروتکل ESP-NOW
ابتدا پروتکل ESP-NOW را راهاندازی و مقداردهی اولیه کنید.
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
ثبت تابع بازخوانی (Callback Function)
یک تابع بازخوانی ثبت نمایید که هنگام ارسال داده از طریق ESP-NOW اجرا خواهد شد.
esp_now_register_send_cb(OnDataSent);
ثبت دستگاه همتا (Peer) در ESP-NOW
دستگاه مقصد (برد گیرنده) را به عنوان همتا در پروتکل 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;
}
تابع ()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 برای ارسال داده به دستگاه همتا (Peer) استفاده میکنیم.
// 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");
تست کد فرستنده
پس از آپلود کد روی برد، سریال مانیتور را با نرخ بادریت 115200 باز کنید.
مشاهده خواهید کرد که برد شروع به ارسال داده از طریق ESP-NOW میکند. در مرحله بعد گیرنده را آماده سازی خواهیم کرد.
فراموش نکنید که این کد را روی هر برد آپلود کنید و شناسه (ID) برد را تغییر دهید.
برد گیرنده ESP-NOW مدل CYD ESP32
برد CYD به عنوان گیرنده ESP-NOW عمل خواهد کرد که دادهها را از دو برد دیگر دریافت میکند
دادههای هر برد روی صفحه نمایش داده خواهد شد. ما دو صفحه مختلف ایجاد میکنیم: یک صفحه مخصوص هر برد.
#include
#include
#include
#include
#include
#include
// 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();
float alpha_x, beta_x, alpha_y, beta_y, delta_x, delta_y;
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 خود، باید کتابخانههای lvgl.h و TFT_eSPI.h را اضافه کنید تا بتوانید محتوا را روی صفحه نمایش دهید.
#include
#include
ما همچنین از صفحه لمسی برای انتخاب بین تبها استفاده میکنیم.بنابراین باید کتابخانه صفحه لمسی را نیز اضافه کنیم.
#include
برای استفاده از ESP-NOW باید کتابخانههای WiFi و esp_now را اضافه کنید.
#include
#include
در کد ما از صفها برای دریافت داده از طریق ESP-NOW همزمان با بهروزرسانی صفحه نمایش استفاده خواهیم کرد.
#include
راهاندازی صفحه لمسی
خطوط زیر پیکربندی پینهای صفحه لمسی را تنظیم میکنند:
#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 خود باید ابعاد نمایشگر را تعریف کنید. اگر از نمایشگر پیشنهادی استفاده میکنید، اندازه 240×320 پیکسل است.
#define SCREEN_WIDTH 241
#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
تابع 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 – رسم رابط کاربری
تابع lv_create_main_gui() رابط کاربری را روی نمایشگر رسم میکند. این تابع بعداً در setup فراخوانی میشود.
ابتدا یک تبویو ایجاد میکنیم که تبهایمان را به آن اضافه میکنیم – یک تب برای هر برد.
// Create a Tab view object
lv_obj_t * tabview;
tabview = lv_tabview_create(lv_screen_active());
lv_tabview_set_tab_bar_size(tabview, 40);
سپس تبهایی که میخواهیم نمایش دهیم را ایجاد و به تبویو اضافه میکنیم.
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) {
ابتدا شناسه برد را بررسی میکنیم تا مشخص کنیم داده از کدام برد دریافت شده، سپس جدول مربوطه را بهروزرسانی میکنیم.
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());
}
تابع 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);
رسم رابط کاربری
تابع 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);
ایجاد صف
کد زیر در خط اول یک صف 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;
}
بهروزرسانی مقادیر جدول در حلقه
در خط اول کد زیر بررسی میکند که آیا پیامی در 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 را انتخاب کنید.
سپس در Tools > Port، پورت COM صحیح را انتخاب نمایید.
به Tools > Partition scheme بروید و گزینهای که بیش از 1.4MB APP دارد را انتخاب کنید، مثلاً:
“Huge APP (3MB No OTA/1MB SPIFFS)”
در نهایت، روی دکمه آپلود کلیک کنید.
پس از چند دقیقه، برد CYD شروع به دریافت پیامها از بردهای فرستنده از طریق ESP-NOW خواهد کرد.
دادههای دریافتی از هر برد در یک جدول نمایش داده میشوند. صفحه را به دو تب تقسیم کردهایم که هر تب جدول مربوط به یک برد را نشان میدهد.
برای جابجایی بین تبها میتوانید:
1- به چپ/راست بکشید.
2- یا روی نام تب در بالای صفحه ضربه بزنید