2026年10月29日 星期四

ESP32-TeleGuard (基於 ESP32 與 Telegram 之智慧門禁監控系統)

ESP32-TeleGuard (基於 ESP32 與 Telegram 之智慧門禁監控系統)

1. 網路與 Telegram 遠端互動 (IoT 雙向控制)

  • 自動初始化通知:ESP32 開機並連上 Wi-Fi 後,會透過 sendWelcomeMessage() 主動向您的 Telegram 傳送控制選單清單。

  • 遠端指令控制 (RX):

    • 輸入 /open、/1 或 open:驅動伺服馬達旋轉 90 度進行開門 3 秒。

    • 輸入 /status:即時回傳系統運作狀態(Wi-Fi 連線、門鎖狀態)。

    • 輸入 /start 或 /help:重新取得操作指令說明。

  • 即時事件推播 (TX):當發生刷卡、密碼輸入、工作證號登錄或門鈴按下時,主動發送推播訊息至指定 Telegram 帳號。

2. 多重驗證解鎖機制

  • RFID 刷卡驗證 (MFRC522):

    • 讀取卡片 UID 並比對內建白名單(如 01020304、11223344 等)。

    • 驗證通過:蜂鳴器發出高音短鳴、LCD 顯示 Card Access OK!、觸發馬達開門,並發送 Telegram 成功通知。

    • 驗證失敗:蜂鳴器發出低音警告聲、LCD 顯示 Card Denied!,並推播未授權警報至 Telegram。

  • 4x4 Keypad 鍵盤驗證:

    • 6 位數快速密碼:輸入 888888 即可自動解鎖開門並傳送 Telegram 通知。

    • 8 位數標準密碼:輸入 88888888 驗證開門。

    • 工作證號輸入模式:按下 # 鍵切換為工作證模式,輸入 8 碼工作證號後,會將號碼上傳至 Telegram 紀錄。

    • 清除鍵:按下 * 鍵可隨時清空已輸入的密碼緩衝區。

3. 硬體周邊與自動省電機制

  • Servo 馬達門鎖控制:0 度代表關鎖,90 度代表開鎖,解鎖後保持 3 秒自動歸位關門。

  • HC-SR04 超音波人體感應:

    • 當偵測前方 50 cm 內有人靠近時,自動點亮 LCD 背光與喚醒畫面。

    • 若超過 10 秒無人靠近,自動關閉 LCD 背光以達到省電與延長螢幕壽命效果。

  • Pushbutton 實體門鈴:採用去彈跳機制,按下 GPIO 34 的門鈴按鍵時,發送 🔔 [門鈴通知] 有訪客按下門鈴! 至 Telegram。

  • Buzzer 蜂鳴器聲響回饋:提供操作成功(高音)與操作失敗(低音)的聽覺提示。

4. 非阻塞式 (Non-blocking) 系統設計

  • 程式中摒棄了傳統會卡死主程式執行的 delay(),全面改採用 millis() 時間差進行倒數計時(如馬達關閉倒數、LCD 提示還原)。

  • 搭配降低去彈跳時間設定(keypad.setDebounceTime(10)),確保 Keypad 矩陣鍵盤保持極高的輸入靈敏度,不會因網路傳送或超音波測距而遺漏按鍵。

  • 所有程式動作與 Telegram 訊息收發均同步輸出至 Serial Monitor(波特率 115200),方便即時監控與除錯。






 WOKWI 程式  要修改 

//#define BOTtoken "YOUR_TELEGRAM_BOT_TOKEN"
//#define CHAT_ID "YOUR_TELEGRAM_CHAT_ID"
#define BOTtoken "838345708831:AAEuRr5eX54FRyv1m--jGK7gOCn8MiEsWXqpMT0"  // 請替換為您的 Bot Token
#define CHAT_ID "379652418469"    // 請替換為您的 Chat ID


#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <ESP32Servo.h>

