/*
Smart Pot - Version for ESP32 (WiFi status only on Serial / environment UI)
- LCD via I2C used only for sensor display (no WiFi status printed to LCD)
- WiFi connection messages go to Serial (so Wokwi/VSCode environment shows them)
- Fixed ThingSpeak URL usage and safer reconnect logic
*/
#include <Arduino.h>
#include <Wire.h>
#include "DHTesp.h"
#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <HTTPClient.h>
// ---------------- PIN CONFIG ----------------
const int PIN_SOIL_ADC = 34;
const int PIN_LDR_ADC = 35;
const int PIN_DHT = 23;
const int PIN_RELAY = 19;
const int PIN_LED_RED = 18;
const int PIN_LED_GREEN = 5;
// ---------------- LCD & DHT ----------------
LiquidCrystal_I2C *plcd = nullptr; // will init if I2C device found
DHTesp dht;
// ---------------- WiFi & ThingSpeak ----------------
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
const long myChannelNumber = 3051948;
const char* THINGSPEAK_API_KEY = "6A0D67MVSQ1T256F";
const char* THINGSPEAK_HOST = "http://api.thingspeak.com/update";
const unsigned long CLOUD_SEND_INTERVAL = 15000UL;
unsigned long lastCloudSendMillis = 0;
// ---------------- Simulation & thresholds ----------------
const bool SIMULATE_SOIL = true;
const bool SIMULATE_DHT_FROM_SOIL = true;
const bool TEST_MODE = true;
const float RED_LED_THRESHOLD = 20.0f;
const float PUMP_START_THRESHOLD = 15.0f;
const float GREEN_ON_THRESHOLD = 50.0f;
const float PUMP_STOP_THRESHOLD = 70.0f;
const unsigned long READ_INTERVAL = (TEST_MODE ? 300UL : 2000UL);
const unsigned long SIM_STEP_MS = (TEST_MODE ? 100UL : 400UL);
const unsigned long MAX_PUMP_TIME = (TEST_MODE ? 15000UL : 60000UL);
// ---------------- Calibration & simulation params ----------------
int SOIL_WET_RAW = 300;
int SOIL_DRY_RAW = 3600;
const float SIM_WATERING_INCREMENT_PER_SEC = (TEST_MODE ? 10.0f : 1.5f);
const float SIM_DRYING_DECREMENT_BASE_PER_SEC = (TEST_MODE ? 2.0f : 0.05f);
// ---------------- State & buffers ----------------
const int SAMPLE_SIZE = 6;
int soilBuf[SAMPLE_SIZE];
int ldrBuf[SAMPLE_SIZE];
int soilIdx = 0, ldrIdx = 0;
bool bufInitialized = false;
unsigned long lastReadMillis = 0;
unsigned long lastSimUpdate = 0;
bool isWatering = false;
unsigned long wateringStartMillis = 0;
unsigned long lastWaterMillis = 0;
float simSoilPct = 55.0f;
float lastTemperature = NAN;
float lastHumidity = NAN;
// ---------------- Relay polarity ----------------
const bool RELAY_ACTIVE_HIGH = true;
void relayWrite(bool on) {
if (RELAY_ACTIVE_HIGH) digitalWrite(PIN_RELAY, on ? HIGH : LOW);
else digitalWrite(PIN_RELAY, on ? LOW : HIGH);
}
// ---------------- Helpers ----------------
int addAndGetAvg(int *buffer, int &index, int value) {
buffer[index] = value;
index = (index + 1) % SAMPLE_SIZE;
long sum = 0;
for (int i = 0; i < SAMPLE_SIZE; ++i) sum += buffer[i];
return (int)(sum / SAMPLE_SIZE);
}
float mapAndClamp(float x, float in_min, float in_max, float out_min, float out_max) {
if (in_max - in_min == 0) return out_min;
float y = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
if (y < out_min) y = out_min;
if (y > out_max) y = out_max;
return y;
}
float soilRawToPct(int raw) {
if (SOIL_WET_RAW < SOIL_DRY_RAW) {
return mapAndClamp(raw, (float)SOIL_WET_RAW, (float)SOIL_DRY_RAW, 100.0f, 0.0f);
} else {
return mapAndClamp(raw, (float)SOIL_DRY_RAW, (float)SOIL_WET_RAW, 0.0f, 100.0f);
}
}
float ldrRawToPct(int raw) {
float pct = 100.0f * (float)raw / 4095.0f;
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
return pct;
}
float tempDryingFactor(float temperatureC) {
if (isnan(temperatureC)) temperatureC = 25.0f;
float factor = 1.0f + (temperatureC - 25.0f) * 0.12f;
if (factor < 0.3f) factor = 0.3f;
if (factor > 6.0f) factor = 6.0f;
return factor;
}
void updateLEDsImmediate(float soilPct, bool watering) {
if (!watering && soilPct <= RED_LED_THRESHOLD) {
digitalWrite(PIN_LED_RED, HIGH);
digitalWrite(PIN_LED_GREEN, LOW);
return;
}
if (watering && soilPct >= GREEN_ON_THRESHOLD) {
digitalWrite(PIN_LED_GREEN, HIGH);
digitalWrite(PIN_LED_RED, LOW);
return;
}
if (soilPct >= GREEN_ON_THRESHOLD) {
digitalWrite(PIN_LED_GREEN, HIGH);
digitalWrite(PIN_LED_RED, LOW);
return;
}
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_RED, LOW);
}
// ---------------- I2C scanner & LCD init ----------------
uint8_t scanI2CAddress() {
for (uint8_t addr = 1; addr < 127; ++addr) {
Wire.beginTransmission(addr);
uint8_t err = Wire.endTransmission();
if (err == 0) return addr;
}
return 0;
}
bool initLCDWithAutoAddress() {
uint8_t addr = scanI2CAddress();
if (addr == 0) {
Serial.println("[LCD] No I2C device found!");
return false;
}
Serial.print("[LCD] Found I2C address 0x");
Serial.println(addr, HEX);
if (plcd) { delete plcd; plcd = nullptr; }
plcd = new LiquidCrystal_I2C(addr, 16, 2);
plcd->init();
plcd->backlight();
delay(50);
return true;
}
// ---------------- WiFi connect with retry/backoff ----------------
// ---------------- WiFi connect (طريقة مشابهه لبيان المشروع الصغير) ----------------
void connectWiFi() {
Serial.print("[NET] Connecting to WiFi: ");
Serial.println(WIFI_SSID);
// نبدأ المحاولة (نستخدم begin العادي)
WiFi.begin(WIFI_SSID, WIFI_PASS);
unsigned long start = millis();
// ننتظر حتى 30 ثانية كحد أقصى للاتصال المبدئي
while (WiFi.status() != WL_CONNECTED && (millis() - start) < 30000UL) {
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println("[NET] Connected to WiFi!");
Serial.print("[NET] IP Address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("[NET] WiFi connect FAILED (will retry later in loop)");
}
}
// ---------------- ThingSpeak send (بناء URL كما في مشروعك الصغير) ----------------
// signature يجب أن تكون مطابقة لتعريفك الأصلي: (float t, float h, float soil, float light, int relayState)
void sendToThingSpeak_real(float t, float h, float soil, float light, int relayState) {
// تأكد من وجود اتصال واي-فاي أو حاول إعادة الاتصال
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[CLOUD] Skipping send - no WiFi");
return;
}
}
// بناء رابط GET لThingSpeak بنفس الحقول الموجودة في مشروعك الكبير
String url = String(THINGSPEAK_HOST) + "?api_key=" + THINGSPEAK_API_KEY;
url += "&field1=" + String(t, 2); // field1 = temperature
url += "&field2=" + String(h, 2); // field2 = humidity
url += "&field3=" + String(soil, 2); // field3 = soil%
url += "&field4=" + String(light, 2); // field4 = light%
url += "&field5=" + String(relayState); // field5 = relay (1/0)
Serial.print("[CLOUD] Sending to ThingSpeak: ");
Serial.println(url);
HTTPClient http;
http.begin(url); // استخدم الرابط المبني
int httpCode = http.GET();
if (httpCode > 0) {
Serial.print("[CLOUD] HTTP code: ");
Serial.println(httpCode);
if (httpCode >= 200 && httpCode < 300) {
Serial.println("[CLOUD] ThingSpeak update OK");
// مؤشر بصري (اختياري) — تأكد أن اسم PIN موجود في مشروعك (مثلاً ledPin أو PIN_LED_GREEN)
#if defined(PIN_LED_GREEN)
digitalWrite(PIN_LED_GREEN, HIGH);
delay(120);
digitalWrite(PIN_LED_GREEN, LOW);
#elif defined(ledPin)
digitalWrite(ledPin, HIGH);
delay(120);
digitalWrite(ledPin, LOW);
#endif
} else {
Serial.println("[CLOUD] ThingSpeak returned non-2xx code");
}
} else {
Serial.print("[CLOUD] Error sending data, httpCode=");
Serial.println(httpCode);
}
http.end();
}
void sendToCloud(float t, float h, float soil, float light, int relayState) {
sendToThingSpeak_real(t,h,soil,light,relayState);
}
// ---------------- Serial command processing ----------------
void processSerialCommands() {
if (!Serial.available()) return;
String line = Serial.readStringUntil('\n');
line.trim();
if (line.length() == 0) return;
if (line.equalsIgnoreCase("pump on")) {
isWatering = true;
wateringStartMillis = millis();
relayWrite(true);
Serial.println("[CMD] pump ON");
updateLEDsImmediate(simSoilPct, true);
} else if (line.equalsIgnoreCase("pump off")) {
isWatering = false;
relayWrite(false);
lastWaterMillis = millis();
Serial.println("[CMD] pump OFF");
updateLEDsImmediate(simSoilPct, false);
} else if (line.equalsIgnoreCase("status")) {
Serial.printf("Status: Soil=%.1f%% T=%.1fC H=%.1f%% Watering=%s\n",
simSoilPct, isnan(lastTemperature)?0.0f:lastTemperature,
isnan(lastHumidity)?0.0f:lastHumidity,
isWatering?"YES":"NO");
} else {
Serial.println("[CMD] Unknown. Use: pump on | pump off | status");
}
}
// ---------------- setup ----------------
void setup() {
Serial.begin(115200);
delay(100);
// ADC config
analogReadResolution(12);
analogSetPinAttenuation(PIN_SOIL_ADC, ADC_11db);
analogSetPinAttenuation(PIN_LDR_ADC, ADC_11db);
// DHT
dht.setup(PIN_DHT, DHTesp::DHT22);
// pins
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
relayWrite(false);
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, LOW);
for (int i=0;i<SAMPLE_SIZE;i++){ soilBuf[i]=0; ldrBuf[i]=0; }
bufInitialized = false;
// Start I2C and init LCD (SDA=21,SCL=22)
Wire.begin(21,22);
if (!initLCDWithAutoAddress()) {
Serial.println("[SETUP] LCD not found - continue without LCD");
} else {
plcd->clear();
plcd->setCursor(0,0);
plcd->print("Smart Pot Init");
}
Serial.println();
Serial.println("=== Smart Pot : WiFi -> ThingSpeak ===");
Serial.print("[MODE] SIMULATE_SOIL="); Serial.print(SIMULATE_SOIL ? "ON" : "OFF");
Serial.print(" TEST_MODE="); Serial.println(TEST_MODE ? "ON" : "OFF");
// initial wifi connect attempt (status goes to Serial)
connectWiFi();
}
// ---------------- main loop ----------------
void loop() {
unsigned long now = millis();
processSerialCommands();
// simulation update
if (SIMULATE_SOIL) {
unsigned long dt = now - lastSimUpdate;
if (dt >= SIM_STEP_MS) {
lastSimUpdate = now;
float secs = dt / 1000.0f;
if (isWatering) {
simSoilPct += SIM_WATERING_INCREMENT_PER_SEC * secs;
} else {
float tempForFactor = isnan(lastTemperature) ? 25.0f : lastTemperature;
float factor = tempDryingFactor(tempForFactor);
float decay = SIM_DRYING_DECREMENT_BASE_PER_SEC * factor * secs;
simSoilPct -= decay;
}
if (simSoilPct > 100.0f) simSoilPct = 100.0f;
if (simSoilPct < 0.0f) simSoilPct = 0.0f;
updateLEDsImmediate(simSoilPct, isWatering);
}
}
// safety max pump time
if (isWatering && (now - wateringStartMillis >= MAX_PUMP_TIME)) {
isWatering = false;
relayWrite(false);
lastWaterMillis = now;
Serial.println("[SAFETY] MAX_PUMP_TIME reached -> stopping pump.");
updateLEDsImmediate(SIMULATE_SOIL ? simSoilPct : 0.0f, false);
}
// sensor reads
if (now - lastReadMillis >= READ_INTERVAL) {
lastReadMillis = now;
int soilRaw = analogRead(PIN_SOIL_ADC);
int ldrRaw = analogRead(PIN_LDR_ADC);
if (!bufInitialized) { for (int i=0;i<SAMPLE_SIZE;i++){ soilBuf[i]=soilRaw; ldrBuf[i]=ldrRaw; } bufInitialized=true; }
int soilAvgRaw = addAndGetAvg(soilBuf, soilIdx, soilRaw);
int ldrAvgRaw = addAndGetAvg(ldrBuf, ldrIdx, ldrRaw);
float measuredSoilPct = soilRawToPct(soilAvgRaw);
float lightPct = ldrRawToPct(ldrAvgRaw);
float t = dht.getTemperature();
float h = dht.getHumidity();
if (!isnan(t) && !isnan(h)) {
lastTemperature = t;
lastHumidity = h;
} else {
if (SIMULATE_DHT_FROM_SOIL) {
lastHumidity = mapAndClamp(SIMULATE_SOIL ? simSoilPct : measuredSoilPct, 0.0f, 100.0f, 20.0f, 85.0f);
if (isnan(lastTemperature)) lastTemperature = 25.0f;
} else {
Serial.println("[WARN] DHT read failed - retaining last values.");
}
}
float soilPct = SIMULATE_SOIL ? simSoilPct : measuredSoilPct;
updateLEDsImmediate(soilPct, isWatering);
// Serial status
Serial.print("["); Serial.print(now/1000); Serial.print("s] ");
Serial.print("SoilRaw:"); Serial.print(soilAvgRaw);
Serial.print(" Soil%:"); Serial.print(soilPct,1);
Serial.print("% Light%:"); Serial.print(lightPct,1);
Serial.print(" T:"); Serial.print(isnan(lastTemperature) ? 0.0f : lastTemperature,1);
Serial.print("C H:"); Serial.print(isnan(lastHumidity) ? 0.0f : lastHumidity,1);
Serial.print("% Watering:"); Serial.println(isWatering ? "YES" : "NO");
// update LCD (if available) - NOTE: WiFi info NOT shown here
if (plcd) {
plcd->clear();
plcd->setCursor(0,0);
if (!isnan(lastTemperature) && !isnan(lastHumidity)) {
plcd->print(String(lastTemperature,1) + "C " + String(lastHumidity,0) + "%");
} else {
plcd->print("DHT err");
}
plcd->setCursor(0,1);
plcd->print("Soil:");
plcd->print(String(soilPct,0));
plcd->print("% ");
plcd->print(isWatering ? "W:ON" : "W:OFF");
}
// watering logic (hysteresis)
if (!isWatering && soilPct <= PUMP_START_THRESHOLD) {
isWatering = true;
wateringStartMillis = now;
relayWrite(true);
Serial.println("[ACTION] START watering (soil <= threshold).");
updateLEDsImmediate(soilPct, true);
}
if (isWatering && soilPct >= PUMP_STOP_THRESHOLD) {
isWatering = false;
relayWrite(false);
lastWaterMillis = now;
Serial.println("[ACTION] STOP watering (soil >= threshold).");
updateLEDsImmediate(soilPct, false);
}
} // end sensors
// cloud send interval
if (now - lastCloudSendMillis >= CLOUD_SEND_INTERVAL) {
lastCloudSendMillis = now;
float soilToSend = SIMULATE_SOIL ? simSoilPct : soilRawToPct(soilBuf[(soilIdx - 1 + SAMPLE_SIZE) % SAMPLE_SIZE]);
float lightToSend = ldrRawToPct(ldrBuf[(ldrIdx - 1 + SAMPLE_SIZE) % SAMPLE_SIZE]);
int wateringState = isWatering ? 1 : 0;
float sendT = isnan(lastTemperature) ? 0.0f : lastTemperature;
float sendH = isnan(lastHumidity) ? 0.0f : lastHumidity;
sendToCloud(sendT, sendH, soilToSend, lightToSend, wateringState);
}
// attempt reconnect if WiFi dropped (non-blocking small check)
static unsigned long lastWifiCheck = 0;
if (millis() - lastWifiCheck > 10000UL) {
lastWifiCheck = millis();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[NET] WiFi lost - attempting reconnect...");
connectWiFi();
}
}
delay(10);
}