پرش به محتوای اصلی
سخت‌افزار و IoT
۴ دقیقه مطالعه

کنترل ESP32 از راه دور با پنل وب: الگوی وضعیت مطلوب و Polling

معماری ساده و مقاوم برای کنترل دستگاه ESP32 (مثل پمپ دوزینگ) از پنل وب بدون IP ثابت و باز کردن پورت: وضعیت مطلوب به‌جای فرمان، HTTPS روی ESP32، وضعیت امن هنگام قطعی، watchdog و مقایسه با MQTT.

برنامه‌نویس فول‌استک

دستگاهی که پشت مودم خانگی یا اینترنت سیم‌کارت است، IP عمومی ثابت ندارد و نمی‌شود مستقیم به آن وصل شد. وقتی کنترل یک پمپ دوزینگ با ESP32 را از طریق پنل وب و اپ اندروید می‌ساختم، به‌جای پیچیدگی‌هایی مثل باز کردن پورت روی مودم یا سرویس‌های DNS پویا، راه ساده‌تری انتخاب کردم: خود دستگاه هر چند ثانیه از سرور می‌پرسد «الان باید در چه وضعیتی باشم؟» و خودش را با آن هماهنگ می‌کند. این مقاله این الگو و جزئیاتی است که آن را در اینترنت ناپایدار قابل اعتماد می‌کند.

وضعیت مطلوب به‌جای فرمان

تفاوت اصلی این الگو در این است که سرور «فرمان» نمی‌فرستد («پمپ را روشن کن»)، بلکه «وضعیت مطلوب» را نگه می‌دارد («پمپ ۱ روشن، دبی ۲۰»). دستگاه در هر بار ارتباط، وضعیت واقعی خودش را گزارش می‌دهد و وضعیت مطلوب را می‌گیرد و اگر فرق داشتند، خودش را تنظیم می‌کند.

این تفاوت در شبکه ناپایدار حیاتی است. در مدل فرمان، اگر پیامی گم شود، فرمان اجرا نمی‌شود؛ اگر دو بار برسد، دو بار اجرا می‌شود. در مدل وضعیت مطلوب، پیام گم‌شده مهم نیست چون ارتباط بعدی همه‌چیز را درست می‌کند، و تکرار هم بی‌خطر است چون تنظیم کردن به همان وضعیت، تغییری ایجاد نمی‌کند. همان ایده‌ای است که در ابزارهای مدیریت زیرساخت هم استفاده می‌شود.

نکات کلیدی

  • سرور: وضعیت مطلوب و آخرین وضعیت گزارش‌شده هر دستگاه.
  • دستگاه: گزارش وضعیت واقعی، گرفتن وضعیت مطلوب، هماهنگ شدن.
  • پیام گم‌شده یا تکراری بی‌خطر است.

سمت سرور

سمت سرور یک endpoint ساده است. دستگاه با توکن اختصاصی خودش احراز هویت می‌شود، وضعیت فعلی‌اش را می‌فرستد و وضعیت مطلوب را می‌گیرد. سرور زمان آخرین ارتباط را ثبت می‌کند تا پنل بتواند نشان دهد دستگاه آنلاین است یا نه. پاسخ هم فاصله ارتباط بعدی را تعیین می‌کند؛ مثلاً وقتی کاربر پنل را باز کرده، فاصله کوتاه‌تر می‌شود تا تغییرات سریع‌تر اعمال شوند.

typescript
// POST /api/device/sync  —  Authorization: Bearer <device token>
app.post('/api/device/sync', async (req, res) => {
  const device = await devices.byToken(bearer(req));
  if (!device) return res.status(401).end();

  const reported = DeviceState.parse(req.body);          // اعتبارسنجی ورودی دستگاه
  await devices.saveReported(device.id, reported, new Date());

  const desired = await devices.desired(device.id);      // همان چیزی که پنل تنظیم کرده
  const watching = await panel.isWatching(device.id);
  res.json({ desired, nextSyncSec: watching ? 2 : 15, serverTime: Date.now() });
});

سمت ESP32

حلقه اصلی دستگاه در فاصله‌های مشخص یک درخواست HTTPS می‌فرستد. برای HTTPS روی ESP32 دو نکته مهم است: گواهی ریشه (Root CA) سرور باید روی دستگاه باشد تا اتصال واقعاً امن باشد (غیرفعال کردن بررسی گواهی، که در نمونه‌کدهای اینترنتی رایج است، یعنی هر کسی در مسیر می‌تواند خودش را جای سرور جا بزند)، و ساعت دستگاه باید با NTP تنظیم شود چون بررسی اعتبار گواهی به تاریخ نیاز دارد.

اگر روزی گواهی سرور از CA دیگری صادر شود، دستگاه‌هایی که فقط یک گواهی ریشه دارند قطع می‌شوند. چند گواهی ریشه رایج را روی دستگاه نگه دارید یا امکان به‌روزرسانی از راه دور (OTA) داشته باشید.

cpp
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

