เมื่อเราต้องการให้บอร์ดไมโครคอนโทรลเลอร์อย่าง 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
- Client (มือถือ/คอม) ส่งคำขอ (HTTP Request) ไปที่ ESP32
- Server (ESP32) ได้รับคำขอแล้วประมวลผลตามโค้ดที่เราเขียน
- 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();
}ขั้นตอนการทดลอง
- Upload โค้ด เมื่ออัปโหลดเสร็จ ให้เปิด Serial Monitor ดูหมายเลข IP Address เช่น
192.168.1.45 - เชื่อมต่อ WiFi เดียวกัน มั่นใจว่ามือถือหรือคอมพิวเตอร์ของนักศึกษา เชื่อมต่อ WiFi ชื่อเดียวกับที่ตั้งไว้ในโค้ด
- เปิด Browser พิมพ์หมายเลข IP ที่ได้ลงในช่อง URL ของ Chrome หรือ Safari แล้วกด Enter
- ดูผลลัพธ์ ข้อความ “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 วินาที
การต่อวงจร
| DHT22 | ESP32 |
| VCC | 3.3V |
| GND | GND |
| DATA | GPIO 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) + " °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°Cเป็นรหัสที่ใช้แสดงผลสัญลักษณ์ องศาเซลเซียส °C บน Browser คำว่า deg ย่อมาจากคำว่า Degree (องศา)
Lab 4.2 ระบบควบคุมปั๊มน้ำผ่านมือถือ (Remote Web Control)
เป้าหมาย เพื่อสร้างปุ่มเปิดปั๊มและปิดปั๊มบนหน้าเว็บ เมื่อกดปุ่มบนมือถือ ให้บอร์ด ESP32 สั่งงาน Relay ทันที และหน้าเว็บต้องแสดงสถานะปัจจุบันว่าตอนนี้ปั๊มเปิดหรือ ปิดอยู่
การต่อวงจร (Wiring)
| Relay Module | ESP32 |
| VCC | VIN (5V) |
| GND | GND |
| IN | GPIO 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)
- Hyperlinks as Commands ปุ่มกดที่เราเห็น จริงๆแล้วมันคือลิงก์
<a href='/pumpOn'>เมื่อเรากดปุ่ม Browser จะแอบวิ่งไปที่ที่อยู่IP_ของบอร์ด/pumpOnซึ่งจะไปบอกฟังก์ชันhandlePumpOnใน ESP32 ให้ทำงาน href“เฮช-เรฟ” ย่อมาจาก Hypertext Reference ในภาษา HTML ที่ใช้สำหรับระบุที่หมาย- 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) | VCC | 3.3V | จ่ายไฟเลี้ยงเซนเซอร์ |
| GND | GND | ต่อสายดิน | |
| AO (Analog Out) | GPIO 34 (D34) | ส่งค่าความชื้นแบบ Analog | |
| รีเลย์ (Relay) | VCC | VIN (5V) | จ่ายไฟให้ขดลวดรีเลย์ |
| GND | GND | ต่อสายดิน | |
| IN | GPIO 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

