Làm gateway LoRa sang MQTT bằng ESP32: node cảm biến chạy pin gửi dữ liệu về broker

Hướng dẫn làm hệ thống LoRa hoàn chỉnh với ESP32 và SX1278: node chạy pin đo điện áp, gửi gói rồi ngủ sâu; gateway nhận gói LoRa và publish dữ liệu JSON lên broker MQTT qua Wi-Fi.

LoRa đưa được số liệu đi xa nhưng không tự lên mạng. Để xem số liệu trên điện thoại hay máy tính, cần một mạch trung gian gọi là gateway: một đầu nhận gói LoRa, đầu kia kết nối Wi-Fi và chuyển tiếp lên broker MQTT. Bài viết hướng dẫn làm trọn một hệ thống nhỏ bằng ESP32 và module SX1278: node chạy pin đo điện áp, gửi qua LoRa rồi ngủ sâu; gateway nhận gói và publish dữ liệu dạng JSON.

Bài dùng lại cách nối module LoRa của bài LoRa cơ bản và kiến thức về MQTT, Deep Sleep ở các bài đã đăng.

1. Kiến trúc hệ thống

Kiến trúc hệ thống LoRa sang MQTT: node LoRa chạy pin gửi gói về gateway ESP32, gateway publish lên broker MQTT cho ứng dụng; kèm định dạng gói, topic và nội dung JSON
Đường đi của một gói từ node đến broker MQTT (hình vẽ minh họa).
  • Node không dùng Wi-Fi. Mỗi 5 phút node thức dậy khoảng một giây để đo và gửi một gói LoRa ngắn.
  • Gateway luôn bật, đặt ở nơi có Wi-Fi và nguồn điện. Gateway nhận gói của mọi node dùng cùng cấu hình LoRa.
  • Broker MQTT phân phối số liệu cho các ứng dụng. Cách cài broker Mosquitto và các khái niệm topic, publish, subscribe có trong bài dùng MQTT với ESP32.

2. Chuẩn bị

  • Hai mạch ESP32 DevKit V1 30 chân và hai module LoRa SX1278 loại Ra-02 kèm anten 433 MHz.
  • Cho node: hai điện trở 100 kΩ, pin 18650 với khay pin 18650 và mạch nguồn phù hợp. Có thể thay mạch ESP32 của node bằng mạch ESP32 tích hợp pin 18650.
  • Breadboard và dây cắm test board.
  • Mạng Wi-Fi 2,4 GHz và một máy tính trong cùng mạng chạy broker Mosquitto.
  • Arduino IDE 2.x, gói esp32 (bài dùng 3.3.10), thư viện LoRa của Sandeep Mistry (0.8.0) và PubSubClient của Nick O'Leary (2.8).

3. Nối dây

Sơ đồ nối node LoRa: ESP32 DevKit V1 với module LoRa SX1278 qua SPI và cầu phân áp hai điện trở 100 kΩ đưa điện áp pin 18650 vào chân D35
Sơ đồ nối node (hình vẽ minh họa). Gateway chỉ cần phần module LoRa.
Linh kiệnChânESP32 DevKit V1
Module LoRa (node và gateway)3.3V3V3
GNDGND
MOSID23
MISOD19
SCKD18
NSSD5
RSTD4
DIO0RX2 (GPIO16)
Cầu phân áp đo pin (chỉ node)R1 100 kΩGiữa cực dương của pin và D35
R2 100 kΩGiữa D35 và GND
Điểm giữa R1, R2D35 (GPIO35)

Hai điện trở bằng nhau chia đôi điện áp pin: pin 4,2 V cho 2,1 V ở chân D35, nằm trong dải đo của ADC. Cách đo và hiệu chỉnh chi tiết có trong bài đọc ADC và đo điện áp pin 18650.

Nguồn cho node: pin 18650 có điện áp từ khoảng 3,0 đến 4,2 V, không được nối thẳng vào chân 3V3 của ESP32. Dùng mạch có sẵn phần quản lý pin, hoặc đưa pin qua mạch ổn áp phù hợp rồi mới cấp cho ESP32.

4. Chương trình cho node

