From 55bec76d881f504b089464261126962948fb3bff Mon Sep 17 00:00:00 2001 From: Alex Petrochenko Date: Thu, 14 May 2026 17:42:59 +0100 Subject: [PATCH] chore: remove internal version notes, clean up CHANGELOG links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/v1.9_RELEASE_NOTES.md, v1.9.1_HYBRID_FIX.md, v1.9.2_WIFI_RESILIENCE.md were internal working documents (incomplete checklists, dev notes). CHANGELOG.md already covers all versions — links removed, status updated to v1.9.3. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 28 ++- docs/v1.9.1_HYBRID_FIX.md | 218 ----------------------- docs/v1.9.2_WIFI_RESILIENCE.md | 313 --------------------------------- docs/v1.9_RELEASE_NOTES.md | 160 ----------------- 4 files changed, 21 insertions(+), 698 deletions(-) delete mode 100644 docs/v1.9.1_HYBRID_FIX.md delete mode 100644 docs/v1.9.2_WIFI_RESILIENCE.md delete mode 100644 docs/v1.9_RELEASE_NOTES.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a935026..719ca65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.9.2] - 2026-01-06 ### Fixed + - **CRITICAL**: WiFi credentials no longer cleared on connection failure - Previous behavior erased SSID/password after failed connection attempts - Now credentials persist indefinitely through WiFi outages - OTA update credential loss issue (SDK credentials now tried first, then EEPROM) ### Added + - **WiFi Resilience**: Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max) - **Fallback AP**: "TJ56654-Setup" enabled after ~5 min of failed attempts - Device continues retry attempts while AP is active (dual STA+AP mode) @@ -23,11 +25,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **SDK credentials support**: Tries WiFiManager-stored credentials first ### Changed + - Clock continues running with last synced time during WiFi outages - Improved user experience during network failures - Removed aggressive credential clearing behavior ### Network Activity + - NTP sync: 1 hour interval (pool.ntp.org:123 UDP) - Weather fetch: 30 min interval (api.open-meteo.com HTTP) - mDNS: continuous (224.0.0.251 UDP multicast) @@ -36,22 +40,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.9.1] - 2026-01-03 ### Fixed + - **CRITICAL**: WiFi startup sequence - synchronous connection in setup() to ensure proper initialization order - Display blank screen for 10+ seconds on boot (now shows time after ~15 seconds) - "DNS resolution failed" errors during startup - Sunrise/sunset labels cut off on 128px screen (removed labels, arrows are self-explanatory) ### Changed + - Hybrid WiFi model: synchronous in setup(), async reconnect in loop() - Display formatting: superscript degree symbol and lowercase 'c' for temperature - Sunrise/sunset screen now shows daylight duration (e.g., "Day 9h 41m") instead of static "Sun Times" text ### Documentation + - Added detailed v1.9.1_HYBRID_FIX.md explaining startup sequence problem and solution ## [1.9.0] - 2026-01-02 ### Added + - Fully async architecture (zero blocking operations in loop) - Custom async NTP implementation (manual UDP packet handling) - Async HTTP weather fetch (AsyncHTTPRequest library) @@ -59,12 +67,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Independent epoch tracking for accurate time between NTP syncs ### Changed + - Replaced blocking NTPClient with custom async UDP implementation - Replaced blocking HTTP weather with AsyncHTTPRequest - Removed all delay() calls from loop() - WiFi connection now async (later fixed in v1.9.1) ### Performance + - Loop time: 10ms → <1ms (10x improvement) - Weather fetch: 1-10s blocking → 0ms - NTP sync: 5-20s blocking → 0ms @@ -72,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - OTA updates now work during active weather fetching ### Technical + - RAM usage: +536 bytes (36,980 → 37,516) - Flash usage: +1040 bytes (407,500 → 408,540) - IRAM: 61,987 bytes (94% - stable) @@ -79,12 +90,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.8.0] - 2026-01-01 ### Security + - **CRITICAL**: Removed hardcoded WiFi credentials - Integrated WiFiManager for secure captive portal setup - Added config validation (magic number check) - Input sanitization to prevent buffer overflows ### Fixed + - IRAM overflow crisis (94% → 70% via ICACHE_FLASH_ATTR) - NTP interval bug (config value was ignored, always used hardcoded 1 hour) - Boolean parsing errors in JSON config import/export @@ -92,17 +105,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Memory leaks from String concatenation in web handlers ### Changed + - Web responses now use chunked transfer (eliminated 140+ String concatenations) - Applied ICACHE_FLASH_ATTR to 26 functions (moved code from IRAM to Flash) - Improved error handling throughout codebase ### Performance + - Peak heap usage reduced by ~8KB - EEPROM validation prevents loading corrupted config ## [1.7.0] - 2025-12-31 ### Added + - Initial working firmware with correct display support - NTP time synchronization - Weather data from Open-Meteo API (free, no API key required) @@ -114,11 +130,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - EEPROM configuration persistence ### Hardware Discovery + - Identified display as GM009605v4.3 (not TM1637 or TM1650) - Discovered swapped I2C pins: SDA=GPIO0, SCL=GPIO2 - Switched to Adafruit_SSD1306 library ### Replaced + - QWeather API → Open-Meteo (no registration required) - Proprietary firmware → Open source custom firmware - Insecure WiFi handling → WiFiManager with timeout @@ -126,11 +144,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.0] - 2025-12-30 (unreleased) ### Attempted + - TM1650 LED driver support (incorrect - device has OLED) ## [1.5.0] - 2025-12-29 (unreleased) ### Attempted + - TM1637 7-segment display support (incorrect - device has OLED) --- @@ -141,12 +161,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Minor version** (1.X.0): New features, backward-compatible - **Patch version** (1.9.X): Bug fixes, no new features -## Links - -- [Full v1.9 Release Notes](docs/v1.9_RELEASE_NOTES.md) -- [v1.9.1 Hybrid Fix Details](docs/v1.9.1_HYBRID_FIX.md) -- [v1.9.2 WiFi Resilience](docs/v1.9.2_WIFI_RESILIENCE.md) - --- -**Status**: v1.9.2 is production-ready and actively used 24/7. +**Status**: v1.9.3 is production-ready and actively used 24/7. diff --git a/docs/v1.9.1_HYBRID_FIX.md b/docs/v1.9.1_HYBRID_FIX.md deleted file mode 100644 index 6481fd4..0000000 --- a/docs/v1.9.1_HYBRID_FIX.md +++ /dev/null @@ -1,218 +0,0 @@ -# v1.9.1 - Hybrid Async Fix - -## Problem in v1.9.0 - -**Symptoms:** -- Display shows blank screen ~10 seconds after boot -- "DNS resolution failed" errors in logs -- Time appears only after 10+ seconds - -**Root Cause:** -```cpp -void setup() { - loadConfig(); - setupWiFi(); // Returns IMMEDIATELY (async) - setupOTA(); // WiFi NOT ready! - setupWebServer(); // WiFi NOT ready! - testInternetConnectivity(); // WiFi NOT ready! -> "DNS resolution failed" -} -``` - -WiFi became **fully asynchronous**, but this is **wrong for setup()**: -- OTA, web server, NTP **require a ready WiFi connection** -- `testInternetConnectivity()` ran **before WiFi connected** -- Time on display appeared only when async WiFi finally connected - -## Solution: Hybrid Model - -| Phase | WiFi Mode | Blocking | Reason | -|-------|-----------|----------|--------| -| **setup()** | **Synchronous** | 10 sec | Required for OTA/web/NTP initialization | -| **loop()** | **Asynchronous** | 0 sec | Don't freeze on reconnect | - -### Code Changes - -#### 1. setupWiFi() - Now Synchronous - -```cpp -void ICACHE_FLASH_ATTR setupWiFi() { - Serial.println("WiFi Setup - Synchronous for initial connection"); - - WiFi.hostname(config.hostname); - - if (strlen(config.ssid) > 0) { - WiFi.mode(WIFI_STA); - WiFi.begin(config.ssid, config.password); - - // SYNCHRONOUS wait (max 10 seconds) - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 20) { - delay(500); - showNumber(attempts, false); // Show progress on display - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - // WiFi ready for OTA/web/NTP! - showIP(); - wifiConnState = WIFI_CONN_CONNECTED; - return; - } - } - - // Fallback to WiFiManager if credentials didn't work - // ... -} -``` - -#### 2. loop() - Async Reconnect - -```cpp -void loop() { - // WiFi reconnection (async, non-blocking) - static unsigned long lastWiFiCheck = 0; - if (millis() - lastWiFiCheck > 5000) { - if (WiFi.status() != WL_CONNECTED && wifiConnState == WIFI_CONN_CONNECTED) { - Serial.println("WiFi disconnected, attempting async reconnect..."); - WiFi.begin(); // Async reconnect - wifiConnState = WIFI_CONN_CONNECTING; - wifiConnectStart = millis(); - } - lastWiFiCheck = millis(); - } - - processWiFiConnection(); // Async reconnect handling - - // Other async operations - processNTPResponse(); - fetchWeatherAsync(); - // ... -} -``` - -## Test Results - -### Before (v1.9.0) -``` -[0-5s] -> Display init -[5-15s] -> WiFi connecting (async, setup() returns immediately) -[15-20s] -> OTA/web init WITHOUT WiFi -> Errors! -[20s] -> testInternetConnectivity() WITHOUT WiFi -> "DNS resolution failed" -[15-25s] -> WiFi finally connects (async) -[25-30s] -> NTP sync begins - -Display blank for 10+ seconds -"DNS resolution failed" errors -``` - -### After (v1.9.1) -``` -[0-5s] -> Display init + startup animation -[5-15s] -> WiFi connection (SYNCHRONOUS, setup() waits) - WiFi connected! -[15-20s] -> OTA/web/NTP init WITH WiFi - Internet test: PASSED - No DNS errors! -[20-30s] -> First async NTP sync - Time synced! - -Display shows time immediately after WiFi connects (~15 sec) -No "DNS resolution failed" errors -Proper initialization order -``` - -## Startup Timeline - -``` -+----------------------------------------------------------+ -| SETUP PHASE (Synchronous WiFi) | -+----------------------------------------------------------+ -| | -| [0s] +-------------+ | -| | Display | Startup animation | -| [5s] | Init | "Weather Clock v1.9.1" | -| +-------------+ | -| | -| [5s] +---------------------------------+ | -| | WiFi Connect (SYNCHRONOUS) | | -| | - Connecting to network... | | -| [15s] | - Connected! IP assigned | | -| +---------------------------------+ | -| | | -| WiFi is READY here | -| | | -| [15s] +---------------------------------+ | -| | OTA Init (needs WiFi) | | -| | Web Server (needs WiFi) | | -| [20s] | NTP Client (needs WiFi) | | -| | Internet Test (needs WiFi) | | -| +---------------------------------+ | -| | -| [20s] Setup complete! -> loop() starts | -| | -+----------------------------------------------------------+ - -+----------------------------------------------------------+ -| LOOP PHASE (Async Operations) | -+----------------------------------------------------------+ -| | -| [Every loop] +------------------------+ | -| | WiFi Health Check | | -| | (every 5 sec) | | -| | If disconnected: | | -| | -> Async reconnect | | -| +------------------------+ | -| | -| [Every loop] +------------------------+ | -| | Async NTP Processing | | -| | (non-blocking) | | -| +------------------------+ | -| | -| [Every 30m] +------------------------+ | -| | Async Weather Fetch | | -| | (non-blocking) | | -| +------------------------+ | -| | -| Loop time: <1ms (no blocking!) | -| | -+----------------------------------------------------------+ -``` - -## Benefits of Hybrid Approach - -### In setup(): -1. **Correct initialization order** - WiFi -> OTA -> web -> NTP -2. **No DNS errors** - internet connectivity test runs AFTER WiFi -3. **Predictable behavior** - setup() completes when everything is ready -4. **Display shows time immediately** - no need to wait for async WiFi - -### In loop(): -1. **No freeze on reconnect** - async handling of WiFi loss -2. **Async NTP** - doesn't block loop -3. **Async weather** - doesn't block loop -4. **Exponential backoff** - smart retry on errors -5. **Loop <1ms** - always responsive device - -## Memory - -| Resource | v1.9.0 | v1.9.1 | Change | -|----------|--------|--------|--------| -| RAM | 37,516 | 37,644 | +128 bytes | -| IRAM | 61,987 | 61,987 | 0 bytes | -| Flash | 408,540 | 408,844 | +304 bytes | - -Minimal memory changes (+0.3%) for critical UX improvement. - -## Conclusion - -**v1.9.1 implements the ideal balance:** -- Setup: Synchronous for reliable initialization -- Loop: Asynchronous for responsiveness - -**Result:** -- Display shows time after 15 sec (instead of 25+ sec) -- No "DNS resolution failed" errors -- Correct startup order -- Device doesn't freeze on WiFi loss during operation - -**Status**: Production ready diff --git a/docs/v1.9.2_WIFI_RESILIENCE.md b/docs/v1.9.2_WIFI_RESILIENCE.md deleted file mode 100644 index bc27145..0000000 --- a/docs/v1.9.2_WIFI_RESILIENCE.md +++ /dev/null @@ -1,313 +0,0 @@ -# v1.9.2 - WiFi Resilience - -## Problem in v1.9.1 - -**Symptoms:** -- After WiFi outage (router restart, temporary network issues), device enters AP mode -- WiFi credentials are cleared, requiring manual reconfiguration -- User must reconnect to "TJ56654-Setup" AP and re-enter WiFi password -- This happens every time WiFi goes down temporarily - -**Root Cause:** - -Aggressive credential clearing after connection failures: -```cpp -void processWiFiConnection() { - // After 5 failed attempts... - if (wifiRetry.currentRetry >= wifiRetry.maxRetries) { - // Clear credentials! - memset(config.ssid, 0, sizeof(config.ssid)); - memset(config.password, 0, sizeof(config.password)); - saveConfig(); - - // Start AP mode for reconfiguration - WiFiManager wm; - wm.startConfigPortal("TJ56654-Setup", "12345678"); - } -} -``` - -**Impact:** -- Poor user experience during network outages -- Unnecessary manual intervention required -- Device unusable until reconfigured - -## Solution: Resilient WiFi - -### Key Changes - -| Feature | Before (v1.9.1) | After (v1.9.2) | -|---------|-----------------|----------------| -| Credential clearing | After 5 failed attempts | Never | -| Retry strategy | Give up after 5 tries | Infinite retries | -| Retry interval | Fixed 5 seconds | Exponential backoff (5s-5min) | -| Fallback AP | After clearing credentials | After ~5 min (keeps credentials) | -| Clock during outage | Shows connection counter | Shows last synced time | -| User notification | Numeric counter | "No WiFi" message | - -### Implementation - -#### 1. WiFi Retry Configuration - -```cpp -// Infinite retries with exponential backoff -struct WiFiRetryConfig { - uint8_t currentRetry = 0; - unsigned long nextRetryTime = 0; - static const unsigned long MAX_BACKOFF_MS = 300000; // Max 5 minutes - - unsigned long getBackoffDelay() { - // 5s, 10s, 20s, 40s, 80s, 160s, 300s (max) - unsigned long delay = 5000UL * (1UL << currentRetry); - return (delay > MAX_BACKOFF_MS) ? MAX_BACKOFF_MS : delay; - } - - void recordAttempt() { - currentRetry++; - nextRetryTime = millis() + getBackoffDelay(); - } - - void reset() { - currentRetry = 0; - nextRetryTime = 0; - } -}; -``` - -#### 2. No Credential Clearing - -```cpp -void processWiFiConnection() { - if (WiFi.status() == WL_CONNECTED) { - if (wifiConnState != WIFI_CONN_CONNECTED) { - Serial.println("WiFi connected!"); - wifiConnState = WIFI_CONN_CONNECTED; - wifiRetry.reset(); - - // Disable fallback AP if it was enabled - if (WiFi.getMode() == WIFI_AP_STA) { - Serial.println("Disabling fallback AP"); - WiFi.mode(WIFI_STA); - } - } - return; - } - - // Connection failed - but DON'T clear credentials! - if (millis() >= wifiRetry.nextRetryTime) { - Serial.printf("WiFi retry %d, next in %lu sec\n", - wifiRetry.currentRetry, - wifiRetry.getBackoffDelay() / 1000); - - wifiRetry.recordAttempt(); - - // Try to connect with SDK or EEPROM credentials - if (strlen(config.password) > 0) { - WiFi.begin(config.ssid, config.password); - } else { - WiFi.begin(); // Use SDK-stored credentials - } - - // Enable fallback AP after 5+ attempts (~2.5 min) - if (wifiRetry.currentRetry >= 5 && WiFi.getMode() != WIFI_AP_STA) { - Serial.println("Enabling fallback AP (dual mode)"); - WiFi.mode(WIFI_AP_STA); - WiFi.softAP("TJ56654-Setup", "12345678"); - } - } -} -``` - -#### 3. "No WiFi" Display - -```cpp -void ICACHE_FLASH_ATTR showNoWiFi(unsigned long nextRetrySeconds) { - display.clearDisplay(); - display.setTextSize(2); - display.setTextColor(SSD1306_WHITE); - - // Center "No WiFi" text - display.setCursor(20, 8); - display.println("No WiFi"); - - // Show retry countdown - display.setTextSize(1); - display.setCursor(10, 52); - if (nextRetrySeconds < 60) { - display.printf("Retry in %lu sec", nextRetrySeconds); - } else { - display.printf("Retry in %lu min", nextRetrySeconds / 60); - } - - display.display(); -} -``` - -#### 4. Clock Continues During Outage - -```cpp -void updateDisplay() { - if (WiFi.status() != WL_CONNECTED) { - // Show "No WiFi" status - unsigned long nextRetry = (wifiRetry.nextRetryTime > millis()) - ? (wifiRetry.nextRetryTime - millis()) / 1000 - : 0; - showNoWiFi(nextRetry); - return; - } - - // Normal display modes when WiFi is connected - // ... -} -``` - -#### 5. WiFi Disconnect Indicator - -When WiFi is disconnected but time is still valid, show "!" in date line: -```cpp -void showTimeMode() { - // ... time display ... - - // Date line with WiFi status indicator - display.setCursor(0, 0); - if (WiFi.status() != WL_CONNECTED) { - display.print("! "); // WiFi disconnected indicator - } - display.print(dateString); -} -``` - -#### 6. SDK Credentials Support - -WiFiManager stores credentials in ESP SDK flash, not EEPROM. This caused issues after OTA updates: - -```cpp -void setupWiFi() { - // Try SDK-stored credentials first (from WiFiManager) - if (strlen(config.password) > 0) { - // EEPROM has credentials - use them - WiFi.begin(config.ssid, config.password); - } else { - // EEPROM empty - try SDK credentials - WiFi.begin(); // Uses last saved WiFi from SDK - } -} -``` - -## Retry Backoff Schedule - -| Attempt | Delay | Cumulative Time | -|---------|-------|-----------------| -| 1 | 5 sec | 5 sec | -| 2 | 10 sec | 15 sec | -| 3 | 20 sec | 35 sec | -| 4 | 40 sec | 1 min 15 sec | -| 5 | 80 sec | 2 min 35 sec | -| 6 | 160 sec | 5 min 15 sec | -| 7+ | 300 sec (5 min) | +5 min each | - -**Fallback AP enabled after attempt 5** (~2.5 min of failures) - -## Network Activity Summary - -| Service | Interval | Endpoint | Protocol | Daily Requests | -|---------|----------|----------|----------|----------------| -| NTP | 1 hour | pool.ntp.org:123 | UDP | 24 | -| Weather | 30 min | api.open-meteo.com | HTTP | 48 | -| mDNS | continuous | 224.0.0.251 | UDP multicast | N/A | - -**Total**: ~50-75 requests/day (minimal network load) - -## Dual STA+AP Mode - -When fallback AP is enabled, device operates in dual mode: -- **STA (Station)**: Continues trying to connect to configured WiFi -- **AP (Access Point)**: Allows user to reconfigure if needed - -```cpp -// Enable dual mode -WiFi.mode(WIFI_AP_STA); -WiFi.softAP("TJ56654-Setup", "12345678"); - -// Continue WiFi connection attempts in STA mode -WiFi.begin(config.ssid, config.password); -``` - -When WiFi reconnects: -```cpp -if (WiFi.status() == WL_CONNECTED && WiFi.getMode() == WIFI_AP_STA) { - // Disable AP, return to STA-only mode - WiFi.mode(WIFI_STA); - Serial.println("WiFi reconnected, disabled fallback AP"); -} -``` - -## Test Scenarios - -### Scenario 1: Router Restart (5 minutes) - -**Before (v1.9.1):** -1. WiFi disconnects -2. 5 retry attempts (25 seconds) -3. Credentials cleared -4. Device enters AP mode -5. User must reconfigure WiFi - -**After (v1.9.2):** -1. WiFi disconnects -2. Retry with backoff (5s, 10s, 20s, 40s, 80s) -3. After ~2.5 min: Fallback AP enabled (dual mode) -4. Device continues retrying -5. Router comes back -6. Device reconnects automatically -7. Fallback AP disabled -8. No user intervention needed - -### Scenario 2: Extended Outage (30+ minutes) - -**v1.9.2 Behavior:** -1. Retries every 5 minutes after initial backoff -2. Fallback AP available for reconfiguration if needed -3. Clock shows last synced time -4. "No WiFi" indicator on display -5. Reconnects automatically when WiFi returns - -### Scenario 3: OTA Update - -**Problem**: WiFiManager stores credentials in SDK flash, but our EEPROM may be empty after update. - -**Solution**: Try SDK credentials first: -```cpp -if (strlen(config.password) > 0) { - WiFi.begin(config.ssid, config.password); -} else { - WiFi.begin(); // SDK credentials -} -``` - -## Memory Impact - -| Resource | v1.9.1 | v1.9.2 | Change | -|----------|--------|--------|--------| -| RAM | 37,644 | 37,800 | +156 bytes | -| IRAM | 61,987 | 61,987 | 0 bytes | -| Flash | 408,844 | 409,100 | +256 bytes | - -Minimal memory increase (+0.4%) for significant UX improvement. - -## Conclusion - -**v1.9.2 provides true WiFi resilience:** -- Never clears credentials -- Infinite retries with smart backoff -- Fallback AP for emergency reconfiguration -- Clock continues during outages -- Clear user feedback on display - -**Results:** -- No more manual reconfiguration after WiFi outages -- Device recovers automatically when network returns -- User can still reconfigure via fallback AP if needed -- Minimal network overhead (~50 requests/day) - -**Status**: Production ready diff --git a/docs/v1.9_RELEASE_NOTES.md b/docs/v1.9_RELEASE_NOTES.md deleted file mode 100644 index 011d21e..0000000 --- a/docs/v1.9_RELEASE_NOTES.md +++ /dev/null @@ -1,160 +0,0 @@ -# TJ-56-654 Weather Clock - v1.9.0 Release Notes - -## Release Date -2026-01-03 - -## Overview -Version 1.9 is a major async refactoring that eliminates **ALL blocking operations** from the firmware, transforming the device from a frequently-frozen system into a fully responsive, production-ready clock. - -## Performance Improvements - -### Before (v1.8): -- **WiFi connection**: 10 seconds blocking (v1.7 credential migration) -- **NTP sync**: 5-20 seconds blocking -- **Weather fetch**: 1-10 seconds blocking -- **Loop delay**: 10ms blocking every iteration -- **Total freeze time**: Up to **45+ seconds** - -### After (v1.9): -- **WiFi connection**: 0ms blocking (async state machine) -- **NTP sync**: 0ms blocking (async UDP) -- **Weather fetch**: 0ms blocking (AsyncHTTPRequest) -- **Loop delay**: 0ms (removed) -- **Total freeze time**: **0 seconds** ✅ - -**Loop responsiveness**: <1ms typical (was 10ms minimum) - -## New Features - -### 1. Async HTTP Weather Fetch (v1.9.2) -- **Library**: AsyncHTTPRequest_Generic v1.13.0 -- **State machine**: IDLE → REQUESTING → SUCCESS/FAILED -- **Callback**: `onWeatherResponse()` processes data non-blocking -- **Result**: OTA updates work during weather fetch - -### 2. Async NTP Implementation (v1.9.3) -- **Manual NTP**: Custom UDP packet building/parsing -- **Independent epoch tracking**: `syncedEpoch`, `syncedMillis`, `timeIsSynced` -- **Workaround**: NTPClient library is inherently blocking, so we bypass it -- **State machine**: IDLE → REQUEST_SENT → WAITING → SUCCESS/FAILED -- **Timeout**: 5 seconds non-blocking - -### 3. Async WiFi Connection (v1.9.4) -- **v1.7 migration**: Non-blocking credential attempt -- **State machine**: IDLE → CONNECTING → CONNECTED/FAILED -- **Fallback**: WiFiManager (still blocking, but only on first boot) -- **Benefit**: Device stays responsive during connection attempts - -### 4. Zero Blocking Delays (v1.9.5) -- **Removed**: `delay(10)` from loop() -- **Replaced**: `delay(3000)` in `showIP()` with scheduled clear via `ipDisplayUntil` timer -- **Kept**: Startup animation delays (acceptable, only runs once in setup) -- **Kept**: Pre-reboot delays (acceptable, device is rebooting anyway) - -### 5. Exponential Backoff Retries (v1.9.6) -- **Strategy**: 1s → 2s → 4s (max 3 retries) -- **Struct**: `RetryConfig` with `getBackoffDelay()`, `scheduleRetry()`, `isRetryTime()` -- **Applied to**: - - NTP failures: graceful retry instead of hammering server - - Weather API failures: same exponential strategy -- **Benefit**: Network resilience without aggressive retry behavior - -## Memory Footprint - -| Resource | v1.8 (baseline) | v1.9.0 (final) | Increase | -|----------|----------------|----------------|----------| -| RAM | 36,980 bytes | 37,516 bytes | +536 bytes (1.4%) | -| IRAM | 61,987 bytes | 61,987 bytes | 0 bytes | -| Flash | 407,500 bytes | 408,540 bytes | +1040 bytes (0.25%) | - -### Memory Budget Status: -- **RAM**: 37,516 / 80,192 bytes (46%) - ✅ Safe -- **IRAM**: 61,987 / 65,536 bytes (94%) - ⚠️ Near limit but stable -- **Flash**: 408,540 / 1,048,576 bytes (38%) - ✅ Plenty of room - -**Verdict**: Less than 1.5% RAM increase for fully async operation - excellent ROI! - -## Code Quality Improvements - -### Line Count: -- **v1.8**: ~1,950 lines -- **v1.9**: 2,026 lines (+76 lines for async infrastructure) - -### New Data Structures: -```cpp -enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED }; -enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED }; -enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED, SKIP_ASYNC }; - -struct RetryConfig { - uint8_t maxRetries = 3; - uint8_t currentRetry = 0; - unsigned long nextRetryTime = 0; - - unsigned long getBackoffDelay(); - void scheduleRetry(); - bool isRetryTime(); - void reset(); - bool maxRetriesReached(); -}; -``` - -### Key Functions Added: -1. `onWeatherResponse()` - AsyncHTTPRequest callback -2. `fetchWeatherAsync()` - Non-blocking weather fetch -3. `sendNTPRequestAsync()` - Manual NTP packet send -4. `processNTPResponse()` - Non-blocking NTP response check -5. `processWiFiConnection()` - Async WiFi state handler -6. `getAsyncEpoch()` - Independent time tracking - -## Testing Checklist - -Before OTA upload to device: - -- [x] Compilation successful -- [x] Memory usage within safe limits -- [ ] OTA responsive during weather fetch -- [ ] Web UI responsive during NTP sync -- [ ] Display updates smoothly during network ops -- [ ] Exponential backoff triggers on failures -- [ ] Max retry limits respected -- [ ] Config persistence across reboots -- [ ] 24-hour stability test - -## Migration from v1.8 - -**OTA Upgrade Path**: ✅ Safe -- Config struct unchanged - binary compatible -- All settings preserved -- Smooth transition from v1.7 credentials - -**Rollback**: Keep v1.8.bin for emergency rollback via web upload - -## Known Limitations - -1. **WiFiManager**: Still blocking on first boot (acceptable) -2. **IRAM**: At 94% - future features must use `ICACHE_FLASH_ATTR` -3. **Startup animation**: Still uses blocking delays (acceptable, only runs once) -4. **Test handlers**: Some debug endpoints still block (low priority) - -## Next Steps (v2.0) - -Future improvements planned for v2.0: -1. **Modular architecture**: Split into separate files -2. **ArduinoJson**: Replace manual JSON parsing -3. **Constants**: Eliminate remaining magic numbers -4. **Code deduplication**: Display helper refactoring -5. **Enhanced error handling**: Pre-flight checks, better validation - -## Credits - -**Firmware**: TJ-56-654 Weather Clock -**Hardware**: ESP-01S (ESP8266EX, 1MB flash) -**Author**: Generated with Claude Code (Opus 4.5) -**Repository**: clock/firmware/clock_ntp_ota_v1.9 - -## Conclusion - -v1.9 transforms the weather clock from a frequently-frozen device into a **fully responsive**, **production-ready** system with **zero blocking operations**. The 536-byte RAM overhead is a negligible cost for the massive UX improvement of instant responsiveness to OTA, web requests, and display updates even during active network operations. - -**Status**: ✅ Ready for OTA deployment