2025年10月11日 星期六

Telegram 控制 WOKWI(ESP32+LED+DHT22)

Telegram 控制 WOKWI(ESP32+LED+DHT22) 



 將所有與 Telegram 的網路呼叫集中到單一 telegramTask(避免在多個任務同時做 TLS/HTTPS),其餘任務(LED、DHT)透過一個 FreeRTOS queue 向 telegramTask 傳送要發出的訊息或由 telegramTask 處理收到的指令。這樣能顯著降低 crash(backtrace)風險、減少記憶體競爭,並保留雙核心執行。

Commands: /on /off /flash /timer /status

WOKWI程式

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <DHT.h>
#include <DHT_U.h>

// ====================== 配置區 ======================
const char* ssid = "Wokwi-GUEST";
const char* password = "";

// --- Telegram: 填入你的 Bot Token 與授權 Chat ID(string) ---
//#define BOT_TOKEN    "YOUR_BOT_TOKEN_HERE"
//#define AUTH_CHAT_ID "YOUR_CHAT_ID_HERE" // 例如 "123456789"(請放引號)

#define BOT_TOKEN    "802257009856:AAGymymK9_d1HcT5GJWl3mtqHmil5xB64_5Zw"
#define AUTH_CHAT_ID "79152158469"

// DHT 設定
#define DHTPIN 4
#define DHTTYPE DHT22

// LED 引腳
const int ledPin = 2;

// ====================== 全域物件 ======================
WiFiClientSecure clientSecure;
UniversalTelegramBot bot(BOT_TOKEN, clientSecure);
DHT dht(DHTPIN, DHTTYPE);

// LED 狀態
enum LedMode { LED_ON, LED_OFF, LED_FLASH, LED_TIMER };
volatile LedMode currentLedMode = LED_OFF;
volatile unsigned long timerStartTime = 0;
volatile bool ledState = false;

// DHT 資料快存(在 DHT 任務更新,由 telegramTask 讀取 / 回報)
volatile float g_temperature = NAN;
volatile float g_humidity = NAN;

// Queue 用來把要發的文字訊息交給 telegramTask(所有發送集中處理)
struct TgMsg {
  char text[256];
  // 若未指定 chatId,默認發到 AUTH_CHAT_ID
};
static QueueHandle_t tgQueue = NULL;

// 任務 handle(如需在未來終止或檢查)
TaskHandle_t TaskTelegram = NULL;
TaskHandle_t TaskLEDControl = NULL;
TaskHandle_t TaskDHTSensor = NULL;

// ====================== Helper ======================
bool enqueueTgMessage(const char* txt) {
  if (!tgQueue) return false;
  TgMsg m;
  strncpy(m.text, txt, sizeof(m.text) - 1);
  m.text[sizeof(m.text) - 1] = '\0';
  if (xQueueSend(tgQueue, &m, pdMS_TO_TICKS(100)) == pdTRUE) return true;
  return false;
}

void printMemoryStats(const char* tag) {
  Serial.printf("[%s] FreeHeap=%u\n", tag, ESP.getFreeHeap());
}

