[TH] ESP-NOW

บทความกล่าวถึงการใช้การสื่อสารที่ทาง espressif พัฒนาเพื่อใช้ในการสื่อสารระหว่างไมโครคอนโทรลเลอร์ของตนเองผ่านทางการสื่อสารไร้สายซึ่งเป็นอีกทางเลือกหนึ่งของการพัฒนาระบบแบบ Client/Server โดยอธิบายขั้นตอนการทำงานของการทำงานเป็นผู้ให้บริการ ผู้สั่งการ และชุดคำสั่งที่เกี่ยวข้องผ่านทาง Arduino core พร้อมอธิบายตัวอย่างการทำงานที่มากับ Arduino core ทั้ง 2 ตัวอย่าง คือ basic และ Multi-slave ซึ่งสามารถนำไปประยุกต์ใช้งานได้หลากหลาย

ESP-NOW

ESP-NOW เป็นโปรโทคอลสื่อสารที่บริษัท espressif ผู้ผลิตไมโครคอนโทรลเลอร์ esp8266 และ esp32 เป็นผู้ออกแบบเพื่อใช้ในการสื่อสารระหว่างอุปกรณ์ผ่านทางระบบไร้สายหรือ WiFi โดยรองรับการสื่อสารแบบเข้ารหัสและไม่เข้ารหัส รองรับการส่งข้อมูล/รับข้อมูลได้สูงสุดเฟรมละ 250 ไบต์ และรองรับการใช้ callback สำหรับรับการแจ้งเตือนผลของการส่งข้อมูล แต่อย่างไรก็ดี ขึดจำกัดของ ESP-NEW มีด้วยกัน 3 ประเด็น คือ

  • เมื่อใช้การเข้ารหัส (Encryption) ในการสื่อสารจะทำงานได้เพียง 10 อุปกรณ์ในโหมด Station และเหลือเพียง 6 อุปกรณ์เมื่อทำงานโหมด SoftAP หรือ SoftAP+Station
  • การสื่อสารแบบไม่เข้ารหัสสามารถทำงานร่วมกันได้ไม่เกิน 20 อุปกรณ์
  • ข้อมูลในเฟรมของการรับ/ส่งหรือส่วนของ Payload นั้นมีขนาดได้ไม่เกิน 250 ไบต์

สถาปัตยกรรมการสื่อสารของ ESP-NOW กำหนดให้ในแต่ละวงสื่อสารมีโหนดที่เรียกว่า master หรือ controller กับ slave หรือ peer โดยระหว่าง 2 ประเภทสามารถสื่อสารได้ 2 ลักษระ คือ

  • สื่อสารทางเดียว (one-way communication) ที่ให้อุปกรณ์หนึ่งเป็นผู้ส่งและอีกอุปกรณ์เป็นผู้รับ โดยทำงานได้ 3 รูปแบบ คือ
    • 1 ผู้ส่ง และ 1 ผู้รับ (one to one)
    • 1 ผู้ส่ง หลายผู้รับ (one master, multiple slaves)
    • หลายผู้ส่ง และ 1 ผู้รับ (multiple masters, one slave)
  • การสื่อสารสองทาง (two-way communication) ที่ให้ทั้ง master และ slave สามารถเป็นทั้งผู้ส่งและผู้รับ

ชุดคำสั่ง

ชุดคำสั่งและค่าคงที่ที่ควรรู้จักมีดังต่อไปนี้

รหัสของข้อผิดพลาด

ค่าคงที่เหลือนี้เป็นผลที่คืนค่ากลับมาในประเภท esp_err_t ซึ่งเป็นประเภทข้อมูลที่คืนกลับจากชุดคำสั่งที่เกี่ยวกับ ESP-NOW

