ESP32 ESP-NOW
10 ใบงานจากง่ายไปยาก

เรียนรู้การเชื่อมต่อไร้สายผ่านโปรโตคอล ESP-NOW ของ ESP32 ตั้งแต่พื้นฐานไปจนถึงการแสดงผลผ่านเว็บ และการประหยัดพลังงาน

1 การส่งข้อมูลพื้นฐานทางเดียว (Basic Sender)

วัตถุประสงค์: เพื่อเรียนรู้วิธีการกำหนดค่าเริ่มต้นของ ESP-NOW และส่งข้อความตัวอักษรอย่างง่ายไปยังอุปกรณ์ปลายทาง

ในการใช้งาน ESP-NOW เราต้องตั้งค่า WiFi ให้อยู่ในโหมด Station (WIFI_STA) จากนั้นทำการเพิ่ม Peer (อุปกรณ์ปลายทาง) ด้วย MAC Address

โค้ดสำหรับผู้ส่ง (Sender)

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

// กำหนด MAC Address ของตัวรับ (เปลี่ยนให้ตรงกับบอร์ดรับของคุณ)
uint8_t receiverAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// ฟังก์ชัน Callback เมื่อส่งข้อมูลเสร็จ
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("Send Status: ");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA); // ตั้งเป็นโหมด Station

  // เริ่มต้น ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // ลงทะเบียนฟังก์ชัน Callback
  esp_now_register_send_cb(OnDataSent);

  // ลงทะเบียน Peer
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, receiverAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }
}

void loop() {
  String message = "Hello ESP-NOW!";
  // ส่งข้อมูล
  esp_now_send(receiverAddress, (uint8_t *)message.c_str(), message.length());
  delay(2000);
}

2 การรับข้อมูลพื้นฐาน (Basic Receiver)

วัตถุประสงค์: เพื่อเรียนรู้วิธีการรับข้อความจากใบงานที่ 1 และแสดงผลออกทาง Serial Monitor

ตัวรับจำเป็นต้องสร้างฟังก์ชัน Callback สำหรับรับข้อมูล (`esp_now_register_recv_cb`) ซึ่งจะถูกเรียกใช้อัตโนมัติเมื่อมีข้อมูลเข้ามา

โค้ดสำหรับผู้รับ (Receiver)

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

// ฟังก์ชัน Callback เมื่อได้รับข้อมูล
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  char message[len + 1];
  memcpy(message, incomingData, len);
  message[len] = '\0'; // ปิดท้ายด้วย null character
  
  Serial.print("Received Message: ");
  Serial.println(message);
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  
  // พิมพ์ MAC Address ของบอร์ดนี้ เพื่อเอาไปใส่ในโค้ดตัวส่ง (ใบงานที่ 1)
  Serial.print("My MAC Address: ");
  Serial.println(WiFi.macAddress());

  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // ลงทะเบียนฟังก์ชัน Callback สำหรับการรับข้อมูล
  esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
  // ไม่ต้องเขียนโค้ดใน loop ระบบจะทำงานผ่าน Callback อัตโนมัติ
}

3 สื่อสารสองทาง (Two-way Communication)

วัตถุประสงค์: เพื่อให้ ESP32 สองบอร์ดสามารถส่งและรับข้อมูลโต้ตอบกันได้ (Ping-Pong)

ใช้เทคนิคการลงทะเบียน Callback ทั้ง `send` และ `recv` ในบอร์ดเดียวกัน เมื่อบอร์ดที่ 1 ส่งไป บอร์ดที่ 2 รับได้แล้วให้ส่งข้อความตอบกลับทันที

โครงสร้างโค้ด (ใช้ร่วมกันทั้ง 2 บอร์ด สลับ MAC Address)

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

uint8_t targetAddress[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; // เปลี่ยนให้ตรงกับอีกบอร์ด

void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Sent OK" : "Sent Fail");
}

void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  Serial.println("Received a message! Sending ACK...");
  String ackMsg = "ACK!";
  // ส่งข้อความตอบกลับ
  esp_now_send(mac, (uint8_t *)ackMsg.c_str(), ackMsg.length());
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  esp_now_init();
  
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);
  
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, targetAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  esp_now_add_peer(&peerInfo);
}

void loop() {
  // ส่งข้อมูลทุกๆ 5 วินาที
  String msg = "Ping";
  esp_now_send(targetAddress, (uint8_t *)msg.c_str(), msg.length());
  delay(5000);
}

4 การส่งข้อมูลแบบโครงสร้าง (Sending Struct Data)

