/*
* ESP32 智慧溫室整合系統 (IoT Smart Greenhouse)
* 平台:Wokwi Simulator
* 格式:已加入標準縮排,所有函數內容完整展開。
*
* 核心功能:
* 1. DHT22 溫濕度監測
* 2. HC-SR04 水位監測
* 3. A4988/步進馬達 (抽風) 控制 (自動/Web手動)
* 4. LED Bar 溫度視覺化
* 5. LEDC PWM 驅動 Servo (澆水閥門)
* 6. WebServer 遠端監測與控制
*/
// ================= 庫與定義 =================
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHTesp.h>
// 引腳定義 (Pin Definitions)
#define DHT_PIN 15 // 溫濕度
#define TRIG_PIN 5 // 超聲波觸發
#define ECHO_PIN 18 // 超聲波接收
#define STEP_PIN 19 // 步進馬達 STEP
#define DIR_PIN 23 // 步進馬達 DIR
#define SERVO_PIN 4 // 伺服馬達 PWM
#define SOIL_PIN 34 // 土壤濕度 (模擬輸入)
#define RELAY_PIN 13 // 補水繼電器
// LED Bar 引腳 (8顆)
const int ledPins[8] = {32, 33, 25, 26, 27, 14, 12, 2};
// 參數與閾值
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const float TEMP_THRESHOLD_FAN = 30.0; // 風扇自動開啟溫度
const int STEP_SPEED = 2000; // 步進馬達速度 (us, 越小越快)
const int servoChannel = 0; // Servo PWM 通道
const int servoFreq = 50; // Servo 頻率 (50Hz)
const int servoRes = 16; // Servo PWM 解析度 (16-bit)
// ================= 全局變量與物件 =================
DHTesp dht;
LiquidCrystal_I2C lcd(0x27, 16, 2);
WebServer server(80);
// 數據變數
float humidity = 0;
float temperature = 0;
float waterDistance = 0;
float waterLevelPercent = 0;
int soilMoisturePercent = 0;
// 控制旗標
bool fanManualOverride = false;
bool fanState = false;
bool pumpState = false;
bool wateringState = false;
// 計時器 (Timer Variables)
unsigned long lastSensorTime = 0;
unsigned long lastLcdTime = 0;
unsigned long lastSerialTime = 0;
unsigned long lastStepTime = 0;
// ================= 函數原型聲明 =================
void setupWifi();
void handleRoot();
void handleFanOn();
void handleFanOff();
void setServoAngle(int angle);
void readSensors();
void updateOutputs();
void updateLCD();
void printASCIIStatus();
// ================= 系統初始化 (Setup) =================
void setup() {
Serial.begin(115200);
// 引腳初始化
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
pinMode(SOIL_PIN, INPUT);
for(int i=0; i<8; i++) {
pinMode(ledPins[i], OUTPUT);
}
// Servo 初始化 (LEDC PWM)
ledcAttach(SERVO_PIN, servoFreq, servoRes);
setServoAngle(0);
// 組件初始化
dht.setup(DHT_PIN, DHTesp::DHT22);
lcd.init();
lcd.backlight();
// WiFi & WebServer 初始化
setupWifi();
}
// ================= 主循環 (Loop) =================
void loop() {
// 處理 Web 請求 (Non-blocking)
server.handleClient();
unsigned long currentMillis = millis();
// 1. 讀取傳感器與更新邏輯 (每 1000ms)
if (currentMillis - lastSensorTime >= 1000) {
lastSensorTime = currentMillis;
readSensors();
updateOutputs();
}
// 2. 更新 LCD 顯示 (每 1000ms)
if (currentMillis - lastLcdTime >= 1000) {
lastLcdTime = currentMillis;
updateLCD();
}
// 3. 串口圖形化輸出 (每 3000ms)
if (currentMillis - lastSerialTime >= 3000) {
lastSerialTime = currentMillis;
printASCIIStatus();
}
// 4. 步進馬達驅動 (非阻塞,極短時間間隔)
bool fanShouldRun = (temperature > TEMP_THRESHOLD_FAN) || fanManualOverride;
if (fanShouldRun) {
fanState = true;
if (micros() - lastStepTime >= STEP_SPEED) {
lastStepTime = micros();
// 翻轉電位產生脈衝
digitalWrite(STEP_PIN, !digitalRead(STEP_PIN));
}
} else {
fanState = false;
}
}
// ================= Servo 控制函數 (使用 LEDC PWM) =================
/**
* @brief 將 Servo 轉到指定角度 (0-180度)
* @param angle 目標角度
*/
void setServoAngle(int angle) {
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
// 映射角度 0-180 到 脈寬 500-2500 微秒
long pulseWidthUs = map(angle, 0, 180, 500, 2500);
// 計算 16-bit 佔空比數值 (Duty Cycle)
// 佔空比 = (脈寬 / 週期20000us) * 65536
long duty = (pulseWidthUs * 65536) / 20000;
ledcWrite(SERVO_PIN, duty);
}
// ================= 傳感器數據讀取函數 =================
/**
* @brief 讀取 DHT22, HC-SR04 和土壤濕度 (Pot)
*/
void readSensors() {
// 讀取 DHT22
TempAndHumidity data = dht.getTempAndHumidity();
if (!isnan(data.temperature)) {
temperature = data.temperature;
humidity = data.humidity;
}
// 讀取超聲波水位
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// 設置超時避免阻塞
long duration = pulseIn(ECHO_PIN, HIGH, 30000);
waterDistance = duration * 0.034 / 2;
if (waterDistance == 0) waterDistance = 30;
// 水位百分比計算 (假設 20cm 空/0%, 2cm 滿/100%)
waterLevelPercent = map(waterDistance, 20, 2, 0, 100);
waterLevelPercent = constrain(waterLevelPercent, 0, 100);
// 讀取土壤濕度 (Pot)
int rawSoil = analogRead(SOIL_PIN);
// 0(濕) ~ 4095(乾) => 0%(乾) ~ 100%(濕)
soilMoisturePercent = map(rawSoil, 0, 4095, 100, 0); // 反向映射
}
// ================= 輸出設備控制函數 =================
/**
* @brief 根據傳感器數據更新繼電器、LED Bar 和 Servo 狀態
*/
void updateOutputs() {
// --- LED Bar (顯示溫度) ---
int ledLevel = map(temperature, 0, 50, 0, 8);
for (int i = 0; i < 8; i++) {
digitalWrite(ledPins[i], (i < ledLevel) ? HIGH : LOW);
}
// --- 補水泵 (Relay) 自動控制 ---
if (waterDistance > 18.0) { // 水位很低
digitalWrite(RELAY_PIN, HIGH);
pumpState = true;
} else if (waterDistance < 5.0) { // 水位很滿
digitalWrite(RELAY_PIN, LOW);
pumpState = false;
}
// --- 澆水閥門 (Servo) 自動控制 ---
// 如果土壤乾燥 (如 < 30%) 且水箱水位足夠 (如 < 15cm 距離)
if (soilMoisturePercent < 30 && waterDistance < 15.0) {
if (!wateringState) {
setServoAngle(90); // 打開閥門
wateringState = true;
}
} else {
if (wateringState) {
setServoAngle(0); // 關閉閥門
wateringState = false;
}
}
}
// ================= LCD 更新顯示函數 =================
/**
* @brief 更新 LCD 1602 上的顯示內容
*/
void updateLCD() {
lcd.setCursor(0, 0);
// 顯示 T:溫度 H:濕度
lcd.printf("T:%.1f H:%.0f%%", temperature, humidity);
lcd.setCursor(0, 1);
// 顯示 Lvl:水位百分比 Fan:風扇狀態
String fStatus = fanState ? "ON" : "OF";
lcd.printf("Lvl:%02.0f%% Fan:%s", waterLevelPercent, fStatus);
}
// ================= 串口圖形化輸出函數 (個人風格) =================
/**
* @brief 輸出 ASCII 格式的狀態條和系統摘要
*/
void printASCIIStatus() {
Serial.println("================ SYSTEM STATUS ================");
Serial.print("Web Control IP: "); Serial.println(WiFi.localIP());
// 溫度條
Serial.print("Temp ["); Serial.print(temperature, 1); Serial.print("C]: ");
int tBars = map(temperature, 0, 50, 0, 20);
for(int i=0; i<20; i++) Serial.print(i < tBars ? "#" : ".");
Serial.println();
// 土壤濕度條 (百分比越高代表越濕潤)
Serial.print("Soil ["); Serial.print(soilMoisturePercent); Serial.print("%]: ");
int sBars = map(soilMoisturePercent, 0, 100, 0, 20);
for(int i=0; i<20; i++) Serial.print(i < sBars ? "=" : ".");
Serial.println();
// 摘要狀態
Serial.printf("Status: FAN[%s] PUMP[%s] VALVE[%s]\n",
fanState ? "RUN" : "OFF", pumpState ? "ON " : "OFF", wateringState ? "OPEN" : "CLOS");
Serial.println("===============================================\n");
}
// ================= WiFi 連接設置函數 =================
/**
* @brief 連接 WiFi 並啟動 WebServer
*/
void setupWifi() {
Serial.print("Connecting Wifi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print(".");
}
Serial.println(" OK!");
// 設定 Web 根目錄與控制路徑
server.on("/", handleRoot);
server.on("/fan/on", handleFanOn);
server.on("/fan/off", handleFanOff);
server.begin();
}
// ================= 網頁處理函數 (Web Handlers) =================
/**
* @brief 根目錄 (/) 顯示狀態和控制按鈕
*/
void handleRoot() {
String html = "<html><body style='font-family:sans-serif; text-align:center; padding:50px;'>";
html += "<h1>ESP32 Greenhouse</h1>";
html += "<h2>Temp: " + String(temperature) + " C | Hum: " + String(humidity) + " %</h2>";
html += "<p>Water Level: " + String(waterLevelPercent) + "%</p>";
html += "<p>Fan Status: " + (fanManualOverride ? "Manual ON" : "Auto Mode") + "</p>";
html += "<p><a href='/fan/on'><button style='font-size:20px;padding:10px;background:green;color:white;'>Fan ON</button></a>";
html += " <a href='/fan/off'><button style='font-size:20px;padding:10px;background:red;color:white;'>Fan Auto</button></a></p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
/**
* @brief 處理 /fan/on 請求:強制開啟風扇
*/
void handleFanOn() {
fanManualOverride = true;
// 重定向回根目錄
server.sendHeader("Location", "/");
server.send(303);
}
/**
* @brief 處理 /fan/off 請求:切換回自動模式
*/
void handleFanOff() {
fanManualOverride = false;
// 重定向回根目錄
server.sendHeader("Location", "/");
server.send(303);
}