// ==========================
// 1. Wi-Fi & Telegram 設定
// ==========================
const char* ssid = "Wokwi-GUEST";
const char* password = "";

//#define BOTtoken "YOUR_TELEGRAM_BOT_TOKEN"
//#define CHAT_ID "YOUR_TELEGRAM_CHAT_ID"
#define BOTtoken "838345708831:AAEuRr5eX54FRyv1m--jGK7gOCn8MiEsWXqpMT0"  // 請替換為您的 Bot Token
#define CHAT_ID "379652418469"    // 請替換為您的 Chat ID



WiFiClientSecure client;
UniversalTelegramBot bot(BOTtoken, client);

unsigned long lastBotCheckTime = 0;
const unsigned long BOT_MTBS = 1000; // Telegram 每秒輪詢一次

// ==========================
// 2. MFRC522 (SDA=5, SCK=18, MOSI=23, MISO=19, RST=4)
// ==========================
#define SS_PIN  5
#define RST_PIN 4
MFRC522 rfid(SS_PIN, RST_PIN);

const String whitelist[] = {
  "01020304",
  "11223344",
  "55667788",
  "AABBCCDD",
  "04112233445566",
  "C0FFEE99"
};
const int whitelistSize = sizeof(whitelist) / sizeof(whitelist[0]);

// ==========================
// 3. LCD I2C (SDA=21, SCL=22)
// ==========================
LiquidCrystal_I2C lcd(0x27, 16, 2);

// ==========================
// 4. 周邊腳位定義
// ==========================
#define BUZZER_PIN 15
#define BELL_PIN   34   // 門鈴按鈕
#define SERVO_PIN  2    // Servo 馬達控制腳位

Servo doorServo;

// ==========================
// 5. HC-SR04 超音波感測器 (TRIG=16, ECHO=17)
// ==========================
#define TRIG_PIN 16
#define ECHO_PIN 17

// ==========================
// 6. 4x4 Keypad 矩陣鍵盤
// ==========================
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = {26, 25, 33, 32};
byte colPins[COLS] = {13, 12, 14, 27};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// ==========================
// 全域變數與非阻塞 Timer 管理
// ==========================
String inputBuffer = "";
bool isCardMode = true; // true: 標準/密碼模式, false: 工作證模式
unsigned long lastMotionTime = 0;
bool lcdBacklightOn = true;
bool lastBellState = HIGH;

// 非阻塞開鎖控制變數
bool doorIsOpen = false;
unsigned long doorOpenStartTime = 0;
const unsigned long DOOR_OPEN_DURATION = 3000; // 開門保持 3 秒

// 液晶清屏非阻塞控制
unsigned long lcdMessageTime = 0;
bool lcdNeedsReset = false;

// 函式宣告
void triggerDoorOpen();
void updateDoorState();
void playBeep(bool success);
void checkProximity();
void checkDoorbell();
void handleNewMessages(int numNewMessages);
void sendWelcomeMessage();
void sendTelegramMessage(String msg, String parseMode = "");

void setup() {
  Serial.begin(115200);
  delay(100);
  Serial.println("\n==========================================");
  Serial.println("  ESP32 門禁監控系統 啟動中 (Keypad優化版) ");
  Serial.println("==========================================");

  // 硬體腳位初始化
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BELL_PIN, INPUT);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  // 馬達初始化
  doorServo.attach(SERVO_PIN);
  doorServo.write(0);

  // LCD 初始化
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("System Starting");

  // SPI & RFID 初始化
  SPI.begin();
  rfid.PCD_Init();

  // 優化 Keypad 去彈跳與回應速度
  keypad.setDebounceTime(10); // 去彈跳延遲降低至 10ms,提高反應靈敏度
  keypad.setHoldTime(100);

  // Wi-Fi 連線
  WiFi.begin(ssid, password);
  Serial.print("[WiFi] 正在連線至 ");
  Serial.print(ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n[WiFi] 連線成功!IP: " + WiFi.localIP().toString());

  client.setInsecure();

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Access System");
  lcd.setCursor(0, 1);
  lcd.print("Ready...");

  sendWelcomeMessage();
  lastMotionTime = millis();
}

