Mirko Pavleski
Published © GPL3+

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

BeginnerFull instructions provided2 hours89
DIY 5-Day Rainfall Forecast Device - ESP32 E-Paper Project

Things used in this project

Hardware components

Elecrow CrowPanel ESP32 4.2” E-paper Display module with built-in ESP32S3 MCU
×1
Polymer Lithium Ion Battery - 2200mAh 3.7V
Seeed Studio Polymer Lithium Ion Battery - 2200mAh 3.7V
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

Schematic

...

Code

Code

C/C++
....
// ESP32 + E-paper weather forecast display with inversion feature by mircemk, June 2025

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClient.h>
#include <ArduinoJson.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <time.h>

// Network and API Configuration
const int MAX_NETWORKS = 2;
const char* ssid[MAX_NETWORKS] = {"*****", "*****"};
const char* password[MAX_NETWORKS] = {"*****", "*****"};
const char* apiKey = "*****";
const float latitude = 41.117199;  // for Ohrid
const float longitude = 20.801901; // for Ohrid

// Pin Definitions
#define PWR 7
#define BUSY 48
#define RES 47
#define DC 46
#define CS 45
#define BUTTON_PIN 2  // New: Button for display inversion

// Global Variables
RTC_DATA_ATTR bool rtcInvertDisplay = false;  // Persists across deep sleep
bool invertDisplay = false;  // Current display state

// Display Configuration
GxEPD2_BW<GxEPD2_420_GYE042A87, GxEPD2_420_GYE042A87::HEIGHT> epd(GxEPD2_420_GYE042A87(CS, DC, RES, BUSY));

const int screenW = 400, screenH = 300;
const int graphBottom = 278, graphTop = 160, graphHeight = graphBottom - graphTop;
const int todayTop = 20, todayBottom = 138, todayHeight = todayBottom - todayTop;
const int todayWidth = screenW / 2 - 40, todayX = screenW - todayWidth - 8;

const int currentWeatherLeft = 7;
const int currentWeatherRight = todayX - 19;
const int currentWeatherWidth = currentWeatherRight - currentWeatherLeft;
const int currentWeatherTop = todayTop;
const int currentWeatherHeight = todayHeight;

// Weather Data Storage
int lastUpdateHour = -1;
String currentWeatherDesc = "";
float currentTemp = 0.0;
float currentPressure = 0.0;
int currentHumidity = 0;
int currentDayIndex = 0;

String daysOfWeek[7] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
float hourlyRain[24] = {0};
float dailyRain[6][8] = {0};
int dailyPop[6] = {0};

bool connectToWiFi() {
  for (int i = 0; i < MAX_NETWORKS; i++) {
    Serial.printf("Trying WiFi %d/%d: %s\n", i+1, MAX_NETWORKS, ssid[i]);
    WiFi.begin(ssid[i], password[i]);
    
    unsigned long start = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
      delay(250);
      Serial.print(".");
    }
    
    if (WiFi.status() == WL_CONNECTED) {
      Serial.printf("\nConnected to %s\n", ssid[i]);
      Serial.print("IP Address: ");
      Serial.println(WiFi.localIP());
      return true;
    }
    Serial.println("\nConnection failed");
    WiFi.disconnect();
    delay(1000);
  }
  return false;
}

void epdPower(int state) {
  pinMode(PWR, OUTPUT);
  digitalWrite(PWR, state);
}

void epdInit() {
  epd.init(115200, true, 50, false);
  epd.setRotation(0);
  epd.setTextColor(invertDisplay ? GxEPD_WHITE : GxEPD_BLACK);
  epd.setFont(&FreeMonoBold9pt7b);
  epd.setFullWindow();
  Serial.println("E-paper initialized");
}

int currentHour() {
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) return timeinfo.tm_hour;
  return 0;
}

