ระบบควบคุมผ่านเครือข่ายภายใน (Local Network Control & Network Layer)

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

โหมดการทำงานของ Wi-Fi บน ESP32

ESP32 มีความพิเศษตรงที่มีชิป Wi-Fi ฝังมาในตัว ทำให้มันสามารถเลือกสวมบทบาทในเครือข่ายได้ 2 รูปแบบหลักๆ ขึ้นอยู่กับหน้างานที่เราไปติดตั้ง

1. Station Mode (STA) – โหมด ลูกข่าย

  • หลักการทำงาน ในโหมดนี้ ESP32 จะทำตัวเหมือนสมาร์ทโฟนของนักศึกษา คือมันจะต้องวิ่งไปเกาะสัญญาณ Wi-Fi จาก Router ที่มีอยู่แล้วในพื้นที่
  • เมื่อไหร่ควรใช้ ใช้เมื่อสถานที่นั้น เช่น ฟาร์ม โรงเรือน บ้าน มี Router Wi-Fi และอินเทอร์เน็ตอยู่แล้ว
  • ข้อดี เมื่อ ESP32 เชื่อมต่อ Router ได้ มันจะสามารถส่งข้อมูลขึ้น Cloud และเราสามารถสั่งงานจากที่ไหนก็ได้ในโลกผ่านอินเทอร์เน็ต
  • ภาพจำ ESP32 = แขกที่ไปร่วมงานปาร์ตี้ (ต้องอาศัยบ้านคนอื่น)

2. Access Point Mode (AP) – โหมด ปล่อยสัญญาณ

  • หลักการทำงาน ในโหมดนี้ ESP32 จะแปลงร่างตัวเองเป็น Router ตัวจิ๋ว ที่คอยปล่อยสัญญาณ Wi-Fi ออกมาเอง (มีชื่อ Wi-Fi หรือ SSID เป็นของตัวเอง) เพื่อให้อุปกรณ์อื่นวิ่งมาเชื่อมต่อ
  • เมื่อไหร่ควรใช้ ใช้เมื่อต้องนำ ESP32 ไปติดตั้งในพื้นที่ห่างไกล เช่น กลางทุ่งนา สวนป่า จุดที่ไม่มีสัญญาณอินเทอร์เน็ตเข้าถึง
  • ข้อจำกัด เนื่องจากมันไม่ได้ต่ออินเทอร์เน็ต การสั่งงานหรือดูข้อมูลจะต้องทำในระยะใกล้ๆเท่านั้น (เดินไปใกล้ๆบอร์ด เอาสมาร์ทโฟนต่อ Wi-Fi ที่ ESP32 ปล่อยออกมา แล้วกดสั่งงานผ่านหน้าเว็บ)
  • ภาพจำ ESP32 = เจ้าภาพจัดปาร์ตี้ (สร้างเครือข่ายเล็กๆของตัวเอง)

💡 Note สำหรับนักศึกษา ในการเขียนโปรแกรมจริง เราสามารถสั่งให้ ESP32 ทำงานแบบ AP+STA (ทำทั้ง 2 โหมดพร้อมกัน) ได้ เช่น ให้บอร์ดเชื่อมต่อ Wi-Fi บ้านด้วย และปล่อย Wi-Fi ตัวเองเผื่อไว้ตั้งค่าฉุกเฉินด้วย


พื้นฐาน HTTP Protocol (ภาษาที่เว็บใช้คุยกัน)

เมื่อ ESP32 เชื่อมต่อกับเครือข่ายได้แล้ว ขั้นตอนต่อไป คือ การสื่อสารระหว่างกัน

โปรโตคอล (Protocol) หมายถึง ข้อตกลง หรือ กฎระเบียบมาตรฐาน ที่อุปกรณ์ต่างๆใช้เพื่อสื่อสารและแลกเปลี่ยนข้อมูลกันให้เข้าใจตรงกัน ซึ่งโปรโตคอลที่เป็นมาตรฐานที่สุดในการสั่งงานผ่านหน้าเว็บ คือ HTTP (Hypertext Transfer Protocol) การสื่อสารในระบบเครือข่ายมักจะแบ่งบทบาทผู้เล่นเป็น 2 ฝั่งเสมอ

  • Client (ผู้ร้องขอ) คือ อุปกรณ์ของผู้ใช้ เช่น สมาร์ทโฟน คอมพิวเตอร์ มีหน้าที่หลักในการ ส่งคำขอ (Request) ไปหาเป้าหมาย
  • Server (ผู้ให้บริการ) ในโปรเจกต์ของเรา ESP32 จะรับบทเป็น Server มีหน้าที่หลักในการรอฟังคำขอ เมื่อ Client สั่งอะไรมา ESP32 ก็จะประมวลผล แล้ว ส่งคำตอบกลับไป (Response)

ตัวอย่างเหตุการณ์

  • Client (มือถือ) “นี่ ESP32 ขอดูหน้าเว็บควบคุมฟาร์มหน่อยสิ!” (ส่ง Request)
  • Server (ESP32) “ได้เลย นี่โค้ดหน้าเว็บควบคุมฟาร์ม เอาไปแสดงผลบนจอเลย” (ส่ง Response)

รูปแบบการส่งคำขอ (Request Methods)

เวลา Client ส่งคำขอ (Request) ไปหา Server มันจะมีจุดประสงค์ที่ต่างกันออกไป ที่นิยมใช้มากที่สุดมี 2 แบบ คือ GET และ POST

✅ GET (การขอข้อมูล)

  • จุดประสงค์ ใช้เมื่อต้องการดึงข้อมูลหรืออ่านข้อมูลจาก Server โดยไม่ได้ต้องการไปเปลี่ยนแปลงค่าอะไรที่สำคัญ
  • ตัวอย่างในฟาร์ม
    • Client สั่ง GET เพื่อขอหน้าเว็บ HTML มาแสดงผลบนมือถือ
    • Client สั่ง GET เพื่อถาม ESP32 ว่า “ตอนนี้เซนเซอร์อุณหภูมิกี่องศาแล้ว?”

✅ POST (การส่งข้อมูลหรือคำสั่ง)

  • จุดประสงค์ ใช้เมื่อต้องการส่งข้อมูลให้ Server นำไปประมวลผล เปลี่ยนแปลงสถานะหรืออัปเดตข้อมูล
  • ตัวอย่างในฟาร์ม
    • Client สั่ง POST พร้อมแนบคำสั่งไปบอก ESP32 ว่า “เปิดปั๊มน้ำเครื่องที่ 1 เดี๋ยวนี้!”
    • Client สั่ง POST เพื่อส่งรหัสผ่าน Wi-Fi รหัสใหม่ไปบันทึกลงใน ESP32

🎯 สรุป การทำโปรเจกต์ IoT ด้วย ESP32 คือการทำให้บอร์ดเข้าวงแลนได้ (ผ่าน STA หรือ AP) จากนั้นเขียนโปรแกรมให้บอร์ดทำหน้าที่เป็น Server ที่คอยรับคำสั่งผ่าน HTTP Protocol โดยใช้ GET ในการขอดูข้อมูลเซนเซอร์ และใช้ POST ในการสั่งเปิด-ปิดรีเลย์อุปกรณ์ไฟฟ้านั่นเอง


คำศัพท์เครือข่าย สำหรับใช้งานบอร์ด ESP32

