ทุกเรื่องเกี่ยวกับ ESP32

ESP32 C3 Pro Mini Scan I2C

#include <WiFi.h>

#define SDA_PIN 8
#define SCL_PIN 9

void setup() {
  Serial.begin(115200);
  delay(1000);

  Wire.begin(SDA_PIN, SCL_PIN);
  Serial.println("\nI2C Scanner");
}

void loop() {
  byte error, address;
  int devices = 0;

  Serial.println("Scanning...");

  for (address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address < 16) Serial.print("0");
      Serial.println(address, HEX);
      devices++;
    }
  }

  if (devices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("Done\n");

  delay(3000);
}

ทดสอบ WiFiManager (ESP32-C3)

#include <WiFi.h>
#include <WiFiManager.h>

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("\n=== ESP32-C3 WiFiManager TEST ===");

  // ล้าง WiFi เก่าทิ้งหมด (สำคัญมาก)
  WiFi.disconnect(true, true);
  delay(1000);

  // ตั้งโหมดให้รองรับ AP
  WiFi.mode(WIFI_AP_STA);

  // บังคับ AP ให้อยู่ channel 1 (มือถือเห็นชัวร์)
  WiFi.softAPConfig(
    IPAddress(192,168,4,1),
    IPAddress(192,168,4,1),
    IPAddress(255,255,255,0)
  );
  WiFi.softAP("ESP32-SETUP", NULL, 1);

  Serial.print("AP IP: ");
  Serial.println(WiFi.softAPIP());

  WiFiManager wm;
  wm.setConfigPortalTimeout(180); // เปิด AP 3 นาที

  Serial.println("Starting Config Portal...");
  bool res = wm.startConfigPortal("ESP32-SETUP");

  if (!res) {
    Serial.println("❌ Config Portal timeout or failed");
    ESP.restart();
  }

  Serial.println("✅ WiFi Connected!");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  // ไม่มีอะไรใน loop
}


ทดสอบ WiFiManager (ESP32-C3) เสียหรือไม่

#include <WiFi.h>
#include <WiFiManager.h>

void setup() {
  Serial.begin(115200);
  delay(1000);

  WiFi.mode(WIFI_AP);
  WiFi.softAP("ESP32-C3-RAW", NULL, 1);

  Serial.println("RAW AP Started");
  Serial.println(WiFi.softAPIP());
}

void loop() {}


ระบบรดน้ำอัตโนมัติด้วย 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)

อุปกรณ์ ขาอุปกรณ์ ต่อเข้าขา ESP32-C3 หมายเหตุ
Relay IN (Signal) GPIO 4 ควบคุมปั๊มน้ำ
LED Anode (+) GPIO 5 ไฟสถานะ (ต้องต่อ R 220Ω)
OLED SDA GPIO 8 จอแสดงผล
  SCL GPIO 9  
ปุ่มกด ขา 1 GPIO 3 ปุ่มรีเซ็ต WiFi (กดค้าง)
  ขา 2 GND  
 

ระบบรดน้ำอัตโนมัติด้วย ESP32 (30pin)

/*
PROJECT: ESP32 Smart Farm Pro (DevKit V1 Version)
BOARD: ESP32 DevKit V1 (30 Pin)

AUTHOR: Phayoune Team

FEATURES:
- 3 Independent Watering Schedules (Time + Duration + Days)
- Real-time Web UI
- Fast Configuration Save (Struct)
- OLED Display (SDA=21, SCL=22)
- Manual Control & Emergency Stop
- WiFi Manager (Auto AP)
*/

#include <WiFi.h>
#include <WebServer.h>
#include <WiFiManager.h>
#include <time.h>
#include <Preferences.h>
#include <ESPmDNS.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ================= 1. ตั้งค่า Hardware (สำหรับ ESP32 DevKit V1) =================
#define OLED_SDA 21 // ขา SDA มาตรฐานของ ESP32
#define OLED_SCL 22 // ขา SCL มาตรฐานของ ESP32
#define RELAY_PIN 4 // ขา Relay (D4)
#define LED_PIN 2 // ขา LED (D2) *ปกติเป็นไฟสีฟ้าบนบอร์ด ใช้ดูสถานะได้เลย
#define BOOT_PIN 0 // ปุ่ม BOOT บนบอร์ด (ใช้กดค้างเพื่อ Reset WiFi)

#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}; // เลือกวัน
};

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) {
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 {
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";

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>";

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>";

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>";

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>";

html += "<script>";
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 += "}";

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);";
html += "updateSystem();";
html += "</script>";
}
html += "</div></body></html>";
return html;
}

// ================= HANDLERS =================
void handleGetStatus() {
String json = "{";
json += "\"relay\":" + String(relayState ? 1 : 0) + ",";
json += "\"manual\":" + String(manualMode ? 1 : 0);
json += "}";
server.send(200, "application/json", json);
}

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(">> ESP32 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");

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();
}

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 (ESP32 Standard)
Wire.begin(OLED_SDA, OLED_SCL); // 21, 22
pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW);
pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);
pinMode(BOOT_PIN, INPUT_PULLUP); // ปุ่ม BOOT บนบอร์ด

if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 failed"));
}

showBootStatus("INITIALIZING", "ESP32 Dev V1");
delay(1000);

preferences.begin("sys_cfg_v2", true);
if (preferences.isKey("data")) {
preferences.getBytes("data", &cfg, sizeof(cfg));
} else {
cfg.rounds[0].h = 8;
cfg.rounds[1].h = 12;
cfg.rounds[2].h = 17;
}
preferences.end();

WiFiManager wifiManager;
wifiManager.setAPCallback(configModeCallback);
wifiManager.setConfigPortalTimeout(180);

showBootStatus("CONNECTING...", "WiFi");

if(!wifiManager.autoConnect("Phayoune_Setup", "password")) {
showBootStatus("FAILED", "Restarting...");
delay(2000);
ESP.restart();
}

showBootStatus("ONLINE!", "Syncing Time...");
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
if (!MDNS.begin("phayoune")) { Serial.println("Error mDNS"); }

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();
}

// **การทำงานปุ่ม Reset WiFi**
// ใช้ปุ่ม BOOT (GPIO 0) กดค้าง 6 วินาทีเพื่อล้างค่า WiFi
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;

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;
}
}
}

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);
}
}
}
}
}

📋 แผนผังการต่อสาย ESP32 DevKit V1

อุปกรณ์ ขาที่ ESP32 หมายเหตุ
Relay GPIO 4 (D4) ต่อสาย Signal เข้าที่ D4
OLED SDA GPIO 21 (D21) ขามาตรฐานของ ESP32
OLED SCL GPIO 22 (D22) ขามาตรฐานของ ESP32
LED GPIO 2 (D2) ไม่ต้องต่อก็ได้ ใช้ไฟสีฟ้าบนบอร์ดได้เลย
ปุ่ม Reset WiFi GPIO 0 (BOOT) ไม่ต้องต่อ ใช้ปุ่ม BOOT บนบอร์ดกดค้างได้เลย
Page 1 of 5
Ribbon