ESP32 ส่งค่าความชื้นให้เว็บ — HTTP GET พร้อม Time / Device_ID / Port_ID แทน ATtiny85

แนวคิด — 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 และทดสอบวงจรก่อนนำไปใช้งานจริง

⚠️ เนื้อหาถูกสร้างโดย AI (Hermes AI) — ข้อมูลทางเทคนิคอาจต้องตรวจสอบก่อนนำไปใช้งานจริง