IoT Weather Station
with Cloud Integration
A professional-grade weather station running on ESP32 — 6 sensors, OLED local display, live web dashboard with auto-refresh, and automatic ThingSpeak cloud logging. Simulated 100% in Wokwi.
Project Overview
What You'll BuildThis project simulates a professional IoT weather monitoring system — the kind used in smart cities and agricultural monitoring. The ESP32 reads 6 sensors simultaneously, displays data locally on an OLED, serves a beautiful web dashboard over Wi-Fi, and uploads data to the ThingSpeak cloud every 20 seconds.
Auto Weather Condition Detection
Light >80%
Rain >60%
Rain >30%
Light <20%
Hum >80%
Temp >30°C
Temp <15°C
Default
6 Sensors & Components
Step 1All components are virtual in Wokwi — no hardware shopping needed. Here's what each sensor measures:
DHT22
Temperature & Humidity sensor (−40° to 80°C)
Pin D15BMP280
Pressure (hPa) & Altitude (metres) via I²C
I²C 0x76Light Sensor
Brightness level 0–100% (potentiometer in Wokwi)
Pin D34Rain Sensor
Precipitation % detection (potentiometer in Wokwi)
Pin D35OLED SSD1306
128×64 local display — all readings at once
I²C 0x3CStatus LED
Blue LED blinks on every sensor read cycle
Pin D2Circuit Wiring Reference
Step 2If you paste diagram.json (Step 3), wiring is automatic. This table is for manual reference.
| Component | Pin | ESP32 | Wire |
|---|---|---|---|
| DHT22 | VCC | 3.3V | Red |
| DHT22 | GND | GND | Black |
| DHT22 | SDA (Data) | D15 | Green |
| BMP280 | VCC | 3.3V | Red |
| BMP280 | SDA | D21 | Blue |
| BMP280 | SCL | D22 | Yellow |
| OLED SSD1306 | SDA | D21 | Blue (shared I²C) |
| OLED SSD1306 | SCL | D22 | Yellow (shared I²C) |
| Light Sensor (Pot) | SIG | D34 | Orange |
| Rain Sensor (Pot) | SIG | D35 | Purple |
| Blue LED → 220Ω | Anode (+) | D2 | Blue |
BMP280 and OLED share the same I²C bus (SDA: D21, SCL: D22). BMP280 uses address
Wokwi Setup — Quick Start
Step 3 · 3 MinutesWokwi is free — no account needed to start. Go to wokwi.com, create a new ESP32 project, then paste the files below.
🌐 Create New ESP32 Project
- Go to wokwi.com in any modern browser
- Click "New Project" → select "ESP32" (not Arduino UNO)
- You'll see a blank canvas with the ESP32 DevKit placed
Create a free Wokwi account to save your project and share the simulation link with your teacher or classmates!
📋 Paste diagram.json — Auto-Wires All Components
- Click the "diagram.json" tab next to sketch.ino
- Select all existing text and delete it completely
- Paste the complete JSON below and press Ctrl+S
- All 8 components appear fully wired ✅
Copy the ENTIRE JSON — starts with
{
"version": 1,
"author": "ESP32 IoT Weather Station",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} },
{ "type": "wokwi-dht22", "id": "dht1", "top": -86.4, "left": 143.4, "attrs": { "temperature": "24", "humidity": "60" } },
{ "type": "wokwi-bmp280", "id": "bmp1", "top": -105.6,"left": 249.6, "attrs": { "i2cAddress": "0x76" } },
{ "type": "wokwi-ssd1306", "id": "oled1","top": 144, "left": 288, "attrs": { "i2cAddress": "0x3C" } },
{ "type": "wokwi-potentiometer", "id": "ldr1", "top": -19.2, "left": 364.8, "rotate": 180, "attrs": { "label": "Light Sensor" } },
{ "type": "wokwi-potentiometer", "id": "rain1","top": 96, "left": 364.8, "rotate": 180, "attrs": { "label": "Rain Sensor" } },
{ "type": "wokwi-led", "id": "led1", "top": -124.8,"left": 470.4, "attrs": { "color": "blue", "lightColor": "blue" } },
{ "type": "wokwi-resistor", "id": "r1", "top": -67.2, "left": 470.4, "rotate": 90, "attrs": { "value": "220" } }
],
"connections": [
[ "esp:TX0", "$serialMonitor:RX", "", [] ],
[ "esp:RX0", "$serialMonitor:TX", "", [] ],
[ "dht1:VCC", "esp:3V3", "red", ["v0"] ],
[ "dht1:GND", "esp:GND.1", "black", ["v0"] ],
[ "dht1:SDA", "esp:D15", "green", ["v0"] ],
[ "bmp1:VCC", "esp:3V3", "red", ["v0"] ],
[ "bmp1:GND", "esp:GND.1", "black", ["v0"] ],
[ "bmp1:SDA", "esp:D21", "blue", ["v0"] ],
[ "bmp1:SCL", "esp:D22", "yellow", ["v0"] ],
[ "oled1:VCC","esp:3V3", "red", ["h0"] ],
[ "oled1:GND","esp:GND.2", "black", ["h0"] ],
[ "oled1:SDA","esp:D21", "blue", ["h0"] ],
[ "oled1:SCL","esp:D22", "yellow", ["h0"] ],
[ "ldr1:GND", "esp:GND.1", "black", ["v0"] ],
[ "ldr1:VCC", "esp:3V3", "red", ["v0"] ],
[ "ldr1:SIG", "esp:D34", "orange", ["v0"] ],
[ "rain1:GND","esp:GND.2", "black", ["v0"] ],
[ "rain1:VCC","esp:3V3", "red", ["v0"] ],
[ "rain1:SIG","esp:D35", "purple", ["v0"] ],
[ "led1:A", "esp:D2", "blue", ["v0"] ],
[ "led1:C", "r1:1", "blue", ["v0"] ],
[ "r1:2", "esp:GND.1", "black", ["v0"] ]
],
"dependencies": {}
}
📚 Add Libraries File
Create a file called
DHT sensor library
Adafruit BMP280 Library
Adafruit SSD1306
Adafruit GFX Library
Wokwi auto-downloads all libraries when you start the simulation.
▶ Paste Code & Press Play
- Click sketch.ino, delete default code, paste the complete code from Section 04
- Press the green ▶ Play button
- Watch the Serial Monitor — it will print the ESP32's local IP address
- Click the IP address link in Wokwi's Serial Monitor to open the live web dashboard!
The WiFi SSID in the code is
Open ESP32 Simulation on Wokwi
Create a new ESP32 project and paste the files above, or explore existing ESP32 projects in the Wokwi gallery.
Open Wokwi — ESP32Complete Arduino Code — Explained
Step 4Paste this into sketch.ino in Wokwi. The code is organized into logical sections with explanations.
① Includes, Pins, WiFi & ThingSpeak Config
/*
* ESP32 IoT Weather Station with Cloud Integration
* MakeMindz.com | Wokwi ESP32 Simulation
* Sensors: DHT22, BMP280, Light (LDR), Rain
* Outputs: OLED display, Web Dashboard, ThingSpeak Cloud
*/
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BMP280.h>
// ── WiFi (Wokwi-GUEST = Wokwi's built-in simulated network) ──
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// ── ThingSpeak Cloud ──────────────────────────────────────────
const char* thingSpeakServer = "api.thingspeak.com";
String thingSpeakAPIKey = "YOUR_API_KEY_HERE"; // ← Replace with yours
const long thingSpeakChannel = 123456; // ← Replace with your channel ID
// ── Pin Definitions ───────────────────────────────────────────
#define DHT_PIN 15
#define LDR_PIN 34 // Analog — light sensor
#define RAIN_PIN 35 // Analog — rain sensor
#define LED_PIN 2 // Built-in LED (blue)
#define DHT_TYPE DHT22
// ── Library Objects ───────────────────────────────────────────
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_BMP280 bmp;
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
WebServer server(80);
// ── Weather Data Structure ────────────────────────────────────
struct WeatherData {
float temperature, humidity, pressure, altitude;
int lightLevel, rainLevel;
String weatherCondition;
unsigned long timestamp;
} currentWeather;
// ── Timing (non-blocking with millis()) ───────────────────────
unsigned long lastSensorRead = 0;
unsigned long lastCloudUpload = 0;
const long sensorInterval = 2000; // Read every 2s
const long cloudInterval = 20000; // Upload every 20s
// ── History (last 50 readings) ────────────────────────────────
const int maxPoints = 50;
float tempHistory[50], humidHistory[50];
int dataIndex = 0;
bool cloudConnected = false;
int uploadCount = 0;
② setup() — Boot, WiFi & Sensor Init
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// DHT22
dht.begin();
// BMP280 — I2C address 0x76
if (!bmp.begin(0x76)) {
Serial.println("BMP280 not found!");
} else {
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
Adafruit_BMP280::SAMPLING_X2,
Adafruit_BMP280::SAMPLING_X16,
Adafruit_BMP280::FILTER_X16,
Adafruit_BMP280::STANDBY_MS_500);
}
// OLED — I2C address 0x3C
if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
display.clearDisplay();
display.setTextSize(1); display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Weather Station");
display.println("Initializing...");
display.display(); delay(2000);
}
connectWiFi(); // Connects to Wokwi-GUEST
// Register web server routes
server.on("/", handleRoot);
server.on("/data", handleData);
server.on("/history", handleHistory);
server.on("/api/weather", handleAPIWeather);
server.begin();
readSensors();
updateDisplay();
Serial.println("=== Weather Station Ready ===");
}
void connectWiFi() {
WiFi.begin(ssid, password);
display.clearDisplay(); display.setCursor(0,0);
display.println("Connecting WiFi..."); display.display();
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500); Serial.print("."); attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
display.clearDisplay(); display.setCursor(0,0);
display.println("WiFi Connected!");
display.print("IP: "); display.println(WiFi.localIP());
display.display(); delay(2000);
}
}
③ loop(), Sensor Reading & Cloud Upload
void loop() {
server.handleClient();
unsigned long now = millis();
// Read all sensors every 2 seconds
if (now - lastSensorRead >= sensorInterval) {
lastSensorRead = now;
readSensors();
updateDisplay();
blinkLED();
}
// Upload to ThingSpeak every 20 seconds
if (now - lastCloudUpload >= cloudInterval) {
lastCloudUpload = now;
uploadToCloud();
}
}
void readSensors() {
currentWeather.temperature = dht.readTemperature();
currentWeather.humidity = dht.readHumidity();
currentWeather.pressure = bmp.readPressure() / 100.0F; // Pa → hPa
currentWeather.altitude = bmp.readAltitude(1013.25);
int ldrRaw = analogRead(LDR_PIN);
int rainRaw = analogRead(RAIN_PIN);
currentWeather.lightLevel = map(ldrRaw, 0, 4095, 0, 100);
currentWeather.rainLevel = map(rainRaw, 0, 4095, 100, 0); // Inverted
currentWeather.timestamp = millis();
currentWeather.weatherCondition = determineCondition();
if (isnan(currentWeather.temperature) || isnan(currentWeather.humidity)) {
currentWeather.temperature = currentWeather.humidity = 0;
}
tempHistory[dataIndex] = currentWeather.temperature;
humidHistory[dataIndex] = currentWeather.humidity;
dataIndex = (dataIndex + 1) % maxPoints;
}
String determineCondition() {
if (currentWeather.rainLevel > 60) return "Rainy";
else if (currentWeather.rainLevel > 30) return "Drizzle";
else if (currentWeather.humidity > 80) return "Humid";
else if (currentWeather.lightLevel < 20) return "Cloudy";
else if (currentWeather.lightLevel > 80) return "Sunny";
else if (currentWeather.temperature > 30) return "Hot";
else if (currentWeather.temperature < 15) return "Cold";
else return "Clear";
}
void uploadToCloud() {
if (WiFi.status() != WL_CONNECTED) { cloudConnected = false; return; }
HTTPClient http;
String url = "http://" + String(thingSpeakServer) + "/update?api_key=" + thingSpeakAPIKey;
url += "&field1=" + String(currentWeather.temperature);
url += "&field2=" + String(currentWeather.humidity);
url += "&field3=" + String(currentWeather.pressure);
url += "&field4=" + String(currentWeather.lightLevel);
url += "&field5=" + String(currentWeather.rainLevel);
url += "&field6=" + String(currentWeather.altitude);
http.begin(url);
int code = http.GET();
if (code == 200) { cloudConnected = true; uploadCount++; Serial.println("✓ Cloud upload OK"); }
http.end();
}
void updateDisplay() {
display.clearDisplay();
display.setTextSize(1); display.setCursor(0, 0);
display.println("WEATHER STATION");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);
display.setCursor(0, 12); display.print("Temp: "); display.print(currentWeather.temperature, 1); display.println(" C");
display.setCursor(0, 22); display.print("Hum: "); display.print(currentWeather.humidity, 1); display.println(" %");
display.setCursor(0, 32); display.print("Press:"); display.print(currentWeather.pressure, 0); display.println("hPa");
display.setCursor(0, 42); display.print("Light:"); display.print(currentWeather.lightLevel); display.println("%");
display.setCursor(0, 52); display.print("Status:"); display.println(currentWeather.weatherCondition);
display.display();
}
void blinkLED() { digitalWrite(LED_PIN, HIGH); delay(50); digitalWrite(LED_PIN, LOW); }
④ Web Server Handlers — Dashboard & JSON API
// ── Main Dashboard (/) — gradient card UI, auto-refresh 5s ──
void handleRoot() {
String html = "<!DOCTYPE html><html><head>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<meta http-equiv='refresh' content='5'>" // Auto-refresh every 5s
"<style>body{font-family:Arial;background:linear-gradient(135deg,#667eea,#764ba2);"
"margin:0;padding:20px;color:#fff}.container{max-width:900px;margin:0 auto}"
"h1{text-align:center;font-size:2.2em;margin-bottom:8px}"
".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:20px 0}"
".card{background:rgba(255,255,255,.15);backdrop-filter:blur(10px);padding:20px;"
"border-radius:14px;border:1px solid rgba(255,255,255,.2)}"
".value{font-size:2.4em;font-weight:700;margin:8px 0}"
".icon{font-size:2em}.status{text-align:center;margin-top:16px;"
"background:rgba(255,255,255,.15);padding:14px;border-radius:10px}"
"@media(max-width:600px){.grid{grid-template-columns:1fr}}</style></head><body>";
html += "<div class='container'><h1>🌤️ IoT Weather Station</h1>";
html += "<p style='text-align:center;opacity:.8'>Auto-refreshes every 5 seconds</p>";
html += "<div class='card' style='text-align:center;font-size:1.5em;margin-bottom:16px'>"
+ getWeatherIcon() + " <strong>" + currentWeather.weatherCondition + "</strong></div>";
html += "<div class='grid'>";
// 6 sensor cards
String sensors[6][3] = {
{"🌡️","Temperature", String(currentWeather.temperature,1)+" °C"},
{"💧","Humidity", String(currentWeather.humidity,1)+" %"},
{"🎚️","Pressure", String(currentWeather.pressure,0)+" hPa"},
{"⛰️","Altitude", String(currentWeather.altitude,0)+" m"},
{"☀️","Light Level", String(currentWeather.lightLevel)+" %"},
{"🌧️","Rain Level", String(currentWeather.rainLevel)+" %"}
};
for (int i=0; i<6; i++) {
html += "<div class='card'><div class='icon'>"+sensors[i][0]+"</div>"
+ "<h3 style='opacity:.8'>"+sensors[i][1]+"</h3>"
+ "<div class='value'>"+sensors[i][2]+"</div></div>";
}
html += "</div><div class='status'>";
html += cloudConnected ? "<span style='color:#4ade80'>☁️ Cloud Connected</span>"
: "<span style='color:#f87171'>☁️ Cloud Offline</span>";
html += " · WiFi: " + WiFi.localIP().toString()
+ " · Uploads: " + String(uploadCount)
+ " · Uptime: " + String(millis()/1000) + "s</div></div></body></html>";
server.send(200, "text/html", html);
}
String getWeatherIcon() {
if (currentWeather.weatherCondition == "Rainy") return "🌧️";
if (currentWeather.weatherCondition == "Drizzle") return "🌦️";
if (currentWeather.weatherCondition == "Sunny") return "☀️";
if (currentWeather.weatherCondition == "Cloudy") return "☁️";
if (currentWeather.weatherCondition == "Hot") return "🔥";
if (currentWeather.weatherCondition == "Cold") return "❄️";
return "🌤️";
}
// ── JSON API endpoints ──────────────────────────────────────
void handleData() {
String json = "{";
json += "\"temperature\":" + String(currentWeather.temperature,2) + ",";
json += "\"humidity\":" + String(currentWeather.humidity,2) + ",";
json += "\"pressure\":" + String(currentWeather.pressure,2) + ",";
json += "\"altitude\":" + String(currentWeather.altitude,2) + ",";
json += "\"light\":" + String(currentWeather.lightLevel) + ",";
json += "\"rain\":" + String(currentWeather.rainLevel) + ",";
json += "\"condition\":\"" + currentWeather.weatherCondition + "\",";
json += "\"cloudConnected\":" + String(cloudConnected ? "true" : "false") + "}";
server.send(200, "application/json", json);
}
void handleHistory() {
String json = "{\"temperature\":[";
for (int i=0; i<maxPoints; i++) { json += String(tempHistory[i],1); if(i<maxPoints-1) json+=","; }
json += "],\"humidity\":[";
for (int i=0; i<maxPoints; i++) { json += String(humidHistory[i],1); if(i<maxPoints-1) json+=","; }
json += "]}";
server.send(200, "application/json", json);
}
void handleAPIWeather() { handleData(); }
ThingSpeak Cloud Setup
Optional but PowerfulThingSpeak is a free IoT platform by MathWorks. It lets you collect, visualize, and analyze sensor data from anywhere in the world using MATLAB-powered charts.
- 1Create ThingSpeak account — go to thingspeak.com → click "Get Started For Free" → verify email
- 2Create a new Channel — Channels → My Channels → New Channel. Name it "ESP32 Weather Station"
- 3Enable 6 fields: Field 1: Temperature · Field 2: Humidity · Field 3: Pressure · Field 4: Light Level · Field 5: Rain Level · Field 6: Altitude → Click "Save Channel"
- 4Get your Write API Key — click the "API Keys" tab on your channel page. Copy the Write API Key (looks like:
XXXXXXXXXXXXXXXX ) - 5Update the code — replace the two lines in Part 1 of the code:
// Replace these two lines with your real ThingSpeak credentials:
String thingSpeakAPIKey = "XXXXXXXXXXXXXXXX"; // ← Your Write API Key
const long thingSpeakChannel = 123456; // ← Your Channel ID number
- 6Restart the simulation — open Serial Monitor. After Wi-Fi connects, look for:
✓ Cloud upload OK appearing every 20 seconds - 7View your charts — go to ThingSpeak → Channels → My Channels → ESP32 Weather Station → you'll see live data charts for all 6 fields!
ThingSpeak free plan allows 3 million messages per year (≈8,200/day). Uploading every 20 seconds = 4,320 messages/day — well within the free limit!
Interactive Testing Guide
The Fun Part!Press Play & Watch Serial Monitor
After pressing ▶ in Wokwi, the Serial Monitor will show boot messages, then print the local IP address once Wi-Fi connects. Click that IP address to open the live web dashboard!
Compile error? Check that your
Adjust DHT22 Temperature & Humidity
- Click the DHT22 sensor in Wokwi — a popup shows sliders
- Set temp to 35°C → condition changes to "Hot" on OLED and dashboard
- Set temp to 10°C → condition changes to "Cold" + ❄️ icon
- Set humidity to 85% → condition becomes "Humid"
Rotate Light & Rain Sensor Knobs
- Rotate Light Sensor knob fully clockwise → "Sunny" ☀️
- Rotate Light Sensor knob fully counter-clockwise → "Cloudy" ☁️
- Rotate Rain Sensor knob to max → "Rainy" 🌧️ + watch rain % climb
- Rotate partially → "Drizzle" 🌦️ at medium rain level
Explore the JSON API
The ESP32 serves 3 API endpoints you can call from the browser:
/data — current sensor readings as JSON/history — last 50 temperature + humidity readings/api/weather — same as /data (RESTful alias)
Access these by appending to the IP address: e.g.
Comments
Post a Comment