คำศัพท์ความหมายและหน้าที่การทำงานเปรียบเทียบให้เห็นภาพ
SSIDชื่อของเครือข่าย Wi-Fi ที่ต้องการให้บอร์ดเชื่อมต่อป้ายชื่อหน้าบ้าน
Passwordรหัสผ่านสำหรับขออนุญาตเข้าใช้งาน Wi-Fi นั้นกุญแจเข้าบ้าน
IP Addressหมายเลขประจำตัวของอุปกรณ์ในระบบเครือข่ายบ้านเลขที่ของ ESP32
MAC Addressรหัสประจำตัวชิป Wi-Fi ที่ฝังมาจากโรงงาน (ไม่ซ้ำใคร)เลขบัตรประชาชนของชิป
Local IPหมายเลข IP ที่ติดต่อกันได้เฉพาะอุปกรณ์ในวงแลนเดียวกันเบอร์โทรศัพท์ภายใน (เบอร์ต่อ)
DHCPระบบของ Router ที่สุ่มแจก IP Address ให้อุปกรณ์อัตโนมัติเจ้าหน้าที่สุ่มแจกบ้านเลขที่
Static IPการตั้งค่าล็อก IP Address ให้บอร์ดใช้เลขเดิมตลอดไปการซื้อบ้านเลขที่ถาวร (จำง่าย)
Portช่องทางที่เปิดรับส่งข้อมูลเฉพาะประเภท เช่น เว็บพอร์ต 80หมายเลขประตูบ้านแต่ละบาน
Pathเส้นทางหรือคำสั่งย่อยที่ต่อท้าย IP เพื่อระบุการทำงานคำสั่งว่าเข้าประตูไปแล้วให้ทำอะไร

Lab 3 การเชื่อมต่อ Wi-Fi บน ESP32

เราจะใช้ Library WiFi.h ซึ่งเป็นมาตรฐานของ ESP32

โค้ดตัวอย่าง

#include <WiFi.h>

const char* ssid = "ชื่อWiFi_ของคุณ";     // ชื่อ WiFi
const char* password = "รหัสผ่าน_ของคุณ"; // รหัสผ่าน

void setup() {
  Serial.begin(115200);
  Serial.println();                    // ขึ้นบรรทัดใหม่รอไว้
  Serial.print("Connecting to ");
  Serial.println(ssid);

  // เริ่มการเชื่อมต่อ
  WiFi.begin(ssid, password);

  // รอจนกว่าจะเชื่อมต่อสำเร็จ
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected!");
  Serial.print("IP address ");
  Serial.println(WiFi.localIP()); // แสดงหมายเลข IP ของบอร์ด
}

void loop() {
  // ไม่ต้องใส่อะไรในส่วนนี้
}

Lab 4 Hello Web Server

เป้าหมาย เขียนโปรแกรมให้ ESP32 ทำหน้าที่เป็น Web Server และตอบกลับข้อความข้อความ “Welcome to My Smart Farm” เมื่อมีอุปกรณ์อื่นเรียกเข้ามาที่หมายเลข IP ของบอร์ด

แนวคิดพื้นฐาน (The HTTP Concept)

เมื่อเรานำหมายเลข IP ของ ESP32 ไปกรอกใน Browser

  1. Client (มือถือ/คอม) ส่งคำขอ (HTTP Request) ไปที่ ESP32
  2. Server (ESP32) ได้รับคำขอแล้วประมวลผลตามโค้ดที่เราเขียน
  3. Response ESP32 ส่งข้อความ HTML กลับมาแสดงผลบนหน้าจอ

โค้ดตัวอย่าง

ให้นักศึกษาใช้ Library มาตรฐานชื่อ WebServer.h

#include <WiFi.h>
#include <WebServer.h>

// --- ตั้งค่า WiFi ---
const char* ssid = "ชื่อWiFi_ของคุณ";
const char* password = "รหัสผ่าน_ของคุณ";

// สร้างออบเจกต์ server ที่พอร์ต 80 (พอร์ตมาตรฐานของเว็บ)
WebServer server(80);

// ฟังก์ชันที่จะทำงานเมื่อมีคนเข้ามาที่หน้าแรก (/)
void handleRoot() {
  // ส่งสถานะ 200 (Success), ชนิดข้อมูลเป็น text/plain และข้อความ
  server.send(200, "text/plain", "Welcome to My Smart Farm");
}

void setup() {
  Serial.begin(115200);
  
  // 1. เชื่อมต่อ WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("\nWiFi connected!");
  Serial.print("IP address ");
  Serial.println(WiFi.localIP()); // *** สำคัญ จดหมายเลขนี้ไว้ ***

  // 2. กำหนดเส้นทาง (Route)
  server.on("/", handleRoot); // เครื่องหมาย / ตัวเดียวโดดๆ หมายถึง ไปยังหน้าโฮมเพจ

  // 3. เริ่มต้นการทำงานของ Server
  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  // สั่งให้ Server รอรับคำขออยู่ตลอดเวลา
  server.handleClient();
}

ขั้นตอนการทดลอง

  1. Upload โค้ด เมื่ออัปโหลดเสร็จ ให้เปิด Serial Monitor ดูหมายเลข IP Address เช่น 192.168.1.45
  2. เชื่อมต่อ WiFi เดียวกัน มั่นใจว่ามือถือหรือคอมพิวเตอร์ของนักศึกษา เชื่อมต่อ WiFi ชื่อเดียวกับที่ตั้งไว้ในโค้ด
  3. เปิด Browser พิมพ์หมายเลข IP ที่ได้ลงในช่อง URL ของ Chrome หรือ Safari แล้วกด Enter
  4. ดูผลลัพธ์ ข้อความ “Welcome to My Smart Farm” จะต้องปรากฏขึ้นบนหน้าจอ

ชนิดของข้อมูล

📝 text/plain ส่งไปเป็นข้อความทื่อๆธรรมดา เช่น ส่งคำว่า “OK” หรือ ค่าอุณหภูมิ “35.5”

🌐 text/html ส่งไปเป็นโค้ดสร้างเว็บ มือถือรับไปแล้วจะสร้างเป็นหน้าเว็บสวยงาม มีปุ่มกด มีสีสัน

📦 application/json ส่งไปเป็นแพ็กเกจข้อมูลที่เป็นระเบียบ เหมาะสำหรับส่งข้อมูลหลายๆค่าพร้อมกัน เช่น อุณหภูมิ ความชื้น และสถานะปั๊มน้ำ ให้แอปพลิเคชันเอาไปแยกส่วนต่อได้ง่าย

คำอธิบายเพิ่มเติม

  • Port 80 คือ พอร์ตมาตรฐานที่ใช้สื่อสารผ่าน HTTP ทั่วโลก
  • HTTP 200 OK คือ รหัสสถานะที่บอกว่า “การสื่อสารสำเร็จไม่มีข้อผิดพลาด” (ถ้าหาหน้าไม่เจอจะเป็น 404 Not Found)
  • Local Network ใน Lab นี้ บอร์ดจะทำงานได้เฉพาะในวง WiFi เดียวกันเท่านั้น หากต้องการสั่งงานข้ามจังหวัด เราจะต้องขยับไปใช้ระบบ Cloud (Blynk/MQTT)

โจทย์ท้าทาย

ให้นักศึกษาลองเปลี่ยนจาก text/plain เป็น text/html แล้วใส่ Code HTML เช่น <h1>Welcome to My Smart Farm</h1> ดูครับว่าตัวอักษรบนหน้าจอมือถือจะเปลี่ยนไปอย่างไร


Lab 4.1 ระบบเฝ้าระวังโรงเรือนผ่านหน้าเว็บ (Live Web Monitor)

เป้าหมาย ให้นักศึกษานำค่าจาก DHT22 มาแสดงผลบน Browser และทำให้หน้าเว็บรีเฟรชตัวเองอัตโนมัติทุกๆ 5 วินาที

การต่อวงจร

DHT22ESP32
VCC3.3V
GNDGND
DATAGPIO 4 (D4)

โค้ดตัวอย่าง

#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>

// --- ตั้งค่า WiFi ---
const char* ssid = "ชื่อWiFi_ของคุณ";
const char* password = "รหัสผ่าน_ของคุณ";

// --- ตั้งค่า DHT22 ---
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

WebServer server(80);

