মূল বিষয়বস্তুতে যান

ESP32 Wi-Fi Reliability on the Factory Floor: A Field Guide

GizanTech EngineeringFirmware & Hardware Teamপ্রকাশিত ৩০ সেপ্টেম্বর, ২০২৬13 মিনিট পড়া

We get asked the same question every few weeks. The nodes pass on the bench, then sit in the plant and drop off every few hours. The logs say connected, but the dashboard shows gaps.

The root mistake is nearly always the same. The firmware treats link state as if it meant the device is delivering data. It doesn't. Here is how we build ESP32 firmware for plants, where steel, forklifts, enterprise controllers and variable-frequency drives all work against you.

The approach rests on three things:

  • Diagnose the RF and network failures that cause drops.
  • Run a reconnect state machine that escalates.
  • Drive the watchdog from application liveness.

Definition: a node is healthy only when it has recently had an application message acknowledged. Being associated with an IP address is not enough.

Most drops that customers notice happen while the ESP32 still reports it is associated and has a valid IP. Three common causes:

  • A firewall idle timeout leaves a half-open TCP session.
  • An AP silently drops a quiet client.
  • ARP breaks after a MAC-table age-out.

None of these raise a Wi-Fi event. The only watchdog that catches all of them is keyed on the last acknowledged message. Before that watchdog, though, you have to understand why the link itself fails.

RF physics on the plant floor

Multipath off steel

At 2.4 GHz the wavelength is about 12.5 cm. Steel racks, mezzanines, tanks and conveyors are strong reflectors, so the channel shows Rayleigh-like fading. Fades of tens of dB can appear or vanish when the node, or something near it, moves a fraction of a wavelength. A passing forklift or a changed pallet load is enough to alter the channel.

Most ESP32 parts are 1T1R SISO, so there is no MIMO diversity to smooth this out. The classic ESP32 does support an external antenna-switch GPIO (esp_wifi_set_ant_gpio / esp_wifi_set_ant) for two-antenna diversity. Few designs use it, and more should.

The antenna itself is where most designs fail:

  • A PCB antenna inside a steel box is a design defect. Trace antennas detune inside metal enclosures and near DIN rails. Use U.FL/IPEX modules with a bulkhead external antenna.
  • Mount above rack height where you can. Fresnel-zone obstruction matters on long aisle runs.
  • Use vertical dipoles on both ends as the default. Polarization mismatch after reflections adds loss.

2.4 GHz congestion

There are only three non-overlapping 20 MHz channels: 1, 6 and 11 (1/5/9/13 in some regulatory domains). Never use HT40 on 2.4 GHz in a plant.

The main interferer is other APs: neighbouring SSIDs, rogue hotspots, and handheld scanners and printers that run their own APs. Bluetooth and BLE, Zigbee, wireless cameras, microwave-based dryers near 2.45 GHz and proprietary industrial remotes all add to it.

CSMA/CA deferral and retries push rate adaptation toward low rates. Legacy 802.11b rates (1/2/5.5/11 Mbps) take up a lot of airtime. Disable the DSSS rates at the AP. Where the AP allows it, restrict the node with esp_wifi_set_protocol() to WIFI_PROTOCOL_11G|11N.

VFDs, honestly

VFDs get blamed for everything. Their EMI is mostly conducted and radiated energy from the kHz range up to tens of MHz, and they rarely jam 2.4 GHz directly. They hurt ESP32 nodes in other ways:

  • Supply noise and ground bounce
  • Brownouts on shared rails
  • Coupling into unshielded cabling near motor leads
  • A higher broadband noise floor near the drive or its cable run

The fixes are filtering, keeping cables away from motor leads, a shielded and grounded enclosure, and an isolated DC-DC. Changing the Wi-Fi channel will not help.

An enterprise AP may transmit at 20+ dBm while the node's uplink is weaker. The AP may also have turned its own power down. Either way, the node hears beacons fine but needs retries to get its uplink frames through. RSSI measured at the node overstates link health.

Set TX power with esp_wifi_set_max_tx_power() in 0.25 dBm units. The ESP32 ceiling is about 20 dBm, depending on regulatory settings. Set the country with esp_wifi_set_country_code() so channel and power limits match the site.

