A network failure should be an observation in an experiment, not an invisible reason to restart its controller forever. This ESP32 project runs a bounded one-minute session, reports local progress over its serial console and tries to join one existing access point under an explicit retry policy. It does not upload readings, queue messages or claim that a connected radio means a server received anything.
Use Espressif ESP32-DevKitC V4 populated with ESP32-WROOM-32E and official MicroPython1.26.0 for the original ESP32. A board carrying an ESP32-S3, C3 or a differently named module is not the selected device. The module supplies the 2.4GHz radio; the board adds regulation, a Universal Serial Bus (USB) to serial bridge and the power/reset circuit. No external sensor, resistor or signal wire is required.
You need your own authorized access point and a computer with a serial console. The read–evaluate–print loop (REPL) is the interactive Python prompt. Keep network names and keys out of articles, shared screenshots and source control. The listing contains no credentials: import the module and call main with your own privately supplied string variables. This is not a claim that the radio firmware never retains configuration internally; the application simply does not print or write your key to its files.
An Internet Protocol (IP) address identifies the network interface on its local network. The diagram deliberately separates that status from delivery evidence.

Use USB as the board's sole power source. Leave header5 V and 3V3 supply inputs disconnected. The manufacturer's alternatives are mutually exclusive, not additional power connections. There are no general-purpose input/output (GPIO) assignments in this experiment, so no boot-strapping or flash pins need a jumper. The existing power indicator shows supply presence, not network health.
The manufacturer's guide notes an earlier-board C15 issue involving download mode and GPIO0 clock output. This experiment does not drive GPIO0 or instruct you to remove components. Record the actual module and board condition; a board that cannot boot is a hardware investigation before a retry-policy investigation. Ordinary radio power transients and a poor USB cable can still cause a real reset; this code cannot repair a collapsing supply.
A wireless local-area network (WLAN) station joins an access point rather than creating one. Its service set identifier (SSID) is the network name. Association and authentication establish the radio relationship; a valid Internet Protocol (IP) address establishes a network interface configuration. Neither proves that a remote service is reachable, that a message was sent, or that it was stored exactly once. Every emitted record therefore keeps application_delivery_confirmed false.
MicroPython's isconnected reports the station relationship with a valid IP address. The adapter separately maps the documented wrong-password status to AUTH_BLOCKED, which is terminal for this session. It maps no-access-point and connection-failed statuses to an unsuccessful attempt. Authentication errors require checking the supplied settings; repeatedly hammering the same incorrect key does not help.
The adapter disables its background reconnect count with reconnects=0 before starting. Otherwise the radio stack and this policy could both retry, hiding the real number of attempts. It activates the station and disconnects any earlier relationship first. If the selected firmware does not accept that option, initialization raises an error and attempts to turn the radio off; silently substituting unlimited retries would violate the experiment.
The first connect request is issued at age zero. Each attempt has a five-second application deadline. A timeout or explicit failure disconnects and waits before the next request: one second after attempt one, two after attempt two and four after attempt three. After attempt four fails, EXHAUSTED remains terminal. A later apparently connected observation does not silently clear that state. A new run is a deliberate new budget.

