After a successful connect via SDK-cached credentials (Try 1) or the
WiFiManager captive portal, only the SSID was saved to EEPROM. On the next
boot config.password was empty, so setupWiFi() and the reconnect loop called
WiFi.begin(ssid) as if the network were open and failed with
WL_WRONG_PASSWORD.
v1.9.6 masked this: with an empty password the retry loop fell back to a
bare WiFi.begin(), which reuses the SDK-stored credentials. Commit 02834ba
(v1.9.7) replaced that fallback with WiFi.begin(config.ssid), and v1.9.9
(M2) added the same call to setupWiFi(), so every version after 1.9.6
never reconnects after a reboot when the device was set up through the
portal.
Read the password back with WiFi.psk() and store it next to the SSID in
all three places that sync the SSID. Devices already in this state recover
by entering the password once in the web UI (fallback AP after ~2.5 min).
Verified: builds with PlatformIO (espressif8266 core 3.1.2, esp01_1m).
Fixes#12
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From Sonnet pre-release code review (3 real issues):
- F1 (high): factory reset atomicity — save cleared creds BEFORE zeroing reset
counter, so a power loss between commits still results in WiFiManager AP boot
instead of inconsistent "counter=0 + stale creds" state
- F2: isValidSSID now allows single-char SSIDs (per 802.11 spec)
- F3: ntp_interval changes now trigger needsRestart (NTPClient constructed once
in setup() with this value, doesn't pick up runtime changes)
From internal backlog:
- M2: setupWiFi Try-2 now supports open networks (no password) — fixed the
&&-condition that required both ssid and password
- M3: removed dead NTP_WAITING/NTP_SUCCESS/NTP_FAILED enum values
- M4: randomSeed() with ESP.getChipId() ^ micros() — dissolve pattern varies
Tested on hardware: 72/73 tests pass (single failure is expected — ntp_interval
fuzz cases now trigger restart due to F3, second case lands in reboot window).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously every successful config save triggered ESP.restart() — meant rapid
config changes caused reboot cascades and made the test suite unable to run
multiple cases against /config. Now restarts only if ssid/password/hostname/
ntp_server changed; other settings (brightness, timezone, intervals, coords,
display options) apply live without reboot.
tests/test_device.py: fixed expected field names for /api/time
(time/hours/minutes/epoch instead of current/timezone_offset/ntp_synced).
Test suite: 73/73 passing on hardware. Heap drift <1KB across full fuzz run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously /config accepted any value and called ESP.restart() — fuzz tests
(or any malicious POST) could save garbage SSIDs and brick the device until
FTDI recovery. Now:
- SSID: rejected if empty, >31 chars, non-printable, or all-same-char (HTTP 400)
- Password: rejected if >63 chars
- Hostname/city_name/ntp_server: length-validated
- Numeric fields (timezone, brightness, intervals, lat/lon, display): clamped
to safe ranges via constrain()
Tested on hardware: ssid="AAAA..." now correctly returns HTTP 400 and
preserves existing config. Device survives entire fuzz suite.
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>