ชื่อข้อผิดพลาดความหมาย
ESP_ERR_ESPNOW_NOT_INITผู้เขียนโปรแกรมยังไม่ได้เรียก esp_now_init()
ESP_ERR_ESPNOW_ARGอาร์กิวเมนต์ที่ส่งให้คำสั่งมีความผิดพลาด
ESP_ERR_ESPNOW_NO_MEMหน่วยความจำไม่เพียงพอ
ESP_ERR_ESPNOW_FULLจำนวน Peer ในลิสต์เต็ม
ESP_ERR_ESPNOW_NOT_FOUNDไม่พบ Peer ของ ESP-NNOW
ESP_ERR_ESPNOW_INTERNALเกิดความผิดพลาดภายใน (Internal error)
ESP_ERR_ESPNOW_EXISTมี Peer นี้อยู่ก่อนแล้ว
ESP_ERR_ESPNOW_IFเกิดความผิดพลาดที่อินเทอร์เฟซ (Interface error)

กรณีที่คำสั่งทำงานสำเร็จหรือการรับ/ส่งสำเร็จจะได้ค่าเป็น ESP_OK

ค่าคงที่ภายใน ESP-NOW

รายการค่าคงที่สำหรับใช้อ้างอิงเกี่ยวกับข้อจำนวนของข้อมูล ขนาดคีย์ และจำนวน Peer มีดังนี้

  • ESP_NOW_ETH_ALEN มีค่าเป็น 6 หมายถึง ขนาดของ MAC Address ของ Peer
  • ESP_NOW_KEY_LEN มีค่าเป็น 16 หมายถึง ขนาดของกุญแจต้นฉบับ (master key) ของ Peer
  • ESP_NOW_MAX_TOTAL_PEER_NUM มีค่าเป็น20 หมายถึง จำนวนสูงสุดของ Peer
  • ESP_NOW_MAX_ENCRYPT_PEER_NUM มีค่าเป็น 6 หมายถึง จำนวนสูงสุดของ Peer เมื่อสื่อสารแบบเข้ารหัส
  • ESP_NOW_MAX_DATA_LEN มีค่าเป็น 250 หมายถึง จำนวนไบต์สูงสุดของข้อมูลที่ใช้ในการสื่อสารต่อ 1 เฟรม

ข้อมูลของ peer

โครงสร้างข้อมูลสำหรับเก็บรายละเอียดของแต่ละ Peer อยูในชื่อ esp_now_peer_info หรือ esp_now_peer_info_t ซึ่งประกอบด้วยข้อมูลดังนี้

typedef struct esp_now_peer_info {
    uint8_t peer_addr[ESP_NOW_ETH_ALEN];     
    uint8_t lmk[ESP_NOW_KEY_LEN];            
    uint8_t channel;                         
    wifi_interface_t ifidx;  
    bool encrypt;
    void *priv; 
} esp_now_peer_info_t;

จากโครงสร้างดังกล่าวจะได้ว่า

  1. peer_addr เป็นค่า MAC Address ของ Peer ซึ่งถูกใช้ในการทำงานโหมด Station หรือ SoftAP
  2. lmk เป็นค่าขนาดของคีย์ต้นฉบับ (master key) ที่ใช้เป็นตัวเข้ารหัสและถอดรหัสข้อมูล
  3. channel คือ หมายเลขช่องสื่อสารระหว่างกัน แต่ถ้ากำหนดเป็น 0 หมายถึงการใช้ช่องสื่อสารตาม Station หรือ SoftAP ที่สื่อสารด้วย สสำหรับกรณีอื่น ต้องระบุหมายเลขช่องสัญญาณให้ตรงกับ Station หรือ SoftAP ที่สื่อสาร
  4. ifidx คือ ค่าหมายเลขของส่วนเชื่อมประสานหรืออินเตอร์เฟซ (IF: Interface)
  5. encrypt ถ้าเป็น true หมายถึง การสื่อกับ peer จะถูกเข้ารหัส แต่ถ้าเป็น false เป็นการสื่อสารแบบไม่เข้ารหัส
  6. priv เป็นตัวแปรตัวชี้ที่ชี้ไปยังข้อมูลส่วนตัวของ Peer

