8-Bit Labs

Experiments in retro computers, assembly language and electronics

ESP32 WiFi Connection

25 Jun 2026

Most tutuorials for connecting your ESP32 devices to WiFi are not robust and don’t handle automatic reconnect, error logging, etc.

The following is the code that I use for all projects that require a WiFi connection.

// =====================================================================
//  ESP32 robust WiFi connect + auto-reconnect (Arduino core)
// ---------------------------------------------------------------------
//  - Blocking connect with timeout for setup()
//  - Human-readable status + disconnect-reason decoding over Serial
//  - Event-driven auto-reconnect for the long-running case
//  - Loop watchdog: reboots a headless node if WiFi stays down too long
//
//  Built-in libraries only (WiFi ships with the ESP32 Arduino core),
//  so nothing extra is needed in platformio.ini lib_deps.
//  ESP32 is 2.4 GHz only — a 5 GHz / band-steered SSID looks like a typo.
// =====================================================================

#include <WiFi.h>

// ---------------------- Configuration --------------------------------
static const char* WIFI_SSID     = "your-ssid";
static const char* WIFI_PASS     = "your-pass";
static const char* WIFI_HOSTNAME = "sensor-node";

static const uint32_t CONNECT_TIMEOUT_MS   = 15000;  // per attempt in setup()
static const uint8_t  CONNECT_MAX_ATTEMPTS = 3;      // before rebooting
static const uint32_t DOWN_REBOOT_MS       = 120000; // reboot if down this long at runtime

// Tracks when we last had a working connection (for the loop watchdog).
static uint32_t lastConnectedMs = 0;

// ---------------------- Diagnostics helpers --------------------------
const char* wlStatusStr(wl_status_t s) {
  switch (s) {
    case WL_IDLE_STATUS:     return "IDLE";
    case WL_NO_SSID_AVAIL:   return "NO_SSID_AVAIL (wrong SSID, out of range, or 5 GHz-only AP)";
    case WL_SCAN_COMPLETED:  return "SCAN_COMPLETED";
    case WL_CONNECTED:       return "CONNECTED";
    case WL_CONNECT_FAILED:  return "CONNECT_FAILED (usually wrong password)";
    case WL_CONNECTION_LOST: return "CONNECTION_LOST";
    case WL_DISCONNECTED:    return "DISCONNECTED";
    default:                 return "UNKNOWN";
  }
}

// Common IDF disconnect reason codes (info.wifi_sta_disconnected.reason).
const char* wifiReasonStr(uint8_t reason) {
  switch (reason) {
    case 2:   return "AUTH_EXPIRE";
    case 4:   return "ASSOC_EXPIRE";
    case 15:  return "4WAY_HANDSHAKE_TIMEOUT (often wrong password)";
    case 200: return "BEACON_TIMEOUT";
    case 201: return "NO_AP_FOUND (bad SSID, out of range, or wrong band)";
    case 202: return "AUTH_FAIL (wrong password)";
    case 203: return "ASSOC_FAIL";
    case 204: return "HANDSHAKE_TIMEOUT";
    default:  return "see esp_wifi_types.h";
  }
}

// ---------------------- WiFi event handler ---------------------------
// Registered BEFORE WiFi.begin(). This is where the real "why did it
// drop?" diagnosis lives — the status code alone can't tell you.
void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
      Serial.println("[WiFi] Associated with AP");
      break;

    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      lastConnectedMs = millis();
      Serial.printf("[WiFi] Got IP: %s  (RSSI %d dBm, ch %d)\n",
                    WiFi.localIP().toString().c_str(),
                    WiFi.RSSI(), WiFi.channel());
      break;

    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: {
      uint8_t reason = info.wifi_sta_disconnected.reason;
      Serial.printf("[WiFi] Disconnected, reason=%u (%s)\n",
                    reason, wifiReasonStr(reason));
      // setAutoReconnect(true) handles routine retries for us; nudge it.
      WiFi.reconnect();
      break;
    }

    default:
      break;
  }
}

