Build a Solar-Powered Hyperlocal Weather Station with ESP32, BME280 & Rain Gauge
Comprehensive step-by-step engineering guide for building an off-grid solar weather station with rainfall tracking, barometric telemetry, and dusk brownout protection.
- Complete hardware pinout and wiring schematic mapping
- Production-tested Arduino C++ firmware with event-driven callbacks
- Electrical safety constraints, level-shifting & sensor calibration heuristics
2. Overview & Engineering Objectives
Standard commercial weather stations cost thousands of rupees and lock historical data behind proprietary cloud silos. This project implements a fully open, off-grid hyperlocal meteorological terminal that monitors ambient pressure, humidity, temperature, and cumulative rainfall.
Core Engineering Capabilities:
Calibrated Rainfall Measurement: Captures discrete 0.2794 mm (0.01 in) bucket tips using a magnetic reed switch interrupt, storing cumulative precipitation in RTC persistent SRAM memory.
Contact Bounce Suppression: Eliminates phantom tip counts caused by mechanical switch bounce using hardware filtering and software interrupt timestamp rejection.
Deep Sleep Power Budgeting: Keeps the ESP32 in deep sleep (>98% of the operational life cycle), waking up every 15 minutes to sample atmospheric metrics and broadcast over Wi-Fi.
Dusk Brownout Lockout: Protects the 18650 Li-ion cell from cyclic reboot destruction when solar irradiance collapses during sunset.
3. Bill of Materials (BOM)
Component
Quantity
Specification / Rating
Est. Cost (INR)
Sourcing / Affiliate Link
ESP32 DevKit V1 Board
1
30-Pin Dual-Core Xtensa LX6 (Wi-Fi + BLE)
₹380
Buy on Mentora Store
Bosch BME280 Module
1
Temperature, Humidity & Barometric Pressure (I2C)
₹280
Buy on Mentora Store
Tipping Bucket Rain Gauge
1
Magnetic reed switch, 0.2794mm per tip resolution
₹650
Buy on Mentora Store
6V 2W Solar Panel
1
Monocrystalline waterproof outdoor panel
₹220
Buy on Mentora Store
TP4056 Solar Charger Board
1
5V 1A Li-ion charger with DW01A protection IC
₹45
Buy on Mentora Store
18650 Li-ion Battery (2600mAh)
1
Protected 3.7V rechargeable cell
₹160
Buy on Mentora Store
10kΩ & 100kΩ Resistors
1 Set
Pull-up & battery divider resistors
₹15
Buy on Mentora Store
IP65 Weatherproof Enclosure
1
Ventilated radiation shield Stevenson screen housing
₹340
Buy on Mentora Store
Total Hardware Cost
~₹2,090
4. Hardware Engineering: Rain Gauge Debounce & Solar Dusk Brownouts
1. Tipping Bucket Contact Bounce & Debounce Filtering
The tipping bucket contains a small magnet on the tipping rocker that sweeps past a magnetic reed switch.
The Physical Problem: When the delicate glass reed switch contacts make contact, mechanical inertia causes the metal leaves to bounce 3 to 8 times within 5 milliseconds. Without filtering, a single bucket tip registers as 5 to 8 tips, exaggerating rain measurements by 500%!
The Solution: Use an external 10kΩ pull-up resistor and a 100nF capacitor parallel filter across the reed switch, combined with software ISR lockout:
void IRAM_ATTR rainBucketISR() {
unsigned long now = millis();
if (now - lastTipMillis > 200) { // 200ms debounce lockout
tipCount++;
lastTipMillis = now;
}
}
2. The Solar Dusk Brownout Lockout
As daylight fades at dusk, the solar panel output drops to ~3.0V–3.4V. The battery charge circuit cannot deliver full current, causing the 18650 cell voltage to sag under Wi-Fi transmission bursts (drawing ~250mA).
This triggers repetitive brownout resets, draining the battery below its safe 2.8V cutoff.
The Engineering Fix: In setup(), read battery voltage first via a 2:1 resistive divider on GPIO 35 (ADC1). If batteryVoltage < 3.35V, abort Wi-Fi transmission, log to RTC memory, and immediately enter deep sleep for 1 hour to protect the battery.
5. Rainfall & Barometric Calculations
Rainfall Accumulation: $Rainfall (mm) = tipCount × 0.2794 mm$
Sea-Level Elevation Pressure Compensation: $P_0 = P × left(1 - (0.0065 · h)/(T_C + 0.0065 · h + 273.15)right)^-5.257 Where h = \text{Station elevation in meters}, T_C = \text{Temperature in }^\circ\text{C}, P = \text{Measured absolute pressure in hPa}$.
6. Real-World Troubleshooting FAQs
Q1: Why does bme.begin() fail intermittently after deep sleep wake-up?
Root Cause: Inadequate I2C bus power settling time. After waking from deep sleep, the 3.3V rail needs ~15ms to stabilize. Always insert delay(20); after Wire.begin() before calling bme.begin(0x76).
Q2: Why does rainfall count reset to zero every time the ESP32 wakes up?
Root Cause: Variable declared in normal volatile SRAM. Normal variables are cleared upon deep sleep reset!
Fix: Prefix the tip counter with the RTC memory attribute:
RTCDATAATTR volatile uint32_t tipCount = 0;
Q3: Why does Home Assistant show duplicate MQTT weather entries?
Fix: Publish MQTT telemetry with the retain flag set to false, or use distinct message sequence numbers.
7. Mentora Monetization & Student Support Hooks
🛒 Official Mentora Solar Weather Station Kit: ESP32 DevKit, BME280 sensor, precision tipping bucket gauge, 6V solar panel, TP4056 board, and 3D printable Stevenson screen for ₹2,699.
⚡ ₹49 Mentora Token Vault: Instantly unlock the companion Home Assistant MQTT auto-discovery YAML, Weather Underground upload script, and Grafana dashboard templates.
🛠️ ₹299 1-on-1 Mentora Live Video Debugging: 30-minute live session to calibrate tipping bucket volume coefficients, tune interrupt debounce capacitors, and eliminate solar dusk brownouts.
Hardware Pinout & Wiring Connections
Hardware Pinout & Wiring Connection Guide
Project 14: Hyperlocal Solar Weather Station with BME280 & Tipping Rain Gauge
Microcontroller: ESP32 DevKit V1 (30-pin / 36-pin)Meteorological Sensors: Bosch BME280 Environmental Sensor (I2C), Tipping Bucket Rain Gauge (Reed Switch Interrupt)Power Stage: 6V 2W Monocrystalline Solar Panel + TP4056 Solar Charger + 18650 Li-ion Battery (3.7V)Analog Monitoring: 2:1 Precision Voltage Divider (Battery Level into ADC1 GPIO 35)
1. Complete Fritzing Pin Connection Mapping Table
Table A: Environmental Sensors & Tipping Bucket Inputs
Component
Component Pin
ESP32 Pin
Wire Color
Bus Signal / Function
Electrical Guidelines & Constraints
BME280 Module
VCC / VIN
3V3 Rail
Red
3.3V DC Sensor Supply
Powered strictly from 3.3V.
BME280 Module
GND
GND Rail
Black
Ground Return
Common system ground.
BME280 Module
SCL / SCK
GPIO 22
Yellow
I2C Hardware Clock
Primary I2C Clock line. Shared on Wire bus.
BME280 Module
SDA / SDI
GPIO 21
Blue
I2C Hardware Data
Primary I2C Data line.
Rain Gauge Gauge
Lead 1
GPIO 13
Orange
Reed Switch Interrupt
Pulled HIGH via 10kΩ resistor; pulsed to GND on bucket tip.
Rain Gauge Gauge
Lead 2
GND Rail
Black
Switch Ground Return
Connected to common system ground.
10kΩ Pull-Up
Lead 1 to Lead 2
Between 3V3 & GPIO 13
—
Hardware Pull-Up
Ensures sharp clean digital HIGH state during idle.
100nF Capacitor
Lead 1 to Lead 2
Across GPIO 13 & GND
—
Hardware RC Debounce
Suppresses high-frequency mechanical reed switch chatter.
Table B: Solar Power, Battery & Voltage Divider
Component / Sub-circuit
Terminal
Connected Node
Wire Color
Role / Operation
6V Solar Panel
Positive (+)
TP4056 IN (+) Terminal
Red
Photovoltaic solar harvesting line.
6V Solar Panel
Negative (-)
TP4056 IN (-) Terminal
Black
Solar panel ground return.
18650 Li-ion Cell
Positive (+)
TP4056 BAT (+) Terminal
Red
3.7V–4.2V rechargeable battery storage.
18650 Li-ion Cell
Negative (-)
TP4056 BAT (-) Terminal
Black
Battery ground return.
TP4056 Power Out
OUT (+)
ESP32 3V3 Pin (or LDO IN)
Red
Powers ESP32 logic rail.
TP4056 Power Out
OUT (-)
Common GND Rail
Black
Common system ground.
Battery Divider (Top 100kΩ)
Lead 1 to Lead 2
Battery (+) to GPIO 35
Yellow
Top leg of 2:1 divider. Safe 0–2.1V input into ADC1.
Battery Divider (Bottom 100kΩ)
Lead 1 to Lead 2
GPIO 35 to GND Rail
Black
Bottom leg of divider to ground.
2. Circuit Connection Schematic Diagram (ASCII Art)
+-------------------------------------------------------------+
| ESP32 DevKit V1 |
| |
| [ 3V3 ] [ GND ] [GPIO 21] [GPIO 22] [GPIO 13] [GPIO 35]
+----+--------+------------+---------+-----------+---------+---+
| | | | | |+3.3V | GND | SDA | SCL | Rain | Battery |
Rail | Rail | Line | Line | Interrupt | Divider |
=======+========+============+=========+===========+=========+====
| | | | | || | | | [10k Pullup] || | | | +---+---+ || | | | | | |+--+--------+----+ | | | +---------+
| VCC GND SDA SCL| | | | || BME280 |<----+---------+ | || (Addr: 0x76) | | [100nF Debounce]+------------------+ | |
| GND
|
+---------------------------------------+
|
+--+---------------+
| Lead 1 Lead 2 |
| TIPPING BUCKET |
| REED SWITCH |
+---------+--------+
|
GND Rail <-----+
BATTERY MONITOR DIVIDER
Battery (+) ---> [100k] ---+---> [GPIO 35] (ADC1)
|
GND <--- [100k]-+
3. Golden Engineering Rules for Outdoor Weather Nodes
Stevenson Screen Ventilation:
Always house the BME280 sensor inside a louvred Stevenson screen or solar radiation shield. Direct sunlight hitting the sensor will inflate temperature readings by 5°C to 12°C.
Rain Gauge Leveling:
The tipping bucket must be mounted dead-level. Use the small bubble level on the base of the gauge; an off-level bucket tips unevenly, causing a 15% to 30% calibration error between directions.
Hardware + Software Debounce:
Mechanical reed switches bounce. The combination of an external 100nF capacitor and the software 200ms ISR lockout ensures zero false tip events.
Complete Tested Arduino C++ Firmware
``cpp
/*
- ==============================================================================
- Project: Hyperlocal Solar Weather Station with BME280 & Tipping Rain Gauge
- Microcontroller: ESP32 DevKit V1 (Xtensa 32-bit Dual-Core)
- Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
- License: MIT
*
- Hardware Peripherals:
- - BME280 Sensor: Shared I2C (SDA -> GPIO 21, SCL -> GPIO 22, Auto 0x76/0x77)
- - Tipping Rain Gauge: Reed Switch -> GPIO 13 (External Interrupt with Debounce)
- - Battery Divider: Signal -> GPIO 35 (ADC1 Channel 7, 100k + 100k)
- - Power: 6V Solar Panel + TP4056 + 18650 Battery (Deep Sleep Architecture)
- ==============================================================================
*/
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <HTTPClient.h>
// -----------------------------------------------------------------------------
// USER CONFIGURATION (WIFI & TELEMETRY TARGET)
// -----------------------------------------------------------------------------
const char* WIFISSID = "YOURWIFI_SSID";
const char* WIFIPASSWORD = "YOURWIFI_PASSWORD";
const char* TELEMETRY_URL = "http://api.mentora.in/telemetry/weather";
#define RAIN_PIN 13
#define BATTERYADCPIN 35
#define STATUSLEDPIN 2
#define SLEEPINTERVALSEC 900ULL // 15-minute sleep interval
#define MMPERTIP 0.2794 // Metric rainfall per bucket tip (mm)
// RTC Persistent Memory across Deep Sleep reboots
RTCDATAATTR volatile uint32_t tipCount = 0;
RTCDATAATTR int wakeCycleCounter = 0;
RTCDATAATTR unsigned long lastTipMillis = 0;
Adafruit_BME280 bme;
// -----------------------------------------------------------------------------
// REED SWITCH INTERRUPT SERVICE ROUTINE (WITH DEBOUNCE)
// -----------------------------------------------------------------------------
void IRAM_ATTR rainTipISR() {
unsigned long now = millis();
// 200ms software debounce lock to suppress mechanical switch chatter
if (now - lastTipMillis > 200) {
tipCount++;
lastTipMillis = now;
}
}
// -----------------------------------------------------------------------------
// BATTERY VOLTAGE SAMPLER & DUSK LOCKOUT CHECK
// -----------------------------------------------------------------------------
float readBatteryVoltage() {
long sum = 0;
for (int i = 0; i < 16; i++) {
sum += analogRead(BATTERYADCPIN);
delay(2);
}
float pinVolts = (sum / 16.0 / 4095.0) * 3.3;
return pinVolts * 2.0; // 2:1 resistive divider (100k + 100k)
}
// -----------------------------------------------------------------------------
// BME280 AUTO-DETECTION INITIALIZATION
// -----------------------------------------------------------------------------
bool initializeBME280() {
Wire.begin(21, 22);
delay(20); // Power rail settling time
if (bme.begin(0x76, &Wire)) {
Serial.println("[BME280] Detected successfully at address 0x76.");
return true;
}
if (bme.begin(0x77, &Wire)) {
Serial.println("[BME280] Detected successfully at address 0x77.");
return true;
}
Serial.println("[ERROR] BME280 sensor not detected on 0x76 or 0x77!");
return false;
}
// -----------------------------------------------------------------------------
// HTTP CLOUD TELEMETRY DISPATCHER
// -----------------------------------------------------------------------------
void uploadWeatherTelemetry(float tempC, float humidity, float pressureHPa, float totalRainMM, float batteryVolts) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
http.begin(TELEMETRY_URL);
http.addHeader("Content-Type", "application/json");
String json = "{";
json += "\"station\":\"SOLARWX01\",";
json += "\"cycle\":" + String(wakeCycleCounter) + ",";
json += "\"temperature_c\":" + String(tempC, 2) + ",";
json += "\"humidity_pct\":" + String(humidity, 1) + ",";
json += "\"pressure_hpa\":" + String(pressureHPa, 2) + ",";
json += "\"rainaccummm\":" + String(totalRainMM, 3) + ",";
json += "\"battery_v\":" + String(batteryVolts, 2);
json += "}";
Serial.println("[HTTP] Transmitting payload: " + json);
int httpCode = http.POST(json);
Serial.printf("[HTTP] Server Response Code: %d\n", httpCode);
http.end();
}
// -----------------------------------------------------------------------------
// ARDUINO SETUP
// -----------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(100);
wakeCycleCounter++;
Serial.println("\n========================================================");
Serial.printf(" MENTORA HYPERLOCAL SOLAR WEATHER STATION (CYCLE #%d)\n", wakeCycleCounter);
Serial.println("========================================================");
// Configure Pins
pinMode(RAINPIN, INPUTPULLUP);
attachInterrupt(digitalPinToInterrupt(RAIN_PIN), rainTipISR, FALLING);
pinMode(BATTERYADCPIN, INPUT);
pinMode(STATUSLEDPIN, OUTPUT);
digitalWrite(STATUSLEDPIN, HIGH);
analogReadResolution(12);
analogSetAttenuation(ADC_11db);
// 1. Evaluate Battery Health (Dusk Brownout Lockout)
float batteryVolts = readBatteryVoltage();
Serial.printf("[BATTERY] Measured Voltage: %.2fV\n", batteryVolts);
if (batteryVolts < 3.35) {
Serial.println("[LOCKOUT] Battery depleted / Dusk brownout detected (< 3.35V)!");
Serial.println("[LOCKOUT] Skipping Wi-Fi to preserve Li-ion cell. Sleeping for 1 hour...");
digitalWrite(STATUSLEDPIN, LOW);
espsleepenabletimerwakeup(3600ULL * 1000000ULL);
espdeepsleep_start();
}
// 2. Read BME280 Sensors
float tempC = 0.0, humidity = 0.0, pressure = 0.0;
if (initializeBME280()) {
tempC = bme.readTemperature();
humidity = bme.readHumidity();
pressure = bme.readPressure() / 100.0F; // Pa to hPa
}
float totalRainMM = (float)tipCount * MMPERTIP;
Serial.printf("[WEATHER] Temp: %.2f C | Humidity: %.1f%% | Pressure: %.2f hPa | Rain: %.3f mm\n",
tempC, humidity, pressure, totalRainMM);
// 3. Connect Wi-Fi and Upload
Serial.printf("[WIFI] Connecting to SSID: %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFISSID, WIFIPASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected! Dispatching weather packet...");
uploadWeatherTelemetry(tempC, humidity, pressure, totalRainMM, batteryVolts);
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
} else {
Serial.println("\n[WARNING] Wi-Fi timed out. Data stored in RTC memory.");
}
digitalWrite(STATUSLEDPIN, LOW);
// 4. Configure Wake-ups (Timer + Rain Bucket External Interrupt)
espsleepenabletimerwakeup(SLEEPINTERVALSEC * 1000000ULL);
espsleepenableext0wakeup((gpionumt)RAIN_PIN, 0); // Wake up immediately if bucket tips!
Serial.printf("[SYSTEM] Entering deep sleep for %llu seconds...\n\n", SLEEPINTERVALSEC);
espdeepsleep_start();
}
void loop() {
// Not reached in deep sleep architecture
}
``
Build a Solar-Powered Hyperlocal Weather Station with ESP32, BME280 & Rain Gauge Firmware & Schematics Vault
Download verified Arduino C++ firmware, pinout table, and BOM for Build a Solar-Powered Hyperlocal Weather Station with ESP32, BME280 & Rain Gauge.
Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized
void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }
void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }
float calculatePID(float error, float kp, float ki, float kd) { ... }
void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }
1 Code Snippets & 0 Schematics Locked
Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.
Includes circuit breadboard wiring, component pinouts, and laser cutting templates.
Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.
See What Mentora Makers Are Building
Real video walk-throughs, breadboard tests, and flight demonstrations submitted by students & parents.
Review This Build & Claim a Free ₹49 Token
Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.
Have a Reward Voucher?
Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.
Earn ₹50 Per Friend You Refer
Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!
Why does bme.begin() fail intermittently after deep sleep wake-up?
Root Cause: Inadequate I2C bus power settling time. After waking from deep sleep, the 3.3V rail needs ~15ms to stabilize. Always insert delay(20); after Wire.begin() before calling bme.begin(0x76).
Why does rainfall count reset to zero every time the ESP32 wakes up?
Root Cause: Variable declared in normal volatile SRAM. Normal variables are cleared upon deep sleep reset! Fix: Prefix the tip counter with the RTC memory attribute: RTC_DATA_ATTR volatile uint32_t tipCount = 0;
Why does Home Assistant show duplicate MQTT weather entries?
Fix: Publish MQTT telemetry with the retain flag set to false, or use distinct message sequence numbers.
Learn this in our Junior Tech: Game Design & Scratch
Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.