แนวคิด — ESP32 ส่งค่าความชื้นให้เว็บ Server
จากบทความที่แล้วเราใช้ ATtiny85 ทำกล่องวัดความชื้นแบบ standalone (LED/OLED) ที่อ่านค่าได้เฉพาะหน้ากล่องเท่านั้น
คราวนี้ลองเปลี่ยนมาใช้ ESP32 แทน — โดย ESP32 จะเชื่อมต่อ WiFi แล้วส่งค่าความชื้นจาก Capacitive Sensor ไปยังเว็บเซิร์ฟเวอร์ของคุณ ผ่าน HTTP GET แบบ Real-time:
GET /api/moisture.php?time=2026-06-11T14:30:00&moisture=450&device_id=sensor01&port_id=A0
ข้อดีของการส่งเข้าบล็อก/เว็บ:
- ดูประวัติค่าความชื้นย้อนหลังเป็นกราฟได้
- แจ้งเตือนเมื่อดินแห้งผ่าน LINE/Telegram
- บันทึกเวลาเริ่มรดน้ำ-หยุดรดน้ำ
- ไม่ต้องเดินไปดูกล่องทีละกระถาง
- ใช้ WiFi ในบ้าน — สายส่งข้อมูลแค่ 2 เสน (ไฟ + GND)
ESP32 Moisture Sensor — แผนผังระบบ
แบตมอเตอร์ไซค์ 12V 7Ah
|
[Fuse 3A]
|
Step Down 12V→5V (LM2596)
ESP32 Development Board (ESP32-WROOM-32)
|
+-- 3.3V -- Capacitive Sensor v1.2 (VCC)
+-- GND -- Capacitive Sensor v1.2 (GND)
+-- GPIO34 (ADC) -- Capacitive Sensor v1.2 (SIG/AOUT)
|
[WiFi → Router → Internet]
|
HTTPS GET → bc221.duckdns.org/api/moisture.php
?time=2026-06-11T14:30:00
&moisture=450
&device_id=sensor01
&port_id=A0
อุปกรณ์ต่อชุด (12V Input, ESP32)
| อุปกรณ์ | ราคา |
|---|---|
| ESP32 Dev Board (ESP32-WROOM-32, 30-pin) | ~120-200 บาท |
| Capacitive Soil Sensor v1.2 | ~50-80 บาท |
| Step Down 12V→5V (LM2596) | ~30-50 บาท |
| Resistor 10kΩ (Pull-down ADC) | ~2 บาท |
| Capacitor 100µF (ลด Noise) | ~5 บาท |
| กล่องกันน้ำ ABS 6x4x3 cm | ~20-50 บาท |
| สายไฟ + Terminal + Header | ~10-20 บาท |
| ราคาต่อชุด (รวม Sensor) | ~237-407 บาท |
💡 เทียบกับ ATtiny85 (~185-335 บ.) — ESP32 แพงกว่าเล็กน้อย (~50-70 บ.) แต่ได้ WiFi + ส่งค่าให้เว็บ + ตั้งค่า OTA ได้ + แก้ไข Over-the-Air โดยไม่ต้องถอดกล่อง!
ESP32 Arduino Code — ส่งค่า HTTP GET
ใช้ Arduino IDE 2.x เขียนลง ESP32 (Board: ESP32 Dev Module):
/*
* ESP32 Moisture Sensor — HTTP GET to Web Server
* ใช้ Capacitive Soil Sensor v1.2
* ส่ง: time, moisture, device_id, port_id
*
* ⚠️ เนื้อหาค้นคว้าและเรียบเรียงโดย AI — ตรวจสอบก่อนนำไปใช้
*/
#include <WiFi.h>
#include <HTTPClient.h>
// ==== WiFi ====
const char* ssid = "your_wifi_name";
const char* password = "your_wifi_password";
// ==== Server ====
const char* serverUrl = "https://bc221.duckdns.org/api/moisture.php";
// ==== Device Config ====
const char* device_id = "sensor01"; // เปลี่ยนตามกล่อง
const char* port_id = "A0"; // พอร์ต ADC ที่ใช้
// ==== Sensor ====
#define SENSOR_PIN 34 // GPIO34 (ADC1_CH6)
#define DRY_LEVEL 3000 // ค่าแห้ง (ปรับตาม Sensor)
#define WET_LEVEL 1200 // ค่าเปียก (ปรับตาม Sensor)
// ==== Timing ====
unsigned long lastSend = 0;
const unsigned long SEND_INTERVAL = 60000; // ส่งทุก 60 วินาที
void setup() {
Serial.begin(115200);
// WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n✅ WiFi Connected");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\n❌ WiFi Failed! Restarting...");
ESP.restart();
}
analogReadResolution(12); // 0-4095
}
void loop() {
unsigned long now = millis();
if (now - lastSend >= SEND_INTERVAL) {
lastSend = now;
int raw = analogRead(SENSOR_PIN);
float voltage = (raw / 4095.0) * 3.3;
Serial.print("Raw: ");
Serial.print(raw);
Serial.print(" | Voltage: ");
Serial.print(voltage, 3);
Serial.print("V | Status: ");
if (raw > DRY_LEVEL) {
Serial.println("🔴 แห้ง");
} else if (raw < WET_LEVEL) {
Serial.println("🟢 ชื้น");
} else {
Serial.println("🟡 ปานกลาง");
}
// ส่ง HTTP GET
sendMoisture(raw);
}
}
void sendMoisture(int value) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("❌ WiFi disconnected, skipping");
return;
}
HTTPClient http;
String url = String(serverUrl) +
"?time=" + getTimeStr() +
"&moisture=" + String(value) +
"&device_id=" + String(device_id) +
"&port_id=" + String(port_id);
http.begin(url);
int httpCode = http.GET();
if (httpCode > 0) {
Serial.print("✅ HTTP ");
Serial.print(httpCode);
String response = http.getString();
if (response.length() < 100) {
Serial.print(" | Response: ");
Serial.println(response);
} else {
Serial.println(" | (response too long)");
}
} else {
Serial.print("❌ HTTP Error: ");
Serial.println(http.errorToString(httpCode).c_str());
}
http.end();
}
String getTimeStr() {
// ใช้เวลาจาก NTP — ต้องเรียก configTime() ใน setup
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
return "unknown";
}
char buf[25];
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &timeinfo);
return String(buf);
}
เพิ่ม NTP — ให้ ESP32 รู้เวลา
เพิ่มใน setup() ก่อน loop:
void setup() {
// ... WiFi connection ...
// NTP — ตั้งเวลาให้ ESP32
configTime(7 * 3600, 0, "pool.ntp.org", "time.nist.gov");
Serial.print("Waiting for NTP");
time_t now = time(nullptr);
int ntries = 0;
while (now < 100000 && ntries < 20) {
delay(500);
now = time(nullptr);
ntries++;
}
Serial.println(now > 100000 ? " ✅" : " ⚠️ Fail");
}
Server Side — PHP รับค่าความชื้น
สร้างไฟล์ /api/moisture.php ในโฮสต์ของคุณ:
<?php
/**
* API endpoint — รับค่าความชื้นจาก ESP32
* เรียก: GET /api/moisture.php
* ?time=2026-06-11T14:30:00
* &moisture=450
* &device_id=sensor01
* &port_id=A0
*
* ⚠️ เนื้อหาค้นคว้าและเรียบเรียงโดย AI — ตรวจสอบก่อนนำไปใช้
*/
$db_host = 'localhost';
$db_user = 'your_user';
$db_pass = 'your_pass';
$db_name = 'your_database';
$table = 'moisture_log';
$pdo = new PDO(
"mysql:host=$db_host;dbname=$db_name;charset=utf8mb4",
$db_user, $db_pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// ==== Validate Input ====
$time = $_GET['time'] ?? date('Y-m-d\TH:i:s');
$moisture = isset($_GET['moisture']) ? (int)$_GET['moisture'] : 0;
$device_id = $_GET['device_id'] ?? 'unknown';
$port_id = $_GET['port_id'] ?? 'unknown';
if ($moisture < 0 || $moisture > 4095) {
http_response_code(400);
die('Invalid moisture value');
}
// ==== Insert ====
$stmt = $pdo->prepare("
INSERT INTO $table (time, moisture, device_id, port_id, created_at)
VALUES (?, ?, ?, ?, NOW())
");
$stmt->execute([$time, $moisture, $device_id, $port_id]);
echo "OK";
สร้างตาราง MySQL — moisture_log
CREATE TABLE moisture_log (
id INT AUTO_INCREMENT PRIMARY KEY,
time DATETIME NOT NULL,
moisture INT NOT NULL,
device_id VARCHAR(50) NOT NULL,
port_id VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device_time (device_id, time),
INDEX idx_time (time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ดูค่าล่าสุด — PHP Dashboard ง่ายๆ
<?php
// dashboard.php — แสดงค่าความชื้นล่าสุดของทุก Device
$pdo = new PDO("mysql:host=localhost;dbname=your_db;charset=utf8mb4", "user", "pass");
$devices = $pdo->query("
SELECT *
FROM moisture_log m1
WHERE m1.id = (
SELECT MAX(m2.id)
FROM moisture_log m2
WHERE m2.device_id = m1.device_id
)
ORDER BY m1.device_id
")->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head><title>🌱 Soil Moisture</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{font-family:sans-serif;background:#0d1117;color:#c9d1d9;max-width:600px;margin:0 auto;padding:20px;}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;margin-bottom:10px;}
.wet{color:#3fb950;}
.mid{color:#d29922;}
.dry{color:#f85149;}
.time{color:#8b949e;font-size:0.85rem;}
</style>
</head>
<body>
<h1>🌱 Soil Moisture Status</h1>
<?php foreach ($devices as $d):
$val = (int)$d['moisture'];
$status = $val > 3000 ? '🔴 dry' : ($val < 1200 ? '🟢 wet' : '🟡 mid');
$cls = $val > 3000 ? 'dry' : ($val < 1200 ? 'wet' : 'mid');
?>
<div class="card">
<strong>📡 <?=htmlspecialchars($d['device_id'])?> — Port <?=htmlspecialchars($d['port_id'])?></strong><br>
<span class="<?=$cls?>" style="font-size:1.5rem;"><?=$val?></span> <?=$status?><br>
<span class="time">🕐 <?=$d['time']?></span>
</div>
<?php endforeach; ?>
</body>
</html>
เปรียบเทียบ — ATtiny85 vs ESP32
| คุณสมบัติ | ATtiny85 (Standalone) | ESP32 (Web) |
|---|---|---|
| ราคาต่อชุด (รวม Sensor) | ~185-335 บาท 🏆 | ~237-407 บาท |
| ดูค่าที่หน้ากล่อง | ✅ LED / OLED | ❌ ไม่มีจอ (หรือเพิ่ม OLED ได้) |
| ดูค่าผ่านเว็บ | ❌ ไม่ได้ | ✅ ส่ง HTTP GET |
| ดูประวัติย้อนหลัง | ❌ ไม่ได้ | ✅ เก็บใน MySQL |
| แจ้งเตือน LINE | ❌ ไม่ได้ | ✅ Webhook ต่อได้ |
| ใช้ WiFi | ❌ ไม่ต้อง | ✅ ต้องใช้ WiFi ในบ้าน |
| กินไฟ | ~5mA 🏆 | ~80mA (Deep Sleep ~10µA) |
| แก้โปรแกรม | ต้องใช้ ISP Programmer | ✅ OTA (Over-the-Air) |
| หลาย Device | แต่ละกล่องแยกกัน | แต่ละกล่องแยก device_id |
Deep Sleep — ยืดอายุแบตสำหรับ ESP32
ถ้าต้องการให้ ESP32 อยู่ได้นานหลายเดือน ใช้ Deep Sleep ตื่นมาส่งค่าทุก 30 นาที:
// เรียกหลัง sendMoisture() เสร็จ
void goToSleep() {
Serial.println("😴 Deep Sleep 30 min...");
esp_sleep_enable_timer_wakeup(30 * 60 * 1000000ULL); // 30 นาที
esp_deep_sleep_start();
}
// loop() จะไม่รันอีก — ESP32 เริ่มต้นใหม่ทุกครั้งที่ตื่น
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
// ... ที่เหลือเหมือนเดิม ...
readSensor();
sendMoisture(value);
goToSleep(); // เข้า Deep Sleep
}
Deep Sleep กินไฟเพียง ~10µA — แบต 12V 7Ah อยู่ได้หลายเดือน!
ข้อควรระวัง
- ⚠️ ESP32 ใช้ไฟ 5V จาก Step Down — อย่าต่อ 12V เข้า ESP32 โดยตรง!
- ⚠️ Capacitive Sensor v1.2 ใช้ไฟ 3.3V — ESP32 จ่าย 3.3V ได้จาก Pin ของบอร์ด
- ⚠️ ADC ของ ESP32 มีความแม่นยำ ±6% — ควรเทียบกับค่าจริง (Calibrate)
- ⚠️ WiFi สัญญาณต้องถึงจุดติดตั้ง — ถ้าไกลเกิน ใช้ ESP32-WROOM-32 + External Antenna
- ⚠️ กันน้ำกล่องให้ดี — ความชื้นเข้า ESP32 ได้
📝 สรุป: ESP32 แบบส่งค่า HTTP ไปเว็บนี้เหมาะกับคนที่ต้องการดูประวัติย้อนหลัง ดูผ่านมือถือ หรือแจ้งเตือนอัตโนมัติ — ส่วน ATtiny85 แบบ Standalone ถูกและง่ายกว่าถ้าแค่เดินไปดูหน้ากล่อง
📖 เนื้อหาค้นคว้าและเรียบเรียงโดย Hermes AI — กรุณาตรวจสอบ datasheet และทดสอบวงจรก่อนนำไปใช้งานจริง