void loop() {
  // 1. 馬達與 LCD 畫面非阻塞狀態更新(防止 delay 卡住鍵盤)
  updateDoorState();

  // 2. Telegram 訊息輪詢處理
  if (millis() - lastBotCheckTime > BOT_MTBS) {
    int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    while (numNewMessages) {
      handleNewMessages(numNewMessages);
      numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    }
    lastBotCheckTime = millis();
  }

  // 3. 超音波感測與 LCD 省電控制
  checkProximity();

  // 4. 門鈴按鈕監測
  checkDoorbell();

  // 5. RFID 刷卡檢查
  if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
    String cardUID = "";
    for (byte i = 0; i < rfid.uid.size; i++) {
      if (rfid.uid.uidByte[i] < 0x10) cardUID += "0";
      cardUID += String(rfid.uid.uidByte[i], HEX);
    }
    cardUID.toUpperCase();
    Serial.println("\n[RFID] 刷卡 UID: " + cardUID);

    bool authorized = false;
    for (int i = 0; i < whitelistSize; i++) {
      if (cardUID == whitelist[i]) {
        authorized = true;
        break;
      }
    }

    if (authorized) {
      Serial.println("[RFID] 驗證成功");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Card Access OK!");
      lcd.setCursor(0, 1);
      lcd.print("UID:" + cardUID.substring(0, 8));
      playBeep(true);
      sendTelegramMessage("✅ [刷卡成功] UID: " + cardUID + " 已開門。");
      triggerDoorOpen();
    } else {
      Serial.println("[RFID] 未授權卡號");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Card Denied!");
      lcd.setCursor(0, 1);
      lcd.print("UID:" + cardUID.substring(0, 8));
      playBeep(false);
      sendTelegramMessage("⚠️ [刷卡失敗] 未授權 UID: " + cardUID);
      lcdNeedsReset = true;
      lcdMessageTime = millis();
    }
    rfid.PICC_HaltA();
    rfid.PCD_StopCrypto1();
    lastMotionTime = millis();
  }

  // 6. Keypad 鍵盤輸入處理 (非阻塞高靈敏掃描)
  char key = keypad.getKey();
  if (key) {
    Serial.print("[KEYPAD] 按下: ");
    Serial.println(key);
   
    lastMotionTime = millis();
    if (!lcdBacklightOn) {
      lcd.backlight();
      lcdBacklightOn = true;
    }

    if (key == '#') {
      inputBuffer = "";
      isCardMode = false;
      Serial.println("[KEYPAD] 切換至工作證模式");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("ID Card Mode:");
      lcd.setCursor(0, 1);
      lcd.print("#");
    } else if (key == '*') {
      inputBuffer = "";
      isCardMode = true;
      Serial.println("[KEYPAD] 輸入已清除");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Input Cleared");
      lcdNeedsReset = true;
      lcdMessageTime = millis();
    } else {
      inputBuffer += key;
      Serial.println("[KEYPAD] 當前輸入: " + inputBuffer);
     
      lcd.setCursor(0, 1);
      if (isCardMode) {
        lcd.print("PIN: ");
        for (int i = 0; i < inputBuffer.length(); i++) lcd.print("*");
      } else {
        lcd.print("#" + inputBuffer);
      }

      // 檢查 6 位密碼 (888888)
      if (isCardMode && inputBuffer == "888888") {
        Serial.println("[KEYPAD] 密碼正確 (888888)");
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("PIN Verified!");
        playBeep(true);
        sendTelegramMessage("🔑 [密碼驗證] 6位數密碼 (888888) 正確,已開門。");
        triggerDoorOpen();
        inputBuffer = "";
        isCardMode = true;
      }
      // 檢查 8 位密碼 / 工作證
      else if (inputBuffer.length() == 8) {
        if (isCardMode && inputBuffer == "88888888") {
          Serial.println("[KEYPAD] 密碼正確 (88888888)");
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("PIN Verified!");
          playBeep(true);
          sendTelegramMessage("🔑 [密碼驗證] 8位數密碼 (88888888) 正確,已開門。");
          triggerDoorOpen();
        } else if (!isCardMode) {
          Serial.println("[KEYPAD] 工作證號碼: #" + inputBuffer);
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Work ID Sent!");
          playBeep(true);
          sendTelegramMessage("🆔 [工作證登錄] 輸入號碼: #" + inputBuffer);
          lcdNeedsReset = true;
          lcdMessageTime = millis();
        } else {
          Serial.println("[KEYPAD] 密碼錯誤: " + inputBuffer);
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Invalid PIN!");
          playBeep(false);
          sendTelegramMessage("❌ [密碼錯誤] 輸入內容: " + inputBuffer);
          lcdNeedsReset = true;
          lcdMessageTime = millis();
        }
        inputBuffer = "";
        isCardMode = true;
      }
    }
  }
}

