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.
Khi cần theo dõi nhiều điểm đo cùng lúc, ESP-NOW cho phép nhiều mạch ESP32 gửi số liệu về một mạch trung tâm mà không cần router Wi-Fi. Bài viết hướng dẫn dựng mô hình này: tối đa bốn node đo điện áp gửi về một trạm thu, trạm thu hiện số đo, cường độ tín hiệu và tình trạng từng node trên màn hình OLED 0.96 inch.
Bài dùng lại kiến thức của bài ESP-NOW cơ bản (địa chỉ MAC, peer, hàm gọi lại). Nếu mới làm quen, nên chạy thử hai mạch gửi, nhận đơn giản trước.
1. Mô hình nhiều node, một trạm thu
- Mỗi node chỉ cần biết địa chỉ MAC của trạm thu và khai báo trạm thu làm peer.
- Trạm thu nhận gói của mọi node mà không cần khai báo từng node. Trạm thu phân biệt các node bằng trường
idNodetrong gói. - Mỗi gói mang số thứ tự tăng dần. Khi số thứ tự bị nhảy cóc, trạm thu biết đã mất gói và cộng vào bộ đếm.
- Node nào quá 10 giây không gửi thì màn hình báo mất tín hiệu thay vì giữ số đo cũ.
2. Chuẩn bị
- Từ hai đến năm mạch ESP32 DevKit V1 30 chân: một mạch làm trạm thu, các mạch còn lại làm node.
- Một màn hình OLED 0.96 inch SSD1306 giao tiếp I2C cho trạm thu.
- Breadboard, dây cắm test board và cáp USB có truyền dữ liệu.
- Nguồn tín hiệu để đo ở mỗi node, ví dụ một biến trở 10 kΩ hoặc cầu phân áp đo pin.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10).
3. Nối dây
| Màn hình OLED | ESP32 DevKit V1 (trạm thu) |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SCL | D22 (GPIO22) |
| SDA | D21 (GPIO21) |
Ở mỗi node: đưa điện áp cần đo vào chân D34 và nối chung GND. Điện áp ở chân ADC của ESP32 không được vượt 3,3 V, và phép đo chỉ tuyến tính đến khoảng 3,1 V. Để thử nhanh, nối hai đầu biến trở 10 kΩ vào 3V3 và GND, chân giữa vào D34. Muốn đo pin 18650 thì dùng cầu phân áp như bài đọc ADC và đo điện áp pin 18650.
4. Cài thư viện cho màn hình
- Mở Tools > Manage Libraries.
- Tìm và cài Adafruit SSD1306. Khi được hỏi, chọn cài luôn các thư viện phụ thuộc (Adafruit GFX Library, Adafruit BusIO).
Bài dùng Adafruit SSD1306 2.5.17 và Adafruit GFX Library 1.12.6. Phần ESP-NOW có sẵn trong gói esp32.
5. Chương trình cho node
Trước tiên xem địa chỉ MAC của mạch làm trạm thu (chương trình ở bài ESP-NOW cơ bản) rồi điền vào MAC_TRAM_THU. Nạp cùng chương trình cho mọi node, mỗi lần nạp đổi ID_NODE thành 1, 2, 3, 4:
/*
PYWORLD.VN - ESP-NOW nhiều node: chương trình cho NODE đo
Mỗi node đo điện áp ở chân D34 rồi gửi về trạm thu mỗi 2 giây
Nạp cho từng node với một ID_NODE khác nhau (1 đến 4)
*/
#include <WiFi.h>
#include <esp_now.h>
#define ID_NODE 1 // Đổi thành 2, 3, 4 khi nạp cho các node khác
#define CHAN_DO 34 // Chân ADC đo điện áp, tối đa khoảng 3,1 V
// Địa chỉ MAC của trạm thu: sửa theo mạch thật
uint8_t MAC_TRAM_THU[6] = {0x24, 0x6F, 0x28, 0x12, 0x34, 0x56};
const unsigned long CHU_KY_GUI_MS = 2000;
// Node và trạm thu phải khai báo cấu trúc này giống hệt nhau
typedef struct {
uint8_t idNode; // Số của node
uint32_t soThuTu; // Số thứ tự gói, dùng để phát hiện gói bị mất
uint16_t dienApMv; // Điện áp đo được, đơn vị mV
} GoiNode;
GoiNode goi;
bool sanSang = false;
unsigned long lanGuiTruoc = 0;
volatile uint32_t soGoiLoi = 0;
void khiGuiXong(const esp_now_send_info_t *thongTin, esp_now_send_status_t trangThai) {
if (trangThai != ESP_NOW_SEND_SUCCESS) {
soGoiLoi = soGoiLoi + 1;
}
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Khoi dong ESP-NOW that bai");
return;
}
esp_now_register_send_cb(khiGuiXong);
esp_now_peer_info_t tramThu = {};
memcpy(tramThu.peer_addr, MAC_TRAM_THU, 6);
tramThu.channel = 0;
tramThu.encrypt = false;
if (esp_now_add_peer(&tramThu) != ESP_OK) {
Serial.println("Khong them duoc tram thu");
return;
}
goi.idNode = ID_NODE;
sanSang = true;
Serial.printf("Node %d san sang\n", ID_NODE);
}
void loop() {
if (!sanSang || millis() - lanGuiTruoc < CHU_KY_GUI_MS) {
return;
}
lanGuiTruoc = millis();
goi.soThuTu++;
goi.dienApMv = (uint16_t)analogReadMilliVolts(CHAN_DO);
esp_now_send(MAC_TRAM_THU, (const uint8_t *)&goi, sizeof(goi));
Serial.printf("Node %d gui goi %lu: %u mV (so goi khong duoc xac nhan: %lu)\n",
ID_NODE, (unsigned long)goi.soThuTu, goi.dienApMv, (unsigned long)soGoiLoi);
}
6. Chương trình cho trạm thu
/*
PYWORLD.VN - ESP-NOW nhiều node: chương trình cho TRẠM THU có màn hình OLED
Nhận số đo của tối đa 4 node, hiện điện áp và cường độ tín hiệu từng node trên OLED 0.96 inch
OLED SSD1306 I2C: VCC = 3V3, GND = GND, SCL = D22, SDA = D21
*/
#include <WiFi.h>
#include <esp_now.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SO_NODE 4
#define DIA_CHI_OLED 0x3C
const unsigned long QUA_HAN_MS = 10000; // Quá 10 giây không có gói thì coi node mất tín hiệu
const unsigned long CHU_KY_VE_MS = 500;
Adafruit_SSD1306 manHinh(128, 64, &Wire, -1);
// Phải giống hệt cấu trúc trong chương trình node
typedef struct {
uint8_t idNode;
uint32_t soThuTu;
uint16_t dienApMv;
} GoiNode;
typedef struct {
bool daNhan; // Đã từng nhận gói của node này
uint32_t soThuTu;
uint16_t dienApMv;
int rssi;
uint32_t soGoiMat; // Số gói bị thiếu, tính theo số thứ tự
unsigned long lucNhan; // Thời điểm nhận gói gần nhất
} TrangThaiNode;
TrangThaiNode node[SO_NODE + 1]; // Dùng chỉ số 1 đến SO_NODE
portMUX_TYPE khoaDuLieu = portMUX_INITIALIZER_UNLOCKED;
volatile bool coGoiMoi = false;
volatile uint8_t nodeVuaNhan = 0;
unsigned long lanVeTruoc = 0;
// Chạy trong tác vụ Wi-Fi: chỉ cập nhật bảng trạng thái rồi thoát
void khiNhan(const esp_now_recv_info_t *thongTin, const uint8_t *duLieu, int doDai) {
if (doDai != sizeof(GoiNode)) {
return;
}
GoiNode goi;
memcpy(&goi, duLieu, sizeof(goi));
if (goi.idNode < 1 || goi.idNode > SO_NODE) {
return;
}
portENTER_CRITICAL(&khoaDuLieu);
TrangThaiNode &n = node[goi.idNode];
if (n.daNhan && goi.soThuTu > n.soThuTu + 1) {
n.soGoiMat += goi.soThuTu - n.soThuTu - 1;
}
n.daNhan = true;
n.soThuTu = goi.soThuTu;
n.dienApMv = goi.dienApMv;
n.rssi = thongTin->rx_ctrl->rssi;
n.lucNhan = millis();
portEXIT_CRITICAL(&khoaDuLieu);
nodeVuaNhan = goi.idNode;
coGoiMoi = true;
}
void veManHinh() {
TrangThaiNode banSao[SO_NODE + 1];
portENTER_CRITICAL(&khoaDuLieu);
memcpy(banSao, node, sizeof(node));
portEXIT_CRITICAL(&khoaDuLieu);
manHinh.clearDisplay();
manHinh.setTextSize(1);
manHinh.setCursor(0, 4);
manHinh.print("ESP-NOW TRAM THU");
for (int i = 1; i <= SO_NODE; i++) {
manHinh.setCursor(0, 6 + i * 12);
if (!banSao[i].daNhan) {
manHinh.printf("N%d chua co du lieu", i);
} else if (millis() - banSao[i].lucNhan > QUA_HAN_MS) {
manHinh.printf("N%d mat tin hieu", i);
} else {
manHinh.printf("N%d %4umV %4ddBm", i, banSao[i].dienApMv, banSao[i].rssi);
}
}
manHinh.display();
}
void setup() {
Serial.begin(115200);
if (!manHinh.begin(SSD1306_SWITCHCAPVCC, DIA_CHI_OLED)) {
Serial.println("Khong tim thay OLED, kiem tra day SDA, SCL");
}
manHinh.setTextColor(SSD1306_WHITE);
manHinh.clearDisplay();
manHinh.display();
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Khoi dong ESP-NOW that bai");
return;
}
esp_now_register_recv_cb(khiNhan);
Serial.println("Tram thu san sang");
}
void loop() {
if (coGoiMoi) {
coGoiMoi = false;
uint8_t id = nodeVuaNhan;
Serial.printf("Node %u | goi %lu | %u mV | RSSI %d dBm | mat %lu goi\n",
id, (unsigned long)node[id].soThuTu, node[id].dienApMv, node[id].rssi,
(unsigned long)node[id].soGoiMat);
}
if (millis() - lanVeTruoc >= CHU_KY_VE_MS) {
lanVeTruoc = millis();
veManHinh();
}
}
7. Kết quả mong đợi
- Bật trạm thu trước: bốn dòng đều ghi
chua co du lieu. - Bật từng node: dòng tương ứng đổi thành số đo (mV) và RSSI (dBm), cập nhật mỗi 2 giây.
- Rút nguồn một node: sau 10 giây dòng của node đó đổi thành
mat tin hieu. - Đưa một node ra xa dần: RSSI giảm, bộ đếm
mat ... goitrên Serial Monitor của trạm thu bắt đầu tăng.
8. Giải thích các điểm chính
- Bảng trạng thái: mảng
node[]lưu số đo gần nhất, RSSI, số gói mất và thời điểm nhận của từng node. Hàm nhận gói chỉ cập nhật bảng này. - Vùng găng: hàm nhận chạy trong tác vụ Wi-Fi, còn màn hình được vẽ trong
loop(). Cặp lệnhportENTER_CRITICALvàportEXIT_CRITICALbảo đảmloop()không đọc bảng lúc bảng đang được ghi dở. Hàm vẽ chép bảng ra bản sao rồi mới vẽ để thời gian giữ khóa thật ngắn. - Phát hiện mất gói: nếu số thứ tự mới lớn hơn số cũ cộng 1 thì phần chênh là số gói đã mất. Khi node khởi động lại, số thứ tự quay về 1 và không bị tính là mất gói.
- Quá hạn: phép so
millis() - lucNhan > QUA_HAN_MSquyết định hiển thị số đo hay báo mất tín hiệu. - Bố trí màn hình: cỡ chữ 1 cao 8 điểm ảnh, mỗi dòng chứa được 21 ký tự. Tiêu đề đặt ở 16 hàng trên cùng, là vùng màu vàng trên loại màn hình hai màu.
9. Mở rộng
- Thêm node: tăng
SO_NODEvà đặtID_NODEtương ứng. Màn hình 0.96 inch chỉ đủ bốn dòng số liệu, nên khi có nhiều node hơn cần chia trang hoặc chỉ hiện các node vừa thay đổi. - Thêm đại lượng đo: thêm biến vào
GoiNode(nhiệt độ, độ ẩm, trạng thái cửa) rồi nạp lại cả node và trạm thu. - Node chạy pin: cho node ngủ sâu giữa hai lần gửi, xem bài Deep Sleep trên ESP32. Có thể dùng mạch ESP32 tích hợp pin 18650 làm node.
- Đưa số liệu lên mạng: trạm thu có thể vừa nhận ESP-NOW vừa kết nối Wi-Fi để gửi tiếp lên broker MQTT. Khi đó trạm thu chạy trên kênh của router, các node phải đặt cùng kênh đó.
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ý |
|---|---|---|
Màn hình không sáng, Serial in Khong tim thay OLED | Nối nhầm SDA với SCL, thiếu nguồn hoặc màn hình có địa chỉ khác 0x3C | Kiểm tra bốn dây; quét địa chỉ I2C theo bài giao tiếp I2C trên ESP32 rồi sửa DIA_CHI_OLED |
Mọi dòng luôn ghi chua co du lieu | Node dùng sai địa chỉ MAC của trạm thu hoặc cấu trúc GoiNode hai bên khác nhau | Xem lại MAC của đúng mạch trạm thu; chép cấu trúc cho giống hệt |
| Hai node hiện chung một dòng, số đo nhảy qua lại | Hai node được nạp cùng một ID_NODE | Nạp lại, mỗi node một số khác nhau |
| Số đo luôn bằng 0 hoặc dao động lung tung | Chân D34 để hở hoặc chưa nối chung GND với nguồn tín hiệu | Nối tín hiệu vào D34 và nối chung GND |
| Bộ đếm gói mất tăng nhanh dù để gần | Nhiều node gửi trùng thời điểm hoặc kênh Wi-Fi đang đông | Cho mỗi node một chu kỳ gửi lệch nhau đôi chút; thử ở vị trí ít mạng Wi-Fi hơn |
11. Xem thêm
- Làm đồng hồ Internet với ESP32 và màn hình OLED 0.96 inch: thêm ví dụ vẽ chữ trên OLED.
- Đọc ADC trên ESP32 và đo điện áp pin 18650.
- Dùng MQTT với ESP32: bước tiếp theo khi muốn xem số liệu trên điện thoại.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm