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

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èmoffset = update_id + 1thì 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ị
- ESP32 DevKit V1 30 chân, cổng Type-C.
- Module relay 4 kênh 5 V kích mức thấp.
- Công tắc cửa từ (reed, loại thường hở khi cửa mở) hoặc một nút nhấn để thử.
- Breadboard, dây cắm, dây USB nạp code.
- Ứng dụng Telegram trên điện thoại.
3. Tạo bot, lấy token và chat id

- Trong Telegram, tìm tài khoản @BotFather (có dấu xác thực), bấm Start.
- 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). - BotFather trả về token dạng
123456789:AA…. Dán vàoBOT_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/revokecho BotFather để đổi token. - 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òngTin tu chat id 812345678 (bo qua): /start. Chép số đó vàoCHAT_IDrồ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

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ả


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ượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Serial in "HTTP 401" | Token sai hoặc đã bị đổi | Chép lại token từ BotFather, giữ nguyên dấu hai chấm |
| "HTTP 400" khi gửi tin | Chat id sai, hoặc người dùng chưa từng nhắn /start cho bot | Nhắn /start cho bot, lấy lại chat id theo bước 4 |
| "HTTP 403" khi gửi tin | Người dùng đã chặn bot | Mở 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 token | Mỗ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ác | Mất Wi-Fi, mạng chặn HTTPS, DNS lỗi | Kiể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ở, đóng | Dây công tắc dài nhặt nhiễu, công tắc lắp lỏng | Dâ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
- ESP32 điều khiển relay an toàn
- Dùng MQTT với ESP32 khi có nhiều thiết bị cần điều khiển
- Nạp chương trình cho ESP32 qua Wi-Fi khi mạch đã lắp ở cửa
- Các bài cùng loạt: điều khiển relay bằng ứng dụng Blynk IoT, ghi dữ liệu lên Google Sheets, đưa ESP32 vào Home Assistant.
Xem thêm