ESP32, IOT物聯網

【ESP32教學】第三十四篇:ESP-NOW 雙向無線通訊實戰:免路由器、低延遲範例教學

ESP32 ESP-NOW 無線通訊實戰指南,展示兩塊 ESP32 開發板點對點傳輸數據

在物聯網(IoT)專案中,若需要兩台或多台 ESP32 進行感測器數據同步,通常會透過 MQTT 或 HTTP 協定,但這類做法必須依賴 Wi-Fi 路由器與網際網路。

若專案部署在戶外或沒有 Wi-Fi 的環境,ESP-NOW 就是最佳解答。本文將帶你了解 ESP-NOW 的優勢,並提供完整的程式碼範例。

什麼是 ESP-NOW?

ESP-NOW 是樂鑫科技(Espressif)開發的一種無伺服器(Serverless)無線通訊協定,採用 2.4GHz 頻段,運作方式類似 2.4GHz 無線滑鼠。

ESP-NOW 的核心優勢

  • 免 Wi-Fi 路由器:裝置間可直接點對點(P2P)連線。

  • 極低延遲與低功耗:無需經歷繁瑣的 Wi-Fi 握手連線過程,封包傳輸僅需數毫秒,非常適合電池供電專案。

  • 支援多種拓撲:支援一對一、一對多(Master to Slaves)、多對一(Slaves to Master)等架構。

  • 傳輸距離與穩定度:在空曠無遮蔽環境下,採用板載天線可達 50~100 公尺;若加上障礙物或牆壁遮蔽,有效距離約為 10~30 公尺。若需更遠距離,可改用外接天線版本。

ESP32 ESP-NOW 支援的四種網路拓撲示意圖:點對點、一對多星狀、多對一匯聚與網狀 Mesh 拓撲

ESP-NOW 支援點對點(P2P)、星狀(一對多)、匯聚(多對一)及混和網狀等多種彈性通訊拓撲架構

步驟一:獲取接收端(Receiver)的 MAC 位址

ESP-NOW 是透過硬體 MAC 位址(MAC Address)來指定資料要傳給哪一台裝置。在開始傳輸前,必須先查詢接收端 ESP32 的 MAC 位址。

將以下程式碼燒錄至接收端 ESP32,並打開 Serial Monitor(波特率 115200):

#include "WiFi.h"

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_MODE_STA);
  Serial.print("ESP32 Board MAC Address: ");
  Serial.println(WiFi.macAddress());
}

void loop() {
  // 僅需執行一次取得 MAC 位址
}

提示:記下 Serial Monitor 中輸出的 MAC 位址(格式如 08:3A:8D:12:34:56),後續發送端程式碼會用到。

步驟二:傳送端(Sender)程式碼範例

將取得的 MAC 位址替換至程式碼中的 broadcastAddress 陣列內。此程式碼將每 2 秒發送一組溫濕度結構體(Struct)資料給接收端。

#include <esp_now.h> 
#include <WiFi.h> 

// 填入接收端 ESP32 的 MAC 位址(請根據實際查詢結果修改)
// 格式:{0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}
uint8_t broadcastAddress[] = {0x08, 0x3A, 0x8D, 0x12, 0x34, 0x56};

// 定義要傳送的資料結構(傳送端與接收端結構必須一致)
typedef struct struct_message {
  char a[32]; // 字串資料(例如訊息或狀態描述)
  int b;      // 整數資料(例如感測器數值或計數)
  float c;    // 浮點數資料(例如溫度或濕度)
  bool d;     // 布林值(例如開關狀態 true/false)
} struct_message;

// 建立資料結構變數,用來存放要發送的資料
struct_message myData;

// 建立 Peer 裝置資訊結構體變數,用來登記接收端資訊
esp_now_peer_info_t peerInfo;

/**
 * @brief 發送狀態回調函數(Callback Function)
 * 當發送動作完成時,ESP-NOW 會自動調用此函數回報發送結果
 * 
 * @param mac_addr 接收端的 MAC 位址
 * @param status 發送狀態(ESP_NOW_SEND_SUCCESS 或 ESP_NOW_SEND_FAIL)
 */
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("\r\n最後一次封包發送狀態:\t");
  // 檢查是否成功送達
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "發送成功 (Delivery Success)" : "發送失敗 (Delivery Fail)");
}

void setup() {
  // 初始化序列埠通訊(波特率 115200)
  Serial.begin(115200);

  // 將 ESP32 設為 Wi-Fi 站台模式(Station Mode)
  // ESP-NOW 必須在 Wi-Fi 啟動的狀態下才能運行
  WiFi.mode(WIFI_STA);

  // 初始化 ESP-NOW 通訊協定
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW 初始化失敗!");
    return;
  }

  // 註冊「發送狀態」回調函數,當數據發送完畢時自動執行 OnDataSent
  esp_now_register_send_cb(OnDataSent);
  
  // 設定 Peer 裝置資訊(把目標接收端的 MAC 位址複製進去)
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;     // 設定 Wi-Fi 信道(0 表示使用當前預設信道)
  peerInfo.encrypt = false; // 是否加密傳輸(預設不加密)
  
  // 新增 Peer 裝置(將目標接收端登記至 ESP-NOW 允許清單中)
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("新增 Peer 裝置失敗!");
    return;
  }
}