// ฟังก์ชันสร้างหน้าเว็บ HTML
void handleRoot() {
  // อ่านค่าจากเซนเซอร์
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  // ตรวจสอบความถูกต้องของข้อมูล
  if (isnan(h) || isnan(t)) {
    server.send(500, "text/plain", "Error ไม่สามารถอ่านค่าจากเซนเซอร์ได้");
    return;
  }

  // สร้างหน้าจอ UI ด้วย HTML และ CSS
  String html = "<!DOCTYPE html><html>";
  html += "<head><meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  // หัวใจสำคัญ คำสั่งให้หน้าเว็บรีเฟรชตัวเองทุก 5 วินาที
  html += "<meta http-equiv='refresh' content='5'>"; 
  html += "<title>Smart Farm Monitor</title>";
  
  // แก้ไข: เติมเครื่องหมายโคลอน (:) ใน CSS ให้ถูกต้องทั้งหมด
  html += "<style>";
  html += "body { font-family: 'Segoe UI', Arial; text-align: center; background-color: #eef2f3; margin-top: 50px; }";
  html += ".container { background: white; padding: 30px; border-radius: 15px; display: inline-block; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }";
  html += "h1 { color: #2c3e50; }";
  html += ".data-box { font-size: 2.5rem; font-weight: bold; margin: 20px 0; }";
  html += ".temp { color: #e74c3c; }";
  html += ".humi { color: #3498db; }";
  html += ".footer { font-size: 0.8rem; color: #7f8c8d; }";
  html += "</style></head>";
  
  html += "<body><div class='container'>";
  html += "<h1>🌿 Smart Farm Monitor</h1>";
  html += "<hr>";
  html += "<div class='data-box temp'>🌡️ " + String(t, 1) + " &deg;C</div>";
  html += "<div class='data-box humi'>💧 " + String(h, 0) + " %</div>";
  html += "<p class='footer'>อัปเดตข้อมูลอัตโนมัติทุก 5 วินาที</p>";
  html += "</div></body></html>";

  server.send(200, "text/html", html); // ส่งหน้าเว็บออกไป
}

void setup() {
  Serial.begin(115200);
  dht.begin();

  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("\nWiFi connected!");
  Serial.print("IP Address: "); // เพิ่มเครื่องหมาย : ให้อ่านค่า IP ใน Serial Monitor ได้ชัดเจนขึ้น
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot); // กำหนดว่าหน้าแรกให้รันฟังก์ชัน handleRoot
  server.begin();
}

void loop() {
  server.handleClient(); // รอรับคำขอจาก Browser
}

อธิบายการสร้างหน้าจอ UI

การสร้างหน้าจอ UI ในโค้ด Live Web Monitor บน ESP32 คือ การเขียนภาษา HTML และ CSS ซ้อนลงไปในภาษา C++ ของ Arduino โดยเราใช้ตัวแปร String มาทำหน้าที่เป็น “ถังเก็บโค้ดเว็บ” เพื่อส่งไปให้ Browser แสดงผล

อธิบายเป็น 3 ส่วนหลัก เพื่อให้นักศึกษาเข้าใจโครงสร้างของหน้าเว็บนี้

1. ส่วนหัวใจ (Meta Tags ใน <head>)

ส่วนนี้อยู่ภายใต้แท็ก <head> ซึ่งเป็นส่วนที่ผู้ชมไม่เห็นบนหน้าจอ แต่เป็นคำสั่งควบคุมการทำงานของ Browser

  • <meta charset='UTF-8'> สั่งให้ Browser รองรับภาษาไทยและอักขระพิเศษ
  • <meta name='viewport' ...> สั่งให้หน้าจอ Responsive ปรับขนาดให้พอดีกับหน้าจอมือถืออัตโนมัติ
  • <meta http-equiv='refresh' content='5'> เป็นคำสั่งบอก Browser ว่า “เฮ้! ทุก ๆ 5 วินาที ให้ช่วยกด Refresh หน้าเว็บนี้ให้ทีนะ” เพื่อให้ค่าเซนเซอร์ที่ส่งมาจาก ESP32 เป็นค่าปัจจุบันเสมอ

2. ส่วนการตกแต่ง (CSS ใน <style>)

