WeMos D1 R1 LCD Keypad Shield Firmware [DEPRICIATED]

EDIT: THIS SKETCH HAS BEEN DEPRECIATED. Updated Firmware sketch available:
LOLIN/WeMos/NodeMCU D1 R1 LCD Keypad Shield Firmware

Here is the recent code I assembled and wrote, which has been wet tested to be working amicably with the hardware. Posting as a reference if any one needs a well implemented firmware to kickstart any capstone project. I have spent days and nights collecting, polishing and fixing the necessary wisdom together, so that you don't have to do the same, and utilize the time saved in making your project more creative or appealing. Just trying to uphold the people who tirelessly democratize computer education. I merely stand on the shoulder of the giants.

As always, please verify the pins of your respective hardware, and when using Arduino IDE, make sure the selected board is LOLIN/WeMos D1 ESP WROOM 2, which is the WeMos D1 R1 (for Revision 1 i.e. the now retired board version).

/**
 * Wemos D1 R1 Pocket Card LCD Weather Station and World Clock
 * 
 * A weather station firmware for the Wemos D1 R1 board with an LCD keypad shield.
 * Displays weather information for multiple cities using the OpenWeatherMap API,
 * with time synchronization via NTP, animated weather icons, and adaptive brightness control.
 * 
 * Features:
 * - Displays temperature, humidity, pressure, and weather description
 * - Supports multiple cities with navigation via keypad
 * - Animated weather icons (sun, moon, clouds, rain, thunderstorm)
 * - Time display with NTP synchronization and timezone support
 * - Adaptive brightness based on time of day and inactivity
 * - WiFi signal strength indicator
 * - Smooth scrolling for long text displays
 * - Error handling for network and API failures
 * - Memory management and system health monitoring
 * 
 * Hardware Requirements:
 * - Wemos/n NodeMCU D1 R1 (ESP8266-UNO-based)
 * - 16x2 LCD with keypad shield (Arduino)
 * - Internet connection via WiFi

       .________________________________.
       Wemos D1 R1 with LCD Keypad Shield
       ----------------------------------
       | Wemos D1 R1                    |
       |                                |
       |   [3V3] [5V] [GND] [GND] [Vin] |      ___________________________.
       |   [D0]  [D1]  [D2]  [D3]  [D4] |  <-- D0  (GPIO3): Not used      |
       |   [D5]  [D6]  [D7]  [D8]  [D9] |  <-- D1  (GPIO1): Not used      |
       |  [D10] [RXD] [TXD] [GND] [3V3] |  <-- D2  (GPIO16): Not used     |
       |                                |      D3  (GPIO5): LCD Backlight |
       |                                |      D4  (GPIO4): LCD D4        |
       |  [ ] [ ] [ ] [ ] [ ] [ ] [A0]  |      D5  (GPIO14): LCD D5       |
       |--------------------------------|      D6  (GPIO12): LCD D6       |
                                               D7  (GPIO13): LCD D7       |
                                               D8  (GPIO0): LCD RS        |
                                               D9  (GPIO2): LCD EN        |
                                               D10 (GPIO15): Not used     |
                                               A0:  Keypad input          |
                                              |---------------------------|
            ._________________________.
            | 16x2 LCD Keypad Shield  |
            |-------------------------|
            | [LCD 16x2 Display]      |
            | [RS] -> D8 (GPIO0)      |
            | [EN] -> D9 (GPIO2)      |
            | [D4] -> D4 (GPIO4)      |
            | [D5] -> D5 (GPIO14)     |
            | [D6] -> D6 (GPIO12)     |
            | [D7] -> D7 (GPIO13)     |
            | [BL] -> D3 (GPIO5)      |
            | [Keypad] -> A0 (ADC)    |
            |-------------------------|

 * 
 * Dependencies:
 * - ESP8266WiFi[](https://github.com/esp8266/Arduino) - WiFi functionality
 * - ESP8266HTTPClient[](https://github.com/esp8266/Arduino) - HTTP requests
 * - LiquidCrystal (Arduino core library) - LCD display control
 * - ArduinoJson[](https://arduinojson.org/) - JSON parsing for API responses
 * - WiFiUdp (Arduino core library) - UDP for NTP
 * - NTPClient[](https://github.com/arduino-libraries/NTPClient) - NTP time synchronization
 * 
 * Sketch Artist: Sir Ronnie from Core1D Automations and Labs
 * License: MIT
 * 
 * Copyright (c) 2025 Core1D Labs
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <LiquidCrystal.h>
#include <ArduinoJson.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <time.h>

// Pin definitions for Wemos D1 R1
#define D0   3   // GPIO3 maps to Arduino D0
#define D1   1   // GPIO1 maps to Arduino D1
#define D2  16   // GPIO16 maps to Arduino D2
#define D3   5   // GPIO5 maps to Arduino D3
#define D4   4   // GPIO4 maps to Arduino D4
#define D5  14   // GPIO14 maps to Arduino D5
#define D6  12   // GPIO12 maps to Arduino D6
#define D7  13   // GPIO13 maps to Arduino D7
#define D8   0   // GPIO0 maps to Arduino D8
#define D9   2   // GPIO2 maps to Arduino D9
#define D10 15   // GPIO15 maps to Arduino D10

#define LCD_BACKLIGHT D3  // GPIO5
#define KEYPAD_PIN    A0

// Button constants
#define btnNONE   0
#define btnRIGHT  1
#define btnUP     2
#define btnDOWN   3
#define btnLEFT   4
#define btnSELECT 5

// LCD pins (standard LCD keypad shield configuration)
LiquidCrystal lcd(D8, D9, D4, D5, D6, D7);

// NTP Client setup
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000); // Update every minute

// WiFi credentials
const char* ssid     = "YourSSID";
const char* password = "YourPassword";

// API Keys and URLs
String WEATHER_API_KEY  = "sign.in.and.generate.your.own.free.api";
String WEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather?id=";

// City data structure
struct CityData {
  String  name;
  String  id;
  int     timezoneOffset; // Offset in seconds from UTC
  String  country;
  float   temperature;
  float   humidity;
  float   pressure;
  String  description;
  String  iconCode;
  unsigned long lastUpdate;
  bool          isNight;
};

// Cities array with timezone offsets (in seconds from UTC)
CityData cities[] = {
  {"Mumbai", "1275339", 19800, "IN", 0, 0, 0, "", "", 0, false},      // UTC+5:30
  {"Lndn", "2643743", 0, "GB", 0, 0, 0, "", "", 0, false},          // UTC+0 (will adjust for BST)
  {"NY", "5128581", -18000, "US", 0, 0, 0, "", "", 0, false},   // UTC-5 (will adjust for DST)
  {"Sydny", "2147714", 36000, "AU", 0, 0, 0, "", "", 0, false},      // UTC+10 (will adjust for DST)
  {"HngKng", "1819729", 28800, "HK", 0, 0, 0, "", "", 0, false},   // UTC+8
  {"Bjng", "1816670", 28800, "CN", 0, 0, 0, "", "", 0, false},     // UTC+8
  {"Pari", "2988507", 3600, "FR", 0, 0, 0, "", "", 0, false},        // UTC+1 (will adjust for DST)
  {"Brln", "2950159", 3600, "DE", 0, 0, 0, "", "", 0, false}        // UTC+1 (will adjust for DST)
};

const int numCities = sizeof(cities) / sizeof(cities[0]);
int currentCity                 = 0;
int brightness                  = 200;
unsigned long lastKeyPress      = 0;
unsigned long lastWeatherUpdate = 0;
unsigned long lastAnimation     = 0;
unsigned long lastDetailDisplay = 0;
unsigned long lastTimeUpdate    = 0;
int animFrame                   = 0;
bool showDetailedInfo           = false;
bool manualDetailTrigger        = false;
int backlightLevel              = 2; // 0=off, 1=low, 2=medium, 3=high
bool colonBlink                 = true;
bool systemReady                = false;
bool timeInitialized            = false;

// Custom characters for animations and UI
const byte sunChar1[] PROGMEM = {
  0b00000, 
  0b00100, 
  0b01110, 
  0b11111, 
  0b11111, 
  0b01110, 
  0b00100, 
  0b00000
};
const byte sunChar2[] PROGMEM = {
  0b10001, 
  0b01010, 
  0b00100, 
  0b11111, 
  0b11111, 
  0b00100, 
  0b01010, 
  0b10001
};
const byte sunChar3[] PROGMEM = {
  0b00100, 
  0b10101, 
  0b01110, 
  0b11111, 
  0b11111, 
  0b01110, 
  0b10101, 
  0b00100
};

const byte moonChar1[] PROGMEM = {
  0b00000, 
  0b01100, 
  0b10010, 
  0b10000, 
  0b10000, 
  0b10010, 
  0b01100, 
  0b00000
};
const byte moonChar2[] PROGMEM = {
  0b00000, 
  0b01110, 
  0b10001, 
  0b10000, 
  0b10000, 
  0b10001, 
  0b01110, 
  0b00000
};
const byte moonChar3[] PROGMEM = {
  0b00000, 
  0b01111, 
  0b10000, 
  0b10000, 
  0b10000, 
  0b10000, 
  0b01111, 
  0b00000
};

const byte cloudChar1[] PROGMEM = {
  0b00000, 
  0b00000, 
  0b01110, 
  0b11111, 
  0b11111, 
  0b01111, 
  0b00000, 
  0b00000
};
const byte cloudChar2[] PROGMEM = {
  0b00000, 
  0b00000, 
  0b00111, 
  0b01111, 
  0b11111, 
  0b11110, 
  0b00000, 
  0b00000
};
const byte cloudChar3[] PROGMEM = {
  0b00000, 
  0b00000, 
  0b01110, 
  0b01111, 
  0b11111, 
  0b11111, 
  0b00000, 
  0b00000
};

const byte rainChar1[] PROGMEM = {
  0b01010, 
  0b00000, 
  0b01110, 
  0b11111, 
  0b01010, 
  0b10101, 
  0b01010, 
  0b00000
};
const byte rainChar2[] PROGMEM = {
  0b10101, 
  0b00000, 
  0b01110, 
  0b11111, 
  0b10101, 
  0b01010, 
  0b10101, 
  0b00000
};
const byte rainChar3[] PROGMEM = {
  0b01010, 
  0b00000, 
  0b01110, 
  0b11111, 
  0b00000, 
  0b01010, 
  0b00000, 
  0b10101
};
const byte rainChar4[] PROGMEM = {
  0b10101, 
  0b00000, 
  0b01110, 
  0b11111, 
  0b00100, 
  0b10101, 
  0b01000, 
  0b00010
};

const byte degreeChar[] PROGMEM = {
  0b01100, 
  0b10010, 
  0b10010, 
  0b01100, 
  0b00000, 
  0b00000, 
  0b00000, 
  0b00000
};

const byte wifiChar1[] PROGMEM = {
  0b00000, 
  0b00000, 
  0b00000, 
  0b00000, 
  0b00000, 
  0b00000, 
  0b00100, 
  0b00000
};
const byte wifiChar2[] PROGMEM = {
  0b00000, 
  0b00000, 
  0b00000, 
  0b00000, 
  0b01110, 
  0b00000, 
  0b00100, 
  0b00000
};
const byte wifiChar3[] PROGMEM = {
  0b00000, 
  0b00000, 
  0b11111, 
  0b00000, 
  0b01110, 
  0b00000, 
  0b00100, 
  0b00000
};
const byte wifiChar4[] PROGMEM = {
  0b11111, 
  0b00000, 
  0b11111, 
  0b00000, 
  0b01110, 
  0b00000, 
  0b00100, 
  0b00000
};

// Get current time with timezone adjustment
unsigned long getCurrentTime() {
  if (!timeInitialized) return 0;
  return timeClient.getEpochTime() + cities[currentCity].timezoneOffset;
}

// Check if current time is night (6 PM to 6 AM local time)
bool isNightTime() {
  if (!timeInitialized) return false;
  unsigned long currentTime = getCurrentTime();
  if (currentTime == 0) return false;
  
  int hour = (currentTime % 86400) / 3600;
  return (hour >= 18 || hour < 6);
}

// Helper function to invert character for night mode
void invertChar(byte* dest, const byte* src) {
  for (int i = 0; i < 8; i++) {
    dest[i] = ~src[i] & 0x1F;
  }
}

// Helper function to convert string to lowercase
String toLowerCase(String str) {
  String result = str;
  for (int i = 0; i < result.length(); i++) {
    if (result.charAt(i) >= 'A' && result.charAt(i) <= 'Z') {
      result.setCharAt(i, result.charAt(i) + 32);
    }
  }
  return result;
}

// Keypad reading function
int readKeypad() {
  int adc = analogRead(KEYPAD_PIN);
  if (adc > 1000) return btnNONE;
  if (adc < 50)   return btnRIGHT;
  if (adc < 195)  return btnUP;
  if (adc < 380)  return btnDOWN;
  if (adc < 555)  return btnLEFT;
  if (adc < 790)  return btnSELECT;
  return btnNONE;
}

// Initialize NTP time client
bool initializeTime() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Syncing time...");
  
  timeClient.begin();
  timeClient.setTimeOffset(0); // We'll handle timezone manually
  
  int attempts = 0;
  while (!timeClient.update() && attempts < 20) {
    lcd.setCursor(attempts % 16, 1);
    lcd.print(".");
    delay(500);
    attempts++;
  }
  
  if (timeClient.getEpochTime() > 946684800) { // Valid timestamp
    timeInitialized = true;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Time synced!");
    
    // Display current time for verification
    unsigned long currentTime = getCurrentTime();
    int hours = (currentTime % 86400) / 3600;
    int minutes = (currentTime % 3600) / 60;
    
    lcd.setCursor(0, 1);
    char timeStr[16];
    sprintf(timeStr, "%02d:%02d %s", hours, minutes, cities[currentCity].name.substring(0, 3).c_str());
    lcd.print(timeStr);
    delay(2000);
    return true;
  } else {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Time sync failed");
    lcd.setCursor(0, 1);
    lcd.print("Check connection");
    delay(3000);
    return false;
  }
}

// WiFi connection with progress animation
void connectWiFi() {
  WiFi.begin(ssid, password);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Connecting WiFi");
  
  int attempts  = 0;
  int animPhase = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 40) {
    delay(250);
    lcd.setCursor(0, 1);
    for (int i = 0; i < 16; i++) {
      if (i == animPhase % 16) {
        lcd.print("*");
      } else if (i < (animPhase % 16)) {
        lcd.print("=");
      } else {
        lcd.print(" ");
      }
    }
    
    // Smooth breathing effect
    int pulseValue = 100 + 100 * sin(animPhase * 0.3);
    analogWrite(LCD_BACKLIGHT, pulseValue);
    animPhase++;
    attempts++;
  }
  
  // Restore brightness
  analogWrite(LCD_BACKLIGHT, 255);
  lcd.clear();
  lcd.setCursor(0, 0);
  
  if (WiFi.status() == WL_CONNECTED) {
    lcd.print("WiFi Connected!");
    lcd.setCursor(0, 1);
    lcd.print(WiFi.localIP().toString().substring(0, 15));
    delay(1500);
  } else {
    lcd.print("WiFi Failed!");
    lcd.setCursor(0, 1);
    lcd.print("Check settings");
    // Flash backlight for error indication
    for (int i = 0; i < 5; i++) {
      analogWrite(LCD_BACKLIGHT, 0);
      delay(100);
      analogWrite(LCD_BACKLIGHT, 255);
      delay(100);
    }
    delay(3000);
  }
  lcd.clear();
}

// Load weather characters with enhanced animations
void loadWeatherChars() {
  byte tempChar[8];
  byte invertedChar[8];
  String weather = toLowerCase(cities[currentCity].description);
  bool isNight   = cities[currentCity].isNight;
  
  // Weather animation (uses CGRAM 0-3 for 4-frame animation)
  bool isThunderstorm = weather.indexOf("thunderstorm") != -1;
  if (isThunderstorm) {
    // Enhanced thunderstorm: 6-frame animation with lightning
    int frame = animFrame % 6;
    const byte* rainFrames[] = {
      rainChar1, 
      rainChar2, 
      rainChar3, 
      rainChar4, 
      rainChar3, 
      rainChar2
    };
    memcpy_P(tempChar, rainFrames[frame], 8);
    
    if (frame >= 3 && frame <= 4) {
      // Lightning effect - invert during flash
      invertChar(invertedChar, tempChar);
      lcd.createChar(0, invertedChar);
    } else {
      if (isNight) {
        invertChar(invertedChar, tempChar);
        lcd.createChar(0, invertedChar);
      } else {
        lcd.createChar(0, tempChar);
      }
    }
  } else if (weather.indexOf("rain") != -1 || weather.indexOf("drizzle") != -1) {
    // Enhanced rain animation - 4 frames
    const byte* rainFrames[] = {
      rainChar1, 
      rainChar2, 
      rainChar3, 
      rainChar4
    };
    memcpy_P(tempChar, rainFrames[animFrame % 4], 8);
    
    if (isNight) {
      invertChar(invertedChar, tempChar);
      lcd.createChar(0, invertedChar);
    } else {
      lcd.createChar(0, tempChar);
    }
  } else if (weather.indexOf("cloud") != -1) {
    // Cloud animation - 3 frames
    const byte* cloudFrames[] = {
      cloudChar1, 
      cloudChar2, 
      cloudChar3
    };
    memcpy_P(tempChar, cloudFrames[animFrame % 3], 8);
    
    if (isNight) {
      invertChar(invertedChar, tempChar);
      lcd.createChar(0, invertedChar);
    } else {
      lcd.createChar(0, tempChar);
    }
  } else {
    // Clear: sun (day) or moon (night) - 3 frames each
    if (isNight) {
      const byte* moonFrames[] = {
        moonChar1, 
        moonChar2, 
        moonChar3
      };
      memcpy_P(tempChar, moonFrames[animFrame % 3], 8);
      invertChar(invertedChar, tempChar);
      lcd.createChar(0, invertedChar);
    } else {
      const byte* sunFrames[] = {sunChar1, sunChar2, sunChar3};
      memcpy_P(tempChar, sunFrames[animFrame % 3], 8);
      lcd.createChar(0, tempChar);
    }
  }
  
  // Degree symbol (CGRAM 1)
  memcpy_P(tempChar, degreeChar, 8);
  lcd.createChar(1, tempChar);
  
  // WiFi signal strength (CGRAM 2)
  int rssi = WiFi.RSSI();
  int signalStrength = (WiFi.status() != WL_CONNECTED) ? 0 :
                       (rssi > -50) ? 4 : (rssi > -60) ? 3 : (rssi > -70) ? 2 : 1;
  
  const byte* wifiFrames[] = {wifiChar1, wifiChar2, wifiChar3, wifiChar4};
  if (signalStrength > 0) {
    memcpy_P(tempChar, wifiFrames[signalStrength - 1], 8);
  } else {
    // No signal indicator
    byte noSignal[] = {
      0b00000, 
      0b00000, 
      0b00000, 
      0b00000, 
      0b00000, 
      0b00000, 
      0b00100, 
      0b00100};
    memcpy(tempChar, noSignal, 8);
  }
  
  if (isNight) {
    invertChar(invertedChar, tempChar);
    lcd.createChar(2, invertedChar);
  } else {
    lcd.createChar(2, tempChar);
  }
}

// Update weather data
bool updateWeatherData(int cityIndex) {
  if (WiFi.status() != WL_CONNECTED) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("No WiFi!");
    delay(2000);
    lcd.clear();
    return false;
  }

  WiFiClient client;
  HTTPClient http;
  String url = WEATHER_BASE_URL + cities[cityIndex].id + "&appid=" + WEATHER_API_KEY + "&units=metric";
  
  http.begin(client, url);
  http.setTimeout(15000);
  http.addHeader("User-Agent", "ESP8266WeatherStation/1.0");
  
  int httpCode = http.GET();
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    
    // Parse JSON with error handling
    DynamicJsonDocument doc(1024);
    DeserializationError error = deserializeJson(doc, payload);
    
    if (!error) {
      cities[cityIndex].temperature = doc["main"]["temp"];
      cities[cityIndex].humidity = doc["main"]["humidity"];
      cities[cityIndex].pressure = doc["main"]["pressure"];
      cities[cityIndex].description = doc["weather"][0]["description"].as<String>();
      cities[cityIndex].iconCode = doc["weather"][0]["icon"].as<String>();
      cities[cityIndex].lastUpdate = millis();
      
      // Determine if it's night based on icon code
      cities[cityIndex].isNight = cities[cityIndex].iconCode.endsWith("n");
      
      http.end();
      return true;
    } else {
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Data parse error");
      delay(2000);
      lcd.clear();
    }
  } else {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("HTTP Error: ");
    lcd.print(httpCode);
    delay(2000);
    lcd.clear();
  }
  
  http.end();
  return false;
}

// Enhanced boot animation using weather characters
void bootAnimation() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Core1D Labs");
  lcd.setCursor(0, 1);
  lcd.print("D1 Weather Card");
  
  // Smooth fade-in
  for (int i = 0; i <= 255; i += 8) {
    analogWrite(LCD_BACKLIGHT, i);
    delay(20);
  }
  delay(1500);
  
  // Loading animation using sun character
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Initializing...");
  
  // Create simple sun animation for boot
  byte tempChar[8];
  memcpy_P(tempChar, sunChar1, 8);
  lcd.createChar(0, tempChar);
  
  for (int phase = 0; phase <= 16; phase++) {
    lcd.setCursor(0, 1);
    for (int i = 0; i < 16; i++) {
      if (i < phase) {
        if (i % 2 == 0) lcd.write(0);
        else lcd.print("=");
      } else {
        lcd.print(" ");
      }
    }
    delay(80);
  }
  
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("System Ready");
  delay(1000);
  lcd.clear();
  systemReady = true;
}

// Smooth scrolling text function
void displayScrollingText(String text, int line, int startPos, int maxLen, bool oneLoop = false) {
  static unsigned long lastScrollTime = 0;
  static int scrollPosition           = 0;
  static String lastText              = "";
  static bool scrollPause             = true;
  static unsigned long pauseStart     = 0;
  static bool completedOneLoop        = false;

  if (text != lastText) {
    scrollPosition   = 0;
    scrollPause      = true;
    pauseStart       = millis();
    lastText         = text;
    completedOneLoop = false;
  }

  if (text.length() <= maxLen) {
    lcd.setCursor(startPos, line);
    lcd.print(text);
    for (int i = text.length(); i < maxLen; i++) {
      lcd.print(" ");
    }
    completedOneLoop = true;
    return;
  }

  if (scrollPause) {
    if (millis() - pauseStart > 2000) {
      scrollPause    = false;
      lastScrollTime = millis();
    }
    lcd.setCursor(startPos, line);
    String displayText = text.substring(0, maxLen);
    lcd.print(displayText);
    return;
  }

  if (millis() - lastScrollTime > 200) {
    lcd.setCursor(startPos, line);
    String displayText = text.substring(scrollPosition, scrollPosition + maxLen);
    if (displayText.length() < maxLen) {
      displayText += " " + text.substring(0, maxLen - displayText.length());
    }
    lcd.print(displayText);
    scrollPosition++;
    
    if (scrollPosition >= text.length()) {
      scrollPosition   = 0;
      scrollPause      = true;
      pauseStart       = millis();
      completedOneLoop = true;
    }
    lastScrollTime = millis();
  }

  if (oneLoop && completedOneLoop) {
    showDetailedInfo    = false;
    manualDetailTrigger = false;
    lcd.clear();
  }
}

// Update animations with improved timing
void updateAnimations() {
  if (!systemReady) return;
  
  // Slower animation during night hours and when idle
  unsigned long animDelay = 600;
  if (cities[currentCity].isNight) animDelay     = 1000;
  if (millis() - lastKeyPress > 60000) animDelay = 1200;
  
  if (millis() - lastAnimation < animDelay) return;
  
  loadWeatherChars();
  
  // Display weather icon
  lcd.setCursor(15, 0);
  lcd.write(0);
  
  // Display WiFi signal
  lcd.setCursor(15, 1);
  lcd.write(2);
  
  animFrame++;
  lastAnimation = millis();
}

// Enhanced brightness control
void adjustBrightness() {
  static int targetBrightness               = 200;
  static unsigned long lastBrightnessUpdate = 0;
  
  if (millis() - lastBrightnessUpdate < 50) return;
  
  int baseBrightness;
  switch (backlightLevel) {
    case 0: baseBrightness = 0; break;      // Off
    case 1: baseBrightness = 50; break;     // Low
    case 2: baseBrightness = 180; break;    // Medium  
    case 3: baseBrightness = 255; break;    // High
    default: baseBrightness = 180; break;
  }
  
  targetBrightness = baseBrightness;
  
  // Apply night dimming (except when manually set to high or off)
  if (cities[currentCity].isNight && backlightLevel != 0 && backlightLevel != 3) {
    int dimmedBrightness = (int)(targetBrightness * 0.6);
    targetBrightness = (dimmedBrightness > 20) ? dimmedBrightness : 20;
  }
  
  // Auto-dim after long inactivity (except when set to off)
  if (millis() - lastKeyPress > 120000 && backlightLevel > 0) {
    int autoDimBrightness = targetBrightness / 4;
    targetBrightness = (autoDimBrightness > 10) ? autoDimBrightness : 10;
  }
  
  // Smooth brightness transition
  if (abs(brightness - targetBrightness) > 2) {
    if (brightness < targetBrightness) {
      int increment = (targetBrightness - brightness > 10) ? 10 : (targetBrightness - brightness);
      brightness += increment;
    } else {
      int decrement = (brightness - targetBrightness > 10) ? 10 : (brightness - targetBrightness);
      brightness -= decrement;
    }
    brightness = constrain(brightness, 0, 255);
    analogWrite(LCD_BACKLIGHT, brightness);
  }
  
  lastBrightnessUpdate = millis();
}

// Display weather with detailed mode
void displayWeather() {
  if (!systemReady) return;
  
  if (showDetailedInfo) {
    // Show simplified date on first line
    if (timeInitialized) {
      unsigned long currentTime = getCurrentTime();
      time_t rawTime            = currentTime;
      struct tm* timeInfo = localtime(&rawTime);
      
      char dateStr[16];
      sprintf(dateStr, "%02d/%02d/%04d", timeInfo->tm_mday, timeInfo->tm_mon + 1, timeInfo->tm_year + 1900);
      lcd.setCursor(0, 0);
      lcd.print(dateStr);
      for (int i = strlen(dateStr); i < 14; i++) {
        lcd.print(" ");
      }
    } else {
      lcd.setCursor(0, 0);
      lcd.print("No Time Data");
      for (int i = 12; i < 14; i++) {
        lcd.print(" ");
      }
    }
    
    // Detailed weather info scrolling
    String detailLine = cities[currentCity].name + " " + cities[currentCity].country + ": " +
                        String(cities[currentCity].temperature, 1) + "C " +
                        String(cities[currentCity].humidity, 0) + "% " +
                        String(cities[currentCity].pressure, 0) + "hPa " +
                        cities[currentCity].description;
    displayScrollingText(detailLine, 1, 0, 14, true);
  } else {
    // Main display: City, temperature, and weather icon
    String cityTemp = cities[currentCity].name + " " + cities[currentCity].country +
                      " " + String(cities[currentCity].temperature, 1);
    lcd.setCursor(0, 0);
    if (cityTemp.length() > 12) {
      lcd.print(cityTemp.substring(0, 12));
    } else {
      lcd.print(cityTemp);
      for (int i = cityTemp.length(); i < 12; i++) {
        lcd.print(" ");
      }
    }
    lcd.write(1);  // Degree symbol
    lcd.print("C");
    
    // Time and humidity on second line
    char timeStr[8];
    if (timeInitialized) {
      unsigned long currentTime = getCurrentTime();
      int hours   = (currentTime % 86400) / 3600;
      int minutes = (currentTime % 3600) / 60;
      
      if (colonBlink && (millis() / 1000) % 2 == 0) {
        sprintf(timeStr, "%02d %02d", hours, minutes);
      } else {
        sprintf(timeStr, "%02d:%02d", hours, minutes);
      }
    } else {
      strcpy(timeStr, "--:--");
    }
    
    String timeLine = String(timeStr) + " H" + String(cities[currentCity].humidity, 0) + "%";
    lcd.setCursor(0, 1);
    if (timeLine.length() > 14) {
      lcd.print(timeLine.substring(0, 14));
    } else {
      lcd.print(timeLine);
      for (int i = timeLine.length(); i < 14; i++) {
        lcd.print(" ");
      }
    }
  }
}

// Show city selection menu
void showCityList() {
  int selectedCity  = currentCity;
  bool citySelected = false;
  
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Select City:");
  
  while (!citySelected) {
    String cityLine = cities[selectedCity].name + " " + cities[selectedCity].country;
    displayScrollingText(cityLine, 1, 0, 16);
    
    int key = readKeypad();
    if (key != btnNONE) {
      lastKeyPress = millis();
      switch (key) {
        case btnUP:
          selectedCity = (selectedCity + 1) % numCities;
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Select City:");
          break;
        case btnDOWN:
          selectedCity = (selectedCity - 1 + numCities) % numCities;
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Select City:");
          break;
        case btnSELECT:
        case btnRIGHT:
          currentCity = selectedCity;
          citySelected = true;
          // Update weather data for new city if needed
          if (millis() - cities[currentCity].lastUpdate > 300000) {
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("Loading...");
            updateWeatherData(currentCity);
          }
          lcd.clear();
          break;
        case btnLEFT:
          citySelected = true;
          lcd.clear();
          break;
      }
      delay(200);
    }
    adjustBrightness();
  }
}

void setup() {
  // Initialize LCD
  lcd.begin(16, 2);
  pinMode(LCD_BACKLIGHT, OUTPUT);
  analogWrite(LCD_BACKLIGHT, 0);
  
  // Boot animation
  bootAnimation();
  
  // Connect to WiFi
  connectWiFi();
  
  // Initialize time system
  if (!initializeTime()) {
    // If time sync fails, show error and continue with limited functionality
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Time sync failed");
    lcd.setCursor(0, 1);
    lcd.print("Limited mode");
    delay(3000);
  }
  
  // Get initial weather data
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Getting weather");
  updateWeatherData(currentCity);
  delay(1500);
  lcd.clear();
  
  // Initialize timestamps
  lastKeyPress      = millis();
  lastWeatherUpdate = millis();
  lastTimeUpdate    = millis();
}

void loop() {
  // Handle keypad input
  int key            = readKeypad();
  static int lastKey = btnNONE;
  static unsigned long keyTime = 0;
  
  if (key != lastKey && key != btnNONE && millis() - keyTime > 200) {
    lastKeyPress = millis();
    
    switch (key) {
      case btnRIGHT:
        currentCity = (currentCity + 1) % numCities;
        lcd.clear();
        // Update weather if data is old
        if (millis() - cities[currentCity].lastUpdate > 300000) {
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Loading...");
          updateWeatherData(currentCity);
          lcd.clear();
        }
        break;
        
      case btnLEFT:
        currentCity = (currentCity - 1 + numCities) % numCities;
        lcd.clear();
        // Update weather if data is old
        if (millis() - cities[currentCity].lastUpdate > 300000) {
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Loading...");
          updateWeatherData(currentCity);
          lcd.clear();
        }
        break;
        
      case btnUP:
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Refreshing...");
        updateWeatherData(currentCity);
        lcd.clear();
        break;
        
      case btnDOWN:
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print(cities[currentCity].name);
        lcd.setCursor(0, 1);
        lcd.print("Press:" + String(cities[currentCity].pressure, 0) + "hPa");
        delay(2000);
        lcd.clear();
        break;
        
      case btnSELECT:
        // Cycle through brightness levels
        backlightLevel = (backlightLevel + 1) % 4;
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Brightness:");
        lcd.setCursor(0, 1);
        switch(backlightLevel) {
          case 0: lcd.print("Off"); break;
          case 1: lcd.print("Low"); break;
          case 2: lcd.print("Medium"); break;
          case 3: lcd.print("High"); break;
        }
        delay(1500);
        lcd.clear();
        break;
    }
    keyTime = millis();
  }
  lastKey = key;
  
  // Time update - sync every 30 minutes
  if (timeInitialized && millis() - lastTimeUpdate > 1800000) {
    if (WiFi.status() == WL_CONNECTED) {
      timeClient.update();
      lastTimeUpdate = millis();
    }
  }
  
  // Weather update with adaptive timing
  unsigned long weatherUpdateInterval;
  bool isSleepHours = false;
  
  if (timeInitialized) {
    unsigned long currentTime = getCurrentTime();
    if (currentTime > 0) {
      int hour = (currentTime % 86400) / 3600;
      isSleepHours = (hour >= 22 || hour < 6); // 10 PM to 6 AM
    }
  }
  
  // Adaptive update intervals
  if (isSleepHours) {
    weatherUpdateInterval = 1800000; // 30 minutes during sleep hours
  } else if (millis() - lastKeyPress > 300000) { // 5 minutes of inactivity
    weatherUpdateInterval = 900000;  // 15 minutes when idle
  } else {
    weatherUpdateInterval = 600000;  // 10 minutes when active
  }
  
  if (millis() - lastWeatherUpdate > weatherUpdateInterval && !manualDetailTrigger) {
    if (WiFi.status() == WL_CONNECTED) {
      updateWeatherData(currentCity);
      lastWeatherUpdate = millis();
      
      // Show detailed info every 5th update during active hours
      static int updateCounter = 0;
      updateCounter++;
      if (!isSleepHours && updateCounter % 5 == 0) {
        showDetailedInfo = true;
        lastDetailDisplay = millis();
        lcd.clear();
      }
    }
  }
  
  // Auto-hide detailed info after 30 seconds
  if (showDetailedInfo && millis() - lastDetailDisplay > 30000) {
    showDetailedInfo = false;
    lcd.clear();
  }
  
  // WiFi reconnection handling
  if (WiFi.status() != WL_CONNECTED) {
    static unsigned long lastReconnectAttempt = 0;
    if (millis() - lastReconnectAttempt > 60000) { // Try every minute
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("WiFi reconnect..");
      
      WiFi.reconnect();
      int reconnectAttempts = 0;
      while (WiFi.status() != WL_CONNECTED && reconnectAttempts < 10) {
        delay(500);
        lcd.setCursor(reconnectAttempts % 16, 1);
        lcd.print(".");
        reconnectAttempts++;
      }
      
      lcd.clear();
      if (WiFi.status() == WL_CONNECTED) {
        lcd.setCursor(0, 0);
        lcd.print("WiFi restored!");
        delay(1000);
        // Re-sync time after WiFi restoration
        if (timeInitialized) {
          timeClient.update();
        }
      } else {
        lcd.setCursor(0, 0);
        lcd.print("WiFi failed!");
        delay(1000);
      }
      lcd.clear();
      lastReconnectAttempt = millis();
    }
  }
  
  // Main display update
  displayWeather();
  updateAnimations();
  adjustBrightness();
  
  // Adaptive delay based on system state and time
  unsigned long timeSinceLastKey = millis() - lastKeyPress;
  
  if (isSleepHours && timeSinceLastKey > 300000) {
    // Very slow updates during sleep hours when idle
    delay(5000);
  } else if (timeSinceLastKey > 120000) {
    // Slower updates when idle
    delay(1000);
  } else if (showDetailedInfo) {
    // Fast updates for scrolling text
    delay(50);
  } else {
    // Normal update rate
    delay(100);
  }
  
  // Memory management and system health
  static unsigned long lastHealthCheck = 0;
  if (millis() - lastHealthCheck > 300000) { // Every 5 minutes
    // Check free heap
    if (ESP.getFreeHeap() < 1000) {
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Low memory!");
      lcd.setCursor(0, 1);
      lcd.print("Restarting...");
      delay(2000);
      ESP.restart();
    }
    
    // Watchdog reset to prevent hangs
    ESP.wdtFeed();
    lastHealthCheck = millis();
  }
  
  // Handle millis() overflow (every ~49 days)
  static unsigned long lastMillisCheck = millis();
  if (millis() < lastMillisCheck) {
    // Reset all timing variables
    lastKeyPress = millis();
    lastWeatherUpdate = millis();
    lastAnimation = millis();
    lastDetailDisplay = millis();
    lastTimeUpdate = millis();
  }
  lastMillisCheck = millis();
  
  // Yield to prevent watchdog timeout
  yield();
}

Any improvements or tips are welcome.

Thx for sharing

Hum

Bright daylight here in France at 6pm in the summer… may be you want to include calculations to find out when the sun sets or rises based on the day and city?

You could also use more const char * rather than String, and this exist already https://docs.arduino.cc/language-reference/en/variables/data-types/stringObject/Functions/toLowerCase/

1 Like

You seem to have followed an outdated NTP example.
NTP is now included in the latest ESP core.
No extra libraries needed and no more "time offsets from UTC".
Link to other locations is in the code. Automatic daylight savings too.
I have included an example. with a webportal, to enter your WiFi credentials at run time and with call back that waits for valid time. A default update of 3 hours is more than enough to keep perfect time. Every minute could get you kicked off eventually.

I was standing on the shoulders of this poster.
Here you can find more time elements that you can use in your code. Day, month, year etc.
Tested on a NodeMCU ESP8266.
Leo..

#define MY_TZ "NZST-12NZDT,M9.5.0,M4.1.0/3"  // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define NTP_SERVER "nz.pool.ntp.org"         // use a local pool
#include <ESP8266WiFi.h>                     // we need wifi to get internet access
#include <coredecls.h>                       // optional settimeofday_cb() callback to check on time update
#include <WiFiManager.h>                     // https://github.com/tzapu/WiFiManager
bool reSync;
unsigned long prevTime;
time_t now;  // "now" is the seconds since Epoch (1970) - UTC
tm tm;       // the structure tm holds time information in a convenient way

void time_is_set() {
  Serial.println("time was sent!");  // prints with every NTP update, defaults to 3 hours
  reSync = true;
}

void setup() {
  Serial.begin(74880);            // ESP8266 native baud rate
  configTime(MY_TZ, NTP_SERVER);  // --> Here is the IMPORTANT ONE LINER needed in your sketch!
  settimeofday_cb(time_is_set);   // optional: callback if time was sent
  WiFi.mode(WIFI_STA);
  WiFiManager wm;
  wm.setConnectTimeout(120);      // optional timeout before portal starts (router power cut)
  wm.setConfigPortalTimeout(60);  // optional portal active period (for safety)
  wm.autoConnect("Portal");       // open, 192.168.4.1
  //wm.autoConnect("Portal, 12345678"); // or portal with password
  while (!reSync) yield();  // wait here for a valid time string
}

void loop() {
  time(&now);                // load epoch
  if (now != prevTime) {     // if time (seconds) has changed
    prevTime = now;          // remember
    localtime_r(&now, &tm);  // convert to local time
    printf("%02u:%02u:%02u\n", tm.tm_hour, tm.tm_min, tm.tm_sec);
  }
}
1 Like

What is that.
Three lines of code added to the above sketch could calculate sun azimuth and elevation. (sunrise, sunset, etc, for each location.
Leo..

loop example:

calcHorizontalCoordinates(time(&now), latitude, longitude, az, el);
Serial.print("sun elevation: ");
Serial.println(el);
1 Like

There is no need to use PROGMEM on an ESP8266, that is only for certain AVR processors that have flash memory and ram in separate address spaces.

2 Likes

Thank you everyone for the valuable feedbacks. I will keep collecting them, give the due credits and re upload a corrected version that everyone can safely rely on.

(post deleted by author)