void loop() {
  // 1. 準備要發送的測試數據
  strcpy(myData.a, "Hello ESP-NOW"); // 給予字串數值
  myData.b = random(1, 20);          // 產生 1~20 的隨機整數
  myData.c = 25.5;                   // 寫入浮點數(例如溫度 25.5 度)
  myData.d = false;                  // 寫入布林狀態
  
  // 2. 透過 ESP-NOW 發送封包
  // 參數分別為:目標 MAC 位址、資料記憶體指標、資料長度
  esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
   
  // 檢查呼叫發送函式本身是否成功(非最終送達狀態,最終狀態由回調函數回報)
  if (result == ESP_OK) {
    Serial.println("封包已發送至緩衝區...");
  } else {
    Serial.println("發送指令執行錯誤!");
  }

  // 每隔 2 秒發送一次
  delay(2000);
}

步驟三:接收端(Receiver)程式碼範例

接收端 ESP32 收到封包時,會自動觸發 OnDataRecv 回調函數,解析並印出傳來的結構體內容。

#include <esp_now.h> 
#include <WiFi.h> 

// 定義要接收的資料結構(必須與傳送端結構完全一致)
typedef struct struct_message {
    char a[32]; // 字串資料(例如訊息或狀態描述)
    int b;      // 整數資料(例如感測器數值或計數)
    float c;    // 浮點數資料(例如溫度或濕度)
    bool d;     // 布林值(例如開關狀態 true/false)
} struct_message;

// 建立資料結構變數,用來存放接收到的數據
struct_message myData;

/**
 * @brief 接收資料回調函數(ESP32 Core v2.x 舊版語法)
 * 當 ESP32 收到 ESP-NOW 數據封包時,系統會自動呼叫此函數進行解析
 * 
 * @param mac 傳送端 ESP32 的 MAC 位址指標 (uint8_t *)
 * @param incomingData 接收到的原始記憶體資料指標
 * @param len 接收到的資料位元組(Bytes)長度
 */
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) {
  // 將接收到的原始記憶體資料,複製填入 myData 結構體中
  memcpy(&myData, incomingData, sizeof(myData));

  // 1. 印出發送端的 MAC 位址(格式:XX:XX:XX:XX:XX:XX)
  Serial.print("收到來自 MAC 位址:");
  for (int i = 0; i < 6; i++) {
    Serial.printf("%02X", mac[i]);
    if (i < 5) Serial.print(":");
  }
  Serial.println();

  // 2. 於 Serial Monitor 印出解析後的資料欄位內容
  Serial.print("接收到的位元組數:");
  Serial.println(len);

  Serial.print("字串內容 (a):");
  Serial.println(myData.a);

  Serial.print("整數內容 (b):");
  Serial.println(myData.b);

  Serial.print("浮點數內容 (c):");
  Serial.println(myData.c);

  Serial.print("布林狀態 (d):");
  Serial.println(myData.d ? "true" : "false");
  
  Serial.println("----------------------------------------");
}

void setup() {
  // 初始化序列埠通訊(波特率 115200)
  Serial.begin(115200);

  // 將 ESP32 設為 Wi-Fi 站台模式(Station Mode)
  // ESP-NOW 必須在 Wi-Fi 啟動的狀態下才能運行
  WiFi.mode(WIFI_STA);

  // 初始化 ESP-NOW 協定
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW 初始化失敗!");
    return;
  }
  
  // 註冊「接收資料」回調函數,當收到封包時自動觸發 OnDataRecv
  esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
  // 接收端完全由中斷回調函數(OnDataRecv)自動處理資料
  // loop 內部可保持空閒,或放置其他控制邏輯(如驅動馬達、控制繼電器、顯示器繪製等)
}

ESP-NOW 常見問題與除錯指南(FAQ)

Q1:報錯 'esp_now_recv_info' does not name a type 怎麼辦?

  • 原因:Arduino ESP32 核心庫版本不同(v3.0+ 與 v2.x 語法差異)。

  • 解法:使用舊版 v2.x 時,請將接收回調函數參數改為 (const uint8_t *mac, const uint8_t *incomingData, int len)。

Q2:序列埠一直顯示 Delivery Fail(發送失敗)?

  1. 檢查 MAC 位址:核對 broadcastAddress 填寫是否與接收端一致。

  2. Wi-Fi 信道相符:確保兩台 ESP32 處於相同的 Wi-Fi Channel。

  3. 確認接收端狀態:接收端必須已通電並成功執行 esp_now_init(),否則無法回傳 ACK。

Q3:單次傳送的資料容量上限是多少?

  • 大小限制:單一封包最多 250 Bytes。

  • 建議作法:使用 struct 結構體打包資料一次發送;超過容量需手動分包。

Q4:ESP32 與 ESP8266 能互相通訊嗎?

  • 可以。但定義 struct 時建議加上 __attribute__((packed)),避免不同晶片間的記憶體對齊差異導致解析錯誤。

Q5:傳輸距離有多遠?如何延長?

  • 預設距離:空曠無遮蔽環境約 50~100 公尺(室內穿牆約 10~30 公尺)。

  • 改善方式:選用帶 IPEX 介面的 ESP32 改接外接天線,或於程式中調整 Wi-Fi 發射功率:

WiFi.setTxPower(WIFI_POWER_19_5dBm);

 

▼【ESP32教學】教學總索引頁面