ระบบรดน้ำอัตโนมัติด้วย ESP32-C3 Pro mini
/*
PROJECT: ESP32-C3 Smart Farm Pro (Final Version)
BOARD: ESP32-C3 SuperMini / Pro Mini
AUTHOR: Phayoune Team
FEATURES:
- 3 Independent Watering Schedules (Time + Duration + Days)
- Real-time Web UI (AJAX)
- Fast Configuration Save (Struct/Preferences)
- OLED Display Status
- Manual Control & Emergency Stop
- WiFi Manager (Auto AP)
*/
#include <WiFi.h>
#include <WebServer.h>
#include <WiFiManager.h> // Install via Library Manager
#include <time.h>
#include <Preferences.h>
#include <ESPmDNS.h>
#include <Wire.h>
#include <Adafruit_GFX.h> // Install via Library Manager
#include <Adafruit_SSD1306.h> // Install via Library Manager
// ================= 1. HARDWARE SETTINGS (ESP32-C3 SuperMini) =================
#define OLED_SDA 8 // ขา SDA ของจอ OLED
#define OLED_SCL 9 // ขา SCL ของจอ OLED
#define RELAY_PIN 4 // ขา Relay สั่งปั๊มน้ำ
#define LED_PIN 5 // ขา LED แสดงสถานะปั๊ม (ต่อ R 220-1k ลง GND)
#define BOOT_PIN 3 // ขาปุ่มกดสำหรับ Reset WiFi (กดค้าง 6 วิ)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// ================= SYSTEM SETTINGS =================
const char* www_username = "admin";
const char* www_password = "12345678";
const char* ntpServer = "th.pool.ntp.org";
const long gmtOffset_sec = 7 * 3600;
const int daylightOffset_sec = 0;
WebServer server(80);
Preferences preferences;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Global Variables
bool manualMode = false;
bool relayState = false;
bool forceStopAuto = false;
unsigned long buttonPressStart = 0;
bool isResetting = false;
// ================= DATA STRUCTURES =================
struct RoundConfig {
int h = 0, m = 0; // เวลาเริ่ม (ชม:นาที)
int dh = 0, dm = 0, ds = 0; // ระยะเวลารด (ชม:นาที:วินาที)
bool everyday = true; // ทำงานทุกวันหรือไม่
bool days[7] = {true,true,true,true,true,true,true}; // วันที่ทำงาน (0=อาทิตย์)
};
struct SystemConfig {
RoundConfig rounds[3]; // 3 รอบการทำงาน
};
SystemConfig cfg;
const char* dayNamesTh[] = {"อา", "จ", "อ", "พ", "พฤ", "ศ", "ส"};
const char* roundNames[] = {"รอบเช้า (Morning)", "รอบบ่าย (Afternoon)", "รอบเย็น (Evening)"};
// ================= WEB PAGE GENERATION =================
String getPage(bool isLogin = false, String msg = "") {
String html = "<!DOCTYPE html><html lang='th'><head>";
html += "<meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<title>Smart Farm Pro</title>";
html += "<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css' rel='stylesheet'>";
html += "<style>";
html += "body { background-color:#f0f2f5; font-family:'Sarabun', sans-serif; }";
html += ".container { max-width:600px; padding-top:15px; }";
html += ".card { margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); border: none; }";
html += ".big-text { font-size: 1.2rem; font-weight: bold; }";
html += ".ip-text { font-size: 1.1rem; color: #555; }";
html += ".dur-input { min-width:60px; }";
html += ".day-opt { margin-right:5px; font-size: 0.9rem; }";
html += "</style>";
html += "</head><body><div class='container'>";
if (isLogin) {
// --- Login Page ---
html += "<div class='card'><div class='card-header bg-primary text-white'><h4>เข้าสู่ระบบ</h4></div><div class='card-body'>";
if(msg != "") html += "<div class='alert alert-danger'>" + msg + "</div>";
html += "<form action='/login' method='POST'>";
html += "<div class='mb-3'><label>Username</label><input type='text' name='user' class='form-control' required></div>";
html += "<div class='mb-3'><label>Password</label><input type='password' name='pass' class='form-control' required></div>";
html += "<button type='submit' class='btn btn-primary w-100'>Login</button></form></div></div>";
} else {
// --- Main Control Page ---
String ipAddr = WiFi.localIP().toString();
html += "<h3 class='text-center mb-3 text-primary'>Auto Watering System💦</h3>";html += "<h6 class='text-center mb-3 text-dark'>By. Phayoune Team. email:phayoune@gmail.com </h6>";
html += "<div class='card'><div class='card-body py-2 d-flex justify-content-between align-items-center'>";
html += " <span class='ip-text'>IP: " + ipAddr + "</span>";
html += " <span id='clock' class='big-text text-dark'>กำลังโหลด...</span>";
html += "</div></div>";
// Status Card
String statusClass = relayState ? "bg-success" : "bg-secondary";
String statusText = relayState ? "เปิดน้ำอยู่ (ON)" : "ปิดน้ำอยู่ (OFF)";
String displayCancel = (!manualMode && relayState) ? "block" : "none";
html += "<div id='statusCard' class='card text-white " + statusClass + "'><div class='card-body text-center py-4'>";
html += "<h1 id='statusText'>" + statusText + "</h1>";
html += "<div id='cancelBtnArea' style='display:" + displayCancel + ";' class='mt-3'>";
html += "<a href='/cancel_auto' class='btn btn-danger btn-lg w-100 shadow'>⛔ หยุดทำงานรอบนี้ทันที (Cancel)</a>";
html += "<div class='mt-2 small'>ทำงานตามตารางเวลา</div></div>";
html += "</div></div>";
// Manual Control
html += "<div class='card'><div class='card-header'>ควบคุมด้วยมือ (Manual)</div><div class='card-body text-center'>";
if (manualMode) {
html += "<a href='/toggle' class='btn btn-danger w-100 py-3'>ปิด Manual เดี๋ยวนี้</a>";
} else {
if(relayState) html += "<div id='autoWarn' class='alert alert-warning'>ระบบ Auto ทำงานอยู่</div>";
html += "<a href='/toggle' class='btn btn-success w-100 py-3'>เปิด Manual เดี๋ยวนี้</a>";
}
html += "</div></div>";
// Settings Form
html += "<form action='/save' method='POST'>";
for(int r=0; r<3; r++) {
String rName = "r" + String(r);
html += "<div class='card'><div class='card-header text-bg-info text-white'>" + String(roundNames[r]) + "</div><div class='card-body'>";
String chkEvery = cfg.rounds[r].everyday ? "checked" : "";
String displayDays = cfg.rounds[r].everyday ? "none" : "block";
// Checkbox: Every Day
html += "<div class='form-check form-switch mb-3'>";
html += "<input class='form-check-input' type='checkbox' name='" + rName + "_all' id='" + rName + "_all' " + chkEvery + " onchange='toggleDays(\"" + rName + "\")'>";
html += "<label class='form-check-label fw-bold' for='" + rName + "_all'>ทำงานทุกวัน (Every Day)</label>";
html += "</div>";
// Checkbox: Select Days
html += "<div id='div_" + rName + "' style='display:" + displayDays + ";' class='mb-3 p-2 border rounded bg-light'>";
html += "<small class='text-muted d-block mb-2'>เลือกวันที่ต้องการ:</small>";
html += "<div class='d-flex flex-wrap'>";
for(int d=0; d<7; d++) {
String chkDay = cfg.rounds[r].days[d] ? "checked" : "";
html += "<div class='form-check form-check-inline day-opt'>";
html += "<input class='form-check-input' type='checkbox' name='" + rName + "_d" + String(d) + "' " + chkDay + ">";
html += "<label class='form-check-label'>" + String(dayNamesTh[d]) + "</label>";
html += "</div>";
}
html += "</div></div>";
// Time Inputs
html += "<div class='input-group mb-2'><span class='input-group-text'>เริ่ม</span>";
html += "<input type='number' name='" + rName + "_h' class='form-control' value='" + String(cfg.rounds[r].h) + "' min='0' max='23'>";
html += "<span class='input-group-text'>:</span>";
html += "<input type='number' name='" + rName + "_m' class='form-control' value='" + String(cfg.rounds[r].m) + "' min='0' max='59'></div>";
html += "<div class='input-group mb-1'><span class='input-group-text'>นาน</span>";
html += "<input type='number' name='" + rName + "_dh' class='form-control dur-input' value='" + String(cfg.rounds[r].dh) + "' min='0' placeholder='ชม.'>";
html += "<span class='input-group-text'>:</span>";
html += "<input type='number' name='" + rName + "_dm' class='form-control dur-input' value='" + String(cfg.rounds[r].dm) + "' min='0' max='59' placeholder='น.'>";
html += "<span class='input-group-text'>:</span>";
html += "<input type='number' name='" + rName + "_ds' class='form-control dur-input' value='" + String(cfg.rounds[r].ds) + "' min='0' max='59' placeholder='วิ.'></div>";
html += "</div></div>";
}
html += "<button type='submit' class='btn btn-warning w-100 mt-2 p-3 shadow'><b>บันทึกการตั้งค่าทั้งหมด</b></button>";
html += "</form>";
// Logout & Reset Section
html += "<div class='mt-4'>";
html += "<a href='/logout' class='btn btn-outline-danger w-100 mb-3'>ออกจากระบบ (Logout)</a>";
html += "<div class='text-center'><a href='/resetwifi' class='text-danger small' onclick=\"return confirm('Reset WiFi?');\">Reset WiFi Settings</a></div>";
html += "</div><div class='mb-5'></div>";
// --- JavaScript ---
html += "<script>";
// Logic for toggling day selection
html += "function toggleDays(roundName) {";
html += " var chk = document.getElementById(roundName + '_all');";
html += " var div = document.getElementById('div_' + roundName);";
html += " if(chk.checked) { div.style.display = 'none'; } else { div.style.display = 'block'; }";
html += "}";
// Logic for Real-time Update
html += "function updateSystem() {";
html += " var d = new Date();";
html += " var options = { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };";
html += " document.getElementById('clock').innerHTML = d.toLocaleDateString('th-TH', options);";
html += " fetch('/status').then(r => r.json()).then(data => {";
html += " var card = document.getElementById('statusCard');";
html += " var txt = document.getElementById('statusText');";
html += " var cancelArea = document.getElementById('cancelBtnArea');";
html += " var warn = document.getElementById('autoWarn');";
html += " if(data.relay == 1) {";
html += " card.className = 'card text-white bg-success';";
html += " txt.innerHTML = 'เปิดน้ำอยู่ (ON)';";
html += " if(data.manual == 0) { cancelArea.style.display = 'block'; } else { cancelArea.style.display = 'none'; }";
html += " } else {";
html += " card.className = 'card text-white bg-secondary';";
html += " txt.innerHTML = 'ปิดน้ำอยู่ (OFF)';";
html += " cancelArea.style.display = 'none';";
html += " }";
html += " if(data.relay == 0 && warn) warn.style.display = 'none';";
html += " }).catch(e => console.log(e));";
html += "}";
html += "setInterval(updateSystem, 1000);"; // Update every 1 sec
html += "updateSystem();";
html += "</script>";
}
html += "</div></body></html>";
return html;
}
// ================= HANDLERS =================
// API: Send JSON status to browser
void handleGetStatus() {
String json = "{";
json += "\"relay\":" + String(relayState ? 1 : 0) + ",";
json += "\"manual\":" + String(manualMode ? 1 : 0);
json += "}";
server.send(200, "application/json", json);
}
// Security Check
bool checkAuth() {
if (server.hasHeader("Cookie")) {
String cookie = server.header("Cookie");
if (cookie.indexOf("ESPSESSIONID=12345678") != -1) return true;
}
return false;
}
void showBootStatus(String status1, String status2) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 10);
display.println(">> C3 FINAL BOOT <<");
display.setCursor(0, 30);
display.println(status1);
display.setCursor(0, 45);
display.println(status2);
display.display();
}
void updateOLED() {
if (isResetting) return;
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){ return; }
char dateStr[20];
char timeStr[10];
strftime(dateStr, 20, "%d %b %Y", &timeinfo);
strftime(timeStr, 10, "%H:%M:%S", &timeinfo);
display.clearDisplay();
display.fillRect(0, 0, 128, 16, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
display.setCursor(2, 4);
display.print(WiFi.localIP());
display.setCursor(95, 4);
if (manualMode) display.print("MAN");
else if (forceStopAuto) display.print("STP");
else display.print("AUT");
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(18, 25);
display.print(timeStr);
display.setTextSize(1);
display.setCursor(30, 48);
display.print(dateStr);
if(relayState) display.fillCircle(120, 56, 4, SSD1306_WHITE);
else display.drawCircle(120, 56, 4, SSD1306_WHITE);
display.display();
}
void handleLogin() {
String msg = "";
if (server.hasArg("user") && server.hasArg("pass")) {
if (server.arg("user") == www_username && server.arg("pass") == www_password) {
server.sendHeader("Location", "/");
server.sendHeader("Set-Cookie", "ESPSESSIONID=12345678; Path=/");
server.send(303);
return;
} else { msg = "User/Pass Incorrect"; }
}
server.send(200, "text/html", getPage(true, msg));
}
void handleRoot() {
if (!checkAuth()) { server.sendHeader("Location", "/login"); server.send(303); return; }
server.send(200, "text/html", getPage(false));
}
void handleSave() {
if (!checkAuth()) return server.send(403, "text/plain", "Forbidden");
// Save all 3 rounds
for(int r=0; r<3; r++) {
String rName = "r" + String(r);
cfg.rounds[r].everyday = server.hasArg(rName + "_all");
for(int d=0; d<7; d++) {
cfg.rounds[r].days[d] = server.hasArg(rName + "_d" + String(d));
}
cfg.rounds[r].h = server.arg(rName + "_h").toInt();
cfg.rounds[r].m = server.arg(rName + "_m").toInt();
cfg.rounds[r].dh = server.arg(rName + "_dh").toInt();
cfg.rounds[r].dm = server.arg(rName + "_dm").toInt();
cfg.rounds[r].ds = server.arg(rName + "_ds").toInt();
}
// Fast Save
preferences.begin("sys_cfg_v2", false);
preferences.putBytes("data", &cfg, sizeof(cfg));
preferences.end();
forceStopAuto = false;
server.sendHeader("Location", "/"); server.send(303);
}
void handleToggle() {
if (!checkAuth()) return server.send(403, "text/plain", "Forbidden");
manualMode = !manualMode;
relayState = manualMode ? true : false;
if(manualMode) forceStopAuto = false;
digitalWrite(RELAY_PIN, relayState ? HIGH : LOW);
digitalWrite(LED_PIN, relayState ? HIGH : LOW);
server.sendHeader("Location", "/"); server.send(303);
}
void handleCancelAuto() {
if (!checkAuth()) return server.send(403, "text/plain", "Forbidden");
if (!manualMode && relayState) {
forceStopAuto = true;
relayState = false;
digitalWrite(RELAY_PIN, LOW);
digitalWrite(LED_PIN, LOW);
}
server.sendHeader("Location", "/"); server.send(303);
}
void handleLogout() {
server.sendHeader("Set-Cookie", "ESPSESSIONID=0; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
server.sendHeader("Location", "/login"); server.send(303);
}
void handleResetWifi() {
if (!checkAuth()) return server.send(403, "text/plain", "Forbidden");
WiFiManager wm; wm.resetSettings(); ESP.restart();
}
void configModeCallback (WiFiManager *myWiFiManager) {
showBootStatus("Setup WiFi", "AP: Phayoune_Setup");
}
// ================= SETUP =================
void setup() {
delay(1000);
Serial.begin(115200);
// Init Hardware
Wire.begin(OLED_SDA, OLED_SCL);
pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW);
pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);
pinMode(BOOT_PIN, INPUT_PULLUP);
// Init Display
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 failed"));
}
showBootStatus("INITIALIZING", "C3 Final System");
delay(1000);
// Load Config
preferences.begin("sys_cfg_v2", true);
if (preferences.isKey("data")) {
preferences.getBytes("data", &cfg, sizeof(cfg));
} else {
// Default Values
cfg.rounds[0].h = 8; // 8:00
cfg.rounds[1].h = 12; // 12:00
cfg.rounds[2].h = 17; // 17:00
}
preferences.end();
// WiFi Manager
WiFiManager wifiManager;
wifiManager.setAPCallback(configModeCallback);
wifiManager.setConfigPortalTimeout(180);
showBootStatus("CONNECTING...", "WiFi");
if(!wifiManager.autoConnect("Phayoune_Setup", "password")) {
showBootStatus("FAILED", "Restarting...");
delay(2000);
ESP.restart();
}
// Time Sync
showBootStatus("ONLINE!", "Syncing Time...");
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
if (!MDNS.begin("phayoune")) { Serial.println("Error mDNS"); }
// Server Routing
const char *headerkeys[] = {"Cookie"};
server.collectHeaders(headerkeys, 1);
server.on("/", handleRoot);
server.on("/login", handleLogin);
server.on("/save", handleSave);
server.on("/toggle", handleToggle);
server.on("/cancel_auto", handleCancelAuto);
server.on("/status", handleGetStatus);
server.on("/logout", handleLogout);
server.on("/resetwifi", handleResetWifi);
server.begin();
}
// ================= LOOP =================
void loop() {
server.handleClient();
static unsigned long lastDisplayUpdate = 0;
if(millis() - lastDisplayUpdate > 1000) {
lastDisplayUpdate = millis();
updateOLED();
}
// HARDWARE RESET WIFI (Button Press > 6 sec)
if (digitalRead(BOOT_PIN) == LOW) {
if (buttonPressStart == 0) buttonPressStart = millis();
if (millis() - buttonPressStart > 6000 && !isResetting) {
isResetting = true;
display.clearDisplay(); display.setCursor(0, 30); display.println("RESETTING WIFI..."); display.display();
WiFiManager wm; wm.resetSettings(); delay(1000); ESP.restart();
}
} else {
buttonPressStart = 0;
}
// ================= MAIN LOGIC =================
if (!manualMode) {
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
long currentSec = (timeinfo.tm_hour * 3600) + (timeinfo.tm_min * 60) + timeinfo.tm_sec;
bool shouldBeOn = false;
// Check all 3 rounds
for(int r=0; r<3; r++) {
bool isDayActive = cfg.rounds[r].everyday || cfg.rounds[r].days[timeinfo.tm_wday];
if (isDayActive) {
long start = (cfg.rounds[r].h * 3600) + (cfg.rounds[r].m * 60);
long duration = (cfg.rounds[r].dh * 3600) + (cfg.rounds[r].dm * 60) + cfg.rounds[r].ds;
long end = start + duration;
if (currentSec >= start && currentSec < end) {
shouldBeOn = true;
}
}
}
// Relay Control Logic
if (!shouldBeOn) {
forceStopAuto = false;
if(relayState) {
relayState = false;
digitalWrite(RELAY_PIN, LOW);
digitalWrite(LED_PIN, LOW);
}
} else {
if (!forceStopAuto && !relayState) {
relayState = true;
digitalWrite(RELAY_PIN, HIGH);
digitalWrite(LED_PIN, HIGH);
}
else if (forceStopAuto && relayState) {
relayState = false;
digitalWrite(RELAY_PIN, LOW);
digitalWrite(LED_PIN, LOW);
}
}
}
}
}
📋 สรุปการต่อสาย (Wiring Diagram)