Gói tin của node là một dòng chữ ngắn dạng PW;số node;số thứ tự;điện áp pin. Tiền tố PW giúp gateway bỏ qua gói của thiết bị khác.

/*
  PYWORLD.VN - Node LoRa chạy pin: đo điện áp pin, gửi về gateway rồi ngủ sâu
  LoRa: NSS = D5, SCK = D18, MISO = D19, MOSI = D23, RST = D4, DIO0 = RX2 (GPIO16), nguồn 3V3
  Đo pin: cực dương pin -> R1 100 kΩ -> D35 -> R2 100 kΩ -> GND
  Thư viện: LoRa (Sandeep Mistry)
*/

#include <SPI.h>
#include <LoRa.h>
#include <esp_sleep.h>
#include <driver/gpio.h>

#define LORA_NSS  5
#define LORA_RST  4
#define LORA_DIO0 16
#define CHAN_DO_PIN 35

#define ID_NODE 1                       // Mỗi node một số khác nhau
#define R1_OHM 100000.0                 // Điện trở nối lên cực dương của pin
#define R2_OHM 100000.0                 // Điện trở nối xuống GND

const long TAN_SO_HZ = 433E6;
const int  TU_DONG_BO = 0xF3;
const int  HE_SO_TRAI_PHO = 9;          // Phải giống gateway
const uint64_t THOI_GIAN_NGU_GIAY = 300;

// Biến đặt trong bộ nhớ RTC giữ được giá trị qua các lần ngủ sâu
RTC_DATA_ATTR uint32_t soThuTu = 0;

uint32_t doPinMv() {
  uint32_t tong = 0;
  for (int i = 0; i < 16; i++) {
    tong += analogReadMilliVolts(CHAN_DO_PIN);
  }
  float taiChan = tong / 16.0;
  return (uint32_t)(taiChan * (R1_OHM + R2_OHM) / R2_OHM);
}

void nguSau() {
  LoRa.sleep();                                     // Đưa SX1278 về chế độ ngủ
  digitalWrite(LORA_NSS, HIGH);
  gpio_hold_en((gpio_num_t)LORA_NSS);               // Giữ NSS ở mức cao trong lúc ESP32 ngủ
  gpio_deep_sleep_hold_en();

  Serial.printf("Ngu %llu giay\n", THOI_GIAN_NGU_GIAY);
  Serial.flush();
  esp_sleep_enable_timer_wakeup(THOI_GIAN_NGU_GIAY * 1000000ULL);
  esp_deep_sleep_start();
}

void setup() {
  Serial.begin(115200);
  gpio_hold_dis((gpio_num_t)LORA_NSS);              // Nhả chân NSS đã giữ ở lần ngủ trước
  soThuTu++;

  uint32_t pinMv = doPinMv();

  LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0);
  if (!LoRa.begin(TAN_SO_HZ)) {
    Serial.println("Khong tim thay module LoRa");
    nguSau();
  }
  LoRa.setSyncWord(TU_DONG_BO);
  LoRa.setSpreadingFactor(HE_SO_TRAI_PHO);
  LoRa.enableCrc();

  // Gói dạng chữ: PW;<số node>;<số thứ tự>;<điện áp pin mV>
  char goi[40];
  int soByte = snprintf(goi, sizeof(goi), "PW;%d;%lu;%lu", ID_NODE, (unsigned long)soThuTu, (unsigned long)pinMv);
  LoRa.beginPacket();
  LoRa.write((const uint8_t *)goi, soByte);
  LoRa.endPacket();
  Serial.printf("Da gui: %s\n", goi);

  nguSau();
}

void loop() {
  // Không dùng: mỗi lần thức dậy chương trình chạy lại từ setup()
}
Chu kỳ thức và ngủ của node LoRa dùng ESP32: thức dậy, đo pin, phát một gói rồi ngủ sâu 300 giây; biểu đồ dòng tiêu thụ theo thời gian
Chu kỳ hoạt động của node (hình vẽ minh họa, không theo tỉ lệ).

5. Chương trình cho gateway

Sửa tên Wi-Fi, mật khẩu và MQTT_HOST cho đúng với mạng của bạn. HE_SO_TRAI_PHO và TU_DONG_BO phải giống node.