CSS คือ การแต่งหน้าทาปากให้เว็บดูสวยงาม เราเขียนไว้ในแท็ก <style>

  • .container สร้าง “กล่องขาว” (Card) ตรงกลางหน้าจอ ใส่ box-shadow เพื่อให้ดูมีมิติเหมือนแอปมือถือสมัยใหม่
  • .temp { color #e74c3c; } กำหนดให้ตัวเลขอุณหภูมิเป็น สีแดง (สื่อถึงความร้อน)
  • .humi { color #3498db; } กำหนดให้ตัวเลขความชื้นเป็น สีฟ้า (สื่อถึงน้ำ)
  • font-family 'Segoe UI' เลือกใช้ตัวอักษรที่ดูสะอาดตาและทันสมัย

3. ส่วนการแสดงผลข้อมูล (Body & Data Injection)

ส่วนนี้คือสิ่งที่จะปรากฏบนจอ และเป็นจุดเชื่อมต่อระหว่างเซนเซอร์กับหน้าเว็บ

  • String(t, 1) เป็นเทคนิคสำคัญ เราเอาค่าตัวแปร t (อุณหภูมิ) จาก DHT22 มาแปลงเป็นข้อความ (String) โดยเอาทศนิยม 1 ตำแหน่ง แล้วเอามาต่อเข้ากับรหัส HTML
  • &deg;C เป็นรหัสที่ใช้แสดงผลสัญลักษณ์ องศาเซลเซียส °C บน Browser คำว่า deg ย่อมาจากคำว่า Degree (องศา)

Lab 4.2 ระบบควบคุมปั๊มน้ำผ่านมือถือ (Remote Web Control)

เป้าหมาย เพื่อสร้างปุ่มเปิดปั๊มและปิดปั๊มบนหน้าเว็บ เมื่อกดปุ่มบนมือถือ ให้บอร์ด ESP32 สั่งงาน Relay ทันที และหน้าเว็บต้องแสดงสถานะปัจจุบันว่าตอนนี้ปั๊มเปิดหรือ ปิดอยู่

การต่อวงจร (Wiring)

Relay ModuleESP32
VCCVIN (5V)
GNDGND
INGPIO 2 (D2)

โค้ดตัวอย่าง

#include <WiFi.h>
#include <WebServer.h>

// --- ตั้งค่า WiFi ---
const char* ssid = "ชื่อWiFi_ของคุณ";
const char* password = "รหัสผ่าน_ของคุณ";

// กำหนดขา Relay
const int relayPin = 2;

// ตัวแปรเก็บสถานะปั๊ม (0 = ปิด, 1 = เปิด)
bool pumpStatus = false; // ตัวแปรประเภท Boolean ใช้เก็บสถานะ 2 ค่าเท่านั้น คือ ใช่/ไม่ใช่ หรือ เปิด/ปิด

WebServer server(80);

// ฟังก์ชันสร้างหน้าเว็บหลักพร้อมปุ่มกด
void handleRoot() {
  String html = "<!DOCTYPE html><html>";
  html += "<head><meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<title>Smart Farm Control</title>";
  html += "<style>";
  // แก้ไข: เติมเครื่องหมาย : ใน CSS ให้ถูกต้องทั้งหมด
  html += "body { font-family: Arial; text-align: center; background-color: #f4f4f4; padding-top: 50px; }";
  html += ".btn { display: inline-block; padding: 20px 40px; font-size: 20px; color: white; border: none; border-radius: 10px; cursor: pointer; text-decoration: none; margin: 10px; }";
  html += ".btn-on { background-color: #2ecc71; }";
  html += ".btn-off { background-color: #e74c3c; }";
  html += ".status { font-size: 24px; font-weight: bold; margin-bottom: 30px; }";
  html += ".on { color: #2ecc71; } .off { color: #e74c3c; }";
  html += "</style></head>";
  
  html += "<body>";
  html += "<h1>🚜 Pump Control Panel</h1>";
  
  // แสดงสถานะปัจจุบัน
  html += "<div class='status'>สถานะปั๊มตอนนี้: ";
  if(pumpStatus) html += "<span class='on'>กำลังทำงาน (ON)</span>";
  else html += "<span class='off'>หยุดทำงาน (OFF)</span>";
  html += "</div>";

  // ปุ่มควบคุม
  html += "<a href='/pumpOn' class='btn btn-on'>เปิดปั๊มน้ำ</a>";
  html += "<a href='/pumpOff' class='btn btn-off'>ปิดปั๊มน้ำ</a>";
  
  html += "</body></html>";

  server.send(200, "text/html", html);
}

// ฟังก์ชันทำงานเมื่อกด "เปิดปั๊ม"
void handlePumpOn() {
  digitalWrite(relayPin, HIGH);
  pumpStatus = true;
  Serial.println("Pump turned ON");
  // หลังจากสั่งงานเสร็จ ให้กระโดดกลับไปหน้าหลัก (/) เพื่อดูสถานะอัปเดต
  server.sendHeader("Location", "/");
  server.send(303); // สั่งให้ไปเปิดหน้าตาม Location ที่ระบุ
}

// ฟังก์ชันทำงานเมื่อกด "ปิดปั๊ม"
void handlePumpOff() {
  digitalWrite(relayPin, LOW);
  pumpStatus = false;
  Serial.println("Pump turned OFF");
  // หลังจากสั่งงานเสร็จ ให้กระโดดกลับไปหน้าหลัก (/)
  server.sendHeader("Location", "/");
  server.send(303); // สั่งให้ไปเปิดหน้าตาม Location ที่ระบุ
}

void setup() {
  Serial.begin(115200);
  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW); // เริ่มต้นให้ปั๊มปิดก่อน

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("\nWiFi Connected!");
  Serial.println(WiFi.localIP());

  // กำหนดเส้นทาง (Routes) ผู้ใช้งานเรียกเข้า Path ไหน จะต้องส่งเขาไปหาฟังก์ชันอะไร
  server.on("/", handleRoot);
  server.on("/pumpOn", handlePumpOn);
  server.on("/pumpOff", handlePumpOff);

  server.begin();
}

void loop() {
  server.handleClient();
}

อธิบายหัวใจสำคัญ (Teacher’s Guide)

  1. Hyperlinks as Commands ปุ่มกดที่เราเห็น จริงๆแล้วมันคือลิงก์ <a href='/pumpOn'> เมื่อเรากดปุ่ม Browser จะแอบวิ่งไปที่ที่อยู่ IP_ของบอร์ด/pumpOn ซึ่งจะไปบอกฟังก์ชัน handlePumpOn ใน ESP32 ให้ทำงาน
  2. href “เฮช-เรฟ” ย่อมาจาก Hypertext Reference ในภาษา HTML ที่ใช้สำหรับระบุที่หมาย
  3. HTTP Redirect (Status 303) ในฟังก์ชัน handlePumpOn เราใช้การส่ง Header “Location /” กลับไป เพื่อบอก Browser ว่า “สั่งงานเสร็จแล้วนะ ให้กลับไปที่หน้าแรกทันที” นักศึกษาจะเห็นสถานะบนหน้าจอเปลี่ยนจาก OFF เป็น ON ทันทีที่กดปุ่ม

💡 โจทย์ท้าทาย (The Pro Challenge)

สร้างระบบป้องกันการทำงานซ้ำซ้อน โดยให้รวมโค้ด Lab 4.1 และ 4.2 เข้าด้วยกัน โดยมีเงื่อนไขว่า “ถ้าค่าความชื้นดินเกิน 90% (เปียกมาก) แม้จะกดปุ่มเปิดปั๊มบนมือถือ ปั๊มก็จะไม่ทำงาน พร้อมส่งการแจ้งเตือน”

โค้ดตัวอย่าง

#include <WiFi.h>
#include <WebServer.h>

// --- ตั้งค่า WiFi ---
const char* ssid = "ชื่อWiFi_ของคุณ";
const char* password = "รหัสผ่าน_ของคุณ";

// --- กำหนดขาอุปกรณ์ ---
const int relayPin = 2;   // ขาควบคุมปั๊มน้ำ
const int soilPin = 34;   // ขาอ่านค่าเซนเซอร์ความชื้นดิน (Analog)

// --- ตัวแปรระบบ ---
bool pumpStatus = false;  // สถานะปั๊ม
String alertMsg = "";     // ตัวแปรเก็บข้อความแจ้งเตือนบนหน้าเว็บ

WebServer server(80);

// ฟังก์ชันอ่านค่าความชื้นดิน (จำลองจาก Lab ก่อนหน้า)
int getSoilMoisture() {
  int analogValue = analogRead(soilPin);
  // แปลงค่า Analog ช่วง 0-4095 เป็นเปอร์เซ็นต์ 0-100%
  // ค่านี้ต้องปรับให้ตรงกับการตั้งค่าใน Lab ของนักศึกษา
  int moisturePercent = map(analogValue, 4095, 0, 0, 100); 
  
  if(moisturePercent > 100) moisturePercent = 100;
  if(moisturePercent < 0) moisturePercent = 0;
  
  return moisturePercent;
}

// ฟังก์ชันสร้างหน้าเว็บหลัก
void handleRoot() {
  int currentMoisture = getSoilMoisture(); // อ่านค่าความชื้นปัจจุบัน
  
  String html = "<!DOCTYPE html><html>";
  html += "<head><meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<meta http-equiv='refresh' content='5'>";
  html += "<title>Smart Farm Control</title>";
  html += "<style>";
  html += "body { font-family: 'Segoe UI', Arial; text-align: center; background-color: #f4f4f4; padding-top: 50px; }";
  html += ".btn { display: inline-block; padding: 20px 40px; font-size: 20px; color: white; border: none; border-radius: 10px; cursor: pointer; text-decoration: none; margin: 10px; }";
  html += ".btn-on { background-color: #2ecc71; }";
  html += ".btn-off { background-color: #e74c3c; }";
  html += ".status { font-size: 24px; font-weight: bold; margin-bottom: 10px; }";
  html += ".alert { background-color: #f39c12; color: white; padding: 15px; border-radius: 8px; margin: 20px auto; width: 80%; max-width: 400px; font-weight: bold; }";
  html += ".on { color: #2ecc71; } .off { color: #e74c3c; }";
  html += "</style></head>";
  
  html += "<body>";
  html += "<h1>ระบบควบคุมฟาร์มอัจฉริยะ</h1>";
  
  // โชว์ค่าความชื้นดิน
  html += "<h2>ความชื้นดินปัจจุบัน: " + String(currentMoisture) + "%</h2>";

  // เช็คว่ามีข้อความแจ้งเตือนไหม ถ้ามีให้แสดงกล่อง Alert
  if (alertMsg != "") {
    html += "<div class='alert'>⚠️ " + alertMsg + "</div>";
  }
  
  // แสดงสถานะปั๊มปัจจุบัน
  html += "<div class='status'>สถานะปั๊มตอนนี้: ";
  if(pumpStatus) html += "<span class='on'>กำลังทำงาน (ON)</span>";
  else html += "<span class='off'>หยุดทำงาน (OFF)</span>";
  html += "</div>";

  // ปุ่มควบคุม
  html += "<a href='/pumpOn' class='btn btn-on'>เปิดปั๊มน้ำ</a>";
  html += "<a href='/pumpOff' class='btn btn-off'>ปิดปั๊มน้ำ</a>";
  
  html += "</body></html>";

  server.send(200, "text/html", html);
}

// ฟังก์ชันทำงานเมื่อผู้ใช้กด "เปิดปั๊ม"
void handlePumpOn() {
  int currentMoisture = getSoilMoisture(); // เช็คความชื้นดิน ณ วินาทีที่กดปุ่ม
  
  // เงื่อนไขป้องกันการทำงานซ้ำซ้อน
  if (currentMoisture > 90) {
    // กรณีดินเปียกมาก ให้ปิดกั้นคำสั่งไว้และสร้างข้อความแจ้งเตือน
    alertMsg = "ไม่อนุญาตให้เปิดปั๊ม เนื่องจากความชื้นดินสูงเกิน 90% !";
    Serial.println("Action Blocked: Soil is too wet.");
  } else {
    // กรณีปกติ สั่งเปิดปั๊มน้ำ และล้างข้อความแจ้งเตือนทิ้ง
    digitalWrite(relayPin, HIGH);
    pumpStatus = true;
    alertMsg = ""; 
    Serial.println("Pump turned ON");
  }
  
  server.sendHeader("Location", "/");
  server.send(303);
}

// ฟังก์ชันทำงานเมื่อผู้ใช้กด "ปิดปั๊ม"
void handlePumpOff() {
  digitalWrite(relayPin, LOW);
  pumpStatus = false;
  alertMsg = ""; // ล้างข้อความแจ้งเตือนเมื่อสั่งปิดปกติ
  Serial.println("Pump turned OFF");
  
  server.sendHeader("Location", "/");
  server.send(303);
}

void setup() {
  Serial.begin(115200);
  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);

  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("\nWiFi Connected!");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/pumpOn", handlePumpOn);
  server.on("/pumpOff", handlePumpOff);

  server.begin();
}

