Skip to content

09 · ESP32 WiFi Intro

The ESP32's superpower is a full WiFi radio on the same chip as your code — this is the moment your device becomes an IoT device. This module covers the three foundational moves: joining a network, fetching data from the internet (HTTP GET), and serving a web page from the board so any browser on the network can see your sensors. All of it runs in Wokwi, which simulates a WiFi network complete with internet access.

WiFi in the simulator

Wokwi's virtual ESP32 sees an open access point named Wokwi-GUEST (empty password). Real board? Just substitute your own SSID/password — everything else is identical. Note: ESP32 WiFi is 2.4 GHz only; 5 GHz networks are invisible to it — a top-3 real-hardware gotcha.

Connecting to WiFi

#include <WiFi.h>

const char *WIFI_SSID = "Wokwi-GUEST";   // your SSID on real hardware
const char *WIFI_PASS = "";              // your password on real hardware

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);                   // station mode: join a network
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print("Connecting");
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("Connected, IP: ");
  Serial.println(WiFi.localIP());        // the board's address on the network
  Serial.print("Signal: ");
  Serial.print(WiFi.RSSI());             // dBm; > -70 is decent
  Serial.println(" dBm");
}

void loop() {}

Station mode (STA) means the ESP32 joins an existing network like a laptop would. (It can also be an access point — WIFI_AP — handy for first-time device setup; a Level 2 topic.) The while loop blocks on purpose here — there's nothing useful to do before the network is up. The capstone shows the polite non-blocking version.

WiFi drops happen; production code watches for them:

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi lost, reconnecting...");
    WiFi.reconnect();
    delay(1000);
    return;
  }
  // ... normal work ...
}

Making an HTTP GET request

With a connection up, HTTPClient makes web requests look almost like desktop code:

#include <WiFi.h>
#include <HTTPClient.h>

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin("Wokwi-GUEST", "");
  while (WiFi.status() != WL_CONNECTED) delay(250);
  Serial.println("Connected.");

  HTTPClient http;
  // Open time API — returns a small JSON document
  http.begin("http://worldtimeapi.org/api/timezone/Etc/UTC");
  int code = http.GET();                 // performs the request

  if (code == HTTP_CODE_OK) {            // 200
    String body = http.getString();      // response body
    Serial.println(body);                // {"utc_datetime":"2026-07-21T09:00:00Z",...}
  } else {
    Serial.printf("HTTP error: %d\n", code);   // negative = connection failure
  }
  http.end();                            // free the connection — don't forget
}

void loop() {}

Always check the status code: 200 is success, -1 means the connection itself failed, and anything else is the server talking back. Real projects parse the JSON with the ArduinoJson library rather than string-hacking — worth exploring after this module. (HTTPS works too via WiFiClientSecure; plain HTTP keeps today simple.)

Serving a web page from the board

Flip the roles: the ESP32 listens on port 80 and answers browsers. The built-in WebServer class maps URL paths to handler functions:

#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);                    // standard HTTP port

uint32_t hits = 0;

void handleRoot() {
  hits++;
  char page[400];
  snprintf(page, sizeof(page),
    "<!DOCTYPE html><html><head><title>ESP32</title>"
    "<meta http-equiv='refresh' content='5'></head>"       // auto-reload every 5 s
    "<body style='font-family:sans-serif'>"
    "<h1>Hello from ESP32</h1>"
    "<p>Uptime: %lu s</p><p>Page hits: %lu</p>"
    "<p>Free heap: %u bytes</p>"
    "</body></html>",
    millis() / 1000, (unsigned long)hits, ESP.getFreeHeap());
  server.send(200, "text/html", page);   // status, content type, body
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin("Wokwi-GUEST", "");
  while (WiFi.status() != WL_CONNECTED) delay(250);

  Serial.print("Open http://");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);            // route: GET / → handleRoot()
  server.on("/api", []() {               // a JSON "API endpoint", lambda style
    char json[80];
    snprintf(json, sizeof(json), "{\"uptime_s\":%lu,\"heap\":%u}",
             millis() / 1000, ESP.getFreeHeap());
    server.send(200, "application/json", json);
  });
  server.begin();
}