Power integrity, the fake Wi-Fi bug

TX bursts draw peaks of several hundred mA. A weak LDO or long supply leads cause brownout resets or PHY calibration errors that look like Wi-Fi drops. Log esp_reset_reason() on every boot. ESP_RST_BROWNOUT means the problem is not RF. Add bulk and local decoupling at the module.

Rule out power, antenna and enclosure problems before you tune reconnect logic. Two signs point at hardware: brownout resets, and RSSI that is poor even next to the AP.

Network-layer failure modes

Roaming: the sticky client

By default, ESP32 roaming is client-driven and basic. The node stays on a poor BSSID until beacon loss forces a disconnect. Enterprise networks offer three standards that help:

  • 802.11k (rm_enabled): neighbor reports tell the client which APs and channels to scan, so it skips a full-band scan.
  • 802.11v (btm_enabled): BSS Transition Management lets the controller steer the client. Some controllers disassociate clients that ignore BTM requests.
  • 802.11r (ft_enabled): Fast BSS Transition pre-derives keys. With WPA2/3-Enterprise, a full EAP re-auth on every roam can take hundreds of ms to seconds, and RADIUS latency makes it worse.

ESP-IDF's roaming examples use RSSI-threshold triggers (esp_wifi_set_rssi_threshold raising WIFI_EVENT_STA_BSS_RSSI_LOW) together with 11k/v. The exact API surface varies by IDF version, so check your target release.

For scanning, use WIFI_ALL_CHANNEL_SCAN with WIFI_CONNECT_AP_BY_SIGNAL. WIFI_FAST_SCAN joins the first match, which may be a weak AP. Set threshold.rssi and threshold.authmode to reject poor or insecure APs.

BSSID pinning: deterministic until it isn't

Pinning (bssid_set = true) suits fixed nodes. It stops flapping between two APs of equal strength and makes RF behaviour predictable. It becomes fatal when plant IT replaces, re-channels or takes down that AP for maintenance.

Our rule: pin, but fall back to an unpinned scan after N failures, and report the pinned or unpinned state in telemetry. Without that fallback, the next AP swap strands the whole line, and nobody can see it from the central dashboard.

Security

Use esp_eap_client for WPA2/WPA3-Enterprise (EAP-TLS, PEAP, TTLS). Enable PMF (802.11w) through pmf_cfg. It is mandatory for WPA3 and blocks spoofed deauth frames.

On EAP-TLS, certificate expiry causes a fleet-wide outage on a single date. Track not_after in telemetry.

DHCP

RFC 2131 sets T1 (renew) at 50% of the lease and T2 (rebind) at 87.5%. On plant networks DHCP fails in four common ways:

  • IoT VLAN pools run out, faster when short-lived handhelds churn through them.
  • The DHCP relay is unreachable during maintenance.
  • A rogue DHCP server answers.
  • The lease expires during a long disconnect.

IP_EVENT_STA_LOST_IP fires after CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL, which defaults to 120 s. Link-up without an IP is a distinct state, so count it separately. Measure time-to-IP apart from time-to-associate; a slow relay shows up there. Our usual choice is a DHCP reservation keyed to the MAC. It avoids the IPAM and conflict risk of static IPs and keeps DHCP snooping working.

ARP and idle timeouts

The idle timers along the path rarely line up:

  • Router ARP caches often age over hours.
  • Switch and AP MAC tables often age in about 5 minutes.
  • Controller client idle timeouts are separate again.

A quiet node ends up unreachable, or the AP deauths it as idle. Keep CONFIG_LWIP_ESP_GRATUITOUS_ARP enabled.

Proxy ARP on the AP only works if the AP learned the node's IP through DHCP snooping. Static-IP nodes can fall through that gap.

NAT and firewall idle timeouts kill idle MQTT sessions without sending a FIN or RST. Set MQTT keepalive or TCP keepalive (TCP_KEEPIDLE/INTVL/CNT) shorter than the tightest idle timeout on the path.

Power save

