ESP32 gửi cảnh báo Telegram, nhận lệnh bật tắt relay: tạo bot, lấy chat id, không cần thư viện bot

Tạo bot Telegram, lấy token và chat id, cho ESP32 gửi cảnh báo cửa mở, báo có điện lại và nhận lệnh /bat, /tat relay bằng HTTPClient và ArduinoJson.

Telegram cho phép tạo "bot" miễn phí, và bot có một giao diện lập trình gọi qua HTTPS. ESP32 chỉ cần gọi đúng địa chỉ là gửi được tin nhắn về điện thoại: báo cửa vừa mở, báo vừa có điện lại sau khi mất điện, báo nhiệt độ vượt ngưỡng. Theo chiều ngược lại, ESP32 đọc tin nhắn người dùng gửi cho bot để bật tắt relay. Bài này làm cả hai việc bằng thư viện HTTPClient có sẵn trong gói esp32 và ArduinoJson, không cần thư viện bot riêng, kèm cách lấy token, chat id và chỉ cho đúng người được điều khiển.

1. ESP32 nói chuyện với Telegram thế nào

Sơ đồ hoạt động: ESP32 gửi tin lên máy chủ Telegram bằng sendMessage, hỏi tin mới bằng getUpdates mỗi 3 giây; điện thoại nhận tin cảnh báo và gửi lệnh /bat1
ESP32 tự gọi lên máy chủ Telegram, không cần mở cổng modem (hình minh họa).

Mọi lệnh gọi có dạng https://api.telegram.org/bot<TOKEN>/<tên phương thức>. Chương trình dùng hai phương thức:

  • sendMessage: gửi tin. ESP32 gửi gói JSON gồm chat_id (gửi tới ai) và text (nội dung).
  • getUpdates: lấy các tin người dùng đã gửi cho bot. Tin nằm chờ trên máy chủ cho tới khi được đọc. Mỗi tin có số update_id; lần hỏi sau gửi kèm offset = update_id + 1 thì máy chủ coi các tin cũ là đã đọc và không gửi lại.

Cách "hỏi định kỳ" này hợp với ESP32 ở sau modem gia đình. Cách còn lại của Telegram là webhook (máy chủ Telegram gọi tới thiết bị) đòi thiết bị có địa chỉ HTTPS công khai, không hợp với mạch đặt trong nhà.

2. Chuẩn bị

3. Tạo bot, lấy token và chat id

Bốn bước tạo bot Telegram: tìm BotFather, gửi /newbot, chép token, lấy chat id; phản hồi getUpdates có update_id và chat id
Bốn bước tạo bot và dữ liệu getUpdates trả về khi có người nhắn /start (số liệu ví dụ).
  1. Trong Telegram, tìm tài khoản @BotFather (có dấu xác thực), bấm Start.
  2. Gửi /newbot, đặt tên hiển thị, rồi tên đăng nhập của bot (phải kết thúc bằng "bot", ví dụ nha_minh_esp32_bot).
  3. BotFather trả về token dạng 123456789:AA…. Dán vào BOT_TOKEN. Ai có token đều điều khiển được bot: không đăng chương trình có token thật lên mạng. Lỡ lộ thì gửi /revoke cho BotFather để đổi token.
  4. Nạp chương trình với CHAT_ID = 0, mở Serial Monitor, mở bot vừa tạo trên điện thoại và nhắn /start. Serial in dòng Tin tu chat id 812345678 (bo qua): /start. Chép số đó vào CHAT_ID rồi nạp lại.

Muốn cả nhà cùng nhận cảnh báo: tạo một nhóm, thêm bot vào nhóm, nhắn /start trong nhóm. Chat id của nhóm là số âm (ví dụ -1001234567890); hằng CHAT_ID kiểu long long nên giữ được số này.

4. Sơ đồ nối dây

Sơ đồ nối ESP32 DevKit V1 với module relay 4 kênh ở GPIO32, 33, 26, 27 và công tắc cửa nối GPIO4 xuống GND
Sơ đồ nối ESP32, relay 4 kênh và công tắc cửa (hình minh họa).

Relay nối như bài module relay 1, 2, 4 kênh: IN1–IN4 ở GPIO32, 33, 26, 27, VCC vào VIN, GND chung. Công tắc cửa nối một đầu vào GPIO4, đầu kia vào GND. Chương trình bật điện trở kéo lên bên trong (INPUT_PULLUP): cửa đóng thì công tắc nối GPIO4 xuống GND (mức LOW), cửa mở thì GPIO4 lên mức HIGH.