/*
  PYWORLD.VN - Gateway LoRa sang MQTT: nhận gói LoRa của các node rồi publish lên broker qua Wi-Fi
  LoRa: NSS = D5, SCK = D18, MISO = D19, MOSI = D23, RST = D4, DIO0 = RX2 (GPIO16), nguồn 3V3
  Thư viện: LoRa (Sandeep Mistry), PubSubClient
*/

#include <WiFi.h>
#include <PubSubClient.h>
#include <SPI.h>
#include <LoRa.h>

#define LORA_NSS  5
#define LORA_RST  4
#define LORA_DIO0 16

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 = "lora-gateway-01";
const char *TOPIC_KET_NOI = "nha/lora/gateway/ket-noi";

const long TAN_SO_HZ = 433E6;
const int  TU_DONG_BO = 0xF3;
const int  HE_SO_TRAI_PHO = 9;               // Phải giống các node

const unsigned long CHU_KY_KET_NOI_LAI_MS = 5000;
unsigned long lanThuKetNoi = 0;

WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);

// Thử kết nối lại mỗi 5 giây, không chờ lâu để không bỏ lỡ gói LoRa
void giuKetNoi() {
  if (mqtt.connected() || millis() - lanThuKetNoi < CHU_KY_KET_NOI_LAI_MS) {
    return;
  }
  lanThuKetNoi = millis();

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Mat WiFi, dang ket noi lai");
    WiFi.reconnect();
    return;
  }
  Serial.printf("Dang ket noi MQTT %s:%u ... ", MQTT_HOST, MQTT_PORT);
  // Lời nhắn cuối: broker tự publish "offline" khi gateway mất kết nối
  if (mqtt.connect(MQTT_CLIENT_ID, TOPIC_KET_NOI, 0, true, "offline")) {
    mqtt.publish(TOPIC_KET_NOI, "online", true);
    Serial.println("thanh cong");
  } else {
    Serial.printf("that bai, ma loi %d\n", mqtt.state());
  }
}

// Gói hợp lệ có dạng: PW;<số node>;<số thứ tự>;<điện áp pin mV>
void xuLyGoi(const char *goi, int rssi, float snr) {
  int idNode = 0;
  unsigned long soThuTu = 0, pinMv = 0;
  if (sscanf(goi, "PW;%d;%lu;%lu", &idNode, &soThuTu, &pinMv) != 3) {
    Serial.printf("Bo qua goi la: %s\n", goi);
    return;
  }

  char topic[48];
  char noiDung[96];
  snprintf(topic, sizeof(topic), "nha/lora/node-%d/trang-thai", idNode);
  snprintf(noiDung, sizeof(noiDung), "{\"so\":%lu,\"pin_mv\":%lu,\"rssi\":%d,\"snr\":%.1f}", soThuTu, pinMv, rssi, snr);

  bool daGui = mqtt.connected() && mqtt.publish(topic, noiDung);
  Serial.printf("[%s] %s%s\n", topic, noiDung, daGui ? "" : "  (chua gui duoc len broker)");
}

void setup() {
  Serial.begin(115200);

  LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0);
  if (!LoRa.begin(TAN_SO_HZ)) {
    Serial.println("Khong tim thay module LoRa, kiem tra day noi va nguon 3V3");
    while (true) {
      delay(1000);
    }
  }
  LoRa.setSyncWord(TU_DONG_BO);
  LoRa.setSpreadingFactor(HE_SO_TRAI_PHO);
  LoRa.enableCrc();

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.printf("Dang ket noi WiFi %s", WIFI_SSID);
  unsigned long batDau = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - batDau < 15000) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("Da ket noi WiFi, IP: %s\n", WiFi.localIP().toString().c_str());
  }

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  Serial.println("Gateway san sang, dang cho goi LoRa");
}

void loop() {
  giuKetNoi();
  mqtt.loop();

  int kichThuoc = LoRa.parsePacket();
  if (kichThuoc == 0) {
    return;
  }
  char goi[64];
  int n = 0;
  while (LoRa.available() && n < (int)sizeof(goi) - 1) {
    goi[n++] = (char)LoRa.read();
  }
  goi[n] = '\0';
  xuLyGoi(goi, LoRa.packetRssi(), LoRa.packetSnr());
}

