4、WiFi进阶:WiFi多连接、WiFi扫描、WiFi重连机制与稳定性优化
上一章我们把WiFi的基础连接跑通了,设备能上网了。但说实话,那只是入门。真正到了产品里,你会发现WiFi这玩意儿远比你想象的「皮」。信号一弱、路由器重启、周围信道干扰——任何一个场景都能让你的设备掉线,而且再也连不回去。
我最早做的一款智能插座,就是栽在这个坑里。用户反馈说「插座用两天就离线了,必须断电重启」。查了三天日志,发现是WiFi重连逻辑写得太天真——只连一次,断了就傻等。从那以后,我对WiFi的「多连接」、「扫描」和「重连机制」这三个东西,再也不敢马虎。
这一章,我们就来把这几个硬骨头啃下来。
本章核心目标:掌握ESP32/ESP8266环境下WiFi多连接管理、主动扫描策略、智能重连机制,以及一套经过实战检验的稳定性优化方案。
4.1 WiFi多连接:Station + AP 共存模式
很多场景下,设备既要连路由器上网(Station模式),又要让手机能直连设备进行配置或调试(AP模式)。这两个模式能不能同时跑?能。ESP32/ESP8266的WiFi协议栈支持这种「混合模式」。
我习惯叫它「双面人模式」。设备一边是客户端,一边是热点。听起来简单,但实际用起来有几个坑。
4.1.1 模式初始化顺序
顺序很重要。先设模式,再开AP,最后连Station。反过来容易出问题。
// 正确的初始化顺序
WiFi.mode(WIFI_AP_STA); // 先设混合模式
WiFi.softAP("MyDevice_AP", "12345678"); // 再开AP
WiFi.begin("HomeWiFi", "password"); // 最后连路由器
注意:不要先调用 WiFi.begin() 再改模式。我曾经这么干过,结果AP死活起不来,日志里报「softAP create failed」。原因是Station连接过程占用了太多底层资源,AP初始化被阻塞了。
4.1.2 多SSID自动切换
产品部署到用户家里,你不可能只写死一个WiFi。用户可能换了路由器,或者家里有多个AP。这时候就需要「多SSID自动切换」机制。
我的做法是:在Flash里存一个SSID列表,按优先级排序。连接失败时自动尝试下一个。
typedef struct {
char ssid[32];
char password[64];
uint8_t priority; // 0最高
} wifi_credential_t;
wifi_credential_t creds[] = {
{"Home_5G", "pass123", 0},
{"Home_2.4G", "pass123", 1},
{"Guest_WiFi", "guest456", 2}
};
void connect_best_wifi() {
for (int i = 0; i < 3; i++) {
WiFi.begin(creds[i].ssid, creds[i].password);
if (wait_for_connect(5000)) { // 5秒超时
Serial.println("Connected to: " + String(creds[i].ssid));
return;
}
}
Serial.println("All SSIDs failed, entering AP mode...");
}
经验之谈:5G频段信号强但穿墙差,2.4G穿墙好但干扰多。我一般把5G放最高优先级,2.4G做备选。用户路由器重启后,5G信号恢复快,能自动切回去。
4.2 WiFi扫描:主动探测与信道管理
WiFi扫描,说白了就是让设备「竖起耳朵听一听周围有哪些路由器」。但怎么听、听多久、听完怎么选,这里面门道不少。
4.2.1 主动扫描 vs 被动扫描
| 扫描方式 | 原理 | 耗时 | 功耗 | 适用场景 |
|---|---|---|---|---|
| 主动扫描 | 设备发送Probe Request,AP回复Probe Response | 快(2-5秒) | 高 | 首次连接、快速切换 |
| 被动扫描 | 设备监听AP定期发送的Beacon帧 | 慢(5-15秒) | 低 | 低功耗巡检、后台维护 |
你想想看,主动扫描就像你站在路口大喊「谁在这?」,AP们纷纷回应。被动扫描则是你蹲在路边听AP们每隔100ms喊一次「我在这」。哪个快?当然是主动扫描。但主动扫描会多耗电,因为要发射信号。
4.2.2 扫描结果排序与选择
扫描完拿到一堆AP信息,不能随便连。我一般按RSSI(信号强度)排序,同时过滤掉信号太弱的(比如RSSI < -80dBm)。
void scan_and_select() {
int n = WiFi.scanNetworks();
if (n == 0) {
Serial.println("No networks found");
return;
}
// 按RSSI降序排序(信号从强到弱)
int indices[n];
for (int i = 0; i < n; i++) indices[i] = i;
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
swap(indices[i], indices[j]);
}
}
}
// 选第一个信号够强且SSID匹配的
for (int i = 0; i < n; i++) {
int idx = indices[i];
if (WiFi.RSSI(idx) > -80 && is_known_ssid(WiFi.SSID(idx))) {
WiFi.begin(WiFi.SSID(idx), get_password(WiFi.SSID(idx)));
return;
}
}
Serial.println("No suitable AP found");
}
关键点:扫描会打断当前的WiFi连接。如果你在Station模式下做扫描,设备会短暂断网。我一般在设备空闲时(比如刚启动、或者检测到掉线时)才触发扫描,避免影响正常通信。
4.3 WiFi重连机制:从「傻等」到「智能」
这是本章最核心的部分。WiFi掉线不可怕,可怕的是掉线后连不回来。我见过太多产品,重连逻辑就是一行 WiFi.reconnect() 放在loop里——结果就是疯狂重连、疯狂失败、死循环。
4.3.1 指数退避重连
正确的做法是:每次重连失败后,等待时间翻倍。第一次等1秒,第二次等2秒,第三次等4秒……直到一个上限(比如60秒)。这叫「指数退避」。好处是:
- 避免频繁重连浪费电
- 给路由器恢复的时间
- 减少WiFi信道拥堵
unsigned long retry_delay = 1000; // 初始1秒
const unsigned long MAX_DELAY = 60000; // 最长60秒
void handle_reconnect() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi lost, reconnecting in " + String(retry_delay / 1000) + "s...");
delay(retry_delay);
WiFi.reconnect();
if (wait_for_connect(10000)) {
Serial.println("Reconnected!");
retry_delay = 1000; // 重置为初始值
} else {
retry_delay = min(retry_delay * 2, MAX_DELAY); // 翻倍
}
}
}
我曾经踩过的坑:指数退避的上限设得太小(比如10秒)。结果路由器断电重启需要30秒,设备在10秒内重连了3次都失败,然后放弃了。后来我把上限改成60秒,配合心跳检测,问题才解决。
4.3.2 心跳保活与看门狗
WiFi连接状态不是实时更新的。有时候你以为还连着,其实路由器已经把你踢了。我习惯加一个「心跳保活」机制:
- 每隔30秒ping一次网关(或DNS服务器)
- 连续3次ping不通,判定为掉线
- 触发重连流程
unsigned long last_heartbeat = 0;
int ping_fail_count = 0;
const int PING_TIMEOUT = 3000; // 3秒超时
const int MAX_FAILS = 3;
void heartbeat_check() {
if (millis() - last_heartbeat > 30000) {
last_heartbeat = millis();
if (ping_gateway(PING_TIMEOUT)) {
ping_fail_count = 0;
} else {
ping_fail_count++;
if (ping_fail_count >= MAX_FAILS) {
Serial.println("Heartbeat failed, forcing reconnect...");
WiFi.disconnect();
handle_reconnect();
ping_fail_count = 0;
}
}
}
}
小技巧:ping网关比检查WiFi.status()靠谱得多。WiFi.status()返回WL_CONNECTED不代表你能上网,可能只是关联上了但没拿到IP。ping一下,真金不怕火炼。
4.4 稳定性优化:实战中的「三板斧」
前面讲了多连接、扫描、重连,但光有这些还不够。产品级的稳定性,需要一套组合拳。我总结了三板斧:
4.4.1 第一板斧:WiFi事件回调
不要用轮询检查WiFi状态。用事件回调,系统会在状态变化时主动通知你。
WiFi.onEvent([](WiFiEvent_t event) {
switch (event) {
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("Disconnected! Reason: " + String(WiFi.disconnectReason()));
handle_reconnect();
break;
case SYSTEM_EVENT_STA_GOT_IP:
Serial.println("Got IP: " + WiFi.localIP().toString());
retry_delay = 1000; // 重置退避
break;
case SYSTEM_EVENT_STA_CONNECTED:
Serial.println("Associated with AP");
break;
}
});
事件回调的好处是:不掉帧。轮询可能错过短暂的状态变化,回调不会。
4.4.2 第二板斧:WiFi掉电保存与快速恢复
设备断电重启后,如果重新扫描再连接,耗时太长。我习惯把上次成功连接的AP信息保存到RTC内存或NVS里,重启后直接尝试连接,省去扫描时间。
#include <Preferences.h>
Preferences prefs;
void save_wifi_config(String ssid, String password) {
prefs.begin("wifi", false);
prefs.putString("ssid", ssid);
prefs.putString("password", password);
prefs.end();
}
bool load_and_connect() {
prefs.begin("wifi", true);
String ssid = prefs.getString("ssid", "");
String password = prefs.getString("password", "");
prefs.end();
if (ssid.length() > 0) {
WiFi.begin(ssid.c_str(), password.c_str());
return wait_for_connect(10000);
}
return false;
}
4.4.3 第三板斧:WiFi与任务看门狗联动
WiFi重连过程中,如果卡死在某个API里(比如WiFi.begin()内部阻塞),整个系统可能死掉。我习惯给WiFi任务单独加一个看门狗定时器,超时自动重启WiFi任务。
// FreeRTOS任务看门狗示例
void wifi_task(void *param) {
while (1) {
esp_task_wdt_reset(); // 喂狗
if (WiFi.status() != WL_CONNECTED) {
handle_reconnect();
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 在主任务中创建
xTaskCreatePinnedToCore(wifi_task, "wifi_task", 4096, NULL, 1, NULL, 0);
总结一下:WiFi稳定性不是靠一个函数搞定的。多连接让你灵活,扫描让你选对路,重连机制让你不掉队,事件回调、掉电保存、看门狗这三板斧则把系统焊死。我做了这么多年物联网,见过太多设备死在WiFi上——不是硬件不行,是软件没做扎实。
嗯,这一章的内容就到这。代码示例都是我在项目里实际跑过的,你可以直接拿去用。但记住,参数要根据你的产品场景调——电池供电的设备,扫描间隔要拉长;插座类设备,重连退避上限可以设短一点。没有银弹,只有适配。