Power-cycle the device 3 times within 10 seconds to trigger factory reset:
- Clears WiFi credentials (SSID + password) in EEPROM
- Shows "FACTORY RESET / WiFi: TJ56654-Setup / Pass: 12345678" on display
- Reboots into WiFiManager AP mode for reconfiguration
Counter stored at EEPROM offset 480 (well past Config ~260 bytes).
Clears automatically after 10s of stable operation.
Prevents the need for FTDI/USB recovery when credentials are corrupted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C1 (critical): Add 15s watchdog for WEATHER_REQUESTING state — resets to
IDLE if TCP connection hangs silently, preventing permanent weather death
C2 (critical): Fix millis() rollover in all retry timers — replace unsafe
`millis() >= nextRetryTime` with subtraction-safe `(millis() - nextRetryTime)
< 0x80000000UL` in RetryConfig and WiFiRetryConfig; fix boot guard with
static flag instead of raw millis() comparison
H1 (high): Fix DST last-Sunday formula — was using incorrect year-only
heuristic; now derives weekday of the 31st from current day's tm_wday:
`weekdayOf31 = (tm_wday + (31 - day)) % 7`. Verified: March 2026 = 29th ✓
H2 (high): Replace String+= with snprintf+sendContent in handleAPITime,
handleAPIStatus, handleAPIDebug, handleAPIWeather — eliminates permanent
heap fragmentation from JS polling every second
H3 (high): Add volatile to weatherState and ntpState — shared between
ESPAsyncTCP callbacks and main loop; prevents stale register-cached reads
RAM: 37,268 bytes (46%) — reduced from 37,560 due to String elimination
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After 3 consecutive API failures, weatherRetry.reset() was never called
and weatherState stayed WEATHER_FAILED permanently. Both the retry path
and periodic refresh path require WEATHER_IDLE, so no new requests were
ever made — even after the API recovered.
Fix: when maxRetriesReached(), reset weatherRetry and set weatherState
back to WEATHER_IDLE so the next periodic refresh interval (default 30min)
triggers a fresh attempt automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
clock_ntp_ota_v1.9/ → weather_clock/ (version no longer baked into path)
clock_ntp_ota_v1.9.ino → weather_clock.ino
Version is tracked in config.h (FIRMWARE_VERSION), not in filenames.
Updated references in README, CONTRIBUTING, and docs/INSTALLATION.md.
Note: forks using the old path will need to sync this rename before
submitting new PRs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
src/clock_ntp_ota_v1.9.ino was a duplicate of firmware/clock_ntp_ota_v1.9/
that had already drifted out of sync. Removing it eliminates the need to
patch both copies on every PR.
Updated CONTRIBUTING.md build path to point to firmware/clock_ntp_ota_v1.9.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PROJECT_CONTEXT.md, PROJECT_STRUCTURE.md, PUBLISH_TO_GITHUB.md were
working notes not intended for contributors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WiFi.begin() without params connects to any SDK-cached network, including
open public hotspots, and then overwrites config.ssid with the wrong SSID.
Now Try 1 (SDK credentials) is only attempted on first boot when no SSID
is configured. Once the user has saved credentials, we go straight to
Try 2 (EEPROM config), so the correct network is always used.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use localTime instead of epochTime when formatting date string,
so date rolls over at local midnight, not UTC midnight (fixes#5)
- Remove WEATHER_SUCCESS state assignment that blocked periodic refresh;
state correctly stays WEATHER_IDLE after successful fetch (fixes#7)
- Fix weather debug page "Last update" to show elapsed seconds
(millis() - lastUpdate) instead of raw timestamp
Applied from PR #6 by @Mysteoa with a compilation fix in web_server.cpp
(missing closing parenthesis in snprintf_P call).
Co-authored-by: Mysteoa <mysteoa@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Problem
Previous versions would clear WiFi credentials on connection failure,
requiring manual reconfiguration via captive portal after each WiFi outage.
## Solution
- Never clear credentials on connection failure
- Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max)
- Fallback AP ("TJ56654-Setup") enabled after ~5 min of failed attempts
- Dual STA+AP mode allows configuration while still retrying connection
- AP automatically disabled when WiFi reconnects
## Changes
### WiFi Credential Handling
- Try SDK-stored credentials first (from WiFiManager)
- Fall back to EEPROM config if SDK credentials fail
- Fixes OTA update credential loss issue
### User Experience
- Display shows "No WiFi" with retry countdown instead of cryptic numbers
- "!" indicator in date line when WiFi is disconnected
- Clock continues running with last synced time during outage
### Network Activity (~50 requests/day)
| Service | Interval | Endpoint |
|---------|----------|----------|
| NTP | 1 hour | pool.ntp.org:123 (UDP) |
| Weather | 30 min | api.open-meteo.com (HTTP) |
| mDNS | continuous | 224.0.0.251 (multicast) |
Co-authored-by: Alex Petrochenko <petrochenko@life-pay.ru>
Complete context file with:
- Full development history (v1.5 → v1.9.1)
- All technical details and architecture
- Security issues and fixes
- Hardware specifications and pinout
- Current device configuration
- Next steps (Home Assistant integration)
- Key learnings from project
- Quick reference commands
This file serves as complete knowledge base for:
- Resuming work after long breaks
- Onboarding new contributors
- Understanding project evolution
- Reference for future features