4、WiFi连接与网络基础:ESP32连接WiFi热点,扫描可用WiFi网络,处理WiFi连接状态机,实现自动重连机制

好,咱们进入第四章。这一章要啃的,是ESP32作为智能音箱的“命根子”——WiFi连接。

你想想看,一个智能音箱如果连不上网,那跟一块砖头有什么区别?语音指令发不出去,音乐播不了,智能家居控制更是空谈。所以,WiFi连接的稳定性和可靠性,直接决定了这个产品的生死。

我个人习惯,在开始任何网络项目之前,先把WiFi这块的“地基”打牢。ESP32的WiFi库其实封装得不错,但坑也不少。今天我们就从零开始,把连接、扫描、状态机、自动重连这些核心机制,一个一个掰开揉碎了讲清楚。

4.1 连接WiFi热点:从“Hello World”到“Hello Network”

先来个最简单的。让ESP32连上你家里的路由器。这就像你第一次用手机输入WiFi密码一样,简单,但至关重要。

代码其实就几行,但背后的逻辑值得琢磨。

#include <WiFi.h>

const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";

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

  WiFi.begin(ssid, password);

  Serial.print("正在连接WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi连接成功!");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  // 你的主程序逻辑
}

这段代码,说白了就是让ESP32去敲门,然后一直等,等到门开了为止。但这里有个问题——如果密码错了呢?如果路由器没开呢?它会一直卡在 while 循环里,出不来。这在产品里是绝对不能接受的。

⚠️ 注意: 生产环境中,绝对不要使用这种阻塞式的等待方式。它会卡死整个系统,导致无法响应其他任务。

4.2 扫描可用WiFi网络:让设备“看见”周围的世界

有时候,我们不知道周围有哪些WiFi热点。或者,我们想让用户从列表里选一个。这时候就需要扫描功能。

ESP32的WiFi扫描,说白了就是让设备像雷达一样,发出探测信号,然后听周围路由器的回应。

void scanNetworks() {
  int n = WiFi.scanNetworks();
  Serial.println("扫描完成");
  if (n == 0) {
    Serial.println("没有找到WiFi网络");
  } else {
    Serial.print("找到 ");
    Serial.print(n);
    Serial.println(" 个网络");
    for (int i = 0; i < n; i++) {
      Serial.print(i + 1);
      Serial.print(": ");
      Serial.print(WiFi.SSID(i));
      Serial.print(" (信号强度: ");
      Serial.print(WiFi.RSSI(i));
      Serial.print(" dBm)");
      Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " [开放]" : " [加密]");
      delay(10);
    }
  }
}

这里有个小细节:WiFi.RSSI() 返回的是信号强度,单位是dBm。数值越大(越接近0),信号越好。比如 -30dBm 就比 -80dBm 强得多。

💡 我的经验: 扫描WiFi时,建议不要频繁调用。每次扫描大约耗时2-3秒,而且会短暂影响WiFi连接。我一般只在设备启动时或用户主动触发时扫描一次。

4.3 WiFi连接状态机:让连接变得“有章可循”

好了,现在我们要解决那个“卡死”的问题。怎么做?用状态机。

状态机,听起来很高大上,其实就是一个有限状态自动机。说白了,就是把WiFi连接的过程,拆成几个明确的状态,然后根据当前状态和事件,决定下一步做什么。

我一般把WiFi连接分成这几个状态:

状态 描述 典型行为
WIFI_DISCONNECTED 未连接或连接断开 尝试连接或等待重连
WIFI_CONNECTING 正在连接中 显示连接动画,超时处理
WIFI_CONNECTED 已成功连接 获取IP,启动网络服务
WIFI_RECONNECTING 自动重连中 指数退避重试

来看看代码实现:

typedef enum {
  WIFI_DISCONNECTED,
  WIFI_CONNECTING,
  WIFI_CONNECTED,
  WIFI_RECONNECTING
} wifi_state_t;

wifi_state_t currentState = WIFI_DISCONNECTED;
unsigned long lastAttemptTime = 0;
int reconnectAttempts = 0;

