Follow-up to #61, which auto-closed on the merge of #64 even though that PR shipped phase 1 only ("Closes #61 — phase 1" — the keyword does not know about phases). Filing this so the deferred half stops living in a PR description.
What is missing
A bridge configured with a static address that is wrong for its network has no way back on its own. Validation refuses everything visible from the config page — unparseable octets, non-contiguous masks, network/broadcast addresses, a gateway outside the subnet, DNS missing while something is addressed by name — but none of that can see whether the gateway actually answers. A typo'd-but-valid address, a VLAN change, or moving the bridge to another network all produce a configuration that passes validation and is unreachable.
Why the existing failure machinery does not cover this
This is the part that makes the issue worth doing rather than nice-to-have, and it is not obvious from reading wifi_manager.cpp alone.
WifiManager::connected() is WiFi.status() == WL_CONNECTED. In Arduino core 3.3.9 that status is set in exactly one place — libraries/WiFi/src/STA.cpp:183 — on the ARDUINO_EVENT_WIFI_STA_GOT_IP event.
- On DHCP that event follows a completed lease. No DHCP server, wrong VLAN, no answer → no
GOT_IP → consecutiveFailures_ climbs in loop(), decideState() reaches PortalAfterFailures, and the setup portal comes up. The user gets back in.
- On a static address there is no lease to wait for. The address is already configured before
WiFi.begin(), so GOT_IP is expected to fire on association alone.
If that holds, connected() returns true forever on a completely unusable network. consecutiveFailures_ stays 0, the portal never opens, and the retry/back-off path in loop() is dead code for this failure. Switching to a static address silently disables the recovery mechanism that DHCP users have. The only way back is the BOOT-hold factory reset, which wipes every other setting with it — so: take a config backup before setting a static address.
Step 0 of this issue is confirming that on hardware, because the whole design rests on it: static address on a network with no matching gateway, then read WiFi.status() and watch whether the portal ever appears. The core source says what should happen; the bridge has not been asked.
Why it is not a small change
The obvious implementation is the wrong one. Association succeeding tells you nothing here, so the probe has to be at layer 3:
- What to probe. The gateway is the natural target, but plenty of gateways drop ICMP, and a bridge that falls back to DHCP because the router ignores pings is worse than the bug. ARP for the gateway is cheaper and harder to filter, since a gateway that does not answer ARP is genuinely not usable.
- When. Only after a static association, only once per boot, with a bounded window. A probe that runs continuously turns a brief network hiccup into an address change.
- What "fall back" means. For this boot only, or persisted? Persisting means the user's configured address quietly disappears from the config; not persisting means a bridge that reboots nightly flaps between the two. Leaning towards this-boot-only plus a visible state in the UI and REST status, so the configured value stays the source of truth and the deviation is observable.
- How it is visible. A fallback that happens silently is its own trap — the bridge is reachable, at an address nobody expects, and the settings page still shows the static one.
Testable how
The probe policy itself belongs in the host-testable pure core, the way ipv4.{h,cpp} and the rate limiter are: a state machine over (associated, probe result, elapsed) with no WiFi.* in it. What genuinely needs hardware is the GOT_IP-on-static behaviour above and one end-to-end run on a wrong-but-valid address.
Not urgent
Every bridge in existence is on DHCP or on a static address that works. This is about the failure mode of a feature that shipped a day ago, not a fault anyone is hitting. It should not be rushed onto hardware that is currently in a metering cupboard.
Follow-up to #61, which auto-closed on the merge of #64 even though that PR shipped phase 1 only ("Closes #61 — phase 1" — the keyword does not know about phases). Filing this so the deferred half stops living in a PR description.
What is missing
A bridge configured with a static address that is wrong for its network has no way back on its own. Validation refuses everything visible from the config page — unparseable octets, non-contiguous masks, network/broadcast addresses, a gateway outside the subnet, DNS missing while something is addressed by name — but none of that can see whether the gateway actually answers. A typo'd-but-valid address, a VLAN change, or moving the bridge to another network all produce a configuration that passes validation and is unreachable.
Why the existing failure machinery does not cover this
This is the part that makes the issue worth doing rather than nice-to-have, and it is not obvious from reading
wifi_manager.cppalone.WifiManager::connected()isWiFi.status() == WL_CONNECTED. In Arduino core 3.3.9 that status is set in exactly one place —libraries/WiFi/src/STA.cpp:183— on theARDUINO_EVENT_WIFI_STA_GOT_IPevent.GOT_IP→consecutiveFailures_climbs inloop(),decideState()reachesPortalAfterFailures, and the setup portal comes up. The user gets back in.WiFi.begin(), soGOT_IPis expected to fire on association alone.If that holds,
connected()returns true forever on a completely unusable network.consecutiveFailures_stays 0, the portal never opens, and the retry/back-off path inloop()is dead code for this failure. Switching to a static address silently disables the recovery mechanism that DHCP users have. The only way back is the BOOT-hold factory reset, which wipes every other setting with it — so: take a config backup before setting a static address.Step 0 of this issue is confirming that on hardware, because the whole design rests on it: static address on a network with no matching gateway, then read
WiFi.status()and watch whether the portal ever appears. The core source says what should happen; the bridge has not been asked.Why it is not a small change
The obvious implementation is the wrong one. Association succeeding tells you nothing here, so the probe has to be at layer 3:
Testable how
The probe policy itself belongs in the host-testable pure core, the way
ipv4.{h,cpp}and the rate limiter are: a state machine over (associated, probe result, elapsed) with noWiFi.*in it. What genuinely needs hardware is theGOT_IP-on-static behaviour above and one end-to-end run on a wrong-but-valid address.Not urgent
Every bridge in existence is on DHCP or on a static address that works. This is about the failure mode of a feature that shipped a day ago, not a fault anyone is hitting. It should not be rushed onto hardware that is currently in a metering cupboard.