คำสั่งเริ่มต้นทำงาน

esp_err_t esp_now_init(void);

คำสั่งนี้ใช้สำหรับเริ่มต้นการทำงานของ ESP-NOW และคืนค่า

  • ESP_OK ถ้าสำเร็จ
  • ESP_ERR_ESPNOW_INTERNAL

esp_err_t esp_now_deinit(void);

คำสั่งนี้ใช้สำหรับยกเลิกการทำงาน ESP-NOW และจะคืนค่ากลับมาเป็น ESP_OK หลังทำการยกเลิกเสร็จสิ้น

คำสั่งขอดูรุ่นของ ESP-NOW

esp_err_t esp_now_get_version(uint32_t *version);

คำสั่งนี้ใช้สำหรับอ่านหมายเลขรุ่นของ ESP-NOW โดยต้องส่งตัวแปรประเภท uint32_t และนำค่ารุ่นเก็บในหน่วยความจำที่อ้างถึง หลังจากนั้นคืนกลับค่าเป็น ESP_OK หรือ ESP_ERR_ESPNOW_ARG ถ้ามีปัญหาเรื่องขออาร์กิวเมนต์ที่ส่งให้กับฟังก์ชัน

คำสั่ง callback

รูปแบบของ callback ที่ใช้ในการกำหนดให้ถูกเรียกใช้เมื่อ ESP-NOW ได้รับข้อมูลจะต้องมีโครงสร้างของการกำหนดฟังก์ชันดังนี้

typedef void (*esp_now_recv_cb_t)(const uint8_t *mac_addr, const uint8_t *data, int data_len);

รุปแบบของฟังก์ชัน callback ที่จะถูกเรียกให้ทำงานเมื่อเกิดข้อผิดพลาดจากการส่งข้อมูลจะต้องมีโครงสร้างดังนี้

typedef void (*esp_now_send_cb_t)(const uint8_t *mac_addr, esp_now_send_status_t status);

คำสั่งสำหรับกำหนดฟังก์ชัน callback เมื่อเกิดเหตุการณ์ register send เป็นดังนี้

esp_err_t esp_now_register_send_cb(esp_now_send_cb_t cb);

จากคำสั่งจะได้ว่า cb คือ ชื่อของฟังก์ชัน callback ที่เขียนเอาไว้สำหรับถูกเรียก และการคืนค่ากลับมีผลดังรายการต่อไปนี้

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_INTERNAL

คำสั่งสำหรับกำหนดฟังก์ชัน callback ที่ต้องถูกเรียกให้ทำงานเม่อเกิดการรับข้อมูลมีรูปแบบดังนี้

esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t cb);

ค่าคืนกลับจากคำสั่งได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_INTERNAL

คำสั่งสำหรับยกเลิกการลงทะเบีนฟังก์ชัน callback ที่ถูกเรียกหลังจากเกิดการรับข้อมูลมีรูปแบบดังนี้

esp_err_t esp_now_unregister_recv_cb(void);

ค่าคืนกลับจากคำสั่งได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT

คำสั่งสำหรับขอยกเลิกการลงทะเบียนฟังก์ชัน callback สำหรับตอบสนองการส่งข้อมูลมีรูปแบบของคำสั่งดังนี้

esp_err_t esp_now_unregister_send_cb(void);

ค่าคืนกลับจากคำสั่งได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT

คำสั่งส่งข้อมูล

คำสั่งสำหรับส่งข้อมูล data ขนาด len ไปให้ peer ที่มีค่าตำแหน่งเป็น peer_addr มีรูปแบบการใช้งานดังนี้

esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len);

ค่าคืนกลับของการทำงานได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG
  • ESP_ERR_ESPNOW_INTERNAL
  • ESP_ERR_ESPNOW_NO_MEM
  • ESP_ERR_ESPNOW_NOT_FOUND
  • ESP_ERR_ESPNOW_IF