void drawCurrentWeatherBox(String weatherDesc, float temperature, float pressure, int humidity, int updateHour) {
  uint16_t fgColor = invertDisplay ? GxEPD_WHITE : GxEPD_BLACK;
  epd.setTextColor(fgColor);
  
  // Draw the box
  epd.drawRect(currentWeatherLeft, currentWeatherTop, currentWeatherWidth, currentWeatherHeight, fgColor);
  epd.drawRect(currentWeatherLeft+1, currentWeatherTop+1, currentWeatherWidth-2, currentWeatherHeight-2, fgColor);
  
  // Draw label
  epd.setCursor(currentWeatherLeft + 5, currentWeatherTop - 5);
  epd.print("Current Weather");

  // Draw divider lines (dashed)
  for (int i = 1; i <= 3; i++) {
    int y = currentWeatherTop + i * currentWeatherHeight / 4;
    for (int x = currentWeatherLeft + 1; x < currentWeatherRight - 1; x += 4) {
      epd.drawPixel(x, y, fgColor);
      epd.drawPixel(x+1, y, fgColor);
    }
  }

  // Calculate text positions
  int rowHeight = currentWeatherHeight / 4;
  int textYOffset = rowHeight / 2 + 5;

  // Row 1: Weather description
  epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 0 + textYOffset);
  epd.print(weatherDesc);

  // Row 2: Temperature
  epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 1 + textYOffset);
  epd.print("Temp: ");
  epd.print(temperature, 1);
  epd.print(" °C");

  // Row 3: Pressure
  epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 2 + textYOffset);
  epd.print("Press: ");
  epd.print(pressure, 1);
  epd.print(" hPa");

  // Row 4: Humidity and Update time
  epd.setCursor(currentWeatherLeft + 5, currentWeatherTop + rowHeight * 3 + textYOffset);
  epd.print("Humid: ");
  epd.print(humidity);
  epd.print(" %");
  
  char updateStr[10];
  sprintf(updateStr, "UT %d", updateHour);
  int textWidth = 6 * strlen(updateStr);
  epd.setCursor(currentWeatherRight - textWidth - 35, currentWeatherTop + rowHeight * 3 + textYOffset);
  epd.print(updateStr);
}

void drawTodayBox(int currentHour) {
  uint16_t fgColor = invertDisplay ? GxEPD_WHITE : GxEPD_BLACK;
  epd.setTextColor(fgColor);

  epd.setCursor(todayX + 5, todayTop - 5);
  epd.print(daysOfWeek[currentDayIndex]);

  // Show POP percentage
  epd.setCursor(todayX + todayWidth - 90, todayTop - 5);
  char popStr[10];
  sprintf(popStr, "POP %d%%", dailyPop[0]);
  epd.print(popStr);

  epd.drawRect(todayX, todayTop, todayWidth, todayHeight, fgColor);
  epd.drawRect(todayX + 1, todayTop + 1, todayWidth - 2, todayHeight - 2, fgColor);

  // Draw hour markers
  for (int h = 6; h <= 18; h += 6) {
    int x = todayX + map(h, 0, 24, 4, todayWidth - 4);
    for (int y = todayTop; y < todayBottom; y += 4)
      epd.drawPixel(x, y, fgColor);
  }

  // Draw horizontal grid lines
  for (int i = 1; i <= 3; i++) {
    int y = todayTop + i * todayHeight / 4;
    for (int x = todayX + 1; x < todayX + todayWidth - 1; x += 4)
      epd.drawPixel(x, y, fgColor);
  }

  float maxRain = 0.1;
  for (int i = currentHour; i < 24; i++)
    if (hourlyRain[i] > maxRain) maxRain = hourlyRain[i];
  float roundedMax = getRoundedMax(maxRain);

  epd.setCursor(todayX - 15, todayTop + 10);
  epd.print((int)(roundedMax));

  epd.setCursor(todayX - 15, todayBottom);
  epd.print("0");

  bool hasRain = false;
  for (int h = currentHour; h < 24; h++) {
    int barHeight = map(hourlyRain[h] * 10, 0, roundedMax * 10, 0, todayHeight - 5);
    if (barHeight > 0) {
      hasRain = true;
      int x = todayX + map(h, 0, 24, 4, todayWidth - 4);
      
   //   for (int w = 0; w < 10; w++) {
      for (int w = 0; w < 15; w++) {   // So Podebeli Barovi

        
        if (x + w < todayX + todayWidth - 1)
          epd.drawFastVLine(x + w, todayBottom - barHeight, barHeight, fgColor);
      }
    }
  }

  if (!hasRain) {
    int midX = todayX + todayWidth / 2 - 10;
    int midY = todayTop + todayHeight / 2;
    epd.setCursor(midX, midY - 5);
    epd.print("NO");
    epd.setCursor(midX-12, midY + 12);
    epd.print("RAIN");
  }
}

