/*
小型溫室物聯網系統 - 核心驅動代碼 (整合註釋版本)
平台: ESP32 (Arduino 框架, Wokwi 模擬測試)
核心功能:
1. 環境監測: DHT22 (溫濕度) + 模擬土壤濕度 (電位器)
2. 水位監測: HC-SR04 超聲波 (判斷水箱是否缺水)
3. 自動化控制: 根據濕度/溫度自動控制水泵與步進馬達
4. 視覺化顯示: I2C LCD (數值與狀態) + LED Bar (水位指示)
5. 安全機制: 水箱缺水時,強制禁止澆水,防止水泵乾燒。
*/
// =================== 引入所需庫文件 ===================
#include <Wire.h> // I2C 通訊庫 (給 LCD 用,ESP32 標準庫)
#include <LiquidCrystal_I2C.h> // LCD 1602 螢幕驅動庫
#include <DHT.h> // DHT 溫濕度傳感器庫
// =================== 引腳定義 (Pin Definitions) ===================
// 這是硬體連接的地圖,確保你的線路與此處定義的 GPIO 號碼一致。
#define PIN_DHT 15 // 溫濕度傳感器 (DHT22) 數據線接 GPIO 15
#define PIN_TRIG 5 // 超聲波發射腳 (Trigger) 接 GPIO 5
#define PIN_ECHO 18 // 超聲波接收腳 (Echo) 接 GPIO 18
#define PIN_RELAY_PUMP 4 // 澆水水泵繼電器控制腳接 GPIO 4 (OUTPUT)
#define PIN_SOIL 34 // 土壤傳感器/模擬電位器接 GPIO 34 (僅能做 Analog 輸入)
#define PIN_STEP 19 // 步進馬達脈衝腳 (Step) 接 GPIO 19 (A4988 驅動)
#define PIN_DIR 23 // 步進馬達方向腳 (Direction) 接 GPIO 23 (A4988 驅動)
// LED Bar 引腳陣列 (從低水位 L1 到高水位 L5)
const int ledPins[5] = {13, 12, 14, 27, 26};
// =================== 系統配置與閥值 (Configuration & Thresholds) ===================
#define DHTTYPE DHT22 // 定義使用的 DHT 傳感器型號
// 水箱設定 (單位: cm)
const float DIST_TANK_HEIGHT = 20.0; // 水箱總深度/高度設定為 20cm
const float DIST_EMPTY_THRESH = 15.0; // 缺水閾值: 測距 > 15cm (即水深 < 5cm) 視為缺水
// 自動澆水控制閥值 (單位: %)
const int SOIL_DRY_THRESH = 30; // 土壤濕度 < 30% 時,啟動澆水
const int SOIL_WET_THRESH = 70; // 土壤濕度 > 70% 時,停止澆水 (遲滯區間 30%~70% 防止頻繁開關)
// 自動通風控制閥值 (單位: 攝氏度 C)
const float TEMP_HOT_THRESH = 30.0; // 氣溫 > 30°C 時,打開通風/窗戶
const float TEMP_COOL_THRESH = 25.0; // 氣溫 < 25°C 時,關閉通風/窗戶
// =================== 全域物件與變數 (Global Objects & Variables) ===================
// 建立硬體物件實例
DHT dht(PIN_DHT, DHTTYPE); // 建立 DHT 傳感器物件
LiquidCrystal_I2C lcd(0x27, 16, 2); // 建立 LCD 螢幕物件 (I2C 地址 0x27, 16字元, 2行)
// 儲存感測數值的變數
float airTemp = 0; // 儲存空氣溫度
float airHum = 0; // 儲存空氣濕度
int soilMoisturePercent = 0; // 儲存土壤濕度 (%)
float waterLevelCm = 0; // 儲存計算後的水位高度 (水箱高度 - 測距距離)
float distanceCm = 0; // 儲存超聲波測到的原始距離
// 系統狀態旗標 (Flag)
bool isWatering = false; // 追蹤目前水泵是否正在運作
bool isVentOpen = false; // 追蹤目前窗戶/風扇是否開啟
bool tankEmpty = false; // 追蹤水箱是否處於缺水狀態 (安全保護用)
// 非阻塞計時器變數 (Non-blocking Timer)
unsigned long lastSensorTime = 0; // 記錄上次執行感測器任務的時間點
unsigned long lastScreenTime = 0; // 記錄上次執行螢幕更新任務的時間點
const long SENSOR_INTERVAL = 2000; // 定義感測器任務每 2000ms (2秒) 執行一次
const long SCREEN_INTERVAL = 1000; // 定義螢幕更新任務每 1000ms (1秒) 執行一次
// =================== 函式宣告 (Function Prototypes) ===================
void readSensors();
void controlSystem();
void moveStepper(bool dir, int steps);
void updateDisplay();
// =================== 初始化設定 (Setup) ===================
void setup() {
Serial.begin(115200); // 啟動序列埠,用於電腦除錯輸出
// 設定所有輸出入引腳模式
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_RELAY_PUMP, OUTPUT);
pinMode(PIN_STEP, OUTPUT);
pinMode(PIN_DIR, OUTPUT);
digitalWrite(PIN_RELAY_PUMP, LOW); // 初始化時確保水泵繼電器關閉 (視繼電器型號可能需改 HIGH)
// 初始化 LED Bar 引腳
for(int i=0; i<5; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW); // 確保所有水位指示燈初始熄滅
}
// 初始化元件
dht.begin(); // 啟動 DHT 傳感器
lcd.init(); // 初始化 LCD 螢幕
lcd.backlight(); // 開啟 LCD 背光
// 顯示開機訊息
lcd.setCursor(0,0);
lcd.print("Greenhouse Sys");
lcd.setCursor(0,1);
lcd.print("System Ready");
delay(1500);
lcd.clear();
}
// =================== 主循環 (Main Loop) ===================
void loop() {
unsigned long currentMillis = millis(); // 獲取當前 ESP32 開機時間 (毫秒)
// 1. 任務: 讀取傳感器與控制邏輯 (每 SENSOR_INTERVAL 執行一次)
// 這段邏輯檢查時間是否到了,若到點則執行,避免使用 delay() 造成程式阻塞。
if (currentMillis - lastSensorTime >= SENSOR_INTERVAL) {
lastSensorTime = currentMillis; // 更新上次執行時間
readSensors(); // 執行讀取所有傳感器的函式
controlSystem(); // 執行自動控制的函式
}
// 2. 任務: 更新 LCD 顯示 (每 SCREEN_INTERVAL 執行一次)
if (currentMillis - lastScreenTime >= SCREEN_INTERVAL) {
lastScreenTime = currentMillis;
updateDisplay(); // 執行更新螢幕顯示的函式
}
// 因為核心邏輯是非阻塞的,所以網路連線不會被卡住。
}
// =================== 傳感器數據讀取函式 ===================
void readSensors() {
// 讀取 DHT22 (溫濕度)
airTemp = dht.readTemperature();
airHum = dht.readHumidity();
// 讀取土壤濕度 (GPIO 34)
int rawSoil = analogRead(PIN_SOIL); // 讀取 0~4095 的原始模擬值
soilMoisturePercent = map(rawSoil, 0, 4095, 0, 100); // 將原始值映射到 0~100%
// 讀取超聲波距離 (水箱水位)
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10); // 發送 10us 的觸發脈衝
digitalWrite(PIN_TRIG, LOW);
long duration = pulseIn(PIN_ECHO, HIGH); // 測量回音持續時間
distanceCm = duration * 0.034 / 2; // 計算距離 (cm)
// 計算實際水位高度
waterLevelCm = DIST_TANK_HEIGHT - distanceCm;
if (waterLevelCm < 0) waterLevelCm = 0; // 確保水位不會是負數
// 安全檢查: 判斷水箱是否缺水
if (distanceCm >= DIST_EMPTY_THRESH) {
tankEmpty = true; // 標記為缺水,觸發安全保護
} else {
tankEmpty = false;
}
// 更新 LED Bar (水位視覺化顯示)
// 將水位高度 (0~20cm) 映射到 0~5 顆 LED
int ledLevel = map((int)waterLevelCm, 0, (int)DIST_TANK_HEIGHT, 0, 5);
for(int i=0; i<5; i++) {
// 根據計算出的 ledLevel 點亮對應數量的燈
if (i < ledLevel) digitalWrite(ledPins[i], HIGH);
else digitalWrite(ledPins[i], LOW);
}
}
// =================== 系統自動控制函式 ===================
void controlSystem() {
// --- 澆水控制邏輯 ---
// **安全保護機制**: 如果水箱缺水,強制關閉水泵
if (tankEmpty) {
digitalWrite(PIN_RELAY_PUMP, LOW); // 關閉水泵
isWatering = false; // 更新狀態
}
else {
// 水箱水位正常,執行自動澆水判斷
// 條件 1: 如果目前沒在澆水 (防止重複啟動) 且 土壤濕度低於閥值 -> 啟動水泵
if (!isWatering && soilMoisturePercent < SOIL_DRY_THRESH) {
digitalWrite(PIN_RELAY_PUMP, HIGH); // 啟動水泵
isWatering = true;
}
// 條件 2: 如果目前正在澆水 且 土壤濕度高於停止閥值 -> 關閉水泵
else if (isWatering && soilMoisturePercent > SOIL_WET_THRESH) {
digitalWrite(PIN_RELAY_PUMP, LOW); // 關閉水泵
isWatering = false;
}
}
// --- 通風控制邏輯 (步進馬達) ---
// 條件 1: 如果窗戶沒開 且 氣溫超過熱閾值 -> 打開窗戶/風扇
if (!isVentOpen && airTemp > TEMP_HOT_THRESH) {
moveStepper(true, 200); // 正轉 200 步 (模擬開窗)
isVentOpen = true;
}
// 條件 2: 如果窗戶開著 且 氣溫低於冷卻閾值 -> 關閉窗戶/風扇
else if (isVentOpen && airTemp < TEMP_COOL_THRESH) {
moveStepper(false, 200); // 反轉 200 步 (模擬關窗)
isVentOpen = false;
}
}
// =================== 步進馬達驅動函式 (Helper) ===================
void moveStepper(bool dir, int steps) {
digitalWrite(PIN_DIR, dir ? HIGH : LOW); // 設定馬達轉動方向
for(int i=0; i<steps; i++) {
digitalWrite(PIN_STEP, HIGH); // 發送高電平脈衝
delayMicroseconds(1000); // 延時 1000us (1ms) 控制轉速
digitalWrite(PIN_STEP, LOW); // 拉低電平
delayMicroseconds(1000); // 延時 1000us (1ms)
}
// 注意:這裡使用 delayMicroseconds() 會短暫阻塞,但由於步數少 (200步),
// 總阻塞時間約 0.4 秒,對系統影響可接受。
}
// =================== 螢幕顯示更新函式 ===================
void updateDisplay() {
static int page = 0; // 使用 static 變數記住上次的頁面編號
page = !page; // 切換頁面 (0 變 1,1 變 0)
lcd.setCursor(0, 0); // 游標移到第 0 行,第 0 列 (左上角)
lcd.clear(); // 先清除螢幕內容
if (page == 0) {
// 顯示頁面 1: 環境數值
lcd.print("T:"); lcd.print((int)airTemp); lcd.print((char)223); lcd.print("C"); // 溫度 (使用 ASCII 223 顯示 ° 符號)
lcd.print(" H:"); lcd.print((int)airHum); lcd.print("% "); // 空氣濕度
lcd.setCursor(0, 1);
lcd.print("Soil:"); lcd.print(soilMoisturePercent); lcd.print("% "); // 土壤濕度
lcd.print(isWatering ? "PUMP:ON " : "PUMP:OFF"); // 顯示水泵狀態
} else {
// 顯示頁面 2: 系統狀態
lcd.print("Tank Lvl: ");
if(tankEmpty) lcd.print("EMPTY!"); // 如果缺水,顯示警示
else { lcd.print((int)waterLevelCm); lcd.print("cm "); } // 顯示水位高度
lcd.setCursor(0, 1);
lcd.print("Vent Status: ");
lcd.print(isVentOpen ? "OPEN " : "SHUT "); // 顯示通風狀態
}
// 序列埠除錯輸出 (顯示所有關鍵數值)
Serial.printf("T:%.1f H:%.1f Soil:%d%% Tank:%.1fcm Pump:%d Vent:%d\n",
airTemp, airHum, soilMoisturePercent, waterLevelCm, isWatering, isVentOpen);
}