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
- 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
| Linh kiện | Chân | ESP32 DevKit V1 |
|---|---|---|
| Module LoRa (node và gateway) | 3.3V | 3V3 |
| GND | GND | |
| MOSI | D23 | |
| MISO | D19 | |
| SCK | D18 | |
| NSS | D5 | |
| RST | D4 | |
| DIO0 | RX2 (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, R2 | D35 (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()
}
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
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ớiRTC_DATA_ATTRnằ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ằnggpio_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ằnggpio_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ở topicnha/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_NODEkhác nhau. Mỗi node tự có topic riêngnha/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ượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Gateway không in gì khi node gửi | Khác tần số, SF hoặc từ đồng bộ; chưa gắn anten | So 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 broker | Xem 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 la | Có 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ệch | Sai 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ại | Pin yếu làm sụt áp khi phát; nguồn không đủ dòng | Sạ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 node | Node đang ngủ sâu nên cổng nối tiếp không phản hồi | Giữ 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 |
Xem thêm