void loop() {
  server.handleClient();
}

Project 2 สร้างหน้า Dashboard ควบคุมฟาร์ม (Web Dashboard Control Panel)

เป้าหมาย สร้างหน้าเว็บส่วนตัวบน ESP32 ที่มีปุ่มกดควบคุมปั๊มน้ำ และแสดงค่าความชื้นดินแบบ Real-time โดยใช้เทคนิคการ Redirect เพื่อให้หน้าเว็บอัปเดตสถานะทันทีที่กดปุ่ม

โครงสร้างเส้นทาง (Routes Map)

  • http//[IP-Address]/  เรียกฟังก์ชัน handleRoot (โชว์หน้าหลัก)
  • http//[IP-Address]/on  เรียกฟังก์ชัน handlePumpOn (สั่งเปิด)
  • http//[IP-Address]/off  เรียกฟังก์ชัน handlePumpOff (สั่งปิด)

ตารางการต่อวงจร (Wiring Table)

อุปกรณ์ขาอุปกรณ์ต่อเข้ากับ ESP32หน้าที่
เซนเซอร์ดิน (Soil)VCC3.3Vจ่ายไฟเลี้ยงเซนเซอร์
GNDGNDต่อสายดิน
AO (Analog Out)GPIO 34 (D34)ส่งค่าความชื้นแบบ Analog
รีเลย์ (Relay)VCCVIN (5V)จ่ายไฟให้ขดลวดรีเลย์
GNDGNDต่อสายดิน
INGPIO 2 (D2)รับคำสั่ง เปิด/ปิด จากหน้าเว็บ

ตัวอย่างโค้ด

#include <WiFi.h>
#include <WebServer.h>

// --- 1. ตั้งค่า WiFi และขาอุปกรณ์ ---
const char* ssid = "ชื่อWiFi_ของคุณ";
const char* password = "รหัสผ่าน_ของคุณ";

const int relayPin = 2;   // ขา Relay
const int soilPin = 34;   // ขาเซนเซอร์ดิน
bool pumpStatus = false;  // ตัวแปรเก็บสถานะเปิด/ปิด

WebServer server(80);

// --- 2. ฟังก์ชันหน้าหลัก (/) ---
void handleRoot() {
  int rawSoil = analogRead(soilPin);
  int percent = map(rawSoil, 4095, 0, 0, 100);
  
  // ป้องกันค่าเปอร์เซ็นต์ติดลบหรือเกิน 100
  if (percent < 0) percent = 0;
  if (percent > 100) percent = 100;

  String html = "<!DOCTYPE html><html lang='th'>";
  html += "<head><meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<title>Smart Farm Dashboard</title>";
  html += "<style>";
  // ตกแต่งพื้นหลังและจัดกึ่งกลาง
  html += "body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%); margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; color: #333; }";
  // ตกแต่งการ์ด
  html += ".card { background: #ffffff; padding: 40px 30px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); text-align: center; max-width: 400px; width: 90%; }";
  html += "h1 { margin-top: 0; color: #2c3e50; font-size: 26px; margin-bottom: 25px; }";
  // วงกลมแสดงความชื้น
  html += ".moisture-circle { width: 140px; height: 140px; border-radius: 50%; border: 8px solid #007bff; display: flex; flex-direction: column; justify-content: center; align-items: center; margin: 0 auto 25px auto; box-shadow: 0 4px 15px rgba(0,123,255,0.2); }";
  html += ".moisture-val { font-size: 38px; font-weight: bold; color: #007bff; }";
  html += ".moisture-label { font-size: 14px; color: #666; }";
  // ป้ายสถานะปั๊ม
  html += ".status-box { padding: 12px; border-radius: 10px; background: #f8f9fa; margin-bottom: 25px; font-size: 16px; font-weight: bold; }";
  html += ".on { color: #28a745; } .off { color: #dc3545; }";
  // ปุ่มกด
  html += ".btn-group { display: flex; gap: 15px; }";
  html += ".btn { flex: 1; padding: 15px; font-size: 16px; font-weight: bold; color: white; border-radius: 12px; text-decoration: none; transition: all 0.2s; border: none; cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 8px; }";
  html += ".btn:hover { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0,0,0,0.2); }";
  html += ".btn-on { background: #28a745; } .btn-off { background: #dc3545; }";
  // ปุ่มรีเฟรช
  html += ".refresh-link { display: inline-block; margin-top: 25px; color: #6c757d; text-decoration: none; font-size: 14px; transition: color 0.2s; }";
  html += ".refresh-link:hover { color: #343a40; text-decoration: underline; }";
  html += "</style></head><body>";

  html += "<div class='card'>";
  html += "<h1>🌱 Farm Dashboard</h1>";
  
  // แสดงค่าความชื้นในรูปแบบวงกลม
  html += "<div class='moisture-circle'>";
  html += "<span class='moisture-val'>" + String(percent) + "%</span>";
  html += "<span class='moisture-label'>ความชื้นดิน</span>";
  html += "</div>";
  
  // ป้ายแสดงสถานะ
  html += "<div class='status-box'>สถานะปั๊มน้ำ: ";
  if(pumpStatus) {
    html += "<span class='on'>🟢 กำลังทำงาน</span>";
  } else {
    html += "<span class='off'>🔴 หยุดทำงาน</span>";
  }
  html += "</div>";
  
  // ปุ่มกดซ้าย-ขวา
  html += "<div class='btn-group'>";
  html += "<a href='/on' class='btn btn-on'>💧 เปิดปั๊ม</a>";
  html += "<a href='/off' class='btn btn-off'>🛑 ปิดปั๊ม</a>";
  html += "</div>";
  
  html += "<a href='/' class='refresh-link'>🔄 กดเพื่ออัปเดตค่าล่าสุด</a>";
  
  html += "</div></body></html>";

  server.send(200, "text/html", html);
}

// --- 3. ฟังก์ชันเส้นทาง /on ---
void handlePumpOn() {
  digitalWrite(relayPin, HIGH);
  pumpStatus = true;
  server.sendHeader("Location", "/");
  server.send(303); 
}

// --- 4. ฟังก์ชันเส้นทาง /off ---
void handlePumpOff() {
  digitalWrite(relayPin, LOW);
  pumpStatus = false;
  server.sendHeader("Location", "/");
  server.send(303);
}

void setup() {
  Serial.begin(115200);
  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { 
    delay(500); 
    Serial.print("."); 
  }
  
  Serial.println("\nReady! IP " + WiFi.localIP().toString());

  // การผูก URL กับ ฟังก์ชัน (Routing)
  server.on("/", handleRoot);
  server.on("/on", handlePumpOn);
  server.on("/off", handlePumpOff);

  server.begin();
}

void loop() {
  server.handleClient();
}

