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ô hình ESP-NOW nhiều node: bốn mạch ESP32 gửi gói GoiNode về một trạm thu ESP32 có màn hình OLED 0.96 inch
Bốn node gửi về một trạm thu; mỗi node mang một số riêng (hình vẽ minh họa).
  • 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 idNode trong 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ị

3. Nối dây

Sơ đồ nối trạm thu ESP-NOW: ESP32 DevKit V1 với màn hình OLED 0.96 inch, VCC nối 3V3, GND nối GND, SCL nối D22, SDA nối D21
Sơ đồ nối trạm thu với màn hình OLED 0.96 inch (hình vẽ minh họa).
Màn hình OLEDESP32 DevKit V1 (trạm thu)
VCC3V3
GNDGND
SCLD22 (GPIO22)
SDAD21 (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

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

Hình minh họa màn hình OLED của trạm thu ESP-NOW: tiêu đề, node 1 và node 2 có số đo và RSSI, node 3 mất tín hiệu, node 4 chưa có dữ liệu
Màn hình trạm thu với bốn tình trạng khác nhau của node (hình minh họa, số đo là ví dụ).
Hình minh họa Serial Monitor của trạm thu và của node 1 trong mô hình ESP-NOW nhiều node, có số thứ tự gói, điện áp, RSSI và số gói mất
Kết quả mong đợi trên Serial Monitor (hình minh họa).
  • 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 ... goi trê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ệnh portENTER_CRITICAL và portEXIT_CRITICAL bảo đảm loop() 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_MS quyế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_NODE và đặt ID_NODE tươ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ượngNguyên nhân thường gặpCách xử lý
Màn hình không sáng, Serial in Khong tim thay OLEDNối nhầm SDA với SCL, thiếu nguồn hoặc màn hình có địa chỉ khác 0x3CKiể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 lieuNode dùng sai địa chỉ MAC của trạm thu hoặc cấu trúc GoiNode hai bên khác nhauXem 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ạiHai node được nạp cùng một ID_NODENạp lại, mỗi node một số khác nhau
Số đo luôn bằng 0 hoặc dao động lung tungChân D34 để hở hoặc chưa nối chung GND với nguồn tín hiệuNối tín hiệu vào D34 và nối chung GND
Bộ đếm gói mất tăng nhanh dù để gầnNhiều node gửi trùng thời điểm hoặc kênh Wi-Fi đang đôngCho 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


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.

Làm gateway LoRa sang MQTT bằng ESP32: node cảm biến chạy pin gửi dữ liệu về broker
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.

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.

Đ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