AJAX (เอ-แจ็กซ์) ย่อมาจาก Asynchronous JavaScript and XML คือ เทคนิคการแอบส่งข้อมูลไปมาหลังบ้าน โดยที่ไม่ต้องรีเฟรชหน้าเว็บใหม่ทั้งหน้า
สิ่งที่ปรับเพิ่มในโค้ด
- สร้างเส้นทาง (Route) ใหม่
/dataสำหรับส่งแค่ตัวเลขความชื้นออกไป (ไม่ส่งหน้าเว็บทั้งหน้า) - เพิ่ม 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();
}💡 จุดที่เพิ่มเข้ามา
- เอาปุ่ม “กดเพื่ออัปเดต” ออกไปแล้ว เพราะตอนนี้เราใช้ JavaScript (AJAX) ทำงานแทนเราทุกๆ 2 วินาทีแบบอัตโนมัติ หน้าจอจะไม่กระพริบอีกต่อไป
- การตั้งชื่อเป้าหมาย (
id='moistureValue') ในภาษา HTML ถ้าเราต้องการให้ JavaScript มาทำงานกับข้อความตรงไหน เราต้องติดป้ายชื่อ (id) ไว้ตรงนั้นก่อน เหมือนเป็นการบอกพิกัดให้ JavaScript รู้ว่าต้องเอาตัวเลขใหม่ไปทับตรงจุดไหน - คำสั่ง
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 ปกติ
Project 2.2 ระบบวัดระดับน้ำอัจฉริยะ (Smart Water Level Monitor)
สำหรับ Project 2.2 นี้ จะเป็นระบบที่ช่วยแก้ปัญหาเรื่องน้ำหมดถังหรือน้ำล้นได้อย่างแม่นยำ โดยเราจะใช้ Ultrasonic Sensor (HC-SR04) มาทำหน้าที่เป็นไม้บรรทัดดิจิทัลคอยวัดระดับน้ำจากปากถังลงไป

โปรเจคนี้จะเน้นการเรียนรู้เรื่องการคำนวณระยะทางจากคลื่นเสียงและการนำค่าที่ได้ไปสั่งงานอุปกรณ์จริงผ่าน Serial Monitor
แนวคิด: ติดตั้งเซนเซอร์ไว้ที่ฝาถังน้ำ เมื่อน้ำน้อยกว่าเกณฑ์ที่กำหนด ให้รีเลย์สั่งเปิดปั๊มน้ำ และเมื่อน้ำเต็มให้สั่งปิด พร้อมมีเสียงเตือนผ่าน Buzzer ครับ
1. การต่อวงจร (Wiring)
| อุปกรณ์ | ขาเซนเซอร์ | ต่อเข้ากับ ESP32 | หน้าที่ |
| Ultrasonic | VCC | 5V (VIN) | จ่ายไฟเลี้ยง (เซนเซอร์รุ่นนี้ใช้ 5V จะเสถียรกว่า) |
| GND | GND | ต่อสายดิน | |
| Trig | GPIO 5 | ส่งสัญญาณเสียงออกไป | |
| Echo | GPIO 18 | รับสัญญาณเสียงสะท้อนกลับ | |
| Relay | IN | GPIO 2 | สั่งงานปั๊มน้ำ |
| Buzzer | + | GPIO 19 | ส่งเสียงเตือนเมื่อน้ำวิกฤต |
2. โค้ดโปรแกรมแบบละเอียด (Full Serial Monitor Version)
อาจารย์ Nu สามารถให้นักศึกษาตั้งค่าตัวแปร tankHeight ตามขนาดถังจริงที่ใช้ทดลองได้เลยครับ
C++
// --- กำหนดขาอุปกรณ์ ---
const int trigPin = 5;
const int echoPin = 18;
const int relayPin = 2;
const int buzzerPin = 19;
// --- ตั้งค่าขนาดถัง (หน่วยเซนติเมตร) ---
const int tankHeight = 20; // ความสูงของถังน้ำ
const int lowWaterGap = 15; // ระยะห่างที่ถือว่าน้ำน้อย (เปิดปั๊ม)
const int fullWaterGap = 5; // ระยะห่างที่ถือว่าน้ำเต็ม (ปิดปั๊ม)
void setup() {
Serial.begin(115200);
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
pinMode(relayPin, OUTPUT);
pinMode(buzzerPin, OUTPUT);
digitalWrite(relayPin, LOW);
Serial.println("--- ระบบวัดระดับน้ำ เริ่มทำงาน ---");
}
void loop() {
// 1. อ่านค่าระยะทางแบบหาค่าเฉลี่ย 5 ครั้งเพื่อความนิ่ง
long totalDuration = 0;
int validCount = 0;
for(int i = 0; i < 5; i++) {
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// ใส่ Timeout 25000us ป้องกันค่าค้างที่ 799 cm
long duration = pulseIn(echoPin, HIGH, 25000);
if (duration > 0) {
totalDuration += duration;
validCount++;
}
delay(10);
}
// 2. คำนวณค่าเฉลี่ยและระยะทาง
long avgDuration = (validCount > 0) ? (totalDuration / validCount) : 0;
int distance = avgDuration * 0.034 / 2;
// 3. คำนวณความลึกน้ำ
int waterDepth = tankHeight - distance;
if (waterDepth < 0 || avgDuration == 0) waterDepth = 0;
if (distance > tankHeight) waterDepth = 0;
// 4. แสดงผลบน Serial Monitor
Serial.print("เวลาสะท้อน : "); Serial.print(avgDuration);
Serial.print(" us (ไมโครวินาที) | ระยะห่าง : "); Serial.print(distance);
Serial.print(" ซม. | ความลึกน้ำ : "); Serial.print(waterDepth);
Serial.println(" ซม.");
// 5. ระบบควบคุมและแจ้งเตือน (Logic)
if (avgDuration == 0) {
Serial.println(">> [ข้อผิดพลาด] : ตรวจไม่พบเซนเซอร์ / อยู่นอกระยะ!");
}
else if (distance >= lowWaterGap) {
digitalWrite(relayPin, HIGH); // เปิดปั๊ม
Serial.println(">> [สถานะ] : น้ำน้อย - กำลังเปิดปั๊มน้ำ...");
// เสียงเตือนสั้นๆ
digitalWrite(buzzerPin, HIGH); delay(100); digitalWrite(buzzerPin, LOW);
}
else if (distance <= fullWaterGap) {
digitalWrite(relayPin, LOW); // ปิดปั๊ม
Serial.println(">> [สถานะ] : น้ำเต็มถัง - ปิดปั๊มน้ำเรียบร้อย");
}
Serial.println("---------------------------------------");
delay(1500);
}3. จุดที่ต้องเน้นให้นักศึกษาเรียนรู้ (Computing & Technology)
- The Math of Sound: สอนให้นักศึกษาเข้าใจว่าคอมพิวเตอร์ไม่ได้ “เห็น” ระยะทาง แต่มัน “นับเวลา” ตัวเลข
0.034มาจากความเร็วเสียงในอากาศประมาณ 340 เมตร/วินาที แปลงเป็นหน่วยเซนติเมตรต่อไมโครวินาทีนั่นเองครับ - Logic Mapping: ฝึกให้นักศึกษาคิดแบบย้อนกลับ (Inverse Thinking) ระยะทางที่เซนเซอร์วัดได้ ยิ่งน้อย หมายความว่าน้ำ ยิ่งเต็ม ซึ่งเป็นบททดสอบตรรกะที่ดีสำหรับนักศึกษา Computing ครับ
- Real-world Physics: ระยะห่างจากเซนเซอร์ถึงน้ำ ไม่ควรให้โดนน้ำโดยตรง (ระยะเผื่อเลือกเป็น
fullWaterGap) เพื่อป้องกันความชื้นเข้าทำลายแผงวงจร
4. การต่อยอดที่โรงเรียนสุเหร่าคลองใหญ่ และ COCONUT Thailand
อาจารย์ Nu สามารถให้นักศึกษาลองประหยัดทรัพยากรเพิ่มขึ้นได้ครับ:
Night Mode: ใช้เซนเซอร์แสง (LDR) ร่วมด้วย เพื่อสั่งห้ามปั๊มน้ำทำงานในเวลากลางคืนเพื่อลดเสียงรบกวนในบริเวณโรงเรียนครับ
Safety First: เขียนเงื่อนไขเพิ่มว่า “ถ้าปั๊มน้ำทำงานเกิน 5 นาทีแล้วน้ำยังไม่เพิ่มขึ้น ให้ตัดการทำงานและส่งเสียงเตือนยาวๆ” (ป้องกันท่อน้ำแตกหรือปั๊มไหม้)
สอนโดย อาจารย์นุ/ครูนุ (ภานุพงศ์ สะและหมัด)
ติดต่อ Line ID : salae44476
