首页 / 学习路径 / 第 05 章 · WiFi 联网与 Web 服务器
05
进阶 · 物联网

WiFi 联网与 Web 服务器

STA 与 AP 双模式、连接超时处理、HTTP 请求响应、自适应网页。让设备连上互联网,在手机浏览器里就能控制它。

阅读时长 约 55 分钟硬件 2 件实战案例 1 个前置要求 第 4 章
1

本章要点

ESP32 内置 2.4GHz WiFi 射频,只支持 2.4GHz——5GHz 频段的路由器无法连接,这是「连不上」的首要原因。它有 STA(客户端,连路由器)和 AP(热点,自己当路由器)两种模式。

永远不要用 delay(15000) 等连接。用 millis() 做带超时的循环,程序在这期间依然能响应。连接失败时降级为 AP 热点,保证用户至少能打开页面。

HTTP 是「请求-响应」模型:浏览器发 GET 请求,服务器回一段 HTML。WebServer 库把 TCP 细节封装好了,你只需要写「收到什么路径 → 返回什么内容」。

2

硬件清单与原理

名称数量作用与原理
ESP32 DevKit V11 块内置 2.4GHz WiFi 射频(802.11 b/g/n),天线为 PCB 走线或外置 IPEX。
USB 数据线1 根供电 + 串口。必须是数据线。
LED + 220Ω 电阻1 套状态可视化。板载 LED 已接 GPIO2,可直接复用。
3

接线说明

接线示意 · WiFi 仅需供电,LED 用板载 GPIO2 ESP32 DevKit V1 2.4GHz WiFi GPIO2 板载 LED 手机浏览器 · http://192.168.x.x WiFi 2.4GHz 注意:ESP32 不支持 5GHz 频段,路由器需为 2.4GHz
WiFi 无需外接接线。LED 可用板载 GPIO2。

USB 数据线 → ESP32 → 电脑(仅供电与烧录)

板载 LED 已接 GPIO2,用于状态指示

① 代码顶部 WIFI_SSID / WIFI_PASSWORD 改成你的路由器信息

② 烧录后打开串口监视器,记下打印出的 IP 地址

提示:连接失败时代码会降级为 AP 热点 ESP32-Teaching,密码 12345678。

4

实战案例 05-1 · WiFi 联网 + 迷你 HTTP 服务器

功能用途

让 ESP32 接入路由器,并在自己的 IP 上开一个小网站,显示实时状态并控制 LED。

预期现象

串口打印 IP 地址,用手机连到同一路由器,浏览器输入该 IP 即可打开控制页面。

ch05_wifi.ino编译通过
/*
 * ============================================================================
 * 第 5 章 · 实战案例 05-1:WiFi 联网 + 迷你 HTTP 服务器
 * ----------------------------------------------------------------------------
 * 硬件清单:
 *   - ESP32 DevKit V1 × 1
 *   - USB 数据线 × 1
 *   - LED × 1 + 220Ω 电阻 × 1
 *
 * 接线方式:
 *   GPIO2 → [220Ω] → LED 阳极,LED 阴极 → GND
 *
 * 接口/协议原理:
 *   1) ESP32 内置 WiFi 射频,支持 2.4GHz(注意:5GHz 频段的路由器无法连接)。
 *      有 STA(客户端,连路由器)和 AP(热点,芯片自己当路由器)两种模式。
 *   2) HTTP 是「请求-响应」模型:浏览器发 GET 请求,服务器回一段 HTML。
 *   3) WebServer 库把 TCP 的底层细节封装好了,你只需要写「收到什么路径 → 返回什么内容」。
 *
 * 功能用途:
 *   让 ESP32 接入路由器,并在自己的 IP 上开一个小网站:
 *   - 首页显示实时状态(IP、信号强度、运行时长、LED 状态)
 *   - 点击按钮可以直接控制 LED
 *
 * 预期现象:
 *   串口打印出「AP 模式」或「STA 模式连接成功」和 IP 地址,
 *   用手机/电脑连到同一路由器,浏览器输入该 IP 即可打开控制页面。
 *
 * ⚠ 使用前必改:把下面的 WIFI_SSID / WIFI_PASSWORD 改成你自己的路由器信息。
 * ============================================================================
 */

#include <Arduino.h>     // Arduino 核心库
#include <WiFi.h>        // ESP32 WiFi 驱动库(ESP32 专属,Arduino UNO 没有)
#include <WebServer.h>   // ESP32 自带的轻量 HTTP 服务器库

/* ==========================================================================
 * 一、必须修改的配置区
 * ======================================================================== */
const char* WIFI_SSID     = class="st">"YOUR_WIFI_NAME";    // ← 改成你的 WiFi 名称
const char* WIFI_PASSWORD = class="st">"YOUR_WIFI_PASSWORD";// ← 改成你的 WiFi 密码