เจาะลึกโค้ด CSS : เสกหน้าเว็บ Smart Farm ให้สวยระดับมือโปร

นักศึกษาคงเคยเห็นเว็บไซต์ที่หน้าตาโบราณๆ มีแต่ตัวหนังสือสีดำบนพื้นหลังสีขาวใช่ไหม นั่นคือหน้าเว็บที่มีแต่ HTML

แต่สิ่งที่จะทำให้บ้านของเราน่าอยู่ ทาสีสวยงาม มีโซฟานุ่มๆ ก็คือ CSS (Cascading Style Sheets) ในโค้ด ESP32 ของเรา CSS จะถูกเขียนซ่อนอยู่ระหว่างแท็ก <style> ... </style> นั่นเอง

มาดูกันว่าโค้ดแต่ละบรรทัดทำหน้าที่อะไรบ้าง และเราจะปรับแต่งมันได้อย่างไร

การตั้งกฎ CSS

ก่อนจะแต่งหน้าเว็บ เราต้องรู้จักวิธีเรียกชื่อสิ่งที่เราจะตกแต่งก่อน

  • ถ้าไม่มีจุดนำหน้า เช่น body, h1 หมายถึง การสั่งเปลี่ยนรูปแบบของ “แท็ก HTML” นั้นๆ ทั้งหมดในหน้าเว็บ
  • ถ้ามีจุดนำหน้า เช่น .card, .btn หมายถึง การเรียก Class (คลาส) ซึ่งเปรียบเหมือน “ป้ายชื่อ” ที่เราเอาไปแปะไว้ที่แท็กไหนก็ได้ เพื่อให้ตกแต่งได้เฉพาะเจาะจงมากขึ้น

🖌️ ส่วนที่ 1 ตกแต่งพื้นหลังหน้าจอ (body)

