A Weather Clock Firmware for the card sized hardware setup of a LOLIN/WeMos/NodeMCU D1R1 ESP8266 with an Arduino pin layout LCD Keypad Shield.
Bypass capacitor for the circuit and limiting resistors for the backlight are optional for regulated 3v3 [provided it is powered from the VOUT (3V3) marked pin) of Devkit C1, Devkit C1 handles it to a functional level]. If used, recommended values are 10μF and 220Ω respectively. Also ceramic capacitors are recommended for any effective noise reduction or decoupling. Use electrolytic only in the non availability of ceramic.
The board is also a WeMos D1 R1, but in the fritzing file it is R2. While the physical marriage of the shield will be all the similar, the firmware will assume the Pin Layout standard of D1 R1.
Do remember to set your board to LOLIN/WeMos D1 R1 in your Arduino IDE.
basic capstone firmware to reduce your headache of expanding on anything else within this hardware setup, or at least is the intent.
While the basic clock functionality have been wet tested, the other features remain to be fully tested out.
If you find any edge case issues, problems, or corrections, please feel free to comment, and I will include them in the next revision along with due credits.
/**
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ WEMOS D1 R1 WEATHER STATION ║
* ║ Multi-City Weather Display with Solar Data ║
* ║ Revision 2.0 ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* A comprehensive weather station firmware for the Wemos D1 R1 board with an LCD keypad shield.
* Displays real-time weather information for multiple international cities using the OpenWeatherMap API,
* featuring advanced astronomical calculations, adaptive brightness control, animated weather icons,
* and user interface.
*
* ChangeLog for Rev1 > Rev 2: Further hardware specific firmware alignments to account for the 3V3 logic of ESP82xx
* systems compared to Atmega2560 5V electrical logic.
*
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ HARDWARE REQUIREMENTS ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Primary Components ───────────────────────────────────────────────────────────────────────────────────────────┐
* │ ▪ LOLIN/Wemos/NodeMCU D1 R1 Development Board (Or any ESP8266-board if you are familiar with the pins) │
* │ ▪ 16x2 Character LCD with I2C Backpack OR Arduino LCD Keypad Shield (Hitachi HD44780 or supported clone) │
* │ ▪ 5-Button Analog Keypad (RIGHT, UP, DOWN, LEFT, <SELECT> (NO OP in ESP82xx with default hardware config) │
* │ ▪ Stable 5V power supply (minimum 500mA recommended) │
* │ ▪ WiFi internet connection for weather data and time synchronization │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔═══════════════════════════════════════════════════════════════════════════════╗
* ║ WEMOS D1 R1 WEATHER STATION ║
* ║ Hardware Configuration ║
* ╚═══════════════════════════════════════════════════════════════════════════════╝
*
* [Wifi Connection via WROOM]
* ┌─────────────────────────────────┐ ┌──────────────────────────────────────┐
* │ Wemos D1 R1 │ │ 16x2 LCD Keypad Shield │
* │ ESP8266 Core │ │ Hitachi HD44780 │─►●[D2]
* | ┌─────────────────────────┐ │ ●◄─│ ┌─────────────────────────────┐ │
* │ │ ESP8266 WiFi │ │ ●◄─│ │ (5x8)x16x2 dots LCD Display │ │
* │ │ 802.1 b/g/n │ │ ●◄─│ │ │ │
* │ └─────────────────────────┘ │ ●◄─│ │ ███████████████████████ │ │
* │ │ ●◄─│ │ ███████████████████████ │ │
* │ 3V3 ● 5V ● GND ● GND ● VIN●│ ●◄─│ └─────────────────────────────┘ │
* │ D0 ● D1 ● D2 ● [D3] ● [D4]●│ ●◄─│──────────────────────────────────────┤
* │[D5] ● [D6] ● [D7] ● [D8] ● [D9]●│ │ Voltage Divided Switches │─►●[D10]
* │ D10 ● RX/D ● TX/D ● GND ● 3V3 ●│ │ ┌─ [SELECT] [LEFT] [DOWN] [UP] ─┐ │
* │ │ │ │ ^(NO OP for D1R1) │ │
* │ [A0] ●◄────────────┼────┼──┤ [RIGHT] Button │ │
* └─▼───────────────────────────────┘ │ └───────────────────────────────┘ │
* [USB Power (5V) or Barrel Jack setup] └──────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ PIN CONFIGURATION ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─────────────┬───────────┬─────────────────┬────────────────────────────────────────────────────────────────────┐
* │ Wemos Pin │ GPIO Pin │ LCD Function │ Description │
* ├─────────────┼───────────┼─────────────────┼────────────────────────────────────────────────────────────────────┤
* │ D0 │ GPIO3 │ (Reserved) │ Hardware Serial RX - Do not use (USB communication) │
* │ D1 │ GPIO1 │ (Reserved) │ Hardware Serial TX - Do not use (USB communication) │
* │ D2 ● │ GPIO16 │ (Available) │ General purpose I/O - Available for expansion │
* │ ● D3 │ GPIO5 │ LCD_BACKLIGHT │ PWM output for LCD backlight control (0-255) (Default NO OP) │
* │ ● D4 │ GPIO4 │ LCD_D4 │ LCD Data Bus Line 4 (4-bit mode) │
* │ ● D5 │ GPIO14 │ LCD_D5 │ LCD Data Bus Line 5 (4-bit mode) │
* │ ● D6 │ GPIO12 │ LCD_D6 │ LCD Data Bus Line 6 (4-bit mode) │
* │ ● D7 │ GPIO13 │ LCD_D7 │ LCD Data Bus Line 7 (4-bit mode) │
* │ ● D8 │ GPIO0 │ LCD_RS │ LCD Register Select (Command/Data selection) │
* │ ● D9 │ GPIO2 │ LCD_EN │ LCD Enable Signal (Data latch trigger) │
* │ D10 ● │ GPIO15 │ (Available) │ General purpose I/O - Available for expansion │
* │ ● A0 │ ADC │ KEYPAD │ Analog input for 5-button keypad (voltage divider) │
* │ GND │ GND │ GND │ Ground connection (multiple pins available) │
* │ 5V │ VIN │ VCC │ 5V power supply input (USB or external) │
* │ 3V3 │ 3.3V │ (Available) │ 3.3V regulated output from ESP8266 (limited current) │
* └─────────────┴───────────┴─────────────────┴────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ KEYPAD BUTTON MAPPING ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─────────────┬─────────────┬────────────────────────────────────────────────────────────────────────────────────┐
* │ Button │ ~ADC Value │ Function │
* ├─────────────┼─────────────┼────────────────────────────────────────────────────────────────────────────────────┤
* │ NONE │ > 1000 │ No button pressed (idle state) │
* │ RIGHT │ 2 │ Navigate to next city / Confirm selection │
* │ UP │ 169 │ Refresh current weather data immediately │
* │ DOWN │ 416 │ Navigate to previous city / Cancel current operation │
* │ LEFT │ 666 │ Cycle display modes: │
* │ <SELECT> │ <N/A> │ Normal → Detailed → Sun Info → Brightness Control (DEPRECIATED, NO OP in 3V Logic) |
* | │ └─────►│<No Op in D1R1 due to 3.3 VCC Logic. Feature currently implements with Left button> |
* └─────────────┴─────────────┴────────────────────────────────────────────────────────────────────────────────────┘
* NB: Actual physical values may vary from board to board. Use kepad threshold verifying example tests to get the
* correct hardwre ADC readings.
* I will be working on one for D1R1 and Arduino standard LCD Keypad Shield and will upload soon.
*
* ┌─ Display Mode Cycling (LEFT Button) ───────────────────────────────────────────────────────────────────────────┐
* │ 1st Press: Show detailed weather information with scrolling text │
* │ 2nd Press: Display astronomical data (sun elevation, azimuth, sunrise/sunset times) │
* │ 3rd Press: Brightness control (Off → Low → Medium → High) (DEPRECIATED) │
* │ 4th Press: Return to normal display mode │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ FIRMWARE DEPENDENCIES ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Required Libraries (Arduino IDE Library Manager) ─────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ ESP8266WiFi │ ESP8266 Arduino Core │ WiFi connection and network management │
* │ ▪ ESP8266HTTPClient │ ESP8266 Arduino Core │ HTTP client for API requests │
* │ ▪ LiquidCrystal │ Arduino Core Library │ LCD display control and character rendering │
* │ ▪ ArduinoJson │ Benoit Blanchon │ JSON parsing for OpenWeatherMap API (v6.x required) │
* │ ▪ coredecls.h │ ESP8266 Arduino Core │ Time synchronization callback functions │
* │ ▪ time.h │ Standard C Library │ Time functions and timezone handling │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Library Installation Instructions ────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ 1. Arduino IDE → Tools → Board → ESP8266 Boards → Generic ESP8266 Module │
* │ 2. Arduino IDE → Tools → Manage Libraries │
* │ 3. Search and install: "ArduinoJson" by Benoit Blanchon (version 6.x) │
* │ 4. Other libraries are included with ESP8266 Arduino Core │
* │ │
* │ ▪ ESP8266 Arduino Core: https://github.com/esp8266/Arduino │
* │ ▪ Installation: Arduino IDE → File → Preferences → Additional Board Manager URLs: │
* │ http://arduino.esp8266.com/stable/package_esp8266com_index.json │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ API REQUIREMENTS ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ OpenWeatherMap API Configuration ─────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ 1. Register for FREE API key at: https://openweathermap.org/api │
* │ 2. Free tier limits: 1,000 calls/day, 60 calls/minute │
* │ 3. API endpoint: http://api.openweathermap.org/data/2.5/weather │
* │ 4. Required parameters: City ID, API key, units=metric │
* │ │
* │ ▪ Weather data includes: Temperature, humidity, pressure, description, weather icons │
* │ ▪ Update frequency: 10-30 minutes (adaptive based on time and user activity) │
* │ ▪ Error handling: Automatic retry with exponential backoff │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ NTP Time Synchronization ─────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ NTP Server: pool.ntp.org (automatic regional selection) │
* │ ▪ Timezone Support: POSIX timezone strings for accurate local time │
* │ ▪ Sync Frequency: Automatic with ESP8266 NTP client │
* │ ▪ Fallback: Manual time setting if NTP fails │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ SETUP AND CONFIGURATION ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Step 1: Configure WiFi and API Settings ──────────────────────────────────────────────────────────────────────┐
* │ │
* │ Before uploading, modify these configuration variables in the code: │
* │ │
* │ const char* ssid = "YOUR_WIFI_NETWORK_NAME"; │
* │ const char* password = "YOUR_WIFI_PASSWORD"; │
* │ String WEATHER_API_KEY = "YOUR_OPENWEATHERMAP_API_KEY"; │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 2: Upload Configuration ─────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Arduino IDE Settings for Wemos D1 R1: │
* │ ▪ Board: "Generic ESP8266 Module" or "WeMos D1 R1" │
* │ ▪ Flash Size: "4M (3M SPIFFS)" or "4M (1M SPIFFS)" │
* │ ▪ CPU Frequency: "80 MHz" │
* │ ▪ Upload Speed: "115200" │
* │ ▪ Port: Select appropriate COM port │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 3: First Boot Sequence ──────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ 1. Display boot animation with "Core1D Labs" branding │
* │ 2. Initialize LCD and backlight system │
* │ 3. Connect to WiFi network (with visual progress indicator) │
* │ 4. Synchronize time with NTP server │
* │ 5. Fetch initial weather data for default city (Mumbai) │
* │ 6. Calculate initial astronomical data (sun position, sunrise/sunset) │
* │ 7. Enter normal operation mode │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ TECHNICAL SPECIFICATIONS ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Performance Characteristics ──────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ Memory Usage: Program: ~85% of flash, RAM: ~65% during operation │
* │ ▪ Power Consumption: Active: ~180mA, Dimmed: ~120mA, Night: ~100mA @ 3.3V │
* │ ▪ Update Intervals: Weather: 10-30min, Time: 1sec, Animation: 0.6-1.2sec │
* │ ▪ Response Time: Button press: <200ms, Weather refresh: 2-5sec │
* │ ▪ WiFi Range: Standard ESP8266 range (~50m indoor, ~100m outdoor) │
* │ ▪ Accuracy: Time: ±1sec (NTP), Temperature: ±0.5°C (API), Pressure: ±1hPa │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Environmental Operating Conditions ───────────────────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ Temperature Range: 0°C to +70°C (operational), -40°C to +85°C (storage) │
* │ ▪ Humidity Range: 10% to 90% RH (non-condensing) │
* │ ▪ Power Requirements: 5V DC, 300-500mA (depending on backlight setting) │
* │ ▪ WiFi Standards: 802.11 b/g/n (2.4GHz only) │
* │ ▪ Security Support: WPA/WPA2-PSK, WEP (not recommended) │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ SUPPORTED CITIES & TIMEZONES ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─────────────┬─────────────┬─────────────────┬──────────────────────────────────────────────────────────────────┐
* │ City │ Country │ OpenWeather │ Timezone │
* │ │ │ ID │ (POSIX Format) │
* ├─────────────┼─────────────┼─────────────────┼──────────────────────────────────────────────────────────────────┤
* │ Mumbai │ India │ 1275339 │ IST-5:30 (India Standard Time, UTC+5:30) │
* │ London │ UK │ 2643743 │ GMT0BST,M3.5.0/1,M10.5.0 (GMT/BST with DST) │
* │ NewYork │ USA │ 5128581 │ EST5EDT,M3.2.0,M11.1.0 (Eastern Time with DST) │
* │ Sydney │ Australia │ 2147714 │ AEST-10AEDT,M10.1.0,M4.1.0/3 (Australian Eastern with DST) │
* │ HongKong │ China │ 1819729 │ HKT-8 (Hong Kong Time, UTC+8) │
* │ Beijing │ China │ 1816670 │ CST-8 (China Standard Time, UTC+8) │
* │ Paris │ France │ 2988507 │ CET-1CEST,M3.5.0,M10.5.0/3 (Central European with DST) │
* │ Berlin │ Germany │ 2950159 │ CET-1CEST,M3.5.0,M10.5.0/3 (Central European with DST) │
* │ Los Angeles │ USA │ 5368361 │ PST8PDT,M3.2.0,M11.1.0 (Pacific Time with DST) │
* │ Tokyo │ Japan │ 1850147 │ JST-9 (Japan Standard Time, UTC+9) │
* │ Moscow │ Russia │ 524901 │ MSK-3 (Moscow Time, UTC+3) │
* │ Nairobi │ Kenya │ 184745 │ EAT-3 (East Africa Time, UTC+3) │
* └─────────────┴─────────────┴─────────────────┴──────────────────────────────────────────────────────────────────┘
*
* ┌─ Adding Custom Cities ─────────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ To add new cities, modify the cities[] array with: │
* │ 1. City name (display name, keep under 8 characters for best fit) │
* │ 2. OpenWeatherMap city ID (find at: openweathermap.org) │
* │ 3. POSIX timezone string (find at: https://github.com/nayarsystems/posix_tz_db) │
* │ 4. Country code (2-letter ISO country code) │
* │ 5. Latitude and longitude (for astronomical calculations) │
* │ │
* │ Example: │
* │ {"Tokyo", "1850147", "JST-9", "JP", 35.6762, 139.6503, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0} │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ CUSTOMIZATION OPTIONS ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ User Configurable Parameters ─────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ Weather Update Intervals: │
* │ → Active mode: 600000ms (10 minutes) │
* │ → Idle mode: 900000ms (15 minutes) │
* │ → Sleep hours: 1800000ms (30 minutes) │
* │ │
* │ ▪ Animation Timing: │
* │ → Day mode: 600ms between frames │
* │ → Night mode: 1000ms between frames │
* │ → Idle mode: 1200ms between frames │
* │ │
* │ │
* │ ▪ Sleep Hours Definition: │
* │ → Sleep period: 22:00 to 06:00 (10 PM to 6 AM local time) │
* │ → Auto-dimming after 2 minutes of inactivity │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Advanced Customization ───────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ Custom Weather Icons: │
* │ → Modify character arrays (sunChar1-3, moonChar1-3, cloudChar1-3, rainChar1-4) │
* │ → Create 8x5 pixel custom characters using LCD character generator tools │
* │ → Support for animated sequences (2-6 frames per weather condition) │
* │ │
* │ ▪ Additional Display Modes: │
* │ → Add new display functions following existing pattern │
* │ → Integrate with LEFT button cycling mechanism │
* │ → Consider memory usage when adding new features │
* │ │
* │ ▪ Extended City Database: │
* │ → Modify cities[] array structure to add new cities │
* │ → Obtain city IDs from OpenWeatherMap API documentation │
* │ → Use accurate latitude/longitude for astronomical calculations │
* │ → Find correct POSIX timezone strings for accurate local time │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ VERSION HISTORY & CONTRIBUTIONS ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Version History ──────────────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Version 3.0 Enhanced (Current): │
* │ ▪ Added comprehensive astronomical calculations (sun position, elevation, azimuth) │
* │ ▪ Implemented accurate sunrise/sunset calculations for each city │
* │ ▪ Enhanced day/night detection using actual sun position vs simple time-based │
* │ ▪ Added sun information display mode with elevation and sunrise/sunset times │
* │ ▪ Improved NTP implementation with modern ESP8266 core functions │
* │ ▪ Enhanced error handling and system stability │
* │ ▪ Added comprehensive documentation and hardware diagrams │
* │ │
* │ Version 2.0 (Previous): │
* │ ▪ Multi-city weather display with timezone support │
* │ ▪ Animated weather icons with day/night variants │
* │ ▪ Adaptive brightness control and power management │
* │ ▪ WiFi auto-reconnection and network stability improvements │
* │ │
* │ Version 1.0 (Original): │
* │ ▪ Basic weather display for single city │
* │ ▪ Simple LCD interface with keypad navigation │
* │ ▪ OpenWeatherMap API integration │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Community Contributions (Arduino Forum) ──────────────────────────────────────────────────────────────────────┐
* │ │
* │ Special thanks to the Arduino Forum community members who contributed to this project: │
* │ │
* │ ▪ J-M-L (Jackson) - Karma: 15000+ │
* │ → Identified incorrect timezone handling for France and other European cities │
* │ → Suggested implementing city-based astronomical calculations for accurate sunrise/sunset │
* │ → Provided guidance on proper POSIX timezone string formatting │
* │ │
* │ ▪ Wawa - Karma: 2000+ │
* │ → Correctly identified outdated NTP client implementation │
* │ → Suggested migration to modern ESP8266 core NTP functions │
* │ → Recommended adding sun azimuth calculations for astronomical integrity and emulation │
* │ │
* │ ▪ david_2018 - Active Contributor │
* │ → Pointed out unnecessary PROGMEM usage in ESP8266 environment │
* │ → Suggested code optimization for better memory management │
* │ → Provided insights on ESP8266-specific programming best practices │
* │ │
* │ Once again, my special thanks for donating their time. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ TECHNICAL IMPLEMENTATION ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Astronomical Calculations ────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ The firmware implements simplified astronomical formulas for sun position calculations: │
* │ │
* │ ▪ Julian Day Calculation: Converts UTC time to Julian day number for astronomical calculations │
* │ ▪ Solar Coordinates: Calculates sun's ecliptic longitude and declination │
* │ ▪ Horizontal Coordinates: Converts to local azimuth and elevation angles │
* │ ▪ Sunrise/Sunset: Finds times when sun elevation crosses -0.833° (atmospheric refraction) │
* │ │
* │ Key Formulas Used: │
* │ - Mean Longitude: L = 280.460 + 0.9856474 × n │
* │ - Mean Anomaly: g = 357.528 + 0.9856003 × n │
* │ - Ecliptic Longitude: λ = L + 1.915×sin(g) + 0.020×sin(2g) │
* │ - Elevation: alt = arcsin(sin(φ)×sin(δ) + cos(φ)×cos(δ)×cos(H)) │
* │ │
* │ Where: n = days since J2000.0, φ = latitude, δ = declination, H = hour angle │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Memory Management Strategy ───────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ ▪ Stack Usage: Minimized through careful variable scoping and function design │
* │ ▪ Heap Monitoring: Continuous monitoring with automatic restart below 1KB free │
* │ ▪ String Handling: Efficient String class usage with minimal concatenation │
* │ ▪ JSON Parsing: Streaming JSON parser to minimize memory allocation │
* │ ▪ Custom Characters: Efficient CGRAM management for animated weather icons │
* │ ▪ Static Arrays: Pre-allocated city data structures to avoid dynamic allocation │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ LICENSE AND COPYRIGHT ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* Assembled and ____________________________________________
* written by: Sir Ronnie from Core1D Automations and Labs
* Version: 3.0 Enhanced (with Astronomical Calculations)
* License: MIT Open Source License
* Copyright: © 2025 Core1D Labs
* Arduino Forum: https://forum.arduino.cc/___________________
*
* ┌─ MIT License Terms ────────────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Permission is hereby granted, free of charge, to any person obtaining a copy of this firmware and │
* │ associated documentation files (the "Firmware"), to deal in the Firmware 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 FIRMWARE 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 FIRMWARE OR │
* │ THE USE OR OTHER DEALINGS IN THE FIRMWARE. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
*/
// ====== ENHANCED WEMOS D1R1 WEATHER STATION ======
// Compatible with ESP8266 and LCD Keypad Shield (3V3 logic)
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <LiquidCrystal.h>
#include <ArduinoJson.h>
#include <coredecls.h>
#include <time.h>
// ====== HARDWARE CONFIGURATION ======
// Pin definitions for Wemos D1 R1 (GPIO to Arduino pin mapping)
#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 (3.3V compatible ADC values)
#define btnNONE 0
#define btnRIGHT 1
#define btnUP 2
#define btnDOWN 3
#define btnLEFT 4
// LCD configuration
LiquidCrystal lcd(D8, D9, D4, D5, D6, D7);
// ====== USER CONFIGURATION ======
// WiFi credentials
const char* ssid = "YourWiFiSSID";
const char* password = "YourPassword";
// Weather API configuration
String WEATHER_API_KEY = "cb840413161e7ee08e831af35dfb9c53"; // You can use mine as long as it lasts for quick tests, or you can create yours for free!
String WEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather?id=";
// NTP Configuration
#define NTP_SERVER "pool.ntp.org"
// ====== DATA STRUCTURES ======
struct CityData {
// const variables, const char* can also be used in these cases (Suggestion by J-M-L)
String name;
String id;
String timezone;
String country;
const float latitude; // const correction by J-M-L
const float longitude;
// dynamic variables
float temperature;
float humidity;
float pressure;
String description;
String iconCode;
unsigned long lastUpdate;
bool isNight;
double sunElevation;
double sunAzimuth;
int sunriseHour;
int sunriseMin;
int sunsetHour;
int sunsetMin;
};
// Cities array
CityData cities[] = {
{"Mumbai", "1275339", "IST-5:30", "IN", 19.0760, 72.8777, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"London", "2643743", "GMT0BST,M3.5.0/1,M10.5.0", "GB", 51.5074, -0.1278, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"NewYork", "5128581", "EST5EDT,M3.2.0,M11.1.0", "US", 40.7128, -74.0060, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Sydney", "2147714", "AEST-10AEDT,M10.1.0,M4.1.0/3", "AU", -33.8688, 151.2093, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"HongKong", "1819729", "HKT-8", "HK", 22.3193, 114.1694, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Beijing", "1816670", "CST-8", "CN", 39.9042, 116.4074, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Paris", "2988507", "CET-1CEST,M3.5.0,M10.5.0/3", "FR", 48.8566, 2.3522, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Berlin", "2950159", "CET-1CEST,M3.5.0,M10.5.0/3", "DE", 52.5200, 13.4050, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"LosAngeles", "5368361", "PST8PDT,M3.2.0,M11.1.0", "US", 34.0522, -118.2437, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Tokyo", "1850147", "JST-9", "JP", 35.6762, 139.6503, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Moscow", "524901", "MSK-3", "RU", 55.7558, 37.6176, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0},
{"Nairobi", "184745", "EAT-3", "KE", -1.2921, 36.8219, 0, 0, 0, "", "", 0, false, 0, 0, 0, 0, 0, 0}
};
const int numCities = sizeof(cities) / sizeof(cities[0]);
// ====== GLOBAL VARIABLES ======
// System state
int currentCity = 0;
int brightness = 200;
unsigned long lastKeyPress = 0;
unsigned long lastWeatherUpdate = 0;
unsigned long lastAnimation = 0;
unsigned long lastDetailDisplay = 0;
int animFrame = 0;
int backlightLevel = 2;
bool colonBlink = true;
bool systemReady = false;
bool timeInitialized = false;
bool reSync = false;
unsigned long prevTime = 0;
unsigned long lastSunUpdate = 0;
time_t now;
struct tm tm;
// Button handling
int lastButtonState = btnNONE;
unsigned long lastButtonTime = 0;
const unsigned long BUTTON_DEBOUNCE = 200;
// Display modes
enum DisplayMode {
MODE_NORMAL = 0,
MODE_DETAILED = 1,
MODE_SUN_INFO = 2,
MODE_PRESSURE = 3,
MODE_BRIGHTNESS = 4
};
DisplayMode currentDisplayMode = MODE_NORMAL;
// ====== SPRITE DEFINITIONS ======
// Custom characters for animations and UI
const byte sunChar1[] = {
0b00000,
0b00100,
0b01110,
0b11111,
0b11111,
0b01110,
0b00100,
0b00000
};
const byte sunChar2[] = {
0b10001,
0b01010,
0b00100,
0b11111,
0b11111,
0b00100,
0b01010,
0b10001
};
const byte sunChar3[] = {
0b00100,
0b10101,
0b01110,
0b11111,
0b11111,
0b01110,
0b10101,
0b00100
};
const byte moonChar1[] = {
0b00000,
0b01100,
0b10010,
0b10000,
0b10000,
0b10010,
0b01100,
0b00000
};
const byte moonChar2[] = {
0b00000,
0b01110,
0b10001,
0b10000,
0b10000,
0b10001,
0b01110,
0b00000
};
const byte moonChar3[] = {
0b00000,
0b01111,
0b10000,
0b10000,
0b10000,
0b10000,
0b01111,
0b00000
};
const byte cloudChar1[] = {
0b00000,
0b00000,
0b01110,
0b11111,
0b11111,
0b01111,
0b00000,
0b00000
};
const byte cloudChar2[] = {
0b00000,
0b00000,
0b00111,
0b01111,
0b11111,
0b11110,
0b00000,
0b00000
};
const byte cloudChar3[] = {
0b00000,
0b00000,
0b01110,
0b01111,
0b11111,
0b11111,
0b00000,
0b00000
};
const byte rainChar1[] = {
0b01010,
0b00000,
0b01110,
0b11111,
0b01010,
0b10101,
0b01010,
0b00000
};
const byte rainChar2[] = {
0b10101,
0b00000,
0b01110,
0b11111,
0b10101,
0b01010,
0b10101,
0b00000
};
const byte rainChar3[] = {
0b01010,
0b00000,
0b01110,
0b11111,
0b00000,
0b01010,
0b00000,
0b10101
};
const byte rainChar4[] = {
0b10101,
0b00000,
0b01110,
0b11111,
0b00100,
0b10101,
0b01000,
0b00010
};
const byte degreeChar[] = {
0b01100,
0b10010,
0b10010,
0b01100,
0b00000,
0b00000,
0b00000,
0b00000
};
const byte wifiChar1[] = {
0b00000,
0b00000,
0b00000,
0b00000,
0b00000,
0b00000,
0b00100,
0b00000
};
const byte wifiChar2[] = {
0b00000,
0b00000,
0b00000,
0b00000,
0b01110,
0b00000,
0b00100,
0b00000
};
const byte wifiChar3[] = {
0b00000,
0b00000,
0b11111,
0b00000,
0b01110,
0b00000,
0b00100,
0b00000
};
const byte wifiChar4[] = {
0b11111,
0b00000,
0b11111,
0b00000,
0b01110,
0b00000,
0b00100,
0b00000
};
// ====== UTILITY FUNCTIONS ======
void time_is_set() {
Serial.println("NTP time synchronized");
reSync = true;
timeInitialized = true;
}
void setTimezoneForCity(int cityIndex) {
if (cityIndex >= 0 && cityIndex < numCities) {
configTime(cities[cityIndex].timezone.c_str(), NTP_SERVER);
}
}
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;
}
void showLoadingMessage(const char* message) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(message);
}
bool getCurrentLocalTime(struct tm &timeinfo) {
time(&now);
if (now < 946684800) return false;
localtime_r(&now, &timeinfo);
return true;
}
void invertChar(byte* dest, const byte* src) {
for (int i = 0; i < 8; i++) {
dest[i] = ~src[i] & 0x1F;
}
}
// ====== BUTTON HANDLING ======
int readButton() {
int adc = analogRead(KEYPAD_PIN);
if (adc > 1000) return btnNONE;
if (adc < 10) return btnRIGHT;
if (adc < 195) return btnUP;
if (adc < 450) return btnDOWN;
if (adc < 800) return btnLEFT;
return btnNONE;
}
void processButtonAction(int button) {
switch (button) {
case btnRIGHT:
// Next city
currentCity = (currentCity + 1) % numCities;
setTimezoneForCity(currentCity);
lcd.clear();
if (millis() - cities[currentCity].lastUpdate > 300000) {
showLoadingMessage("Loading...");
updateWeatherData(currentCity);
lcd.clear();
}
currentDisplayMode = MODE_NORMAL;
break;
case btnLEFT:
// Cycle through display modes
currentDisplayMode = (DisplayMode)((currentDisplayMode + 1) % 5);
handleDisplayModeChange();
break;
case btnUP:
// Refresh weather data
showLoadingMessage("Refreshing...");
updateWeatherData(currentCity);
lcd.clear();
currentDisplayMode = MODE_NORMAL;
break;
case btnDOWN:
// Previous city
currentCity = (currentCity - 1 + numCities) % numCities;
setTimezoneForCity(currentCity);
lcd.clear();
if (millis() - cities[currentCity].lastUpdate > 300000) {
showLoadingMessage("Loading...");
updateWeatherData(currentCity);
lcd.clear();
}
currentDisplayMode = MODE_NORMAL;
break;
}
}
void handleDisplayModeChange() {
lcd.clear();
switch (currentDisplayMode) {
case MODE_NORMAL:
break;
case MODE_DETAILED:
lastDetailDisplay = millis();
break;
case MODE_SUN_INFO:
lastDetailDisplay = millis();
break;
case MODE_PRESSURE:
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();
currentDisplayMode = MODE_NORMAL;
break;
case MODE_BRIGHTNESS:
backlightLevel = (backlightLevel + 1) % 4;
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();
currentDisplayMode = MODE_NORMAL;
break;
}
}
// ====== NETWORK FUNCTIONS ======
bool initializeTime() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Syncing time...");
setTimezoneForCity(currentCity);
settimeofday_cb(time_is_set);
int attempts = 0;
while (!reSync && attempts < 20) {
lcd.setCursor(attempts % 16, 1);
lcd.print(".");
delay(250); // Faster sync animation
yield();
attempts++;
}
if (reSync && timeInitialized) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Time synced!");
struct tm timeinfo;
if (getCurrentLocalTime(timeinfo)) {
lcd.setCursor(0, 1);
char timeStr[16];
sprintf(timeStr, "%02d:%02d %s", timeinfo.tm_hour, timeinfo.tm_min,
cities[currentCity].name.substring(0, 3).c_str());
lcd.print(timeStr);
}
delay(1500);
return true;
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Time sync failed");
lcd.setCursor(0, 1);
lcd.print("Check connection");
delay(2000);
return false;
}
}
void connectWiFi() {
WiFi.begin(ssid, password);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi Setup");
int attempts = 0;
int progressPhase = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
// Enhanced loading animation with faster progression
lcd.setCursor(0, 1);
int progressPos = (attempts * 16) / 40;
for (int i = 0; i < 16; i++) {
if (i < progressPos - 1) {
lcd.print("="); // Body of progress bar
} else if (i == progressPos - 1) {
// Animated head progressing from "." to "x" to "*"
char headChar;
if (progressPhase % 3 == 0) headChar = '.';
else if (progressPhase % 3 == 1) headChar = 'x';
else headChar = '*';
lcd.print(headChar);
} else if (i == progressPos) {
lcd.print(">"); // Leading edge
} else {
lcd.print(" ");
}
}
delay(100);
progressPhase++;
attempts++;
yield();
}
lcd.clear();
if (WiFi.status() == WL_CONNECTED) {
lcd.setCursor(0, 0);
lcd.print("WiFi Connected!");
lcd.setCursor(0, 1);
lcd.print(WiFi.localIP().toString().substring(0, 15));
delay(1000);
} else {
lcd.setCursor(0, 0);
lcd.print("WiFi Failed!");
lcd.setCursor(0, 1);
lcd.print("Check settings");
delay(2000);
}
lcd.clear();
}
bool updateWeatherData(int cityIndex) {
if (WiFi.status() != WL_CONNECTED) {
showLoadingMessage("No WiFi!");
delay(1500);
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(10000);
http.addHeader("User-Agent", "ESP8266WeatherStation/2.0");
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
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();
cities[cityIndex].isNight = cities[cityIndex].iconCode.endsWith("n");
http.end();
return true;
} else {
showLoadingMessage("Parse error");
delay(1500);
lcd.clear();
}
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("HTTP Error: ");
lcd.print(httpCode);
delay(1500);
lcd.clear();
}
http.end();
return false;
}
// ====== ASTRONOMICAL CALCULATIONS ======
void calcHorizontalCoordinates(time_t currentTime, double lat, double lon, double &azimuth, double &elevation) {
double julianDay = (currentTime / 86400.0) + 2440587.5;
double n = julianDay - 2451545.0;
double L = fmod(280.460 + 0.9856474 * n, 360.0);
if (L < 0) L += 360.0;
double g = fmod(357.528 + 0.9856003 * n, 360.0) * PI / 180.0;
if (g < 0) g += 2 * PI;
double lambda = (L + 1.915 * sin(g) + 0.020 * sin(2 * g));
if (lambda >= 360.0) lambda -= 360.0;
if (lambda < 0) lambda += 360.0;
lambda = lambda * PI / 180.0;
double alpha = atan2(cos(23.439 * PI / 180.0) * sin(lambda), cos(lambda));
double delta = asin(sin(23.439 * PI / 180.0) * sin(lambda));
double GMST = fmod(280.460 + 360.9856235 * n, 360.0);
if (GMST < 0) GMST += 360.0;
double LST = GMST + lon;
if (LST >= 360.0) LST -= 360.0;
if (LST < 0) LST += 360.0;
double H = (LST * PI / 180.0) - alpha;
lat = lat * PI / 180.0;
elevation = asin(sin(lat) * sin(delta) + cos(lat) * cos(delta) * cos(H));
azimuth = atan2(sin(H), cos(H) * sin(lat) - tan(delta) * cos(lat));
elevation = elevation * 180.0 / PI;
azimuth = azimuth * 180.0 / PI;
if (azimuth < 0) azimuth += 360.0;
}
void calculateSunriseSunset(int cityIndex) {
if (!timeInitialized) return;
time(&now);
struct tm timeinfo;
localtime_r(&now, &timeinfo);
timeinfo.tm_hour = 12;
timeinfo.tm_min = 0;
timeinfo.tm_sec = 0;
time_t noonTime = mktime(&timeinfo);
double lat = cities[cityIndex].latitude;
double lon = cities[cityIndex].longitude;
time_t sunriseTime = noonTime - 12 * 3600;
double elevation, azimuth;
for (int minutes = 0; minutes < 720; minutes++) {
calcHorizontalCoordinates(sunriseTime + minutes * 60, lat, lon, azimuth, elevation);
if (elevation > -0.833) {
struct tm sunriseInfo;
time_t sunriseTimeCalc = sunriseTime + minutes * 60;
localtime_r(&sunriseTimeCalc, &sunriseInfo);
cities[cityIndex].sunriseHour = sunriseInfo.tm_hour;
cities[cityIndex].sunriseMin = sunriseInfo.tm_min;
break;
}
}
time_t sunsetTime = noonTime;
for (int minutes = 0; minutes < 720; minutes++) {
calcHorizontalCoordinates(sunsetTime + minutes * 60, lat, lon, azimuth, elevation);
if (elevation < -0.833) {
struct tm sunsetInfo;
time_t sunsetTimeCalc = sunsetTime + minutes * 60;
localtime_r(&sunsetTimeCalc, &sunsetInfo);
cities[cityIndex].sunsetHour = sunsetInfo.tm_hour;
cities[cityIndex].sunsetMin = sunsetInfo.tm_min;
break;
}
}
}
void updateSunData(int cityIndex) {
if (!timeInitialized) return;
time(&now);
double azimuth, elevation;
calcHorizontalCoordinates(now, cities[cityIndex].latitude, cities[cityIndex].longitude, azimuth, elevation);
cities[cityIndex].sunElevation = elevation;
cities[cityIndex].sunAzimuth = azimuth;
static int lastDay = -1;
struct tm timeinfo;
localtime_r(&now, &timeinfo);
if (timeinfo.tm_yday != lastDay) {
calculateSunriseSunset(cityIndex);
lastDay = timeinfo.tm_yday;
}
}
bool isNightTime() {
if (!timeInitialized) return false;
time(&now);
double azimuth, elevation;
calcHorizontalCoordinates(now, cities[currentCity].latitude, cities[currentCity].longitude, azimuth, elevation);
return elevation < -6.0;
}
// ====== DISPLAY FUNCTIONS ======
void loadWeatherChars() {
byte tempChar[8];
byte invertedChar[8];
String weather = toLowerCase(cities[currentCity].description);
bool isNight = cities[currentCity].isNight; // Use API's night indicator for weather icons
bool isDayTime = !isNightTime(); // Flip the logic - invert during DAY, not night
// Weather animation sprites
bool isThunderstorm = weather.indexOf("thunderstorm") != -1;
if (isThunderstorm) {
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 (isDayTime) {
invertChar(invertedChar, tempChar);
lcd.createChar(0, invertedChar);
} else {
lcd.createChar(0, tempChar);
}
}
} else if (weather.indexOf("rain") != -1 || weather.indexOf("drizzle") != -1) {
const byte* rainFrames[] = {rainChar1, rainChar2, rainChar3, rainChar4};
memcpy_P(tempChar, rainFrames[animFrame % 4], 8);
if (isDayTime) {
invertChar(invertedChar, tempChar);
lcd.createChar(0, invertedChar);
} else {
lcd.createChar(0, tempChar);
}
} else if (weather.indexOf("cloud") != -1) {
const byte* cloudFrames[] = {cloudChar1, cloudChar2, cloudChar3};
memcpy_P(tempChar, cloudFrames[animFrame % 3], 8);
if (isDayTime) {
invertChar(invertedChar, tempChar);
lcd.createChar(0, invertedChar);
} else {
lcd.createChar(0, tempChar);
}
} else {
// Clear weather: choose sun or moon based on API's day/night indicator
if (isNight) {
const byte* moonFrames[] = {moonChar1, moonChar2, moonChar3};
memcpy_P(tempChar, moonFrames[animFrame % 3], 8);
if (isDayTime) {
invertChar(invertedChar, tempChar);
lcd.createChar(0, invertedChar);
} else {
lcd.createChar(0, tempChar);
}
} else {
const byte* sunFrames[] = {sunChar1, sunChar2, sunChar3};
memcpy_P(tempChar, sunFrames[animFrame % 3], 8);
if (isDayTime) {
invertChar(invertedChar, tempChar);
lcd.createChar(0, invertedChar);
} else {
lcd.createChar(0, tempChar);
}
}
}
// Degree symbol
memcpy_P(tempChar, degreeChar, 8);
lcd.createChar(1, tempChar);
// WiFi signal strength - also follow the inverted day/night logic
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 {
byte noSignal[] = {0,4,4,4,4,0,4,0}; // Exclamation mark
memcpy(tempChar, noSignal, 8);
}
if (isDayTime) {
invertChar(invertedChar, tempChar);
lcd.createChar(2, invertedChar);
} else {
lcd.createChar(2, tempChar);
}
}
void bootAnimation() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Core1D Labs");
lcd.setCursor(0, 1);
lcd.print("Weather Station");
// Smooth fade-in
for (int i = 0; i <= 255; i += 10) {
analogWrite(LCD_BACKLIGHT, i);
delay(15);
}
delay(1000);
// Fast loading animation
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Initializing...");
for (int phase = 0; phase <= 16; phase++) {
lcd.setCursor(0, 1);
for (int i = 0; i < 16; i++) {
if (i < phase - 1) {
lcd.print("="); // Body
} else if (i == phase - 1) {
// Animated head
char headChar = (phase < 8) ? '.' : (phase < 12) ? 'x' : '*';
lcd.print(headChar);
} else if (i == phase) {
lcd.print(">");
} else {
lcd.print(" ");
}
}
delay(50); // Fast animation
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("System Ready");
delay(800);
lcd.clear();
systemReady = true;
}
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 > 1500) {
scrollPause = false;
lastScrollTime = millis();
}
lcd.setCursor(startPos, line);
String displayText = text.substring(0, maxLen);
lcd.print(displayText);
return;
}
if (millis() - lastScrollTime > 180) {
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) {
currentDisplayMode = MODE_NORMAL;
lcd.clear();
}
}
void updateAnimations() {
if (!systemReady) return;
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();
}
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;
case 1: baseBrightness = 50; break;
case 2: baseBrightness = 180; break;
case 3: baseBrightness = 255; break;
default: baseBrightness = 180; break;
}
targetBrightness = baseBrightness;
// Night dimming
if (cities[currentCity].isNight && backlightLevel != 0 && backlightLevel != 3) {
int dimmedBrightness = (int)(targetBrightness * 0.6);
targetBrightness = (dimmedBrightness > 20) ? dimmedBrightness : 20;
}
// Auto-dim after inactivity
if (millis() - lastKeyPress > 120000 && backlightLevel > 0) {
int autoDimBrightness = targetBrightness / 4;
targetBrightness = (autoDimBrightness > 10) ? autoDimBrightness : 10;
}
// Smooth 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();
}
void displayWeather() {
if (!systemReady) return;
switch (currentDisplayMode) {
case MODE_SUN_INFO:
displaySunInfo();
break;
case MODE_DETAILED:
displayDetailedWeather();
break;
case MODE_NORMAL:
default:
displayNormalWeather();
break;
}
}
void displaySunInfo() {
lcd.setCursor(0, 0);
lcd.print("Sun ");
lcd.print(cities[currentCity].name.substring(0, 4));
lcd.print(" ");
if (cities[currentCity].sunElevation >= 0) {
lcd.print("+");
}
lcd.print(cities[currentCity].sunElevation, 1);
lcd.write(1); // degree symbol
lcd.setCursor(0, 1);
char sunTimes[16];
sprintf(sunTimes, "%02d:%02d->%02d:%02d",
cities[currentCity].sunriseHour, cities[currentCity].sunriseMin,
cities[currentCity].sunsetHour, cities[currentCity].sunsetMin);
lcd.print(sunTimes);
}
void displayDetailedWeather() {
// Date display
if (timeInitialized) {
struct tm timeinfo;
if (getCurrentLocalTime(timeinfo)) {
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(" ");
}
}
// Scrolling weather details
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);
}
void displayNormalWeather() {
// City and temperature
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
char timeStr[8];
if (timeInitialized) {
struct tm timeinfo;
if (getCurrentLocalTime(timeinfo)) {
if (colonBlink && (millis() / 1000) % 2 == 0) {
sprintf(timeStr, "%02d %02d", timeinfo.tm_hour, timeinfo.tm_min);
} else {
sprintf(timeStr, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
}
} else {
strcpy(timeStr, "--:--");
}
} 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(" ");
}
}
}
// ====== MAIN PROGRAM ======
void setup() {
Serial.begin(74880);
// Initialize LCD
lcd.begin(16, 2);
pinMode(LCD_BACKLIGHT, OUTPUT);
analogWrite(LCD_BACKLIGHT, 0);
// Boot sequence
bootAnimation();
connectWiFi();
// Initialize time
if (!initializeTime()) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Time sync failed");
lcd.setCursor(0, 1);
lcd.print("Limited mode");
delay(2000);
}
// Get initial weather
showLoadingMessage("Getting weather");
updateWeatherData(currentCity);
delay(1000);
lcd.clear();
// Initialize system
lastKeyPress = millis();
lastWeatherUpdate = millis();
lastSunUpdate = millis();
updateSunData(currentCity);
Serial.println("Weather Station Ready");
}
void loop() {
// Handle time updates
time(&now);
if (now != prevTime) {
prevTime = now;
}
// Button handling
int buttonPressed = readButton();
if (buttonPressed != btnNONE) {
// Debounce check
if (buttonPressed != lastButtonState && millis() - lastButtonTime > BUTTON_DEBOUNCE) {
lastButtonState = buttonPressed;
lastButtonTime = millis();
lastKeyPress = millis();
processButtonAction(buttonPressed);
}
}
// Sun data update every 5 minutes
if (millis() - lastSunUpdate > 300000) {
updateSunData(currentCity);
lastSunUpdate = millis();
}
// Weather update with adaptive timing
unsigned long weatherUpdateInterval;
bool isSleepHours = false;
if (timeInitialized) {
struct tm timeinfo;
if (getCurrentLocalTime(timeinfo)) {
int hour = timeinfo.tm_hour;
isSleepHours = (hour >= 22 || hour < 6);
}
}
// Adaptive intervals
if (isSleepHours) {
weatherUpdateInterval = 1800000; // 30 minutes
} else if (millis() - lastKeyPress > 300000) {
weatherUpdateInterval = 900000; // 15 minutes
} else {
weatherUpdateInterval = 600000; // 10 minutes
}
if (millis() - lastWeatherUpdate > weatherUpdateInterval) {
if (WiFi.status() == WL_CONNECTED) {
updateWeatherData(currentCity);
lastWeatherUpdate = millis();
// Auto-show details occasionally
static int updateCounter = 0;
updateCounter++;
if (!isSleepHours && updateCounter % 5 == 0) {
currentDisplayMode = MODE_DETAILED;
lastDetailDisplay = millis();
lcd.clear();
}
}
}
// Auto-hide detailed modes after 30 secs
if ((currentDisplayMode == MODE_DETAILED || currentDisplayMode == MODE_SUN_INFO) &&
millis() - lastDetailDisplay > 30000) {
currentDisplayMode = MODE_NORMAL;
lcd.clear();
}
// WiFi recon
if (WiFi.status() != WL_CONNECTED) {
static unsigned long lastReconnectAttempt = 0;
if (millis() - lastReconnectAttempt > 60000) {
showLoadingMessage("WiFi reconnect..");
WiFi.reconnect();
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 10) {
delay(500);
lcd.setCursor(attempts % 16, 1);
lcd.print(".");
attempts++;
}
lcd.clear();
if (WiFi.status() == WL_CONNECTED) {
lcd.setCursor(0, 0);
lcd.print("WiFi restored!");
delay(1000);
} else {
lcd.setCursor(0, 0);
lcd.print("WiFi failed!");
delay(1000);
}
lcd.clear();
lastReconnectAttempt = millis();
}
}
// Main display updates
displayWeather();
updateAnimations();
adjustBrightness();
// Adaptive delays
unsigned long timeSinceLastKey = millis() - lastKeyPress;
if (isSleepHours && timeSinceLastKey > 300000) {
delay(5000); // Very slow during sleep
} else if (timeSinceLastKey > 120000) {
delay(1000); // Slow when idle
} else if (currentDisplayMode == MODE_DETAILED) {
delay(50); // Fast for scrolling
} else {
delay(100); // Normal rate
}
// System health monitoring
static unsigned long lastHealthCheck = 0;
if (millis() - lastHealthCheck > 300000) {
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();
}
ESP.wdtFeed();
lastHealthCheck = millis();
}
// Handling millis() overflow
static unsigned long lastMillisCheck = millis();
if (millis() < lastMillisCheck) {
lastKeyPress = millis();
lastWeatherUpdate = millis();
lastAnimation = millis();
lastDetailDisplay = millis();
lastSunUpdate = millis();
prevTime = 0;
}
lastMillisCheck = millis();
yield();
}
/*
// Call this function in loop() to debug buttons
void debugButtons() {
static unsigned long lastDebug = 0;
if (millis() - lastDebug > 1000) { // Every second
int adc = analogRead(KEYPAD_PIN);
Serial.print("ADC Value: ");
Serial.println(adc);
lastDebug = millis();
}
}
// Additional Matterials and Declarations:
/**
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ ASTRONOMICAL CALCULATIONS - TECHNICAL DOCUMENTATION ║
* ║ Simplified Core1D Solar Position Engine ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* IMPORTANT DISCLAIMER REGARDING ASTRONOMICAL ACCURACY:
*
* This firmware implements SIMPLIFIED astronomical calculations optimized for microcontroller environments.
* While mathematically sound and suitable for weather station applications, these calculations are NOT intended
* for precision astronomy, navigation, or scientific research requiring high accuracy.
*
* ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
* │ SCOPE AND LIMITATIONS │
* ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
* │ │
* │ Enhanced day/night detection and user experience for weather display applications │
* │ │
* │ ACCURACY LEVEL: ±2-4 minutes for sunrise/sunset times, ±1-2 degrees for sun position │
* │ │
* │ SUITABLE FOR: |
* │ Weather station day/night mode switching |
* │ Approximate sunrise/sunset display for user information │
* │ Basic solar elevation awareness for brightness control │
* │ Educational demonstration of astronomical concepts │
* │ Home automation timing based on solar events │
* │ │
* │ NOT SUITABLE FOR: │
* │ Precision astronomy calculations │
* │ Navigation or GPS applications │
* │ Solar panel positioning systems │
* │ Professional meteorological equipment │
* │ Scientific research requiring sub-minute accuracy │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ MATHEMATICAL APPROACH OVERVIEW ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* Our implementation uses a streamlined version of standard astronomical algorithms, optimized for:
* - Low computational overhead suitable for ESP8266 (80MHz, limited float precision)
* - Sufficient accuracy for weather station applications
* - Minimal memory footprint
* - Fast execution for real-time updates
*
* ┌─ Coordinate System Transformations ────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ The sun's position calculation involves several coordinate system transformations: │
* │ │
* │ 1. TIME → JULIAN DAY NUMBER │
* │ Convert UTC timestamp to Julian Day for astronomical calculations │
* │ │
* │ 2. JULIAN DAY → SOLAR COORDINATES │
* │ Calculate sun's position in ecliptic coordinate system │
* │ │
* │ 3. ECLIPTIC → EQUATORIAL │
* │ Transform to right ascension and declination │
* │ │
* │ 4. EQUATORIAL → HORIZONTAL │
* │ Final transformation to local azimuth and elevation angles │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ DETAILED ALGORITHM BREAKDOWN ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Step 1: Time System Conversion ───────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ To convert Unix timestamp to Julian Day Number │
* │ │
* │ Julian Day = (Unix_Time / 86400) + 2440587.5 │
* │ │
* │ WHERE: │
* │ • Unix_Time = Seconds since January 1, 1970 (UTC) │
* │ • 86400 = Seconds per day │
* │ • 2440587.5 = Julian Day Number for Unix epoch (Jan 1, 1970 12:00 UTC) │
* │ │
* │ Julian Day Numbers provide a continuous count of days since a fixed reference point, │
* │ simplifying astronomical calculations by eliminating calendar complexities. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 2: Days Since J2000.0 Epoch ─────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Calculating days since the J2000.0 astronomical epoch │
* │ │
* │ n = Julian_Day - 2451545.0 │
* │ │
* │ WHERE: │
* │ • n = Days since J2000.0 epoch │
* │ • 2451545.0 = Julian Day Number for J2000.0 (January 1, 2000, 12:00 TT) │
* │ │
* │ J2000.0 is the standard reference epoch for modern astronomical calculations, │
* │ providing a recent baseline that minimizes computational errors from large time differences. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 3: Solar Mean Longitude ─────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Calculating the sun's mean longitude (ignoring perturbations) │
* │ │
* │ L = (280.460 + 0.9856474 × n) mod 360° │
* │ │
* │ WHERE: │
* │ • L = Solar mean longitude (degrees) │
* │ • 280.460 = Mean longitude at J2000.0 epoch (degrees) │
* │ • 0.9856474 = Mean daily motion of sun (degrees per day) │
* │ • n = Days since J2000.0 │
* │ │
* │ The sun's mean longitude represents its average position along the ecliptic, │
* │ ignoring short-term variations. The daily motion accounts for Earth's orbital motion. │
* │ │
* │ Mean longitude is used instead of true longitude, ignoring: │
* │ • Orbital eccentricity variations beyond first-order correction │
* │ • Planetary perturbations │
* │ • Nutation effects │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 4: Solar Mean Anomaly ───────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Calculating the sun's mean anomaly (angular distance from periapsis) │
* │ │
* │ g = (357.528 + 0.9856003 × n) mod 360° │
* │ │
* │ WHERE: │
* │ • g = Solar mean anomaly (degrees) │
* │ • 357.528 = Mean anomaly at J2000.0 epoch (degrees) │
* │ • 0.9856003 = Mean daily motion in anomaly (degrees per day) │
* │ │
* │ Mean anomaly represents Earth's position in its elliptical orbit relative to perihelion │
* │ (closest approach to sun). This is crucial for calculating orbital velocity variations. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 5: Equation of Center (Orbital Correction) ──────────────────────────────────────────────────────────────┐
* │ │
* │ To apply first-order correction for Earth's elliptical orbit │
* │ │
* │ Correction = 1.915 × sin(g) + 0.020 × sin(2g) │
* │ λ = L + Correction │
* │ │
* │ ∀ │
* │ • λ = True solar longitude (ecliptic longitude) │
* │ • L = Mean solar longitude │
* │ • g = Mean anomaly │
* │ • 1.915 = First-order eccentricity coefficient │
* │ • 0.020 = Second-order eccentricity coefficient │
* │ │
* │ Earth's orbit is elliptical, not circular. The sun appears to move faster when Earth │
* │ is closer (perihelion) and slower when farther (aphelion). This correction accounts for that variation. │
* │ │
* │ We only include first two terms of the infinite series. Higher-order terms │
* │ contribute less than 0.1° and are omitted for computational efficiency. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 6: Ecliptic to Equatorial Transformation ────────────────────────────────────────────────────────────────┐
* │ │
* │ To convert ecliptic coordinates to equatorial (celestial) coordinates │
* │ │
* │ α = atan2(cos(ε) × sin(λ), cos(λ)) [Right Ascension] │
* │ δ = asin(sin(ε) × sin(λ)) [Declination] │
* │ │
* │ WHERE: │
* │ • α = Right ascension (celestial longitude) │
* │ • δ = Declination (celestial latitude) │
* │ • λ = Solar ecliptic longitude │
* │ • ε = 23.439° = Earth's axial tilt (obliquity of ecliptic) │
* │ │
* │ The ecliptic coordinate system is based on Earth's orbital plane, while equatorial │
* │ coordinates are based on Earth's equatorial plane. The transformation accounts for Earth's axial tilt. │
* │ │
* │ We use a fixed obliquity value, ignoring: │
* │ • Long-term obliquity variations (±1.3° over 41,000 years) │
* │ • Nutation (periodic variations of ±9.2 arcseconds) │
* │ • Aberration effects │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 7: Sidereal Time Calculation ────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ To calculate Greenwich Mean Sidereal Time and Local Sidereal Time │
* │ │
* │ GMST = (280.460 + 360.9856235 × n) mod 360° [Greenwich Mean Sidereal Time] │
* │ LST = GMST + longitude [Local Sidereal Time] │
* │ │
* │ WHERE: │
* │ • GMST = Greenwich Mean Sidereal Time (degrees) │
* │ • LST = Local Sidereal Time (degrees) │
* │ • 280.460 = GMST at J2000.0 epoch │
* │ • 360.9856235 = Sidereal day progression rate (degrees per day) │
* │ • longitude = Observer's longitude (positive East) │
* │ │
* │ Sidereal time represents the rotation of Earth relative to distant stars, │
* │ different from solar time due to Earth's orbital motion. It's essential for converting │
* │ between celestial coordinates and local horizontal coordinates. │
* │ │
* │ NB: A sidereal day (23h 56m 4s) is shorter than a solar day (24h) because │
* │ Earth must rotate an extra ~1° to face the sun again due to orbital motion. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 8: Hour Angle Calculation ───────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Calculating the sun's hour angle (angular distance from local meridian) │
* │ │
* │ H = LST - α │
* │ │
* │ WHERE: │
* │ • H = Hour angle (radians, converted from degrees) │
* │ • LST = Local Sidereal Time │
* │ • α = Sun's right ascension │
* │ │
* │ Hour angle represents how far the sun is from the local meridian (the north-south line │
* │ passing through the zenith). H = 0° when sun is due south, negative before noon, positive after noon. │
* │ │
* │ KEY: │
* │ • H = -90° → Sun is due east (6 AM solar time) │
* │ • H = 0° → Sun is due south (12 PM solar time) │
* │ • H = +90° → Sun is due west (6 PM solar time) │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ┌─ Step 9: Horizontal Coordinate Calculation ────────────────────────────────────────────────────────────────────┐
* │ │
* │ Final transformation to local horizontal coordinates (altitude and azimuth) │
* │ │
* │ ELEVATION: alt = asin(sin(φ) × sin(δ) + cos(φ) × cos(δ) × cos(H)) │
* │ AZIMUTH: az = atan2(sin(H), cos(H) × sin(φ) - tan(δ) × cos(φ)) + 180° │
* │ │
* │ WHERE: │
* │ • alt = Solar elevation angle (degrees above horizon) │
* │ • az = Solar azimuth angle (degrees from north, clockwise) │
* │ • φ = Observer's latitude (positive north) │
* │ • δ = Sun's declination │
* │ • H = Hour angle │
* │ │
* │ COORDS: │
* │ • Elevation: 0° = horizon, +90° = zenith, -90° = nadir │
* │ • Azimuth: 0° = north, 90° = east, 180° = south, 270° = west │
* │ │
* │ KEY: │
* │ • Positive elevation → Sun is above horizon (daytime) │
* │ • Negative elevation → Sun is below horizon (nighttime) │
* │ • Azimuth shows compass direction to sun │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ SUNRISE/SUNSET CALCULATION METHOD ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Sunrise/Sunset Algorithm ─────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ Iterative search for times when sun crosses the horizon │
* │ │
* │ SUNRISE SEARCH: │
* │ 1. Start at local midnight (00:00) │
* │ 2. Calculate sun elevation for each minute │
* │ 3. Find first time when elevation > -0.833° │
* │ │
* │ SUNSET SEARCH: │
* │ 1. Start at local noon (12:00) │
* │ 2. Calculate sun elevation for each minute │
* │ 3. Find first time when elevation < -0.833° │
* │ │
* │ HORIZON DEFINITION: -0.833° accounts for: │
* │ • Geometric horizon: 0° │
* │ • Atmospheric refraction: ~0.567° (lifts sun image) │
* │ • Solar disk radius: ~0.267° (sun's edge vs center) │
* │ • Total correction: -0.833° below geometric horizon │
* │ │
* │ While analytical solutions exist, they involve complex inverse trigonometric functions │
* │ and multiple correction terms. The iterative approach is: │
* │ • Simpler to implement and understand │
* │ • More numerically stable on limited-precision systems │
* │ • Easier to debug and verify │
* │ • Sufficient accuracy for our application (±1-2 minutes) │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ COMPUTATIONAL OPTIMIZATIONS ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ ESP8266-Specific Optimizations ───────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ FLOATING-POINT PRECISION: │
* │ • ESP8266 uses 32-bit IEEE 754 single precision (~7-8 decimal digits) │
* │ • Sufficient for weather station accuracy requirements │
* │ • Avoids computational overhead of double precision on this platform │
* │ │
* │ TRIGONOMETRIC FUNCTIONS: │
* │ • Uses hardware-accelerated sin/cos/atan2 functions when available │
* │ • Angles normalized to avoid large argument errors │
* │ • Results cached when possible to avoid repeated calculations │
* │ │
* │ UPDATE FREQUENCY: │
* │ • Sun position: Updated every 5 minutes (sufficient for UI changes) │
* │ • Sunrise/sunset: Calculated once per day (stored in city structure) │
* │ • Display updates: Separate timing to maintain responsive UI │
* │ │
* │ MEMORY MANAGEMENT: │
* │ • Intermediate calculations use local variables (stack allocation) │
* │ • Results stored in persistent city data structure │
* │ • No dynamic memory allocation during calculations │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ ACCURACY VALIDATION & TESTING ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Validation Methods ───────────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* │ COMPARISON REFERENCES: │
* │ • timeanddate.com sunrise/sunset calculator │
* │ • NOAA Solar Position Calculator │
* │ • Local weather service predictions │
* │ • Smartphone weather apps with astronomical data │
* │ │
* │ TYPICAL ACCURACY ACHIEVED: │
* │ • Sunrise/sunset times: ±2-4 minutes │
* │ • Solar elevation: ±1-2 degrees │
* │ • Solar azimuth: ±2-3 degrees │
* │ │
* │ FACTORS AFFECTING ACCURACY: │
* │ • Geographic location (higher latitudes may show larger deviations) │
* │ • Time of year (accuracy varies slightly with seasons) │
* │ • Local topography not accounted for (mountains, hills) │
* │ • Atmospheric conditions (refraction varies with weather) │
* │ │
* │ TESTING RECOMMENDATIONS: │
* │ Users can verify accuracy by comparing displayed sunrise/sunset times with local weather │
* │ services or astronomical websites. Small discrepancies are normal and expected. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*
* ╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
* ║ FUTURE ENHANCEMENT POSSIBILITIES ║
* ╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
*
* ┌─ Potential Improvements for Advanced Users ────────────────────────────────────────────────────────────────────┐
* │ │
* │ HIGHER PRECISION ALGORITHMS: │
* │ • VSOP87 theory for planetary positions (much more complex) │
* │ • JPL DE series ephemeris data (requires large lookup tables) │
* │ • Nutation and aberration corrections │
* │ │
* │ ATMOSPHERIC CORRECTIONS: │
* │ • Pressure and temperature dependent refraction │
* │ • Humidity effects on atmospheric refraction │
* │ • Altitude-dependent horizon corrections │
* │ │
* │ GEOGRAPHIC ENHANCEMENTS: │
* │ • Topographic horizon profiles │
* │ • Local terrain shadowing effects │
* │ • Elevation-dependent calculations │
* │ │
* │ COMPUTATIONAL IMPROVEMENTS: │
* │ • Lookup tables for frequently used values │
* │ • Interpolation methods for smoother calculations │
* │ • Adaptive precision based on required accuracy │
* │ │
* │ NOTE: These enhancements would significantly increase complexity and memory usage, │
* │ and may not be justified for weather station applications. │
* │ │
* └────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
DISCLAIMER: Final Firmware may have exactly the same appearance. Appearance and layout is subject to the future development direction.


