DIY 5-Day Rainfall Forecast Device - ESP32 E-Paper Project
How to make 5-day precipitation forecast device with probability percentages and Current weather conditions (temp/humidity/pressure)
Devices & Components
1
Lithium Ion Polymer Battery - 3.7v 500mAh
1
CrowPanel ESP32 4.2inch E-paper HMI Display with 400*300 Resolution
Hardware & Tools
1
Soldering Iron Kit
Software & Tools
Arduino IDE
Project description
Code
Code
cpp
...
1// ESP32 + E-paper weather forecast display with inversion feature by mircemk, June 2025 2 3#include <WiFi.h> 4#include <HTTPClient.h> 5#include <WiFiClient.h> 6#include <ArduinoJson.h> 7#include <GxEPD2_BW.h> 8#include <Fonts/FreeMonoBold9pt7b.h> 9#include <time.h> 10 11// Network and API Configuration 12const int MAX_NETWORKS = 2; 13const char* ssid[MAX_NETWORKS] = {"*****", "*****"}; 14const char* password[MAX_NETWORKS] = {"*****", "*****"}; 15const char* apiKey = "*****"; 16const float latitude = 41.117199; // for Ohrid 17const float longitude = 20.801901; // for Ohrid 18 19// Pin Definitions 20#define PWR 7 21#define BUSY 48 22#define RES 47 23#define DC 46 24#define CS 45 25#define BUTTON_PIN 2 // New: Button for display inversion 26 27// Global Variables 28RTC_DATA_ATTR bool rtcInvertDisplay = false; // Persists across deep sleep 29bool invertDisplay = false; // Current display state 30 31// Display Configuration 32GxEPD2_BW<GxEPD2_420_GYE042A87, GxEPD2_420_GYE042A87::HEIGHT> epd(GxEPD2_420_GYE042A87(CS, DC, RES, BUSY)); 33 34const int screenW = 400, screenH = 300; 35const int graphBottom = 278, graphTop = 160, graphHeight = graphBottom - graphTop; 36const int todayTop = 20, todayBottom = 138, todayHeight = todayBottom - todayTop; 37const int todayWidth = screenW / 2 - 40, todayX = screenW - todayWidth - 8; 38 39const int currentWeatherLeft = 7; 40const int currentWeatherRight = todayX - 19; 41const int currentWeatherWidth = currentWeatherRight - currentWeatherLeft; 42const int currentWeatherTop = todayTop; 43const int currentWeatherHeight = todayHeight; 44 45// Weather Data Storage 46int lastUpdateHour = -1; 47String currentWeatherDesc = ""; 48float currentTemp = 0.0; 49float currentPressure = 0.0; 50int currentHumidity = 0; 51int currentDayIndex = 0; 52 53String daysOfWeek[7] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 54float hourlyRain[24] = {0}; 55float dailyRain[6][8] = {0}; 56int dailyPop[6] = {0}; 57 58bool connectToWiFi() { 59 for (int i = 0; i < MAX_NETWORKS; i++) { 60 Serial.printf("Trying WiFi %d/%d: %s\n", i+1, MAX_NETWORKS, ssid[i]); 61 WiFi.begin(ssid[i], password[i]); 62 63 unsigned long start = millis(); 64 while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { 65 delay(250); 66 Serial.print("."); 67 } 68 69 if (WiFi.status() == WL_CONNECTED) { 70 Serial.printf("\nConnected to %s\n", ssid[i]); 71 Serial.print("IP Address: "); 72 Serial.println(WiFi.localIP()); 73 return true; 74 } 75 Serial.println("\nConnection failed"); 76 WiFi.disconnect(); 77 delay(1000); 78 } 79 return false; 80} 81 82void epdPower(int state) { 83 pinMode(PWR, OUTPUT); 84 digitalWrite(PWR, state); 85} 86 87void epdInit() { 88 epd.init(115200, true, 50, false); 89 epd.setRotation(0); 90 epd.setTextColor(invertDisplay ? GxEPD_WHITE : GxEPD_BLACK); 91 epd.setFont(&FreeMonoBold9pt7b); 92 epd.setFullWindow(); 93 Serial.println("E-paper initialized"); 94} 95 96int currentHour() { 97 struct tm timeinfo; 98 if (getLocalTime(&timeinfo)) return timeinfo.tm_hour; 99 return 0; 100} 101 102void drawCurrentWeatherBox(String weatherDesc, float temperature, float pressure, int humidity, int updateHour) { 103 uint16_t fgColor = invertDisplay ? GxEPD_WHITE : GxEPD_BLACK; 104 epd.setTextColor(fgColor); 105 106 // Draw the box 107 epd.drawRect(currentWeatherLeft, currentWeatherTop, currentWeatherWidth, currentWeatherHeight, fgColor); 108 epd.drawRect(currentWeatherLeft+1, currentWeatherTop+1, currentWeatherWidth-2, currentWeatherHeight-2, fgColor); 109 110 // Draw label 111 epd.setCursor(currentWeatherLeft + 5, currentWeatherTop - 5); 112 epd.print("Current Weather"); 113 114 // Draw divider lines (dashed) 115 for (int i = 1; i <= 3; i++) { 116 int y = currentWeatherTop + i * currentWeatherHeight / 4; 117 for (int x = currentWeatherLeft + 1; x < currentWeatherRight - 1; x += 4) { 118 epd.drawPixel(x, y, fgColor); 119 epd.drawPixel(x+1, y, fgColor); 120 } 121 } 122 123 // Calculate text positions 124 int rowHeight = currentWeatherHeight / 4; 125 int textYOffset = rowHeight / 2 + 5; 126 127 // Row 1: Weather description 128 epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 0 + textYOffset); 129 epd.print(weatherDesc); 130 131 // Row 2: Temperature 132 epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 1 + textYOffset); 133 epd.print("Temp: "); 134 epd.print(temperature, 1); 135 epd.print(" °C"); 136 137 // Row 3: Pressure 138 epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 2 + textYOffset); 139 epd.print("Press: "); 140 epd.print(pressure, 1); 141 epd.print(" hPa"); 142 143 // Row 4: Humidity and Update time 144 epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 3 + textYOffset); 145 epd.print("Humid: "); 146 epd.print(humidity); 147 epd.print(" %"); 148 149 char updateStr[10]; 150 sprintf(updateStr, "UT %d", updateHour); 151 int textWidth = 6 * strlen(updateStr); 152 epd.setCursor(currentWeatherRight - textWidth - 35, currentWeatherTop + rowHeight * 3 + textYOffset); 153 epd.print(updateStr); 154} 155 156void drawTodayBox(int currentHour) { 157 uint16_t fgColor = invertDisplay ? GxEPD_WHITE : GxEPD_BLACK; 158 epd.setTextColor(fgColor); 159 160 epd.setCursor(todayX + 5, todayTop - 5); 161 epd.print(daysOfWeek[currentDayIndex]); 162 163 // Show POP percentage 164 epd.setCursor(todayX + todayWidth - 90, todayTop - 5); 165 char popStr[10]; 166 sprintf(popStr, "POP %d%%", dailyPop[0]); 167 epd.print(popStr); 168 169 epd.drawRect(todayX, todayTop, todayWidth, todayHeight, fgColor); 170 epd.drawRect(todayX + 1, todayTop + 1, todayWidth - 2, todayHeight - 2, fgColor); 171 172 // Draw hour markers 173 for (int h = 6; h <= 18; h += 6) { 174 int x = todayX + map(h, 0, 24, 4, todayWidth - 4); 175 for (int y = todayTop; y < todayBottom; y += 4) 176 epd.drawPixel(x, y, fgColor); 177 } 178 179 // Draw horizontal grid lines 180 for (int i = 1; i <= 3; i++) { 181 int y = todayTop + i * todayHeight / 4; 182 for (int x = todayX + 1; x < todayX + todayWidth - 1; x += 4) 183 epd.drawPixel(x, y, fgColor); 184 } 185 186 float maxRain = 0.1; 187 for (int i = currentHour; i < 24; i++) 188 if (hourlyRain[i] > maxRain) maxRain = hourlyRain[i]; 189 float roundedMax = getRoundedMax(maxRain); 190 191 epd.setCursor(todayX - 15, todayTop + 10); 192 epd.print((int)(roundedMax)); 193 194 epd.setCursor(todayX - 15, todayBottom); 195 epd.print("0"); 196 197 bool hasRain = false; 198 for (int h = currentHour; h < 24; h++) { 199 int barHeight = map(hourlyRain[h] * 10, 0, roundedMax * 10, 0, todayHeight - 5); 200 if (barHeight > 0) { 201 hasRain = true; 202 int x = todayX + map(h, 0, 24, 4, todayWidth - 4); 203 204 // for (int w = 0; w < 10; w++) { 205 for (int w = 0; w < 15; w++) { // So Podebeli Barovi 206 207 208 if (x + w < todayX + todayWidth - 1) 209 epd.drawFastVLine(x + w, todayBottom - barHeight, barHeight, fgColor); 210 } 211 } 212 } 213 214 if (!hasRain) { 215 int midX = todayX + todayWidth / 2 - 10; 216 int midY = todayTop + todayHeight / 2; 217 epd.setCursor(midX, midY - 5); 218 epd.print("NO"); 219 epd.setCursor(midX-12, midY + 12); 220 epd.print("RAIN"); 221 } 222} 223 224void drawWeekBoxes() { 225 uint16_t fgColor = invertDisplay ? GxEPD_WHITE : GxEPD_BLACK; 226 epd.setTextColor(fgColor); 227 228 float maxRain = 0.1; 229 for (int d = 1; d <= 5; d++) { 230 for (int i = 0; i < 8; i++) { 231 if (dailyRain[d][i] > maxRain) maxRain = dailyRain[d][i]; 232 } 233 } 234 235 float roundedMax = getRoundedMax(maxRain); 236 Serial.print("Rounded max rain: "); Serial.println(roundedMax); 237 238 int rectW = 70, gap = 6, startX = 19; 239 const int topPadding = 5; 240 const int usableGraphHeight = graphHeight - topPadding; 241 242 // Only draw numerical labels without grid lines 243 epd.setCursor(4, graphBottom); 244 epd.print("0"); 245 epd.setCursor(4, graphTop + topPadding + 8); 246 epd.print((int)roundedMax); 247 248 // Draw boxes for next 5 days 249 for (int d = 1; d <= 5; d++) { 250 int boxIndex = d - 1; 251 int x = startX + boxIndex * (rectW + gap); 252 253 // Draw box outline 254 epd.drawRect(x, graphTop, rectW, graphHeight, fgColor); 255 epd.drawRect(x + 1, graphTop + 1, rectW - 2, graphHeight - 2, fgColor); 256 257 // Day label 258 epd.setCursor(x + 15, graphTop - 7); 259 epd.print(daysOfWeek[(currentDayIndex + d) % 7]); 260 261 // Vertical center line 262 for (int y = graphTop; y < graphBottom; y += 4) 263 epd.drawPixel(x + rectW / 2, y, fgColor); 264 265 // Internal horizontal grid lines - only within each box 266 for (int j = 1; j <= 3; j++) { 267 int y = graphTop + j * graphHeight / 4; 268 for (int i = x + 1; i < x + rectW - 1; i += 4) 269 epd.drawPixel(i, y, fgColor); 270 } 271 272 // Draw rain bars 273 bool hasRain = false; 274 for (int i = 0; i < 8; i++) { 275 float rainVal = dailyRain[d][i]; 276 int barHeight = map(rainVal * 10, 0, roundedMax * 10, 0, usableGraphHeight); 277 if (barHeight > 0) { 278 hasRain = true; 279 int barX = x + 5 + i * 7; 280 for (int w = 0; w < 5; w++) 281 epd.drawFastVLine(barX + w, graphBottom - barHeight, barHeight, fgColor); 282 } 283 } 284 285 // Show POP percentage 286 char popStr[6]; 287 sprintf(popStr, "%d%%", dailyPop[d]); 288 epd.setCursor(x + 20, graphBottom + 15); 289 epd.print(popStr); 290 291 // "NO RAIN" text if applicable 292 if (!hasRain) { 293 int midX = x + rectW / 2 - 10; 294 int midY = graphTop + graphHeight / 2; 295 epd.setCursor(midX, midY - 5); 296 epd.print("NO"); 297 epd.setCursor(midX-12, midY + 12); 298 epd.print("RAIN"); 299 } 300 } 301} 302 303float getRoundedMax(float maxRain) { 304 if (maxRain <= 0) return 1.0; 305 if (maxRain <= 1.0) return 1.0; 306 return ceil(maxRain); 307} 308 309bool fetchCurrentWeather(String &weatherDesc, float &temperature, float &pressure, int &humidity, int &updateHour) { 310 WiFiClient client; 311 HTTPClient http; 312 String url = "http://api.openweathermap.org/data/2.5/weather?lat=" + String(latitude, 6) + 313 "&lon=" + String(longitude, 6) + "&units=metric&appid=" + apiKey; 314 315 http.begin(client, url); 316 int httpCode = http.GET(); 317 318 if (httpCode == 200) { 319 String payload = http.getString(); 320 DynamicJsonDocument doc(1024); 321 DeserializationError error = deserializeJson(doc, payload); 322 323 if (!error) { 324 weatherDesc = doc["weather"][0]["description"].as<String>(); 325 weatherDesc.setCharAt(0, toupper(weatherDesc[0])); 326 327 temperature = doc["main"]["temp"].as<float>(); 328 pressure = doc["main"]["pressure"].as<float>(); 329 humidity = doc["main"]["humidity"].as<int>(); 330 331 time_t updateTime = doc["dt"].as<time_t>(); 332 updateTime += 0; 333 struct tm *timeinfo = localtime(&updateTime); 334 updateHour = timeinfo->tm_hour; 335 336 return true; 337 } 338 } 339 http.end(); 340 return false; 341} 342 343 344void fetchForecastData() { 345 Serial.println("Fetching forecast data..."); 346 WiFiClient client; 347 HTTPClient http; 348 String url = "http://api.openweathermap.org/data/2.5/forecast?lat=" + String(latitude, 6) + 349 "&lon=" + String(longitude, 6) + "&units=metric&appid=" + apiKey; 350 351 http.begin(client, url); 352 int httpCode = http.GET(); 353 354 if (httpCode == 200) { 355 String payload = http.getString(); 356 DynamicJsonDocument doc(50000); 357 DeserializationError error = deserializeJson(doc, payload); 358 359 if (!error) { 360 JsonArray list = doc["list"]; 361 362 for (int d = 0; d < 6; d++) { 363 dailyPop[d] = 0; 364 } 365 366 for (int i = 0; i < list.size(); i++) { 367 JsonObject entry = list[i]; 368 const char* dt_txt = entry["dt_txt"]; 369 struct tm tm; 370 strptime(dt_txt, "%Y-%m-%d %H:%M:%S", &tm); 371 int dayIndex = (tm.tm_wday == 0 ? 6 : tm.tm_wday - 1); 372 int dayOffset = (dayIndex - currentDayIndex + 7) % 7; 373 374 float rain = 0.0; 375 if (entry.containsKey("rain") && entry["rain"].containsKey("3h")) { 376 rain = entry["rain"]["3h"].as<float>(); 377 } 378 379 int pop = int(entry["pop"].as<float>() * 100); 380 381 if (dayOffset == 0) { 382 if (tm.tm_hour < 24) { 383 hourlyRain[tm.tm_hour] = rain; 384 } 385 if (pop > dailyPop[0]) { 386 dailyPop[0] = pop; 387 } 388 } 389 else if (dayOffset >= 1 && dayOffset <= 5) { 390 int slot = tm.tm_hour / 3; 391 if (slot < 8) { 392 dailyRain[dayOffset][slot] = rain; 393 if (pop > dailyPop[dayOffset]) { 394 dailyPop[dayOffset] = pop; 395 } 396 } 397 } 398 } 399 400 } 401 402 } 403 http.end(); 404} 405 406 407void syncTime() { 408 Serial.println("Syncing time..."); 409 configTime(7200, 0, "pool.ntp.org"); 410 struct tm timeinfo; 411 while (!getLocalTime(&timeinfo)) delay(100); 412 currentDayIndex = timeinfo.tm_wday == 0 ? 6 : timeinfo.tm_wday - 1; 413 Serial.print("Current hour: "); Serial.println(timeinfo.tm_hour); 414} 415 416void setup() { 417 Serial.begin(115200); 418 delay(1000); 419 Serial.println("Weather Display Booting..."); 420 421 // Setup button 422 pinMode(BUTTON_PIN, INPUT_PULLUP); 423 424 // Load inversion state from RTC memory 425 invertDisplay = rtcInvertDisplay; 426 427 // Check button state during boot 428 if (digitalRead(BUTTON_PIN) == LOW) { 429 invertDisplay = !invertDisplay; 430 rtcInvertDisplay = invertDisplay; // Save to RTC memory 431 delay(100); // Debounce 432 } 433 434 // Initialize display 435 epdPower(HIGH); 436 epdInit(); 437 438 // Attempt WiFi connection 439 bool wifiConnected = connectToWiFi(); 440 441 if (!wifiConnected) { 442 Serial.println("Failed to connect to any network"); 443 currentWeatherDesc = "Offline"; 444 currentTemp = 0.0; 445 currentPressure = 0.0; 446 currentHumidity = 0; 447 448 struct tm timeinfo; 449 if (getLocalTime(&timeinfo)) { 450 lastUpdateHour = timeinfo.tm_hour; 451 } else { 452 lastUpdateHour = 0; 453 } 454 } else { 455 syncTime(); 456 fetchForecastData(); 457 458 String weatherDesc; 459 float temperature, pressure; 460 int humidity, updateHour; 461 if (fetchCurrentWeather(weatherDesc, temperature, pressure, humidity, updateHour)) { 462 currentWeatherDesc = weatherDesc; 463 currentTemp = temperature; 464 currentPressure = pressure; 465 currentHumidity = humidity; 466 lastUpdateHour = updateHour; 467 } 468 } 469 470 // Draw display 471 Serial.println("Drawing to e-paper..."); 472 epd.fillScreen(invertDisplay ? GxEPD_BLACK : GxEPD_WHITE); 473 epd.drawRect(0, 0, screenW, screenH, invertDisplay ? GxEPD_WHITE : GxEPD_BLACK); 474 475 drawCurrentWeatherBox(currentWeatherDesc, currentTemp, currentPressure, currentHumidity, lastUpdateHour); 476 drawTodayBox(currentHour()); 477 drawWeekBoxes(); 478 479 epd.display(); 480 epd.hibernate(); 481 epdPower(LOW); 482 483 484 Serial.println("Entering deep sleep..."); 485 esp_sleep_enable_ext0_wakeup(GPIO_NUM_2, 0); // Wake on button press 486 esp_sleep_enable_timer_wakeup(900LL * 1000000); // 15 min 487 esp_deep_sleep_start(); 488} 489 490void loop() { 491 // Empty - device will be in deep sleep 492}
Documentation
Schematic
...
Schemaric.jpg

Comments
Only logged in users can leave comments