Six ESP32-S3 WiFi bugs that I think are one bug. Here's the list and the fix I'm planning, tell me where I'm wrong.
ESP32-S3, IDF 5.4. It's a desk terminal I built: WiFi STA, a WebSocket to my PC, another to a relay. Disclosure, it's headed toward being a product, but there's nothing to sell and no link, I just want the radio to stop embarrassing me.
I've been filing WiFi bugs against this thing for months as if they were unrelated. Last week I lined them up and I don't think they are.
1. It dies overnight. On a plain USB charger, usb_serial_jtag_is_connected() sees no host, so my "it's plugged in, stay awake" exemption never fires. Ten minutes after the last interaction, standby calls esp_wifi_disconnect() then esp_wifi_stop(). After that a duty-cycle poll opens a 2500 ms window to get back on. My socket layer retries on a 3000 ms timer. 3000 > 2500, so the window shuts before the retry lands, every time, forever. One night's logs: ~660 polls, zero successes. Dead 22:24 to 09:30. Touch it in the morning and it's back in a second.
2. It can't survive booting while the router is down. Every saved network gets 5 tries at boot. If they all fail I set a fail bit and stop. Forever-retry and roam only arm once s_wifi_ever_connected is true. So "AP wasn't ready at boot" means offline until a human power-cycles it.
3. It won't leave a weak mesh node. Three nodes, one SSID. scan_method on defaults, so FAST_SCAN takes the first match instead of the strongest. Reconnect by SSID, never pinning BSSID. Roam only fires on a disconnect event. Result: parked at -80 with a -45 node in the same room.
4. Roaming is directionally asymmetric. Coming back to the office it reconnects. Coming home it doesn't. Same two saved networks, same code path. I still don't have a clean repro and that bothers me more than the bug.
5. Half-open sockets. Keepalive send failures weren't counted as failures. A socket could be dead for 75 seconds while the UI just greyed out instead of reconnecting.
6. WiFi buffers are tiny. STATIC_RX 4, DYNAMIC_RX 8, DYNAMIC_TX 8, AMPDU off. I cut those to claw back internal RAM back when BT was still in the build. BT has been gone for months. The numbers stayed.
What they have in common. Every one of these fires a recovery attempt exactly once and then nothing checks whether it worked. The retry timer, the roam trigger, the standby poll, the keepalive counter: all ignition, no supervisor. When it works I never notice. When it doesn't there's no second attempt and no log line, so the device sits there looking perfectly healthy and is simply not on the network. #1 lived in my house for weeks disguised as "wifi is bad in that room."
What I'm planning:
- One connection manager task that owns the radio, with explicit states. Everything else asks it for connectivity instead of calling
esp_wifi_connect()itself. At least four places currently do. - Arm forever-retry on "credentials exist", not "has connected at least once".
- Standby stops tearing down WiFi.
WIFI_PS_MAX_MODEMwith DTIM, keep the association. Have not measured the power cost yet. esp_wifi_set_rssi_threshold()and treatWIFI_EVENT_STA_BSS_RSSI_LOWas a roam trigger, so "associated but bad" becomes a state I can act on instead of a blind spot.- ALL_CHANNEL_SCAN with CONNECT_AP_BY_SIGNAL, and pin the BSSID.
- Put a boot-id in the frames I send upstream. A good chunk of my earlier log analysis was wrong because I couldn't distinguish a reboot from a reconnect.
Where I'd like to be told I'm wrong:
- Anyone regret MAX_MODEM plus DTIM on an S3? Wake latency is what I'm nervous about, this thing is supposed to be always-listening.
- What WiFi buffer numbers have people actually landed on for sustained WebSocket traffic on an S3 with esp-sr resident? I have very little internal RAM to spend and I'd rather steal someone's tested numbers than binary-search my own.
- Is one connection-manager task the right shape, or does it just relocate the problem into a bottleneck?
Mostly I want to know whether "one-shot ignition with no supervisor" is a named mistake that everyone else already learned to avoid, or whether we all write it once and pay for it.