/* =====================================================================
SmartGrainStorage.ino
---------------------------------------------------------------------
Smart Grain Storage Spoilage Prevention System - AI Enabled
Smart India Hackathon 2026 - Wokwi / ESP32 Reference Build
---------------------------------------------------------------------
Board : ESP32 DevKit V1
Simulator : Wokwi (also runs unmodified on real ESP32 hardware)
WIRING SUMMARY
---------------------------------------------------------------------
Indoor DHT22 DATA ......... GPIO 4
Outdoor DHT22 DATA ......... GPIO 2
DS18B20 OneWire bus (x10) .. GPIO 5
MQ135 gas sensor (analog) .. GPIO 34
Moisture sensor (analog) ... GPIO 35 (potentiometer in Wokwi)
IR insect sensor 1 (button). GPIO 12
IR insect sensor 2 (button). GPIO 13
OLED SSD1306 SDA ........... GPIO 21
OLED SSD1306 SCL ........... GPIO 22
RTC DS1307 SDA ........... GPIO 21 (shares the OLED I2C bus)
RTC DS1307 SCL ........... GPIO 22
Green LED .................. GPIO 27
Yellow LED ................. GPIO 14
Red LED .................... GPIO 26
Buzzer ..................... GPIO 25
Relay (drives exhaust fan) . GPIO 33
Fan control / status line .. GPIO 32 (mirrors relay state)
LIBRARIES REQUIRED (install via Arduino Library Manager)
---------------------------------------------------------------------
- WiFi.h (bundled with ESP32 board package)
- WebServer.h (bundled with ESP32 board package)
- Wire.h (bundled)
- RTClib by Adafruit
- DHT sensor library by Adafruit
- OneWire by Jim Studt / Paul Stoffregen
- DallasTemperature by Miles Burton
- Adafruit GFX Library by Adafruit
- Adafruit SSD1306 by Adafruit
NOTES
---------------------------------------------------------------------
- No GSM / no LoRa / no TensorFlow in this build. predictSpoilage()
is a rule-based scoring model, written so it can later be swapped
for a TensorFlow Lite Micro model without touching any other
function.
- All timing (LED blink, buzzer, OLED page rotation) uses millis(),
never delay(), so the web server and sensors stay responsive.
===================================================================== */
// ================================================================
// ====================== LIBRARIES ============================
// ================================================================
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <RTClib.h>
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ================================================================
// ====================== PIN DEFINITIONS =======================
// ================================================================
#define DHT_INDOOR_PIN 4
#define DHT_OUTDOOR_PIN 2
#define DHT_TYPE DHT22
#define ONE_WIRE_BUS 5
#define MQ135_PIN 34
#define MOISTURE_PIN 35
#define IR_BUTTON1_PIN 12
#define IR_BUTTON2_PIN 13
#define OLED_SDA_PIN 21
#define OLED_SCL_PIN 22
// RTC DS1307 shares the same I2C bus as the OLED (SDA 21 / SCL 22)
#define GREEN_LED_PIN 27
#define YELLOW_LED_PIN 14
#define RED_LED_PIN 26
#define BUZZER_PIN 25
#define RELAY_PIN 33
#define FAN_PIN 32
// ================================================================
// ====================== CONSTANTS =============================
// ================================================================
#define NUM_SACK_SENSORS 10
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_I2C_ADDR 0x3C
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASSWORD ""
#define WIFI_CONNECT_TIMEOUT 10000UL
// Non-blocking timing intervals (all millis()-based)
#define SENSOR_READ_INTERVAL_MS 2000UL
#define SERIAL_PRINT_INTERVAL_MS 5000UL
#define OLED_PAGE_INTERVAL_MS 4000UL
#define YELLOW_BLINK_INTERVAL_MS 500UL
#define RED_BLINK_INTERVAL_MS 200UL
#define WARNING_BEEP_INTERVAL_MS 1000UL
#define WARNING_BEEP_DURATION_MS 120UL
#define INSECT_WINDOW_MS 3600000UL // 1 hour window
#define DEBOUNCE_DELAY_MS 50UL
// Safe fallback values used whenever a sensor read fails
#define DEFAULT_INDOOR_TEMP 30.0
#define DEFAULT_OUTDOOR_TEMP 29.0
#define DEFAULT_HUMIDITY 60.0
#define DEFAULT_SACK_TEMP 30.0
#define DEFAULT_GAS_PPM 900.0
#define DEFAULT_MOISTURE 8.0
// ================================================================
// ====================== GLOBAL OBJECTS ========================
// ================================================================
DHT dhtIndoor(DHT_INDOOR_PIN, DHT_TYPE);
DHT dhtOutdoor(DHT_OUTDOOR_PIN, DHT_TYPE);
OneWire oneWireBus(ONE_WIRE_BUS);
DallasTemperature sackSensors(&oneWireBus);
RTC_DS1307 rtc;
bool rtcAvailable = false;
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
WebServer server(80);
// ================================================================
// ====================== SENSOR / STATE VARIABLES ===============
// ================================================================
float indoorTemp = DEFAULT_INDOOR_TEMP;
float indoorHum = DEFAULT_HUMIDITY;
float outdoorTemp = DEFAULT_OUTDOOR_TEMP;
float outdoorHum = DEFAULT_HUMIDITY;
float sackTemps[NUM_SACK_SENSORS];
float avgSackTemp = DEFAULT_SACK_TEMP;
float maxSackTemp = DEFAULT_SACK_TEMP;
int validSackSensorCount = 0;
int gasRawValue = 0;
float gasPPM = DEFAULT_GAS_PPM;
float previousGasPPM = DEFAULT_GAS_PPM;
int moistureRawValue = 0;
float moisturePercent = DEFAULT_MOISTURE;
float previousMoisture = DEFAULT_MOISTURE;
int insectCount = 0; // detections in the trailing 1-hour window (0-10)
float insectActivityScore = 0.0; // 0-100 recency + volume weighted score
// Derived features
float tempDifference = 0.0;
float humidityDifference = 0.0;
float heatStressIndex = 0.0;
float gasRate = 0.0;
float moistureRate = 0.0;
// AI prediction output
float spoilageRisk = 0.0;
String warehouseState = "SAFE";
// Sensor fault flags (used for OLED / Serial "Sensor Error" reporting)
bool indoorDhtFault = false;
bool outdoorDhtFault = false;
bool sackSensorFault = false;
bool rtcFault = false;
// ================================================================
// ====================== TIMING / OUTPUT STATE ==================
// ================================================================
unsigned long lastSensorReadTime = 0;
unsigned long lastSerialPrintTime = 0;
unsigned long lastOledPageChange = 0;
unsigned long lastYellowToggle = 0;
unsigned long lastRedToggle = 0;
unsigned long lastWarningBeepStart = 0;
bool yellowLedState = false;
bool redLedState = false;
bool warningBeepOn = false;
int oledPage = 1; // rotates 1..5 normally, 1..6 while CRITICAL
// Insect detection circular buffer - stores millis() timestamp of each
// beam-break event so we can score activity over the trailing hour.
#define MAX_INSECT_EVENTS 20
unsigned long insectEventTimes[MAX_INSECT_EVENTS];
int insectEventWriteIndex = 0;
bool insectEventBufferFull = false;
bool lastButton1Reading = HIGH;
bool lastButton2Reading = HIGH;
unsigned long lastButton1DebounceTime = 0;
unsigned long lastButton2DebounceTime = 0;
// ================================================================
// ====================== FUNCTION PROTOTYPES ====================
// ================================================================
void connectWiFi();
void readSensors();
void readDS18B20();
void readInsectButtons();
void recordInsectEvent();
void updateInsectMetrics();
void calculateDerivedValues();
void predictSpoilage();
void controlOutputs();
void updateOLED();
void drawPage1();
void drawPage2();
void drawPage3();
void drawPage4();
void drawPage5();
void showSMSAlert();
void webDashboard();
String buildCard(String label, String value);
void printSerialData();
String getTimeString();
float clampFloat(float value, float minVal, float maxVal);
float scorePoints(float value, float lowBound, float highBound, float maxPoints);
float mapFloat(float x, float inMin, float inMax, float outMin, float outMax);
// ================================================================
// ============================ SETUP =============================
// ================================================================
void setup() {
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println(F("================================================="));
Serial.println(F(" SMART GRAIN STORAGE SPOILAGE PREVENTION SYSTEM"));
Serial.println(F(" SIH 2026 - AI Enabled ESP32 Build"));
Serial.println(F("================================================="));
// -------- Output pins --------
pinMode(GREEN_LED_PIN, OUTPUT);
pinMode(YELLOW_LED_PIN, OUTPUT);
pinMode(RED_LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
pinMode(FAN_PIN, OUTPUT);
digitalWrite(GREEN_LED_PIN, LOW);
digitalWrite(YELLOW_LED_PIN, LOW);
digitalWrite(RED_LED_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(RELAY_PIN, LOW);
digitalWrite(FAN_PIN, LOW);
noTone(BUZZER_PIN);
// -------- Insect IR-simulation buttons --------
pinMode(IR_BUTTON1_PIN, INPUT_PULLUP);
pinMode(IR_BUTTON2_PIN, INPUT_PULLUP);
// -------- Shared I2C bus (OLED + RTC) --------
Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
// -------- DHT22 sensors --------
dhtIndoor.begin();
dhtOutdoor.begin();
// -------- DS18B20 sack temperature bus --------
sackSensors.begin();
Serial.print(F("DS18B20 sensors detected: "));
Serial.println(sackSensors.getDeviceCount());
for (int i = 0; i < NUM_SACK_SENSORS; i++) {
sackTemps[i] = DEFAULT_SACK_TEMP;
}
Serial.print("Sensors Found: ");
Serial.println(sackSensors.getDeviceCount());
if (sackSensors.getDeviceCount() == 0) {
Serial.println("❌ OneWire Bus Error!");
}
// -------- RTC DS1307 --------
if (!rtc.begin()) {
Serial.println(F("[WARNING] RTC DS1307 not found. Continuing without RTC."));
rtcAvailable = false;
rtcFault = true;
} else {
rtcAvailable = true;
if (!rtc.isrunning()) {
Serial.println(F("[WARNING] RTC is not running. Setting to compile time."));
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
// -------- OLED SSD1306 --------
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) {
Serial.println(F("[WARNING] SSD1306 OLED not found."));
} else {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("SMART GRAIN STORAGE"));
display.println(F("Initializing..."));
display.display();
}
// -------- WiFi + Web dashboard --------
connectWiFi();
server.on("/", webDashboard);
server.begin();
Serial.println(F("Web server started."));
// -------- Insect event buffer init --------
for (int i = 0; i < MAX_INSECT_EVENTS; i++) {
insectEventTimes[i] = 0;
}
Serial.println(F("Setup complete. Entering main loop.\n"));
}
// ================================================================
// ============================ MAIN LOOP ==========================
// ================================================================
void loop() {
unsigned long now = millis();
// Web dashboard requests are handled every iteration
server.handleClient();
// Insect buttons are polled every loop for responsive debouncing
readInsectButtons();
// Full sensor read + AI prediction cycle
if (now - lastSensorReadTime >= SENSOR_READ_INTERVAL_MS) {
lastSensorReadTime = now;
readSensors();
readDS18B20();
updateInsectMetrics();
calculateDerivedValues();
predictSpoilage();
}
// Outputs are re-applied every loop for smooth, non-blocking blink timing
controlOutputs();
// OLED page rotation (extra alert page inserted while CRITICAL)
int maxOledPage = (warehouseState == "CRITICAL") ? 6 : 5;
if (now - lastOledPageChange >= OLED_PAGE_INTERVAL_MS) {
lastOledPageChange = now;
oledPage++;
if (oledPage > maxOledPage) oledPage = 1;
}
if (oledPage > maxOledPage) oledPage = 1; // safety clamp on state change
updateOLED();
// Serial monitor status print
if (now - lastSerialPrintTime >= SERIAL_PRINT_INTERVAL_MS) {
lastSerialPrintTime = now;
printSerialData();
}
}
// ================================================================
// ============================ WIFI ===============================
// ================================================================
void connectWiFi() {
Serial.print(F("Connecting to WiFi: "));
Serial.println(WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long startAttempt = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttempt < WIFI_CONNECT_TIMEOUT) {
delay(300); // one-time blocking wait at boot only, not in loop()
Serial.print(F("."));
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print(F("WiFi connected. Dashboard IP: "));
Serial.println(WiFi.localIP());
} else {
Serial.println(F("[WARNING] WiFi not connected. Dashboard will be unavailable."));
}
}
// ================================================================
// ============================ SENSOR READING ======================
// ================================================================
// Reads both DHT22 sensors, the MQ135 gas sensor and the moisture
// sensor. Insect-button reading/counting is handled separately by
// readInsectButtons() (called every loop) and updateInsectMetrics()
// (called on the sensor cycle) so beam-break events are never missed
// between sensor reads.
void readSensors() {
// -------- Indoor DHT22 --------
float t1 = dhtIndoor.readTemperature();
float h1 = dhtIndoor.readHumidity();
if (isnan(t1) || isnan(h1)) {
indoorDhtFault = true;
indoorTemp = DEFAULT_INDOOR_TEMP;
indoorHum = DEFAULT_HUMIDITY;
Serial.println(F("[WARNING] Indoor DHT22 read failed. Using default values."));
} else {
indoorDhtFault = false;
indoorTemp = t1;
indoorHum = h1;
}
// -------- Outdoor DHT22 --------
float t2 = dhtOutdoor.readTemperature();
float h2 = dhtOutdoor.readHumidity();
if (isnan(t2) || isnan(h2)) {
outdoorDhtFault = true;
outdoorTemp = DEFAULT_OUTDOOR_TEMP;
outdoorHum = DEFAULT_HUMIDITY;
Serial.println(F("[WARNING] Outdoor DHT22 read failed. Using default values."));
} else {
outdoorDhtFault = false;
outdoorTemp = t2;
outdoorHum = h2;
}
// -------- MQ135 gas sensor --------
gasRawValue = analogRead(MQ135_PIN); // 0-4095, ESP32 12-bit ADC
previousGasPPM = gasPPM;
gasPPM = mapFloat((float)gasRawValue, 0.0, 4095.0, 300.0, 4000.0);
gasPPM = clampFloat(gasPPM, 300.0, 4000.0);
// -------- Capacitive moisture sensor (potentiometer in Wokwi) --------
moistureRawValue = analogRead(MOISTURE_PIN); // 0-4095
previousMoisture = moisturePercent;
moisturePercent = mapFloat((float)moistureRawValue, 0.0, 4095.0, 0.0, 20.0);
moisturePercent = clampFloat(moisturePercent, 0.0, 20.0);
}
// ================================================================
// ============================ DS18B20 SACK SENSORS ================
// ================================================================
void readDS18B20() {
// ==================== DS18B20 READ (WOKWI FIX) ====================
sackSensors.requestTemperatures();
float baseTemp = sackSensors.getTempCByIndex(0);
// If sensor is disconnected, use default
if (baseTemp == DEVICE_DISCONNECTED_C || baseTemp < -55 || baseTemp > 125) {
sackSensorFault = true;
baseTemp = DEFAULT_SACK_TEMP;
Serial.println("[WARNING] DS18B20 not responding. Using default temperature.");
} else {
sackSensorFault = false;
}
// Generate 10 grain sack temperatures
avgSackTemp = 0;
maxSackTemp = baseTemp;
for (int i = 0; i < NUM_SACK_SENSORS; i++) {
sackTemps[i] = baseTemp + (i % 5) * 0.5;
avgSackTemp += sackTemps[i];
if (sackTemps[i] > maxSackTemp)
maxSackTemp = sackTemps[i];
}
avgSackTemp /= NUM_SACK_SENSORS;
}
// ================================================================
// ============================ INSECT DETECTION (IR SIM) ===========
// ================================================================
// Two push buttons emulate a pair of IR break-beam sensors placed
// across the grain-bag stack. Each press (debounced falling edge)
// represents one insect crossing the beam.
void readInsectButtons() {
unsigned long now = millis();
bool reading1 = digitalRead(IR_BUTTON1_PIN);
if (reading1 != lastButton1Reading) {
lastButton1DebounceTime = now;
}
if ((now - lastButton1DebounceTime) > DEBOUNCE_DELAY_MS) {
if (reading1 == LOW && lastButton1Reading == HIGH) {
recordInsectEvent();
}
}
lastButton1Reading = reading1;
bool reading2 = digitalRead(IR_BUTTON2_PIN);
if (reading2 != lastButton2Reading) {
lastButton2DebounceTime = now;
}
if ((now - lastButton2DebounceTime) > DEBOUNCE_DELAY_MS) {
if (reading2 == LOW && lastButton2Reading == HIGH) {
recordInsectEvent();
}
}
lastButton2Reading = reading2;
}
// Stores the timestamp of one insect detection into the circular buffer.
void recordInsectEvent() {
insectEventTimes[insectEventWriteIndex] = millis();
insectEventWriteIndex++;
if (insectEventWriteIndex >= MAX_INSECT_EVENTS) {
insectEventWriteIndex = 0;
insectEventBufferFull = true;
}
}
// Recomputes insectCount (0-10, detections in the trailing hour) and
// insectActivityScore (0-100, blends how many events and how recent
// they were) from the circular event buffer.
void updateInsectMetrics() {
unsigned long now = millis();
int countInWindow = 0;
float recencyWeightSum = 0.0;
int entries = insectEventBufferFull ? MAX_INSECT_EVENTS : insectEventWriteIndex;
for (int i = 0; i < entries; i++) {
unsigned long t = insectEventTimes[i];
if (t == 0) continue;
unsigned long age = now - t;
if (age <= INSECT_WINDOW_MS) {
countInWindow++;
float weight = 1.0 - ((float)age / (float)INSECT_WINDOW_MS); // 1.0 = just happened
recencyWeightSum += weight;
}
}
insectCount = (countInWindow > 10) ? 10 : countInWindow;
float countComponent = ((float)insectCount / 10.0) * 70.0; // up to 70 pts
float avgRecencyWeight = (countInWindow > 0) ? (recencyWeightSum / countInWindow) : 0.0;
float recencyComponent = avgRecencyWeight * 30.0; // up to 30 pts
insectActivityScore = clampFloat(countComponent + recencyComponent, 0.0, 100.0);
}
// ================================================================
// ============================ DERIVED FEATURES =====================
// ================================================================
void calculateDerivedValues() {
tempDifference = avgSackTemp - outdoorTemp;
humidityDifference = indoorHum - outdoorHum;
// Heat Stress Index: a simple 0-100 linear model that rises with both
// indoor temperature and indoor humidity together - not a certified
// heat-index formula, tuned for Indian warehouse conditions.
heatStressIndex = 20.0 + (indoorTemp - 22.0) * 1.2 + (indoorHum - 35.0) * 0.35;
heatStressIndex = clampFloat(heatStressIndex, 0.0, 100.0);
gasRate = gasPPM - previousGasPPM;
moistureRate = moisturePercent - previousMoisture;
}
// ================================================================
// ============================ AI ENGINE ===========================
// ================================================================
// Lightweight rule-based scoring model for the Wokwi/ESP32 build.
// predictSpoilage() is the only function that would need to change to
// swap this for a TensorFlow Lite Micro model trained on real warehouse
// data later - every other function only reads spoilageRisk/warehouseState.
void predictSpoilage() {
spoilageRisk = 0;
// Temperature Score (30)
if (indoorTemp >= 38.0)
spoilageRisk += 30;
else if (indoorTemp >= 33.0)
spoilageRisk += 20;
else if (indoorTemp >= 30.0)
spoilageRisk += 10;
// Humidity Score (25)
if (indoorHum >= 80.0)
spoilageRisk += 25;
else if (indoorHum >= 65.0)
spoilageRisk += 15;
// Gas Score (25)
if (gasRawValue >= 3000)
spoilageRisk += 25;
else if (gasRawValue >= 1800)
spoilageRisk += 15;
else if (gasRawValue >= 1000)
spoilageRisk += 8;
// Moisture Score (10)
if (moisturePercent >= 16.0)
spoilageRisk += 10;
else if (moisturePercent >= 12.0)
spoilageRisk += 5;
// Insect Score (10)
if (insectCount >= 2)
spoilageRisk += 10;
else if (insectCount == 1)
spoilageRisk += 5;
// Final Decision
if (spoilageRisk >= 70)
warehouseState = "CRITICAL";
else if (spoilageRisk >= 40)
warehouseState = "WARNING";
else
warehouseState = "SAFE";
}
// Linearly scales 'value' between lowBound (-> 0 points) and highBound
// (-> maxPoints), clamped at both ends.
float scorePoints(float value, float lowBound, float highBound, float maxPoints) {
if (value <= lowBound) return 0.0;
if (value >= highBound) return maxPoints;
return ((value - lowBound) / (highBound - lowBound)) * maxPoints;
}
// ================================================================
// ============================ OUTPUT CONTROL =======================
// ================================================================
// Only one warehouse state is ever active at a time. Every call
// re-applies the full output pattern for the current state so no
// output can get stuck on from a previous state.
void controlOutputs() {
unsigned long now = millis();
if (warehouseState == "SAFE") {
digitalWrite(GREEN_LED_PIN, HIGH);
digitalWrite(YELLOW_LED_PIN, LOW);
digitalWrite(RED_LED_PIN, LOW);
digitalWrite(RELAY_PIN, LOW);
digitalWrite(FAN_PIN, LOW);
noTone(BUZZER_PIN);
yellowLedState = false;
redLedState = false;
warningBeepOn = false;
}
else if (warehouseState == "WARNING") {
digitalWrite(GREEN_LED_PIN, LOW);
digitalWrite(RED_LED_PIN, LOW);
digitalWrite(RELAY_PIN, LOW);
digitalWrite(FAN_PIN, LOW);
redLedState = false;
// Yellow LED blinks every 500 ms
if (now - lastYellowToggle >= YELLOW_BLINK_INTERVAL_MS) {
lastYellowToggle = now;
yellowLedState = !yellowLedState;
digitalWrite(YELLOW_LED_PIN, yellowLedState ? HIGH : LOW);
}
// Short buzzer beep once every second
if (!warningBeepOn && (now - lastWarningBeepStart >= WARNING_BEEP_INTERVAL_MS)) {
lastWarningBeepStart = now;
warningBeepOn = true;
tone(BUZZER_PIN, 1500);
}
if (warningBeepOn && (now - lastWarningBeepStart >= WARNING_BEEP_DURATION_MS)) {
noTone(BUZZER_PIN);
warningBeepOn = false;
}
}
else { // CRITICAL
digitalWrite(GREEN_LED_PIN, LOW);
digitalWrite(YELLOW_LED_PIN, LOW);
digitalWrite(RELAY_PIN, HIGH);
digitalWrite(FAN_PIN, HIGH);
yellowLedState = false;
// Red LED blinks fast (every 200 ms)
if (now - lastRedToggle >= RED_BLINK_INTERVAL_MS) {
lastRedToggle = now;
redLedState = !redLedState;
digitalWrite(RED_LED_PIN, redLedState ? HIGH : LOW);
}
// Continuous alert tone
tone(BUZZER_PIN, 2000);
}
}
// ================================================================
// ============================ OLED ================================
// ================================================================
void updateOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if (oledPage == 6) {
showSMSAlert();
} else {
switch (oledPage) {
case 1: drawPage1(); break;
case 2: drawPage2(); break;
case 3: drawPage3(); break;
case 4: drawPage4(); break;
case 5: drawPage5(); break;
default: drawPage1(); break;
}
}
display.display();
}
void drawPage1() {
display.setCursor(0, 0);
display.println(F("SMART GRAIN STORAGE"));
if (indoorDhtFault || outdoorDhtFault || sackSensorFault || rtcFault) {
display.println(F("** Sensor Error **"));
} else {
display.println();
}
display.print(F("Indoor Temp : "));
display.print(indoorTemp, 1);
display.println(F(" C"));
display.print(F("Indoor Hum : "));
display.print(indoorHum, 1);
display.println(F(" %"));
}
void drawPage2() {
display.setCursor(0, 0);
display.println(F("OUTDOOR CONDITIONS"));
display.println();
display.print(F("Outdoor Temp: "));
display.print(outdoorTemp, 1);
display.println(F(" C"));
display.print(F("Outdoor Hum : "));
display.print(outdoorHum, 1);
display.println(F(" %"));
}
void drawPage3() {
display.setCursor(0, 0);
display.println(F("SACK TEMPERATURE"));
display.println();
display.print(F("Average Sack: "));
display.print(avgSackTemp, 1);
display.println(F(" C"));
display.print(F("Maximum Sack: "));
display.print(maxSackTemp, 1);
display.println(F(" C"));
}
void drawPage4() {
display.setCursor(0, 0);
display.println(F("GAS / MOISTURE"));
display.println();
display.print(F("Gas (MQ135) : "));
display.println((int)gasPPM);
display.print(F("Moisture : "));
display.print(moisturePercent, 1);
display.println(F(" %"));
display.print(F("Insects : "));
display.println(insectCount);
}
void drawPage5() {
display.setCursor(0, 0);
display.println(F("AI SPOILAGE RISK"));
display.println();
display.print(F("Risk Score : "));
display.print(spoilageRisk, 1);
display.println(F(" %"));
display.print(F("State : "));
display.println(warehouseState);
}
// Dedicated full-screen alert page shown (in rotation) while CRITICAL.
void showSMSAlert() {
display.setCursor(0, 0);
display.println(F("*** SMS ALERT ***"));
display.println();
display.println(F("Warehouse Spoilage"));
display.println(F("Risk Detected!"));
display.println();
display.println(F("Fan Started"));
display.println(F("Check Warehouse"));
display.println(F("Immediately!"));
}
// ================================================================
// ============================ SERIAL MONITOR =======================
// ================================================================
void printSerialData() {
Serial.println(F("-------------------------------------------------"));
Serial.print(F("Time : "));
Serial.println(getTimeString());
Serial.print(F("Indoor Temp : "));
Serial.print(indoorTemp, 1);
Serial.println(F(" C"));
Serial.print(F("Outdoor Temp : "));
Serial.print(outdoorTemp, 1);
Serial.println(F(" C"));
Serial.print(F("Indoor Humidity : "));
Serial.print(indoorHum, 1);
Serial.println(F(" %"));
Serial.print(F("Outdoor Humidity : "));
Serial.print(outdoorHum, 1);
Serial.println(F(" %"));
Serial.print(F("Avg Sack Temp : "));
Serial.print(avgSackTemp, 1);
Serial.println(F(" C"));
Serial.print(F("Max Sack Temp : "));
Serial.print(maxSackTemp, 1);
Serial.println(F(" C"));
Serial.print(F("Gas (MQ135) : "));
Serial.println((int)gasPPM);
Serial.print(F("Moisture : "));
Serial.print(moisturePercent, 1);
Serial.println(F(" %"));
Serial.print(F("Insect Count : "));
Serial.println(insectCount);
Serial.print(F("Heat Stress Index: "));
Serial.println(heatStressIndex, 1);
Serial.print(F("Spoilage Risk : "));
Serial.print(spoilageRisk, 1);
Serial.println(F(" %"));
Serial.print(F("Warehouse State : "));
Serial.println(warehouseState);
if (indoorDhtFault || outdoorDhtFault || sackSensorFault || rtcFault) {
Serial.println(F("[SENSOR ERROR] One or more sensors are using fallback default values."));
}
Serial.println(F("-------------------------------------------------\n"));
}
// Returns "HH:MM:SS" from the RTC, or a placeholder if the RTC is unavailable.
String getTimeString() {
if (!rtcAvailable) {
return String("--:--:--");
}
DateTime now = rtc.now();
char buf[9];
sprintf(buf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
return String(buf);
}
// ================================================================
// ============================ WEB SERVER ===========================
// ================================================================
// Serves the mobile-friendly dashboard at "/". Uses a meta refresh tag
// so the page reloads (and re-reads the latest globals) every 3 seconds
// with no external JS/CSS dependency, per the "inline HTML/CSS only"
// requirement.
void webDashboard() {
String stateColor = "#2ecc71"; // green = SAFE
if (warehouseState == "WARNING") stateColor = "#f1c40f";
if (warehouseState == "CRITICAL") stateColor = "#e74c3c";
String fanStatus = (warehouseState == "CRITICAL") ? "ON" : "OFF";
String html = "<!DOCTYPE html><html><head>";
html += "<meta charset='UTF-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<meta http-equiv='refresh' content='3'>";
html += "<title>Smart Grain Storage Dashboard</title>";
html += "<style>";
html += "body{font-family:Arial,Helvetica,sans-serif;background:#f4f6f7;margin:0;padding:16px;}";
html += "h1{font-size:20px;text-align:center;color:#2c3e50;}";
html += ".grid{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;}";
html += ".card{background:#ffffff;border-radius:10px;box-shadow:0 2px 6px rgba(0,0,0,0.15);";
html += "padding:14px;width:150px;text-align:center;}";
html += ".card h2{font-size:12px;color:#7f8c8d;margin:0 0 6px 0;text-transform:uppercase;}";
html += ".card p{font-size:20px;margin:0;color:#2c3e50;font-weight:bold;}";
html += ".status{margin:16px auto;max-width:320px;text-align:center;color:#ffffff;";
html += "padding:16px;border-radius:10px;font-size:22px;font-weight:bold;}";
html += "</style></head><body>";
html += "<h1>SMART GRAIN STORAGE DASHBOARD</h1>";
html += "<div class='status' style='background:" + stateColor + ";'>";
html += warehouseState + " (" + String(spoilageRisk, 1) + "% risk)";
html += "</div>";
html += "<div class='grid'>";
html += buildCard("Indoor Temp", String(indoorTemp, 1) + " C");
html += buildCard("Outdoor Temp", String(outdoorTemp, 1) + " C");
html += buildCard("Humidity", String(indoorHum, 1) + " %");
html += buildCard("Avg Sack Temp", String(avgSackTemp, 1) + " C");
html += buildCard("Gas (MQ135)", String((int)gasPPM));
html += buildCard("Moisture", String(moisturePercent, 1) + " %");
html += buildCard("Insect Count", String(insectCount));
html += buildCard("AI Spoilage Risk", String(spoilageRisk, 1) + " %");
html += buildCard("Fan Status", fanStatus);
html += "</div>";
html += "</body></html>";
server.send(200, "text/html", html);
}
// Builds one dashboard status card as an HTML snippet.
String buildCard(String label, String value) {
String card = "<div class='card'><h2>" + label + "</h2><p>" + value + "</p></div>";
return card;
}
// ================================================================
// ============================ HELPERS =============================
// ================================================================
float clampFloat(float value, float minVal, float maxVal) {
if (value < minVal) return minVal;
if (value > maxVal) return maxVal;
return value;
}
float mapFloat(float x, float inMin, float inMax, float outMin, float outMax) {
return (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
Loading
ssd1306
ssd1306