คำสั่งเกี่ยวกับ peer

คำสั่งสำหรับเพิ่ม peer เข้าในระบบมีรูปแบบของการใช้งานดังนี้

esp_err_t esp_now_add_peer(const esp_now_peer_info_t *peer);

ค่าคืนกลับของฟังก์ชันเพิ่ม peer ได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG
  • ESP_ERR_ESPNOW_FULL
  • ESP_ERR_ESPNOW_NO_MEM
  • ESP_ERR_ESPNOW_EXIST

กรณีที่ต้องการนำออก peer ที่ไม่ต้องการโดยต้องระบุ peer_addr มีรูปของการใช้งานดังนี้

esp_err_t esp_now_del_peer(const uint8_t *peer_addr);

เมื่อทำงานเสร็จจะรายงานผลของการทำงาน คือ

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG
  • ESP_ERR_ESPNOW_NOT_FOUND

กรณีที่ต้องการเปลี่ยนแปลงข้อมูลของ peer ต้องส่งค่าข้อมูลชุดใหม่ให้กับฟังก์ชันต่อไปนี้

esp_err_t esp_now_mod_peer(const esp_now_peer_info_t *peer);

ค่าคืนกลับจากฟังก์ชัน esp_now_mod_peer() ได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG
  • ESP_ERR_ESPNOW_FULL

สำหรับการอ่านข้อมูล peer ที่มีค่าตำแหน่งเป็น peer_addr ต้องเตรียมบัฟเฟอร์สำหรับเก็บ esp_now_peer_info_t และส่งค่าตำแหน่งไปให้กับคำสั่งต่อไปนี้เพื่ออ่านข้อมูลของ peer ที่ระบุมาเก็บในบัฟเฟอร์ที่เตรียมไว้

esp_err_t esp_now_get_peer(const uint8_t *peer_addr, esp_now_peer_info_t *peer);

ค่าผลลัพธ์ของการทำงานเป็นดังนี้

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG
  • ESP_ERR_ESPNOW_NOT_FOUND

กรณีที่ไม่ทราบหมายเลขของ peer ที่ต้องการอ่านข้อมูล ผู้เขียนโปรแกรมสามารถทำการอ่านจากลิสต์ของ peer ได้ด้วยคำสั่งต่อไปนี้ เพื่อเข้าถึงทีละรายการ โดยการกำหนดให้ from_head เป็น true หมายถึงการอ่านรายการแรกจากลิสต์ และถ้ากำหนดเป็น false จะเป็นรายการถัดไปเรื่อย ๆ ในทุกครั้งที่เรียกใช้

esp_err_t esp_now_fetch_peer(bool from_head, esp_now_peer_info_t *peer);

ค่าคืนกลับจาก esp_now_fetch_peer() มีดังนี้

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG
  • ESP_ERR_ESPNOW_NOT_FOUND

การตรวจสอบว่ามี peer_addr อยู่ในลิสตต์ของ peer หรือไม่ โดยได้ผลลัพธ์จากการทำงานเป็น true หรือ false มีรูปแบบของคำสั่งใช้งานดังนี้

bool esp_now_is_peer_exist(const uint8_t *peer_addr);

กรณีที่ต้องการทราบจำนวน peer ที่เพิ่มเข้ามาในระบบ สามารถสั่งนับจำนวนของ peer ด้วยคำสั่ง esp_now_get_peer_num() ดดยส่งค่าตำแหน่งของตัวแปรเก็บจำนวน peer ให้กับฟังก์ชั้นต่อไปนี้

esp_err_t esp_now_get_peer_num(esp_now_peer_num_t *num);

ผลลัพธ์ของการทำงานของฟังก์ชัน หรือค่าคืนกลับจากฟังก์ชันนั้นได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG

คำสั่งเกี่ยวกับ master

