/*
* ESP32 智慧溫室整合系統 (IoT Smart Greenhouse) - 無庫版 Servo
* 平台:Wokwi Simulator
* 修正:移除 ESP32Servo 依賴,改用 ledc 底層控制
*/
#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
#define DIR_PIN 23
#define SERVO_PIN 4 // 伺服馬達
#define SOIL_PIN 34
#define RELAY_PIN 13
// LED Bar 引腳
const int ledPins[8] = {32, 33, 25, 26, 27, 14, 12, 2};
// ================= 參數設置 (Settings) =================
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const float TEMP_THRESHOLD_FAN = 30.0;
const int STEP_SPEED = 2000; // 步進馬達速度 (us)
// Servo PWM 設置 (模擬 Servo 庫)
const int servoChannel = 0; // 使用 PWM 通道 0
const int servoFreq = 50; // Servo 需要 50Hz
const int servoRes = 16; // 16-bit 解析度 (0-65535)
// ================= 全局物件與變數 =================
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;
// 計時器
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); // 手動控制 Servo 的函數
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); // Wokwi 支援的新版語法,或舊版 ledcSetup
setServoAngle(0); // 初始關閉
// 初始化組件
dht.setup(DHT_PIN, DHTesp::DHT22);
lcd.init();
lcd.backlight();
setupWifi();
}
// ================= Loop =================
void loop() {
server.handleClient();
unsigned long currentMillis = millis();
// 1. 讀取傳感器 (1秒一次)
if (currentMillis - lastSensorTime >= 1000) {
lastSensorTime = currentMillis;
readSensors();
updateOutputs();
}
// 2. 更新 LCD (1秒一次)
if (currentMillis - lastLcdTime >= 1000) {
lastLcdTime = currentMillis;
updateLCD();
}
// 3. 串口圖形化輸出 (3秒一次)
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 函數 (核心修改) =================
// 將角度 (0-180) 轉換為 PWM 佔空比
// Servo 0度 ≈ 0.5ms, 180度 ≈ 2.5ms, 週期 20ms
// 16-bit 解析度: 0-65535
void setServoAngle(int angle) {
// 限制角度範圍
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
// 計算脈寬 (0.5ms ~ 2.5ms)
// 映射角度 0-180 到 脈寬 500-2500 微秒
long pulseWidthUs = map(angle, 0, 180, 500, 2500);
// 計算佔空比數值 (Duty Cycle)
// 佔空比 = (脈寬 / 週期20000us) * 65536
long duty = (pulseWidthUs * 65536) / 20000;
ledcWrite(SERVO_PIN, duty);
}
// ================= 邏輯處理 =================
void readSensors() {
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; // 避免除錯或超時顯示0
waterLevelPercent = map(waterDistance, 20, 2, 0, 100);
waterLevelPercent = constrain(waterLevelPercent, 0, 100);
int rawSoil = analogRead(SOIL_PIN);
soilMoisturePercent = map(rawSoil, 0, 4095, 0, 100);
}
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);
}
// 補水 Pump
if (waterDistance > 18.0) {
digitalWrite(RELAY_PIN, HIGH);
pumpState = true;
} else if (waterDistance < 5.0) {
digitalWrite(RELAY_PIN, LOW);
pumpState = false;
}
// 澆水 Servo (自動控制)
// 如果土壤乾且水箱有水
if (soilMoisturePercent < 30 && waterDistance < 15.0) {
if (!wateringState) {
setServoAngle(90); // 打開閥門
wateringState = true;
}
} else {
if (wateringState) {
setServoAngle(0); // 關閉閥門
wateringState = false;
}
}
}
void updateLCD() {
lcd.setCursor(0, 0);
lcd.printf("T:%.1f H:%.0f%%", temperature, humidity);
lcd.setCursor(0, 1);
String fStatus = fanState ? "ON" : "OF";
lcd.printf("Lvl:%02.0f%% Fan:%s", waterLevelPercent, fStatus);
}
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");
}
void setupWifi() {
Serial.print("Connecting Wifi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print(".");
}
Serial.println(" OK!");
server.on("/", handleRoot);
server.on("/fan/on", handleFanOn);
server.on("/fan/off", handleFanOff);
server.begin();
}
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><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);
}
void handleFanOn() { fanManualOverride = true; server.sendHeader("Location", "/"); server.send(303); }
void handleFanOff() { fanManualOverride = false; server.sendHeader("Location", "/"); server.send(303); }