An toàn điện: phía tiếp điểm relay đóng cắt điện 220 V. Thử chương trình khi relay chưa nối tải; lắp tải thật thì ngắt điện, đặt mạch trong hộp kín.

5. Thư viện

HTTPClient và WiFiClientSecure có sẵn trong gói esp32. Thư viện đọc JSON cài trong Arduino IDE 2: Sketch > Include Library > Manage Libraries…, tìm "ArduinoJson", chọn thư viện của Benoit Blanchon, bản 7.x. Bài dùng gói esp32 phiên bản 3.3.10 và ArduinoJson 7.4.3.

6. Chương trình

Điền tên, mật khẩu Wi-Fi 2,4 GHz và token, để CHAT_ID = 0 ở lần nạp đầu, chọn bo ESP32 Dev Module rồi nạp.

// PYWORLD.VN - ESP32 gửi cảnh báo qua Telegram và nhận lệnh bật tắt relay 4 kênh
// Bo mạch: ESP32 DevKit V1, module relay kích mức thấp ở GPIO32, 33, 26, 27, công tắc cửa (hoặc nút nhấn) ở GPIO4
// Gọi thẳng Telegram Bot API bằng HTTPClient của gói esp32, đọc JSON bằng ArduinoJson 7

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

const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const char *BOT_TOKEN = "123456789:MA_TOKEN_CUA_BOT";  // Lấy từ BotFather
const long long CHAT_ID = 0;  // Để 0 lúc đầu: Serial sẽ in chat id của người nhắn, chép vào đây rồi nạp lại

const uint8_t RELAY_PINS[4] = {32, 33, 26, 27};  // IN1, IN2, IN3, IN4
const bool RELAY_ACTIVE_LOW = true;
const uint8_t CUA = 4;  // Công tắc cửa nối GPIO4 xuống GND: đóng cửa = LOW

const unsigned long CHU_KY_HOI = 3000;  // Hỏi tin nhắn mới mỗi 3 giây

WiFiClientSecure tls;
bool relayBat[4] = {false, false, false, false};
long long updateTiep = 0;  // offset của getUpdates: tin đã đọc thì không nhận lại
unsigned long lanHoi = 0;
unsigned long lanBao = 0;
bool daBaoKhoiDong = false;
bool cuaDong = true;

void datRelay(uint8_t kenh, bool bat) {
  relayBat[kenh] = bat;
  bool muc = RELAY_ACTIVE_LOW ? !bat : bat;
  digitalWrite(RELAY_PINS[kenh], muc ? HIGH : LOW);
  Serial.printf("Relay K%u: %s\n", (unsigned)(kenh + 1), bat ? "BAT" : "TAT");
}

String lyDoKhoiDong() {
  switch (esp_reset_reason()) {
    case ESP_RST_POWERON: return "vừa có điện lại (hoặc bấm nút EN)";
    case ESP_RST_BROWNOUT: return "sụt áp nguồn";
    case ESP_RST_SW: return "chương trình tự khởi động lại";
    case ESP_RST_PANIC: return "lỗi chương trình";
    case ESP_RST_INT_WDT:
    case ESP_RST_TASK_WDT:
    case ESP_RST_WDT: return "watchdog (chương trình bị treo)";
    default: return "lý do khác";
  }
}

// Gửi một tin nhắn tới CHAT_ID bằng phương thức sendMessage (POST JSON)
bool guiTin(const String &noiDung) {
  if (WiFi.status() != WL_CONNECTED || CHAT_ID == 0) return false;
  HTTPClient http;
  http.begin(tls, String("https://api.telegram.org/bot") + BOT_TOKEN + "/sendMessage");
  http.addHeader("Content-Type", "application/json");
  JsonDocument doc;
  doc["chat_id"] = CHAT_ID;
  doc["text"] = noiDung;
  String than;
  serializeJson(doc, than);
  int ma = http.POST(than);
  http.end();
  Serial.printf("Gui tin Telegram: HTTP %d\n", ma);
  return ma == 200;
}

String trangThai() {
  String s = "Trạng thái relay:";
  for (uint8_t i = 0; i < 4; i++) {
    s += "\nK" + String(i + 1) + ": " + (relayBat[i] ? "BẬT" : "tắt");
  }
  s += "\nCửa: " + String(cuaDong ? "đóng" : "MỞ");
  s += "\nWi-Fi: " + String(WiFi.RSSI()) + " dBm, đã chạy " + String(millis() / 60000) + " phút";
  return s;
}