คำสั่งสำหรับตั้งค่า PMK (Primary Master Key) ซึ่งเป็นคีย์ของการเข้าและถอดรหัสของการสื่อสารภายในเครือข่ายของ ESP-NOW มีรูปแบบการใช้งานดังนี้

esp_err_t esp_now_set_pmk(const uint8_t *pmk);

ค่าคืนกลับจากการกำหนดค่ารหัสของการเข้าและถอดรหัสได้แก่

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT
  • ESP_ERR_ESPNOW_ARG

การตั้งค่าสำหรับการออกจากโหมดประหยัดพลังงานโดยการกำหนดค่า window ในหน่วยไมโครวินาที ซึ่งอยู่ในช่วง 0 ถึง 65535 เพื่อให้ชิพถูกปลุกในทุก window ไมโครวินาทีนั้นมีรูปแบบการใช้งานดังนี้

esp_err_t esp_now_set_wake_window(uint16_t window);

ค่าคืนกลับของฟังก์ชันตั้งค่าระยะเวลาในการปลุกให้ชิพทำงานมีดังนี้

  • ESP_OK
  • ESP_ERR_ESPNOW_NOT_INIT

หมายเหตุ

  1. มีผลเฉพาะเมื่อเปิดโหมดการทำงาน ESP_WIFI_STA_DISCONNECTED_PM_ENABLE
  2. การกำหนดค่านี้ทำได้เฉพาะกับโหมด Station และเมื่ออยู่ในสถานไม่ได้เชื่อมต่อ
  3. หากมีการเรียกตั้งค่าหลายโมดูลชิพจะเลือกค่าที่มากที่สุดเป็นค่าที่ถูกนำมาใช้งาน
  4. หากมีช่องว่างระหว่างเวลากับค่า window น้อกว่า 5ms จะหมายถึงชิพนั้นทำงานตลอดเวลา
  5. ถ้าไม่มีการกำหนดค่า window จะทำให้ชิพถูกปลุกเมื่อไม่มีการเชื่อมต่อเมื่อใช้ ESP-NOW

ตัวอย่างโปรแกรม

ในชุด Arduino core for ESP32 มีตัวอย่างให้ศึกษา 2 ตัวอย่าง คือ

ขั้นตอนวิธีการเขียนโปรแกรมฝั่ง master

  • ESPNow Init on Master and set it in STA mode
  • Start scanning for Slave ESP32 (we have added a prefix of `slave` to the SSID of slave for an easy setup)
  • Once found, add Slave as peer
  • Register for send callback
  • Start Transmitting data from Master to Slave

ขั้นตอนวิธีการเขียนโปรแกรมฝั่ง slave

  • ESPNow Init on Slave
  • Update the SSID of Slave with a prefix of `slave`
  • Set Slave in AP mode
  • Register for receive callback and wait for data
  • Once data arrives, print it in the serial monitor

ตัวอย่าง Basic

ตัวอย่าง Basic ประกอบไปด้วยโปรแกรมฝั่ง master และ slave ที่ใช้ทำการสื่อสารระหว่างกัน โดย master ทำการตรวจสอบว่ามี slave ในระบบหรือไม่ และเมื่อพบจะรายงานข้อมูลที่ได้รับจาก slave ส่วนฝั่ง slave ทำการลงทะเบียนเข้าระบบและส่งข้อมูลออกมา ตัวอย่างการทำงานเป็นดังภาพที่ 1

โค้ดของ Master

/**
   ESPNOW - Basic communication - Master
   Date: 26th September 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: ESPNow Communication between a Master ESP32 and a Slave ESP32
   Description: This sketch consists of the code for the Master module.
   Resources: (A bit outdated)
   a. https://espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf
   b. http://www.esploradores.com/practica-6-conexion-esp-now/

   << This Device Master >>

   ..

   Note: Master and Slave have been defined to easily understand the setup.
         Based on the ESPNOW API, there is no concept of Master and Slave.
         Any devices can act as master or salve.
*/

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

