2025年6月4日 星期三

ESP32 整合 MQTT 與 Telegram 控制 LED 及溫濕度顯示

 ESP32 整合 MQTT 與 Telegram 控制 LED 及溫濕度顯示








這是一個整合了 MQTT 訂閱控制 LEDDHT22 溫濕度數據發布到 MQTT

https://alex9ufoexploer.blogspot.com/2025/06/mqtt-mqttx-clinet.html

並且新增 Telegram 接收 LED 控制指令發送溫濕度數據到 Telegram 的 ESP32 Arduino 程式碼。

由於 Telegram bot 的實作需要額外的函式庫和設定,這裡將使用 UniversalTelegramBot 函式庫,並透過 ESP32 的 多工處理 (FreeRTOS) 來確保 MQTT 和 Telegram 任務能同時且順暢地運行。


ESP32 整合 MQTT 與 Telegram 控制 LED 及溫濕度顯示

這個程式碼將包含兩個主要的任務:

  1. Core 0 任務 (DHT22 & MQTT 發布):負責讀取 DHT22 感測器數據並發布到 MQTT Broker。
  2. Core 1 任務 (MQTT 訂閱 & Telegram 互動):負責處理 MQTT 訂閱接收 LED 控制指令,以及與 Telegram Bot 進行互動(接收控制指令和發送溫濕度)。

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <UniversalTelegramBot.h> // Telegram 函式庫
#include <WiFiClientSecure.h>     // 支援 HTTPS 用於 Telegram
#include <ArduinoJson.h>

// WiFi 設定
const char* ssid = "Wokwi-GUEST";   // "你的WiFi名稱" <<--- 請修改成你的 WiFi 名稱
const char* password = "" ;         // "你的WiFi密碼" <<--- 請修改成你的 WiFi 密碼

// --- MQTT 設定 ---
const char* mqtt_broker = "broker.mqttgo.io";
const int mqtt_port = 1883;
const char* mqtt_client_id = "ESP32_DHT22_LED_Telegram_Client"; // 請為你的裝置設定一個獨特的 ID

// 發布主題
const char* mqtt_publish_topic_temphumi = "alex9ufo/esp32/dht/temphumi";
// 訂閱主題 (LED 控制)
const char* mqtt_subscribe_topic_led_control = "alex9ufo/esp32/led/control";
// 回覆主題 (LED 狀態)
const char* mqtt_publish_topic_led_status = "alex9ufo/esp32/led/status";

// --- Telegram 設定 ---
#define BOT_TOKEN "72713891420254:AAHbrWu9ovb1BKPQyWsbNSjNxfCGCrEWU-o" // <<--- 請修改成你的 Bot Token
// 從 BotFather 取得,格式為 "YOUR_BOT_TOKEN"
// 找到你個人或群組的聊天 ID,可以用 @userinfobot 或 @get_id_bot 取得
// 為了安全,建議將此 ID 寫死,或透過 Bot 發送指令後從 Serial 監控器中取得。
#define CHAT_ID "7926542181469"  // <<--- 請修改成你的 Telegram 聊天 ID (通常是負數代表群組ID)

WiFiClientSecure client_telegram;
UniversalTelegramBot bot(BOT_TOKEN, client_telegram);
unsigned long lastTimeBotChecked; // 上次檢查 Telegram 訊息的時間
const unsigned long BOT_MTBS = 1000; // 檢查 Telegram 訊息的間隔 (毫秒)

// --- DHT22 設定 ---
#define DHTPIN 4          // DHT22 連接到 ESP32 的 GPIO Pin 4
#define DHTTYPE DHT22     // DHT 22 (AM2302)
DHT dht(DHTPIN, DHTTYPE);

// --- LED 設定 ---
const int ledPins[] = {16, 17, 18, 19}; // 假設 LED 連接到 GPIO 16, 17, 18, 19
const int numLeds = sizeof(ledPins) / sizeof(ledPins[0]);
bool ledStatus[numLeds]; // 儲存 LED 的狀態 (on/off)