// 热点模式:没有路由器时的备用方案。设为 true 则跳过联网,直接开热点
const bool USE_AP_MODE = false;   // false = 尝试连接路由器;true = 自建热点

const char* AP_SSID     = class="st">"ESP32-Teaching";     // 自建热点的名称
const char* AP_PASSWORD = class="st">"12345678";            // 自建热点的密码(至少 8 位)

/* ==========================================================================
 * 二、引脚与状态
 * ======================================================================== */
#define LED_PIN 2
bool     ledState  = false;
class="kw">unsigned long bootTime = 0;     // 记录程序启动时刻,用于计算运行时长

// 创建 HTTP 服务器对象,监听 80 端口
// 80 端口是 HTTP 的默认端口,所以浏览器只要输 IP 地址,不用加端口号
WebServer server(80);

/* ==========================================================================
 * 三、自定义函数:把 LED 状态写进 HTML 变量
 * 目的:让首页能动态显示当前 LED 状态,而不是写死一段文字。
 * ======================================================================== */
class="kw">String buildStatusHtml() {
  class="kw">unsigned long uptimeSec = (millis() - bootTime) / 1000;  // 换算成秒
  class="kw">String html;

  html += class="st">"<!DOCTYPE html><html><head><meta charset='UTF-8'>";
  html += class="st">"<meta name='viewport' content='width=device-width,initial-scale=1'>";
  html += class="st">"<title>ESP32 状态面板</title>";
  html += class="st">"<style>";
  html += class="st">"body{font-family:monospace;background:#0b0f14;color:#e6edf3;padding:24px;margin:0}";
  html += class="st">"h1{color:#22d3ee;font-size:20px}";
  html += class="st">".card{background:#111820;border:1px solid #22303c;border-radius:12px;padding:16px;margin:12px 0}";
  html += class="st">"b{color:#f5a524}";
  html += class="st">"a{display:inline-block;background:#22d3ee;color:#06121a;padding:12px 22px;border-radius:8px;text-decoration:none;font-weight:bold;margin:6px}";
  html += class="st">"a.off{background:#334155;color:#cbd5e1}";
  html += class="st">"</style></head><body>";
  html += class="st">"<h1>ESP32 实时状态面板</h1>";

  // 第一张卡:连接信息
  html += class="st">"<div class='card'>";
  html += class="st">"<p>运行模式:<b>" + class="kw">String(USE_AP_MODE ? class="st">"AP 热点模式" : class="st">"STA 联网模式") + class="st">"</b></p>";
  html += class="st">"<p>IP 地址:<b>" + WiFi.localIP().toString() + class="st">"</b></p>";
  html += class="st">"<p>信号强度:<b>" + class="kw">String(WiFi.RSSI()) + class="st">" dBm</b></p>";
  html += class="st">"<p>运行时长:<b>" + class="kw">String(uptimeSec) + class="st">" 秒</b></p>";
  html += class="st">"<p>芯片可用堆内存:<b>" + class="kw">String(ESP.getFreeHeap()) + class="st">" 字节</b></p>";
  html += class="st">"</div>";

  // 第二张卡:LED 控制
  html += class="st">"<div class='card'><p>LED 当前状态:<b>" + class="kw">String(ledState ? class="st">"ON 点亮" : class="st">"OFF 熄灭") + class="st">"</b></p>";
  html += class="st">"<a href='/on'>点亮 LED</a>";
  html += class="st">"<a href='/off' class='off'>熄灭 LED</a>";
  html += class="st">"<a href='/toggle' class='off'>翻转状态</a>";
  html += class="st">"</div>";

  html += class="st">"</body></html>";
  class="kw">return html;
}

/* ==========================================================================
 * 四、HTTP 路由处理函数
 * onServerNotFound 是「兜底」:任何没注册的路径都走这里。
 * ======================================================================== */
void handleRoot() {
  server.send(200, class="st">"text/html", buildStatusHtml());   // 200 = OK,text/html = 内容类型
}

void handleNotFound() {
  server.send(404, class="st">"text/plain", class="st">"404 - 页面不存在");  // 404 = Not Found
}

void handleOn() {
  ledState = true;
  digitalWrite(LED_PIN, LOW);           // 低电平点亮
  Serial.println(class="st">"[HTTP] /on  → LED 点亮");
  server.sendHeader(class="st">"Location", class="st">"/");   // 让浏览器跳回首页
  server.send(302);                     // 302 = 临时重定向
}

void handleOff() {
  ledState = false;
  digitalWrite(LED_PIN, HIGH);
  Serial.println(class="st">"[HTTP] /off → LED 熄灭");
  server.sendHeader(class="st">"Location", class="st">"/");
  server.send(302);
}

