Skip to content

feat(#295): WiFi credentials to NVS, non-blocking connect, one-shot SD import - #300

Merged
Strycher merged 3 commits into
mainfrom
fc/295-wifi-store
Sep 9, 2026
Merged

feat(#295): WiFi credentials to NVS, non-blocking connect, one-shot SD import#300
Strycher merged 3 commits into
mainfrom
fc/295-wifi-store

Conversation

@Strycher

@Strycher Strycher commented Sep 9, 2026

Copy link
Copy Markdown
Owner

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.cpp since the #245 rebuild, so a main build could not join WiFi and every flash needed a local uncommitted edit. initWiFi() blocked setup() up to 22.5 s and checkWiFi() blocked loop() 5 s every 30 s while down, the stall #292 measured.

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):

  • Import of the owner's three networks from the SD file, "Setup complete!" printed before the connect finished, connected to the first network at RSSI -27 dBm, web server up, NTP synced.
  • fieldcompass.local/json and /diags answered from the UM board for the first time; every peripheral OK on /diags.
  • Not exercised: reconnect after an access-point drop. The integration test Epic #99 integration test: WiFi configuration on hardware #298 covers it.

Builds: um_feathers3 RAM 22.9% Flash 27.7%; feather_s3 RAM 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 from esp_sntp.h; a stale COMPLETED status cannot carry across configTime(), 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

Strycher and others added 3 commits September 9, 2026 01:14
…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>
@Strycher

Strycher commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Agent: EmberDelta (session f0e32f9f)

@Strycher
Strycher merged commit e0f5f7c into main Sep 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: WiFi credentials hardcoded in Field_Compass.ino — needs a provisioning mechanism

1 participant