void drawWeekBoxes() {
  uint16_t fgColor = invertDisplay ? GxEPD_WHITE : GxEPD_BLACK;
  epd.setTextColor(fgColor);
  
  float maxRain = 0.1;
  for (int d = 1; d <= 5; d++) {
    for (int i = 0; i < 8; i++) {
      if (dailyRain[d][i] > maxRain) maxRain = dailyRain[d][i];
    }
  }
  
  float roundedMax = getRoundedMax(maxRain);
  Serial.print("Rounded max rain: "); Serial.println(roundedMax);

  int rectW = 70, gap = 6, startX = 19;
  const int topPadding = 5;
  const int usableGraphHeight = graphHeight - topPadding;

  // Only draw numerical labels without grid lines
  epd.setCursor(4, graphBottom);
  epd.print("0");
  epd.setCursor(4, graphTop + topPadding + 8);
  epd.print((int)roundedMax);

  // Draw boxes for next 5 days
  for (int d = 1; d <= 5; d++) {
    int boxIndex = d - 1;
    int x = startX + boxIndex * (rectW + gap);
    
    // Draw box outline
    epd.drawRect(x, graphTop, rectW, graphHeight, fgColor);
    epd.drawRect(x + 1, graphTop + 1, rectW - 2, graphHeight - 2, fgColor);

    // Day label
    epd.setCursor(x + 15, graphTop - 7);
    epd.print(daysOfWeek[(currentDayIndex + d) % 7]);

    // Vertical center line
    for (int y = graphTop; y < graphBottom; y += 4)
      epd.drawPixel(x + rectW / 2, y, fgColor);

    // Internal horizontal grid lines - only within each box
    for (int j = 1; j <= 3; j++) {
      int y = graphTop + j * graphHeight / 4;
      for (int i = x + 1; i < x + rectW - 1; i += 4)
        epd.drawPixel(i, y, fgColor);
    }

    // Draw rain bars
    bool hasRain = false;
    for (int i = 0; i < 8; i++) {
      float rainVal = dailyRain[d][i];
      int barHeight = map(rainVal * 10, 0, roundedMax * 10, 0, usableGraphHeight);
      if (barHeight > 0) {
        hasRain = true;
        int barX = x + 5 + i * 7;
        for (int w = 0; w < 5; w++)
          epd.drawFastVLine(barX + w, graphBottom - barHeight, barHeight, fgColor);
      }
    }

    // Show POP percentage
    char popStr[6];
    sprintf(popStr, "%d%%", dailyPop[d]);
    epd.setCursor(x + 20, graphBottom + 15);
    epd.print(popStr);

    // "NO RAIN" text if applicable
    if (!hasRain) {
      int midX = x + rectW / 2 - 10;
      int midY = graphTop + graphHeight / 2;
      epd.setCursor(midX, midY - 5);
      epd.print("NO");
      epd.setCursor(midX-12, midY + 12);
      epd.print("RAIN");
    }
  }
}

float getRoundedMax(float maxRain) {
  if (maxRain <= 0) return 1.0;
  if (maxRain <= 1.0) return 1.0;
  return ceil(maxRain);
}

bool fetchCurrentWeather(String &weatherDesc, float &temperature, float &pressure, int &humidity, int &updateHour) {
  WiFiClient client;
  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/weather?lat=" + String(latitude, 6) +
               "&lon=" + String(longitude, 6) + "&units=metric&appid=" + apiKey;
  
  http.begin(client, url);
  int httpCode = http.GET();

  if (httpCode == 200) {
    String payload = http.getString();
    DynamicJsonDocument doc(1024);
    DeserializationError error = deserializeJson(doc, payload);
    
    if (!error) {
      weatherDesc = doc["weather"][0]["description"].as<String>();
      weatherDesc.setCharAt(0, toupper(weatherDesc[0]));
      
      temperature = doc["main"]["temp"].as<float>();
      pressure = doc["main"]["pressure"].as<float>();
      humidity = doc["main"]["humidity"].as<int>();
      
      time_t updateTime = doc["dt"].as<time_t>();
      updateTime += 0;
      struct tm *timeinfo = localtime(&updateTime);
      updateHour = timeinfo->tm_hour;
      
      return true;
    }
  }
  http.end();
  return false;
}


