سنوضّح في هذا المقال كيفيّة استخدام بروتوكول الاتّصال ESP-NOW لإرسال قراءات عدّة حسّاسات من عدّة لوحات ESP32 إلى لوحة واحدة تعمل كخادم ويب لعرض البيانات المستقبَلة (اتّصال من نوع عدّة أجهزة إلى واحد many-to-one)، وسنبرمج اللّوحات باستخدام بيئة أردوينو البرمجيّة Arduino IDE.
يوضّح الفيديو التالي آليّة تنفيذ المشروع.
استخدام بروتوكول ESP-NOW لإرسال البيانات عبر الواي فاي
تؤخذ عدّة أمور بعين الاعتبار عند استخدام بروتوكول ESP-NOW لإرسال البيانات من حسّاس ما إلى خادم الويب عبر الواي فاي، وهي:
- على لوحات ESP32 المرسِلة والمستقبِلَة استخدام نفس قناة الواي فاي التردّديّة.
- يضبُط موجّهك (router) قناة واي فاي المستقبِل تلقائيّاً.
- يعمل المستقبِل وفق النّمط Station، أي يُمثّل نقطة اتّصال لباقي الأجهزة (WIFI_AP_STA).
- يمكننا ضبط قناة الواي فاي يدويّاً، أو بواسطة برنامج بسيط يُبرمج عليه المُرسل لضبط قناته على قناة المستقبِل.
نظرة عامّة على المشروع
يَعرض الشكل التالي مخطّطاً عامّاً للمشروع المراد بناؤه.
- يحوي المشروع لوحتي ESP32 ترسلان درجة الحرارة والرّطوبة المُقاسة من حسّاس DHT22 عبر بروتوكول ESP-NOW إلى اللوحة المستقبِلة (وذلك وفق اتّصال من نوع عدّة أجهزة إلى واحد).
- تتلقّى اللوحة المستقبِلة مجموعة القيم المُرسلَة وتعرضها على خادم الويب.
- تتحدّث البيانات الموجودة على صفحة الويب تلقائيّاً عند كلّ مرّة يتلقّى فيها الخادم قيم جديدة باستخدام تقنيّة Server-Sent Events) SSE)، وهي طريقة تعتمد على تحديث الخادم للبيانات المعروضة على صفحة الويب دون إرسال طلب من المستخدم، أي متصفّح الويب client، إلى الخادم لتحديث البيانات.
مُتطلّبات تنفيذ المشروع
نتأكّد في البداية من تثبيت بيئة أردوينو البرمجيّة التي سنبرمج لوحات ESP32 بواسطتها، وتحميل المكتبات التالية ضمنها:
- مكتبة لوحة ESP32.
- مكتبات حسّاس الرطوبة: تُقاس درجة الحرارة والرطوبة من خلال حسّاس DHT22، ثُمّ تُقرأ بواسطة مكتبة DHT الخاصّة بالحسّاس، وتُرسَل إلى صفحة الويب.
- يجب تحميل مكتبة Adafruit Unified Sensor أوّلاً، ثمّ مكتبة DHT، وذلك باتّباع الخطوات التالية:
- نفتح بيئة الأردوينو، ونتّبع المسار التالي:
Sketch > Include Library > Manage Libraries
نبحث عن “DHT” عبر مربّع البحث، ونبدأ بتنزيل مكتبة DHT من Adafruit.
بعد تنزيل مكتبة DHT، ننسخ العبارة “Adafruit Unified Sensor ” إلى مربّع البحث، ونبدأ عمليّة التنزيل.
بعد الانتهاء من تنزيل المكتبات السّابقة، نعيد تشغيل بيئة الأردوينو.
تضمين مكتبات خادم الويب:
لبناء خادم الويب، تحتاج إلى تنزيل المكتبات التالية:
لا تتوافر هذه المكتبات ضمن بيئة الأردوينو لتنزيلها مباشرةً، لذلك تحتاج إلى نسخ الملفات التي تحوي تلك المكتبات إلى مجلّد التنزيلات الخاصّ بالأردوينو Installation Libraries، أو يمكن اتّباع ما يلي:
Sketch > Include Library > AddZipLibrary.
واختيار المكتبات المطلوبة.
تنزيل مكتبة Arduino_JSON ضمن البيئة البرمجيّة:
يمكن تنزيل مكتبة Arduino_JSON من نافذة إدارة المكتبات Library Manager ضمن بيئة الأردوينو، وذلك باتّباع الخطوات:
Sketch > Include Library > Manage Libraries.
والبحث عن اسم المكتبة المطلوبة.
القطع المطلوبة:
– ثلاث لوحات ESP32.
– حسّاسان للرطوبة والحرارة DHT22.
– مقاومتان 4.7K Ohm.
– لوحة اختبار Breadboard.
– أسلاك توصيل Jumper wires.
الحصول على العنوان الفيزيائيّ MAC للوحة المستقبِلة:
نحتاج إلى معرفة العنوان الفيزيائيّ للوحة المستقبِلة لإرسال رسائل عبر ESP-NOW، فلكلّ لوحة عنوان فيزيائيّ فريد نحصل عليه بتحميل البرنامج التالي على اللوحة:
لتحميل الكود البرمجي: من هنا.
نضغط على زرّ RST/EN بعد تحميل الكود لعرض العنوان الفيزيائيّ على واجهة الاتّصال التسلسليّ serial monitor.
تلقّي البيانات من ESP32 إلى صفحة الويب عبر ESP-NOW:
تُرسِل اللوحات المرسلة من ESP32 حزم البيانات إلى اللوحة المستقبِلة التي تشغّل خادم الويب لعرض البيانات المرسَلة على صفحة الويب.
نحمّل الكود التالي المُجهّز لاستقبال البيانات من لوحتين مختلفتين إلى اللوحة المستقبِلة:
لتحميل الكود البرمجي: من هنا.
كيفيّة عمل الكود البرمجيّ
نضمّن المكتبات الضروريّة كما يلي:
#include <esp_now.h>
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
#include <Arduino_JSON.h>
تُعدّ مكتبة Arduino_JSON ضروريّة لأنّنا سنتعامل مع متغيّرات JSON لإرسال جميع البيانات المستقبَلة إلى صفحة الويب كما سنرى لاحقاً.
أدخل بيانات شبكتك المحلّيّة لتتّصل ESP32 بها، وذلك عبر التعليمات التالية:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Data structure إنشاء
سنُعرّف structure ونسمّيها Struct-message؛ لتخزين البيانات المستقبَلة، ورقم المعرّف ID الخاص باللوحة، والبيانات المقروءة من الحسّاس:
typedef struct struct_message {
int id;
float temp;
float hum;
int readingId;
} struct_message;
سنعرّف أيضاً متغيّراً آخراً من نوع struct_message، ونسمّيه incomingReadings لنُخزّن ضمنه قيم المتغيّرات.
struct_message incomingReadings;
ثمّ نعرّف متغيّر من نوع JSON اسمه board:
JSONVar board;
وننشئ خادم ويب غير متزامن على المنفذ 80 بتعريف كائن من الصفّ AsyncWebServer:
AsyncWebServer server(80);
event source تعريف
سنستخدم تقنيّة Server-Sent Events لعرض المعلومات الجديدة الواردة على صفحة الويب تلقائيّاً عند وصولها، ويوضّح السطر البرمجيّ التالي إنشاء event source على المسار “/events” (عنوان URL):
AsyncEventSource events("/events");
تسمح هذه التقنيّة لصفحة الويب بتحديث بياناتها وعرضها مباشرة، وهي غير مدعومة على متصفّح Internet Explorer.
OnDataRecv الدالّة
ستُنفَّذ هذه الدالّة عند استقبال حزم بيانات جديدة بوساطة ESP-NOW:
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
وستتضمّن تعليمة طباعة العنوان الفيزيائيّ للمرسِل:
// Copies the sender mac address to a string
char macStr[18];
Serial.print("Packet received from: ");
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.println(macStr);
ثمّ ننسخ المعلومات المخزّنة ضمن المُتغيّر incomingData إلى incomingReadings:
memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));
ثمَّ نعرّف متغيّراً نصّيّاً من نوع JSON يحوي المعلومات الواردة (jsonString variable):
board["id"] = incomingReadings.id;
board["temperature"] = incomingReadings.temp;
board["humidity"] = incomingReadings.hum;
board["readingId"] = String(incomingReadings.readingId);
String jsonString = JSON.stringify(board);
وسنوضّح بمثال بسيط كيف تكون قيم متغيّرات jsonString بعد تلقّي البيانات:
board = {
"id": "1",
"temperature": "24.32",
"humidity" = "65.85",
"readingId" = "2"
}
وبعد تجميع كلّ البيانات المُستقبلة في متغيّر من نوع jsonString، نرسل المعلومات إلى المتصفّح كحدث event باسم (”new_readings”):
events.send(jsonString.c_str(), "new_readings", millis());
وأخيراً، نعرض المعلومات المستقبَلة على واجهة الاتّصال التسلسليّ في البيئة البرمجيّة Arduino IDE بهدف معالجة وتصحيح الأخطاء (debugging):
Serial.printf("Board ID %u: %u bytes\n", incomingReadings.id, len);
Serial.printf("t value: %4.2f \n", incomingReadings.temp);
Serial.printf("h value: %4.2f \n", incomingReadings.hum);
Serial.printf("readingID value: %d \n", incomingReadings.readingId);
Serial.println();
events بناء صفحة الويب والتعامل مع الأحداث
يتضمّن المتغيّر index_html جميع أكواد HTML وcss وJavascript المخصَّصة لبناء صفحة الويب، ولكن سنتطرّق فقط إلى شرح معالجة الأحداث المرسَلة من الخادم، دون شرح آليّة عمل HTML وCSS.
ننشئ كائن من نوع EventSource (وهو في مثالنا هذا /events)، ونُحدّد عنوان URL للصفحة المُرسِلَة للتّحديثات:
if (!!window.EventSource) {
var source = new EventSource('/events');
حينها يمكن استقبال البيانات المُرسَلة من الخادم عبر الدالّة (addEventListener). وفيما يلي الدوالّ الافتراضيّة المسمّاة event listeners لاستقبال البيانات من الخادم (event listener هو إجراء يُنفذ عند تحقق حدث معين مثل حدث النقر على عنصر ما):
لتحميل الكود البرمجي: من هنا.
ثمّ نضيف التعليمة التالية:
source.addEventListener('new_readings', function(e) {
ترسل لوحة ESP32 قراءات الحسّاسات ضمن ملفّ نصّيّ من نوع JSON كحدث (”new_readings”) إلى المستقبِل (المتصفّح) عندما تصلها حزم بيانات جديدة. وتوضّح السّطور البرمجيّة التالية ما يحدث عند استقبال المُتصفّح لهذا الحدث:
لتحميل الكود البرمجي: من هنا.
إذ نطبع القيم الجديدة المقروءة على واجهة الأوامر للمتصفّح (browser console)، ونضيف تلك البيانات إلى عناصر الويب elements ذات قيمة ID المتوافقة معها لعرضها على صفحة الويب.
setup الدالّة
نضبط من خلالها لوحة ESP32 المُستقبِلة لتعمل وفق النمط Access Point and Station:
WiFi.mode(WIFI_AP_STA);
ثمّ نستخدم التعليمات التالية لتتّصل لوحة ESP32 بالشبكة المحلّيّة، ونطبع العنوان المنطقيّ لها (IP)، والقناة التردّديّة للواي فاي:
لتحميل الكود البرمجي: من هنا.
والآن، نضبط ونهيّئ بروتوكول الاتّصال ESP-NOW:
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
وعندما تصل حزم بيانات جديدة، تُستدعى الدالّة OnDataRecv (وتدعى callback function وهي دالة تُمرر كوسيط إلى دالة أخرى أي باراميتر)، ونحدّد هذا بوساطة التعليمة التالية:
esp_now_register_recv_cb(OnDataRecv);
requests معالجة الطلبات
نرسل النصّ المُخزّن في المتغيّر index_html عند الوصول إلى العنوان المنطقيّ للوح ESP32 على عنوان الخادم الرئيسيّ (root / URL)، وذلك لبناء صفحة الويب:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html);
});
لتحميل الكود البرمجي: من هنا.
ضبط وتهيئة الخادم
إنّ تعليمة تشغيل الخادم هي:
server.begin();
loop الدالّة
ترسل رسالة (ping) كلّ 5 ثوانٍ ليتحقّق المتصفّح (العميل) من عمل الخادم:
static unsigned long lastEventTime = millis();
static const unsigned long EVENT_INTERVAL_MS = 5000;
if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
events.send("ping",NULL,millis());
lastEventTime = millis();
}
يوضّح المخطّط التالي آليّة عمل التقنيّة SSE في هذا المشروع، وكيفيّة إظهارها للقيم الجديدة على صفحة الويب دون تحديثها.
بعد تحميل البرنامج إلى اللوح المستقبل، نضغط على الزرّ EN/RST المتوضِّع عليه لعرض العنوان المنطقيّ له على واجهة الاتّصال التسلسليّ.
دارة الإرسال
نصل ألواح ESP32 المرسِلة مع حسّاس الحرارة والرطوبة DHT22، وطرف البيانات للحسّاس (data pin) مع الطرف الرابع (GPIO4) من أطراف الدخل والخرج للوح ESP32 (ولك حرّيّة اختيار أيّ طرف مناسب).
برنامج ألواح الإرسال
يرسل كلّ لوح منها structure بوساطة بروتوكول ESP-NOW، وتحوي مُعرّف اللوح ID لمعرفة اللوح المرسِل للبيانات، ودرجة الحرارة والرطوبة، ومعرّف حزمة البيانات الجديدة (قراءات الحسّاسات)، والذي يفيد في معرفة عدد الرسائل المرسَلة.
حمّل البرنامج التالي إلى كلّ لوح مرسِل، مع الانتباه إلى زيادة قيمة معرّف كلّ لوح عن سابقه، وإدخال اسم شبكتك في المتغيّر WIFI_SSID:
لتحميل الكود البرمجي: من هنا.
شرح خطوات كتابة البرنامج السابق:
- ضمّن المكتبات التالية:
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
- اضبط معرّفات الألواح:
نحدّد معرّفات الألواح المرسِلة. مثلاً، يكون معرّف الأوّل BOARD_ID 1، وهكذا دواليك:
// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 1
- حسّاس الحرارة والرطوبة:
يجب تعريف الطرف المتّصل بالحسّاس، وهو في مثالنا GPIO4:
#define DHTPIN 4
واختيار نوع الحسّاس المستخدم ضمن المشروع، وهو DHT22:
// Uncomment the type of sensor in use:
//#define DHTTYPE DHT11 // DHT 11
#define DHTTYPE DHT22 // DHT 22 (AM2302)
//#define DHTTYPE DHT21 // DHT 21 (AM2301)
ننشئ كائن من نوع DHT على الطرف وبالنوع المعرّفين سابقاً:
DHT dht(DHTPIN, DHTTYPE);
- العنوان الفيزيائيّ للمرسِل:
نحدّد العنوان الفيزيائيّ للمرسِل بالتعليمة التالية:
uint8_t broadcastAddress[] = {0x30, 0xAE, 0xA4, 0x15, 0xC7, 0xFC};
- إنشاء Data Structure:
ننشئ structure تعرّف متغيّرات البيانات المراد إرسالها، ونسمّيها strucrt_message، وتحوي متغيّرات معرّف اللوح، ودرجة الحرارة والرطوبة، ومعرّف حزمة البيانات:
لتحميل الكود البرمجي: من هنا.
ننشئ متغيّر جديد من نوع strucrt_message، ونسمّيه myData؛ لتخزين قيم المتغيّرات المذكورة سابقاً:
struct_message myData;
- الفاصل الزمنيّ بين تحديثات القراءات:
ننشئ بعض المتغيّرات المساعدة لتنظيم قراءة الحسّاس للقيم، ففي مثالنا، يقرأ الحسّاس قيمة جديدة كلّ 10 ثوانٍ، ويمكن تغيير المدّة الزمنيّة من خلال المتغيّر interval:
unsigned long previousMillis = 0; // Stores last time temperature was published
const long interval = 10000; // Interval at which to publish sensor readings
ونضبط المتغيّر readingId لتتبّع عدد القراءات التي يرسلها الحسّاس:
unsigned int readingId = 0;
- تغيير قناة الواي فاي:
من الضروريّ معرفة قناة الواي فاي للمستقبِل لتعيين قناة المرسِل تلقائيّاً. ولتحقيق ذلك، نُدخل اسم الشبكة كالتالي:
constexpr char WIFI_SSID[] = "REPLACE_WITH_YOUR_SSID";
ثمّ تبحث الدالّة getWiFiChannel)) عن الشبكة وتحدّد قناتها:
لتحميل الكود البرمجي: من هنا.
- قراءة درجة الحرارة:
تقرأ الدالّة readDHTTemperature) درجة الحرارة المقاسة من الحسّاس، وعند عدم قدرتها على الحصول على درجة الحرارة، فإنّها تعيد القيمة صفر:
لتحميل الكود البرمجي: من هنا.
- قراءة درجة الرطوبة:
تشابه الدالّة readDHTHumidity)) سابقتها تماماً:
لتحميل الكود البرمجي: من هنا.
- استدعاء الدالّة OnDataSent:
تنفذ الدالّة OnDataSent لحظة إرسال حزمة البيانات، وتطبع عبارة تظهر فيما إذا أرسلت الحزمة بنجاح أو لا:
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print(“\r\nLast Packet Send Status:\t”);
Serial.println(status == ESP_NOW_SEND_SUCCESS ? “Delivery Success” : “Delivery Fail”);
}
- الدالّة setup:
تهيئة واجهة الاتّصال التسلسليّ:
Serial.begin(115200);
ضبط وضع عمل لوح ESP32 المرسِل على Wi-Fi station:
WiFi.mode(WIFI_STA);
ضبط قناة واي فاي المرسِل لتطابق قناة المستقبِل:
لتحميل الكود البرمجي: من هنا.
تهيئة البروتوكول ESP-NOW:
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
بعد نجاح تهيئة البروتوكول، نربط استدعاء الدالّة OnDataSent بحدث إرسال حزمة البيانات كما يلي:
esp_now_register_send_cb(OnDataSent);
- إضافة نظير (نِدّ peer):
لإرسال البيانات إلى اللوح المستقبِل، نحتاج إلى ربطهم كنظراء، وتوضّح السطور التالية تسجيل وإضافة المستقبل الجديد كنظير:
لتحميل الكود البرمجي: من هنا.
- الدالّة loop:
نتحقّق من خلالها من ملاءمة الوقت لإرسال القيم المقروءة من الحسّاس:
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// Save the last time a new reading was published
previousMillis = currentMillis;
- إرسال البيانات عبر ESP-NOW:
نرسل الـ structure التي تحوي قراءات الحسّاس عبر ESP-NOW المُسمّاة myData كما يلي:
// Send message via ESP-NOW
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
if (result == ESP_OK) {
Serial.println("Sent with success");
}
else {
Serial.println("Error sending the data");
}
ختاماً، نرفع البرنامج كاملاً إلى اللوح المرسِل، مع الانتباه إلى أنّ اللوحات تُغيّر قناتها الواي فاي لتماثل قناة المستقبِل، ورقم القناة هنا هو 6.
التّحقّق من عمل المشروع
يبدأ لوح ESP32 المستقبل بتلقّي القيم المقروءة من الحسّاسات الموصولة باللوحات الأخرى بعد تحميل البرنامج إلى كلّ اللوحات وبدء تنفيذ المشروع.
نفتح المتصفّح عبر شبكتنا المحلّيّة، ونلصق العنوان المنطقيّ للوح ضمن شريط العناوين لعرض القيم والتأكّد من عمل المشروع.
يجب أن تظهر على صفحة الويب درجة الحرارة والرطوبة ومعرّفات الألواح، حيث تظهر القيم الجديدة على الصفحة عند استقبال كلّ حزمة بيانات مرسَلة دون الحاجة إلى تحديث الصفحة.
مُلخّص
تعلّمنا في هذا المشروع كيفيّة استخدام البروتوكول ESP-NOW واتّصال الواي فاي لضبط وتهيئة خادم الويب لاستقبال البيانات عبر البروتوكول من عدّة لوحات (وفق اتّصال عدّة أجهزة إلى واحد)، إضافةً إلى استخدام تقنيّة Server Sent Events لتحديث صفحة الويب تلقائيّاً عند كلّ استلام لحزم بيانات جديدة دون الحاجة إلى تحديث الصفحة.
ترجمة: اسراء اسماعيل، مراجعة: ايليا سليمان، تديق لغوي: سلام أحمد، تصميم: علي العلي، تحرير: نور شريفة