6. Kết quả mong đợi

Hình minh họa Serial Monitor của node LoRa và của gateway: node gửi gói PW;1;12;3912 rồi ngủ, gateway kết nối Wi-Fi, MQTT và publish dữ liệu JSON
Kết quả mong đợi trên Serial Monitor của node và gateway (hình minh họa).

Trên máy chạy broker, mở cửa sổ dòng lệnh và đăng ký nhận mọi topic của hệ thống:

mosquitto_sub -h 192.168.1.10 -t "nha/lora/#" -v

Mỗi lần node gửi, cửa sổ hiện thêm một dòng tương tự:

nha/lora/gateway/ket-noi online
nha/lora/node-1/trang-thai {"so":12,"pin_mv":3912,"rssi":-97,"snr":8.5}

Khi thử nghiệm, có thể giảm THOI_GIAN_NGU_GIAY xuống 10 để không phải chờ lâu, rồi đặt lại 300 khi dùng thật.

7. Giải thích các điểm chính

  • Biến giữ qua lần ngủ: sau khi ngủ sâu, chương trình chạy lại từ setup() và mọi biến thường bị xóa. Biến khai báo với RTC_DATA_ATTR nằm trong bộ nhớ RTC nên giữ được số thứ tự gói.
  • Cho module LoRa ngủ: LoRa.sleep() đưa SX1278 về chế độ tiêu thụ thấp nhất. Chương trình giữ chân NSS ở mức cao bằng gpio_hold_en() để module không bị kích hoạt nhầm khi các chân của ESP32 thả nổi lúc ngủ, và nhả giữ bằng gpio_hold_dis() khi thức dậy.
  • Tách gói ở gateway: sscanf() đọc ba số sau tiền tố PW. Gói không đúng dạng bị bỏ qua và ghi ra Serial.
  • Không chặn vòng lặp: gateway chỉ thử kết nối lại MQTT mỗi 5 giây một lần và không chờ lâu trong loop(), nhờ đó không bỏ lỡ gói LoRa trong lúc mạng trục trặc.
  • Lời nhắn cuối: gateway đăng ký với broker bản tin offline ở topic nha/lora/gateway/ket-noi. Khi gateway mất điện hoặc mất mạng, broker tự publish bản tin này.
  • Gateway bổ sung RSSI và SNR: hai số này cho biết chất lượng đường truyền của từng node, hữu ích khi chọn vị trí đặt node và gateway.

8. Ước lượng thời gian dùng pin

Dòng trung bình của node tính theo công thức:

I_trung_binh = (I_thuc x t_thuc + I_ngu x t_ngu) / (t_thuc + t_ngu)
Thoi_gian_dung_pin (gio) = Dung_luong_pin (mAh) / I_trung_binh (mA)

Ví dụ với các số giả định: node thức 1 giây ở 60 mA, ngủ 300 giây ở 1 mA thì dòng trung bình khoảng 1,2 mA; pin 2500 mAh dùng được khoảng 2000 giờ, tức gần ba tháng. Con số thật phụ thuộc chủ yếu vào dòng lúc ngủ của cả mạch. Mạch DevKit có sẵn chip USB, IC ổn áp và đèn nguồn nên dòng lúc ngủ lớn hơn nhiều so với riêng chip ESP32; muốn dùng pin lâu cần đo dòng thật bằng đồng hồ và chọn mạch có dòng nghỉ thấp.

9. Mở rộng

  • Nhiều node: nạp cùng chương trình với ID_NODE khác nhau. Mỗi node tự có topic riêng nha/lora/node-N/trang-thai. Khi có nhiều node, cho mỗi node ngủ thêm một khoảng ngẫu nhiên vài giây để hai gói ít trùng thời điểm.
  • Thêm đại lượng đo: nối thêm trường vào chuỗi gói (nhiệt độ, độ ẩm, mức nước) và sửa sscanf(), chuỗi JSON ở gateway cho khớp.
  • Tăng tầm xa: đặt anten gateway ở chỗ cao; tăng SF ở cả node và gateway. Xem cách đánh đổi giữa SF và thời gian phát trước khi tăng.
  • Bảo mật: gói LoRa điểm nối điểm không được mã hóa sẵn; ai có module cùng cấu hình đều đọc và giả gói được. Không dùng hệ thống này cho lệnh điều khiển quan trọng khi chưa thêm mã hóa và kiểm tra nguồn gửi.

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