void xuLyLenh(String lenh) {
  lenh.trim();
  lenh.toLowerCase();
  Serial.printf("Lenh: %s\n", lenh.c_str());
  if ((lenh.startsWith("/bat") || lenh.startsWith("/tat")) && lenh.length() >= 5) {
    int kenh = lenh.charAt(4) - '1';
    if (kenh >= 0 && kenh < 4) {
      datRelay(kenh, lenh.startsWith("/bat"));
      guiTin("Đã " + String(relayBat[kenh] ? "bật" : "tắt") + " K" + String(kenh + 1) + ".");
      return;
    }
  }
  if (lenh == "/trangthai") {
    guiTin(trangThai());
    return;
  }
  guiTin("Lệnh dùng được:\n/bat1 … /bat4: bật relay\n/tat1 … /tat4: tắt relay\n/trangthai: xem trạng thái");
}

// Hỏi tin nhắn mới bằng getUpdates, chỉ làm theo tin từ đúng CHAT_ID
void docTinMoi() {
  HTTPClient http;
  String url = String("https://api.telegram.org/bot") + BOT_TOKEN + "/getUpdates?timeout=0&limit=5&offset=";
  char so[24];
  snprintf(so, sizeof(so), "%lld", updateTiep);
  url += so;
  http.begin(tls, url);
  int ma = http.GET();
  if (ma != 200) {
    Serial.printf("getUpdates loi: %d %s\n", ma, HTTPClient::errorToString(ma).c_str());
    http.end();
    return;
  }
  String than = http.getString();
  http.end();
  JsonDocument doc;
  if (deserializeJson(doc, than)) {
    Serial.println("Khong doc duoc JSON");
    return;
  }
  for (JsonObject u : doc["result"].as<JsonArray>()) {
    updateTiep = u["update_id"].as<long long>() + 1;  // Lần hỏi sau Telegram coi tin này là đã đọc
    long long chat = u["message"]["chat"]["id"].as<long long>();
    const char *chu = u["message"]["text"] | "";
    if (CHAT_ID == 0 || chat != CHAT_ID) {
      Serial.printf("Tin tu chat id %lld (bo qua): %s\n", chat, chu);
      continue;
    }
    xuLyLenh(String(chu));
  }
}

// Công tắc cửa: báo khi đổi trạng thái, chống dội 50 ms
void kiemTraCua() {
  static bool docTruoc = HIGH;
  static unsigned long lucDoi = 0;
  bool doc = digitalRead(CUA);
  if (doc != docTruoc) {
    docTruoc = doc;
    lucDoi = millis();
  }
  bool dong = (doc == LOW);
  if (millis() - lucDoi > 50 && dong != cuaDong) {
    cuaDong = dong;
    Serial.printf("Cua: %s\n", cuaDong ? "DONG" : "MO");
    guiTin(cuaDong ? "Cửa đã đóng." : "CẢNH BÁO: cửa vừa mở!");
  }
}

void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < 4; i++) {
    pinMode(RELAY_PINS[i], INPUT);
    digitalWrite(RELAY_PINS[i], RELAY_ACTIVE_LOW ? HIGH : LOW);
    pinMode(RELAY_PINS[i], OUTPUT);
  }
  pinMode(CUA, INPUT_PULLUP);
  cuaDong = (digitalRead(CUA) == LOW);

  tls.setInsecure();  // Có mã hóa nhưng không kiểm tra chứng chỉ máy chủ (xem giải thích trong bài)
  WiFi.mode(WIFI_STA);
  WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
  Serial.println("Dang ket noi Wi-Fi...");
}

void loop() {
  kiemTraCua();
  if (WiFi.status() != WL_CONNECTED) return;
  if (!daBaoKhoiDong && (lanBao == 0 || millis() - lanBao > 10000)) {  // Gửi không được thì 10 giây sau thử lại
    lanBao = millis();
    Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
    if (CHAT_ID == 0) Serial.println("Chua co CHAT_ID: nhan /start cho bot roi xem chat id in ra o day");
    daBaoKhoiDong = guiTin("ESP32 đã chạy lại: " + lyDoKhoiDong() + ".\n" + trangThai()) || CHAT_ID == 0;
  }
  if (millis() - lanHoi >= CHU_KY_HOI) {
    lanHoi = millis();
    docTinMoi();
  }
}

7. Giải thích các phần chính

Gửi tin: guiTin()