// ---------------------- Blocking connect (setup) ---------------------
bool wifiConnect(uint32_t timeoutMs) {
  Serial.printf("[WiFi] Connecting to \"%s\" ...\n", WIFI_SSID);

  WiFi.persistent(false);            // don't rewrite creds to NVS every boot
  WiFi.mode(WIFI_STA);               // station only
  WiFi.setSleep(false);              // keep radio responsive for a server
  WiFi.setAutoReconnect(true);
  WiFi.setHostname(WIFI_HOSTNAME);   // must be BEFORE begin()

  WiFi.begin(WIFI_SSID, WIFI_PASS);

  uint32_t start = millis();
  wl_status_t status = WiFi.status();

  while (status != WL_CONNECTED && (millis() - start) < timeoutMs) {
    delay(250);
    Serial.printf("  [%5lu ms] status=%s\n", millis() - start, wlStatusStr(status));
    status = WiFi.status();
  }

  if (status == WL_CONNECTED) {
    lastConnectedMs = millis();
    Serial.printf("[WiFi] Connected in %lu ms\n", millis() - start);
    Serial.printf("  IP:      %s\n", WiFi.localIP().toString().c_str());
    Serial.printf("  Gateway: %s\n", WiFi.gatewayIP().toString().c_str());
    Serial.printf("  Subnet:  %s\n", WiFi.subnetMask().toString().c_str());
    Serial.printf("  RSSI:    %d dBm\n", WiFi.RSSI());
    Serial.printf("  MAC:     %s\n", WiFi.macAddress().c_str());
    Serial.printf("  Channel: %d\n", WiFi.channel());
    return true;
  }

  Serial.printf("[WiFi] FAILED after %lu ms — last status: %s\n",
                millis() - start, wlStatusStr(WiFi.status()));
  return false;
}

// ---------------------- Loop watchdog --------------------------------
// Auto-reconnect handles transient drops. This is the failsafe for a
// headless node: if we've been down past the grace period, reboot.
void wifiWatchdog() {
  if (WiFi.status() == WL_CONNECTED) {
    lastConnectedMs = millis();
    return;
  }
  if (millis() - lastConnectedMs > DOWN_REBOOT_MS) {
    Serial.printf("[WiFi] Down > %lu ms — rebooting.\n", DOWN_REBOOT_MS);
    delay(100);
    ESP.restart();
  }
}

// ---------------------- Arduino entry points -------------------------
void setup() {
  Serial.begin(115200);
  delay(200);

  WiFi.onEvent(onWiFiEvent);  // register BEFORE begin()

  uint8_t attempts = 0;
  while (!wifiConnect(CONNECT_TIMEOUT_MS) && ++attempts < CONNECT_MAX_ATTEMPTS) {
    Serial.printf("[WiFi] Retry %u after backoff...\n", attempts);
    WiFi.disconnect(true);       // clear stale state before retrying
    delay(2000 * attempts);      // linear backoff
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[WiFi] Giving up — rebooting in 5s.");
    delay(5000);
    ESP.restart();
  }

  // ---- start your server / sensors here ----
  // server.begin();
}

void loop() {
  wifiWatchdog();   // non-blocking; auto-reconnect does the heavy lifting

  // ---- your sensor reads / app code here ----
  delay(1000);
}

A few notes on how the pieces interact, since the reconnect logic has some redundancy that’s intentional rather than accidental:

There are three layers of resilience, and they’re meant to overlap. setAutoReconnect(true) handles the common case (AP reboots, brief RF dropouts) silently. The disconnect event handler logs the reason code so you can actually diagnose why it dropped, and nudges reconnect(). The loop watchdog is the last resort for a headless node. If you’ve been disconnected past DOWN_REBOOT_MS (two minutes as written), it reboots rather than sitting dark forever. You can drop the watchdog if there’s always someone around to power-cycle, but for a sensor node tucked behind a wall it’s the difference between a momentary blip and a dead device.

The two reason codes you’ll actually see in practice are 201 (NO_AP_FOUND which is bad SSID, out of range, or the 2.4 vs 5 GHz trap) and 202 / 15 (auth failure which is wrong password). If you’re staring at one of those on first bring-up, that comment block tells you which mistake you made before you start second-guessing the wiring.