Hiện tượngNguyên nhân thường gặpCách xử lý
Gateway không in gì khi node gửiKhác tần số, SF hoặc từ đồng bộ; chưa gắn antenSo lại ba hằng số ở hai chương trình; thử với chương trình gửi, nhận của bài LoRa cơ bản để kiểm tra phần cứng
Gateway in (chua gui duoc len broker)Chưa kết nối được Wi-Fi hoặc brokerXem dòng báo lỗi MQTT; kiểm tra địa chỉ broker, tường lửa cổng 1883 và cấu hình listener của Mosquitto
Gateway in Bo qua goi laCó thiết bị LoRa khác cùng cấu hình, hoặc node dùng định dạng gói khácĐổi từ đồng bộ ở cả hai bên; kiểm tra dòng snprintf() ở node
Điện áp pin báo sai lệchSai số điện trở và ADCĐo pin bằng đồng hồ rồi hiệu chỉnh hệ số như bài đo điện áp pin 18650
Node không thức dậy lạiPin yếu làm sụt áp khi phát; nguồn không đủ dòngSạc pin; thêm tụ 100 µF sát module LoRa; kiểm tra mạch nguồn
Không nạp được chương trình mới cho nodeNode đang ngủ sâu nên cổng nối tiếp không phản hồiGiữ nút BOOT, nhấn nút EN rồi bấm nạp; hoặc nạp ngay lúc node vừa thức dậy

11. Xem thêm


Tin tức liên quan

Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng
Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng

Phần 2 của loạt phased array siêu âm: thêm mạch đệm 12 V bằng hai module L298N, lắp mạch thu dùng LM358, đo biên độ 40 kHz bằng ADC của ESP32 và chạy chương trình tự quét góc lái để xem búp sóng.

Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha
Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha

Nguyên lý phased array và cách dùng ESP32 tạo 8 kênh xung 40 kHz lệch pha để lái búp sóng siêu âm: công thức độ trễ, giới hạn do khoảng cách đầu phát, cấu hình LEDC với hpoint, sơ đồ nối và code.

Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến
Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến

Giải thích Wi-Fi CSI và hướng dẫn dùng ESP32 phát hiện người di chuyển trong phòng chỉ bằng sóng Wi-Fi từ router: lấy dữ liệu CSI, tính điểm chuyển động, đặt ngưỡng và xem trên Serial Plotter.

Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay
Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay

Hướng dẫn dùng cảm biến chuyển động PIR HC-SR501 với ESP32: nguyên lý, cách chỉnh module, đọc tín hiệu bằng ngắt và ghép relay làm đèn tự bật khi có người, tự tắt sau 60 giây.

Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa
Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa

Giải thích bốn tham số cấu hình LoRa (SF, băng thông, tỉ lệ mã, công suất), cách tính thời gian phát một gói và hướng dẫn làm bộ thử tầm xa bằng ESP32, SX1278 và màn hình OLED.

Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên
Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên

Hướng dẫn nối module LoRa SX1278 Ra-02 433 MHz với ESP32 qua SPI, cài thư viện LoRa trên Arduino IDE, viết chương trình gửi và nhận gói tin, đọc RSSI, SNR và xử lý lỗi thường gặp.

ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED
ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED

Hướng dẫn dựng mô hình ESP-NOW nhiều node: tối đa bốn ESP32 gửi số đo về một trạm thu, trạm thu hiện điện áp, RSSI và tình trạng từng node trên màn hình OLED 0.96 inch.

Điều khiển relay từ xa bằng ESP-NOW: remote ESP32 một nút nhấn có phản hồi trạng thái
Điều khiển relay từ xa bằng ESP-NOW: remote ESP32 một nút nhấn có phản hồi trạng thái

Hướng dẫn làm bộ điều khiển relay từ xa bằng hai mạch ESP32 và ESP-NOW hai chiều: remote gửi lệnh, mạch relay báo lại trạng thái, lọc theo địa chỉ MAC và bật mã hóa.


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