void loop() {
  server.handleClient();                 // MUST run often — answers pending requests
}

server.handleClient() is the whole trick: the server only processes requests when you call it, which is why it sits in a fast-spinning, non-blocking loop() — your module 7 discipline is what keeps a web server responsive. In Wokwi, the serial monitor prints the IP and Wokwi offers a way to open the simulated device's web server right from the browser (via its IoT gateway); on real hardware, any phone or laptop on the same network just visits http://<the-ip>/.

Serving text/html for humans and application/json for programs — that tiny /api route is the seed of every device dashboard and REST-controlled gadget you'll build.

How It Actually Works

What WiFi.begin() triggers under the API: the ESP32's WiFi is a separate radio subsystem with its own MAC/PHY hardware, driven by a binary blob running on the chip's internal architecture that the Arduino WiFi class talks to via a thin driver layer (in ESP-IDF terms, esp_wifi_* calls). WiFi.begin() kicks off, in hardware and firmware below your sketch: an 802.11 probe/scan to find the SSID's access point, then authentication and association frames, then (for WPA2) a 4-way handshake that derives session encryption keys, and finally DHCP (a UDP broadcast exchange) to obtain an IP address — all of that is what WiFi.status() != WL_CONNECTED is silently polling underneath the . dots. 2.4 GHz-only is a hardware fact, not a software limitation: the ESP32's radio front-end (antenna matching network, RF filters, and the transceiver's tunable oscillator range) is physically built only to transmit/receive in the 2.400–2.4835 GHz ISM band; a 5 GHz signal is simply outside the frequency range its analog RF circuitry can amplify or demodulate at all.

Why the connect loop blocks on purpose, but handleClient() doesn't: underneath both, network I/O ultimately rides on a TCP/IP stack (lwIP) that itself depends on the WiFi driver delivering received frames via — again — interrupts from the radio hardware, dispatched into a background FreeRTOS task the ESP32's WiFi driver spins up for you invisibly (module 21 covers FreeRTOS and tasks properly). WebServer::handleClient() is a non-blocking poll of that stack's listening socket: it checks whether a TCP SYN or request has arrived in the socket's receive buffer and returns immediately if not — which is exactly why it belongs in a tight, undelayed loop(): every pass you skip is a pass where an already-connected client's request sits queued, adding latency, and where a new SYN could sit unacknowledged, since nothing else in this sketch drives that socket forward.

Why heap matters here specifically: every HTTPClient/WebServer request causes lwIP to allocate socket buffers and TLS/TCP control structures from the same general-purpose heap ESP.getFreeHeap() reports — string formatting into char page[400] avoids per-request String heap churn on your side, but the underlying TCP stack still allocates and frees its own buffers per connection, which is precisely why the exercise asks you to watch free heap across "many page loads": a slow leak there would be the network stack failing to release a buffer, not your own code.

Cheat sheet

Concept Detail
WiFi.mode(WIFI_STA) + WiFi.begin(ssid, pass) Join a network as a client
WiFi.status() == WL_CONNECTED Poll until (and monitor after) connection
WiFi.localIP() / WiFi.RSSI() Board's IP / signal strength (dBm)
Wokwi network SSID Wokwi-GUEST, empty password, has internet
2.4 GHz only ESP32 cannot see 5 GHz networks
HTTPClient GET begin(url)GET() → check code → getString()end()
Status codes 200 OK; negative = connection failed
WebServer server(80) HTTP server on the board
server.on(path, handler) + server.begin() Routing setup
server.handleClient() in loop() Actually serves requests — keep loop non-blocking

Exercise

Combine this module with module 4: an ESP32 with a potentiometer ("sensor") and an LED. Serve a page at / showing the current reading (auto-refresh every 2 s), a JSON endpoint at /data returning {"raw":...,"percent":...}, and two control routes /led/on and /led/off that switch the LED and redirect back to / (server.sendHeader("Location", "/"); server.send(303);). Everything must stay responsive: the page and the LED routes should react instantly even while you twist the knob. Verify all three routes from the browser, and confirm in the serial monitor that free heap stays stable across many page loads.