WiFiClient espClient_mqtt;
PubSubClient client_mqtt(espClient_mqtt);

// --- 全局變數用於跨任務存取 DHT 數據 ---
float currentTemperature = 0.0;
float currentHumidity = 0.0;
SemaphoreHandle_t dhtMutex; // 互斥鎖,用於保護 DHT 數據的讀寫
float lastTemp = NAN;
float lastHum = NAN;
//=================================================================
// --- Function Prototypes ---
void setup_wifi();
void reconnect_mqtt();
void mqtt_callback(char* topic, byte* payload, unsigned int length);
void handleNewMessages(int numNewMessages);
void sendLedStatus(int ledNum, bool status);
void parseAndControlLED(String message); // 新增解析 LED 控制指令的函式

// --- FreeRTOS 任務函式 ---
void dht_mqtt_task(void * parameter);
void telegram_led_task(void * parameter);
//=================================================================
void setup() {
  Serial.begin(115200);
  dht.begin();

  // 初始化 LED 腳位為輸出模式,並關閉所有 LED
  for (int i = 0; i < numLeds; i++) {
    pinMode(ledPins[i], OUTPUT);
    digitalWrite(ledPins[i], LOW);
    ledStatus[i] = false; // 初始狀態為關閉
  }

  setup_wifi();

  // 配置 Telegram HTTPS Client
  // 保持這行,因為它通常能幫助解決 SSL 相關的連線問題,特別是在時間同步不佳時。
  client_telegram.setInsecure();

  client_mqtt.setServer(mqtt_broker, mqtt_port);
  client_mqtt.setCallback(mqtt_callback);

  // 創建互斥鎖
  dhtMutex = xSemaphoreCreateMutex();

  // 創建 FreeRTOS 任務
  xTaskCreatePinnedToCore(
    dht_mqtt_task,            // 任務函式
    "DHTMQTTTask",            // 任務名稱
    4096,                     // 堆疊大小 (位元組)
    NULL,                     // 傳遞給任務的參數
    1,                        // 任務優先級 (0 最低,configMAX_PRIORITIES-1 最高)
    NULL,                     // 任務句柄
    0                         // 運行在 Core 0
  );

  xTaskCreatePinnedToCore(
    telegram_led_task,        // 任務函式
    "TelegramLEDTask",        // 任務名稱
    8192,                     // 堆疊大小 (Telegram 需要更多)
    NULL,                     // 傳遞給任務的參數
    1,                        // 任務優先級
    NULL,                     // 任務句柄
    1                         // 運行在 Core 1
  );

  Serial.println("ESP32 setup complete. Tasks created.");
}
//=================================================================
void loop() {
  // loop() 保持空閒,所有邏輯都在 FreeRTOS 任務中運行
  vTaskDelete(NULL); // 刪除 loop() 任務,讓兩個核心專注於 FreeRTOS 任務
}
//=================================================================
// --- WiFi 連線函式 ---
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}
//=================================================================
// --- MQTT 相關函式 ---
void reconnect_mqtt() {
  while (!client_mqtt.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client_mqtt.connect(mqtt_client_id)) {
      Serial.println("connected");
      // 訂閱 LED 控制主題
      client_mqtt.subscribe(mqtt_subscribe_topic_led_control);
      Serial.print("Subscribed to MQTT topic: ");
      Serial.println(mqtt_subscribe_topic_led_control);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client_mqtt.state());
      Serial.println(" try again in 5 seconds");
      vTaskDelay(pdMS_TO_TICKS(5000));
    }
  }
}
//=================================================================
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("MQTT Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  // 檢查是否是 LED 控制主題
  if (String(topic) == mqtt_subscribe_topic_led_control) {
    parseAndControlLED(message);
  }
}
//=================================================================
// --- LED 狀態發送函式 ---
void sendLedStatus(int ledNum, bool status) {
  String statusPayload = "";
  for (int i = 0; i < numLeds; i++) {
    if (i > 0) {
      statusPayload += ",";
    }
    statusPayload += String(i + 1);
    statusPayload += (ledStatus[i] ? "on" : "off");
  }

  if (client_mqtt.connected()) {
    client_mqtt.publish(mqtt_publish_topic_led_status, statusPayload.c_str());
    Serial.print("Published MQTT LED status: ");
    Serial.println(statusPayload);
  }
}
//=================================================================
// --- LED 控制指令解析函式 (通用於 MQTT 和 Telegram) ---
void parseAndControlLED(String message) {
  int ledIndex = -1; // LED 陣列的索引 (0-3)
  bool turnOn = false;

  // 將收到的訊息與預期的 payload 進行比較
  if ((message == "1on") || (message == "/1on")) {
    ledIndex = 0;
    turnOn = true;
  } else if ((message == "1off") || (message == "/1off")) {
    ledIndex = 0;
    turnOn = false;
  } else if ((message == "2on") || (message == "/2on")) {
    ledIndex = 1;
    turnOn = true;
  } else if ((message == "2off") || (message == "/2off")) {
    ledIndex = 1;
    turnOn = false;
  } else if ((message == "3on") || (message =="/3on")) {
    ledIndex = 2;
    turnOn = true;
  } else if ((message == "3off") || (message =="/3off")) {
    ledIndex = 2;
    turnOn = false;
  } else if ((message == "4on") || (message =="/4on")) {
    ledIndex = 3;
    turnOn = true;
  } else if ((message == "4off") || (message =="/4off")) {
    ledIndex = 3;
    turnOn = false;
  }

  if (ledIndex != -1) {
    if (turnOn) {
      digitalWrite(ledPins[ledIndex], HIGH);
      ledStatus[ledIndex] = true;
      Serial.print("LED ");
      Serial.print(ledIndex + 1); // 顯示第幾個 LED (1-4)
      Serial.println(" ON");
    } else {
      digitalWrite(ledPins[ledIndex], LOW);
      ledStatus[ledIndex] = false;
      Serial.print("LED ");
      Serial.print(ledIndex + 1); // 顯示第幾個 LED (1-4)
      Serial.println(" OFF");
    }
    // 回覆 LED 狀態 (透過 MQTT)
    sendLedStatus(ledIndex + 1, ledStatus[ledIndex]);
  } else {
    Serial.println("Invalid LED control payload.");
  }
}
//=================================================================
// --- Telegram 訊息處理函式 ---
void handleNewMessages(int numNewMessages) {
  Serial.print("Handling ");
  Serial.print(numNewMessages);
  Serial.println(" new messages");

  for (int i = 0; i < numNewMessages; i++) {
    String chat_id = String(bot.messages[i].chat_id);
    String text = bot.messages[i].text;

    Serial.print("Received Telegram message from ID ");
    Serial.print(chat_id);
    Serial.print(": ");
    Serial.println(text);

    // 檢查訊息是否來自允許的 CHAT_ID
    if (chat_id == CHAT_ID) {
      if (text == "/start") {
        String welcome = "歡迎使用 ESP32 智慧控制!\n";
        welcome += "你可以使用以下指令控制 LED:\n";
        welcome += "  /1on, /1off\n";
        welcome += "  /2on, /2off\n";
        welcome += "  /3on, /3off\n";
        welcome += "  /4on, /4off\n";
        welcome += "查詢溫濕度:\n";
        welcome += "  /temphumi\n";
        bot.sendMessage(CHAT_ID, welcome);
      } else if (text == "/temphumi") {
        String tempHumiMessage = "";
        // 獲取受保護的 DHT 數據
        if (xSemaphoreTake(dhtMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 嘗試獲取互斥鎖,避免長時間阻塞
          tempHumiMessage = "當前溫度: " + String(currentTemperature, 1) + "°C\n";
          tempHumiMessage += "當前濕度: " + String(currentHumidity, 1) + "%";
          xSemaphoreGive(dhtMutex);
        } else {
          tempHumiMessage = "無法讀取溫濕度數據,請稍後再試。";
        }
        bot.sendMessage(CHAT_ID, tempHumiMessage);
      } else {
        // 嘗試解析 LED 控制指令
        parseAndControlLED(text);
        // 如果是 LED 控制指令,回覆狀態
        if (text.endsWith("on") || text.endsWith("off")) {
          String currentLedStates = "LED 狀態更新:\n";
          for(int j=0; j<numLeds; j++){
            currentLedStates += String(j+1);
            currentLedStates += (ledStatus[j] ? "on" : "off");
            if(j < numLeds -1){
              currentLedStates += ", ";
            }
          }
          bot.sendMessage(CHAT_ID, currentLedStates);
        } else {
          bot.sendMessage(CHAT_ID, "無法識別的指令:" + text);
        }
      }
    } else {
      bot.sendMessage(chat_id, "抱歉,您無權控制此裝置。");
      Serial.print("Unauthorized access from chat ID: ");
      Serial.println(chat_id);
    }
  }
}

//=================================================================
// --- FreeRTOS 任務實作 ---

// DHT22 讀取與 MQTT 發布任務 (運行在 Core 0)
void dht_mqtt_task(void * parameter) {
  while (true) {
    if (WiFi.status() == WL_CONNECTED) {
      if (!client_mqtt.connected()) {
        reconnect_mqtt();
      }
      client_mqtt.loop(); // 保持 MQTT 連線活躍

      // 讀取濕度
      float h = dht.readHumidity();
      // 讀取溫度 (攝氏)
      float t = dht.readTemperature();

      // 檢查讀取是否失敗,如果是則跳過
      if (isnan(h) || isnan(t)) {
        Serial.println("Failed to read from DHT sensor!");
      } else {
        // 保護共享變數
        if (xSemaphoreTake(dhtMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 嘗試獲取互斥鎖,避免長時間阻塞
          currentTemperature = t;
          currentHumidity = h;
          xSemaphoreGive(dhtMutex); // 釋放互斥鎖
        }

          if (t != lastTemp || h != lastHum) {
            lastTemp = t;
            lastHum = h;

            // 格式化溫濕度字串
            String temperatureString = String(t, 1); // 顯示一位小數
            String humidityString = String(h, 1);   // 顯示一位小數
            String mqttPayload = "{\"temperature\": " + temperatureString + ", \"humidity\": " + humidityString + "}";

            // 發布到 MQTT 主題
            if (client_mqtt.connected()) {
              client_mqtt.publish(mqtt_publish_topic_temphumi, mqttPayload.c_str());
              Serial.print("Published MQTT temphumi: ");
              Serial.println(mqttPayload);
            }
          }
      }
    }
    vTaskDelay(pdMS_TO_TICKS(5000)); // 每 5 秒讀取並發布一次
  }
}
//=================================================================
// Telegram 互動與 LED 控制任務 (運行在 Core 1)
void telegram_led_task(void * parameter) {
  while (true) {
    if (WiFi.status() == WL_CONNECTED) {
      // 檢查 Telegram 訊息
      if (millis() > lastTimeBotChecked + BOT_MTBS) {
        int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
        while(numNewMessages) {
          Serial.println("got response");
          handleNewMessages(numNewMessages);
          numNewMessages = bot.getUpdates(bot.last_message_received + 1);
        }
        lastTimeBotChecked = millis();
      }
    }
    vTaskDelay(pdMS_TO_TICKS(50)); // 短暫延遲,避免佔用所有 CPU 資源
  }
}
//=================================================================

沒有留言:

張貼留言

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...