#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <DHT.h>
#include <Adafruit_Sensor.h> // Required for DHT library
// --- Telegram Bot Configuration ---
//#define BOT_TOKEN "YOUR_TELEGRAM_BOT_TOKEN" // 從 BotFather 取得你的 Bot Token
//#define CHAT_ID "YOUR_CHAT_ID" // 你的 Telegram Chat ID
#define BOT_TOKEN "773289402a54:AAHbrWu95ovb1BKPQy1WsbNSjNxfCG4CrEWU-o"
#define CHAT_ID "7965218469"
// --- Wokwi Wi-Fi Configuration ---
// 針對 Wokwi 模擬器,使用 Wokwi-GUEST 網路
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// --- Hardware Pin Definitions ---
const int ledPin = 2; // LED 接到 ESP32 GPIO 2 (請串接電阻)
#define DHTPIN 4 // DHT22 數據腳位接駁到 ESP32 GPIO 4
#define DHTTYPE DHT22 // DHT 感測器類型 (DHT11, DHT21, DHT22)
// --- Global Variables & FreeRTOS Handles ---
// DHT 感測器實例
DHT dht(DHTPIN, DHTTYPE);
// Telegram Bot 實例
WiFiClientSecure client;
UniversalTelegramBot bot(BOT_TOKEN, client);
// 用於追蹤 Telegram 更新的最後 ID
// 這個變數將取代 bot.lastUpdateID,因為您的庫版本中沒有它
long lastProcessedUpdateId = 0;
// LED 狀態變數
bool ledState = false; // true = ON, false = OFF
long timerStartTime = 0;
long timerDuration = 10000; // 10 秒 (10000 毫秒)
bool timerActive = false;
// FreeRTOS 任務句柄
TaskHandle_t telegramTaskHandle = NULL;
TaskHandle_t deviceControlTaskHandle = NULL;
// 訊息佇列 (Queue) 用於在任務間傳遞命令 (Telegram -> DeviceControl)
QueueHandle_t commandQueue;
// 新增:訊息佇列 (Queue) 用於在任務間傳遞 Telegram 回覆 (DeviceControl -> Telegram)
QueueHandle_t responseQueue;
// 定義命令類型 (從 Telegram 傳給設備)
enum CommandType {
CMD_NONE,
CMD_LED_ON,
CMD_LED_OFF,
CMD_LED_FLASH,
CMD_LED_TIMER,
CMD_GET_TEMP,
CMD_MAX // 用於數組大小或枚舉範圍檢查
};
// 命令結構體
struct Command {
CommandType type;
// 未來可以增加更多參數,例如 timerDuration 或 flashCount
};
// 新增:回覆訊息結構體 (從設備傳給 Telegram)
struct BotResponse {
String message; // 要發送的訊息內容
// String chatId; // 如果需要回覆給不同的人,可以加上 chatId
};
// --- Function Prototypes ---
void connectToWiFi();
void telegramTask(void *pvParameters);
void deviceControlTask(void *pvParameters);
void handleTelegramMessages(int numNewMessages); // 處理從 Telegram 收到的訊息
void setup() {
Serial.begin(115200);
Serial.println("\nStarting ESP32 Dual Core FreeRTOS Telegram Bot...");
// 設定 LED 腳位為輸出,並確保初始為關閉
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
ledState = LOW; // 更新 LED 狀態變數
// 初始化 DHT 感測器
dht.begin();
Serial.println("DHT sensor initialized.");
// 連接 Wi-Fi
connectToWiFi();
// 設定 Telegram 的根憑證,確保安全連線
client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
Serial.println("Telegram CA certificate set.");
// 創建命令佇列 (Telegram -> DeviceControl)
commandQueue = xQueueCreate(5, sizeof(Command));
if (commandQueue == NULL) {
Serial.println("Error creating command queue. Restarting...");
ESP.restart(); // 創建失敗則重啟
}
// 創建回覆佇列 (DeviceControl -> Telegram)
// 佇列大小為 5,每個元素是一個 BotResponse 結構體
// 注意:String 佔用記憶體較多,如果訊息很長,需謹慎考慮佇列大小
responseQueue = xQueueCreate(5, sizeof(BotResponse));
if (responseQueue == NULL) {
Serial.println("Error creating response queue. Restarting...");
ESP.restart(); // 創建失敗則重啟
}
// 創建 Telegram 任務 (在 Core 0 上運行,處理 Wi-Fi 和網路通訊)
xTaskCreatePinnedToCore(
telegramTask, // 任務函數
"TelegramTask", // 任務名稱
8192, // 棧大小 (位元組),視需求調整,Telegram 庫較耗記憶體
NULL, // 傳遞給任務的參數
5, // 任務優先級 (1-24,數字越大優先級越高)
&telegramTaskHandle, // 任務句柄
0 // 綁定到 Core 0
);
Serial.println("Telegram Task created on Core 0.");
// 創建設備控制任務 (在 Core 1 上運行,處理 LED 和 DHT 感測器)
xTaskCreatePinnedToCore(
deviceControlTask, // 任務函數
"DeviceControlTask", // 任務名稱
4096, // 棧大小 (位元組),一般應用較小
NULL, // 傳遞給任務的參數
4, // 任務優先級 (略低於 Telegram 任務)
&deviceControlTaskHandle, // 任務句柄
1 // 綁定到 Core 1
);
Serial.println("Device Control Task created on Core 1.");
}
// 主循環 (loop),在 FreeRTOS 環境下,通常不會在 loop() 中放置太多邏輯
// 它會不斷地執行任務排程器
void loop() {
vTaskDelay(pdMS_TO_TICKS(100)); // 簡單的延遲,讓排程器有機會切換任務
}
// --- Function Definitions ---
void connectToWiFi() {
Serial.print("Connecting to Wi-Fi: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\nWiFi Connected!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
}
// Telegram 任務 - 運行在 Core 0
void telegramTask(void *pvParameters) {
(void) pvParameters; // 避免編譯器警告
unsigned long botLastUpdateTime = 0;
const int botRequestDelay = 1000; // 1 秒檢查一次新訊息
BotResponse resp; // 用於接收回覆訊息
for (;;) { // FreeRTOS 任務通常是無限循環
// 檢查是否有新的 Telegram 訊息
if (millis() > botLastUpdateTime + botRequestDelay) {
int numNewMessages = bot.getUpdates(lastProcessedUpdateId + 1);
while (numNewMessages) {
handleTelegramMessages(numNewMessages); // 處理收到的訊息
// 更新 lastProcessedUpdateId 到最後一條訊息的 ID
if (numNewMessages > 0) {
lastProcessedUpdateId = bot.messages[numNewMessages - 1].update_id;
}
numNewMessages = bot.getUpdates(lastProcessedUpdateId + 1);
}
botLastUpdateTime = millis();
}
// 檢查是否有來自 DeviceControlTask 的回覆訊息需要發送
// 等待 0 毫秒,表示非阻塞式檢查,如果沒有訊息立即返回
if (xQueueReceive(responseQueue, &resp, 0) == pdPASS) {
Serial.print("Telegram Task: Sending response: ");
Serial.println(resp.message);
if (WiFi.status() == WL_CONNECTED) {
bot.sendMessage(CHAT_ID, resp.message);
} else {
Serial.println("Telegram Task: WiFi not connected, cannot send response.");
}
}
vTaskDelay(pdMS_TO_TICKS(50)); // 短暫延遲,讓出 CPU 時間給其他任務
}
}
// 處理 Telegram 訊息的函數
void handleTelegramMessages(int numNewMessages) {
Serial.print("Telegram Task: Received ");
Serial.print(numNewMessages);
Serial.println(" new message(s).");
for (int i = 0; i < numNewMessages; i++) {
String chat_id = String(bot.messages[i].chat_id);
String text = bot.messages[i].text;
Serial.print("Telegram Task: Message from Chat ID: ");
Serial.print(chat_id);
Serial.print(", Text: ");
Serial.println(text);
// --- 安全檢查: 只回應來自你的 CHAT_ID 的訊息 ---
if (chat_id != CHAT_ID) {
bot.sendMessage(chat_id, "Unauthorized user.", "");
Serial.println("Telegram Task: Unauthorized message ignored.");
continue;
}
// --- 命令處理 ---
Command cmd; // 創建一個命令結構體
cmd.type = CMD_NONE; // 預設為無命令
if (text == "/start") {
String welcome = "Hello! I'm your ESP32 Telegram Bot.\n";
welcome += "Control me with these commands:\n";
welcome += "/on - Turn LED ON\n";
welcome += "/off - Turn LED OFF\n";
welcome += "/flash - Flash LED 3 times\n";
welcome += "/timer - Activate 10-second LED timer\n";
welcome += "/temp - Get current temperature and humidity\n";
bot.sendMessage(chat_id, welcome);
Serial.println("Telegram Task: '/start' command executed.");
} else if (text == "/on") {
cmd.type = CMD_LED_ON;
// bot.sendMessage(chat_id, "LED is now ON (command sent to Core 1)."); // 由設備任務回應
Serial.println("Telegram Task: Sending CMD_LED_ON.");
} else if (text == "/off") {
cmd.type = CMD_LED_OFF;
// bot.sendMessage(chat_id, "LED is now OFF (command sent to Core 1)."); // 由設備任務回應
Serial.println("Telegram Task: Sending CMD_LED_OFF.");
} else if (text == "/flash") {
cmd.type = CMD_LED_FLASH;
// bot.sendMessage(chat_id, "Flashing LED (command sent to Core 1)."); // 由設備任務回應
Serial.println("Telegram Task: Sending CMD_LED_FLASH.");
} else if (text == "/timer") {
cmd.type = CMD_LED_TIMER;
// bot.sendMessage(chat_id, "LED timer started (command sent to Core 1)."); // 由設備任務回應
Serial.println("Telegram Task: Sending CMD_LED_TIMER.");
} else if (text == "/temp") {
cmd.type = CMD_GET_TEMP;
// bot.sendMessage(chat_id, "Getting temperature and humidity (request sent to Core 1)."); // 由設備任務回應
Serial.println("Telegram Task: Sending CMD_GET_TEMP.");
} else {
bot.sendMessage(chat_id, "Sorry, I don't understand that command. Try /start for help.");
Serial.println("Telegram Task: Unknown command received.");
}
// 如果是有效的控制命令,則發送到設備控制任務的佇列
if (cmd.type != CMD_NONE) {
// 嘗試將命令發送到佇列,等待最多 100ms
if (xQueueSend(commandQueue, &cmd, pdMS_TO_TICKS(100)) != pdPASS) {
bot.sendMessage(chat_id, "Error: Could not send command to device. Queue full?", "");
Serial.println("Telegram Task: Failed to send command to queue.");
} else {
// 命令成功發送後,可以立即給用戶一個確認回覆
// 這裡可以選擇是否回覆,如果設備任務會發送最終狀態,這裡可以簡化
if (text != "/start") { // /start 命令已經有回覆了
bot.sendMessage(chat_id, "Command received, processing...");
}
}
}
}
}
// 設備控制任務 - 運行在 Core 1
void deviceControlTask(void *pvParameters) {
(void) pvParameters; // 避免編譯器警告
Command receivedCommand;
BotResponse resp; // 用於發送回覆訊息
// 將可能引起 "jump to case label" 錯誤的變數聲明移到 switch 外部
float h, t;
for (;;) { // FreeRTOS 任務通常是無限循環
// 等待從命令佇列接收命令,如果沒有命令則等待 100ms
if (xQueueReceive(commandQueue, &receivedCommand, pdMS_TO_TICKS(100))) {
Serial.print("Device Control Task: Received command: ");
// 根據收到的命令類型執行操作
switch (receivedCommand.type) {
case CMD_LED_ON:
Serial.println("CMD_LED_ON");
digitalWrite(ledPin, HIGH);
ledState = HIGH;
timerActive = false; // 停止任何進行中的計時器
resp.message = "LED is now ON."; // 準備回覆訊息
xQueueSend(responseQueue, &resp, portMAX_DELAY); // 發送給 Telegram 任務
break;
case CMD_LED_OFF:
Serial.println("CMD_LED_OFF");
digitalWrite(ledPin, LOW);
ledState = LOW;
timerActive = false; // 停止任何進行中的計時器
resp.message = "LED is now OFF.";
xQueueSend(responseQueue, &resp, portMAX_DELAY);
break;
case CMD_LED_FLASH:
Serial.println("CMD_LED_FLASH");
timerActive = false; // 停止任何進行中的計時器
resp.message = "Flashing LED..."; // 先發送一個提示
xQueueSend(responseQueue, &resp, portMAX_DELAY);
for (int j = 0; j < 3; j++) {
digitalWrite(ledPin, HIGH);
vTaskDelay(pdMS_TO_TICKS(200)); // 使用 vTaskDelay 代替 delay()
digitalWrite(ledPin, LOW);
vTaskDelay(pdMS_TO_TICKS(200));
}
ledState = LOW; // 閃爍後回到關閉狀態
resp.message = "LED flashed 3 times."; // 閃爍完成後的最終回覆
xQueueSend(responseQueue, &resp, portMAX_DELAY);
break;
case CMD_LED_TIMER:
Serial.println("CMD_LED_TIMER");
digitalWrite(ledPin, HIGH);
ledState = HIGH;
timerStartTime = millis(); // 記錄計時器開始時間
timerActive = true; // 激活計時器
resp.message = "LED timer started! LED will turn off in 10 seconds.";
xQueueSend(responseQueue, &resp, portMAX_DELAY);
break;
case CMD_GET_TEMP:
Serial.println("CMD_GET_TEMP");
h = dht.readHumidity(); // 讀取濕度
t = dht.readTemperature(); // 讀取攝氏溫度
if (isnan(h) || isnan(t)) { // 檢查讀取是否失敗
Serial.println("Device Control Task: Error reading from DHT sensor.");
resp.message = "Error: Could not read data from DHT sensor.";
xQueueSend(responseQueue, &resp, portMAX_DELAY);
} else {
String message = "Temperature: " + String(t, 1) + " °C\n";
message += "Humidity: " + String(h, 1) + " %";
Serial.println("Device Control Task: DHT read successful.");
resp.message = message;
xQueueSend(responseQueue, &resp, portMAX_DELAY);
}
break;
case CMD_NONE: // 不應該收到此命令,但為了完整性保留
default:
Serial.println("Unknown command received in Device Control Task.");
break;
}
}
// 處理計時器邏輯
if (timerActive && ledState == HIGH && (millis() - timerStartTime >= timerDuration)) {
digitalWrite(ledPin, LOW);
ledState = LOW;
timerActive = false;
Serial.println("Device Control Task: LED timer finished, LED OFF.");
// 計時器結束後,發送訊息回 Telegram
resp.message = "LED timer finished. LED is OFF.";
xQueueSend(responseQueue, &resp, portMAX_DELAY);
}
vTaskDelay(pdMS_TO_TICKS(10)); // 短暫延遲,讓出 CPU 時間
}
}
好的,這段程式碼各部分的中文解釋如下:
程式碼各部分說明
Hello! I'm your ESP32 Telegram Bot.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <DHT.h>
// WiFi 設置
const char* ssid = "Wokwi-GUEST";
const char* password = ""; // 如果Wokwi-GUEST沒有密碼,就留空
// Telegram Bot 設置
#define BOT_TOKEN "7738940254:AAHbrWu9ovb1BKPQyWsbNSjNxfCGCrEWU-o" // 請替換成你自己的 Bot Token
#define CHAT_ID "7965218469" // 請替換成你自己的 Chat ID
// LED 控制狀態變數 (只宣告一次)
String ledMode = "off";
unsigned long ledTimerStart = 0;
unsigned long lastFlashTime = 0;
bool ledState = false; // 用於未來可能的常規 LED 狀態追蹤,儘管目前 flashLedState 更常用
bool flashLedState = false; // 用於閃爍模式的 LED 狀態追蹤
WiFiClientSecure secured_client;
UniversalTelegramBot bot(BOT_TOKEN, secured_client);
// DHT22 感測器設置
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// LED 引腳設置
#define ledPin 2
// 任務 Handle (FreeRTOS 相關)
TaskHandle_t TaskCore0;
TaskHandle_t TaskCore1;
// 感測值儲存 (全域變數,供不同任務存取)
float temp = 0.0;
float humi = 0.0;
// 連接 WiFi 函數
void connectWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
}
Serial.println(" connected!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
}
void setup() {
Serial.begin(115200); // 設置序列埠通訊速率
pinMode(ledPin, OUTPUT); // 設定 LED 引腳為輸出模式
dht.begin(); // 啟動 DHT 感測器
connectWiFi(); // 連接 WiFi
secured_client.setInsecure(); // 跳過 SSL 憑證檢查(在 Wokwi 或某些開發環境中是必要的)
// 建立雙核心任務 (使用 FreeRTOS 特性)
// 任務名稱, 堆疊大小, 參數, 優先級, 任務句柄, 核心號碼 (0 或 1)
xTaskCreatePinnedToCore(
DHT_Task, // 任務函數
"DHT22_Task", // 任務名稱
4096, // 堆疊大小 (位元組)
NULL, // 任務參數
1, // 任務優先級 (較低)
&TaskCore0, // 任務句柄
0 // 運行在 Core 0
);
xTaskCreatePinnedToCore(
Telegram_LED_Task, // 任務函數
"Telegram_LED_Task", // 任務名稱
8192, // 堆疊大小 (位元組,Telegram 任務可能需要更多)
NULL, // 任務參數
1, // 任務優先級 (較低)
&TaskCore1, // 任務句柄
1 // 運行在 Core 1
);
}
// 主迴圈 (在 FreeRTOS 環境下通常只做少量工作或留空)
void loop() {
// FreeRTOS 會負責調度任務,所以主迴圈通常只需要一個短暫的延遲,
// 防止它在沒有其他任務時過度消耗 CPU
delay(100);
}
void DHT_Task(void *pvParameters) {
for (;;) { // 無限迴圈,任務持續運行
float h = dht.readHumidity(); // 讀取濕度
float t = dht.readTemperature(); // 讀取溫度
if (!isnan(h) && !isnan(t)) { // 檢查讀取是否成功
humi = h; // 更新濕度全域變數
temp = t; // 更新溫度全域變數
Serial.printf("[DHT22] 溫度: %.2f°C 濕度: %.2f%%\n", temp, humi);
} else {
Serial.println("[DHT22] 讀取失敗");
}
delay(5000); // 每 5 秒讀取一次感測器,可調整
}
}
void Telegram_LED_Task(void *pvParameters) {
for (;;) { // 無限迴圈,任務持續運行
// 檢查是否有新的 Telegram 訊息
int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
// 迴圈處理所有新訊息
while (numNewMessages) {
for (int i = 0; i < numNewMessages; i++) {
String chat_id = bot.messages[i].chat_id; // 獲取聊天 ID
String msg = bot.messages[i].text; // 獲取訊息內容
msg.toLowerCase(); // 將訊息轉為小寫,方便比對
// 判斷指令並執行相應操作
if (msg == "on" || msg == "off" || msg == "flash" || msg == "timer") {
ledMode = msg; // 設定 LED 模式
if (msg == "timer") {
ledTimerStart = millis(); // 如果是 timer 模式,記錄開始時間
}
bot.sendMessage(chat_id, "✅ LED 模式: " + msg, ""); // 回覆使用者
} else if (msg == "status") {
// 如果是 status 指令,回覆溫濕度資訊
String dhtInfo = "🌡️ 溫度: " + String(temp) + "°C\n💧 濕度: " + String(humi) + "%";
bot.sendMessage(chat_id, dhtInfo, "");
} else {
// 如果是未知指令,提供詳細的幫助訊息
String helpMessage = "您好!我是您的智慧裝置控制Bot。\n";
helpMessage += "請輸入以下指令來控制設備或查詢狀態:\n";
helpMessage += "💡 **LED 控制指令:**\n";
helpMessage += " `/on` - 開啟 LED\n";
helpMessage += " `/off` - 關閉 LED\n";
helpMessage += " `/flash` - 讓 LED 閃爍\n";
helpMessage += " `/timer` - 讓 LED 亮 10 秒後自動關閉\n";
helpMessage += "🌡️ **感測器查詢指令:**\n";
helpMessage += " `/status` - 查詢目前的溫度與濕度\n";
helpMessage += "\n請注意,指令不區分大小寫。"; // 告知大小寫不敏感
// Telegram Bot API 支援 MarkdownV2,你可以用 `parse_mode` 參數來格式化訊息
// 但這裡為了簡單,先用普通文字,如果需要,可以自行研究 Telegram 的 Markdown 語法
bot.sendMessage(chat_id, helpMessage, "Markdown"); // 使用 Markdown 模式發送訊息
// 也可以直接使用普通模式,如果不想處理 Markdown
// bot.sendMessage(chat_id, helpMessage, "");
}
}
// 處理完當前批次訊息後,再次檢查是否有更多新訊息
numNewMessages = bot.getUpdates(bot.last_message_received + 1);
}
// 根據 ledMode 變數控制 LED 的實際行為
if (ledMode == "on") {
digitalWrite(ledPin, HIGH); // LED 長亮
Serial.println("開啟 LED\n");
} else if (ledMode == "off") {
digitalWrite(ledPin, LOW); // LED 熄滅
Serial.println("關閉 LED\n");
} else if (ledMode == "flash") {
// 閃爍模式:使用非阻塞方式控制 LED 狀態
Serial.println("LED 閃爍\n");
if (millis() - lastFlashTime >= 500) { // 每 500 毫秒切換一次狀態
lastFlashTime = millis();
flashLedState = !flashLedState; // 反轉 LED 狀態
digitalWrite(ledPin, flashLedState);
}
} else if (ledMode == "timer") {
Serial.println("LED 亮 10 秒後自動關閉\n");
// 計時器模式:LED 亮 10 秒後自動關閉
if (millis() - ledTimerStart < 10000) { // 檢查是否在 10 秒內
digitalWrite(ledPin, HIGH); // LED 亮
} else {
digitalWrite(ledPin, LOW); // 超過 10 秒則熄滅
ledMode = "off"; // 重置模式為 off
}
}
// 控制 Telegram 訊息檢查頻率
// 將延遲縮短為 10ms,以提高反應速度
if (ledMode != "flash") {
delay(10); // 可以根據需要調整這個值,越小反應越快,但可能增加 Wokwi 負載
} else {
// 在 flash 模式下,因為已經有 500ms 的內部計時,這裡不需要額外的大延遲
delay(1); // 讓出 CPU 時間給其他任務
}
}
}
沒有留言:
張貼留言