body { 
  font-family: 'Segoe UI', Tahoma, sans-serif; 
  background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%); 
  display: flex; justify-content: center; align-items: center; min-height: 100vh;
}
  • font-family สั่งเปลี่ยนฟอนต์ตัวหนังสือให้ดูทันสมัยขึ้น
  • background: linear-gradient(...) เป็นการสั่งให้ไล่สีพื้นหลัง จากสีม่วงพาสเทล (#e0c3fc) ไปหาสีฟ้า (#8ec5fc) ในมุมเฉียง 135 องศา ถ้านักศึกษาอยากได้สีอื่น ให้นำรหัสสี HEX Code จาก Google มาแปะแทนได้เลย ..ลองทำดู
  • display: flex; justify-content: center; align-items: center; 3 คำสั่งนี้ทำงานร่วมกัน เปรียบเหมือน แม่เหล็กที่คอยดูดให้เนื้อหาทั้งหมดให้อยู่กึ่งกลางหน้าจอพอดี ไม่ว่าจะเปิดบนมือถือหรือจอคอมที่กว้างแค่ไหนก็ตาม

📦 ส่วนที่ 2 กล่องการ์ดสีขาว (.card)

.card { 
  background: #ffffff; 
  border-radius: 20px; 
  box-shadow: 0 10px 30px rgba(0,0,0,0.15); 
}
  • border-radius: 20px; สั่งให้มุมของกล่องมีความโค้งมน (ยิ่งตัวเลขเยอะ ยิ่งโค้งมาก)
  • box-shadow: ...: สั่งสร้างมิติเงาให้กับกล่อง ค่าแต่ละส่วน หมายถึง X, Y, Blur (ความฟุ้ง) ทำให้การ์ดดูลอยขึ้นมาจากพื้นหลัง
  • rgba ย่อมาจาก Red, Green, Blue, Alpha (ความโปร่งใส โดย 0 คือไม่มีเงา และ 1 คือดำมืด)

🔵 ส่วนที่ 3 วงกลมเกจวัดความชื้น (.moisture-circle)

.moisture-circle { 
  width: 140px; height: 140px; 
  border-radius: 50%; 
  border: 8px solid #007bff; 
}
  • ทำไมถึงเป็นวงกลม เคล็ดลับคือการสร้างกรอบสี่เหลี่ยมจัตุรัส (กว้าง 140 สูง 140) แล้วสั่ง border-radius: 50%; ขอบมันจะโค้งเข้าหากันจนกลายเป็นวงกลมอย่างสมบูรณ์
  • border: 8px solid #007bff; สร้างเส้นขอบความหนา 8 พิกเซล เป็นเส้นทึบสีน้ำเงิน

🖱️ ส่วนที่ 4 ปุ่มกดล้ำๆ (.btn และ .btn:hover)

.btn { 
  border-radius: 12px; 
  transition: all 0.2s; 
}
.btn:hover { 
  transform: translateY(-3px); 
  box-shadow: 0 6px 15px rgba(0,0,0,0.2); 
}
  • transition: all 0.2s; สั่งให้ปุ่มมีความสมูท (Animation) เวลาเกิดการเปลี่ยนแปลงใดๆ โดยใช้เวลา 0.2 วินาที
  • :hover คือ เหตุการณ์เมื่อเอาเมาส์ไปชี้ที่ปุ่ม หรือเอานิ้วแตะค้าง
  • transform: translateY(-3px); เมื่อเอาเมาส์ชี้ปุ่มจะลอยขยับขึ้นไปด้านบน 3 พิกเซล พร้อมกับมีเงาโผล่ขึ้นมา ทำให้ผู้ใช้รู้สึกว่าปุ่มนี้กดได้จริงและดูมีชีวิตชีวา

ภารกิจสำหรับนักศึกษา

🚀 ภารกิจที่ 1 เปลี่ยนธีมฟาร์ม (Color Challenge)

ให้นักศึกษาลองเปลี่ยนสีสันของหน้า Dashboard จากสีเดิม ให้กลายเป็นธีมใหม่ตามที่ชอบ เช่น ธีมสวนทุเรียน (เน้นเขียว-เหลือง) ธีมฟาร์มตอนกลางคืน เน้นน้ำเงินเข้ม

  • โจทย์ เปลี่ยนสีพื้นหลัง (background) และสีวงกลมความชื้น (moisture-circle)
  • สิ่งที่ต้องทำ ไปที่ Google แล้วค้นหาคำว่า HTML Color Picker เพื่อเลือกสีที่ชอบ แล้วนำรหัสสี เช่น #2d5a27 มาเปลี่ยนในโค้ด
  • จุดที่ต้องแก้ ในส่วน body { background: ... } และ .moisture-circle { border: ... }

🚀 ภารกิจที่ 2 สร้างเอฟเฟกต์การ์ดลอยได้ (Shadow Master)

สอนให้นักศึกษารู้จักการใช้แสงและเงา เพื่อทำให้หน้าเว็บดูมีความลึกเหมือนแอปพลิเคชันราคาแพง

  • โจทย์ ปรับค่า box-shadow ของคลาส .card ให้ดูเหมือนว่าการ์ดกำลังลอยสูงขึ้นมาจากพื้นหลังจริงๆ
  • คำใบ้ ลองเปลี่ยนจาก 0 10px 30px เป็นค่าที่ฟุ้งกว่าเดิม เช่น 0 25px 60px และลองปรับค่า rgba ตัวสุดท้าย (ความเข้มเงา) จาก 0.15 เป็น 0.25
  • ผลลัพธ์ นักศึกษาจะเห็นว่าการ์ดดูมีมิติและเด่นขึ้นมาทันที

🚀 ภารกิจที่ 3 ปุ่มกดมีชีวิต (Hover Animation)

ภารกิจนี้จะทำให้นักศึกษาตื่นเต้นมาก เพราะเราจะเปลี่ยนปุ่มนิ่งๆให้ขยับได้ เมื่อเอาเมาส์ไปชี้ (Hover)

  • โจทย์ เมื่อเอาเมาส์ไปชี้ที่ปุ่ม “เปิดปั๊ม” ให้ปุ่มขยายใหญ่ขึ้น (Scale Up) และเปลี่ยนสีเป็นสีเขียวสว่าง
  • สิ่งที่ต้องเติมลงใน CSS .btn:hover { transform: scale(1.1) translateY(-5px); /* ขยายใหญ่ขึ้น 10% และลอยขึ้น 5px */ filter: brightness(1.2); /* เพิ่มความสว่างให้สีปุ่ม */ }
  • ความรู้ที่ได้รับ นักศึกษาจะได้เข้าใจเรื่อง transition และการตอบสนองของผู้ใช้งาน (User Experience)

AJAX เทคนิคการส่งข้อมูลโดยไม่ต้องรีเฟรชหน้าใหม่

จาก Project ก่อนหน้านี้ นักศึกษาจะเห็นว่า เราต้องกดปุ่มอัปเดตข้อมูลเอง จึงจะเห็นการเปลี่ยนแปลงของค่าต่างๆ

เพื่อให้หน้าจอ Dashboard อัปเดตค่าความชื้นอัตโนมัติโดยที่นักศึกษาไม่ต้องกด Refresh หน้าเว็บเองนั้นมี 2 วิธีหลักๆ แต่วิธีที่ดูเป็นมืออาชีพและนิยมที่สุดในสายงานไอที คือการใช้ AJAX

เปรียบเทียบ Meta Refresh vs AJAX

การทำงานของ Meta Refresh และ AJAX/Fetch มีความแตกต่างกันอย่างชัดเจนทั้งในด้านประสิทธิภาพและประสบการณ์ผู้ใช้ โดย Meta Refresh เป็นวิธีแบบดั้งเดิมที่เขียนง่ายเพียงบรรทัดเดียว เหมาะสำหรับงานระดับเบื้องต้น แต่มีข้อเสียคือต้องโหลดหน้าเว็บรวมถึงไฟล์ทรัพยากรต่างๆ ใหม่ทั้งหมดทุกครั้งที่ตั้งเวลาไว้ ส่งผลให้หน้าจอกระพริบและสิ้นเปลืองปริมาณอินเทอร์เน็ต ในทางตรงกันข้าม AJAX หรือ Fetch ซึ่งเป็นมาตรฐานของอุตสาหกรรมไอทีในปัจจุบัน แม้จะมีความซับซ้อนในการเขียนโค้ดมากขึ้นเพราะต้องใช้ JavaScript แต่ก็ช่วยแก้ปัญหาดังกล่าวได้อย่างสมบูรณ์แบบ โดยระบบจะโหลดและเปลี่ยนค่าเฉพาะข้อมูลที่จำเป็นต้องอัปเดตเท่านั้น ทำให้การแสดงผลมีความลื่นไหล ประหยัดอินเทอร์เน็ต และไม่มีอาการหน้าจอกระพริบมากวนใจผู้ใช้งาน

AJAX (เอ-แจ็กซ์) ย่อมาจาก Asynchronous JavaScript and XML คือ เทคนิคการแอบส่งข้อมูลไปมาหลังบ้าน โดยที่ไม่ต้องรีเฟรชหน้าเว็บใหม่ทั้งหน้า

สิ่งที่ปรับเพิ่มในโค้ด

  1. สร้างเส้นทาง (Route) ใหม่ /data สำหรับส่งแค่ตัวเลขความชื้นออกไป (ไม่ส่งหน้าเว็บทั้งหน้า)
  2. เพิ่ม JavaScript ในหน้าเว็บ สั่งให้ Browser ไปดึงค่าจาก /data ทุกๆ 2 วินาที

โค้ดตัวอย่าง

#include <WiFi.h>
#include <WebServer.h>

// --- 1. ตั้งค่า WiFi และขาอุปกรณ์ ---
const char* ssid = "ชื่อWiFi_ของคุณ";
const char* password = "รหัสผ่าน_ของคุณ";

const int relayPin = 2;   // ขา Relay
const int soilPin = 34;   // ขาเซนเซอร์ดิน
bool pumpStatus = false;  // ตัวแปรเก็บสถานะเปิด/ปิด

WebServer server(80);

// --- ฟังก์ชันย่อยสำหรับอ่านค่าความชื้น เพื่อไม่ต้องเขียนโค้ดซ้ำ ---
int getMoisturePercent() {
  int rawSoil = analogRead(soilPin);
  int percent = map(rawSoil, 4095, 0, 0, 100);
  if (percent < 0) percent = 0;
  if (percent > 100) percent = 100;
  return percent;
}

// --- 🌟 2. ฟังก์ชัน API สำหรับตอบค่าความชื้นกลับไปให้ AJAX ---
void handleGetMoisture() {
  int currentMoisture = getMoisturePercent();
  // ส่งค่ากลับไปเป็น "ข้อความธรรมดา" (text/plain) มีแค่ตัวเลขเพียวๆ
  server.send(200, "text/plain", String(currentMoisture));
}

// --- 3. ฟังก์ชันหน้าหลัก (/) ---
void handleRoot() {
  int initialMoisture = getMoisturePercent();

  String html = "<!DOCTYPE html><html lang='th'>";
  html += "<head><meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<title>Smart Farm Dashboard</title>";
  html += "<style>";
  // ตกแต่งพื้นหลังและการ์ด
  html += "body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%); margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; color: #333; }";
  html += ".card { background: #ffffff; padding: 40px 30px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); text-align: center; max-width: 400px; width: 90%; }";
  html += "h1 { margin-top: 0; color: #2c3e50; font-size: 26px; margin-bottom: 25px; }";
  html += ".moisture-circle { width: 140px; height: 140px; border-radius: 50%; border: 8px solid #007bff; display: flex; flex-direction: column; justify-content: center; align-items: center; margin: 0 auto 25px auto; box-shadow: 0 4px 15px rgba(0,123,255,0.2); }";
  html += ".moisture-val { font-size: 38px; font-weight: bold; color: #007bff; }";
  html += ".moisture-label { font-size: 14px; color: #666; }";
  html += ".status-box { padding: 12px; border-radius: 10px; background: #f8f9fa; margin-bottom: 25px; font-size: 16px; font-weight: bold; }";
  html += ".on { color: #28a745; } .off { color: #dc3545; }";
  html += ".btn-group { display: flex; gap: 15px; }";
  html += ".btn { flex: 1; padding: 15px; font-size: 16px; font-weight: bold; color: white; border-radius: 12px; text-decoration: none; transition: all 0.2s; border: none; cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 8px; }";
  html += ".btn:hover { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0,0,0,0.2); }";
  html += ".btn-on { background: #28a745; } .btn-off { background: #dc3545; }";
  html += "</style></head><body>";

  html += "<div class='card'>";
  html += "<h1>🌱 Farm Dashboard</h1>";
  
  // 🌟 จุดสำคัญที่ 1 เติม id='moistureValue' เข้าไป เพื่อให้ JavaScript หาตำแหน่งเจอ
  html += "<div class='moisture-circle'>";
  html += "<span class='moisture-val' id='moistureValue'>" + String(initialMoisture) + "%</span>";
  html += "<span class='moisture-label'>ความชื้นดิน</span>";
  html += "</div>";
  
  html += "<div class='status-box'>สถานะปั๊มน้ำ: ";
  if(pumpStatus) {
    html += "<span class='on'>🟢 กำลังทำงาน</span>";
  } else {
    html += "<span class='off'>🔴 หยุดทำงาน</span>";
  }
  html += "</div>";
  
  html += "<div class='btn-group'>";
  html += "<a href='/on' class='btn btn-on'>💧 เปิดปั๊ม</a>";
  html += "<a href='/off' class='btn btn-off'>🛑 ปิดปั๊ม</a>";
  html += "</div>";
  
  html += "</div>";

  // 🌟 จุดสำคัญที่ 2: ฝังโค้ด JavaScript (AJAX) ลงไปท้ายหน้าเว็บ
  html += "<script>";
  html += "setInterval(function() {";
  html += "  fetch('/getMoisture')";                 // สั่งให้ไปดึงข้อมูลจากเส้นทาง /getMoisture
  html += "    .then(response => response.text())";  // เมื่อได้คำตอบมา ให้แปลงเป็นข้อความธรรมดา
  html += "    .then(data => {";
  html += "      document.getElementById('moistureValue').innerText = data + '%';"; // เอาตัวเลขใหม่ไปแปะทับที่ id=moistureValue
  html += "    });";
  html += "}, 2000);";                               // วนลูปทำซ้ำทุกๆ 2000 มิลลิวินาที (2 วินาที)
  html += "</script>";

  html += "</body></html>";

  server.send(200, "text/html", html);
}

// --- 4. ฟังก์ชันเส้นทาง /on ---
void handlePumpOn() {
  digitalWrite(relayPin, HIGH);
  pumpStatus = true;
  server.sendHeader("Location", "/");
  server.send(303); 
}

// --- 5. ฟังก์ชันเส้นทาง /off ---
void handlePumpOff() {
  digitalWrite(relayPin, LOW);
  pumpStatus = false;
  server.sendHeader("Location", "/");
  server.send(303);
}

void setup() {
  Serial.begin(115200);
  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { 
    delay(500); 
    Serial.print("."); 
  }
  
  Serial.println("\nReady! IP " + WiFi.localIP().toString());

  // ฟังก์ชัน (Routing)
  server.on("/", handleRoot);
  server.on("/on", handlePumpOn);
  server.on("/off", handlePumpOff);
  // 🌟 จุดสำคัญที่ 3 เปิดเส้นทางใหม่ให้ JavaScript วิ่งเข้ามาขอข้อมูลได้
  server.on("/getMoisture", handleGetMoisture);

  server.begin();
}

void loop() {
  server.handleClient();
}

💡 จุดที่เพิ่มเข้ามา

  1. เอาปุ่ม “กดเพื่ออัปเดต” ออกไปแล้ว เพราะตอนนี้เราใช้ JavaScript (AJAX) ทำงานแทนเราทุกๆ 2 วินาทีแบบอัตโนมัติ หน้าจอจะไม่กระพริบอีกต่อไป
  2. การตั้งชื่อเป้าหมาย (id='moistureValue') ในภาษา HTML ถ้าเราต้องการให้ JavaScript มาทำงานกับข้อความตรงไหน เราต้องติดป้ายชื่อ (id) ไว้ตรงนั้นก่อน เหมือนเป็นการบอกพิกัดให้ JavaScript รู้ว่าต้องเอาตัวเลขใหม่ไปทับตรงจุดไหน
  3. คำสั่ง fetch(...) เป็นคำสั่ง JavaScript สมัยใหม่ที่ใช้ทำ AJAX มันจะไปเรียกฟังก์ชัน handleGetMoisture ที่บอร์ด ESP32 เพื่อขอรับเฉพาะค่าตัวเลขกลับมาแสดงผล

อัปเกรด Web Dashboard ให้เข้าใช้งานง่ายขึ้น ด้วย Static IP และ mDNS

ตอนนี้นักศึกษาทำระบบ Smart Farm ที่มีหน้าเว็บสวยงามและสั่งงานผ่านมือถือได้แล้ว แต่ในโลกของการทำงานจริง หากต้องมาเปิดคอมพิวเตอร์เพื่อดูหน้าจอ Serial Monitor ว่า “วันนี้บอร์ด ESP32 ได้ IP อะไรไปนะ” ถือเป็นเรื่องที่ปวดหัวมาก

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

🅰️ เทคนิคที่ 1 Static IP (การล็อคเลข IP ให้ถาวร)

โดยปกติแล้ว Router อินเทอร์เน็ตที่บ้านเราจะมีระบบที่เรียกว่า DHCP ซึ่งทำหน้าที่เหมือนพนักงานต้อนรับที่คอยแจกป้ายเลขที่ (IP Address) ใหม่ให้กับอุปกรณ์ทุกตัวที่เพิ่งเชื่อมต่อเข้ามา นั่นแปลว่า… ถ้าวันนี้บ้านเราไฟดับ หรือเราถอดปลั๊กบอร์ด ESP32 แล้วเสียบใหม่ บอร์ดของเราอาจจะโดนย้ายไปอยู่เลขที่ใหม่แทน ทำให้เราเข้าเว็บผ่านเลขเดิมไม่ได้

แก้ด้วย (Static IP) เราจะเขียนคำสั่งบอก Router ไปเลยว่า “ฉันขอจองเลขนี้ ห้ามเอาไปแจกให้อุปกรณ์อื่น” ทำให้เราสามารถพิมพ์เลขเดิม เช่น 192.168.1.100 เพื่อเข้าหน้าเว็บฟาร์มของเราได้ตลอด

🛠️ วิธีการเขียนโค้ด

คำสั่งนี้ให้นำไปวางไว้ก่อน void setup()

// กำหนดเลข IP ที่เราต้องการล็อค เช่น .100
IPAddress local_IP(192, 168, 1, 100); 

// กำหนด IP ของ Router มักจะลงท้ายด้วย .1 หรือ .254
IPAddress gateway(192, 168, 1, 1);     
IPAddress subnet(255, 255, 255, 0);

คำสั่งนี้ให้เอาไปใส่ใน void setup() ก่อนคำสั่ง WiFi.begin()

WiFi.config(local_IP, gateway, subnet); 

🅱️ เทคนิคที่ 2 mDNS (เปลี่ยนตัวเลข เป็นชื่อที่จำง่าย)

ต่อให้เราล็อคเลข IP ไว้ที่ 192.168.1.100 ได้แล้ว แต่การจะบอกให้คนงานในฟาร์ม มานั่งพิมพ์ชุดตัวเลขยาวๆในมือถือทุกครั้ง ก็ยังดูไม่เป็นมิตรกับผู้ใช้งาน (User Friendly) เท่าไหร่ใช่ไหม เหมือนเวลาเราโทรหาเพื่อน เรายังบันทึกเบอร์เป็นชื่อเพื่อน แทนการจำเลข 10 หลัก

แก้ด้วย mDNS (Multicast DNS) คือ ระบบที่เราสามารถตั้งชื่อเล่น (Hostname) ให้กับบอร์ด ESP32 ของเราได้ เช่น ถ้าเราตั้งชื่อว่า krunu-farm เวลาจะเข้าใช้งาน เราก็แค่พิมพ์ในช่องค้นหาเว็บว่า http://krunu-farm.local ระบบจะทำการแปลงชื่อนี้กลับไปเป็นเลข IP ให้อัตโนมัติ ง่ายสุดๆ ไปเลย

🛠️ วิธีการเขียนโค้ด

เพิ่ม Library ไว้บนสุดของโค้ด

#include <ESPmDNS.h> // เรียกใช้ไลบรารีสำหรับทำชื่อเล่น

เพิ่มคำสั่งเปิดใช้งาน ใน void setup() ใส่ต่อท้ายหลังจากต่อ WiFi สำเร็จแล้ว

  // เริ่มการทำงาน mDNS และตั้งชื่อที่ต้องการ (ห้ามเว้นวรรค)
  if (MDNS.begin("krunu-farm")) { 
    Serial.println("mDNS responder started");
    Serial.println("เข้าใช้งานหน้าเว็บได้ที่: http://krunu-farm.local");
  }

ข้อควรรู้เรื่อง mDNS

  • อุปกรณ์ของ Apple (iPhone, iPad, Mac) รองรับระบบนี้ 100%
  • คอมพิวเตอร์ Windows รองรับผ่านบราวเซอร์ Chrome หรือ Edge เวอร์ชั่นใหม่ๆ
  • มือถือ Android บางรุ่น/บางบราวเซอร์อาจจะยังไม่รองรับ 100% ถ้าพิมพ์แล้วไม่ขึ้น ให้กลับไปใช้เลข IP ปกติ

สอนโดย อาจารย์นุ/ครูนุ (ภานุพงศ์ สะและหมัด)

ติดต่อ Line ID : salae44476

Scroll to Top