Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.
MQTT là giao thức nhắn tin gọn nhẹ, rất phổ biến trong các hệ thống IoT và nhà thông minh. Nhờ MQTT, một mạch ESP32 có thể gửi số liệu cho nhiều ứng dụng cùng lúc và nhận lệnh điều khiển gần như tức thời. Bài viết giải thích các khái niệm broker, topic, publish, subscribe, rồi hướng dẫn dùng thư viện PubSubClient trên Arduino IDE để ESP32 gửi dữ liệu và điều khiển relay qua MQTT.
1. MQTT hoạt động thế nào?
- Broker là chương trình máy chủ đứng giữa, ví dụ Mosquitto. Các thiết bị không nối trực tiếp với nhau mà đều nối tới broker.
- Client là mọi thứ kết nối tới broker: mạch ESP32, ứng dụng trên điện thoại, phần mềm trên máy tính.
- Topic là tên của một kênh tin, viết giống đường dẫn, ví dụ
nha/esp32-01/relay/lenh. - Publish là gửi một bản tin vào một topic. Subscribe là đăng ký nhận mọi bản tin của một topic.
So với cách cho ESP32 làm web server, MQTT phù hợp hơn khi có nhiều thiết bị, khi cần nhận dữ liệu liên tục hoặc khi nhiều ứng dụng cùng theo dõi một thiết bị. Đổi lại, hệ thống cần một broker luôn chạy.
2. Đặt tên topic
- Đặt topic theo cấp từ chung đến riêng: nơi đặt, tên thiết bị, rồi đến chức năng. Dùng chữ không dấu, không có khoảng trắng.
- Tách riêng topic lệnh (ứng dụng gửi, ESP32 nhận) và topic trạng thái (ESP32 gửi). Nhờ vậy ứng dụng biết lệnh đã được thực hiện hay chưa.
- Bản tin giữ lại (retained): broker lưu bản tin cuối cùng của topic và gửi ngay cho client vừa subscribe. Trạng thái relay nên gửi kiểu này.
- Lời nhắn cuối (last will): bản tin ESP32 gửi gắm cho broker lúc kết nối. Khi ESP32 mất điện hoặc mất mạng, broker tự publish bản tin đó, nhờ vậy ứng dụng biết thiết bị đã ngoại tuyến.
3. Chuẩn bị linh kiện và broker
- ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Module relay bản 5 V (dùng từ mục 6) và dây cắm test board.
- Mạng Wi-Fi 2,4 GHz.
- Một máy tính hoặc Raspberry Pi trong cùng mạng để chạy broker Mosquitto.
- Máy tính đã cài Arduino IDE 2.x và gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10).
Cài broker: tải và cài Mosquitto từ trang mosquitto.org. Từ phiên bản 2.0, Mosquitto mặc định chỉ nhận kết nối từ chính máy đó. Để ESP32 trong mạng nội bộ kết nối được, thêm hai dòng sau vào file mosquitto.conf rồi khởi động lại Mosquitto:
listener 1883
allow_anonymous true
Cấu hình này cho mọi thiết bị trong mạng kết nối không cần mật khẩu, chỉ nên dùng để học và thử trong mạng nhà riêng. Xem địa chỉ IP của máy chạy broker bằng lệnh ipconfig (Windows) hoặc ip addr (Linux); bài dùng địa chỉ ví dụ 192.168.1.10. Nếu máy có tường lửa, cần cho phép cổng 1883.
4. Cài thư viện PubSubClient
- Mở Tools > Manage Libraries.
- Tìm PubSubClient, chọn thư viện của tác giả Nick O'Leary rồi bấm Install.
Bài dùng PubSubClient phiên bản 2.8.
5. Chương trình 1: ESP32 gửi dữ liệu lên broker
Chương trình đầu tiên chỉ cần mạch ESP32. Cứ 5 giây, ESP32 publish một bản tin dạng JSON gồm thời gian đã chạy và cường độ sóng Wi-Fi. Sửa tên Wi-Fi, mật khẩu và MQTT_HOST cho đúng với mạng của bạn:
/*
PYWORLD.VN - ESP32 gửi dữ liệu lên MQTT broker (publish)
Chỉ cần mạch ESP32 và một MQTT broker trong mạng nội bộ
Thư viện: PubSubClient
*/
#include <WiFi.h>
#include <PubSubClient.h>
const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";
const char *MQTT_HOST = "192.168.1.10"; // Địa chỉ IP của máy chạy broker
const uint16_t MQTT_PORT = 1883;
const char *MQTT_CLIENT_ID = "esp32-01"; // Mỗi thiết bị phải có một tên khác nhau
const char *TOPIC_TRANG_THAI = "nha/esp32-01/trang-thai";
const unsigned long CHU_KY_GUI_MS = 5000;
unsigned long lanGuiTruoc = 0;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
void ketNoiWiFi() {
Serial.printf("Dang ket noi WiFi %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nDa ket noi WiFi, IP: %s\n", WiFi.localIP().toString().c_str());
}
void ketNoiMQTT() {
while (!mqtt.connected()) {
Serial.printf("Dang ket noi MQTT %s:%u ... ", MQTT_HOST, MQTT_PORT);
if (mqtt.connect(MQTT_CLIENT_ID)) {
Serial.println("thanh cong");
} else {
Serial.printf("that bai, ma loi %d. Thu lai sau 5 giay\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
delay(500);
ketNoiWiFi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
}
void loop() {
if (!mqtt.connected()) {
ketNoiMQTT();
}
mqtt.loop(); // Giữ kết nối với broker
// Cứ 5 giây gửi một bản tin dạng JSON: thời gian đã chạy và cường độ sóng Wi-Fi
if (millis() - lanGuiTruoc >= CHU_KY_GUI_MS) {
lanGuiTruoc = millis();
char noiDung[64];
snprintf(noiDung, sizeof(noiDung), "{\"uptime\":%lu,\"rssi\":%d}", millis() / 1000, (int)WiFi.RSSI());
mqtt.publish(TOPIC_TRANG_THAI, noiDung);
Serial.printf("Da gui [%s] %s\n", TOPIC_TRANG_THAI, noiDung);
}
}
Để xem bản tin, mở cửa sổ dòng lệnh trên máy chạy broker và subscribe mọi topic của thiết bị:
mosquitto_sub -h 192.168.1.10 -t "nha/esp32-01/#" -v
Ví dụ kết quả (số minh họa):
nha/esp32-01/trang-thai {"uptime":5,"rssi":-58}
nha/esp32-01/trang-thai {"uptime":10,"rssi":-58}
nha/esp32-01/trang-thai {"uptime":15,"rssi":-57}
6. Sơ đồ nối module relay
| Module relay | ESP32 DevKit V1 | Ghi chú |
|---|---|---|
| VCC | VIN | 5 V từ cổng USB. |
| GND | GND | Mass chung. |
| IN1 (hoặc IN ở module 1 kênh) | D18 / GPIO18 | Tín hiệu điều khiển kênh 1. |
Chưa nối tải vào cọc vít ở bước này: tiếng tách của relay và LED của kênh đủ để kiểm tra. Cách đấu tải, ý nghĩa các cọc COM, NO, NC và lưu ý an toàn điện có trong bài ESP32 điều khiển module relay 4 kênh.
7. Chương trình 2: nhận lệnh MQTT để điều khiển relay
ESP32 subscribe topic lệnh, đóng hoặc nhả relay theo nội dung ON hay OFF, rồi publish lại trạng thái. Module relay trong bài kích ở mức thấp; nếu module của bạn kích mức cao, đổi RELAY_ACTIVE_LOW thành false.
/*
PYWORLD.VN - ESP32 nhận lệnh MQTT để điều khiển relay (subscribe) và báo lại trạng thái (publish)
Module relay bản 5 V: VCC = VIN, GND = GND, IN1 = GPIO18
Thư viện: PubSubClient
*/
#include <WiFi.h>
#include <PubSubClient.h>
const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";
const char *MQTT_HOST = "192.168.1.10"; // Địa chỉ IP của máy chạy broker
const uint16_t MQTT_PORT = 1883;
const char *MQTT_CLIENT_ID = "esp32-01"; // Mỗi thiết bị phải có một tên khác nhau
const char *TOPIC_LENH = "nha/esp32-01/relay/lenh"; // Nhận "ON" hoặc "OFF"
const char *TOPIC_TRANG_THAI = "nha/esp32-01/relay/trang-thai"; // Báo "ON" hoặc "OFF"
const char *TOPIC_KET_NOI = "nha/esp32-01/ket-noi"; // "online" hoặc "offline"
#define RELAY_PIN 18
// true: module kích mức thấp (IN = LOW thì relay đóng)
// false: module kích mức cao (IN = HIGH thì relay đóng)
const bool RELAY_ACTIVE_LOW = true;
bool relayDangBat = false;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
void datRelay(bool bat) {
bool muc = RELAY_ACTIVE_LOW ? !bat : bat;
digitalWrite(RELAY_PIN, muc ? HIGH : LOW);
relayDangBat = bat;
}
void baoTrangThai() {
// Tham số true: broker giữ lại bản tin (retained) để client kết nối sau vẫn biết trạng thái
mqtt.publish(TOPIC_TRANG_THAI, relayDangBat ? "ON" : "OFF", true);
}
// Hàm này được gọi mỗi khi có bản tin đến ở topic đã đăng ký
void nhanBanTin(char *topic, byte *payload, unsigned int doDai) {
String noiDung;
for (unsigned int i = 0; i < doDai; i++) {
noiDung += (char)payload[i];
}
Serial.printf("Nhan [%s] %s\n", topic, noiDung.c_str());
if (strcmp(topic, TOPIC_LENH) != 0) {
return;
}
if (noiDung == "ON") {
datRelay(true);
} else if (noiDung == "OFF") {
datRelay(false);
} else {
Serial.println("Lenh khong hop le, chi nhan ON hoac OFF");
return;
}
baoTrangThai();
}
void ketNoiWiFi() {
Serial.printf("Dang ket noi WiFi %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nDa ket noi WiFi, IP: %s\n", WiFi.localIP().toString().c_str());
}
void ketNoiMQTT() {
while (!mqtt.connected()) {
Serial.printf("Dang ket noi MQTT %s:%u ... ", MQTT_HOST, MQTT_PORT);
// Ba tham số cuối là "lời nhắn cuối": nếu ESP32 mất kết nối, broker tự gửi "offline"
if (mqtt.connect(MQTT_CLIENT_ID, TOPIC_KET_NOI, 0, true, "offline")) {
Serial.println("thanh cong");
mqtt.publish(TOPIC_KET_NOI, "online", true);
mqtt.subscribe(TOPIC_LENH); // Phải đăng ký lại sau mỗi lần kết nối
baoTrangThai();
} else {
Serial.printf("that bai, ma loi %d. Thu lai sau 5 giay\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
// Giữ relay tắt khi khởi động: ghi sẵn mức tắt rồi mới bật ngõ ra
pinMode(RELAY_PIN, INPUT);
datRelay(false);
pinMode(RELAY_PIN, OUTPUT);
Serial.begin(115200);
delay(500);
ketNoiWiFi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(nhanBanTin);
}
void loop() {
if (!mqtt.connected()) {
ketNoiMQTT();
}
mqtt.loop(); // Nhận bản tin đến và giữ kết nối với broker
}
Giữ cửa sổ mosquitto_sub ở mục 5 và mở thêm một cửa sổ dòng lệnh để gửi lệnh:
mosquitto_pub -h 192.168.1.10 -t "nha/esp32-01/relay/lenh" -m "ON"
mosquitto_pub -h 192.168.1.10 -t "nha/esp32-01/relay/lenh" -m "OFF"
Cùng lúc đó, cửa sổ mosquitto_sub hiện các bản tin trạng thái do ESP32 gửi lại:
nha/esp32-01/ket-noi online
nha/esp32-01/relay/trang-thai OFF
nha/esp32-01/relay/lenh ON
nha/esp32-01/relay/trang-thai ON
nha/esp32-01/relay/lenh OFF
nha/esp32-01/relay/trang-thai OFF
Rút nguồn ESP32 và chờ khoảng nửa phút: broker tự gửi nha/esp32-01/ket-noi offline. Thay cho dòng lệnh, có thể dùng một ứng dụng MQTT trên điện thoại: tạo nút bấm publish ON, OFF vào topic lệnh và một ô hiển thị subscribe topic trạng thái.
8. Giải thích các lệnh chính
mqtt.setServer(MQTT_HOST, MQTT_PORT): khai báo địa chỉ và cổng của broker. Cổng mặc định của MQTT không mã hóa là 1883.mqtt.connect(MQTT_CLIENT_ID, TOPIC_KET_NOI, 0, true, "offline"): kết nối tới broker với tên thiết bị và lời nhắn cuối. Hai thiết bị trùng tên sẽ liên tục đẩy nhau ra khỏi broker.mqtt.subscribe(TOPIC_LENH): đăng ký nhận lệnh. Mỗi lần kết nối lại phải đăng ký lại, vì vậy lệnh này nằm trong hàmketNoiMQTT().mqtt.setCallback(nhanBanTin): chỉ định hàm sẽ được gọi khi có bản tin đến. Nội dung bản tin là một mảng byte không có ký tự kết thúc chuỗi, nên chương trình ghép từng byte theo độ dàidoDai.mqtt.publish(topic, nội dung, true): gửi bản tin; tham số thứ ba làtruethì broker giữ lại bản tin.mqtt.loop(): phải được gọi liên tục trongloop()để nhận bản tin và giữ kết nối. Không đặtdelay()dài trongloop().mqtt.state(): mã cho biết vì sao kết nối thất bại (bảng dưới).
| Mã | Ý nghĩa | Hướng kiểm tra |
|---|---|---|
| −4 | Broker không trả lời kịp | Mạng chậm hoặc broker quá tải. |
| −2 | Không mở được kết nối tới broker | Sai địa chỉ IP, broker chưa chạy, tường lửa chặn cổng 1883, hoặc broker chỉ nhận kết nối nội bộ. |
| 2 | Broker từ chối tên thiết bị | Đổi MQTT_CLIENT_ID. |
| 4 | Sai tên đăng nhập hoặc mật khẩu | Kiểm tra tài khoản MQTT. |
| 5 | Không được phép kết nối | Broker yêu cầu đăng nhập nhưng chương trình chưa gửi tài khoản. |
9. Mở rộng
- Đặt mật khẩu cho broker: tạo tài khoản bằng công cụ
mosquitto_passwd, tắtallow_anonymous, rồi dùng dạngmqtt.connect(tên thiết bị, tên đăng nhập, mật khẩu, TOPIC_KET_NOI, 0, true, "offline"). - Bản tin dài: PubSubClient mặc định chỉ xử lý gói tin tối đa 256 byte, tính cả tên topic. Bản tin dài hơn cần gọi
mqtt.setBufferSize(512)trongsetup(). - Nhiều relay: đặt mỗi kênh một cặp topic, ví dụ
nha/esp32-01/relay1/lenh,nha/esp32-01/relay2/lenh, và subscribenha/esp32-01/+/lenh. - Điều khiển từ ngoài Internet: không mở cổng 1883 của mạng nhà ra Internet khi chưa bật mật khẩu và mã hóa TLS. Với người mới, nên bắt đầu trong mạng nội bộ.
- Kết hợp phần mềm khác: Node-RED và Home Assistant đều nói chuyện được với thiết bị qua MQTT bằng chính các topic này.
10. Xử lý lỗi thường gặp
- Serial Monitor lặp lại "that bai, ma loi -2": ESP32 không tới được broker. Kiểm tra
MQTT_HOST, hai dòng cấu hìnhlistener 1883vàallow_anonymous true, tường lửa của máy chạy broker; ESP32 và máy đó phải cùng một mạng. - Kết nối được rồi cứ vài giây lại mất: có hai thiết bị dùng chung
MQTT_CLIENT_ID, hoặcloop()bị chặn lâu nênmqtt.loop()không được gọi. - Gửi lệnh mà ESP32 không nhận: sai tên topic (có phân biệt chữ hoa, chữ thường), hoặc chương trình quên subscribe lại sau khi kết nối lại.
- Gửi ON thì relay nhả, gửi OFF thì relay đóng: module kích mức cao; đổi
RELAY_ACTIVE_LOWthànhfalse. - ESP32 tự khởi động lại khi relay đóng: nguồn 5 V bị sụt áp. Cấp nguồn 5 V riêng cho module relay và nối chung GND với ESP32.
- Bản tin dài không tới nơi: vượt quá 256 byte; tăng bộ đệm bằng
mqtt.setBufferSize(). - Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...
Xem thêm: ESP32 điều khiển module relay 4 kênh, ESP32 làm web server điều khiển relay qua Wi-Fi và mạch hạ áp LM2596 và MP1584 để cấp nguồn cho ESP32.
Xem thêm