Skip to content

Retry wifi join on transient negative link status - #241

Open
adrianwedd wants to merge 2 commits into
pimoroni:mainfrom
adrianwedd:wifi-transient-retry
Open

Retry wifi join on transient negative link status#241
adrianwedd wants to merge 2 commits into
pimoroni:mainfrom
adrianwedd:wifi-transient-retry

Conversation

@adrianwedd

@adrianwedd adrianwedd commented Jul 29, 2026

Copy link
Copy Markdown

Retry wifi join on transient negative link status

Problem

reconnect_wifi() gives up on the first negative status from the CYW43: wait_status() raises as soon as wlan.status() goes negative, and the caller re-raises without retrying the join. In practice the CYW43 can report a transient failure (typically CYW43_LINK_FAIL, ~3 s after wlan.connect()) even though a fresh join attempt made seconds later succeeds. By then the firmware has already logged a failure, lit the warning LED, and gone back to sleep — so every blip costs a full reading-upload cycle and leaves the WARN LED blinking, which reads to users like a real configuration problem.

Evidence it's the connect logic, not RF or config

Diagnosed on an Enviro Grow (2.4 GHz, WPA2-only, PMF off, AP under 16% channel utilisation, RSSI −53 at the board's position, AP is a UniFi UDR7):

  • The on-device log.txt showed association failing on ~50% of 5-minute wakes over one evening, ending in six consecutive failures; each "failure" was logged exactly ~3 s after "connecting" — the first negative status poll.
  • After one logged failure, the UniFi controller showed the same radio associating ~2 minutes later (signal −46 dBm, satisfaction 100) — the environment was joinable moments after the firmware gave up.
  • A manual wlan.connect() at the REPL from the same position associated instantly.

Fix (two commits)

Commit 1 fixes an independent pre-existing bug: wait_status(CYW43_LINK_UP) returning False (timeout) was ignored, so a join that associated but never got a DHCP lease fell through to "Connected successfully!" and the caller proceeded with a dead network and no useful error. A timeout now raises, reporting the link state it was stuck in.

Commit 2 retries the join up to 3 times. Design points, deliberately:

  • Between attempts it disconnects and waits for LINK_DOWN first. MicroPython's connect() calls cyw43_wifi_join() with no preceding cyw43_wifi_leave() (extmod/network_cyw43.c), and the Pico W SDK guidance is to leave before rejoining after a failure. This also prevents a stale negative status from the previous attempt being misread as a new failure on the next poll.
  • wlan.connect() is inside the try so a synchronous OSError from a wedged driver counts as a failed attempt rather than crashing the cycle.
  • All negative statuses are retried, including BADAUTH. That is intentional: -3 is reported with a correct password on cold boots (see e.g. micropython#12930 discussion of first-attempt −1/−3 statuses), and on battery an Enviro cold-boots on every wake. A genuinely wrong password now costs ~30 s extra once per wake — but a wrongly-abandoned retry on a spurious BADAUTH would cost the whole cycle.
  • Battery cost is confined to the failure path. Cycles that connect first try are unchanged; worst case on total network outage is ~35 s awake versus ~10 s before. Under the old behaviour those cycles were lost anyway (reading cached, uploaded on a later successful cycle — which itself costs an extra connection).

3 attempts × 1 s pause is a judgment call, not tuned from a large sample: observed transient failures resolved within seconds (manual rejoin instant; radio associated by itself well within the next wake), so three well-separated fresh joins comfortably covers the observed recovery window while keeping the worst case bounded.

Testing

A minimal variant of this retry (re-kick wlan.connect() on negative status, without the disconnect/teardown — the 0.0.10 code this was diagnosed on has a different loop structure) has been running on the affected Enviro Grow: immediately before, the board had failed six consecutive 5-minute wake cycles; since patching, every wake cycle has connected and uploaded (10 consecutive cycles over the first 50 minutes at time of writing) with the warning LED staying off. The exact code in this PR compiles clean (CPython compile() and on-device checks of the equivalent change); I'm happy to run this branch on the board and report back if useful.

For context, similar symptoms (intermittent connect failures / blinking warning LED despite good RF) have been reported in the Enviro Grow and Enviro Indoor forum threads — I can't confirm those share this root cause, but the symptom signature matches.

wait_status() returns False on timeout, but reconnect_wifi() ignored
the return value when waiting for CYW43_LINK_UP. A join that timed out
(e.g. associated but never obtained a DHCP lease) fell through to
'Connected successfully!' and ifconfig(), and the caller carried on as
if the network were up — readings would then fail to upload with no
useful error logged. Raise instead, reporting the link state we were
stuck in.
@adrianwedd
adrianwedd force-pushed the wifi-transient-retry branch from 21246be to 42edf3c Compare July 29, 2026 11:48
The CYW43 can report a transient negative status shortly after
wlan.connect() — typically CYW43_LINK_FAIL ~3s in, and BADAUTH has
also been reported with a correct password on a cold boot (and every
battery wake of an Enviro board is a cold boot) — while a fresh join
attempt made seconds later succeeds. reconnect_wifi() raised on the
first negative status via wait_status(), so a single transient blip
aborted the whole reading cycle and lit the warning LED even though
the network was healthy.

Observed on an Enviro Grow (RSSI -53, WPA2/2.4GHz, AP <16% utilised):
the firmware logged a connection failure and halted, while the AP's
controller showed the same radio subsequently associating with full
signal quality; a manual wlan.connect() at the REPL from the same
position associated instantly.

Retry the join up to 3 times. Between attempts, disconnect and wait
for LINK_DOWN first: MicroPython's connect() calls cyw43_wifi_join()
without a preceding cyw43_wifi_leave(), and the Pico W SDK advises
leaving before rejoining after a failure. This also prevents a stale
negative status from the previous attempt being misread as a new
failure. wlan.connect() itself is inside the try so a synchronous
OSError from a wedged driver counts as a failed attempt instead of
crashing the cycle. Retries only run on cycles that would otherwise
have failed outright, so the added awake time (worst case ~35s) is
confined to the failure path.
@adrianwedd
adrianwedd force-pushed the wifi-transient-retry branch from 42edf3c to 8796988 Compare July 29, 2026 11:51
@adrianwedd
adrianwedd marked this pull request as ready for review July 29, 2026 12:15
@Gadgetoid

Copy link
Copy Markdown
Member

Bad auth should be fixed upstream, ala georgerobotics/cyw43-driver#154, rather than patching every downstream consumer.

Don't just paste Claude code responses into GitHub, please. It talks too darn much.

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.

2 participants