Community project
Pressure-Tendency Rain Forecaster
This 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

Gather all the parts
Assemble it in 5 steps
1. Prepare 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.
- Place the BME280 and DHT11 in a shaded, ventilated radiation shield so sunlight and rain do not distort their readings.
- 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.
2. 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.
- The BME280 address can be 0x76 or 0x77; the firmware automatically tries both.
- The OLED is a local status display; fit it behind a clear, shaded enclosure window.
- Never connect I²C signals to 5 V.
3. 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.
- Adjust the rain-module trimmer so its digital output changes only when the remote rain plate is wet.
- 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.
4. 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.
- Fit an activated SIM card with SMS service and antenna before powering the modem.
- 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.
5. 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.
- For validation, preserve the serial CSV output and record whether rain begins within two hours of each WARNING.
- 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.
Review all connections
1. Connections between "bme280_1" and "ESP32"
2. Connections between "rain_sensor_1" and "ESP32"
3. Connections between "oled_1" and "ESP32"
4. Connections between "dht11_1" and "ESP32"
5. Connections between "usb_supply_1" and "ESP32"
6. Connections between "gsm_buck_4v_1" and "ESP32"
7. Connections between "sim800l_1" and "ESP32"
8. Connections between "gsm_tx_series_1" and "ESP32"
9. Connections between "gsm_rx_pull_down_1" and "ESP32"
10. Connections between "sim800l_bulk_cap_1" and "ESP32"
11. Connections between "alarm_buzzer_1" and "ESP32"
Deploy the firmware
#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();} }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.




