feat(#295): WiFi credentials to NVS, non-blocking connect, one-shot SD import - #300
Merged
Conversation
…ing state machine; SD import Closes #147. Epic #99 child 1; plan in docs/plans/2026-09-09-wifi-configuration.md (approved 2026-09-09). wifi_store: Preferences namespace 'wifi', five entries in priority order, deduplicated by SSID, oldest slot replaced when full; add/remove/move-up/clear for #296 and #297. One-shot import of /config/wifi.txt at boot; the file is never modified here, the Remove/Keep choice belongs to the screen and the web page. web.cpp: the six placeholder literals are gone. initWiFi() loads the store, imports, starts the first attempt and returns; checkWiFi() drives IDLE/CONNECTING/CONNECTED/WAIT from loop() with an 8 s per-network timeout and the 30 s rest, no delay() loops (the #292 WiFi item). NTP is started on connect and checked on later passes instead of a 5 s getLocalTime() wait. WiFi.persistent(false) so the core keeps no copy of the credentials; setAutoReconnect(false) so this machine is the only actor. Builds: um_feathers3 RAM 22.9% Flash 27.7%; feather_s3 RAM 31.8% Flash 64.9%. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… NTP judged by the SNTP client Gemini gate on #295, four findings fixed: persist() no longer clears the namespace first and writes the count last, so a power cut mid-write leaves the previous list instead of an empty one; the import strips a UTF-8 BOM, trims trailing whitespace from an SSID, and logs when a password begins or ends with whitespace (kept as written); NTP sync is read from sntp_get_sync_status(), because the RTC sets the system clock at boot and a sane clock proved nothing. Three findings justified in the PR body: NVS wear (wear-levelled, unchanged values not rewritten), lost-link hysteresis (one begin per 8 s at worst, then the 30 s rest), unhandled transient statuses (the 8 s timeout bounds them). Both envs rebuilt: um 22.9/27.7, adafruit 31.8/64.9. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ld's layout Second gate pass: writing entries in place could still pair an old SSID with a new password when a power cut landed between the two keys of a shifted entry (remove, move-up, add-when-full). The list now goes whole into the bank that is not live and one key flips the live bank. A BOM is stripped at the start of any line, not only the first. The single-bank layout the bench build deac3d1 wrote is still read when no bank key exists. Both envs rebuilt: um 22.9/27.7, adafruit 31.8/65.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner
Author
|
Agent: EmberDelta (session f0e32f9f) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #147. Epic #99, child 1 of 4. Plan:
docs/plans/2026-09-09-wifi-configuration.md(approved by the owner 2026-09-09, lands here).What changed and why
Six placeholder credential literals sat in
src/web.cppsince the #245 rebuild, so amainbuild could not join WiFi and every flash needed a local uncommitted edit.initWiFi()blockedsetup()up to 22.5 s andcheckWiFi()blockedloop()5 s every 30 s while down, the stall #292 measured.src/wifi_store.{h,cpp}(new): saved networks in NVS viaPreferences, namespacewifi, up to five in priority order, deduplicated by SSID, lowest slot replaced when full. Add / remove / move-up / clear for WiFi: /wifi web endpoint to list, add, delete, reorder, forget-all and handle the SD file (#99 child 2) #296 and WiFi: Settings sub-screen with scan list, LVGL keyboard, connect flow, forget-all (#99 child 3) #297. Writes go whole into the bank that is not live, then one key flips the live bank, so a power cut never leaves a half-written list (second gate pass). The layout the first bench build wrote is still read./config/wifi.txt(the Settings: WiFi configuration with on-screen keyboard #99 format) is read at boot; entries are added to the store. The file is never modified or removed by this child: the owner is asked Remove or Keep by the screen (WiFi: Settings sub-screen with scan list, LVGL keyboard, connect flow, forget-all (#99 child 3) #297) and the web page (WiFi: /wifi web endpoint to list, add, delete, reorder, forget-all and handle the SD file (#99 child 2) #296), which callwifiImportFileRemove(). A repeat import is harmless. BOMs and trailing SSID whitespace are handled; a password is kept exactly, with a log line if it starts or ends with whitespace.src/web.cpp: the literals are gone.initWiFi()loads the store, imports, starts the first attempt and returns.checkWiFi()is a state machine IDLE / CONNECTING / CONNECTED / WAIT driven fromloop(): 8 s per network, next network on rejection or absence, 30 s rest after all fail, restart from the top on a lost link, nodelay()anywhere. NTP is started on connect and judged bysntp_get_sync_status()on later passes, never waited on.WiFi.persistent(false)so the core keeps no copy of the credentials;setAutoReconnect(false)so this machine is the only actor.CLAUDE.md: the credential note rewritten for the new provisioning path. Passwords are never logged and never leave the unit except intoWiFi.begin().Hardware
Flashed to the UM bench board at deac3d1 (before the two-bank store; the store rewrite and the legacy read are the only changes since, both build on both envs):
fieldcompass.local/jsonand/diagsanswered from the UM board for the first time; every peripheral OK on/diags.Builds:
um_feathers3RAM 22.9% Flash 27.7%;feather_s3RAM 31.8% Flash 64.9%.Gemini gate (two passes, $0.06)
Fixed: crash-safe store writes (first pass: no
clear()before rewrite; second pass: that was still not enough for shifted entries, hence the two banks); UTF-8 BOM at the start of any line; SSID whitespace trimmed, password whitespace kept and logged; NTP judged by the SNTP client rather than by the clock being sane (the RTC sets the clock at boot, so the old test would have claimed a sync on every boot and rewritten the RTC from itself).Justified: NVS wear (wear-levelled, and NVS skips unchanged values); lost-link hysteresis (one attempt per 8 s at worst, then the 30 s rest); transient
WiFi.status()values (bounded by the 8 s timeout); no symbol clash fromesp_sntp.h; a stale COMPLETED status cannot carry acrossconfigTime(), which resets it.Gap filed from the first bench boot
#299: the on-device Diagnostics sub-screen shows no WiFi status and cannot scroll. Child of #99, after this one.
🤖 Generated with Claude Code