วัตถุประสงค์: ส่งข้อมูลหลายชนิดพร้อมกันในแพ็กเกจเดียว เช่น อุณหภูมิ (float), ความชื้น (int) และสถานะ (boolean)

การส่งข้อมูลด้วย `struct` เป็นวิธีที่นิยมที่สุดใน ESP-NOW เพราะทำให้ข้อมูลเป็นระเบียบและไม่ต้องแปลงเป็น String ให้ยุ่งยาก

การสร้างโครงสร้างข้อมูล (C/C++ Struct)

// โครงสร้างข้อมูล ต้องเหมือนกันทั้งตัวส่งและตัวรับ
typedef struct struct_message {
  int id;          // ไอดีของ Node
  float temp;      // อุณหภูมิ
  float hum;       // ความชื้น
  bool ledState;   // สถานะ LED
} struct_message;

struct_message myData;

// เวลาส่ง (ฝั่ง Sender)
myData.id = 1;
myData.temp = 25.5;
myData.hum = 60.0;
myData.ledState = true;
esp_now_send(targetAddress, (uint8_t *) &myData, sizeof(myData));

// เวลารับ (ฝั่ง Receiver Callback)
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  struct_message receivedData;
  memcpy(&receivedData, incomingData, sizeof(receivedData));
  
  Serial.print("Node ID: "); Serial.println(receivedData.id);
  Serial.print("Temp: "); Serial.println(receivedData.temp);
}

5 การส่งข้อมูลแบบกระจาย (Broadcast)

วัตถุประสงค์: ส่งข้อความเดียวให้เข้าถึงทุกอุปกรณ์ ESP32 ที่อยู่ในบริเวณนั้นโดยไม่ต้องรู้ MAC Address ของผู้รับ

ใช้ MAC Address FF:FF:FF:FF:FF:FF เป็นที่อยู่ปลายทาง อุปกรณ์รับที่อยู่ในโหมด ESP-NOW จะได้รับข้อมูลทั้งหมด

// กำหนด MAC Address เป็น Broadcast
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// การตั้งค่า Peer จะเหมือนปกติ แต่ใช้ Broadcast MAC
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;  
peerInfo.encrypt = false;
esp_now_add_peer(&peerInfo);

// คำสั่งส่งข้อมูล
String alertMsg = "EMERGENCY ALERT!";
esp_now_send(broadcastAddress, (uint8_t *)alertMsg.c_str(), alertMsg.length());

6 เครือข่าย หลายตัวส่ง-หนึ่งตัวรับ (Many-to-One)

วัตถุประสงค์: สร้างระบบ Smart Home ที่มี Sensor Nodes หลายจุด ส่งข้อมูลมารวมที่ Hub ศูนย์กลางตัวเดียว

Node ทุกตัวจะส่งข้อมูล `struct` ที่มี `id` ของตัวเองมาให้ Hub ส่วน Hub จะใช้ Callback เดียวในการรับข้อมูลและแยกแยะว่ามาจาก Node ไหนโดยดูจาก `id` หรือ MAC Address

โค้ดส่วนรับข้อมูล (ฝั่ง Hub)

typedef struct sensor_data {
  int node_id;
  float temperature;
} sensor_data;

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
  sensor_data data;
  memcpy(&data, incomingData, sizeof(data));
  
  Serial.printf("Received from MAC: %02X:%02X:%02X:%02X:%02X:%02X \n", 
                 mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
                 
  Serial.print("Node ID: ");
  Serial.println(data.node_id);
  Serial.print("Temperature: ");
  Serial.println(data.temperature);
  Serial.println("-----------------");
}

7 ควบคุมอุปกรณ์ One-to-Many

วัตถุประสงค์: สร้างรีโมท 1 ตัว สั่งเปิด/ปิดไฟ (LED หรือ Relay) บนอุปกรณ์ปลายทางหลายๆ ตัวแยกกัน

Master ต้อง Add Peer ของ Node ทุกตัว (สูงสุด 20 ตัว) และเมื่อต้องการสั่งการตัวไหน ให้ส่งข้อมูลไปยัง MAC Address ของตัวนั้น

uint8_t node1[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x11};
uint8_t node2[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x22};

typedef struct control_msg {
  bool relayState;
} control_msg;

control_msg cmd;

// สั่ง Node 1 เปิดไฟ
cmd.relayState = true;
esp_now_send(node1, (uint8_t *) &cmd, sizeof(cmd));

delay(1000);

// สั่ง Node 2 ปิดไฟ
cmd.relayState = false;
esp_now_send(node2, (uint8_t *) &cmd, sizeof(cmd));

8 แสดงผลผ่าน Web Server (ESP-NOW + WiFi) 🌐