Nội dung tin được đóng vào gói JSON bằng ArduinoJson (serializeJson) rồi gửi bằng http.POST(). Để thư viện tạo JSON thay vì ghép chuỗi bằng tay thì dấu ngoặc kép, xuống dòng và chữ có dấu trong tin được mã hóa đúng. Máy chủ trả mã 200 là gửi được.

Đọc lệnh: docTinMoi()

Mỗi 3 giây, ESP32 gọi getUpdates?timeout=0&limit=5&offset=…. Với từng tin trả về, chương trình lưu updateTiep = update_id + 1, kiểm tra chat id và chỉ làm theo tin từ đúng CHAT_ID. Người lạ tìm thấy bot và nhắn lệnh sẽ bị bỏ qua.

Telegram còn cho "hỏi dài" (timeout lớn hơn 0): máy chủ giữ lệnh gọi tới khi có tin mới. Cách này phản hồi nhanh hơn nhưng làm vòng lặp đứng chờ tới hàng chục giây, nút nhấn và công tắc cửa không được đọc trong lúc đó; với chương trình một vòng lặp, hỏi mỗi 3 giây là vừa.

Mã hóa và setInsecure()

Kết nối tới api.telegram.org là HTTPS, nội dung và token được mã hóa. Lệnh tls.setInsecure() bỏ qua bước kiểm tra chứng chỉ của máy chủ, cho chương trình gọn và không phải cập nhật khi Telegram đổi chứng chỉ. Đổi lại, ở mạng Wi-Fi không tin cậy, kẻ xấu có thể giả làm máy chủ để đọc token. Thiết bị dùng lâu dài nên nạp chứng chỉ gốc của máy chủ bằng tls.setCACert().

Báo lý do khởi động

esp_reset_reason() cho biết vì sao ESP32 vừa chạy lại. Với ESP32 DevKit, cả bật nguồn lẫn bấm nút EN đều cho cùng mã ESP_RST_POWERON, nên tin báo ghi "vừa có điện lại (hoặc bấm nút EN)". Mã sụt áp (ESP_RST_BROWNOUT) hay watchdog giúp phát hiện nguồn yếu hoặc chương trình bị treo khi mạch đặt ở xa.

Không gửi dồn dập

Telegram khuyên bot không gửi quá khoảng một tin mỗi giây vào cùng một cuộc trò chuyện. Công tắc cửa được chống dội 50 ms; nếu công tắc rung liên tục (cửa gió đập), nên thêm thời gian chờ giữa hai lần báo.

8. Kết quả

Khung trò chuyện minh họa: tin báo ESP32 đã chạy lại kèm trạng thái relay, lệnh /bat1, tin Đã bật K1, cảnh báo cửa vừa mở, lệnh /trangthai và trạng thái gửi về
Tin nhắn giữa người dùng và bot (hình minh họa).
Serial Monitor: lần nạp đầu in chat id của người nhắn /start; sau khi điền CHAT_ID, gửi tin HTTP 200, nhận lệnh /bat1, relay K1 bật, cửa mở
Serial Monitor ở tốc độ 115200: lấy chat id, rồi chạy bình thường (số liệu ví dụ).

Lệnh gửi từ điện thoại được làm sau 1–4 giây (chu kỳ hỏi 3 giây cộng thời gian mạng). Các lệnh: /bat1…/bat4, /tat1…/tat4, /trangthai; gõ lệnh khác thì bot trả về danh sách lệnh.

9. Mở rộng

  • Báo mất điện thật sự: khi mất điện, ESP32 cũng tắt nên không gửi được gì; tin "vừa có điện lại" chỉ đến sau. Muốn báo ngay lúc mất điện, ESP32 cần pin dự phòng, ví dụ mạch ESP32 tích hợp pin 18650, và đo điện áp nguồn ngoài qua cầu phân áp như bài đo điện áp pin bằng ADC. Modem Wi-Fi cũng phải có nguồn dự phòng.
  • Thêm cảm biến: mỗi ngưỡng (nhiệt độ cao, mực nước, điện áp pin thấp) là một lần gọi guiTin(); nhớ chỉ báo một lần khi vượt ngưỡng, không báo lặp mỗi vòng.
  • Lưu lịch sử: gửi cùng dữ liệu lên bảng tính như bài ghi dữ liệu lên Google Sheets cùng loạt.

10. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Serial in "HTTP 401"Token sai hoặc đã bị đổiChép lại token từ BotFather, giữ nguyên dấu hai chấm
"HTTP 400" khi gửi tinChat id sai, hoặc người dùng chưa từng nhắn /start cho botNhắn /start cho bot, lấy lại chat id theo bước 4
"HTTP 403" khi gửi tinNgười dùng đã chặn botMở bot trên điện thoại, bỏ chặn, nhắn /start
"getUpdates loi: 409"Bot đang đặt webhook, hoặc một chương trình khác (mạch thứ hai, máy tính) cũng gọi getUpdates với cùng tokenMỗi token chỉ cho một nơi đọc tin; tắt chương trình kia hoặc tạo bot khác
"getUpdates loi: -1 connection refused" hoặc số âm khácMất Wi-Fi, mạng chặn HTTPS, DNS lỗiKiểm tra Wi-Fi như bài kết nối Wi-Fi trên ESP32, thử Wi-Fi điện thoại phát
Nhắn lệnh, Serial in "Tin tu chat id … (bo qua)"Tin đến từ chat khác CHAT_ID (máy khác, nhóm khác)Điền đúng chat id đang dùng
Liên tục báo cửa mở, đóngDây công tắc dài nhặt nhiễu, công tắc lắp lỏngDây xoắn đôi, thêm tụ 100 nF từ GPIO4 xuống GND, tăng thời gian chống dội

11. Xem thêm


Tin tức liên quan

Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái
Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái

Hướng dẫn Blynk IoT với ESP32 từ đầu: tạo template, datastream V0–V5, thiết bị và token, nối relay 4 kênh, chương trình bật tắt bằng điện thoại kèm nút nhấn tại chỗ.

ESP32 đọc MPU-6050 tính góc nghiêng bằng bộ lọc bù: nền tảng để flycam tự cân bằng
ESP32 đọc MPU-6050 tính góc nghiêng bằng bộ lọc bù: nền tảng để flycam tự cân bằng

Nối MPU-6050 với ESP32, đọc thanh ghi không cần thư viện, tính góc roll, pitch từ gia tốc kế, ghép với con quay bằng bộ lọc bù và kiểm tra dấu trước khi dùng cho flycam.

Hướng dẫn ESP32 điều khiển module relay 4 kênh an toàn
Hướng dẫn ESP32 điều khiển module relay 4 kênh an toàn

Hướng dẫn ESP32 điều khiển relay 4 kênh: chọn GPIO an toàn, cấp nguồn 5 V phù hợp, xác định active LOW và lưu ý khi đóng cắt tải.

Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng
Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng

Hướng dẫn các thao tác Wi-Fi cần dùng nhất trên ESP32 với Arduino IDE: quét mạng, kết nối có thời gian chờ, xem IP và cường độ sóng, tự kết nối lại, đặt IP tĩnh, phát Wi-Fi riêng.

Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi
Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi

Hướng dẫn dùng ESP-NOW để hai mạch ESP32 truyền dữ liệu trực tiếp không cần router: xem địa chỉ MAC, chương trình gửi, chương trình nhận, đọc RSSI và xử lý lỗi thường gặp.

Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: encoder, chỉnh Kp Ki Kd, Serial Plotter
Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: encoder, chỉnh Kp Ki Kd, Serial Plotter

Hướng dẫn điều khiển vòng kín tốc độ động cơ DC bằng PID trên ESP32: đo tốc độ bằng encoder và PCNT, chu kỳ 20 ms, chống tích phân bão hòa, chỉnh Kp Ki Kd và thí nghiệm STEM.

Mạch hạ áp LM2596 và MP1584: cách chọn, chỉnh điện áp và cấp nguồn ESP32
Mạch hạ áp LM2596 và MP1584: cách chọn, chỉnh điện áp và cấp nguồn ESP32

So sánh mạch hạ áp LM2596 và MP1584, cách chỉnh điện áp ra bằng đồng hồ vạn năng trước khi nối tải, cấp nguồn 5 V cho ESP32 DevKit V1 từ nguồn 12 V và xử lý lỗi thường gặp.

ESP32-CAM là gì: sơ đồ chân, cách nạp code, xem video trên trình duyệt, chụp ảnh lưu thẻ nhớ
ESP32-CAM là gì: sơ đồ chân, cách nạp code, xem video trên trình duyệt, chụp ảnh lưu thẻ nhớ

Giới thiệu mạch ESP32-CAM AI Thinker, sơ đồ chân, nạp code bằng đế MB hoặc USB-TTL, chương trình xem video MJPEG trên trình duyệt, chụp ảnh có đèn flash và lưu vào thẻ microSD.


Bình luận
Đã thêm vào giỏ hàng