With prompt driver calls and 100ms polling, timeout-only requests begin near0,6,13 and 22seconds and the last deadline is27seconds. An early reported failure moves its wait earlier. These are calculated policy times, not measured association durations or a hard real-time guarantee. The Python network call itself can block, and scheduling delays postpone observations. A desktop mock cannot bound radio firmware or operating-system latency.
An already connected initial observation is legal even before the first requested attempt. Losing it schedules the minimum one-second wait and does not consume an attempt until a connect request is actually made. A successful link does not reset the consumed-attempt count. If it drops later, the remaining budget applies. This prevents a flapping access point from granting an unbounded fresh budget on every brief reconnection. Local observation numbers continue in both WAIT and EXHAUSTED. They are progress markers, not temperature samples or evidence of packet transmission.
Save as wifi_retry_lab.py, import it, then call main with your private SSID and key variables. Do not post those arguments in a public support screenshot. The loop uses wrap-safe elapsed time, rejects backwards or invalid clock observations, and has an independent601-step cap. Reaching or passing the one-minute deadline ends normally before another network observation or connection request; normal loop overhead is not a clock fault. The one-minute duration is an application bound when the clock and calls behave; the cap bounds a frozen-clock host case. On return, interruption or exception, cleanup attempts both disconnect and interface deactivation. The final returned state describes the last observation before cleanup.
Download full program · Blank link worksheet
"""ESP32-DevKitC V4/WROOM-32E, MicroPython1.26.0. Link status is not delivery."""
class Retry:
def __init__(self):
self.state='WAIT';self.attempts=0;self.due=0;self.started=None;self.last=-1
def failure(self,now):
if self.attempts>=4:self.state='EXHAUSTED';return
self.state='WAIT';self.due=now+(1000 << max(0,self.attempts-1))
def step(self,now,connected,auth_failed=False,attempt_failed=False):
if type(now)is not int or not 0<=now<=60000 or now<self.last or type(connected)is not bool:
self.state='FAULT';raise ValueError('Invalid observation or monotonic run age')
self.last=now
if self.state in ('EXHAUSTED','AUTH_BLOCKED','FAULT'):return None
if auth_failed:self.state='AUTH_BLOCKED';return 'disconnect'
if connected:self.state='ONLINE';return None
if self.state=='ONLINE':self.failure(now);return 'disconnect'
if self.state=='CONNECTING' and (attempt_failed or now-self.started>=5000):
self.failure(now);return 'disconnect'
if self.state=='WAIT' and now>=self.due:
self.attempts+=1;self.started=now;self.state='CONNECTING';return 'connect'
return None
def row(self):
return {'link_state':self.state,'attempts_used':self.attempts,
'application_delivery_confirmed':False}
def run(adapter,ticks_ms,ticks_diff,sleep_ms,emit=print):
policy=Retry();start=ticks_ms();sample=0;previous=-1
try:
for _ in range(601):
age=ticks_diff(ticks_ms(),start)
if type(age)is not int or age<previous or age<0:
raise ValueError('Clock or60s session bound')
previous=age
if age>=60000:
result=policy.row();result.update({'session_end':'TIME_LIMIT','observed_run_age_ms':age})
return result
online=adapter.connected()
auth,failed=adapter.failure_status()
action=policy.step(age,online,auth,failed)
if action=='disconnect':adapter.disconnect()
elif action=='connect':adapter.connect()
if age//1000>sample or age==0:
sample=age//1000
row=policy.row();row.update({'run_age_ms':age,'local_observation':sample})
emit(row) # deliberately no network send or queued replay
sleep_ms(100)
result=policy.row();result['session_end']='ITERATION_LIMIT';return result
finally:
try:adapter.disconnect()
finally:adapter.active(False)
class ESP32Station:
def __init__(self,ssid,key):
import network
if not isinstance(ssid,str)or not ssid or not isinstance(key,str):
raise ValueError('Provide your network name and key privately')
self.network=network;self.ssid=ssid;self.key=key
self.nic=network.WLAN(network.WLAN.IF_STA)
try:
self.nic.active(True)
self.nic.config(reconnects=0) # disable background reconnect policy
self.nic.disconnect()
except BaseException:
try:self.nic.disconnect()
finally:self.nic.active(False)
raise
def connected(self):return self.nic.isconnected()
def failure_status(self):
s=self.nic.status();n=self.network
return s==n.STAT_WRONG_PASSWORD,s in (n.STAT_NO_AP_FOUND,n.STAT_CONNECT_FAIL)
def connect(self):self.nic.connect(self.ssid,self.key)
def disconnect(self):self.nic.disconnect()
def active(self,value):self.nic.active(value)
def main(ssid,key):
from time import ticks_ms,ticks_diff,sleep_ms
adapter=ESP32Station(ssid,key)
return run(adapter,ticks_ms,ticks_diff,sleep_ms)
# Import this module, then call main with private variables from your console.
This differs from a passive signal-strength scan: it deliberately joins a network and studies connection state and bounded retries. It also differs from a sensor dashboard: no stale sensor value is reused and no upload or offline message replay is implemented. The existing learning path supplies the hardware fundamentals.
Challenge: the board reports ONLINE but a server is down. Was the measurement delivered? Answer: this program cannot answer. Link state is not an application acknowledgement. A later delivery design needs its own message identity, acknowledgement and duplicate policy.
Twelve actual-code desktop tests exercise retry timing, early failure, authentication lock, flapping-link budget, terminal exhaustion, clock/input faults, local progress, cleanup and counter wrap using injected adapters. No Wi-Fi association, radio timing, delivery, target execution or firmware build is claimed. Keep the physical worksheet empty until you perform the experiment.
The current Espressif guide and pinned MicroPython interfaces were checked on20October2026. The board guide's documented C15 note is retained as context; no separate applicable correction to this unmodified USB-powered topology was located. Recheck actual board and firmware documentation before relying on behavior outside this local experiment.