/*
* ESP32-S3 Paludarium Controller v8.0 - COMPILATION FIXED
* ========================================================
* Complete biodome control with rain override - compilation errors fixed
*/
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
// WiFi Settings
const char* WIFI_SSID = "PLUSNET-NFFHPJ";
const char* WIFI_PASS = "Halloween342!eomhb";
// Pin Definitions
#define PIN_WATER_LEVEL 8
#define PIN_LDR 6
#define PIN_DHT 4
#define DHT_TYPE DHT11
#define PIN_DS18B20 5
#define PIN_SOIL_MOISTURE 7
#define RELAY_WATERFALL 12
#define RELAY_RAIN 13
#define RELAY_BUBBLE 14
#define RELAY_FAN 15
#define RELAY_HEATER 16
#define RELAY_MIST 17
#define RELAY_LED 18
// Water Level Constants (renamed to avoid enum conflicts)
#define WATER_LEVEL_EMERGENCY_LOW 800
#define WATER_LEVEL_VERY_LOW 1000
#define WATER_LEVEL_LOW_THRESHOLD 1200
#define WATER_LEVEL_OPTIMAL_MIN 1400
#define WATER_LEVEL_OPTIMAL_MAX 1800
#define WATER_LEVEL_HIGH_WARNING 1900
#define WATER_LEVEL_OVERFLOW_STOP 2000
// Durations (milliseconds)
#define RAIN_DURATION_NORMAL (4UL * 60UL * 1000UL)
#define RAIN_DURATION_EMERGENCY (8UL * 60UL * 1000UL)
#define RAIN_DURATION_OVERRIDE (10UL * 60UL * 1000UL)
#define MIST_DURATION_SHORT (2UL * 60UL * 1000UL)
#define MIST_DURATION_NORMAL (4UL * 60UL * 1000UL)
#define MIST_DURATION_LONG (6UL * 60UL * 1000UL)
#define WATERFALL_DURATION (25UL * 60UL * 1000UL)
#define SOIL_WATER_DURATION (90UL * 1000UL)
#define FAN_MIN_RUNTIME (3UL * 60UL * 1000UL)
// Timing
#define SENSOR_READ_INTERVAL 3000UL
#define LOGIC_UPDATE_INTERVAL 1000UL
#define AUTO_TIMEOUT_MS (45UL * 60UL * 1000UL)
// Device states
enum DeviceState {
DEVICE_OFF = 0,
DEVICE_RUNNING,
DEVICE_MANUAL_OVERRIDE,
DEVICE_EMERGENCY_OVERRIDE,
DEVICE_ERROR
};
enum OverrideType {
NO_OVERRIDE = 0,
MANUAL_OVERRIDE,
EMERGENCY_OVERRIDE,
SCHEDULED_OVERRIDE,
RAIN_FORCE_OVERRIDE
};
// Water safety levels (using enum values that don't conflict)
enum WaterSafetyLevel {
WATER_EMERGENCY = 0,
WATER_VERY_LOW,
WATER_LOW,
WATER_OK,
WATER_OPTIMAL,
WATER_HIGH,
WATER_OVERFLOW_RISK
};
struct DeviceController {
DeviceState state = DEVICE_OFF;
OverrideType overrideType = NO_OVERRIDE;
unsigned long stateChangeTime = 0;
unsigned long runDuration = 0;
bool manualOverride = false;
String lastAction = "";
String currentReason = "";
int runCount = 0;
bool isRunning() {
return state == DEVICE_RUNNING || state == DEVICE_MANUAL_OVERRIDE || state == DEVICE_EMERGENCY_OVERRIDE;
}
bool isOff() { return state == DEVICE_OFF; }
bool canStart() { return state == DEVICE_OFF; }
void startDevice(unsigned long duration, String reason = "", OverrideType override = NO_OVERRIDE) {
if (canStart() || override != NO_OVERRIDE) {
if (override == EMERGENCY_OVERRIDE) {
state = DEVICE_EMERGENCY_OVERRIDE;
} else if (override == MANUAL_OVERRIDE) {
state = DEVICE_MANUAL_OVERRIDE;
} else {
state = DEVICE_RUNNING;
}
stateChangeTime = millis();
runDuration = duration;
lastAction = reason;
currentReason = reason;
overrideType = override;
runCount++;
}
}
void stopDevice(String reason = "") {
if (isRunning()) {
state = DEVICE_OFF;
stateChangeTime = millis();
lastAction = reason;
currentReason = "";
manualOverride = false;
overrideType = NO_OVERRIDE;
}
}
void forceStop(String reason = "") {
state = DEVICE_OFF;
stateChangeTime = millis();
lastAction = "FORCED STOP: " + reason;
currentReason = "";
manualOverride = false;
overrideType = NO_OVERRIDE;
}
void update() {
if (!isRunning()) return;
unsigned long elapsed = millis() - stateChangeTime;
if (elapsed >= runDuration && overrideType != MANUAL_OVERRIDE) {
stopDevice("Duration complete");
}
}
String getStatusText() {
unsigned long remaining = 0;
if (isRunning() && runDuration > 0) {
unsigned long elapsed = millis() - stateChangeTime;
remaining = (elapsed < runDuration) ? (runDuration - elapsed) / 1000 : 0;
}
switch(state) {
case DEVICE_OFF: return "OFF";
case DEVICE_RUNNING:
return remaining > 0 ? "ON (" + String(remaining) + "s)" : "ON";
case DEVICE_MANUAL_OVERRIDE:
return "MANUAL ON" + (remaining > 0 ? " (" + String(remaining) + "s)" : "");
case DEVICE_EMERGENCY_OVERRIDE:
return "EMERGENCY ON" + (remaining > 0 ? " (" + String(remaining) + "s)" : "");
default: return "UNKNOWN";
}
}
String getOverrideStatusText() {
switch(overrideType) {
case MANUAL_OVERRIDE: return "Manual Override";
case EMERGENCY_OVERRIDE: return "Emergency Override";
case SCHEDULED_OVERRIDE: return "Scheduled Override";
case RAIN_FORCE_OVERRIDE: return "Rain Force Override";
default: return "Auto Mode";
}
}
};
// Configuration
struct Config {
int humidityTarget = 65;
int humidityMax = 80;
int humidityMin = 45;
int tempTarget = 24;
int tempHeaterOn = 20;
int tempHeaterOff = 26;
int tempFanOn = 28;
int tempFanOff = 25;
int waterTempTarget = 22;
int waterTempOn = 18;
int waterTempOff = 25;
int ledDayStart = 7;
int ledDayEnd = 21;
int rainHour1 = 8;
int rainHour2 = 20;
int soilMoistureMin = 35;
int soilMoistureTarget = 65;
bool enableRainOverride = true;
bool enableEmergencyOverride = true;
bool enableEmergencyRain = true;
} config;
// System state - START IN AUTO MODE
bool manualMode = false; // DEFAULT TO AUTO
bool manualSticky = false;
unsigned long lastManualActivity = 0;
unsigned long systemStartTime = 0;
// Auto mode display
struct AutoModeInfo {
String currentAction = "System Initializing";
String nextScheduledEvent = "Calculating...";
String systemStatus = "Starting Up";
String waterLevelAction = "";
String climateAction = "";
String lightingAction = "";
void updateDisplay() {
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
int hour = timeinfo.tm_hour;
int minute = timeinfo.tm_min;
if (hour < config.rainHour1) {
nextScheduledEvent = "Next Rain: " + String(config.rainHour1) + ":00";
} else if (hour == config.rainHour1 && minute < 5) {
nextScheduledEvent = "Rain Cycle Active";
} else if (hour < config.rainHour2) {
nextScheduledEvent = "Next Rain: " + String(config.rainHour2) + ":00";
} else if (hour == config.rainHour2 && minute < 5) {
nextScheduledEvent = "Rain Cycle Active";
} else {
nextScheduledEvent = "Next Rain: Tomorrow " + String(config.rainHour1) + ":00";
}
if (hour >= config.ledDayStart && hour < config.ledDayEnd) {
lightingAction = "Daylight Active (until " + String(config.ledDayEnd) + ":00)";
} else {
lightingAction = "Night Mode (lights at " + String(config.ledDayStart) + ":00)";
}
}
}
} autoMode;
// Device controllers
DeviceController rainController;
DeviceController mistController;
DeviceController waterfallController;
DeviceController fanController;
DeviceController heaterController;
DeviceController bubbleController;
DeviceController ledController;
DeviceController soilController;
// Hardware
DHT dht(PIN_DHT, DHT_TYPE);
OneWire oneWire(PIN_DS18B20);
DallasTemperature ds18b20(&oneWire);
WebServer server(80);
// Sensor data
struct SensorData {
float humidity = 0;
float ambientTemp = 0;
float waterTemp = 0;
int waterLevel = 0;
int soilMoisture = 0;
int lightLevel = 0;
bool humidityValid = false;
bool tempValid = false;
bool waterTempValid = false;
unsigned long lastHumidityRead = 0;
unsigned long lastTempRead = 0;
unsigned long lastWaterTempRead = 0;
} sensors;
// System health
struct SystemHealth {
unsigned long uptime = 0;
int wifiReconnects = 0;
int sensorErrors = 0;
int safetyStops = 0;
int emergencyRains = 0;
int manualOverrides = 0;
unsigned long lastError = 0;
String lastErrorMsg = "";
void logError(String error) {
sensorErrors++;
lastError = millis();
lastErrorMsg = error;
Serial.println("ERROR: " + error);
}
void logSafetyStop(String reason) {
safetyStops++;
lastError = millis();
lastErrorMsg = "Safety: " + reason;
Serial.println("SAFETY: " + reason);
}
void logManualOverride(String device) {
manualOverrides++;
Serial.println("OVERRIDE: " + device);
}
} health;
struct tm timeinfo;
// Helper functions
inline void relayOn(uint8_t pin) { digitalWrite(pin, LOW); }
inline void relayOff(uint8_t pin) { digitalWrite(pin, HIGH); }
void updateTime() {
getLocalTime(&timeinfo);
}
// Water safety functions
WaterSafetyLevel getWaterSafetyLevel() {
if (sensors.waterLevel < WATER_LEVEL_EMERGENCY_LOW) return WATER_EMERGENCY;
if (sensors.waterLevel < WATER_LEVEL_VERY_LOW) return WATER_VERY_LOW;
if (sensors.waterLevel < WATER_LEVEL_LOW_THRESHOLD) return WATER_LOW;
if (sensors.waterLevel < WATER_LEVEL_OPTIMAL_MIN) return WATER_OK;
if (sensors.waterLevel < WATER_LEVEL_OPTIMAL_MAX) return WATER_OPTIMAL;
if (sensors.waterLevel < WATER_LEVEL_HIGH_WARNING) return WATER_HIGH;
return WATER_OVERFLOW_RISK;
}
String getWaterStatusText() {
WaterSafetyLevel level = getWaterSafetyLevel();
switch(level) {
case WATER_EMERGENCY: return "EMERGENCY";
case WATER_VERY_LOW: return "VERY LOW";
case WATER_LOW: return "LOW";
case WATER_OK: return "OK";
case WATER_OPTIMAL: return "OPTIMAL";
case WATER_HIGH: return "HIGH";
case WATER_OVERFLOW_RISK: return "OVERFLOW RISK";
default: return "UNKNOWN";
}
}
bool isWaterSafeForRain() {
return getWaterSafetyLevel() <= WATER_HIGH;
}
bool canRainOverride() {
return getWaterSafetyLevel() < WATER_OVERFLOW_RISK && config.enableRainOverride;
}
// Sensor reading
void readSensors() {
static unsigned long lastRead = 0;
if (millis() - lastRead < SENSOR_READ_INTERVAL) return;
lastRead = millis();
sensors.waterLevel = analogRead(PIN_WATER_LEVEL);
sensors.soilMoisture = analogRead(PIN_SOIL_MOISTURE);
sensors.lightLevel = analogRead(PIN_LDR);
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && h >= 0 && h <= 100) {
sensors.humidity = h;
sensors.humidityValid = true;
sensors.lastHumidityRead = millis();
} else if (millis() - sensors.lastHumidityRead > 30000) {
sensors.humidityValid = false;
}
if (!isnan(t) && t >= -40 && t <= 80) {
sensors.ambientTemp = t;
sensors.tempValid = true;
sensors.lastTempRead = millis();
} else if (millis() - sensors.lastTempRead > 30000) {
sensors.tempValid = false;
}
static unsigned long ds18b20RequestTime = 0;
static bool ds18b20Requested = false;
if (!ds18b20Requested) {
ds18b20.requestTemperatures();
ds18b20RequestTime = millis();
ds18b20Requested = true;
} else if (millis() - ds18b20RequestTime > 2000) {
float temp = ds18b20.getTempCByIndex(0);
if (temp != DEVICE_DISCONNECTED_C && temp > -10 && temp < 50) {
sensors.waterTemp = temp;
sensors.waterTempValid = true;
sensors.lastWaterTempRead = millis();
} else if (millis() - sensors.lastWaterTempRead > 60000) {
sensors.waterTempValid = false;
}
ds18b20Requested = false;
}
}
int getSoilMoisturePercent() {
if (sensors.soilMoisture >= 3000) return 0;
if (sensors.soilMoisture <= 800) return 100;
int range = 3000 - 800;
int reading = 3000 - sensors.soilMoisture;
return constrain((reading * 100) / range, 0, 100);
}
// Device control
void updateDeviceStates() {
rainController.update();
mistController.update();
waterfallController.update();
fanController.update();
heaterController.update();
bubbleController.update();
ledController.update();
soilController.update();
}
void applyDeviceStates() {
if (rainController.isRunning()) relayOn(RELAY_RAIN); else relayOff(RELAY_RAIN);
if (mistController.isRunning()) relayOn(RELAY_MIST); else relayOff(RELAY_MIST);
if (waterfallController.isRunning()) relayOn(RELAY_WATERFALL); else relayOff(RELAY_WATERFALL);
if (fanController.isRunning()) relayOn(RELAY_FAN); else relayOff(RELAY_FAN);
if (heaterController.isRunning()) relayOn(RELAY_HEATER); else relayOff(RELAY_HEATER);
if (bubbleController.isRunning()) relayOn(RELAY_BUBBLE); else relayOff(RELAY_BUBBLE);
if (ledController.isRunning()) relayOn(RELAY_LED); else relayOff(RELAY_LED);
}
void emergencyStop(String reason) {
health.logSafetyStop(reason);
rainController.forceStop("Emergency");
mistController.forceStop("Emergency");
waterfallController.forceStop("Emergency");
soilController.forceStop("Emergency");
applyDeviceStates();
}
void executeRainOverride(unsigned long duration, String reason, OverrideType overrideType) {
if (!canRainOverride() && overrideType != EMERGENCY_OVERRIDE) {
health.logSafetyStop("Rain override blocked");
return;
}
Serial.println("RAIN OVERRIDE: " + reason);
health.logManualOverride("Rain - " + reason);
rainController.forceStop("Preparing override");
delay(100);
rainController.startDevice(duration, reason, overrideType);
}
// Auto logic
void runAutoLogic() {
unsigned long currentTime = millis();
int hour = timeinfo.tm_hour;
int minute = timeinfo.tm_min;
WaterSafetyLevel waterLevel = getWaterSafetyLevel();
autoMode.updateDisplay();
autoMode.currentAction = "Monitoring systems";
if (waterLevel == WATER_OVERFLOW_RISK) {
emergencyStop("Overflow risk");
autoMode.waterLevelAction = "EMERGENCY STOP - Overflow";
return;
}
// Rain control
if (waterLevel == WATER_EMERGENCY && config.enableEmergencyRain) {
if (rainController.canStart()) {
rainController.startDevice(RAIN_DURATION_EMERGENCY, "EMERGENCY", EMERGENCY_OVERRIDE);
autoMode.currentAction = "EMERGENCY RAIN";
autoMode.waterLevelAction = "Emergency rain active";
health.emergencyRains++;
}
} else if (waterLevel == WATER_VERY_LOW && rainController.canStart()) {
rainController.startDevice(RAIN_DURATION_EMERGENCY, "Very low water");
autoMode.currentAction = "Extended rain";
autoMode.waterLevelAction = "Extended rain cycle";
} else if ((hour == config.rainHour1 || hour == config.rainHour2) &&
minute < 5 && rainController.canStart() && (waterLevel <= WATER_HIGH)) {
unsigned long duration = (waterLevel <= WATER_LOW) ? RAIN_DURATION_EMERGENCY : RAIN_DURATION_NORMAL;
rainController.startDevice(duration, "Scheduled rain");
autoMode.currentAction = "Scheduled rain";
autoMode.waterLevelAction = "Scheduled rain active";
} else {
autoMode.waterLevelAction = "Water: " + getWaterStatusText();
}
// Waterfall
static bool wasRaining = false;
bool currentlyRaining = rainController.isRunning();
if (wasRaining && !currentlyRaining && waterfallController.canStart()) {
waterfallController.startDevice(WATERFALL_DURATION, "Post-rain waterfall");
autoMode.currentAction = "Post-rain waterfall";
}
wasRaining = currentlyRaining;
// Mist control
if (sensors.humidityValid && mistController.canStart()) {
if (sensors.humidity < config.humidityMin) {
mistController.startDevice(MIST_DURATION_LONG, "Very low humidity");
autoMode.climateAction = "Long mist - Very low humidity";
} else if (sensors.humidity < config.humidityTarget) {
mistController.startDevice(MIST_DURATION_NORMAL, "Below target");
autoMode.climateAction = "Normal mist - Below target";
} else if ((hour == 12 || hour == 18) && minute < 3) {
mistController.startDevice(MIST_DURATION_SHORT, "Scheduled boost");
autoMode.climateAction = "Scheduled humidity boost";
} else {
autoMode.climateAction = "Humidity: " + String(sensors.humidity, 1) + "%";
}
} else {
autoMode.climateAction = sensors.humidityValid ? "Humidity stable" : "Humidity sensor error";
}
// Temperature control
bool needHeating = false;
bool needCooling = false;
if (sensors.tempValid) {
if (sensors.ambientTemp < config.tempHeaterOn) needHeating = true;
if (sensors.ambientTemp > config.tempHeaterOff) needHeating = false;
if (sensors.ambientTemp > config.tempFanOn) needCooling = true;
if (sensors.ambientTemp < config.tempFanOff) needCooling = false;
}
if (sensors.waterTempValid) {
if (sensors.waterTemp < config.waterTempOn) needHeating = true;
if (sensors.waterTemp > config.waterTempOff) needHeating = false;
}
if (sensors.humidityValid && sensors.humidity > config.humidityMax) needCooling = true;
if (needHeating && heaterController.canStart()) {
heaterController.startDevice(10UL * 60UL * 1000UL, "Temperature control");
} else if (!needHeating) {
heaterController.stopDevice("Target reached");
}
if (needCooling && fanController.canStart()) {
fanController.startDevice(FAN_MIN_RUNTIME, "Cooling needed");
} else if (!needCooling) {
fanController.stopDevice("Cooling complete");
}
// Bubble control
if (hour >= 7 && hour < 23) {
unsigned long cycleTime = (currentTime / 60000UL) % 60;
if (cycleTime < 45 && bubbleController.canStart()) {
bubbleController.startDevice(45UL * 60UL * 1000UL, "Daytime cycle");
}
} else {
unsigned long cycleTime = (currentTime / 60000UL) % 60;
if (cycleTime < 20 && bubbleController.canStart()) {
bubbleController.startDevice(20UL * 60UL * 1000UL, "Nighttime cycle");
}
}
// LED control
bool shouldHaveLights = (hour >= config.ledDayStart && hour < config.ledDayEnd);
if (shouldHaveLights && ledController.canStart()) {
int hoursLeft = config.ledDayEnd - hour;
unsigned long duration = hoursLeft * 60UL * 60UL * 1000UL;
ledController.startDevice(duration, "Daylight period");
} else if (!shouldHaveLights) {
ledController.stopDevice("Night time");
}
// Soil watering
int soilPercent = getSoilMoisturePercent();
if (soilPercent < config.soilMoistureMin && soilController.canStart() &&
waterLevel >= WATER_LOW && !rainController.isRunning()) {
soilController.startDevice(SOIL_WATER_DURATION, "Soil low: " + String(soilPercent) + "%");
}
if (autoMode.currentAction == "Monitoring systems") {
autoMode.systemStatus = "All systems normal";
} else {
autoMode.systemStatus = autoMode.currentAction;
}
}
// Manual logic
void runManualLogic() {
autoMode.systemStatus = "Manual control active";
autoMode.currentAction = "Manual overrides in effect";
if (rainController.manualOverride) {
if (!canRainOverride()) {
rainController.manualOverride = false;
health.logSafetyStop("Manual rain blocked");
} else if (rainController.canStart()) {
rainController.startDevice(RAIN_DURATION_OVERRIDE, "Manual control", MANUAL_OVERRIDE);
}
} else if (!rainController.manualOverride && rainController.overrideType == MANUAL_OVERRIDE) {
rainController.stopDevice("Manual off");
}
// Apply manual overrides for other devices
if (mistController.manualOverride && mistController.canStart()) {
mistController.startDevice(MIST_DURATION_NORMAL, "Manual control", MANUAL_OVERRIDE);
} else if (!mistController.manualOverride && mistController.overrideType == MANUAL_OVERRIDE) {
mistController.stopDevice("Manual off");
}
if (waterfallController.manualOverride && waterfallController.canStart()) {
waterfallController.startDevice(WATERFALL_DURATION, "Manual control", MANUAL_OVERRIDE);
} else if (!waterfallController.manualOverride && waterfallController.overrideType == MANUAL_OVERRIDE) {
waterfallController.stopDevice("Manual off");
}
if (fanController.manualOverride && fanController.canStart()) {
fanController.startDevice(FAN_MIN_RUNTIME, "Manual control", MANUAL_OVERRIDE);
} else if (!fanController.manualOverride && fanController.overrideType == MANUAL_OVERRIDE) {
fanController.stopDevice("Manual off");
}
if (heaterController.manualOverride && heaterController.canStart()) {
heaterController.startDevice(10UL * 60UL * 1000UL, "Manual control", MANUAL_OVERRIDE);
} else if (!heaterController.manualOverride && heaterController.overrideType == MANUAL_OVERRIDE) {
heaterController.stopDevice("Manual off");
}
if (bubbleController.manualOverride && bubbleController.canStart()) {
bubbleController.startDevice(60UL * 60UL * 1000UL, "Manual control", MANUAL_OVERRIDE);
} else if (!bubbleController.manualOverride && bubbleController.overrideType == MANUAL_OVERRIDE) {
bubbleController.stopDevice("Manual off");
}
if (ledController.manualOverride && ledController.canStart()) {
ledController.startDevice(4UL * 60UL * 60UL * 1000UL, "Manual control", MANUAL_OVERRIDE);
} else if (!ledController.manualOverride && ledController.overrideType == MANUAL_OVERRIDE) {
ledController.stopDevice("Manual off");
}
}
// Main logic
void runLogic() {
static unsigned long lastLogicUpdate = 0;
if (millis() - lastLogicUpdate < LOGIC_UPDATE_INTERVAL) return;
lastLogicUpdate = millis();
// Auto return check
if (manualMode && !manualSticky && (millis() - lastManualActivity > AUTO_TIMEOUT_MS)) {
manualMode = false;
Serial.println("Auto-returned to AUTO mode");
// Clear overrides
rainController.manualOverride = false;
mistController.manualOverride = false;
waterfallController.manualOverride = false;
fanController.manualOverride = false;
heaterController.manualOverride = false;
bubbleController.manualOverride = false;
ledController.manualOverride = false;
}
updateDeviceStates();
if (manualMode) {
runManualLogic();
} else {
runAutoLogic();
}
applyDeviceStates();
}
// JSON builder
String buildJSON() {
health.uptime = millis() - systemStartTime;
char timeBuf[32];
strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &timeinfo);
String json = "{";
json += "\"system\":{\"time\":\"" + String(timeBuf) + "\",\"mode\":\"" + String(manualMode ? "MANUAL" : "AUTO") + "\",\"uptime\":" + String(health.uptime / 1000) + "},";
json += "\"autoMode\":{\"active\":" + String(!manualMode ? "true" : "false") + ",\"currentAction\":\"" + autoMode.currentAction + "\",\"nextEvent\":\"" + autoMode.nextScheduledEvent + "\",\"systemStatus\":\"" + autoMode.systemStatus + "\",\"waterAction\":\"" + autoMode.waterLevelAction + "\",\"climateAction\":\"" + autoMode.climateAction + "\",\"lightingAction\":\"" + autoMode.lightingAction + "\"},";
json += "\"sensors\":{\"waterLevel\":" + String(sensors.waterLevel) + ",\"waterStatus\":\"" + getWaterStatusText() + "\",\"humidity\":" + String(sensors.humidityValid ? sensors.humidity : -1) + ",\"humidityValid\":" + String(sensors.humidityValid ? "true" : "false") + ",\"ambientTemp\":" + String(sensors.tempValid ? sensors.ambientTemp : -1) + ",\"tempValid\":" + String(sensors.tempValid ? "true" : "false") + ",\"waterTemp\":" + String(sensors.waterTempValid ? sensors.waterTemp : -1) + ",\"waterTempValid\":" + String(sensors.waterTempValid ? "true" : "false") + ",\"soilPercent\":" + String(getSoilMoisturePercent()) + ",\"lightLevel\":" + String(sensors.lightLevel) + ",\"rainSafe\":" + String(isWaterSafeForRain() ? "true" : "false") + ",\"rainOverridePossible\":" + String(canRainOverride() ? "true" : "false") + "},";
json += "\"devices\":{";
json += "\"rain\":{\"active\":" + String(rainController.isRunning() ? "true" : "false") + ",\"status\":\"" + rainController.getStatusText() + "\",\"override\":\"" + rainController.getOverrideStatusText() + "\",\"count\":" + String(rainController.runCount) + ",\"reason\":\"" + rainController.currentReason + "\"},";
json += "\"mist\":{\"active\":" + String(mistController.isRunning() ? "true" : "false") + ",\"status\":\"" + mistController.getStatusText() + "\",\"override\":\"" + mistController.getOverrideStatusText() + "\",\"count\":" + String(mistController.runCount) + ",\"reason\":\"" + mistController.currentReason + "\"},";
json += "\"waterfall\":{\"active\":" + String(waterfallController.isRunning() ? "true" : "false") + ",\"status\":\"" + waterfallController.getStatusText() + "\",\"override\":\"" + waterfallController.getOverrideStatusText() + "\"},";
json += "\"fan\":{\"active\":" + String(fanController.isRunning() ? "true" : "false") + ",\"status\":\"" + fanController.getStatusText() + "\",\"override\":\"" + fanController.getOverrideStatusText() + "\"},";
json += "\"heater\":{\"active\":" + String(heaterController.isRunning() ? "true" : "false") + ",\"status\":\"" + heaterController.getStatusText() + "\",\"override\":\"" + heaterController.getOverrideStatusText() + "\"},";
json += "\"bubble\":{\"active\":" + String(bubbleController.isRunning() ? "true" : "false") + ",\"status\":\"" + bubbleController.getStatusText() + "\",\"override\":\"" + bubbleController.getOverrideStatusText() + "\"},";
json += "\"led\":{\"active\":" + String(ledController.isRunning() ? "true" : "false") + ",\"status\":\"" + ledController.getStatusText() + "\",\"override\":\"" + ledController.getOverrideStatusText() + "\"}";
json += "},";
json += "\"health\":{\"sensorErrors\":" + String(health.sensorErrors) + ",\"safetyStops\":" + String(health.safetyStops) + ",\"emergencyRains\":" + String(health.emergencyRains) + ",\"manualOverrides\":" + String(health.manualOverrides) + ",\"wifiReconnects\":" + String(health.wifiReconnects) + ",\"lastError\":\"" + health.lastErrorMsg + "\"}";
json += "}";
return json;
}
// Complete HTML interface
String getCompleteHTML() {
return R"HTML(<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Full-Featured Paludarium Controller</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #0a0a23, #1a1a3a);
color: #fff;
min-height: 100vh;
}
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { text-align: center; margin-bottom: 20px; }
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
background: linear-gradient(45deg, #00f5ff, #0083B0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.mode-indicator {
padding: 10px 20px;
border-radius: 25px;
font-weight: bold;
font-size: 1.2em;
margin: 10px 0;
display: inline-block;
}
.mode-auto {
background: linear-gradient(45deg, #00ff88, #00cc70);
color: #000;
box-shadow: 0 0 20px rgba(0,255,136,0.3);
}
.mode-manual {
background: linear-gradient(45deg, #ff6b35, #f7931e);
color: #000;
box-shadow: 0 0 20px rgba(255,107,53,0.3);
}
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
.card {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 15px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.card h3 { margin-bottom: 15px; color: #00f5ff; font-size: 1.3em; }
.auto-display {
background: rgba(0,255,136,0.1);
border: 2px solid rgba(0,255,136,0.3);
}
.auto-action {
background: rgba(0,255,136,0.1);
padding: 10px;
border-radius: 8px;
margin: 8px 0;
border-left: 4px solid #00ff88;
}
.sensor-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
padding: 8px;
background: rgba(255,255,255,0.03);
border-radius: 8px;
}
.sensor-label { font-weight: 500; }
.sensor-value { font-weight: bold; font-size: 1.1em; }
.status-good { color: #00ff88; }
.status-warning { color: #ffaa00; }
.status-critical { color: #ff4444; }
.status-optimal { color: #00f5ff; }
.controls-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }
.control-btn {
padding: 15px;
border: none;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
position: relative;
}
.control-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
.btn-on { background: linear-gradient(45deg, #00ff88, #00cc70); color: #000; }
.btn-off { background: linear-gradient(45deg, #333, #555); color: #fff; }
.btn-auto { background: linear-gradient(45deg, #00f5ff, #0083B0); color: #000; }
.btn-manual { background: linear-gradient(45deg, #ff6b35, #f7931e); color: #000; }
.btn-emergency { background: linear-gradient(45deg, #ff4444, #cc0000); color: #fff; }
.btn-override { background: linear-gradient(45deg, #9d4edd, #7209b7); color: #fff; }
.override-indicator {
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff6b35;
}
.device-detail {
font-size: 0.85em;
opacity: 0.8;
margin-top: 5px;
}
.progress-bar {
width: 100%;
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
margin: 5px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00f5ff, #00ff88);
transition: width 0.3s ease;
}
.override-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 15px;
}
.rain-override-section {
background: rgba(255,107,53,0.1);
border: 1px solid rgba(255,107,53,0.3);
border-radius: 10px;
padding: 15px;
margin-top: 15px;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Paludarium Controller</h1>
<div id="modeIndicator" class="mode-indicator mode-auto">AUTO MODE ACTIVE</div>
<div id="systemStatus">System Initializing...</div>
</div>
<div class="card auto-display" id="autoModeCard">
<h3>Automatic Control System</h3>
<div id="autoModeContent">
<div class="auto-action">
<strong>Current Action:</strong> <span id="autoCurrentAction">Initializing...</span>
</div>
<div class="auto-action">
<strong>Next Scheduled:</strong> <span id="autoNextEvent">Calculating...</span>
</div>
<div class="auto-action">
<strong>Water System:</strong> <span id="autoWaterAction">Monitoring...</span>
</div>
<div class="auto-action">
<strong>Climate Control:</strong> <span id="autoClimateAction">Stabilizing...</span>
</div>
<div class="auto-action">
<strong>Lighting:</strong> <span id="autoLightAction">Scheduling...</span>
</div>
</div>
</div>
<div class="status-grid">
<div class="card">
<h3>Water System</h3>
<div class="sensor-row">
<span class="sensor-label">Level:</span>
<span class="sensor-value" id="waterLevel">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Status:</span>
<span class="sensor-value" id="waterStatus">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Temperature:</span>
<span class="sensor-value" id="waterTemp">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Rain Safe:</span>
<span class="sensor-value" id="rainSafe">--</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="waterProgress" style="width: 0%"></div>
</div>
</div>
<div class="card">
<h3>Climate Control</h3>
<div class="sensor-row">
<span class="sensor-label">Humidity:</span>
<span class="sensor-value" id="humidity">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Air Temperature:</span>
<span class="sensor-value" id="airTemp">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Soil Moisture:</span>
<span class="sensor-value" id="soilMoisture">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Light Level:</span>
<span class="sensor-value" id="lightLevel">--</span>
</div>
</div>
<div class="card">
<h3>System Health</h3>
<div class="sensor-row">
<span class="sensor-label">Uptime:</span>
<span class="sensor-value" id="uptime">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Overrides:</span>
<span class="sensor-value" id="overrideCount">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Safety Events:</span>
<span class="sensor-value" id="safetyEvents">--</span>
</div>
<div class="sensor-row">
<span class="sensor-label">Sensors:</span>
<span class="sensor-value" id="sensorHealth">--</span>
</div>
</div>
</div>
<div class="card">
<h3>System Control</h3>
<div class="controls-grid">
<button class="control-btn btn-auto" onclick="setMode('auto')">Auto Mode</button>
<button class="control-btn btn-manual" onclick="setMode('manual')">Manual Mode</button>
<button class="control-btn btn-emergency" onclick="emergencyStop()">Emergency Stop</button>
<button class="control-btn btn-off" onclick="resetHealth()">Reset Health</button>
</div>
</div>
<div class="card">
<h3>Device Controls</h3>
<div class="controls-grid">
<button class="control-btn btn-off" id="rainBtn" onclick="toggleDevice('rain')">
Rain
<div class="device-detail" id="rainDetail">OFF</div>
</button>
<button class="control-btn btn-off" id="mistBtn" onclick="toggleDevice('mist')">
Mist
<div class="device-detail" id="mistDetail">OFF</div>
</button>
<button class="control-btn btn-off" id="waterfallBtn" onclick="toggleDevice('waterfall')">
Waterfall
<div class="device-detail" id="waterfallDetail">OFF</div>
</button>
<button class="control-btn btn-off" id="fanBtn" onclick="toggleDevice('fan')">
Fan
<div class="device-detail" id="fanDetail">OFF</div>
</button>
<button class="control-btn btn-off" id="heaterBtn" onclick="toggleDevice('heater')">
Heater
<div class="device-detail" id="heaterDetail">OFF</div>
</button>
<button class="control-btn btn-off" id="bubbleBtn" onclick="toggleDevice('bubble')">
Bubbles
<div class="device-detail" id="bubbleDetail">OFF</div>
</button>
<button class="control-btn btn-off" id="ledBtn" onclick="toggleDevice('led')">
LED
<div class="device-detail" id="ledDetail">OFF</div>
</button>
</div>
</div>
<div class="card">
<h3>Rain Override System</h3>
<div class="rain-override-section">
<h4>Override Options</h4>
<div class="override-controls">
<button class="control-btn btn-override" onclick="rainOverride('short')">Short (2min)</button>
<button class="control-btn btn-override" onclick="rainOverride('normal')">Normal (4min)</button>
<button class="control-btn btn-override" onclick="rainOverride('long')">Long (8min)</button>
<button class="control-btn btn-emergency" onclick="rainOverride('emergency')" id="emergencyRainBtn">Emergency (10min)</button>
</div>
<div id="rainOverrideStatus" style="margin-top: 10px; font-size: 0.9em; opacity: 0.8;">
Ready for override
</div>
</div>
</div>
<div class="card">
<h3>Quick Actions</h3>
<div class="quick-actions">
<button class="control-btn btn-on" onclick="quickAction('morning')">Morning Routine</button>
<button class="control-btn btn-off" onclick="quickAction('night')">Night Mode</button>
<button class="control-btn btn-on" onclick="quickAction('humidity')">Humidity Boost</button>
<button class="control-btn btn-on" onclick="quickAction('cleaning')">Cleaning Mode</button>
<button class="control-btn btn-emergency" onclick="quickAction('drought')">Drought Response</button>
<button class="control-btn btn-on" onclick="quickAction('showcase')">Showcase Mode</button>
</div>
</div>
</div>
<script>
let currentData = {};
function updateDisplay(data) {
currentData = data;
let isAutoMode = data.system.mode === 'AUTO';
const modeIndicator = document.getElementById('modeIndicator');
if (isAutoMode) {
modeIndicator.className = 'mode-indicator mode-auto';
modeIndicator.textContent = 'AUTO MODE ACTIVE';
} else {
modeIndicator.className = 'mode-indicator mode-manual';
modeIndicator.textContent = 'MANUAL MODE ACTIVE';
}
const autoCard = document.getElementById('autoModeCard');
autoCard.style.display = isAutoMode ? 'block' : 'none';
if (isAutoMode && data.autoMode) {
document.getElementById('autoCurrentAction').textContent = data.autoMode.currentAction;
document.getElementById('autoNextEvent').textContent = data.autoMode.nextEvent;
document.getElementById('autoWaterAction').textContent = data.autoMode.waterAction;
document.getElementById('autoClimateAction').textContent = data.autoMode.climateAction;
document.getElementById('autoLightAction').textContent = data.autoMode.lightingAction;
}
document.getElementById('systemStatus').innerHTML =
`${data.system.mode} Mode | Uptime: ${formatTime(data.system.uptime)} | ${data.sensors.waterStatus} Water`;
document.getElementById('waterLevel').textContent = data.sensors.waterLevel;
document.getElementById('waterStatus').textContent = data.sensors.waterStatus;
document.getElementById('waterStatus').className = 'sensor-value ' + getWaterStatusClass(data.sensors.waterStatus);
document.getElementById('waterTemp').innerHTML = data.sensors.waterTempValid ?
`${data.sensors.waterTemp.toFixed(1)}°C ✓` : `ERROR ✗`;
document.getElementById('rainSafe').innerHTML = data.sensors.rainSafe ?
'<span class="status-good">YES</span>' : '<span class="status-critical">NO</span>';
let waterPercent = Math.min((data.sensors.waterLevel / 2000) * 100, 100);
document.getElementById('waterProgress').style.width = waterPercent + '%';
document.getElementById('humidity').innerHTML = data.sensors.humidityValid ?
`${data.sensors.humidity.toFixed(1)}% ✓` : `ERROR ✗`;
document.getElementById('airTemp').innerHTML = data.sensors.tempValid ?
`${data.sensors.ambientTemp.toFixed(1)}°C ✓` : `ERROR ✗`;
document.getElementById('soilMoisture').textContent = data.sensors.soilPercent + '%';
document.getElementById('lightLevel').textContent = data.sensors.lightLevel;
document.getElementById('uptime').textContent = formatTime(data.system.uptime);
document.getElementById('overrideCount').textContent = data.health.manualOverrides;
document.getElementById('safetyEvents').textContent =
`${data.health.safetyStops} stops, ${data.health.emergencyRains} emergency`;
document.getElementById('sensorHealth').innerHTML = getSensorHealthDisplay(data);
updateDeviceButtons(data.devices);
updateRainOverrideStatus(data);
}
function getWaterStatusClass(status) {
switch(status) {
case 'OPTIMAL': return 'status-optimal';
case 'OK': case 'HIGH': return 'status-good';
case 'LOW': case 'VERY LOW': return 'status-warning';
case 'EMERGENCY': case 'OVERFLOW RISK': return 'status-critical';
default: return '';
}
}
function getSensorHealthDisplay(data) {
let html = '';
html += data.sensors.humidityValid ? '<span style="color: #00ff88;">H✓</span> ' : '<span style="color: #ff4444;">H✗</span> ';
html += data.sensors.tempValid ? '<span style="color: #00ff88;">T✓</span> ' : '<span style="color: #ff4444;">T✗</span> ';
html += data.sensors.waterTempValid ? '<span style="color: #00ff88;">W✓</span>' : '<span style="color: #ff4444;">W✗</span>';
return html;
}
function updateDeviceButtons(devices) {
Object.keys(devices).forEach(device => {
let btn = document.getElementById(device + 'Btn');
let detail = document.getElementById(device + 'Detail');
if (btn && detail) {
let info = devices[device];
let isActive = info.active;
btn.className = 'control-btn ' + (isActive ? 'btn-on' : 'btn-off');
if (info.override && info.override !== 'Auto Mode') {
if (!btn.querySelector('.override-indicator')) {
btn.innerHTML += '<div class="override-indicator"></div>';
}
} else {
let indicator = btn.querySelector('.override-indicator');
if (indicator) indicator.remove();
}
detail.innerHTML = info.status + (info.reason ? '<br><em>' + info.reason + '</em>' : '');
}
});
}
function updateRainOverrideStatus(data) {
let status = document.getElementById('rainOverrideStatus');
let emergencyBtn = document.getElementById('emergencyRainBtn');
if (!data.sensors.rainOverridePossible) {
status.innerHTML = '<span style="color: #ff4444;">Override blocked - water too high</span>';
emergencyBtn.disabled = true;
emergencyBtn.style.opacity = '0.5';
} else if (data.devices.rain.active) {
status.innerHTML = '<span style="color: #00ff88;">Rain active: ' + data.devices.rain.status + '</span>';
emergencyBtn.disabled = false;
emergencyBtn.style.opacity = '1';
} else {
status.innerHTML = '<span style="color: #00f5ff;">Ready for override</span>';
emergencyBtn.disabled = false;
emergencyBtn.style.opacity = '1';
}
}
function formatTime(seconds) {
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function toggleDevice(device) {
fetch(`/control?device=${device}&state=toggle`)
.then(response => response.text())
.then(result => {
if (result.includes('BLOCKED')) {
alert('⚠️ ' + result);
}
});
}
function setMode(mode) {
fetch('/' + mode);
}
function emergencyStop() {
if (confirm('Emergency stop all water devices?')) {
fetch('/emergency-stop');
}
}
function resetHealth() {
if (confirm('Reset all health counters?')) {
fetch('/reset-health');
}
}
function rainOverride(type) {
let duration, reason;
switch(type) {
case 'short': duration = 2; reason = 'Short override'; break;
case 'normal': duration = 4; reason = 'Normal override'; break;
case 'long': duration = 8; reason = 'Extended override'; break;
case 'emergency': duration = 10; reason = 'Emergency override'; break;
default: return;
}
if (!currentData.sensors.rainOverridePossible && type !== 'emergency') {
alert('Rain override blocked - water level too high!');
return;
}
if (confirm(`Execute ${reason} (${duration} minutes)?`)) {
fetch(`/rain-override?type=${type}&duration=${duration}&reason=${encodeURIComponent(reason)}`);
}
}
function quickAction(action) {
let messages = {
'morning': 'Execute morning routine?',
'night': 'Switch to night mode?',
'humidity': 'Start humidity boost?',
'cleaning': 'Enable cleaning mode?',
'drought': 'Execute drought response?',
'showcase': 'Enable showcase mode?'
};
if (confirm(messages[action])) {
fetch(`/quick-action?action=${action}`);
}
}
function refreshData() {
fetch('/status')
.then(response => response.json())
.then(data => updateDisplay(data))
.catch(error => {
console.error('Update failed:', error);
document.getElementById('systemStatus').textContent = 'Connection Error';
});
}
setInterval(refreshData, 3000);
refreshData();
</script>
</body>
</html>)HTML";
}
// Web handlers
void handleControl() {
String device = server.arg("device");
String state = server.arg("state");
bool turnOn = (state == "toggle") ?
(device == "rain" ? !rainController.manualOverride :
device == "mist" ? !mistController.manualOverride :
device == "waterfall" ? !waterfallController.manualOverride :
device == "fan" ? !fanController.manualOverride :
device == "heater" ? !heaterController.manualOverride :
device == "bubble" ? !bubbleController.manualOverride :
device == "led" ? !ledController.manualOverride : false) :
(state == "on");
if (!manualMode) {
manualMode = true;
Serial.println("Manual mode via web");
}
lastManualActivity = millis();
if (device == "rain" && turnOn && !canRainOverride()) {
server.send(400, "text/plain", "BLOCKED: Water level too high!");
return;
}
if (device == "rain") rainController.manualOverride = turnOn;
else if (device == "mist") mistController.manualOverride = turnOn;
else if (device == "waterfall") waterfallController.manualOverride = turnOn;
else if (device == "fan") fanController.manualOverride = turnOn;
else if (device == "heater") heaterController.manualOverride = turnOn;
else if (device == "bubble") bubbleController.manualOverride = turnOn;
else if (device == "led") ledController.manualOverride = turnOn;
server.send(200, "text/plain", "OK");
}
void handleRainOverride() {
String type = server.arg("type");
int duration = server.arg("duration").toInt();
String reason = server.arg("reason");
unsigned long durationMs = duration * 60UL * 1000UL;
OverrideType overrideType = (type == "emergency") ? EMERGENCY_OVERRIDE : RAIN_FORCE_OVERRIDE;
executeRainOverride(durationMs, reason, overrideType);
server.send(200, "text/plain", "Override executed");
}
void handleQuickAction() {
String action = server.arg("action");
if (action == "morning") {
executeRainOverride(RAIN_DURATION_NORMAL, "Morning routine", SCHEDULED_OVERRIDE);
ledController.startDevice(8UL * 60UL * 60UL * 1000UL, "Morning routine", SCHEDULED_OVERRIDE);
mistController.startDevice(MIST_DURATION_NORMAL, "Morning routine", SCHEDULED_OVERRIDE);
} else if (action == "night") {
ledController.forceStop("Night mode");
bubbleController.forceStop("Night mode");
fanController.forceStop("Night mode");
} else if (action == "humidity") {
mistController.startDevice(MIST_DURATION_LONG, "Humidity boost", SCHEDULED_OVERRIDE);
} else if (action == "cleaning") {
emergencyStop("Cleaning mode");
} else if (action == "drought") {
if (canRainOverride()) {
executeRainOverride(RAIN_DURATION_EMERGENCY, "Drought response", EMERGENCY_OVERRIDE);
mistController.startDevice(MIST_DURATION_LONG, "Drought response", EMERGENCY_OVERRIDE);
}
} else if (action == "showcase") {
if (canRainOverride()) {
executeRainOverride(RAIN_DURATION_NORMAL, "Showcase", SCHEDULED_OVERRIDE);
}
waterfallController.startDevice(WATERFALL_DURATION, "Showcase", SCHEDULED_OVERRIDE);
mistController.startDevice(MIST_DURATION_NORMAL, "Showcase", SCHEDULED_OVERRIDE);
bubbleController.startDevice(60UL * 60UL * 1000UL, "Showcase", SCHEDULED_OVERRIDE);
ledController.startDevice(4UL * 60UL * 60UL * 1000UL, "Showcase", SCHEDULED_OVERRIDE);
}
server.send(200, "text/plain", "Action executed");
}
void handleAuto() {
manualMode = false;
manualSticky = false;
rainController.manualOverride = false;
mistController.manualOverride = false;
waterfallController.manualOverride = false;
fanController.manualOverride = false;
heaterController.manualOverride = false;
bubbleController.manualOverride = false;
ledController.manualOverride = false;
server.send(200, "text/plain", "AUTO");
Serial.println("AUTO mode via web");
}
void handleManual() {
manualMode = true;
manualSticky = true;
lastManualActivity = millis();
server.send(200, "text/plain", "MANUAL");
Serial.println("MANUAL mode via web");
}
void handleEmergencyStop() {
emergencyStop("Web emergency stop");
server.send(200, "text/plain", "Emergency stop");
}
void handleResetHealth() {
health.sensorErrors = 0;
health.safetyStops = 0;
health.emergencyRains = 0;
health.manualOverrides = 0;
health.lastErrorMsg = "";
server.send(200, "text/plain", "Health reset");
Serial.println("Health reset via web");
}
void handleStatus() {
server.send(200, "application/json", buildJSON());
}
void handleRoot() {
server.send(200, "text/html", getCompleteHTML());
}
// Serial commands
void processSerialCommand(String cmd) {
cmd.toLowerCase();
cmd.trim();
if (cmd == "status") {
Serial.println("=== SYSTEM STATUS ===");
Serial.println("Mode: " + String(manualMode ? "MANUAL" : "AUTO"));
Serial.println("Water: " + String(sensors.waterLevel) + " (" + getWaterStatusText() + ")");
Serial.println("Rain Safe: " + String(isWaterSafeForRain() ? "YES" : "NO"));
Serial.println("Override Possible: " + String(canRainOverride() ? "YES" : "NO"));
Serial.println("=== SENSORS ===");
Serial.println("Humidity: " + String(sensors.humidityValid ? String(sensors.humidity) + "%" : "ERROR"));
Serial.println("Air Temp: " + String(sensors.tempValid ? String(sensors.ambientTemp) + "C" : "ERROR"));
Serial.println("Water Temp: " + String(sensors.waterTempValid ? String(sensors.waterTemp) + "C" : "ERROR"));
Serial.println("Soil: " + String(getSoilMoisturePercent()) + "%");
Serial.println("Light: " + String(sensors.lightLevel));
Serial.println("=== DEVICES ===");
Serial.println("Rain: " + rainController.getStatusText() + " | " + rainController.getOverrideStatusText());
Serial.println("Mist: " + mistController.getStatusText() + " | " + mistController.getOverrideStatusText());
Serial.println("Waterfall: " + waterfallController.getStatusText());
Serial.println("Fan: " + fanController.getStatusText());
Serial.println("Heater: " + heaterController.getStatusText());
Serial.println("Bubbles: " + bubbleController.getStatusText());
Serial.println("LED: " + ledController.getStatusText());
if (!manualMode) {
Serial.println("=== AUTO MODE ===");
Serial.println("Current: " + autoMode.currentAction);
Serial.println("Next: " + autoMode.nextScheduledEvent);
Serial.println("Water: " + autoMode.waterLevelAction);
Serial.println("Climate: " + autoMode.climateAction);
}
Serial.println("=== HEALTH ===");
Serial.println("Uptime: " + String(health.uptime / 1000) + "s");
Serial.println("Overrides: " + String(health.manualOverrides));
Serial.println("Errors: " + String(health.sensorErrors));
Serial.println("Safety: " + String(health.safetyStops));
Serial.println("Emergency: " + String(health.emergencyRains));
return;
}
if (cmd == "auto") {
manualMode = false;
manualSticky = false;
rainController.manualOverride = false;
mistController.manualOverride = false;
waterfallController.manualOverride = false;
fanController.manualOverride = false;
heaterController.manualOverride = false;
bubbleController.manualOverride = false;
ledController.manualOverride = false;
Serial.println("AUTO mode activated");
return;
}
if (cmd == "manual") {
manualMode = true;
manualSticky = true;
Serial.println("MANUAL mode activated");
return;
}
if (cmd.startsWith("rain override ")) {
String type = cmd.substring(14);
unsigned long duration = RAIN_DURATION_OVERRIDE;
OverrideType overrideType = RAIN_FORCE_OVERRIDE;
if (type == "short") duration = 2UL * 60UL * 1000UL;
else if (type == "normal") duration = RAIN_DURATION_NORMAL;
else if (type == "long") duration = RAIN_DURATION_EMERGENCY;
else if (type == "emergency") {
duration = RAIN_DURATION_OVERRIDE;
overrideType = EMERGENCY_OVERRIDE;
}
executeRainOverride(duration, "Serial: " + type, overrideType);
return;
}
if (cmd == "emergency stop") {
emergencyStop("Serial command");
Serial.println("EMERGENCY STOP");
return;
}
if (cmd == "reset health") {
health.sensorErrors = 0;
health.safetyStops = 0;
health.emergencyRains = 0;
health.manualOverrides = 0;
health.lastErrorMsg = "";
Serial.println("Health counters reset");
return;
}
if (cmd == "help") {
Serial.println("=== COMMANDS ===");
Serial.println("status - System status");
Serial.println("auto/manual - Mode control");
Serial.println("rain override [short/normal/long/emergency]");
Serial.println("emergency stop - Stop all water");
Serial.println("reset health - Reset counters");
Serial.println("[device] on/off - Device control");
Serial.println("Devices: rain, mist, waterfall, fan, heater, bubble, led");
return;
}
if (cmd.endsWith(" on") || cmd.endsWith(" off")) {
bool turnOn = cmd.endsWith(" on");
String device = cmd;
device.replace(" on", "");
device.replace(" off", "");
if (!manualMode) {
manualMode = true;
Serial.println("Switched to MANUAL");
}
lastManualActivity = millis();
if (device == "rain") {
if (turnOn && !canRainOverride()) {
Serial.println("RAIN BLOCKED: Water unsafe");
return;
}
rainController.manualOverride = turnOn;
} else if (device == "mist") {
mistController.manualOverride = turnOn;
} else if (device == "waterfall") {
waterfallController.manualOverride = turnOn;
} else if (device == "fan") {
fanController.manualOverride = turnOn;
} else if (device == "heater") {
heaterController.manualOverride = turnOn;
} else if (device == "bubble" || device == "bubbles") {
bubbleController.manualOverride = turnOn;
} else if (device == "led" || device == "light") {
ledController.manualOverride = turnOn;
} else {
Serial.println("Unknown: " + device);
return;
}
Serial.println(device + " " + (turnOn ? "ON" : "OFF"));
} else {
Serial.println("Unknown: " + cmd);
}
}
// Setup web server
void setupWeb() {
server.on("/", handleRoot);
server.on("/status", handleStatus);
server.on("/control", handleControl);
server.on("/auto", handleAuto);
server.on("/manual", handleManual);
server.on("/rain-override", handleRainOverride);
server.on("/quick-action", handleQuickAction);
server.on("/emergency-stop", handleEmergencyStop);
server.on("/reset-health", handleResetHealth);
server.begin();
Serial.println("Web server started");
}
// Initialize sensors
void initSensors() {
Serial.println("Initializing sensors...");
dht.begin();
delay(2000);
ds18b20.begin();
ds18b20.setResolution(12);
ds18b20.setWaitForConversion(false);
Serial.printf("DS18B20 devices: %d\n", ds18b20.getDeviceCount());
readSensors();
Serial.println("Sensors ready");
}
// Main setup
void setup() {
Serial.begin(115200);
delay(2000);
systemStartTime = millis();
Serial.println("=== PALUDARIUM CONTROLLER v8.0 ===");
Serial.println("Full-Featured with Rain Override");
// Initialize
initSensors();
// Setup pins
pinMode(PIN_WATER_LEVEL, INPUT);
pinMode(PIN_LDR, INPUT);
pinMode(PIN_SOIL_MOISTURE, INPUT);
// Setup relays
int relays[] = {RELAY_WATERFALL, RELAY_RAIN, RELAY_BUBBLE, RELAY_FAN,
RELAY_HEATER, RELAY_MIST, RELAY_LED};
for (int pin : relays) {
pinMode(pin, OUTPUT);
relayOff(pin);
}
Serial.println("All devices OFF");
// WiFi
Serial.println("Connecting WiFi: " + String(WIFI_SSID));
WiFi.begin(WIFI_SSID, WIFI_PASS);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(1000);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi connected: " + WiFi.localIP().toString());
Serial.println("Web interface: http://" + WiFi.localIP().toString());
} else {
Serial.println("\nWiFi failed - continuing without web");
health.wifiReconnects++;
}
// Time sync
configTime(0, 3600, "pool.ntp.org");
updateTime();
// Web server
if (WiFi.status() == WL_CONNECTED) {
setupWeb();
}
Serial.println("");
Serial.println("=== SYSTEM READY ===");
Serial.println("Mode: AUTO (default)");
Serial.println("Water: " + getWaterStatusText());
Serial.println("Commands: status, auto, manual, help");
Serial.println("");
Serial.println("Features:");
Serial.println("- Rain override system");
Serial.println("- Comprehensive device control");
Serial.println("- Auto mode display");
Serial.println("- Safety systems");
Serial.println("- Real-time monitoring");
Serial.println("- Quick action presets");
Serial.println("");
Serial.println("AUTO MODE ACTIVE");
}
// Main loop
void loop() {
// Web server
if (WiFi.status() == WL_CONNECTED) {
server.handleClient();
} else {
static unsigned long lastReconnect = 0;
if (millis() - lastReconnect > 30000) {
WiFi.reconnect();
lastReconnect = millis();
health.wifiReconnects++;
}
}
// Serial commands
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
processSerialCommand(command);
}
// Time update
static unsigned long lastTimeUpdate = 0;
if (millis() - lastTimeUpdate > 60000) {
updateTime();
lastTimeUpdate = millis();
}
// Main operations
readSensors();
runLogic();
delay(50);
}
Loading
esp32-s3-devkitc-1
esp32-s3-devkitc-1