วัตถุประสงค์: นำข้อมูลที่รับผ่าน ESP-NOW มาแสดงผลบนหน้าเว็บไซต์ (HTML) แบบ Real-time

ใบงานนี้ถือเป็นจุดเปลี่ยนสำคัญ เราจะให้ ESP32 ตัวกลาง (Hub) ทำงานทั้ง 2 โหมดคือ WIFI_AP_STA เพื่อรับข้อมูล ESP-NOW และปล่อย WiFi Hotspot เพื่อให้สมาร์ทโฟนเชื่อมต่อเข้ามาดูหน้าเว็บ index.html

โค้ดฝั่ง Hub (ใช้ไลบรารี ESPAsyncWebServer)

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

float currentTemp = 0.0;
AsyncWebServer server(80);

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="2"> <!-- รีเฟรชทุก 2 วินาที -->
  <style>
    body { font-family: Arial; text-align: center; margin-top: 50px; background: #222; color: #fff;}
    .data { font-size: 3rem; color: #3b82f6; }
  </style>
</head>
<body>
  <h2>ESP-NOW Sensor Dashboard</h2>
  <p>Temperature: <span class="data">%TEMP%</span> &deg;C</p>
</body>
</html>)rawliteral";

String processor(const String& var){
  if(var == "TEMP") return String(currentTemp);
  return String();
}

void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(¤tTemp, incomingData, sizeof(currentTemp));
}

void setup() {
  Serial.begin(115200);
  
  // โหมด AP (สร้างวง WiFi) + STA (รับ ESP-NOW)
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP("ESP_NOW_HUB", "12345678");

  esp_now_init();
  esp_now_register_recv_cb(OnDataRecv);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });
  
  server.begin();
}
void loop() {}

9 การเข้ารหัสข้อมูล (Encryption)

วัตถุประสงค์: รักษาความปลอดภัยของข้อมูลเซนเซอร์ ไม่ให้ผู้ไม่หวังดีแอบอ่านข้อมูลหรือดักจับกลางทางได้

ESP-NOW รองรับการเข้ารหัสแบบ CCMP เราต้องกำหนด PMK (Primary Master Key) ซึ่งต้องเหมือนกันทั้งระบบ และ LMK (Local Master Key) สำหรับแต่ละคู่ Peer

// คีย์ความยาว 16 ไบต์ (Characters)
const char *PMK = "my_secret_pmk123";
const char *LMK = "my_secret_lmk123";

// ตั้งค่า PMK (ทำทั้งตัวส่งและตัวรับ)
esp_now_set_pmk((uint8_t *)PMK);

// การตั้งค่าเพิ่ม Peer แบบเข้ารหัส
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, targetAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true; // เปิดใช้งาน Encryption
memcpy(peerInfo.lmk, LMK, 16); // ใส่ LMK

esp_now_add_peer(&peerInfo);

10 ประหยัดพลังงาน (Deep Sleep + ESP-NOW) 🔋

วัตถุประสงค์: สร้างเซนเซอร์ทำงานด้วยถ่านก้อนเล็กๆ ที่สามารถอยู่ได้หลายเดือนถึงเป็นปี

เนื่องจาก ESP-NOW ส่งข้อมูลได้รวดเร็วมาก (ระดับมิลลิวินาที) เราจึงให้อุปกรณ์อยู่ในโหมด Deep Sleep ตลอดเวลา เมื่อถึงเวลาก็ตื่นขึ้นมาอ่านค่า ส่งข้อมูล แล้วกลับไปนอนทันที

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

#define uS_TO_S_FACTOR 1000000ULL  
#define TIME_TO_SLEEP  60        // นอนหลับ 60 วินาที

uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

void setup() {
  Serial.begin(115200);
  
  // 1. ตื่นขึ้นมา เปิด WiFi
  WiFi.mode(WIFI_STA);
  esp_now_init();
  
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  esp_now_add_peer(&peerInfo);

  // 2. อ่านค่าเซนเซอร์ และส่งข้อมูล
  String msg = "Sensor Data...";
  esp_now_send(broadcastAddress, (uint8_t *)msg.c_str(), msg.length());
  
  // หน่วงเวลาเล็กน้อยให้ส่งข้อมูลเสร็จ
  delay(100);

  // 3. ปิด WiFi และเข้าโหมด Deep Sleep
  WiFi.mode(WIFI_OFF);
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("Going to sleep now");
  Serial.flush(); 
  esp_deep_sleep_start();
}

void loop() {
  // จะไม่ถูกเรียกใช้งานเมื่อใช้ Deep Sleep
}