// ====================== telegramTask: 負責 poll 訊息 & 發送所有訊息(單一 TLS 來源) ======================
void telegramTask(void *pvParameters) {
  (void) pvParameters;
  clientSecure.setInsecure(); // Wokwi / 測試環境可直接使用;實作時可替換為根憑證
  unsigned long lastPoll = 0;
  const unsigned long pollInterval = 2000; // 每 2 秒檢查一次新的 Telegram 訊息

  Serial.println("[TelegramTask] started");
  printMemoryStats("TelegramTask start");

  for (;;) {
    // 1) 處理要發送的訊息(來自其他任務)
    TgMsg msg;
    while (xQueueReceive(tgQueue, &msg, 0) == pdTRUE) {
      // 發送訊息(所有實際的 HTTPS 都在此處)
      Serial.printf("[TelegramTask] sendMessage: %s\n", msg.text);
      bot.sendMessage(String(AUTH_CHAT_ID).c_str(), msg.text, "");
      vTaskDelay(pdMS_TO_TICKS(500)); // 小延遲以避免頻繁呼叫
      printMemoryStats("after sendMessage");
    }

    // 2) 週期性 poll 新訊息
    if (millis() - lastPoll >= pollInterval) {
      int numNew = bot.getUpdates(bot.last_message_received + 1);
      if (numNew > 0) {
        Serial.printf("[TelegramTask] Received %d new messages\n", numNew);
        for (int i = 0; i < numNew; i++) {
          String chat_id = String(bot.messages[i].chat_id);
          String text = bot.messages[i].text;
          Serial.printf("[TelegramTask] msg from %s : %s\n", chat_id.c_str(), text.c_str());

          // 只接受授權 chat id 的指令
          if (chat_id != String(AUTH_CHAT_ID)) {
            bot.sendMessage(chat_id.c_str(), "⚠️ Unauthorized user.", "");
            continue;
          }

          // 處理指令(由 telegramTask 直接操控全域 LED 模式)
          if (text == "/on") {
            currentLedMode = LED_ON;
            digitalWrite(ledPin, HIGH);
            bot.sendMessage(chat_id.c_str(), "💡 LED ON", "");
          } else if (text == "/off") {
            currentLedMode = LED_OFF;
            digitalWrite(ledPin, LOW);
            bot.sendMessage(chat_id.c_str(), "💡 LED OFF", "");
          } else if (text == "/flash") {
            currentLedMode = LED_FLASH;
            bot.sendMessage(chat_id.c_str(), "⚡ LED FLASH mode", "");
          } else if (text == "/timer") {
            currentLedMode = LED_TIMER;
            timerStartTime = millis();
            digitalWrite(ledPin, HIGH);
            bot.sendMessage(chat_id.c_str(), "⏱ LED TIMER 10s started", "");
          } else if (text == "/status") {
            // 回報最新的溫濕度與 LED 狀態
            char buf[256];
            float t = g_temperature;
            float h = g_humidity;
            if (isnan(t) || isnan(h)) {
              snprintf(buf, sizeof(buf), "📡 Status:\nTemperature: N/A\nHumidity: N/A\nLED: %s",
                       (currentLedMode==LED_ON)?"ON":(currentLedMode==LED_OFF)?"OFF":(currentLedMode==LED_FLASH)?"FLASH":"TIMER");
            } else {
              snprintf(buf, sizeof(buf), "📡 Status:\nTemperature: %.1f °C\nHumidity: %.1f %%\nLED: %s",
                       t, h,
                       (currentLedMode==LED_ON)?"ON":(currentLedMode==LED_OFF)?"OFF":(currentLedMode==LED_FLASH)?"FLASH":"TIMER");
            }
            bot.sendMessage(chat_id.c_str(), buf, "");
          } else {
            bot.sendMessage(chat_id.c_str(), "❓ Commands: /on /off /flash /timer /status", "");
          }
          vTaskDelay(pdMS_TO_TICKS(200)); // 小 delay 以避免快速連續回覆
        }
      }
      lastPoll = millis();
    }

    // 稍作休息
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

// ====================== LED task(跑在 Core 1) ======================
void ledControlTask(void *pvParameters) {
  (void) pvParameters;
  Serial.println("[LED Task] started");
  // 印出 stack high water mark(剛啟動時)
  Serial.printf("[LED Task] stack high water mark: %u\n", uxTaskGetStackHighWaterMark(NULL));

  for (;;) {
    switch (currentLedMode) {
      case LED_ON:
        digitalWrite(ledPin, HIGH);
        break;
      case LED_OFF:
        digitalWrite(ledPin, LOW);
        break;
      case LED_FLASH:
        digitalWrite(ledPin, ledState ? HIGH : LOW);
        ledState = !ledState;
        vTaskDelay(pdMS_TO_TICKS(500));
        continue; // 直接 continue 跳到下一輪,避免再 vTaskDelay(10)
      case LED_TIMER:
        if (millis() - timerStartTime >= 10000UL) {
          digitalWrite(ledPin, LOW);
          currentLedMode = LED_OFF;
          // 通知使用者(透過 queue)
          enqueueTgMessage("⏰ LED timer finished. LED OFF.");
        }
        break;
      default:
        digitalWrite(ledPin, LOW);
        break;
    }
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}

// ====================== DHT task(跑在 Core 1) ======================
void dhtSensorTask(void *pvParameters) {
  (void) pvParameters;
  Serial.println("[DHT Task] started");
  Serial.printf("[DHT Task] stack high water mark: %u\n", uxTaskGetStackHighWaterMark(NULL));

  const TickType_t readInterval = pdMS_TO_TICKS(30000); // 每 30 秒讀一次(節流)
  for (;;) {
    float h = dht.readHumidity();
    float t = dht.readTemperature();

    if (!isnan(h) && !isnan(t)) {
      g_humidity = h;
      g_temperature = t;
      Serial.printf("[DHT Task] Temp: %.2f C  Hum: %.2f %%\n", t, h);

      // 主動上報(節流):每 30 秒主動發送一則狀態(如果你想關掉主動上報,註解掉下面列)
      char buf[200];
      snprintf(buf, sizeof(buf), "🌡 Temperature: %.1f °C\n💧 Humidity: %.1f %%", t, h);
      enqueueTgMessage(buf);
    } else {
      Serial.println("[DHT Task] Failed to read from DHT sensor");
    }

    // 印出 free heap 以供 debug(可視情況註解)
    printMemoryStats("DHT Task loop");
    vTaskDelay(readInterval);
  }
}

// ====================== setup & loop ======================
void setup_wifi() {
  Serial.print("Connecting to WiFi ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
    if (millis() - start > 20000) {
      Serial.println("\n[setup_wifi] connect timeout, retrying...");
      start = millis();
    }
  }
  Serial.println("\nWiFi connected");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
}

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  setup_wifi();

  // 初始化 DHT(在 DHT 任務也會使用)
  dht.begin();

  // 建立 queue(可緩衝 8 條訊息)
  tgQueue = xQueueCreate(8, sizeof(TgMsg));
  if (!tgQueue) {
    Serial.println("Failed to create tgQueue");
  }

  // 建立 telegramTask(單一 TLS 呼叫來源),放在 Core 0,給較大 stack
  BaseType_t ok;
  ok = xTaskCreatePinnedToCore(telegramTask, "TelegramTask", 16384, NULL, 1, &TaskTelegram, 0);
  if (ok != pdPASS) Serial.println("Failed to create TelegramTask");

  // 建立 LED task(Core 1) — 較輕量
  ok = xTaskCreatePinnedToCore(ledControlTask, "LEDTask", 4096, NULL, 1, &TaskLEDControl, 1);
  if (ok != pdPASS) Serial.println("Failed to create LEDTask");

  // 建立 DHT task(Core 1)
  ok = xTaskCreatePinnedToCore(dhtSensorTask, "DHTTask", 4096, NULL, 1, &TaskDHTSensor, 1);
  if (ok != pdPASS) Serial.println("Failed to create DHTTask");

  // 啟動時發個訊息(透過 queue,等 telegramTask 啟動後會發)
  enqueueTgMessage("🤖 ESP32 Telegram Bot is online. Commands: /on /off /flash /timer /status");

  // 最後印一次記憶體狀態
  printMemoryStats("setup end");
}

void loop() {
  // 主迴圈不做網路或 DHT 操作,避免阻塞;所有工作由任務處理
  vTaskDelay(pdMS_TO_TICKS(1000));
}







沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...