void wifiStateMachine() {
  switch (currentState) {
    case WIFI_DISCONNECTED:
      Serial.println("状态: 未连接,准备连接...");
      WiFi.begin(ssid, password);
      currentState = WIFI_CONNECTING;
      lastAttemptTime = millis();
      break;

    case WIFI_CONNECTING:
      if (WiFi.status() == WL_CONNECTED) {
        Serial.println("状态: 连接成功!");
        currentState = WIFI_CONNECTED;
        reconnectAttempts = 0; // 重置重连次数
      } else if (millis() - lastAttemptTime > 10000) { // 10秒超时
        Serial.println("状态: 连接超时,进入重连模式");
        currentState = WIFI_RECONNECTING;
      }
      break;

    case WIFI_CONNECTED:
      if (WiFi.status() != WL_CONNECTED) {
        Serial.println("状态: 连接丢失!");
        currentState = WIFI_DISCONNECTED;
      }
      // 在这里处理已连接的业务逻辑
      break;

    case WIFI_RECONNECTING:
      // 指数退避:每次重连间隔翻倍,但不超过5分钟
      unsigned long waitTime = min(300000, 1000 * (1 << reconnectAttempts));
      if (millis() - lastAttemptTime > waitTime) {
        Serial.print("状态: 尝试第 ");
        Serial.print(reconnectAttempts + 1);
        Serial.println(" 次重连...");
        WiFi.reconnect();
        currentState = WIFI_CONNECTING;
        lastAttemptTime = millis();
        reconnectAttempts++;
      }
      break;
  }
}

这段代码,你仔细看,其实就是一个“有限状态机”的典型实现。每个状态只做一件事,状态之间的转换清晰明了。这样,你的程序就不会乱,也不会卡死。

🔑 核心思想: 状态机的本质,就是把复杂的过程拆解成简单的、可预测的步骤。每个步骤只关心自己的事,然后通过事件驱动状态转换。

4.4 自动重连机制:让设备“打不死的小强”

WiFi断连是家常便饭。路由器重启、信号干扰、甚至有人不小心踢了电源线。所以,自动重连机制是必须的。

我刚才在状态机里已经实现了重连逻辑,但这里有几个关键点要强调:

  1. 指数退避(Exponential Backoff):不要一断就连,连不上就疯狂重试。这样只会让路由器更忙,设备更耗电。正确的做法是:第一次等1秒,第二次等2秒,第三次等4秒...以此类推,直到一个上限(比如5分钟)。
  2. 重连次数限制:不能无限重试。我一般设置一个最大次数(比如10次),如果10次都连不上,就进入深度休眠或通知用户。
  3. 连接状态回调:ESP32提供了WiFi事件回调机制,可以让你在连接成功、断开等事件发生时,自动执行一些操作。

来看看事件回调的用法:

void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case SYSTEM_EVENT_STA_GOT_IP:
      Serial.println("获取到IP地址!");
      Serial.print("IP: ");
      Serial.println(WiFi.localIP());
      // 可以在这里启动MQTT、HTTP等服务
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("WiFi断开连接!");
      // 可以在这里记录断连时间,触发重连
      break;
    default:
      break;
  }
}

void setup() {
  WiFi.onEvent(WiFiEvent);
  WiFi.begin(ssid, password);
}

这个回调机制,我个人非常喜欢。它让你不用在主循环里轮询 WiFi.status(),系统会自动通知你。这样代码更干净,也更高效。

⚠️ 曾经踩过的坑: 有一次,我在回调函数里直接调用了 delay(),结果导致WiFi任务被阻塞,整个系统崩溃。记住,回调函数里不要做耗时操作,只做标记和通知。真正的处理逻辑,放到主循环或任务里去。

4.5 实战建议:让WiFi连接更健壮

好了,理论讲完了,来点实战经验。我做了这么多物联网项目,总结了几条让WiFi连接更健壮的建议:

  • 保存WiFi配置到NVS:第一次连接成功后,把ssid和密码保存到ESP32的非易失性存储(NVS)中。下次启动时,直接读取,不用重新配网。
  • 使用WiFiManager库:如果你想让用户通过手机APP或网页来配网,可以用WiFiManager库。它提供了一个Web界面,用户可以在上面选择WiFi并输入密码。
  • 心跳检测:即使WiFi状态显示已连接,也不代表网络真的通。我习惯每隔30秒ping一下网关或云服务器,如果连续3次ping不通,就主动断开重连。
  • 区分“断连”和“信号弱”:有时候WiFi没断,但信号极差,导致数据传输失败。这时候可以设置一个RSSI阈值,低于阈值就主动切换或重连。

嗯,说到心跳检测,我记得有一次做智能家居网关,设备在角落里,信号时好时坏。WiFi状态一直显示已连接,但数据就是发不出去。后来加了心跳检测,才发现问题。从那以后,我再也不相信 WiFi.status() 的“表面功夫”了。

4.6 小结

这一章,我们从一个简单的WiFi连接开始,逐步深入到扫描、状态机、自动重连。你想想看,这些机制组合起来,就是一个健壮的WiFi连接系统。

下一章,我们会在这个基础上,加入HTTP和WebSocket通信,让智能音箱真正“开口说话”。但在此之前,请确保你的WiFi连接足够稳定。毕竟,地基不牢,地动山摇。

好,今天就到这里。动手试试吧,把代码烧录到你的ESP32上,看看它能不能稳定地连上WiFi,并且在断网后自动恢复。有任何问题,欢迎在课程群里交流。