Community project
Pressure-Tendency Rain Forecaster
Priyanshu Probol Sarma
Published August 6, 2026 · Updated August 11, 2026
Generated with AIThis project builds a weather station that predicts rain by analyzing barometric pressure trends, humidity, dew point, and direct rain detection. The ESP32 reads data from a BME280 pressure and temperature sensor, DHT11 humidity sensor, and rain sensor module, displaying real-time conditions on an SSD1306 OLED screen. When rain risk is detected, the device triggers a local buzzer alarm and sends SMS alerts to multiple phone numbers via the SIM800L GSM module.
The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for connecting the I²C weather sensors, configuring the GSM module with proper voltage regulation, and deploying the forecaster in the field. Firmware is included with edge-AI classification logic that learns pressure patterns over time and issues warnings before rain arrives, making it suitable for remote monitoring, agricultural alerts, or outdoor event planning.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| BME280BME280 | 1 | Bosch BME280 environmental sensor on the exact Adafruit 2652 breakout. Supports I2C via SCK/SCL and SDI/SDA, optional SDO address select, and SPI pins when needed. |
| Rain Sensor ModuleLM393 module | 1 | A rain sensor module consisting of a sensing pad with exposed copper traces and an LM393 comparator circuit. When water bridges the copper traces on the sensing pad, resistance decreases and conductivity increases. The module provides both an analog output (AO) proportional to water level and a digital output (DO) that goes LOW when water exceeds a threshold set by an onboard potentiometer. Includes power and status LEDs. Designed for detecting rain, slush, or hail in weather-responsive automation projects. |
| SSD1306 OLED128x64 I2C | 1 | 0.96 inch 128x64 OLED display with I2C interface |
| DHT11 Digital Temperature and Humidity SensorDHT11 | 1 | Single-wire digital sensor used as the SURAL prototype's additional ambient temperature and relative-humidity measurement source. The BME280 remains the barometric-pressure source. |
| SIM800L GSM/GPRS ModuleSIM800L 2G GSM modem | 1 | Compact GSM/GPRS module based on SIMCom SIM800L chip. Features a built-in SIM card slot and antenna connector. Communicates via UART using AT commands. Operates at 3.4V–4.4V supply (must NOT be powered from 3.3V LDO; use a boost converter such as MT3608). Peak current draw up to 2A during GSM TX bursts — requires external dedicated power supply with bulk decoupling capacitor (100–470uF). UART logic is nominally 2.8V; when driven by ESP32 (3.3V), a resistor voltage divider on the ESP32 TX → SIM800L RX line is required to shift 3.3V down to ~2.8V. Built-in SIM card slot and antenna port are physical features of the module PCB. |
| USB-C 5V Adapter5 V, 3 A minimum | 1 | USB-C wall adapter delivering regulated 5 V to the board's USB or VBUS rail. Default wired power source for desktop / stationary projects. |
| Adjustable 4.0 V 3 A Buck Regulator4.0 V, 3 A minimum | 1 | Dedicated adjustable buck regulator set to 4.0 V for SIM800L. It must sustain 2 A GSM transmit bursts; fit 470–1000 uF low-ESR electrolytic capacitor directly across SIM800L VCC and GND. |
| Low-ESR Electrolytic Capacitor1000 µF, ≥6.3 V low-ESR | 1 | Bulk capacitor placed directly across SIM800L VCC and GND to support GSM transmit current bursts and prevent brownouts. |
| Series Resistor for SIM800L RX1 kΩ | 1 | Upper resistor of a UART level-reduction divider between ESP32 TX and SIM800L RX. |
| UART Level Divider Resistor4.7 kΩ | 1 | Lower resistor of the ESP32-to-SIM800L UART divider. Together with 1 kΩ upper resistor, reduces ESP32 3.3 V TX logic to approximately 2.7 V at SIM800L RX. |
| Buzzer3.3 V active piezo buzzer module | 1 | Piezo buzzer for sound output |
Assembly
5 stepsPrepare the low-voltage electronics
Keep the ESP32, BME280, DHT11, OLED, rain-sensor control board, SIM800L, and buzzer inside a weather-protected enclosure. Leave only the rain plate and the sensor vents exposed. Use the existing 3.3 V and GND rails for the BME280, DHT11, OLED, rain sensor, and a 3.3 V-compatible active buzzer module.
- Tip: Place the BME280 and DHT11 in a shaded, ventilated radiation shield so sunlight and rain do not distort their readings.
- Tip: Keep the BME280 away from the ESP32 and SIM800L heat sources.
- ⚠ Do not power 3.3 V sensors or the buzzer from the dedicated 4.0 V SIM800L rail.
Connect the shared I²C weather display bus
Connect BME280 SDA and OLED SDA to ESP32 GPIO 21. Connect BME280 SCL and OLED SCL to ESP32 GPIO 22. Connect each module ground to ESP32 GND and each module VCC to 3.3 V.
- Tip: The BME280 address can be 0x76 or 0x77; the firmware automatically tries both.
- Tip: The OLED is a local status display; fit it behind a clear, shaded enclosure window.
- ⚠ Never connect I²C signals to 5 V.
Connect local weather inputs and audible warning
Connect DHT11 DATA to GPIO 4, rain-module DO to GPIO 27, and the active buzzer SIGNAL input to GPIO 25. Connect their grounds to ESP32 GND. Use a 3.3 V-compatible active buzzer module; if using a bare DHT11, fit a 4.7–10 kΩ pull-up resistor between its DATA pin and 3.3 V.
- Tip: Adjust the rain-module trimmer so its digital output changes only when the remote rain plate is wet.
- Tip: The buzzer gives a repeating fast warning pattern for WARNING and a slower pattern when rain is confirmed.
- ⚠ If the buzzer draws more current than an ESP32 GPIO can supply, drive it through an NPN/MOSFET transistor rather than directly from GPIO 25.
Wire and power the SIM800L safely
Power SIM800L only from the separate 4.0 V buck regulator. Keep the 1000 µF low-ESR capacitor directly across SIM800L VCC and GND. Connect SIM800L TX through a proper 3.3 V-compatible level shifter to ESP32 GPIO 16. Connect GPIO 17 through the existing resistor divider to SIM800L RX. Join all grounds.
- Tip: Fit an activated SIM card with SMS service and antenna before powering the modem.
- Tip: The firmware scans and registers on the mobile network before it attempts SMS delivery to both configured recipients.
- ⚠ SIM800L can draw short current bursts near 2 A. Do not power it from ESP32 3.3 V or a weak USB lead.
- ⚠ Use a bidirectional logic-level shifter for the SIM800L TX-to-ESP32 RX direction if the module TX is not confirmed 3.3 V-safe.
Install in the field and verify local warning access
Mount the rain plate outdoors at an angle, route its cable into the enclosure, and mount temperature/pressure sensors in shade with free airflow. Power the ESP32 from its USB supply. Configure the Wi-Fi name and password in the firmware before deploying; if Wi-Fi is unavailable, connect a phone to the SURAL-Weather access point to view the dashboard. The local display and buzzer remain functional without Wi-Fi or GSM coverage.
- Tip: For validation, preserve the serial CSV output and record whether rain begins within two hours of each WARNING.
- Tip: After the first 180 one-minute samples, the 3-hour pressure trend and Edge-AI risk become active.
- ⚠ Do not seal the temperature/humidity sensors in an airtight box.
- ⚠ The on-device prediction is offline; SMS delivery still requires mobile coverage.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | bme280_1 VCC | power |
| GND | bme280_1 GND | ground |
| GPIO 21 | bme280_1 SDA | i2c |
| GPIO 22 | bme280_1 SCL | i2c |
| 3V3 | rain_sensor_1 VCC | power |
| GND | rain_sensor_1 GND | ground |
| GPIO 27 | rain_sensor_1 DO | digital |
| 3V3 | oled_1 VCC | power |
| GND | oled_1 GND | ground |
| GPIO 21 | oled_1 SDA | i2c |
| GPIO 22 | oled_1 SCL | i2c |
| 3V3 | dht11_1 VCC | power |
| GND | dht11_1 GND | ground |
| GPIO 4 | dht11_1 DATA | digital |
| EXT | usb_supply_1 +5V → Adjustable 4.0 V 3 A Buck Regulator VIN+ | power |
| GND | usb_supply_1 GND | ground |
| GND | gsm_buck_4v_1 VIN- | ground |
| EXT | gsm_buck_4v_1 VOUT+ → SIM800L GSM/GPRS Module VCC | power |
| GND | gsm_buck_4v_1 VOUT- | ground |
| GND | sim800l_1 GND | ground |
| GPIO 16 | sim800l_1 TX | uart |
| GPIO 17 | gsm_tx_series_1 A | digital |
| EXT | gsm_tx_series_1 B → UART Level Divider Resistor A | digital |
| GND | gsm_rx_pull_down_1 B | ground |
| EXT | sim800l_1 RX → UART Level Divider Resistor A | uart |
| EXT | sim800l_bulk_cap_1 + → SIM800L GSM/GPRS Module VCC | power |
| GND | sim800l_bulk_cap_1 - | ground |
| GND | alarm_buzzer_1 GND | ground |
| GPIO 25 | alarm_buzzer_1 SIGNAL | digital |
Firmware
ESP32#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_BME280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <math.h>
enum ForecastState { LEARNING, NORMAL, WATCH, WARNING, RAINING };
// Change these before deployment.
// Forward declarations
float dewPointC(float t, float rh);
void addPressure(float p);
float threeHourDeltaHpa(float p);
ForecastState classify(bool rain, bool ready, float dp, float rh, float gap);
uint8_t edgeAiRainRisk(bool rain, bool ready, float dp, float rh, float gap);
const char *edgeAiReason(bool rain, bool ready, float dp, float rh, float gap);
void updateAlarm();
void drawScreen(const String &text);
void gsmCommand(const char *cmd, unsigned long waitMs);
void clearGsmInput();
bool waitForNetworkRegistration(unsigned long timeoutMs);
bool sendSmsTo(const char *phone, const String &message);
bool sendSmsAlert();
String jsonPayload();
void connectWifi();
void startWebServer();
void takeSample();
const char *WIFI_SSID = "YOUR_WIFI_NAME";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char *ALERT_PHONES[] = {"+918134024933", "+918638167715"};
constexpr size_t ALERT_PHONE_COUNT = sizeof(ALERT_PHONES) / sizeof(ALERT_PHONES[0]);
constexpr int I2C_SDA = 21;
constexpr int I2C_SCL = 22;
constexpr int RAIN_DO_PIN = 27;
constexpr int DHT11_DATA_PIN = 4;
constexpr int GSM_RX_PIN = 16; // SIM800L TX -> ESP32 GPIO16
constexpr int GSM_TX_PIN = 17; // ESP32 GPIO17 -> 1k/4.7k divider -> SIM800L RX
constexpr int BUZZER_PIN = 25; // 3.3 V active buzzer signal input
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr int SCREEN_WIDTH = 128;
constexpr int SCREEN_HEIGHT = 64;
constexpr unsigned long SAMPLE_INTERVAL_MS = 60000UL;
constexpr unsigned long GSM_ALERT_COOLDOWN_MS = 15UL * 60UL * 1000UL;
constexpr int HISTORY_SAMPLES = 180;
Adafruit_BME280 bme;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHT11_DATA_PIN, DHT11);
WebServer server(80);
HardwareSerial gsm(2);
float pressureHistory[HISTORY_SAMPLES];
int historyCount = 0, historyHead = 0;
unsigned long lastSampleMs = 0, lastGsmAlertMs = 0;
String lastScreen, localIp = "Starting...";
float temperatureC = NAN, humidity = NAN, pressureHpa = NAN, dewC = NAN, dewGap = NAN, delta3h = 0;
bool rainDetected = false, trendReady = false;
uint8_t aiRiskPercent = 0;
const char *aiReason = "Collecting three-hour pressure history";
ForecastState currentState = LEARNING, previousState = LEARNING;
const char *stateText(ForecastState s) {
switch (s) { case LEARNING:return "LEARNING"; case NORMAL:return "NORMAL"; case WATCH:return "WATCH"; case WARNING:return "WARNING"; case RAINING:return "RAINING"; }
return "UNKNOWN";
}
const char *stateClass(ForecastState s) {
switch (s) { case NORMAL:return "normal"; case WATCH:return "watch"; case WARNING:return "warning"; case RAINING:return "raining"; default:return "learning"; }
}
float dewPointC(float t, float rh) {
const float a = 17.62f, b = 243.12f;
rh = constrain(rh, 1.0f, 100.0f);
float g = log(rh / 100.0f) + (a * t) / (b + t);
return (b * g) / (a - g);
}
void addPressure(float p) { pressureHistory[historyHead] = p; historyHead = (historyHead + 1) % HISTORY_SAMPLES; if (historyCount < HISTORY_SAMPLES) historyCount++; }
float threeHourDeltaHpa(float p) { return p - pressureHistory[historyHead]; }
// Explainable edge-AI decision tree. Its thresholds are visible and can later
// be retrained from labelled local rain-event logs.
uint8_t edgeAiRainRisk(bool rain, bool ready, float dp, float rh, float gap) {
if (rain) return 100;
if (!ready) return 0;
if (dp < -3.0f || dp / 3.0f < -1.5f) return (rh >= 85.0f || gap <= 2.5f) ? 92 : 80;
if (dp < -1.0f) return (rh >= 85.0f || gap <= 2.5f) ? 72 : 55;
if (rh >= 90.0f && gap <= 2.5f) return 65;
if (rh >= 85.0f || gap <= 2.5f) return 45;
return 12;
}
const char *edgeAiReason(bool rain, bool ready, float dp, float rh, float gap) {
if (rain) return "Rain sensor confirms onset";
if (!ready) return "Learning 3-hour pressure baseline";
if (dp < -3.0f || dp / 3.0f < -1.5f) return "Rapid pressure fall";
if (dp < -1.0f) return "Falling pressure trend";
if (rh >= 90.0f && gap <= 2.5f) return "Near-saturated humid air";
if (rh >= 85.0f) return "High relative humidity";
if (gap <= 2.5f) return "Dew-point gap is closing";
return "Stable local conditions";
}
ForecastState classify(bool rain, bool ready, float dp, float rh, float gap) {
if (rain) return RAINING;
if (!ready) return LEARNING;
uint8_t risk = edgeAiRainRisk(rain, ready, dp, rh, gap);
if (risk >= 80) return WARNING;
if (risk >= 45) return WATCH;
return NORMAL;
}
void updateAlarm() {
static unsigned long lastPhaseMs = 0;
static bool phaseOn = false;
if (currentState == WARNING) {
if (millis() - lastPhaseMs >= (phaseOn ? 180UL : 420UL)) { phaseOn = !phaseOn; lastPhaseMs = millis(); digitalWrite(BUZZER_PIN, phaseOn); }
} else if (currentState == RAINING) {
if (millis() - lastPhaseMs >= 700UL) { phaseOn = !phaseOn; lastPhaseMs = millis(); digitalWrite(BUZZER_PIN, phaseOn); }
} else { phaseOn = false; digitalWrite(BUZZER_PIN, LOW); }
}
void drawScreen(const String &text) {
if (text == lastScreen) return;
lastScreen = text;
display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 0); display.print(text); display.display();
}
void clearGsmInput() {
while (gsm.available()) gsm.read();
}
void gsmCommand(const char *cmd, unsigned long waitMs) {
clearGsmInput();
gsm.println(cmd);
unsigned long started = millis();
while (millis() - started < waitMs) {
while (gsm.available()) Serial.write(gsm.read());
delay(10);
}
}
bool waitForNetworkRegistration(unsigned long timeoutMs) {
Serial.println("SURAL_GSM,Scanning available mobile networks");
gsmCommand("AT+COPS=?", 60000UL); // Operator scan can take up to about a minute.
Serial.println("SURAL_GSM,Selecting automatic network");
gsmCommand("AT+COPS=0", 3000);
unsigned long started = millis();
while (millis() - started < timeoutMs) {
clearGsmInput();
gsm.println("AT+CREG?");
String reply;
unsigned long queryStarted = millis();
while (millis() - queryStarted < 1200UL) {
while (gsm.available()) reply += char(gsm.read());
delay(10);
}
Serial.print("SURAL_GSM,Network status: "); Serial.println(reply);
// +CREG: n,1 = registered home; n,5 = registered roaming.
if (reply.indexOf(",1") >= 0 || reply.indexOf(",5") >= 0) {
Serial.println("SURAL_GSM,Network registered");
return true;
}
delay(3000);
}
Serial.println("SURAL_GSM,Network registration timed out; alerts will retry on the next state transition");
return false;
}
bool sendSmsTo(const char *phone, const String &message) {
clearGsmInput();
gsm.print("AT+CMGS=\""); gsm.print(phone); gsm.println("\"");
delay(800);
gsm.print(message); gsm.write(26);
String reply;
unsigned long started = millis();
while (millis() - started < 15000UL) {
while (gsm.available()) reply += char(gsm.read());
if (reply.indexOf("+CMGS:") >= 0 && reply.indexOf("OK") >= 0) {
Serial.print("SURAL_GSM,SMS delivered to modem for "); Serial.println(phone);
return true;
}
if (reply.indexOf("ERROR") >= 0) break;
delay(10);
}
Serial.print("SURAL_GSM,SMS failed for "); Serial.print(phone); Serial.print(": "); Serial.println(reply);
return false;
}
bool sendSmsAlert() {
gsmCommand("AT", 500);
gsmCommand("AT+CMGF=1", 500);
if (!waitForNetworkRegistration(30000UL)) return false;
String message = "SURAL " + String(stateText(currentState));
message += ". T=" + String(temperatureC, 1) + "C RH=" + String(humidity, 0) + "%";
message += " P=" + String(pressureHpa, 1) + "hPa dP3h=" + String(delta3h, 1) + "hPa";
message += " Rain=" + String(rainDetected ? "YES" : "NO");
bool anyDelivered = false;
for (size_t i = 0; i < ALERT_PHONE_COUNT; ++i) {
if (sendSmsTo(ALERT_PHONES[i], message)) anyDelivered = true;
delay(1000);
}
return anyDelivered;
}
String jsonPayload() {
String s = "{\"temperature\":" + String(temperatureC, 1);
s += ",\"humidity\":" + String(humidity, 0) + ",\"pressure\":" + String(pressureHpa, 1);
s += ",\"dew\":" + String(dewC, 1) + ",\"gap\":" + String(dewGap, 1);
s += ",\"delta\":" + String(delta3h, 1) + ",\"samples\":" + String(historyCount);
s += ",\"rain\":" + String(rainDetected ? "true" : "false");
s += ",\"aiRisk\":" + String(aiRiskPercent) + ",\"aiReason\":\"" + String(aiReason);
s += "\",\"state\":\"" + String(stateText(currentState)) + "\",\"class\":\"" + String(stateClass(currentState));
s += "\",\"ip\":\"" + localIp + "\"}";
return s;
}
const char DASHBOARD[] PROGMEM = R"HTML(
<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>SURAL Weather</title><style>
*{box-sizing:border-box}body{margin:0;background:#071522;color:#edf6ff;font-family:Arial,sans-serif}main{max-width:1050px;margin:auto;padding:24px}.hero{background:linear-gradient(120deg,#1065a7,#5c2c92);border-radius:22px;padding:25px;box-shadow:0 12px 30px #0007}h1{margin:0;font-size:2rem}.sub{opacity:.85;margin:8px 0}.state{display:inline-block;padding:9px 16px;border-radius:20px;font-weight:bold;letter-spacing:1px}.normal{background:#087f5b}.watch{background:#a96f00}.warning{background:#b3261e}.raining{background:#145da0}.learning{background:#4a5568}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:15px;margin-top:20px}.card{background:#102437;border:1px solid #24445e;border-radius:18px;padding:18px}.label{color:#9fc1dc;font-size:.82rem;text-transform:uppercase;letter-spacing:1px}.value{font-size:2rem;font-weight:bold;margin-top:7px}.unit{font-size:1rem;color:#9fc1dc}.note{margin-top:20px;padding:16px;border-left:4px solid #50bfe6;background:#0d2031;border-radius:8px;color:#c8ddec}footer{color:#8aa9bf;text-align:center;padding:22px;font-size:.85rem}</style></head><body><main><section class="hero"><h1>☁ SURAL Weather Station</h1><p class="sub">Hyperlocal rain-risk monitor · live field conditions</p><div id="state" class="state learning">LOADING</div><p class="sub" id="ip">Connecting…</p></section><section class="grid"><div class="card"><div class="label">Temperature</div><div class="value"><span id="t">--</span><span class="unit"> °C</span></div></div><div class="card"><div class="label">Humidity</div><div class="value"><span id="h">--</span><span class="unit"> %</span></div></div><div class="card"><div class="label">Pressure</div><div class="value"><span id="p">--</span><span class="unit"> hPa</span></div></div><div class="card"><div class="label">3-hour ΔP</div><div class="value"><span id="d">--</span><span class="unit"> hPa</span></div></div><div class="card"><div class="label">Dew-point gap</div><div class="value"><span id="g">--</span><span class="unit"> °C</span></div></div><div class="card"><div class="label">Rain sensor</div><div class="value" id="r">--</div></div><div class="card"><div class="label">Edge-AI rain risk</div><div class="value"><span id="ai">--</span><span class="unit"> %</span></div></div></section><div class="note" id="note">Pressure history is being collected.</div><footer>Updates every 5 seconds · Explainable on-device decision tree · Zambretti-inspired pressure tendency · SURAL</footer></main><script>async function u(){try{let d=await(await fetch('/api')).json(),m={t:'temperature',h:'humidity',p:'pressure',d:'delta',g:'gap'};for(let k in m)document.getElementById(k).textContent=d[m[k]];document.getElementById('r').textContent=d.rain?'WET / RAIN':'DRY';document.getElementById('ai').textContent=d.aiRisk;let s=document.getElementById('state');s.textContent=d.state;s.className='state '+d.class;document.getElementById('ip').textContent='Dashboard: http://'+d.ip;document.getElementById('note').textContent=d.samples<180?'Learning pressure trend: '+d.samples+' / 180 one-minute samples collected.':('Edge-AI reason: '+d.aiReason+'. This explainable model can be locally re-tuned from labelled rain-event logs.')}catch(e){document.getElementById('note').textContent='Waiting for weather-station data…'}}u();setInterval(u,5000);</script></body></html>
)HTML";
void connectWifi() {
WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); unsigned long started = millis();
while (WiFi.status() != WL_CONNECTED && millis() - started < 20000UL) delay(250);
if (WiFi.status() == WL_CONNECTED) { localIp = WiFi.localIP().toString(); Serial.println("SURAL_WEB,http://" + localIp); }
else { WiFi.mode(WIFI_AP); WiFi.softAP("SURAL-Weather", "suralweather"); localIp = WiFi.softAPIP().toString(); Serial.println("SURAL_WEB,AP at http://" + localIp); }
}
void startWebServer() {
server.on("/", HTTP_GET, [](){ server.send_P(200, "text/html", DASHBOARD); });
server.on("/api", HTTP_GET, [](){ server.send(200, "application/json", jsonPayload()); });
server.onNotFound([](){ server.send(404, "text/plain", "SURAL: page not found"); }); server.begin();
}
void takeSample() {
float t = dht.readTemperature(), rh = dht.readHumidity(), p = bme.readPressure() / 100.0f;
if (isnan(t) || isnan(rh) || isnan(p)) { Serial.println("SURAL_ERROR,Sensor read failed"); drawScreen("SURAL ERROR\nSensor read failed"); return; }
temperatureC = t; humidity = rh; pressureHpa = p; rainDetected = digitalRead(RAIN_DO_PIN) == LOW;
dewC = dewPointC(t, rh); dewGap = t - dewC; trendReady = historyCount >= HISTORY_SAMPLES; delta3h = trendReady ? threeHourDeltaHpa(p) : 0.0f;
aiRiskPercent = edgeAiRainRisk(rainDetected, trendReady, delta3h, rh, dewGap);
aiReason = edgeAiReason(rainDetected, trendReady, delta3h, rh, dewGap);
previousState = currentState; currentState = classify(rainDetected, trendReady, delta3h, rh, dewGap);
Serial.printf("%lu,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%s,%d\n",millis(),t,rh,p,dewC,dewGap,delta3h,stateText(currentState),rainDetected?1:0);
String page="SURAL WEATHER\nT:"+String(t,1)+"C RH:"+String(rh,0)+"%\nP:"+String(p,1)+" hPa\n";
page += trendReady ? "dP3h:"+String(delta3h,1)+" AI:"+String(aiRiskPercent)+"%\n" : "dP: "+String(historyCount)+"/180\n"; page += "STATE: "+String(stateText(currentState)); drawScreen(page);
if ((currentState==WARNING || currentState==RAINING) && currentState!=previousState && millis()-lastGsmAlertMs>=GSM_ALERT_COOLDOWN_MS) if (sendSmsAlert()) lastGsmAlertMs=millis();
addPressure(p);
}
void setup() {
Serial.begin(115200); pinMode(RAIN_DO_PIN, INPUT_PULLUP); pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); Wire.begin(I2C_SDA,I2C_SCL); dht.begin(); gsm.begin(9600,SERIAL_8N1,GSM_RX_PIN,GSM_TX_PIN);
if (!display.begin(SSD1306_SWITCHCAPVCC,OLED_ADDRESS)) Serial.println("SURAL_ERROR,SSD1306 not found");
bool found=bme.begin(0x77,&Wire); if(!found) found=bme.begin(0x76,&Wire); if(!found){drawScreen("SURAL ERROR\nBME280 not found");while(true)delay(1000);}
connectWifi(); startWebServer(); Serial.println("uptime_ms,dht_temp_c,dht_rh_pct,bme_pressure_hpa,dew_c,dew_gap_c,delta_p_3h,state,rain_detected"); takeSample(); lastSampleMs=millis();
}
void loop() { server.handleClient(); updateAlarm(); unsigned long now=millis(); if(now-lastSampleMs>=SAMPLE_INTERVAL_MS){lastSampleMs+=SAMPLE_INTERVAL_MS;takeSample();} }“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.