// Global copy of slave
esp_now_peer_info_t slave;
#define CHANNEL 3
#define PRINTSCANRESULTS 0
#define DELETEBEFOREPAIR 0

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// Scan for slaves in AP mode
void ScanForSlave() {
  int8_t scanResults = WiFi.scanNetworks();
  // reset on each scan
  bool slaveFound = 0;
  memset(&slave, 0, sizeof(slave));

  Serial.println("");
  if (scanResults == 0) {
    Serial.println("No WiFi devices in AP Mode found");
  } else {
    Serial.print("Found "); Serial.print(scanResults); Serial.println(" devices ");
    for (int i = 0; i < scanResults; ++i) {
      // Print SSID and RSSI for each device found
      String SSID = WiFi.SSID(i);
      int32_t RSSI = WiFi.RSSI(i);
      String BSSIDstr = WiFi.BSSIDstr(i);

      if (PRINTSCANRESULTS) {
        Serial.print(i + 1);
        Serial.print(": ");
        Serial.print(SSID);
        Serial.print(" (");
        Serial.print(RSSI);
        Serial.print(")");
        Serial.println("");
      }
      delay(10);
      // Check if the current device starts with `Slave`
      if (SSID.indexOf("Slave") == 0) {
        // SSID of interest
        Serial.println("Found a Slave.");
        Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
        // Get BSSID => Mac Address of the Slave
        int mac[6];
        if ( 6 == sscanf(BSSIDstr.c_str(), "%x:%x:%x:%x:%x:%x",  &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
          for (int ii = 0; ii < 6; ++ii ) {
            slave.peer_addr[ii] = (uint8_t) mac[ii];
          }
        }

        slave.channel = CHANNEL; // pick a channel
        slave.encrypt = 0; // no encryption

        slaveFound = 1;
        // we are planning to have only one slave in this example;
        // Hence, break after we find one, to be a bit efficient
        break;
      }
    }
  }

  if (slaveFound) {
    Serial.println("Slave Found, processing..");
  } else {
    Serial.println("Slave Not Found, trying again.");
  }

  // clean up ram
  WiFi.scanDelete();
}

// Check if the slave is already paired with the master.
// If not, pair the slave with master
bool manageSlave() {
  if (slave.channel == CHANNEL) {
    if (DELETEBEFOREPAIR) {
      deletePeer();
    }

    Serial.print("Slave Status: ");
    // check if the peer exists
    bool exists = esp_now_is_peer_exist(slave.peer_addr);
    if ( exists) {
      // Slave already paired.
      Serial.println("Already Paired");
      return true;
    } else {
      // Slave not paired, attempt pair
      esp_err_t addStatus = esp_now_add_peer(&slave);
      if (addStatus == ESP_OK) {
        // Pair success
        Serial.println("Pair success");
        return true;
      } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
        // How did we get so far!!
        Serial.println("ESPNOW Not Init");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_ARG) {
        Serial.println("Invalid Argument");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_FULL) {
        Serial.println("Peer list full");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
        Serial.println("Out of memory");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
        Serial.println("Peer Exists");
        return true;
      } else {
        Serial.println("Not sure what happened");
        return false;
      }
    }
  } else {
    // No slave found to process
    Serial.println("No Slave found to process");
    return false;
  }
}

void deletePeer() {
  esp_err_t delStatus = esp_now_del_peer(slave.peer_addr);
  Serial.print("Slave Delete Status: ");
  if (delStatus == ESP_OK) {
    // Delete success
    Serial.println("Success");
  } else if (delStatus == ESP_ERR_ESPNOW_NOT_INIT) {
    // How did we get so far!!
    Serial.println("ESPNOW Not Init");
  } else if (delStatus == ESP_ERR_ESPNOW_ARG) {
    Serial.println("Invalid Argument");
  } else if (delStatus == ESP_ERR_ESPNOW_NOT_FOUND) {
    Serial.println("Peer not found.");
  } else {
    Serial.println("Not sure what happened");
  }
}

