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?

Mô hình MQTT: ESP32 và ứng dụng trên điện thoại, máy tính cùng kết nối tới broker; số liệu và lệnh đi qua các topic
Mô hình MQTT: mọi bản tin đều đi qua broker (hình vẽ minh họa).
  • 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

Cây topic MQTT của thiết bị esp32-01: trang-thai, ket-noi, relay/lenh, relay/trang-thai và ký tự đại diện # và +
Các topic dùng trong bài và hai ký tự đại diện khi subscribe (hình vẽ minh họa).
  • Đặ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

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

  1. Mở Tools > Manage Libraries.
  2. 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

Đấu nối module relay (bản 5 V) với ESP32 DevKit V1
Module relayESP32 DevKit V1Ghi chú
VCCVIN5 V từ cổng USB.
GNDGNDMass chung.
IN1 (hoặc IN ở module 1 kênh)D18 / GPIO18Tín hiệu điều khiển kênh 1.
Sơ đồ nối ESP32 DevKit V1 với module relay 5 V: IN1 nối D18, VCC nối VIN, GND nối GND
Sơ đồ nối ESP32 DevKit V1 với module relay (hình vẽ minh họa): IN1 – D18, VCC – VIN, GND – GND.

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"
Hình minh họa Serial Monitor khi ESP32 kết nối broker MQTT và nhận hai lệnh ON, OFF ở topic relay/lenh
Kết quả mong đợi trên Serial Monitor khi gửi hai lệnh ON và OFF (hình minh họa theo định dạng in của chương trình).

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àm ketNoiMQTT().
  • 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ài doDai.
  • mqtt.publish(topic, nội dung, true): gửi bản tin; tham số thứ ba là true thì broker giữ lại bản tin.
  • mqtt.loop(): phải được gọi liên tục trong loop() để nhận bản tin và giữ kết nối. Không đặt delay() dài trong loop().
  • mqtt.state(): mã cho biết vì sao kết nối thất bại (bảng dưới).
Một số mã trạng thái của PubSubClient
MãÝ nghĩaHướng kiểm tra
−4Broker không trả lời kịpMạng chậm hoặc broker quá tải.
−2Không mở được kết nối tới brokerSai đị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ộ.
2Broker từ chối tên thiết bịĐổi MQTT_CLIENT_ID.
4Sai tên đăng nhập hoặc mật khẩuKiểm tra tài khoản MQTT.
5Không được phép kết nốiBroker 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ắt allow_anonymous, rồi dùng dạng mqtt.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) trong setup().
  • 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à subscribe nha/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ình listener 1883 và 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ặc loop() bị chặn lâu nên mqtt.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_LOW thành false.
  • 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.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

7 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

5 Lượt xem

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

4 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

4 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

4 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp
Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

5 Lượt xem

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

4 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.

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

4 Lượt xem

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.


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