chore: remove internal version notes, clean up CHANGELOG links
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
29d7c7f8ad
commit
55bec76d88
+21
-7
@@ -8,12 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [1.9.2] - 2026-01-06
|
## [1.9.2] - 2026-01-06
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **CRITICAL**: WiFi credentials no longer cleared on connection failure
|
- **CRITICAL**: WiFi credentials no longer cleared on connection failure
|
||||||
- Previous behavior erased SSID/password after failed connection attempts
|
- Previous behavior erased SSID/password after failed connection attempts
|
||||||
- Now credentials persist indefinitely through WiFi outages
|
- Now credentials persist indefinitely through WiFi outages
|
||||||
- OTA update credential loss issue (SDK credentials now tried first, then EEPROM)
|
- OTA update credential loss issue (SDK credentials now tried first, then EEPROM)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **WiFi Resilience**: Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max)
|
- **WiFi Resilience**: Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max)
|
||||||
- **Fallback AP**: "TJ56654-Setup" enabled after ~5 min of failed attempts
|
- **Fallback AP**: "TJ56654-Setup" enabled after ~5 min of failed attempts
|
||||||
- Device continues retry attempts while AP is active (dual STA+AP mode)
|
- 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
|
- **SDK credentials support**: Tries WiFiManager-stored credentials first
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Clock continues running with last synced time during WiFi outages
|
- Clock continues running with last synced time during WiFi outages
|
||||||
- Improved user experience during network failures
|
- Improved user experience during network failures
|
||||||
- Removed aggressive credential clearing behavior
|
- Removed aggressive credential clearing behavior
|
||||||
|
|
||||||
### Network Activity
|
### Network Activity
|
||||||
|
|
||||||
- NTP sync: 1 hour interval (pool.ntp.org:123 UDP)
|
- NTP sync: 1 hour interval (pool.ntp.org:123 UDP)
|
||||||
- Weather fetch: 30 min interval (api.open-meteo.com HTTP)
|
- Weather fetch: 30 min interval (api.open-meteo.com HTTP)
|
||||||
- mDNS: continuous (224.0.0.251 UDP multicast)
|
- 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
|
## [1.9.1] - 2026-01-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **CRITICAL**: WiFi startup sequence - synchronous connection in setup() to ensure proper initialization order
|
- **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)
|
- Display blank screen for 10+ seconds on boot (now shows time after ~15 seconds)
|
||||||
- "DNS resolution failed" errors during startup
|
- "DNS resolution failed" errors during startup
|
||||||
- Sunrise/sunset labels cut off on 128px screen (removed labels, arrows are self-explanatory)
|
- Sunrise/sunset labels cut off on 128px screen (removed labels, arrows are self-explanatory)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Hybrid WiFi model: synchronous in setup(), async reconnect in loop()
|
- Hybrid WiFi model: synchronous in setup(), async reconnect in loop()
|
||||||
- Display formatting: superscript degree symbol and lowercase 'c' for temperature
|
- 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
|
- Sunrise/sunset screen now shows daylight duration (e.g., "Day 9h 41m") instead of static "Sun Times" text
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- Added detailed v1.9.1_HYBRID_FIX.md explaining startup sequence problem and solution
|
- Added detailed v1.9.1_HYBRID_FIX.md explaining startup sequence problem and solution
|
||||||
|
|
||||||
## [1.9.0] - 2026-01-02
|
## [1.9.0] - 2026-01-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Fully async architecture (zero blocking operations in loop)
|
- Fully async architecture (zero blocking operations in loop)
|
||||||
- Custom async NTP implementation (manual UDP packet handling)
|
- Custom async NTP implementation (manual UDP packet handling)
|
||||||
- Async HTTP weather fetch (AsyncHTTPRequest library)
|
- 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
|
- Independent epoch tracking for accurate time between NTP syncs
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Replaced blocking NTPClient with custom async UDP implementation
|
- Replaced blocking NTPClient with custom async UDP implementation
|
||||||
- Replaced blocking HTTP weather with AsyncHTTPRequest
|
- Replaced blocking HTTP weather with AsyncHTTPRequest
|
||||||
- Removed all delay() calls from loop()
|
- Removed all delay() calls from loop()
|
||||||
- WiFi connection now async (later fixed in v1.9.1)
|
- WiFi connection now async (later fixed in v1.9.1)
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
- Loop time: 10ms → <1ms (10x improvement)
|
- Loop time: 10ms → <1ms (10x improvement)
|
||||||
- Weather fetch: 1-10s blocking → 0ms
|
- Weather fetch: 1-10s blocking → 0ms
|
||||||
- NTP sync: 5-20s 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
|
- OTA updates now work during active weather fetching
|
||||||
|
|
||||||
### Technical
|
### Technical
|
||||||
|
|
||||||
- RAM usage: +536 bytes (36,980 → 37,516)
|
- RAM usage: +536 bytes (36,980 → 37,516)
|
||||||
- Flash usage: +1040 bytes (407,500 → 408,540)
|
- Flash usage: +1040 bytes (407,500 → 408,540)
|
||||||
- IRAM: 61,987 bytes (94% - stable)
|
- 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
|
## [1.8.0] - 2026-01-01
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- **CRITICAL**: Removed hardcoded WiFi credentials
|
- **CRITICAL**: Removed hardcoded WiFi credentials
|
||||||
- Integrated WiFiManager for secure captive portal setup
|
- Integrated WiFiManager for secure captive portal setup
|
||||||
- Added config validation (magic number check)
|
- Added config validation (magic number check)
|
||||||
- Input sanitization to prevent buffer overflows
|
- Input sanitization to prevent buffer overflows
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- IRAM overflow crisis (94% → 70% via ICACHE_FLASH_ATTR)
|
- IRAM overflow crisis (94% → 70% via ICACHE_FLASH_ATTR)
|
||||||
- NTP interval bug (config value was ignored, always used hardcoded 1 hour)
|
- NTP interval bug (config value was ignored, always used hardcoded 1 hour)
|
||||||
- Boolean parsing errors in JSON config import/export
|
- 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
|
- Memory leaks from String concatenation in web handlers
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Web responses now use chunked transfer (eliminated 140+ String concatenations)
|
- Web responses now use chunked transfer (eliminated 140+ String concatenations)
|
||||||
- Applied ICACHE_FLASH_ATTR to 26 functions (moved code from IRAM to Flash)
|
- Applied ICACHE_FLASH_ATTR to 26 functions (moved code from IRAM to Flash)
|
||||||
- Improved error handling throughout codebase
|
- Improved error handling throughout codebase
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
- Peak heap usage reduced by ~8KB
|
- Peak heap usage reduced by ~8KB
|
||||||
- EEPROM validation prevents loading corrupted config
|
- EEPROM validation prevents loading corrupted config
|
||||||
|
|
||||||
## [1.7.0] - 2025-12-31
|
## [1.7.0] - 2025-12-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initial working firmware with correct display support
|
- Initial working firmware with correct display support
|
||||||
- NTP time synchronization
|
- NTP time synchronization
|
||||||
- Weather data from Open-Meteo API (free, no API key required)
|
- 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
|
- EEPROM configuration persistence
|
||||||
|
|
||||||
### Hardware Discovery
|
### Hardware Discovery
|
||||||
|
|
||||||
- Identified display as GM009605v4.3 (not TM1637 or TM1650)
|
- Identified display as GM009605v4.3 (not TM1637 or TM1650)
|
||||||
- Discovered swapped I2C pins: SDA=GPIO0, SCL=GPIO2
|
- Discovered swapped I2C pins: SDA=GPIO0, SCL=GPIO2
|
||||||
- Switched to Adafruit_SSD1306 library
|
- Switched to Adafruit_SSD1306 library
|
||||||
|
|
||||||
### Replaced
|
### Replaced
|
||||||
|
|
||||||
- QWeather API → Open-Meteo (no registration required)
|
- QWeather API → Open-Meteo (no registration required)
|
||||||
- Proprietary firmware → Open source custom firmware
|
- Proprietary firmware → Open source custom firmware
|
||||||
- Insecure WiFi handling → WiFiManager with timeout
|
- 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)
|
## [1.6.0] - 2025-12-30 (unreleased)
|
||||||
|
|
||||||
### Attempted
|
### Attempted
|
||||||
|
|
||||||
- TM1650 LED driver support (incorrect - device has OLED)
|
- TM1650 LED driver support (incorrect - device has OLED)
|
||||||
|
|
||||||
## [1.5.0] - 2025-12-29 (unreleased)
|
## [1.5.0] - 2025-12-29 (unreleased)
|
||||||
|
|
||||||
### Attempted
|
### Attempted
|
||||||
|
|
||||||
- TM1637 7-segment display support (incorrect - device has OLED)
|
- 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
|
- **Minor version** (1.X.0): New features, backward-compatible
|
||||||
- **Patch version** (1.9.X): Bug fixes, no new features
|
- **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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user