void fetchForecastData() {
  Serial.println("Fetching forecast data...");
  WiFiClient client;
  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/forecast?lat=" + String(latitude, 6) +
               "&lon=" + String(longitude, 6) + "&units=metric&appid=" + apiKey;
  
  http.begin(client, url);
  int httpCode = http.GET();

  if (httpCode == 200) {
    String payload = http.getString();
    DynamicJsonDocument doc(50000);
    DeserializationError error = deserializeJson(doc, payload);
    
    if (!error) {
      JsonArray list = doc["list"];
      
      for (int d = 0; d < 6; d++) {
        dailyPop[d] = 0;
      }
      
      for (int i = 0; i < list.size(); i++) {
        JsonObject entry = list[i];
        const char* dt_txt = entry["dt_txt"];
        struct tm tm;
        strptime(dt_txt, "%Y-%m-%d %H:%M:%S", &tm);
        int dayIndex = (tm.tm_wday == 0 ? 6 : tm.tm_wday - 1);
        int dayOffset = (dayIndex - currentDayIndex + 7) % 7;

        float rain = 0.0;
        if (entry.containsKey("rain") && entry["rain"].containsKey("3h")) {
          rain = entry["rain"]["3h"].as<float>();
        }

        int pop = int(entry["pop"].as<float>() * 100);

        if (dayOffset == 0) {
          if (tm.tm_hour < 24) {
            hourlyRain[tm.tm_hour] = rain;
          }
          if (pop > dailyPop[0]) {
            dailyPop[0] = pop;
          }
        }
        else if (dayOffset >= 1 && dayOffset <= 5) {
          int slot = tm.tm_hour / 3;
          if (slot < 8) {
            dailyRain[dayOffset][slot] = rain;
            if (pop > dailyPop[dayOffset]) {
              dailyPop[dayOffset] = pop;
            }
          }
        }
      }

    }

  }
  http.end();
}


void syncTime() {
  Serial.println("Syncing time...");
  configTime(7200, 0, "pool.ntp.org");
  struct tm timeinfo;
  while (!getLocalTime(&timeinfo)) delay(100);
  currentDayIndex = timeinfo.tm_wday == 0 ? 6 : timeinfo.tm_wday - 1;
  Serial.print("Current hour: "); Serial.println(timeinfo.tm_hour);
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Weather Display Booting...");

  // Setup button
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  
  // Load inversion state from RTC memory
  invertDisplay = rtcInvertDisplay;
  
  // Check button state during boot
  if (digitalRead(BUTTON_PIN) == LOW) {
    invertDisplay = !invertDisplay;
    rtcInvertDisplay = invertDisplay;  // Save to RTC memory
    delay(100);  // Debounce
  }

  // Initialize display
  epdPower(HIGH);
  epdInit();

  // Attempt WiFi connection
  bool wifiConnected = connectToWiFi();

  if (!wifiConnected) {
    Serial.println("Failed to connect to any network");
    currentWeatherDesc = "Offline";
    currentTemp = 0.0;
    currentPressure = 0.0;
    currentHumidity = 0;
    
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
      lastUpdateHour = timeinfo.tm_hour;
    } else {
      lastUpdateHour = 0;
    }
  } else {
    syncTime();
    fetchForecastData();
    
    String weatherDesc;
    float temperature, pressure;
    int humidity, updateHour;
    if (fetchCurrentWeather(weatherDesc, temperature, pressure, humidity, updateHour)) {
      currentWeatherDesc = weatherDesc;
      currentTemp = temperature;
      currentPressure = pressure;
      currentHumidity = humidity;
      lastUpdateHour = updateHour;
    }
  }

  // Draw display
  Serial.println("Drawing to e-paper...");
  epd.fillScreen(invertDisplay ? GxEPD_BLACK : GxEPD_WHITE);
  epd.drawRect(0, 0, screenW, screenH, invertDisplay ? GxEPD_WHITE : GxEPD_BLACK);
  
  drawCurrentWeatherBox(currentWeatherDesc, currentTemp, currentPressure, currentHumidity, lastUpdateHour);
  drawTodayBox(currentHour());
  drawWeekBoxes();
  
  epd.display();
  epd.hibernate();
  epdPower(LOW);
 

  Serial.println("Entering deep sleep...");
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_2, 0); // Wake on button press
  esp_sleep_enable_timer_wakeup(900LL * 1000000); // 15 min
  esp_deep_sleep_start();
}

void loop() {
  // Empty - device will be in deep sleep
} 

Credits

Mirko Pavleski
202 projects • 1511 followers

Comments