// ==========================
// 輔助與非阻塞管理函式
// ==========================

// 觸發開門(設定標記與計時器,不阻塞 loop)
void triggerDoorOpen() {
  doorServo.write(90);
  doorIsOpen = true;
  doorOpenStartTime = millis();
  Serial.println("[ACTION] 門鎖已開啟 (Servo 90度)");
}

// 非阻塞檢查馬達與 LCD 自動復位
void updateDoorState() {
  unsigned long now = millis();

  // 馬達倒數 3 秒關閉
  if (doorIsOpen && (now - doorOpenStartTime >= DOOR_OPEN_DURATION)) {
    doorServo.write(0);
    doorIsOpen = false;
    Serial.println("[ACTION] 3秒倒數結束,門鎖自動關閉 (Servo 0度)");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Door Closed");
    lcdNeedsReset = true;
    lcdMessageTime = now;
  }

  // 提示訊息顯示 1.5 秒後自動還原提示畫面
  if (lcdNeedsReset && (now - lcdMessageTime >= 1500)) {
    lcdNeedsReset = false;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Access System");
    lcd.setCursor(0, 1);
    lcd.print("Ready...");
  }
}

void sendTelegramMessage(String msg, String parseMode) {
  Serial.println("[TG SEND] " + msg);
  bot.sendMessage(CHAT_ID, msg, parseMode);
}

void sendWelcomeMessage() {
  String message = "🤖 *ESP32 門禁系統已啟動!*\n\n";
  message += "📋 *指令說明:*\n";
  message += "🔹 `/open` 或 `/1` - 遠端開門 3 秒\n";
  message += "🔹 `/status` - 查詢系統狀態\n";
  message += "🔹 `/help` - 重新發送選單";
  sendTelegramMessage(message, "Markdown");
}

void playBeep(bool success) {
  if (success) {
    tone(BUZZER_PIN, 2000, 150);
  } else {
    tone(BUZZER_PIN, 400, 300);
  }
}

void checkProximity() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH, 20000); // 縮短超音波 timeout 至 20ms 防止拖慢掃描
  float distance = (duration * 0.0343) / 2;

  if (distance > 0 && distance < 50) {
    lastMotionTime = millis();
    if (!lcdBacklightOn) {
      lcd.backlight();
      lcdBacklightOn = true;
    }
  }

  if (lcdBacklightOn && (millis() - lastMotionTime > 10000)) {
    lcd.noBacklight();
    lcdBacklightOn = false;
  }
}

void checkDoorbell() {
  bool currentBellState = digitalRead(BELL_PIN);
  if (lastBellState == HIGH && currentBellState == LOW) {
    Serial.println("\n[HARDWARE] 門鈴按下");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Doorbell Ring!");
    playBeep(true);
    sendTelegramMessage("🔔 [門鈴通知] 有訪客按下門鈴!");
    lastMotionTime = millis();
    lcdNeedsReset = true;
    lcdMessageTime = millis();
  }
  lastBellState = currentBellState;
}