The beacon interval is typically 100 TU (102.4 ms). The AP buffers broadcast frames until DTIM beacons.

WIFI_PS_MAX_MODEM wakes every listen_interval beacons (default 3). If that exceeds the DTIM period, the node misses ARP, mDNS and some roaming signals. A sleeping node also sees fewer beacons, so each beacon lost to fading costs more. For nodes on mains power, use WIFI_PS_NONE.

Read the reason code before you retry

Every disconnect carries a reason code in wifi_event_sta_disconnected_t.reason. A per-site histogram of these codes is the cheapest RF survey you will ever get.

CodeNameTypical plant meaningClass
2AUTH_EXPIREAP-side auth timeout, weak uplinkRF
3AUTH_LEAVEController kick, config push, AP rebootNetwork
4ASSOC_EXPIREAP inactivity timeout, silent clientRF
5ASSOC_TOOMANYAP client limit reachedNetwork
8ASSOC_LEAVELocal disassociation, often our own codeLocal
154WAY_HANDSHAKE_TIMEOUTWrong PSK, or EAPOL lost on a poor linkConfig if link stable
16GROUP_KEY_UPDATE_TIMEOUTGTK rekey lost, marginal RF or PSRF
200BEACON_TIMEOUTFade, AP down, or PS beacon missesRF
201NO_AP_FOUNDPinned AP gone, wrong channel or countryConfig if pinned
202AUTH_FAILCredentials, EAP, certificateConfig
203ASSOC_FAILCapabilities or PMF mismatchConfig
204HANDSHAKE_TIMEOUTKey handshake failedMixed
205CONNECTION_FAILGenericMixed

Newer IDF releases add codes above 205. Give unknown codes their own histogram bucket instead of lumping them into other.

RF-class and config-class disconnects need opposite handling. If a node with an expired certificate keeps hammering a WPA-Enterprise network, the RADIUS server can rate-limit or lock out its MAC. It also floods the controller logs. Config failures should back off to a long interval and raise an alarm. RF failures get the normal retry.

The reconnect state machine

Model the connection as a series of states:

IDLE → SCANNING → ASSOCIATING → AUTHENTICATING → GOT_LINK → GOT_IP → APP_CONNECTED → APP_HEALTHY

When the connection degrades, it can fall back to any earlier state. Drive everything from events on a dedicated task and queue. Never call esp_wifi_connect() in a tight loop from the event handler, and never call it while a scan is running.

Use exponential backoff with full random jitter. Start at a base of about 1 s, double on each failure, and cap it somewhere between tens of seconds and a few minutes. Jitter matters: after an AP reboot, it stops a whole line from reconnecting at the same moment and overloading the AP, RADIUS and DHCP.

The escalation ladder

  1. Retry the same BSSID with backoff.
  2. Run a full unpinned scan and pick the strongest candidate.
  3. Call esp_wifi_disconnect(), esp_wifi_stop() and esp_wifi_start() to reset driver state.
  4. Reinitialise the netif and the DHCP client.
  5. Save the telemetry buffer, record the reason in RTC memory or NVS, then call esp_restart() as a controlled step.
  6. Leave a hardware or external watchdog as the last resort for a wedged radio. Set its budget well above the whole software ladder.

Never reboot just because the link is down while the application buffer is still healthy. A reboot discards RF state and the cached DHCP lease, and it can lose any data that was not buffered.

Two watchdogs, not one

The task watchdog (esp_task_wdt) catches hung tasks. The application-liveness watchdog tracks the time since the last confirmed delivery: an MQTT PUBACK at QoS1, an HTTP 2xx, or an application ack. If that time exceeds a budget of about 3 to 5 publish intervals, or several keepalives, escalate. Do this even when the link says connected and the IP is valid.

That single rule catches half-open TCP, a dead broker, firewall changes, a VLAN moved to a captive portal or ACL, and a stuck lwIP stack or driver.

Buffer offline, drain politely

Use a tiered buffer. Start with a RAM ring (in PSRAM if the part has it), then spill to flash with LittleFS or a raw partition ring. Do not use NVS for high-rate data. It is a key-value store with wear and garbage-collection overhead, and it suits counters and config, not time-series.

