// ESP32 Smart Irrigation (local, no Blynk)
// Publishes telemetry to MQTT and listens to "cmd" to toggle pump.
// Moisture (%): potentiometer on ADC34; thirst = 100 - moisture
// Temp/RH: DHT22 on GPIO15 (cached reads; optional)
// Pump LED/relay: GPIO25
// Auto mode is handled by the server (FastAPI). This device just measures & obeys cmd.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHTesp.h>
// ---------- CONFIG ----------
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
// Use a public broker so Wokwi can reach it (your server will also connect here)
#define MQTT_HOST "test.mosquitto.org"
#define MQTT_PORT 1883
// Area / topics
const char* AREA_ID = "area-tn-001";
String TOPIC_TELE; // chrab/<area>/telemetry
String TOPIC_CMD; // chrab/<area>/cmd
String TOPIC_CFG; // chrab/<area>/cfg (optional: {"SAMPLE_MS":3000})
const uint32_t DEFAULT_SAMPLE_MS = 3000;
// Pins
const int PIN_SOIL = 34; // potentiometer
const int PIN_DHT = 15; // DHT22
const int PIN_PUMP = 25; // LED/relay (HIGH=ON)
// ---------- STATE ----------
WiFiClient espClient;
PubSubClient mqtt(espClient);
DHTesp dht;
unsigned long lastPub = 0;
unsigned long lastDht = 0;
uint32_t SAMPLE_MS = DEFAULT_SAMPLE_MS;
bool pumpOn = false;
// Tank & NPK simple sim
float waterLevel = 80.0f; // %
float Nppm = 20.0f, Pppm = 10.0f, Kppm = 15.0f;
// DHT cache
float tC_cache = 25.0f;
float rh_cache = 55.0f;
// ---------- WIFI ----------
void wifiConnect() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("[WiFi] Connecting");
while (WiFi.status() != WL_CONNECTED) { delay(350); Serial.print("."); }
Serial.println("\n[WiFi] Connected: " + WiFi.localIP().toString());
}
// ---------- MQTT ----------
void onMqttMessage(char* topic, uint8_t* payload, unsigned int len) {
String t = topic;
if (t == TOPIC_CMD) {
if (len > 0 && payload[0] == '1') {
pumpOn = true;
Serial.println("[CMD] PUMP=ON");
} else {
pumpOn = false;
Serial.println("[CMD] PUMP=OFF");
}
} else if (t == TOPIC_CFG) {
// optional: { "SAMPLE_MS": 3000 }
StaticJsonDocument<128> doc;
DeserializationError e = deserializeJson(doc, payload, len);
if (!e) {
if (doc.containsKey("SAMPLE_MS")) {
SAMPLE_MS = (uint32_t)doc["SAMPLE_MS"].as<uint32_t>();
Serial.printf("[CFG] SAMPLE_MS=%u\n", SAMPLE_MS);
}
}
}
}
void mqttConnect() {
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMqttMessage);
Serial.print("[MQTT] Connecting");
while (!mqtt.connected()) {
String cid = String("esp32-irrig-") + String((uint32_t)ESP.getEfuseMac(), HEX);
if (mqtt.connect(cid.c_str())) {
Serial.println("\n[MQTT] Connected");
mqtt.subscribe(TOPIC_CMD.c_str(), 1);
mqtt.subscribe(TOPIC_CFG.c_str(), 1);
Serial.println("[MQTT] Subscribed: " + TOPIC_CMD + " , " + TOPIC_CFG);
} else {
Serial.print(".");
delay(600);
}
}
mqtt.setBufferSize(512);
}
// ---------- HELPERS ----------
int readMoisturePct() {
// Pot 0..4095 -> 0..100 (invert so clockwise increases "moist")
int raw = analogRead(PIN_SOIL);
raw = constrain(raw, 0, 4095);
int pct = map(raw, 4095, 0, 0, 100);
return constrain(pct, 0, 100);
}
void readDHT(float &tC, float &rh) {
unsigned long now = millis();
if (now - lastDht >= 2100) {
TempAndHumidity th = dht.getTempAndHumidity();
if (!isnan(th.temperature)) tC_cache = th.temperature;
if (!isnan(th.humidity)) rh_cache = th.humidity;
lastDht = now;
}
tC = tC_cache; rh = rh_cache;
}
void simTankAndNPK(bool pump) {
// very simple: tank drains when pumping, leaks slowly when off
// NPK drift a bit; dilute when pumping
float dtm = SAMPLE_MS / 60000.0f; // minutes per step
if (pump && waterLevel > 0.5f) {
waterLevel -= 3.0f * dtm; // %/min
Nppm -= 0.05f * dtm; Pppm -= 0.03f * dtm; Kppm -= 0.04f * dtm;
} else {
waterLevel -= 0.05f * dtm; // slow leak
}
if (waterLevel < 0) waterLevel = 0; if (waterLevel > 100) waterLevel = 100;
// keep NPK reasonable
if (Nppm < 5) Nppm = 5; if (Nppm > 60) Nppm = 60;
if (Pppm < 3) Pppm = 3; if (Pppm > 40) Pppm = 40;
if (Kppm < 5) Kppm = 5; if (Kppm > 80) Kppm = 80;
}
void publishTelemetry(int moisture, float tC, float rh) {
StaticJsonDocument<256> doc;
doc["ts"] = ""; // server stamps actual time
doc["area_id"] = AREA_ID;
doc["moist"] = moisture;
doc["temp_c"] = tC;
doc["rh"] = rh;
doc["water_level"] = waterLevel;
doc["N"] = Nppm; doc["P"] = Pppm; doc["K"] = Kppm;
doc["last_pump"] = pumpOn ? 1 : 0;
char buf[256];
size_t n = serializeJson(doc, buf, sizeof(buf));
bool ok = mqtt.publish(TOPIC_TELE.c_str(),
reinterpret_cast<const uint8_t*>(buf),
n, false);
Serial.print("[PUB] "); Serial.print(TOPIC_TELE); Serial.print(" "); Serial.println(buf);
if (ok) Serial.println("[PUB->MQTT] sent");
}
// ---------- ARDUINO ----------
void setup() {
Serial.begin(115200);
pinMode(PIN_PUMP, OUTPUT);
digitalWrite(PIN_PUMP, LOW);
analogReadResolution(12);
dht.setup(PIN_DHT, DHTesp::DHT22);
TOPIC_TELE = String("chrab/") + AREA_ID + "/telemetry";
TOPIC_CMD = String("chrab/") + AREA_ID + "/cmd";
TOPIC_CFG = String("chrab/") + AREA_ID + "/cfg";
wifiConnect();
mqttConnect();
lastPub = millis();
lastDht = 0;
}
void loop() {
if (!mqtt.connected()) mqttConnect();
mqtt.loop();
float tC, rh;
readDHT(tC, rh);
unsigned long now = millis();
if (now - lastPub >= SAMPLE_MS) {
lastPub = now;
int moist = readMoisturePct();
// pump indicator mirrors last command from server
digitalWrite(PIN_PUMP, pumpOn ? HIGH : LOW);
simTankAndNPK(pumpOn);
publishTelemetry(moist, tC, rh);
}
}