extern const char* ROOT_CA;        // گواهی ریشه سرور
const char* SYNC_URL = "https://example.com/api/device/sync";
unsigned long lastOk = 0, nextSyncMs = 15000;

void syncOnce() {
  WiFiClientSecure tls;
  tls.setCACert(ROOT_CA);           // هرگز setInsecure در محصول واقعی
  HTTPClient http;
  http.begin(tls, SYNC_URL);
  http.addHeader("Authorization", String("Bearer ") + DEVICE_TOKEN);
  http.addHeader("Content-Type", "application/json");

  JsonDocument report;
  report["pump1"] = pump1On;
  report["flow"] = currentFlow;
  String body; serializeJson(report, body);

  if (http.POST(body) == 200) {
    JsonDocument res;
    if (!deserializeJson(res, http.getString())) {
      applyDesired(res["desired"]);           // هماهنگ شدن با وضعیت مطلوب
      nextSyncMs = (res["nextSyncSec"] | 15) * 1000UL;
      lastOk = millis();
    }
  }
  http.end();
}

وقتی ارتباط قطع می‌شود: وضعیت امن

مهم‌ترین سؤال طراحی این است: اگر اینترنت یک ساعت قطع شد، دستگاه چه کند؟ جواب به کاربرد بستگی دارد، اما باید صریح و در کد دستگاه باشد، نه در سرور. برای پمپ دوزینگ، ادامه کار بدون نظارت برای مدت طولانی می‌تواند خطرناک باشد؛ پس اگر از آخرین ارتباط موفق بیش از یک مدت مشخص گذشت، دستگاه به وضعیت امن (خاموش) می‌رود و بعد از برگشت ارتباط دوباره همگام می‌شود.

کنار این، watchdog سخت‌افزاری ESP32 را فعال کنید تا اگر برنامه در جایی گیر کرد (مثلاً یک درخواست شبکه‌ای که هیچ‌وقت تمام نمی‌شود)، دستگاه ری‌استارت شود. و برای هر درخواست شبکه timeout تعیین کنید.

نکات کلیدی

  • مدت مجاز بدون ارتباط، و وضعیت امن پس از آن، در کد دستگاه.
  • watchdog فعال و timeout برای هر درخواست.
  • پنل «آخرین ارتباط» را نشان دهد تا آفلاین بودن دیده شود.

چرا Polling و نه MQTT؟

MQTT پروتکل استاندارد اینترنت اشیاست و برای تعداد زیاد دستگاه یا پیام‌های خیلی پرتکرار انتخاب بهتری است. اما برای چند دستگاه که هر چند ثانیه یک بار همگام می‌شوند، Polling روی HTTPS مزایای عملی دارد: سرور اضافه‌ای (broker) لازم نیست، از همان پورت ۴۴۳ و همان زیرساخت وب استفاده می‌کند که از پشت هر فایروال و شبکه‌ای باز است، و عیب‌یابی‌اش با ابزارهای معمول وب ساده است. اتصال دائمی MQTT در شبکه‌هایی که اتصال‌های طولانی را قطع می‌کنند هم دردسر خودش را دارد.

اگر تأخیر چندثانیه‌ای قابل قبول نیست یا تعداد دستگاه‌ها به صدها رسید، می‌شود به long-polling یا MQTT رفت؛ الگوی وضعیت مطلوب در هر دو حالت همان می‌ماند.

جمع‌بندی

برای کنترل دستگاه‌های پشت NAT، Polling روی HTTPS به همراه الگوی وضعیت مطلوب ساده‌ترین و مقاوم‌ترین راه است: پیام گم‌شده یا تکراری خطری ندارد، از پورت ۴۴۳ همیشه باز استفاده می‌شود و broker جداگانه لازم نیست. HTTPS را واقعی پیاده کنید (گواهی ریشه و NTP)، وضعیت امن هنگام قطعی را در خود دستگاه تعریف کنید و watchdog را فعال نگه دارید. این کار بخشی از زیرساخت و پروژه‌های IoT من است؛ برای سرور و پنل چنین سیستمی می‌توانید از سرور و زیرساخت درخواست بدهید.

زیرساخت و لینوکس
۷ دقیقه مطالعه

SIGKILL بدون timeout: وقتی کرنل رندر ویدیو را کشت

رندر ویدیو گاهی با SIGKILL می‌مرد و killed هم false بود. ردگیری خطا تا OOM killer کرنل و رفع آن با swap و رندر دو مرحله‌ای.

مطالعه مقاله#OOM killer
وب، سئو و فرانت‌اند
۴ دقیقه مطالعه

پردازش دسته‌ای و خودکار تصاویر محصول با sharp: WebP، AVIF و سرعت فروشگاه

خط پردازش خودکار تصاویر محصول با Node.js و sharp: اندازه‌های ثابت، تبدیل به WebP و AVIF، حذف متادیتا، صف با محدودیت هم‌زمانی، نام‌گذاری بر اساس هش و تگ picture برای LCP و CLS بهتر.

مطالعه مقاله#sharp