// =======================================================
// Unified ESP32 Greenhouse Controller (Potentiometer & Water Alarm)
// Features: Web Control, NTP Time, Precise Stepper, Servo Watering, Water Alarm
// =======================================================
// ================= 庫與定義 =================
#include <WiFi.h>
#include "time.h"
#include <WebServer.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHTesp.h>
// ---------------- WiFi + NTP ----------------
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;
// ---------------- 引腳定義 ----------------
#define DHT_PIN 15
#define US_TRIG 5
#define US_ECHO 18
#define WATER_ALARM_PIN 13 // 缺水警報 LED
#define SOIL_PIN 34 // **連接可變電阻 (Potentiometer) 模擬土壤濕度**
#define SERVO_PIN 4
#define STEP_PIN 19
#define DIR_PIN 23
#define EN_PIN 17
#define BUTTON_PIN 2
// LED Bar (8 LEDs)
const int ledPins[8] = {32, 33, 25, 26, 27, 14, 12, 16};
const int TOTAL_LEDS = 8;
// ---------------- Stepper 參數 ----------------
const int STEPS_PER_REV = 200;
const int MICROSTEPS = 16;
const float FAN_RPM = 120.0;
const bool FAN_DIR_CW = true;
// ---------------- 狀態與數據 ----------------
float tempC = NAN;
float humidity = NAN;
float waterDistance = 0;
float waterLevelPercent = 0;
int soilMoisturePercent = 0; // 0% 極乾 (Pot. 撥到 4095), 100% 極濕 (Pot. 撥到 0)
bool manualOverride = false;
bool fanEnabled = false;
bool waterAlarm = false;
bool wateringState = false;
int stableButtonState = HIGH;
// ---------------- 計時器與閾值 ----------------
uint32_t lastDHT = 0;
uint32_t lastDisplay = 0;
uint32_t lastButtonChange = 0;
uint32_t lastStepUs = 0;
uint32_t DHT_INTERVAL = 2000;
uint32_t DISP_INTERVAL = 500;
uint32_t BUTTON_DEBOUNCE = 50;
uint32_t stepIntervalUs = 0;
bool stepLevel = LOW;
const int servoFreq = 50;
const int servoRes = 16;
const int TEMP_THRESHOLD_ON = 25.0;
const int TEMP_THRESHOLD_OFF = 24.0;
const float LOW_WATER_THRESHOLD_CM = 15.0;
const int DRY_SOIL_THRESHOLD = 30; // 濕度低於 30% 觸發澆水
// ================= 函數原型聲明 =================
void updateStepInterval();
void stepperEnable(bool en);
void stepperDir(bool cw);
void stepperTick();
void updateFanAuto();
void setServoAngle(int angle);
void readButton();
void sampleDHT();
void checkWaterLevelAndAlarm();
void updateOutputs();
void updateLedBar();
void updateLCD();
void connectWiFiAndTime();
String NTP_Time_String();
void handleRoot();
void handleFanOn();
void handleFanOff();
// ================= 步進馬達控制函數 =================
void updateStepInterval() {
float stepsPerSec = FAN_RPM * (STEPS_PER_REV * MICROSTEPS) / 60.0;
if (stepsPerSec < 1) stepsPerSec = 1;
stepIntervalUs = (uint32_t)(1e6 / stepsPerSec / 2);
}
void stepperEnable(bool en) {
digitalWrite(EN_PIN, en ? LOW : HIGH);
}
void stepperDir(bool cw) {
digitalWrite(DIR_PIN, cw ? HIGH : LOW);
}
void stepperTick() {
if (!fanEnabled) {
digitalWrite(STEP_PIN, LOW);
stepLevel = LOW;
return;
}
uint32_t now = micros();
if (now - lastStepUs >= stepIntervalUs) {
lastStepUs = now;
stepLevel = !stepLevel;
digitalWrite(STEP_PIN, stepLevel);
}
}
void updateFanAuto() {
if (manualOverride) {
fanEnabled = true;
return;
}
if (!isnan(tempC)) {
if (!fanEnabled && tempC >= TEMP_THRESHOLD_ON) {
fanEnabled = true;
} else if (fanEnabled && tempC <= TEMP_THRESHOLD_OFF) {
fanEnabled = false;
}
}
}
// ================= 閥門與按鈕控制函數 =================
void setServoAngle(int angle) {
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
long pulseWidthUs = map(angle, 0, 180, 500, 2500);
long duty = (pulseWidthUs * 65536) / 20000;
ledcWrite(SERVO_PIN, duty);
}
void readButton() {
int raw = digitalRead(BUTTON_PIN);
uint32_t now = millis();
if (raw != stableButtonState) {
lastButtonChange = now;
}
if (now - lastButtonChange >= BUTTON_DEBOUNCE) {
if (stableButtonState != raw) {
stableButtonState = raw;
if (raw == LOW) {
manualOverride = !manualOverride;
}
}
}
}
// ================= 傳感器與警報函數 =================
void sampleDHT() {
if (millis() - lastDHT >= DHT_INTERVAL) {
lastDHT = millis();
TempAndHumidity data = dht.getTempAndHumidity();
if (!isnan(data.temperature)) {
tempC = data.temperature;
humidity = data.humidity;
}
// 讀取可變電阻 (Pot.)
int rawSoil = analogRead(SOIL_PIN);
// 模擬:0 (Pot. 撥到底) => 100% 濕潤, 4095 (Pot. 撥到底) => 0% 乾燥
soilMoisturePercent = map(rawSoil, 0, 4095, 100, 0);
}
}
void checkWaterLevelAndAlarm() {
digitalWrite(US_TRIG, LOW); delayMicroseconds(2);
digitalWrite(US_TRIG, HIGH); delayMicroseconds(10);
digitalWrite(US_TRIG, LOW);
long duration = pulseIn(US_ECHO, HIGH, 30000);
float distance = duration * 0.034 / 2;
waterDistance = distance;
waterLevelPercent = map(waterDistance, 20, 2, 0, 100);
waterLevelPercent = constrain(waterLevelPercent, 0, 100);
// 缺水警報邏輯
if (waterDistance > LOW_WATER_THRESHOLD_CM) {
waterAlarm = true;
} else {
waterAlarm = false;
}
}
// ================= 輸出設備控制函數 =================
void updateOutputs() {
// --- LED Bar (顯示溫度) ---
int ledCount = round(tempC / (50.0 / TOTAL_LEDS));
if (ledCount < 0) ledCount = 0;
if (ledCount > TOTAL_LEDS) ledCount = TOTAL_LEDS;
for (int i = 0; i < TOTAL_LEDS; i++) {
digitalWrite(ledPins[i], (i < ledCount) ? HIGH : LOW);
}
// --- 缺水警報 LED (GPIO 13) ---
digitalWrite(WATER_ALARM_PIN, waterAlarm ? HIGH : LOW);
// --- 澆水閥門 (Servo) 自動控制 ---
// 條件:土壤乾燥 (<30%) 且 無缺水警報
if (soilMoisturePercent < DRY_SOIL_THRESHOLD && !waterAlarm) {
if (!wateringState) {
setServoAngle(90);
wateringState = true;
}
} else {
if (wateringState) {
setServoAngle(0);
wateringState = false;
}
}
}
// ================= LCD 顯示函數 (集成 NTP 時間與警報) =================
void updateLCD() {
if (millis() - lastDisplay < DISP_INTERVAL) return;
lastDisplay = millis();
struct tm timeinfo;
getLocalTime(&timeinfo);
char timeStr[9];
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &timeinfo);
lcd.setCursor(0, 0);
if (waterAlarm) {
lcd.print("!!! LOW WATER !!!");
} else {
lcd.print("Time: ");
lcd.print(timeStr);
}
lcd.setCursor(0, 1);
lcd.printf("T:%.1f H:%.0f%% S:%02d%%", tempC, humidity, soilMoisturePercent);
lcd.setCursor(15, 1);
lcd.print(manualOverride ? "M" : "A");
}
// ================= SETUP, Web Handlers, LOOP =================
String NTP_Time_String() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return "N/A";
char output[30];
strftime(output, 30, "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(output);
}
void connectWiFiAndTime() {
Serial.print("Connecting WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(200); Serial.print(".");
}
Serial.println(" OK!");
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
while (!getLocalTime(&timeinfo)) {
delay(200); Serial.print("/");
}
Serial.println(" Time Synced.");
}
void handleRoot() {
String html = "<html><body style='font-family:sans-serif; text-align:center; padding:20px;'>";
html += "<h1>ESP32 Greenhouse Monitor</h1>";
html += String("<p>Time: ") + NTP_Time_String() + "</p>";
html += String("<h3>Temp: ") + String(tempC, 1) + " C | Hum: " + String(humidity, 0) + " %</h3>";
html += String("<p>Soil Moist: ") + String(soilMoisturePercent) + "% | Water Lvl: " + String(waterLevelPercent, 0) + "%</p>";
html += String("<p style='color:") + (waterAlarm ? "red" : "green") + ";'>Water Alarm: " + (waterAlarm ? "**ACTIVE**" : "Normal") + "</p>";
html += String("<p>Fan Status: ") + (manualOverride ? "MANUAL ON" : "AUTO") + "</p>";
html += "<p><a href='/fan/on'><button style='font-size:18px;padding:10px;background:green;color:white;'>Fan ON</button></a>";
html += " <a href='/fan/off'><button style='font-size:18px;padding:10px;background:red;color:white;'>Fan Auto</button></a></p>";
html += String("<p>Valve: ") + (wateringState ? "OPEN" : "CLOSED") + "</p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleFanOn() { manualOverride = true; server.sendHeader("Location", "/"); server.send(303); }
void handleFanOff() { manualOverride = false; server.sendHeader("Location", "/"); server.send(303); }
void setup() {
Serial.begin(115200);
dht.setup(DHT_PIN, DHTesp::DHT22);
lcd.init();
lcd.backlight();
for (int i = 0; i < TOTAL_LEDS; i++) pinMode(ledPins[i], OUTPUT);
pinMode(US_TRIG, OUTPUT);
pinMode(US_ECHO, INPUT);
pinMode(WATER_ALARM_PIN, OUTPUT);
pinMode(SOIL_PIN, INPUT);
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(EN_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
stepperDir(FAN_DIR_CW);
updateStepInterval();
stepperEnable(false);
ledcAttach(SERVO_PIN, servoFreq, servoRes);
setServoAngle(0);
lcd.setCursor(0, 0); lcd.print("Connecting...");
connectWiFiAndTime();
lcd.clear();
server.on("/", handleRoot);
server.on("/fan/on", handleFanOn);
server.on("/fan/off", handleFanOff);
server.begin();
}
void loop() {
sampleDHT();
readButton();
updateFanAuto();
stepperEnable(fanEnabled);
stepperTick();
checkWaterLevelAndAlarm();
updateOutputs();
updateLCD();
server.handleClient();
delay(1);
}