1. 项目缘起:从“信号满格”到“量化感知”的转变
我们每天都在用WiFi,手机、电脑上那个信号格图标是大多数人判断网络好坏的唯一依据。但“满格”真的意味着信号好吗?作为一个经常折腾智能家居和物联网项目的开发者,我早就对那个简单的图标失去了信任。尤其是在部署ESP32这类设备时,把设备放在哪里才能获得最佳连接?隔壁邻居的WiFi会不会干扰我的设备?这些问题,靠肉眼观察信号格是绝对找不到答案的。
这就是为什么我决定动手,用ESP32这个“瑞士军刀”级别的物联网芯片,来做一个真正意义上的WiFi信号扫描仪。它要做的不是简单地告诉你“有信号”或“没信号”,而是像专业的网络分析仪一样,把周围每一个无线接入点(AP)的详细信息都扒出来:它的名字(SSID)、加密方式、信号强度(RSSI)、工作频道(Channel)甚至MAC地址。当你手里有了这些数据,很多问题就迎刃而解了。你可以为你的智能家居中枢找到家里信号最强的位置,可以排查是不是某个邻居的路由器占用了你正在使用的频道导致干扰,甚至可以绘制一张你办公室或家里的WiFi信号热力图。
网上关于ESP32扫描WiFi的代码片段很多,但大多只是把Arduino IDE示例里的
WiFi.scanNetworks()
跑通,在串口打印出一堆数字就结束了。这远远不够。一个真正有用的工具,需要处理扫描过程中的各种边界情况,需要理解RSSI数值背后的含义,需要能把数据以更直观的方式呈现出来。在这篇文章里,我不只会给你一个能跑的代码,更会把我从零搭建这个项目过程中,关于ESP32 WiFi底层机制、扫描策略优化、数据解读以及实际应用场景的所有思考和踩过的坑,毫无保留地分享给你。无论你是物联网新手想了解ESP32的基础无线功能,还是老鸟在寻找一个稳定的信号诊断工具,这篇长文都能给你带来实实在在的收获。
2. ESP32的WiFi扫描能力深度剖析
在开始写代码之前,我们必须先搞清楚手里的“武器”——ESP32的WiFi子系统,到底具备怎样的扫描能力。这决定了我们项目的上限和可能遇到的坑。
2.1 主动扫描 vs. 被动扫描:策略选择背后的权衡
ESP32支持两种基本的WiFi扫描模式:主动扫描和被动扫描。这两种模式的区别,直接影响了扫描速度、功耗和结果的完整性。
主动扫描 是ESP32作为“主动询问者”的角色。它会依次在每个WiFi频道(通常是2.4GHz的1-13频道)上,广播一种叫做“探测请求”的管理帧。这个帧就像在大喊:“这个频道上有人吗?报上名来!”周围处于该频道的无线接入点(AP,比如你的路由器)收到这个请求后,会立刻回复一个“探测响应”帧,其中包含了SSID、支持的速率、加密方式等完整信息。ESP32收到响应,就完成了一次成功的发现。
注意 :主动扫描速度快,能直接获取AP的完整能力信息,但因为它需要发送数据包,所以功耗相对较高,并且在某些严格管制无线频谱的环境(虽然少见)可能不被允许。
被动扫描 则相反,ESP32会像一个“窃听者”,静静地调谐到某个频道,然后监听一段时间。在这段时间里,它会捕获所有经过的无线帧,特别是AP定期广播的“信标帧”。信标帧是AP周期性发送的“心跳包”,里面也包含了SSID、加密方式等核心信息。通过解析这些信标帧,ESP32也能发现网络。
注意 :被动扫描更隐蔽,功耗更低,因为它只接收不发送。但它的缺点是“靠天吃饭”。如果目标AP的信标帧广播间隔很长(比如设为1000毫秒),而你的监听时间很短,就很可能错过它,导致扫描结果不全。此外,一些为了“隐身”而关闭了SSID广播的AP(即不发送包含SSID的信标帧),在被动扫描下是无法被发现的,但在主动扫描下,通过发送定向的探测请求,依然有可能被发现。
在ESP32的Arduino核心库中,默认的
WiFi.scanNetworks()
采用的是
主动扫描
模式。这对于绝大多数通用场景是最佳选择,因为它提供了速度和完整性的最佳平衡。在我们的项目中,也将基于此模式展开。
2.2 RSSI与频道:理解扫描结果的核心指标
扫描完成后,我们会得到一系列AP的信息,其中最关键的两个量化指标是 RSSI 和 频道 。
RSSI 是接收信号强度指示。它是一个负值,单位通常是dBm。这个值越接近0,表示信号越强。例如:
-
-30 dBm到-50 dBm: 信号极强,通常意味着设备就在路由器旁边。 -
-50 dBm到-65 dBm: 信号良好,足以支持高速数据流。 -
-65 dBm到-80 dBm: 信号中等,基础网络连接(如网页浏览)尚可,但可能不稳定。 -
-80 dBm以下: 信号弱,连接会非常不稳定,容易断线。
这里有一个非常重要的 实操心得 :不同厂商、不同型号的WiFi芯片,对RSSI的测量和报告可能存在细微差异。ESP32测出来的-70dBm,和手机测出来的-70dBm,其代表的实际信号质量可能不完全对等。因此,RSSI更适合用于 相对比较 。例如,你可以移动ESP32,观察同一个AP的RSSI变化,来判断哪个位置信号更好,这比绝对值更有意义。
频道 指的是AP工作的无线频率。2.4GHz频段被划分为14个频道(但各地区法规允许的频道不同,常见的是1-13)。每个频道中心频率相隔5MHz,但WiFi信号的实际带宽是20MHz(或更宽的40MHz)。这意味着,工作在频道6的AP,其信号会严重干扰到频道5和频道7。 不重叠的频道只有1、6、11(或2、7、12,或3、8、13) 。扫描结果中的频道信息,是进行无线网络规划和干扰排查的黄金依据。如果你发现家里网速慢,扫描一下发现周围有七八个AP都挤在频道6上,那么把你的路由器切换到频道1或11,很可能就是最立竿见影的优化。
2.3 ESP32 WiFi库的扫描API:灵活性与限制
ESP32 Arduino核心库提供了非常简洁的API。最基本的调用是:
int numNetworks = WiFi.scanNetworks();
这个函数会阻塞执行,直到扫描完成,返回发现的网络数量。之后,你可以通过
WiFi.SSID(i)
,
WiFi.RSSI(i)
,
WiFi.channel(i)
等函数来获取第
i
个网络的具体信息。
但真正强大的地方在于它的
高级参数
。
WiFi.scanNetworks
函数有多个重载版本,允许你精细控制扫描行为:
// 更完整的扫描函数签名
int scanNetworks(bool async = false, bool show_hidden = false, bool passive = false, uint32_t max_ms_per_chan = 300, uint8_t channel = 0, const char* ssid = nullptr);
-
async: 是否异步扫描。如果设为true,函数会立刻返回,你需要通过WiFi.scanComplete()来轮询扫描状态。这在需要同时执行其他任务的场景下非常有用。 -
show_hidden: 是否显示隐藏网络(即关闭SSID广播的网络)。设为true时,主动扫描会尝试发现它们。 -
passive: 是否启用被动扫描模式。如前所述,根据需求选择。 -
max_ms_per_chan: 在每个频道上花费的最大时间(毫秒)。增加这个时间可以提高在被动模式下发现AP的概率,但会显著增加总扫描时间。 -
channel: 指定只扫描单个频道。如果你只想关注2.4GHz的某个特定频道(比如排查频道6的干扰),这个参数就非常高效。 -
ssid: 指定只扫描特定SSID的网络。这用于快速检查某个已知网络是否存在及其信号强度。
理解这些参数,是我们优化扫描行为、适应不同场景的基础。例如,在做快速环境感知时,我们可以用异步、短时间的扫描;而在做精准的信号测量时,则可以用同步、单频道、长时间扫描。
3. 从零构建:一个功能完整的ESP32 WiFi扫描仪
理论铺垫足够,现在让我们动手,一步步搭建一个超越简单示例的、健壮的WiFi扫描仪。我们的目标不仅是列出网络,还要对结果进行排序、过滤,并以更友好的方式输出。
3.1 硬件准备与开发环境搭建
硬件清单:
- ESP32开发板 :任何一款通用的ESP32开发板都可以,比如ESP32 DevKitC、NodeMCU-32S等。它们都集成了天线和必要的电路。
- USB数据线 :用于供电和程序烧录。
开发环境:
我强烈推荐使用
PlatformIO
(作为VSCode插件)而非传统的Arduino IDE。PlatformIO提供了更好的库管理、项目结构和调试体验。在PlatformIO中新建一个项目,选择开发板为
Espressif ESP32 Dev Module
,框架选择
Arduino
即可。
项目的核心依赖就是ESP32的Arduino核心库,PlatformIO会自动处理。如果需要,你可以在
platformio.ini
文件中增加一些配置来优化串口输出速度:
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200 ; 将串口监视器波特率设为115200,与代码中Serial.begin(115200)匹配
3.2 核心代码实现与逐行解析
下面是我们扫描仪项目的核心代码。我会将代码分块,并详细解释每一部分的作用和注意事项。
#include <WiFi.h> // 引入ESP32的WiFi库
// 定义扫描参数
const bool SCAN_ASYNC = false; // 本次使用同步扫描,简单直接
const bool SHOW_HIDDEN = true; // 尝试发现隐藏网络
const uint32_t MAX_SCAN_TIME = 5000; // 最大扫描总时间(毫秒),防止卡死
const uint8_t SCAN_CHANNEL = 0; // 0表示扫描所有频道
// 用于存储扫描结果的简单结构体,方便后续排序
typedef struct {
String ssid;
int32_t rssi;
uint8_t channel;
wifi_auth_mode_t encryption;
bool isHidden;
} WiFiNetworkInfo;
WiFiNetworkInfo foundNetworks[20]; // 假设最多存储20个网络,可根据需要调整
int networkCount = 0;
void setup() {
Serial.begin(115200); // 初始化串口通信,波特率115200
delay(1000); // 给串口和芯片一个稳定的启动时间
Serial.println("\n\n=== ESP32 WiFi 专业扫描仪启动 ===");
Serial.println("初始化WiFi模块为STA模式...");
// 将WiFi设置为工作站(STA)模式,这是进行扫描的必要条件
WiFi.mode(WIFI_STA);
// 断开任何可能存在的先前连接,确保纯净的扫描状态
WiFi.disconnect();
delay(100); // 等待模式切换稳定
Serial.println("准备就绪。");
printHelp(); // 打印操作指南
}
void loop() {
// 监听串口指令
if (Serial.available() > 0) {
char command = Serial.read();
handleCommand(command);
}
// 如果是异步扫描模式,这里可以添加状态检查逻辑,本例同步故省略
}
void handleCommand(char cmd) {
switch(cmd) {
case 's':
case 'S':
performScan();
break;
case 'h':
case 'H':
printHelp();
break;
default:
Serial.println("未知指令,输入 'h' 查看帮助。");
}
}
void printHelp() {
Serial.println("\n--- 指令列表 ---");
Serial.println(" s / S : 执行一次WiFi扫描");
Serial.println(" h / H : 显示此帮助信息");
Serial.println("----------------");
}
以上是框架部分。接下来是最关键的
performScan()
函数,它包含了扫描、数据处理和展示的全流程。
void performScan() {
Serial.println("\n>>> 开始扫描周围WiFi网络...");
Serial.print("模式: ");
Serial.println(SCAN_ASYNC ? "异步" : "同步");
Serial.print("显示隐藏网络: ");
Serial.println(SHOW_HIDDEN ? "是" : "否");
Serial.print("最大扫描时间: ");
Serial.print(MAX_SCAN_TIME);
Serial.println(" ms");
unsigned long scanStartTime = millis(); // 记录开始时间
// 关键步骤1:执行扫描
int numNetworks = WiFi.scanNetworks(SCAN_ASYNC, SHOW_HIDDEN, false, 300, SCAN_CHANNEL);
// 参数解释: async, show_hidden, passive, max_ms_per_chan, channel
// 关键步骤2:处理扫描结果
if (numNetworks == 0) {
Serial.println(">>> 未发现任何网络。");
} else if (numNetworks < 0) {
Serial.println(">>> 扫描失败,WiFi模块可能未就绪。");
} else {
// 清空上一次的存储结果
networkCount = (numNetworks > 20) ? 20 : numNetworks; // 防止溢出
for (int i = 0; i < networkCount; ++i) {
foundNetworks[i].ssid = WiFi.SSID(i);
// 这里有个坑:SSID可能包含非打印字符,直接输出到串口会乱码
// 我们存储String类型,输出时会自动处理。
foundNetworks[i].rssi = WiFi.RSSI(i);
foundNetworks[i].channel = WiFi.channel(i);
foundNetworks[i].encryption = WiFi.encryptionType(i);
foundNetworks[i].isHidden = (WiFi.SSID(i).length() == 0); // 隐藏网络SSID长度为0
}
// 关键步骤3:按信号强度(RSSI)从强到弱排序
bubbleSortNetworksByRSSI();
// 关键步骤4:以表格形式打印结果
printScanResultsTable();
Serial.print(">>> 扫描完成。耗时: ");
Serial.print(millis() - scanStartTime);
Serial.println(" ms");
}
// 关键步骤5:清理扫描缓存,为下一次扫描释放内存
WiFi.scanDelete();
Serial.println(">>> 扫描缓存已清理。\n");
}
代码解析与避坑指南:
-
WiFi.scanNetworks返回值 :这个函数返回发现的网络数量。但需要注意的是,如果扫描还在进行(异步模式),它会返回WIFI_SCAN_RUNNING(-1)。如果扫描失败,可能返回WIFI_SCAN_FAILED(-2)。我们的代码简单处理了0和负数情况,在生产环境中,你可能需要更细致的错误处理。 -
SSID的隐藏与乱码 :这是一个非常容易踩的坑。
WiFi.SSID(i)返回的是String对象。对于隐藏网络,这个String可能是空的(length()==0),也可能包含实际的SSID名(如果show_hidden为true且扫描到了)。更棘手的是,有些AP的SSID可能包含非ASCII字符(如中文),直接通过串口以char*形式输出会导致乱码。使用String类并让Serial.print去处理,兼容性更好。如果你需要将SSID用于其他非String友好的场景(比如某些协议传输),则需要考虑编码转换。 -
排序算法 :我上面用了
bubbleSortNetworksByRSSI这个函数名,实际实现了一个简单的冒泡排序。对于最多20个网络,这完全够用。如果你预期网络数量很多,可以考虑更高效的排序算法,如快速排序。排序能让结果一目了然,快速找到最强的信号源。
void bubbleSortNetworksByRSSI() {
for (int i = 0; i < networkCount - 1; i++) {
for (int j = 0; j < networkCount - i - 1; j++) {
// 按RSSI降序排序(因为RSSI是负值,所以“大于”意味着信号更强)
if (foundNetworks[j].rssi < foundNetworks[j+1].rssi) {
WiFiNetworkInfo temp = foundNetworks[j];
foundNetworks[j] = foundNetworks[j+1];
foundNetworks[j+1] = temp;
}
}
}
}
-
结果展示
:
printScanResultsTable()函数负责美化输出。一个清晰的表格远比杂乱无章的列表有用。我们可以用制表符\t来对齐各列。
void printScanResultsTable() {
Serial.println("\n==================================================================================");
Serial.println("序号\t信号强度\t频道\t加密方式\t\t隐藏\tSSID");
Serial.println("==================================================================================");
for (int i = 0; i < networkCount; ++i) {
Serial.printf("%2d\t", i+1); // 序号
// 信号强度带图形化指示
Serial.printf("%4d dBm\t", foundNetworks[i].rssi);
int bars = calculateSignalBars(foundNetworks[i].rssi);
for(int b=0; b<bars; b++) Serial.print("█");
for(int b=bars; b<4; b++) Serial.print(" ");
Serial.print("\t");
Serial.printf("CH %2d\t", foundNetworks[i].channel);
// 加密方式
switch(foundNetworks[i].encryption) {
case WIFI_AUTH_OPEN: Serial.print("开放\t\t"); break;
case WIFI_AUTH_WEP: Serial.print("WEP\t\t"); break;
case WIFI_AUTH_WPA_PSK: Serial.print("WPA-PSK\t"); break;
case WIFI_AUTH_WPA2_PSK: Serial.print("WPA2-PSK\t"); break;
case WIFI_AUTH_WPA_WPA2_PSK: Serial.print("WPA/WPA2\t"); break;
case WIFI_AUTH_WPA3_PSK: Serial.print("WPA3-PSK\t"); break;
default: Serial.print("未知\t\t");
}
Serial.print(foundNetworks[i].isHidden ? "是\t" : "否\t");
// 输出SSID,如果是隐藏且未获取到名称,则显示<隐藏网络>
String displaySsid = foundNetworks[i].ssid;
if (displaySsid.length() == 0) {
displaySsid = "<隐藏网络>";
}
// 限制SSID显示长度,防止过长破坏表格格式
if (displaySsid.length() > 20) {
displaySsid = displaySsid.substring(0, 17) + "...";
}
Serial.println(displaySsid);
}
Serial.println("==================================================================================");
}
int calculateSignalBars(int32_t rssi) {
if (rssi >= -55) return 4; // 信号极强
else if (rssi >= -65) return 3; // 信号强
else if (rssi >= -75) return 2; // 信号中
else if (rssi >= -85) return 1; // 信号弱
else return 0; // 信号极弱或无
}
-
WiFi.scanDelete()的重要性 :这是很多初学者会忽略的一步。ESP32在内部会缓存扫描结果,如果不手动删除,多次扫描后可能会占用过多内存甚至导致后续扫描失败。 务必在每次处理完扫描结果后调用此函数 。
将以上所有代码段组合起来,你就得到了一个功能完整、输出专业、带有基本错误处理的ESP32 WiFi扫描仪。编译并上传到你的ESP32,打开串口监视器(波特率115200),输入
s
,你就能看到周围WiFi环境的详细报告了。
4. 超越基础:高级功能与实战应用场景
一个能列出网络的扫描仪只是开始。它的真正威力在于我们将这些数据用起来。下面分享几个我实践中摸索出的进阶玩法和应用场景。
4.1 场景一:智能家居设备的最佳位置部署
假设你有一个基于ESP32的智能家居网关,需要稳定的WiFi连接。你可以修改上面的代码,让它定时扫描(比如每30秒一次),但只关注你家的WiFi SSID(比如“MyHomeWiFi”)。
String targetSsid = "MyHomeWiFi";
int targetRssi = -100; // 初始化为一个很弱的值
uint8_t targetChannel = 0;
void scanForTargetNetwork() {
int n = WiFi.scanNetworks(false, false, false, 300, 0, targetSsid.c_str()); // 只扫描特定SSID
for (int i = 0; i < n; ++i) {
if (WiFi.SSID(i) == targetSsid) {
targetRssi = WiFi.RSSI(i);
targetChannel = WiFi.channel(i);
Serial.printf("[位置检测] 找到 %s, RSSI: %d dBm, 频道: %d\n", targetSsid.c_str(), targetRssi, targetChannel);
// 你可以在这里添加逻辑,比如当RSSI低于-75时,触发一个LED报警
break;
}
}
WiFi.scanDelete();
}
然后,你拿着这个ESP32设备在房间里走动,观察串口输出的RSSI值。找到那个信号最强(RSSI值最大,即最接近0)且最稳定的点,那里就是部署网关的绝佳位置。这比凭感觉“大概这里信号好吧”要科学得多。
4.2 场景二:无线网络干扰分析与优化
如果你感觉家里网络时快时慢,很可能是无线干扰。我们的扫描仪可以变身成一个简单的频谱分析仪(虽然只是逻辑上的)。
修改代码,让它按频道分组统计AP数量,并计算每个频道的平均信号强度。
void analyzeChannelCongestion() {
int apCountPerChannel[14] = {0}; // 初始化频道1-13的计数器
long rssiSumPerChannel[14] = {0}; // 记录每个频道的RSSI总和
int n = WiFi.scanNetworks();
for (int i = 0; i < n; ++i) {
uint8_t ch = WiFi.channel(i);
if (ch >= 1 && ch <= 13) { // 确保是合法频道
apCountPerChannel[ch-1]++; // 数组索引从0开始
rssiSumPerChannel[ch-1] += WiFi.RSSI(i);
}
}
Serial.println("\n--- 频道拥堵分析 ---");
for (int ch = 1; ch <= 13; ch++) {
int count = apCountPerChannel[ch-1];
if (count > 0) {
int avgRssi = rssiSumPerChannel[ch-1] / count;
Serial.printf("频道 %2d: 有 %2d 个AP,平均RSSI: %4d dBm", ch, count, avgRssi);
// 高亮显示最拥堵的频道
if (count >= 5) Serial.print(" <<< 高度拥堵!");
Serial.println();
}
}
// 给出建议
Serial.println("\n建议:");
Serial.println("1. 尽量将路由器设置在1, 6, 11这三个互不干扰的频道。");
Serial.println("2. 选择当前AP数量最少、平均信号最强的频道。");
WiFi.scanDelete();
}
运行这个函数,你就能一眼看出哪个频道“车水马龙”,哪个频道“空空荡荡”。把你的路由器切换到那个“空空荡荡”的频道,网络拥堵情况很可能得到显著改善。
4.3 场景三:构建简易的WiFi信号热力图
这是更酷的应用。你需要将ESP32连接一个电池,然后拿着它在空间内移动,在多个预定义的点进行扫描,并将数据(位置坐标和对应的RSSI)通过串口发送到电脑。电脑端可以用Python(配合
pyserial
和
matplotlib
)接收数据,并生成一张简单的信号强度热力图。
ESP32端 需要记录位置信息(比如用编号代表不同测量点):
int measurementPoint = 0; // 假设我们从0号点开始
void takeMeasurementAtPoint(int pointId) {
Serial.printf("[DATA] Point:%d, ", pointId);
int n = WiFi.scanNetworks(false, false, false, 500); // 延长扫描时间获取更稳定数据
for (int i = 0; i < n; ++i) {
if (WiFi.SSID(i) == "MyHomeWiFi") { // 只记录目标网络
Serial.printf("SSID:%s, RSSI:%d\n", WiFi.SSID(i).c_str(), WiFi.RSSI(i));
break;
}
}
WiFi.scanDelete();
delay(2000); // 移动到下一个点前等待
}
电脑端(Python示例) :
import serial
import matplotlib.pyplot as plt
import numpy as np
ser = serial.Serial('COM3', 115200) # 替换成你的串口
points = []
rssi_values = []
while True:
line = ser.readline().decode('utf-8').strip()
if line.startswith('[DATA]'):
parts = line.split(',')
point = int(parts[0].split(':')[1])
rssi = int(parts[2].split(':')[1])
points.append(point)
rssi_values.append(rssi)
print(f"记录点 {point}: RSSI = {rssi} dBm")
# 这里可以添加逻辑,当收集够足够多的点后,生成热力图
# 例如,假设点是在一个网格上测量的,可以将points转换为x,y坐标
# 然后使用 plt.imshow 或 plt.scatter 绘制热力图
通过这个方法,你可以直观地看到整个房间内,WiFi信号的分布情况,找到信号死角,为路由器位置调整或部署WiFi扩展器提供精准的数据支持。
5. 性能调优、常见问题与排错指南
即使代码写好了,在实际使用中你仍可能会遇到各种问题。这一章集中分享我在项目迭代中积累的调优经验和排错方法。
5.1 扫描速度与稳定性的平衡
默认的
WiFi.scanNetworks()
在每个频道上大约停留300毫秒。扫描全部13个频道大约需要4秒。这对于一次性诊断是可以接受的,但对于需要频繁扫描的应用(比如实时信号追踪),就太慢了。
优化策略1:减少扫描频道
如果你确定目标网络只在2.4GHz的特定频道(比如你的路由器固定在频道11),可以将
channel
参数设为11,这样扫描会在1秒内完成。
优化策略2:缩短单频道停留时间
将
max_ms_per_chan
参数降低,比如设为150ms。但这会降低发现那些信号较弱或信标间隔较长的AP的概率,是一种速度与完整性的权衡。
优化策略3:使用异步扫描
在
loop()
函数中启动异步扫描,然后主程序可以继续做其他事情(比如读取传感器),定期检查扫描是否完成。这避免了阻塞,提升了系统响应性。
bool scanInProgress = false;
unsigned long scanStartTime = 0;
void startAsyncScan() {
Serial.println("启动异步扫描...");
WiFi.scanNetworks(true, true); // async=true
scanInProgress = true;
scanStartTime = millis();
}
void loop() {
// ... 其他任务 ...
if (scanInProgress) {
int scanResult = WiFi.scanComplete();
if (scanResult >= 0) {
// 扫描完成,处理结果
processScanResults(scanResult);
WiFi.scanDelete();
scanInProgress = false;
Serial.print("异步扫描完成,总耗时: ");
Serial.println(millis() - scanStartTime);
} else if (scanResult == WIFI_SCAN_FAILED) {
Serial.println("异步扫描失败!");
scanInProgress = false;
}
// 如果 scanResult 仍是 WIFI_SCAN_RUNNING,则扫描仍在继续
}
}
5.2 内存管理与稳定性陷阱
ESP32的内存并不算非常充裕。频繁扫描而不清理缓存,是导致内存泄漏和系统不稳定的常见原因。
-
硬性规定
:每次调用
WiFi.scanNetworks()并处理完数据后, 必须 调用WiFi.scanDelete()。 -
结果数组大小
:我示例中用了固定数组
WiFiNetworkInfo foundNetworks[20]。如果你所处的环境WiFi网络非常多(比如商业中心),20个可能不够。你可以动态分配内存,或者使用std::vector(如果支持STL),但要注意ESP32的堆内存限制。更安全的方法是只存储你关心的前N个最强信号。 -
String对象的隐患
:在
loop中频繁创建和销毁String对象可能导致堆碎片。对于高性能应用,可以考虑使用字符数组(char[])来存储SSID,但要注意缓冲区溢出。
5.3 典型问题排查流程
问题:扫描返回0个网络,但我确定周围有WiFi。
- 检查电源 :ESP32的WiFi射频对电源质量敏感。使用劣质USB线或电脑USB口供电不足会导致射频性能急剧下降。尝试使用手机充电器或稳压电源直接供电。
- 检查天线 :确保开发板上的PCB天线区域没有被金属物体覆盖或用手直接触碰。有些模块有外接天线接口,检查是否连接牢固。
-
验证WiFi模式
:确保在扫描前正确设置了
WiFi.mode(WIFI_STA)和WiFi.disconnect()。 -
增加扫描时间
:将
max_ms_per_chan参数提高到500甚至1000ms,再试一次。 - 检查频道 :有些地区的路由器可能使用不常见的频道(如12, 13),确保你的代码支持扫描所有频道。ESP32的库会根据区域设置自动处理,但可以检查一下。
问题:扫描结果中SSID显示为乱码或奇怪字符。
这几乎可以确定是SSID中包含了非ASCII字符(如中文、表情符号)。ESP32的底层驱动能正确获取这些字节,但当你试图用
Serial.print
以纯文本方式输出时,由于编码不匹配就乱码了。
解决方案
:不要试图去“修复”编码,而是以十六进制的形式输出SSID的原始字节,这样至少能确认数据是正确的。
void printSsidHex(String ssid) {
Serial.print("SSID (Hex): ");
for (size_t i = 0; i < ssid.length(); i++) {
Serial.printf("%02X ", (uint8_t)ssid[i]);
}
Serial.println();
}
问题:设备运行一段时间后,扫描开始失败或系统重启。
这很可能是内存泄漏或看门狗定时器(WDT)超时。
-
检查
scanDelete:确认每次扫描后都调用了。 - 减少堆分配 :避免在循环中动态分配大内存。
-
喂狗
:如果扫描是同步的且时间很长,可能会阻塞主循环,导致看门狗超时。在长时间同步扫描中,可以适当插入
yield()或delay(1),让系统有机会处理后台任务和喂狗。更好的办法是改用 异步扫描 。
5.4 功耗考量
持续扫描是耗电的。对于电池供电的项目,必须精心设计扫描策略。
- 降低扫描频率 :从每秒一次改为每10秒、每1分钟一次。
- 使用深度睡眠 :在两次扫描间隔,让ESP32进入深度睡眠模式。例如,扫描10秒,然后深度睡眠5分钟,再唤醒扫描。这可以将平均电流从几十mA降到几百微安。
- 单次触发 :设计一个物理按钮,只在按下按钮时才执行一次扫描,其他时间完全休眠。
通过这一系列的优化和问题规避,你的ESP32 WiFi扫描仪就能从一个脆弱的实验代码,蜕变成一个稳定、可靠、可用于实际项目的工具。记住,嵌入式开发中,处理边界情况和异常状态的能力,往往比实现核心功能本身更重要。


被折叠的 条评论
为什么被折叠?