void handleToggle() {
  ledState = !ledState;
  digitalWrite(LED_PIN, ledState ? LOW : HIGH);
  Serial.println(class="st">"[HTTP] /toggle → 状态翻转");
  server.sendHeader(class="st">"Location", class="st">"/");
  server.send(302);
}

/* ==========================================================================
 * 五、初始化
 * ======================================================================== */
void setup() {
  Serial.begin(115200);
  delay(200);

  bootTime = millis();          // 记录启动时刻
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);  // 初始熄灭

  if (USE_AP_MODE) {
    /* ---------- AP 模式:芯片自己开热点 ---------- */
    Serial.println(class="st">"[WIFI] 启动 AP 热点模式...");
    WiFi.mode(WIFI_AP);
    WiFi.softAP(AP_SSID, AP_PASSWORD);
    Serial.print(class="st">"[WIFI] 热点名称:");
    Serial.println(AP_SSID);
    Serial.print(class="st">"[WIFI] 热点 IP:");
    Serial.println(WiFi.softAPIP());   // 通常是 192.168.4.1
  } else {
    /* ---------- STA 模式:连接家中的路由器 ---------- */
    Serial.print(class="st">"[WIFI] 正在连接 ");
    Serial.print(WIFI_SSID);
    Serial.println(class="st">" ...");

    WiFi.mode(WIFI_STA);              // 设置为客户端模式
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);  // 开始连接

    // 用 millis 计时最多等 15 秒,而不是 delay(15000) 死等,
    // 这样即使连不上,程序也不会卡住,还能继续跑后面的逻辑
    class="kw">unsigned long startTime = millis();
    class="kw">while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < 15000) {
      delay(500);
      Serial.print(class="st">".");
    }
    Serial.println();

    if (WiFi.status() == WL_CONNECTED) {
      Serial.println(class="st">"[WIFI] 连接成功!");
      Serial.print(class="st">"[WIFI] IP 地址:");
      Serial.println(WiFi.localIP());
    } else {
      Serial.println(class="st">"[WIFI] 连接失败,请检查 WiFi 名称/密码是否正确");
      // 失败时自动降级成 AP 热点,保证用户至少能打开页面看到说明
      WiFi.mode(WIFI_AP);
      WiFi.softAP(AP_SSID, AP_PASSWORD);
      Serial.print(class="st">"[WIFI] 已降级为热点模式,IP:");
      Serial.println(WiFi.softAPIP());
    }
  }

  /* ---------- 注册 HTTP 路由 ---------- */
  server.on(class="st">"/", handleRoot);        // 根路径 → 首页
  server.on(class="st">"/on", handleOn);        // /on → 点亮
  server.on(class="st">"/off", handleOff);      // /off → 熄灭
  server.on(class="st">"/toggle", handleToggle);// /toggle → 翻转
  server.onNotFound(handleNotFound); // 兜底处理
  server.begin();                   // 启动服务器,开始监听 80 端口

  Serial.println(class="st">"[HTTP] 服务器已启动,浏览器输入上面的 IP 即可访问");
}

/* ==========================================================================
 * 六、主循环
 * ======================================================================== */
void loop() {
  // handleClient() 负责处理浏览器发来的 HTTP 请求。
  // 它内部是阻塞式的,但单次调用极快(毫秒级),所以放在 loop 里连续调用即可。
  server.handleClient();
}
完整源码 206 行 · ch05_wifi/ch05_wifi.inoarduino-cli 编译通过
5

本章小结

① ESP32 的 WiFi 射频只支持 2.4GHz,5GHz 路由器无法连接——这是首要原因。

② STA(客户端)和 AP(热点)是两种不同角色,代码里靠 WiFi.mode() 切换。

③ 永远不要用 delay(15000) 等连接,用 millis() 做带超时的循环。

④ WiFi.status() 返回 WL_CONNECTED 才算真正连上,光有 IP 不代表有网。

⑤ 加上「失败降级为 AP 热点」的逻辑,能让设备在路由器不在时依然可用。

6

本章练习

练习 1

把 USE_AP_MODE 改成 true,验证热点模式能正常打开页面。

练习 2

在首页多加一行显示 ESP.getChipId() 和 ESP.getFlashChipSize()。

练习 3

增加 /blink?ms=500 路由,用 server.arg 读取查询参数。

练习 4

故意填错 WiFi 密码,验证降级逻辑生效并观察串口日志。

7

下一章

06
NEXT · 第 06 章

蓝牙 BLE 手机控制

GATT 服务与特征值、读写与通知回调、手机调试工具实操。不需要路由器,手机就是你的遥控器。

开始学习