uint8_t data = 0;
// send data
void sendData() {
  data++;
  const uint8_t *peer_addr = slave.peer_addr;
  Serial.print("Sending: "); Serial.println(data);
  esp_err_t result = esp_now_send(peer_addr, &data, sizeof(data));
  Serial.print("Send Status: ");
  if (result == ESP_OK) {
    Serial.println("Success");
  } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
    // How did we get so far!!
    Serial.println("ESPNOW not Init.");
  } else if (result == ESP_ERR_ESPNOW_ARG) {
    Serial.println("Invalid Argument");
  } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
    Serial.println("Internal Error");
  } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
    Serial.println("ESP_ERR_ESPNOW_NO_MEM");
  } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
    Serial.println("Peer not found.");
  } else {
    Serial.println("Not sure what happened");
  }
}

// callback when data is sent from Master to Slave
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Sent to: "); Serial.println(macStr);
  Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

void setup() {
  Serial.begin(115200);
  //Set device in STA mode to begin with
  WiFi.mode(WIFI_STA);
  Serial.println("ESPNow/Basic/Master Example");
  // This is the mac address of the Master in Station Mode
  Serial.print("STA MAC: "); Serial.println(WiFi.macAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_register_send_cb(OnDataSent);
}

void loop() {
  // In the loop we scan for slave
  ScanForSlave();
  // If Slave is found, it would be populate in `slave` variable
  // We will check if `slave` is defined and then we proceed further
  if (slave.channel == CHANNEL) { // check if slave channel is defined
    // `slave` is defined
    // Add slave as peer if it has not been added already
    bool isPaired = manageSlave();
    if (isPaired) {
      // pair success or already paired
      // Send data to device
      sendData();
    } else {
      // slave pair failed
      Serial.println("Slave pair failed!");
    }
  }
  else {
    // No slave found to process
  }

  // wait for 3seconds to run the logic again
  delay(3000);
}

โค้ดของ slave

/**
   ESPNOW - Basic communication - Slave
   Date: 26th September 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: ESPNow Communication between a Master ESP32 and a Slave ESP32
   Description: This sketch consists of the code for the Slave module.
   Resources: (A bit outdated)
   a. https://espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf
   b. http://www.esploradores.com/practica-6-conexion-esp-now/

   << This Device Slave >>

  ...

   Note: Master and Slave have been defined to easily understand the setup.
         Based on the ESPNOW API, there is no concept of Master and Slave.
         Any devices can act as master or salve.
*/

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

#define CHANNEL 1

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// config AP SSID
void configDeviceAP() {
  const char *SSID = "Slave_1";
  bool result = WiFi.softAP(SSID, "Slave_1_Password", CHANNEL, 0);
  if (!result) {
    Serial.println("AP Config failed.");
  } else {
    Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESPNow/Basic/Slave Example");
  //Set device in AP mode to begin with
  WiFi.mode(WIFI_AP);
  // configure device AP mode
  configDeviceAP();
  // This is the mac address of the Slave in AP Mode
  Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info.
  esp_now_register_recv_cb(OnDataRecv);
}

// callback when data is recv from Master
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Recv from: "); Serial.println(macStr);
  Serial.print("Last Packet Recv Data: "); Serial.println(*data);
  Serial.println("");
}

void loop() {
  // Chill
}
ภาพที่ 1 ตัวอย่างของโปรแกรม basic

ตัวอย่าง Multi-Slave

โค้ดของ Master

/**
   ESPNOW - Basic communication - Slave
   Date: 26th September 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: ESPNow Communication between a Master ESP32 and a Slave ESP32
   Description: This sketch consists of the code for the Slave module.
   Resources: (A bit outdated)
   a. https://espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf
   b. http://www.esploradores.com/practica-6-conexion-esp-now/

   << This Device Slave >>

   ...

   Note: Master and Slave have been defined to easily understand the setup.
         Based on the ESPNOW API, there is no concept of Master and Slave.
         Any devices can act as master or salve.
*/

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

#define CHANNEL 1

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// config AP SSID
void configDeviceAP() {
  const char *SSID = "Slave_1";
  bool result = WiFi.softAP(SSID, "Slave_1_Password", CHANNEL, 0);
  if (!result) {
    Serial.println("AP Config failed.");
  } else {
    Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESPNow/Basic/Slave Example");
  //Set device in AP mode to begin with
  WiFi.mode(WIFI_AP);
  // configure device AP mode
  configDeviceAP();
  // This is the mac address of the Slave in AP Mode
  Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info.
  esp_now_register_recv_cb(OnDataRecv);
}

// callback when data is recv from Master
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Recv from: "); Serial.println(macStr);
  Serial.print("Last Packet Recv Data: "); Serial.println(*data);
  Serial.println("");
}

void loop() {
  // Chill
}

โค้ดของ Slave

/**
   ESPNOW - Basic communication - Slave
   Date: 26th September 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: ESPNow Communication between a Master ESP32 and multiple ESP32 Slaves
   Description: This sketch consists of the code for the Slave module.
   Resources: (A bit outdated)
   a. https://espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf
   b. http://www.esploradores.com/practica-6-conexion-esp-now/

   << This Device Slave >>

   ...

   Note: Master and Slave have been defined to easily understand the setup.
         Based on the ESPNOW API, there is no concept of Master and Slave.
         Any devices can act as master or salve.
*/

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

#define CHANNEL 1

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// config AP SSID
void configDeviceAP() {
  String Prefix = "Slave:";
  String Mac = WiFi.macAddress();
  String SSID = Prefix + Mac;
  String Password = "123456789";
  bool result = WiFi.softAP(SSID.c_str(), Password.c_str(), CHANNEL, 0);
  if (!result) {
    Serial.println("AP Config failed.");
  } else {
    Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESPNow/Basic/Slave Example");
  //Set device in AP mode to begin with
  WiFi.mode(WIFI_AP);
  // configure device AP mode
  configDeviceAP();
  // This is the mac address of the Slave in AP Mode
  Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info.
  esp_now_register_recv_cb(OnDataRecv);
}

// callback when data is recv from Master
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Recv from: "); Serial.println(macStr);
  Serial.print("Last Packet Recv Data: "); Serial.println(*data);
  Serial.println("");
}

void loop() {
  // Chill
}

สรุป

จากบทความนี้จะพบว่า ESP-NOW รองรับการสื่อสารทั้งแบบ One-way และ Two-way ทำให้ออกแบบการเขียนโปรแกรมได้หลากหลาย และเมื่อพิจารณาเรื่องของปริมาณข้อมูลที่จำกัดไว้ 250 ไบต์ และไม่มีรูปแบบ Protocol ที่ซับซ้อนเหมือนตอนใช้ ESP8266WiFi/WiFiClient/WiFiServer พร้อมทั้งสามารถเพิ่มความปลอดภัยด้วยการเข้ารหัสทำให้การสื่อสารนั้นรวดเร็วกวาการทำในบทความ “การเขียนโปรแกรมไคลเอนต์/เซิร์ฟเวอร์สำหรับสถานีอากาศผ่านระบบเครือข่ายไร้สาย” นอกจากนี้ ในการทดลองและใช้งานโดยใช้กับ ESP32 และ ESP32-S2 สามารถทำงานร่วมกันได้ดี สุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารคอมเมนท์ได้เลยครับ

แหล่งอ้างอิง

  1. espressif : API Reference : Networking APIs : ESP-NOW
  2. Arduino core for ESP32 (ESP32): esp_now.h
  3. Arduino core for ESP32 (ESP32-S2): esp_now.h
  4. Arduino core for ESP32 (ESP32-C3): esp_now.h
  5. ตัวอย่าง ESP-NOW : Basic, Multi-Slave

(C) 2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-09-30, 2021-12-13, 2024-04-05