void handleNewMessages(int numNewMessages) {
  for (int i = 0; i < numNewMessages; i++) {
    String text = bot.messages[i].text;
    String from_name = bot.messages[i].from_name;
   
    Serial.println("[TG RECV] " + from_name + ": " + text);

    if (text == "/open" || text == "/1" || text == "open") {
      sendTelegramMessage("🔓 收到 Telegram 指令,正在開門...");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Telegram Open");
      playBeep(true);
      triggerDoorOpen();
    } else if (text == "/status") {
      String statusMsg = "📊 [系統狀態]\n- Wi-Fi: 已連線\n- 門鎖: " + String(doorIsOpen ? "開啟中" : "鎖定中");
      sendTelegramMessage(statusMsg);
    } else if (text == "/start" || text == "/help") {
      sendWelcomeMessage();
    }
  }
}





在 Telegram 中取得 Bot Token 與 Chat ID(對話/群組 ID) 的步驟如下:

一、 取得 Bot Token(機器人金鑰)

Bot Token 是控制機器人的主要密鑰,需透過 Telegram 官方的 @BotFather 建立並取得。

  1. 尋找 BotFather

    • 在 Telegram 搜尋欄輸入 @BotFather。

    • 點擊帶有藍色勾勾(官方驗證標章)的 @BotFather 開啟對話視窗。

  2. 建立新機器人

    • 點擊 Start 或發送 /start。

    • 發送 /newbot 指令。

  3. 設定名稱與 Username

    • 顯示名稱(Name): 設定機器人的顯示名稱(例如:My Notify Bot)。

    • 用戶名稱(Username): 設定機器人的唯一 ID,末尾必須以 bot 或 _bot 結尾(例如:my_notify_123_bot)。

  4. 取得 Token

    • 設定完成後,BotFather 會發送訊息,其中包含一串類似 7123456789:ABCdefGhIJKlmNoPQRstuVWXyz 的文字,這就是你的 HTTP API Token。

    • ⚠️ 注意: Token 相當於機器人的密碼,請妥善保管,勿對外洩露。

二、 取得 Telegram Chat ID(使用者或群組 ID)

Chat ID 用於指定機器人要將訊息發送到哪一個特定對話或群組。

方法 A:使用開源 Bot 快速查詢(最簡單)

  1. 在 Telegram 搜尋 @userinfobot 或 @raw_data_bot。

  2. 發送任意訊息或 /start 給該機器人。

  3. 機器人會回傳你的個人資訊,其中的 Id 數字即為你的 User Chat ID。

方法 B:透過官方 API 網頁查詢

  1. 先開啟 Telegram,搜尋你剛建立的機器人(@你的BotUsername),點擊 Start 並隨便發送一則訊息(例如:hello)。

  2. 開啟瀏覽器,輸入以下網址(將 {YOUR_BOT_TOKEN} 替換為步驟一取得的 Token):

    Plaintext
    https://api.telegram.org/bot{YOUR_BOT_TOKEN}/getUpdates
    
  3. 網頁會回傳 JSON 格式數據,找到 "chat" 物件中的 "id" 欄位:

    • 個人對話 ID: 通常為一串純數字(例如:123456789)。

    • 群組 ID: 通常為負數開頭(例如:-987654321 或 -1001234567890)。


ESP32-TeleGuard (基於 ESP32 與 Telegram 之智慧門禁監控系統)

ESP32-TeleGuard (基於 ESP32 與 Telegram 之智慧門禁監控系統) 1. 網路與 Telegram 遠端互動 (IoT 雙向控制) 自動初始化通知 :ESP32 開機並連上 Wi-Fi 後,會透過 sendWelcomeMessage() 主動向您的...