تصميم شبكة الحساسات اللاسلكية وإدارة العقد
WSN Design & Node Management
طوبولوجيا الشبكات (Star / Mesh / Tree)، التوجيه، مزامنة الوقت، اكتشاف العقد، والتعامل مع الانقطاع المؤقت — ثم نبني شبكة نجمية بـ ESP-NOW من 3–4 عقد وبوابة، وننشرها في مواقع مختلفة بالمزرعة ونقيس نسبة فقد الرسائل.
نظرة عامة على السيشن
بنهاية السيشن ستكون قادراً على
- تقارن Star و Mesh و Tree وتختار الطوبولوجيا المناسبة لمساحة المزرعة.
- تصمم رسالة شبكة فعالة (Packed struct) برقم عقدة ورقم تسلسل.
- تكتشف فقد الرسائل من Sequence numbers وتطبق Retry + Back-off.
- تزامن الوقت بين العقد (NTP / ختم الوقت عند البوابة).
- تنشئ سجل العقد (Node registry) وتكتشف العقدة الصامتة (Offline detection).
التطبيق في مزرعة الكلية
نشر 3–4 عقد ESP-NOW (الصوبة، حقل الخضر، حقل الذرة، النقطة المرجعية) وبوابة واحدة متصلة باللابتوب، وتشغيل الشبكة 30 دقيقة لقياس نسبة الفقد ومعدل إعادة الإرسال لكل عقدة.
المُخرج (Deliverable)
شبكة موزعة تعمل داخل المزرعة: عقد ترسل كل دقيقة مع رقم تسلسل وإعادة محاولة، وبوابة تكشف الرسائل المفقودة والعقد غير المتصلة، وتقرير جودة الشبكة لكل عقدة.
خطة السيشن (180 دقيقة)
- 0:00 مراجعة مراجعة: خريطة التغطية ومصفوفة القرار 10 د
- 0:10 شرح الطوبولوجيا والتوجيه والانقطاع المؤقت 30 د
- 0:40 شرح إدارة العقد: المعرفات، السجل، مزامنة الوقت 15 د
- 0:55 محاكاة Tinkercad: عقدة وبوابة بـ 2 Arduino عبر Serial 30 د
- 1:25 استراحة استراحة 10 د
- 1:35 تطبيق Lab: شبكة ESP-NOW (عقدة + بوابة) 40 د
- 2:15 ميداني ميداني: نشر 3–4 عقد واختبار الاتصال 35 د
- 2:50 تقييم اختبار سريع + الواجب 10 د
الأدوات والمكونات
قاموس المصطلحات (Glossary)
شكل اتصال العقد ببعضها.
كل العقد تتصل ببوابة مركزية واحدة.
العقد تمرر رسائل بعضها — تغطية أوسع وتعقيد أكبر.
العقدة التي تجمع البيانات وتوصلها للسيرفر.
اختيار المسار الذي تسلكه الرسالة.
معرف فريد لكل شريحة راديو (6 Bytes).
عداد يزيد مع كل رسالة لكشف الفقد والتكرار.
رد يؤكد وصول الرسالة.
الانتظار فترة متزايدة قبل إعادة المحاولة لتجنب التصادم.
مزامنة الساعة من خادم وقت عبر الإنترنت.
رسالة دورية تثبت أن العقدة ما زالت تعمل.
الرسائل المستلمة ÷ المرسلة.
الطوبولوجيا والانقطاع المؤقت
30 دقيقة| Star | Mesh | Tree / Cluster | |
|---|---|---|---|
| الشكل | العقد ← البوابة | العقد ↔ العقد ↔ البوابة | عقد ← رؤساء مجموعات ← البوابة |
| المدى | محدود بمدى البوابة | يتوسع بكل عقدة جديدة | متوسط |
| الطاقة | العقد تنام معظم الوقت ✔ | العقد الوسيطة يجب أن تبقى مستيقظة ✘ | رؤساء المجموعات يستهلكون أكثر |
| التعقيد | بسيط | معقد (ESP-MESH / Zigbee) | متوسط |
| نقطة الفشل | البوابة | لا توجد نقطة واحدة | رؤساء المجموعات |
| مناسب لـ | مزرعة الكلية (مساحة < 300 م) | مزارع كبيرة كثيفة | عدة صوب متباعدة |
التعامل مع الانقطاع المؤقت (Temporary loss)
في الحقل ستفقد رسائل حتماً: جرار يمر، شخص يقف أمام الهوائي، مطر. التصميم الجيد يفترض الفقد ويتعامل معه بـ 4 طبقات:
- Retry with back-off: أعد المحاولة 3 مرات بانتظار متزايد (200، 400، 600 ms).
- Sequence numbers: البوابة تكتشف الثغرات (seq 41 ثم 44 ⇒ فقدنا 2).
- Store-and-forward: العقدة تخزن ما لم يُرسل وترسله لاحقاً (السيشن 4).
- Offline detection: البوابة تعلن العقدة «غير متصلة» إذا صمتت أكثر من 3 فترات إرسال.
سؤال تصميم
لماذا لا نجعل كل العقد ترسل في نفس الثانية (مثلاً كل دقيقة عند الثانية 0)؟ — فكر في التصادم (Collision). الحل: أضف تأخيراً عشوائياً صغيراً (Jitter) = random(0, 2000) ms.
إدارة العقد ومزامنة الوقت
15 دقيقة| node_id | MAC | الموقع | GPS | المحصول | الحساسات | Firmware |
|---|---|---|---|---|---|---|
| greenhouse_01 | 24:6F:28:.. | الصوبة التجريبية | 31.1052, 30.9431 | طماطم | DHT22, Soil, LDR | 1.0.0 |
| field_A | 24:6F:28:.. | حقل الخضر | 31.1047, 30.9442 | فلفل | DHT22, Soil | 1.0.0 |
| field_B | 24:6F:28:.. | حقل الذرة | 31.1041, 30.9436 | ذرة | DHT22, Soil | 1.0.0 |
من يضع الوقت على القراءة؟
الخيار الأبسط والأكثر موثوقية: البوابة/السيرفر يختم الوقت عند الاستلام (عقد ESP-NOW لا تعرف الوقت). الخيار الأدق: العقدة المتصلة بالواي فاي تزامن ساعتها من NTP مرة يومياً وتختم القراءة بنفسها — مهم لو خزّنت قراءات وأرسلتها متأخرة.
#include <WiFi.h>
#include <time.h>
void syncTime() {
configTzTime("EET-2EEST,M4.5.5/0,M10.5.4/24", "pool.ntp.org", "time.google.com"); // Africa/Cairo
struct tm t;
if (getLocalTime(&t, 10000)) {
char buf[25];
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &t); // ISO-8601 -> same format the server expects
Serial.printf("time synced: %s\n", buf);
} else {
Serial.println("NTP failed - server will timestamp on arrival");
}
}
سلسلة المنطقة الزمنية POSIX لمصر تتضمن التوقيت الصيفي. إن تغيرت قواعده يكفي تعديل هذا السطر — أو اترك السيرفر يضع الوقت.
Tinkercad: عقدة وبوابة
30 دقيقةمحاكاة شبكة نجمية مصغرة بـ 2 Arduino عبر Serial
30 دقيقةلا يوجد راديو في Tinkercad، لكن يمكننا محاكاة «الرابط» بسلك Serial بين Arduino «العقدة» و Arduino «البوابة». الفكرة نفسها: رسالة برقم عقدة ورقم تسلسل، والبوابة تكتشف الفقد. سنضيف زراً «يقطع الاتصال» لنرى كيف تتصرف البوابة.
المكونات (Components)
- 2 × Arduino Uno R3
- Soil Moisture Sensor (على العقدة)
- Slide switch (قاطع الرابط)
- LED أحمر + 220Ω (على البوابة — Offline)
التوصيل (Wiring)
| من | إلى | ملاحظة |
|---|---|---|
| Node D3 (TX soft) | Slide switch → Gateway D2 (RX soft) | القاطع يحاكي انقطاع الراديو |
| Node GND | Gateway GND | أرضي مشترك ضروري |
| Node A1 | Soil sensor SIG | |
| Gateway D13 | LED أحمر | يضيء إذا صمتت العقدة > 6 ث |
ضع الكود الأول في Arduino العقدة والثاني في Arduino البوابة (كل لوحة لها نافذة كود مستقلة).
شغّل المحاكاة وافتح Serial Monitor للبوابة: ستظهر رسائل
node=1 seq=… soil=….افصل القاطع 10 ثوانٍ ثم أعده: لاحظ رسالة OFFLINE ثم عدد الرسائل المفقودة (lost).
// ===== NODE (Arduino #1) =====
#include <SoftwareSerial.h>
SoftwareSerial radio(2, 3); // RX, TX (TX on D3 -> gateway)
unsigned long seq = 0;
void setup() { radio.begin(9600); }
void loop() {
seq++;
int soil = analogRead(A1);
radio.print("N1,"); radio.print(seq); radio.print(','); radio.println(soil);
delay(2000 + random(0, 300)); // period + jitter
}
// ===== GATEWAY (Arduino #2) =====
#include <SoftwareSerial.h>
SoftwareSerial radio(2, 3); // RX on D2 <- node
unsigned long lastSeq = 0, lost = 0, lastSeen = 0;
void setup() { Serial.begin(9600); radio.begin(9600); pinMode(13, OUTPUT); }
void loop() {
if (radio.available()) {
String line = radio.readStringUntil('\n'); // "N1,42,512"
int a = line.indexOf(','), b = line.indexOf(',', a + 1);
unsigned long seq = line.substring(a + 1, b).toInt();
if (lastSeq && seq > lastSeq + 1) lost += seq - lastSeq - 1;
lastSeq = seq; lastSeen = millis();
Serial.print("node=1 seq="); Serial.print(seq);
Serial.print(" soil="); Serial.print(line.substring(b + 1));
Serial.print(" lost="); Serial.println(lost);
}
bool offline = lastSeen && millis() - lastSeen > 6000; // 3 missed periods
digitalWrite(13, offline);
static bool warned = false;
if (offline && !warned) { Serial.println("# node 1 OFFLINE"); warned = true; }
if (!offline) warned = false;
}
Lab: شبكة ESP-NOW
40 دقيقةبناء شبكة نجمية حقيقية بـ ESP-NOW
- البوابة أولاً
ارفع كود البوابة وانسخ عنوان الـ MAC الذي يطبعه.
- العقد
في كود العقدة ضع MAC البوابة في
GATEWAY_MACورقم عقدة فريدNODE_NUMلكل فريق، ونفس القناةCHANNEL. - نسخة الـ Core
الكود مكتوب لـ ESP32 Arduino core 3.x. لو عندك 2.x غيّر توقيع دوال الـ callback كما هو موضح في التعليق أعلى الملف.
- الاختبار
شغّل كل العقد وراقب البوابة: سطر JSON لكل رسالة به RSSI وعدد المفقود.
- اختبار الانقطاع
افصل عقدة دقيقتين ثم أعدها: هل ظهرت OFFLINE؟ هل حُسب الفقد صحيحاً؟
- إلى السيرفر
وصل البوابة باللابتوب وشغّل
python serial_to_api.py --port COM5(من مشروع Smart Farm) لإرسال البيانات للـ API — سنفهمه بالتفصيل في السيشن 8.
النتيجة المتوقعة: كل العقد تظهر في البوابة بنسبة تسليم (PDR) أعلى من 95% في المعمل.
/*
Session 6 — ESP-NOW sensor node (star topology, no router needed in the field)
Sends a struct to the gateway every 60 s; retries 3 times if the delivery callback reports failure.
Set GATEWAY_MAC to the MAC printed by s06_espnow_gateway. Use the SAME Wi-Fi channel as the gateway.
Compatible with ESP32 Arduino core 3.x (esp_now_send_cb_t uses wifi_tx_info_t). For core 2.x change the
callback signature to: void onSent(const uint8_t *mac, esp_now_send_status_t status)
*/
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <DHT.h>
uint8_t GATEWAY_MAC[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC}; // <-- edit
const uint8_t CHANNEL = 6;
const uint8_t NODE_NUM = 2; // 1..20, unique per node
typedef struct __attribute__((packed)) {
uint8_t node;
uint32_t seq;
float temp;
float hum;
uint16_t soilRaw;
float battery;
} Reading;
DHT dht(4, DHT22);
Reading msg;
volatile bool lastOk = false;
void onSent(const wifi_tx_info_t *info, esp_now_send_status_t status) { lastOk = (status == ESP_NOW_SEND_SUCCESS); }
void setup() {
Serial.begin(115200);
dht.begin();
WiFi.mode(WIFI_STA);
esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE);
if (esp_now_init() != ESP_OK) { Serial.println("ESP-NOW init failed"); ESP.restart(); }
esp_now_register_send_cb(onSent);
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, GATEWAY_MAC, 6);
peer.channel = CHANNEL;
esp_now_add_peer(&peer);
Serial.print("node MAC: "); Serial.println(WiFi.macAddress());
}
void loop() {
msg.node = NODE_NUM;
msg.seq++;
msg.temp = dht.readTemperature();
msg.hum = dht.readHumidity();
msg.soilRaw = analogRead(34);
msg.battery = analogReadMilliVolts(33) * 2 / 1000.0;
for (int attempt = 1; attempt <= 3; attempt++) { // handle temporary loss
lastOk = false;
esp_now_send(GATEWAY_MAC, (uint8_t *)&msg, sizeof(msg));
delay(60);
if (lastOk) break;
Serial.printf("seq %lu attempt %d failed\n", msg.seq, attempt);
delay(200 * attempt); // back-off
}
Serial.printf("seq %lu sent=%s\n", msg.seq, lastOk ? "OK" : "LOST");
delay(60000);
}
/*
Session 6 — ESP-NOW gateway (center of the star) — receives all field nodes and prints JSON lines.
Connect it by USB to the farm laptop: python serial_to_api.py forwards each line to the Flask API,
or extend it to publish over MQTT (session 7). Detects lost packets with the sequence number and
marks a node offline when nothing arrives for 3 minutes.
ESP32 Arduino core 3.x callback signature (core 2.x: void onRecv(const uint8_t *mac, const uint8_t *data, int len)).
*/
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
const uint8_t CHANNEL = 6;
typedef struct __attribute__((packed)) {
uint8_t node; uint32_t seq; float temp; float hum; uint16_t soilRaw; float battery;
} Reading;
uint32_t lastSeq[21] = {0};
unsigned long lastSeen[21] = {0};
uint32_t lost[21] = {0};
void onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
if (len != sizeof(Reading)) return;
Reading r; memcpy(&r, data, sizeof(r));
if (r.node > 20) return;
if (lastSeq[r.node] && r.seq > lastSeq[r.node] + 1) lost[r.node] += r.seq - lastSeq[r.node] - 1;
lastSeq[r.node] = r.seq;
lastSeen[r.node] = millis();
Serial.printf("{\"node_id\":\"node_%02d\",\"seq\":%lu,\"air_temp_c\":%.1f,\"air_humidity_pct\":%.1f,"
"\"soil_raw\":%u,\"battery_v\":%.2f,\"rssi\":%d,\"lost\":%lu}\n",
r.node, r.seq, r.temp, r.hum, r.soilRaw, r.battery, info->rx_ctrl->rssi, lost[r.node]);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE);
esp_now_init();
esp_now_register_recv_cb(onRecv);
Serial.print("# gateway MAC (copy into the nodes): "); Serial.println(WiFi.macAddress());
}
void loop() {
static unsigned long check = 0;
if (millis() - check > 30000) {
check = millis();
for (int n = 1; n <= 20; n++)
if (lastSeen[n] && millis() - lastSeen[n] > 180000) Serial.printf("# node_%02d OFFLINE\n", n);
}
}
__attribute__((packed)) يمنع المترجم من إضافة فراغات داخل الـ struct، فيكون حجمها متطابقاً بين العقدة والبوابة — بدونها قد تُقرأ القيم خطأ.
ميداني: نشر الشبكة
35 دقيقةنشر 3–4 عقد في المزرعة واختبار الاتصال
البوابة عند غرفة المضخات / المبنى، والعقد في نقاط القياس الأربع
- التركيب
ثبّت كل عقدة على وتد بارتفاع 1–1.5 م (من نتائج السيشن 5) مع Power bank.
- البدء المتزامن
شغّل كل العقد خلال دقيقتين وسجّل وقت التشغيل.
- التشغيل 30 دقيقة
البوابة تسجل كل الرسائل في ملف (انسخ Serial Monitor أو استخدم serial_to_api.py).
- حساب PDR
لكل عقدة: المستلم ÷ (آخر seq) × 100. وسجل متوسط RSSI.
- تحسين
العقدة صاحبة أقل PDR: جرّب رفع الهوائي أو تغيير اتجاه اللوحة أو تقريب البوابة، وأعد القياس 10 دقائق.
| العقدة | المسافة للبوابة (م) | آخر seq | المستلم | PDR % | متوسط RSSI | إجراء التحسين |
|---|---|---|---|---|---|---|
- لا تثبت الأوتاد داخل خطوط التنقيط.
- أعد كل الأجهزة في نهاية الجلسة (ما لم يتفق على تركها مع المشرف).
فكّر وناقش
ما العقدة الأسوأ ولماذا؟ هل يطابق ذلك توقعات خريطة التغطية؟
لو فشلت البوابة — ماذا يحدث للشبكة كلها؟ كيف نقلل هذا الخطر؟
اختبر فهمك
5 أسئلة · نتيجة فوريةالواجب والتسليم
الواجب الأسبوعي (Weekly Assignment)
اكتب تقرير جودة الشبكة: جدول PDR و RSSI لكل عقدة + خريطة + إجراءات التحسين ونتيجتها.
حدّث سجل العقد (Node registry) بعناوين MAC الحقيقية وإحداثيات GPS.
تحدٍ: عدّل البوابة لتطبع متوسط RSSI لكل عقدة كل 5 دقائق.
قائمة التسليم (Submission Checklist)
استعد للسيشن القادمة
السيشن القادمة: MQTT. ثبّت Mosquitto (mosquitto.org/download) و MQTT Explorer على اللابتوب قبل الحضور.