/*
============================================================
WR SUMMER BERRY - ESP32 SMART FARM (v2 - IMPROVED)
Strawberry Environmental Control System
============================================================
WHAT CHANGED FROM v1
------------------------------------------------------------
- JSON now built with ArduinoJson (safer than string concat)
- Thresholds (soil/temp/humidity/light) are now adjustable
from the web page via /thresholds (GET + POST) and are
saved to flash (Preferences/NVS), so they survive reboots
- Auto mode state is also persisted
- mDNS added -> reachable at http://wrsummerberry.local
- Non-blocking critical alarm buzzer (old beep() used delay()
which freezes the whole loop - only safe at startup)
- Web dashboard rewritten: live connection indicator,
"last updated Xs ago", per-sensor threshold hints,
toggle-switch style controls, settings panel, toast
notifications, and defensive fetch() error handling
REQUIRED LIBRARIES (Library Manager)
------------------------------------------------------------
- DHT sensor library (Adafruit)
- BH1750 (claws)
- Adafruit GFX Library
- Adafruit SSD1306
- ArduinoJson (Benoit Blanchon) <-- NEW
Built-in (ESP32 core): WiFi, WebServer, HTTPClient,
WiFiClientSecure, Wire, Preferences, ESPmDNS
Sensors
------------------------------------------------------------
Soil Moisture -> GPIO 32
DHT22 -> GPIO 35
BH1750 SDA -> GPIO 23
BH1750 SCL -> GPIO 22
OLED I2C
------------------------------------------------------------
OLED SDA -> GPIO 21
OLED SCL -> GPIO 19
Relays
------------------------------------------------------------
Water Pump -> GPIO 33
Grow Light -> GPIO 25
Cooling -> GPIO 26
Fan -> GPIO 27
Mist Maker -> GPIO 14
Buzzer
------------------------------------------------------------
IMPORTANT:
GPIO34 is INPUT ONLY on ESP32.
Therefore buzzer cannot be connected to GPIO34.
This program uses GPIO13 for buzzer.
============================================================
*/
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Wire.h>
#include <DHT.h>
#include <BH1750.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <ESPmDNS.h>
// ============================================================
// WIFI
// ============================================================
const char* AP_SSID = "WR SUMMER BERRY2";
const char* AP_PASSWORD = "12345678";
const char* MDNS_NAME = "wrsummerberry"; // -> http://wrsummerberry.local
// ============================================================
// PIN CONFIGURATION
// ============================================================
#define SOIL_PIN 32
#define DHT_PIN 35
// Buzzer
// GPIO34 cannot OUTPUT -> use GPIO13 instead
#define BUZZER_PIN 13
// Relays
#define PUMP_RELAY 33
#define LIGHT_RELAY 25
#define COOL_RELAY 26
#define FAN_RELAY 27
#define MIST_RELAY 14
// BH1750
#define LIGHT_SDA 23
#define LIGHT_SCL 22
// OLED
#define OLED_SDA 21
#define OLED_SCL 19
// ============================================================
// SENSOR CONFIG
// ============================================================
#define DHTTYPE DHT22
DHT dht(DHT_PIN, DHTTYPE);
BH1750 lightMeter;
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&Wire,
-1
);
// ============================================================
// WEB SERVER / STORAGE
// ============================================================
WebServer server(80);
Preferences prefs;
// ============================================================
// DISCORD WEBHOOK
// ============================================================
// ใส่ Discord Webhook ของคุณตรงนี้
// ตัวอย่าง:
// https://discord.com/api/webhooks/XXXXXXXX/XXXXXXXX
const char* DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1544386889778208788/eAlhDHGcbjvX-a3zBuPqFrd1NrG-7t0Xbqx9l3DW4GgluGaxyjtS2f2D_4bruRzE-GpR";
// ============================================================
// SENSOR VALUES
// ============================================================
float temperature = 0;
float humidity = 0;
float lightLux = 0;
int soilRaw = 0;
int soilPercent = 0;
bool dhtOk = false; // last DHT read validity, shown on dashboard
// ============================================================
// AUTO MODE
// ============================================================
bool autoMode = true;
bool pumpState = false;
bool lightState = false;
bool coolState = false;
bool fanState = false;
bool mistState = false;
// ============================================================
// THRESHOLDS (now adjustable + persisted)
// ============================================================
int SOIL_DRY = 30; // %
float TEMP_HIGH = 30.0; // C
float HUMIDITY_LOW = 60.0; // %
float LIGHT_LOW = 500.0;// lux
// ============================================================
// CRITICAL ALARM (non-blocking)
// ============================================================
const int SOIL_CRITICAL = 10; // % - below this we sound an alarm
bool alarmActive = false;
bool buzzerToggle = false;
unsigned long lastBuzzerToggle = 0;
const unsigned long BUZZER_INTERVAL = 400; // ms on/off period
// ============================================================
// TIMERS
// ============================================================
unsigned long lastSensorRead = 0;
unsigned long lastDiscord = 0;
unsigned long lastDisplay = 0;
const unsigned long SENSOR_INTERVAL = 2000;
const unsigned long DISCORD_INTERVAL = 60000;
const unsigned long DISPLAY_INTERVAL = 1000;
// ============================================================
// RELAY LOGIC
// ============================================================
// Most relay modules are ACTIVE LOW.
// If your relay works opposite, change this.
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// ============================================================
// SET RELAY
// ============================================================
void setRelay(int pin, bool state)
{
digitalWrite(pin, state ? RELAY_ON : RELAY_OFF);
}
// ============================================================
// BUZZER (startup beep - fine to block here, only runs once)
// ============================================================
void beep(int duration = 200)
{
digitalWrite(BUZZER_PIN, HIGH);
delay(duration);
digitalWrite(BUZZER_PIN, LOW);
}
// Non-blocking alarm updater - call every loop(), never delay()s
void updateAlarm()
{
bool shouldAlarm = (soilPercent > 0) && (soilPercent < SOIL_CRITICAL);
// soilPercent > 0 guards against an unconnected/uncalibrated sensor
// reading a false "0%" and beeping forever on boot.
alarmActive = shouldAlarm;
if (!alarmActive)
{
digitalWrite(BUZZER_PIN, LOW);
buzzerToggle = false;
return;
}
unsigned long now = millis();
if (now - lastBuzzerToggle >= BUZZER_INTERVAL)
{
lastBuzzerToggle = now;
buzzerToggle = !buzzerToggle;
digitalWrite(BUZZER_PIN, buzzerToggle ? HIGH : LOW);
}
}
// ============================================================
// PREFERENCES (persist thresholds + auto mode across reboot)
// ============================================================
void loadSettings()
{
prefs.begin("wrsb", true); // read-only
SOIL_DRY = prefs.getInt("soilDry", SOIL_DRY);
TEMP_HIGH = prefs.getFloat("tempHigh", TEMP_HIGH);
HUMIDITY_LOW = prefs.getFloat("humLow", HUMIDITY_LOW);
LIGHT_LOW = prefs.getFloat("lightLow", LIGHT_LOW);
autoMode = prefs.getBool("autoMode", autoMode);
prefs.end();
}
void saveThresholds()
{
prefs.begin("wrsb", false);
prefs.putInt("soilDry", SOIL_DRY);
prefs.putFloat("tempHigh", TEMP_HIGH);
prefs.putFloat("humLow", HUMIDITY_LOW);
prefs.putFloat("lightLow", LIGHT_LOW);
prefs.end();
}
void saveAutoMode()
{
prefs.begin("wrsb", false);
prefs.putBool("autoMode", autoMode);
prefs.end();
}
// ============================================================
// READ SENSORS
// ============================================================
void readSensors()
{
// Soil
soilRaw = analogRead(SOIL_PIN);
/*
ESP32 ADC calibration:
The raw values below (4095 dry / 1200 wet) are typical
defaults for a capacitive soil sensor but WILL vary by
sensor and supply voltage. To calibrate:
1. Read soilRaw in open air (fully dry) -> use as "dry" bound
2. Read soilRaw fully submerged in water/wet soil -> use as "wet" bound
3. Replace the 4095 / 1200 constants below with those values
*/
soilPercent = map(
soilRaw,
4095,
1200,
0,
100
);
soilPercent = constrain(
soilPercent,
0,
100
);
// DHT22
float newHumidity = dht.readHumidity();
float newTemperature = dht.readTemperature();
dhtOk = !isnan(newHumidity) && !isnan(newTemperature);
if (!isnan(newHumidity))
humidity = newHumidity;
if (!isnan(newTemperature))
temperature = newTemperature;
// BH1750
float newLight = lightMeter.readLightLevel();
if (newLight >= 0)
lightLux = newLight;
Serial.println("================================");
Serial.println("WR SUMMER BERRY SENSOR");
Serial.println("================================");
Serial.print("Soil Raw: "); Serial.println(soilRaw);
Serial.print("Soil: "); Serial.print(soilPercent); Serial.println("%");
Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" C");
Serial.print("Humidity: "); Serial.print(humidity); Serial.println(" %");
Serial.print("Light: "); Serial.print(lightLux); Serial.println(" lux");
Serial.println();
}
// ============================================================
// AUTOMATIC CONTROL
// ============================================================
void automaticControl()
{
if (!autoMode)
return;
pumpState = (soilPercent < SOIL_DRY);
lightState = (lightLux < LIGHT_LOW);
bool overTemp = (temperature > TEMP_HIGH);
coolState = overTemp;
fanState = overTemp;
mistState = (humidity < HUMIDITY_LOW);
setRelay(PUMP_RELAY, pumpState);
setRelay(LIGHT_RELAY, lightState);
setRelay(COOL_RELAY, coolState);
setRelay(FAN_RELAY, fanState);
setRelay(MIST_RELAY, mistState);
}
// ============================================================
// DISCORD
// ============================================================
void sendDiscord(String message)
{
if (
strlen(DISCORD_WEBHOOK) < 20 ||
String(DISCORD_WEBHOOK) == "YOUR_DISCORD_WEBHOOK_HERE"
)
{
Serial.println("Discord webhook not configured.");
return;
}
WiFiClientSecure client;
client.setInsecure();
HTTPClient https;
if (!https.begin(client, DISCORD_WEBHOOK))
{
Serial.println("Discord connection failed.");
return;
}
https.addHeader("Content-Type", "application/json");
StaticJsonDocument<512> doc;
doc["content"] = message;
String json;
serializeJson(doc, json);
int httpCode = https.POST(json);
Serial.print("Discord HTTP: ");
Serial.println(httpCode);
https.end();
}
// ============================================================
// DISCORD STATUS
// ============================================================
void sendStatusToDiscord()
{
String message;
message += "\xF0\x9F\x8D\x93 **WR SUMMER BERRY STATUS**\n\n";
message += "Soil: " + String(soilPercent) + "%\n";
message += "Temperature: " + String(temperature, 1) + " C\n";
message += "Humidity: " + String(humidity, 1) + " %\n";
message += "Light: " + String(lightLux, 0) + " lux\n\n";
message += "AUTO MODE: ";
message += autoMode ? "ON" : "OFF";
if (alarmActive)
{
message += "\n\n WARNING: Soil critically dry (<" + String(SOIL_CRITICAL) + "%)!";
}
sendDiscord(message);
}
// ============================================================
// OLED DISPLAY
// ============================================================
void updateDisplay()
{
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("WR SUMMER BERRY");
display.setCursor(0, 12);
display.print("TEMP: "); display.print(temperature, 1); display.println(" C");
display.setCursor(0, 22);
display.print("HUM : "); display.print(humidity, 0); display.println(" %");
display.setCursor(0, 32);
display.print("SOIL: "); display.print(soilPercent); display.println(" %");
display.setCursor(0, 42);
display.print("LUX : "); display.print(lightLux, 0);
display.setCursor(0, 54);
display.print("AUTO: "); display.print(autoMode ? "ON" : "OFF");
if (alarmActive)
{
display.setCursor(70, 42);
display.print("!DRY");
}
display.display();
}
// ============================================================
// WEB PAGE
// ============================================================
String createWebPage()
{
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WR SUMMER BERRY</title>
<style>
*{box-sizing:border-box;}
:root{
--pink:#ff7eb3;
--blue:#65c7f7;
--green:#18b875;
--red:#e0537a;
--text:#263238;
--muted:#8a94a6;
}
body{
margin:0;
font-family:'Segoe UI',Arial,Helvetica,sans-serif;
background:linear-gradient(135deg,#ffe5f2,#e3f5ff);
color:var(--text);
padding-bottom:30px;
}
.header{
padding:22px 25px;
background:linear-gradient(135deg,var(--pink),var(--blue));
color:white;
border-radius:0 0 28px 28px;
box-shadow:0 10px 30px rgba(0,0,0,.12);
position:sticky; top:0; z-index:10;
}
.header-row{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px;}
.logo{font-size:24px;font-weight:bold;}
.subtitle{opacity:.9;margin-top:4px;font-size:13px;}
.conn{
display:flex;align-items:center;gap:8px;
background:rgba(255,255,255,.18);
padding:8px 14px;border-radius:20px;font-size:13px;font-weight:bold;
}
.dot{width:9px;height:9px;border-radius:50%;background:#9be8c4;transition:.2s;}
.dot.offline{background:#ffb3b3;}
.updated{font-size:11px;opacity:.85;font-weight:normal;margin-top:2px;}
.container{max-width:1100px;margin:auto;padding:20px;}
.mode-banner{
background:white;padding:14px 20px;border-radius:18px;margin-bottom:18px;
display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;
box-shadow:0 8px 25px rgba(0,0,0,.08);
}
.mode-banner span.label{font-size:13px;color:var(--muted);}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;}
.card{
background:rgba(255,255,255,.92);padding:20px;border-radius:22px;
box-shadow:0 10px 30px rgba(74,144,226,.10);transition:.2s;
}
.card:hover{transform:translateY(-3px);}
.icon{font-size:30px;}
.title{color:var(--muted);margin-top:6px;font-size:14px;}
.value{font-size:30px;font-weight:bold;margin-top:4px;}
.unit{font-size:14px;color:var(--muted);}
.hint{margin-top:8px;font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px;}
.hint .tag{
padding:2px 8px;border-radius:10px;font-size:11px;font-weight:bold;
}
.tag.ok{background:#d9fbe9;color:var(--green);}
.tag.warn{background:#ffe0e8;color:var(--red);}
.section{margin-top:26px;}
.section h2{margin-bottom:12px;font-size:18px;}
.controls{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px;}
.control{
background:white;padding:18px;border-radius:20px;text-align:center;
box-shadow:0 8px 20px rgba(0,0,0,.08);
opacity:1;transition:.2s;
}
.control.disabled{opacity:.55;}
.control .name{font-weight:bold;margin-bottom:8px;}
.switch{position:relative;display:inline-block;width:52px;height:28px;}
.switch input{opacity:0;width:0;height:0;}
.slider{
position:absolute;cursor:pointer;inset:0;background:#e2e8f0;
border-radius:28px;transition:.2s;
}
.slider:before{
position:absolute;content:"";height:22px;width:22px;left:3px;bottom:3px;
background:white;border-radius:50%;transition:.2s;box-shadow:0 2px 4px rgba(0,0,0,.2);
}
input:checked + .slider{background:var(--pink);}
input:checked + .slider:before{transform:translateX(24px);}
input:disabled + .slider{cursor:not-allowed;}
.badge{display:inline-block;padding:6px 12px;border-radius:20px;font-size:12px;font-weight:bold;margin-top:8px;}
.active{background:#d9fbe9;color:var(--green);}
.inactive{background:#ffe0e8;color:var(--red);}
.auto-toggle-row{display:flex;align-items:center;gap:12px;}
.settings{background:white;border-radius:20px;padding:18px 20px;box-shadow:0 8px 20px rgba(0,0,0,.08);}
.settings-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin:14px 0;}
.field label{display:block;font-size:12px;color:var(--muted);margin-bottom:5px;}
.field input{
width:100%;padding:9px 10px;border-radius:10px;border:1px solid #e2e8f0;font-size:14px;
}
.save-btn{
background:var(--blue);color:white;border:0;padding:11px 24px;border-radius:24px;
font-weight:bold;cursor:pointer;
}
.save-btn:active{transform:scale(.97);}
.alarm-banner{
background:#fff0f0;border:1px solid #ffb3b3;color:#c0304f;
padding:12px 18px;border-radius:16px;margin-bottom:16px;font-weight:bold;
display:none;align-items:center;gap:8px;
}
.alarm-banner.show{display:flex;}
.footer{text-align:center;margin:30px 10px 10px;color:var(--muted);font-size:12px;}
.toast-wrap{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:50;display:flex;flex-direction:column;gap:8px;align-items:center;}
.toast{
background:#263238;color:white;padding:10px 18px;border-radius:20px;font-size:13px;
box-shadow:0 8px 20px rgba(0,0,0,.25);opacity:0;transform:translateY(10px);
transition:.25s;
}
.toast.show{opacity:1;transform:translateY(0);}
.toast.err{background:#c0304f;}
</style>
</head>
<body>
<div class="header">
<div class="header-row">
<div>
<div class="logo">🍓 WR SUMMER BERRY</div>
<div class="subtitle">Smart Strawberry Environmental Control System</div>
</div>
<div>
<div class="conn">
<span class="dot" id="connDot"></span>
<span id="connText">Connecting...</span>
</div>
<div class="updated" id="lastUpdated"></div>
</div>
</div>
</div>
<div class="container">
<div class="alarm-banner" id="alarmBanner">
⚠ Soil moisture critically low - please check the plants
</div>
<div class="mode-banner">
<div>
<div><b>ESP32 SMART FARM</b></div>
<span class="label">Manual control automatically switches the system to MANUAL mode.</span>
</div>
<div id="autoStatus" class="badge active">AUTO ON</div>
</div>
<div class="cards">
<div class="card">
<div class="icon">🌱</div>
<div class="title">Soil Moisture</div>
<div class="value"><span id="soil">--</span><span class="unit">%</span></div>
<div class="hint">Waters below <span id="soilTh">--</span>% <span class="tag" id="soilTag">--</span></div>
</div>
<div class="card">
<div class="icon">🌡️</div>
<div class="title">Temperature</div>
<div class="value"><span id="temp">--</span><span class="unit">°C</span></div>
<div class="hint">Cools above <span id="tempTh">--</span>°C <span class="tag" id="tempTag">--</span></div>
</div>
<div class="card">
<div class="icon">💧</div>
<div class="title">Humidity</div>
<div class="value"><span id="hum">--</span><span class="unit">%</span></div>
<div class="hint">Mists below <span id="humTh">--</span>% <span class="tag" id="humTag">--</span></div>
</div>
<div class="card">