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.