Each record carries three fields: a monotonic sequence number, a boot ID and a timestamp. Get time from SNTP, and use boot-relative time until the clock syncs. QoS1 is at-least-once delivery, so duplicates will arrive. The ingest side must deduplicate on (device, boot_id, seq).

Define the overflow policy explicitly: drop-oldest, or downsample to min, max and average per window. Report how many records were dropped. After a reconnect, send live data first, then backfill at a limited rate. Size the flash partition for the worst credible outage, not the average one.

Telemetry that makes a fleet debuggable

If you can't see a failure centrally, you can't fix it at scale.

Every heartbeat:

  • RSSI now, plus min and average over the window
  • BSSID, channel, PHY mode and security mode
  • Roam count and pinned or unpinned state
  • Free heap and minimum-ever free heap
  • Uptime, split into link-up, IP-up and app-healthy seconds

Every disconnect, buffered and sent after reconnect:

  • Reason code, BSSID and the RSSI just before the drop
  • How long the node was offline
  • Time to associate, time to IP and time to app-connected
  • The escalation level reached

Counters: reconnects, DHCP failures, MQTT disconnects, publish timeouts, buffer high-water mark, dropped records, reset reason, boot count and any stored panic reason.

Per-frame TX retry counters are not a clean public API on every IDF version. Where you can't get them, use proxies: publish latency, ack timeouts, and TCP retransmits from LWIP_STATS.

On the server side, measure uptime as the fraction of expected intervals actually received, computed from gaps in the sequence numbers. Cross-check it against the offline seconds each device reports. Alert on shifts in the distribution, such as falling median RSSI at a site or rising reason-200 counts, before any single device goes dark.

Key takeaways

  • Link state is not delivery. Key the watchdog on the last acknowledged application message.
  • Rule out brownouts, PCB antennas in metal boxes and VFD supply noise before touching reconnect code.
  • Classify every disconnect by reason code. RF failures retry; credential failures back off and alarm.
  • Pin BSSIDs deliberately, with an unpinned fallback that is visible in telemetry.
  • Escalate step by step with jittered backoff. Reboot last, and only after saving telemetry.

Where we come in

At GizanTech we build and harden ESP32 firmware for industrial deployments, where every one of these failure modes appears in the field sooner or later. If your fleet looks fine on the bench but leaves gaps on the plant floor, our firmware development team can audit the RF design, the reconnect logic and the telemetry pipeline together. In our experience the fix is rarely one line of code. It is almost always a design that treats delivery, not the link, as the thing to monitor.

সচরাচর জিজ্ঞাসিত প্রশ্ন

Why does my ESP32 keep disconnecting from Wi-Fi in a factory?

The usual causes are multipath fading off steel racking, 2.4 GHz congestion from other APs, a node stuck on a weak AP, and idle timeouts on the network path. Power problems are also common and get mistaken for Wi-Fi. Log the disconnect reason code and esp_reset_reason() before changing any code: a brownout reset is a hardware fault, not an RF one.

Should an ESP32 reboot when Wi-Fi is down?

Not as the first response. A reboot discards RF state and the cached DHCP lease, and it can lose data that was never buffered. Work up a ladder instead: retry, rescan, restart the driver, then reinit netif. Restart only as a controlled last step, after saving telemetry and recording why.

Is WIFI_PS_NONE the right power-save mode for industrial ESP32 nodes?

For nodes on mains power, usually yes. Current saved in modem sleep rarely makes up for missed beacons and missed ARP broadcasts. For battery nodes, use WIFI_PS_MIN_MODEM with application-level wake and send. Avoid WIFI_PS_MAX_MODEM with a listen interval longer than the DTIM period.

How do I detect an ESP32 that is connected but not delivering data?

Run an application-liveness watchdog. It tracks the time since the last confirmed delivery, such as an MQTT PUBACK, an HTTP 2xx or an application ack. If that exceeds a budget of about 3 to 5 publish intervals, escalate even when the link and IP look healthy. This catches half-open TCP and dead brokers.

Related solutions

See how we apply this in production, by industry: