基于ESP32的Wi-Fi数据记录器:从环境扫描到物联网数据采集实战
1. 项目概述:一个基于ESP32的Wi-Fi数据记录器
最近在折腾一个物联网数据采集的小项目,需要把几个传感器节点的数据汇总到一个中心点。一开始想用LoRa,但考虑到部署成本和网络覆盖,最后还是决定用最普遍的Wi-Fi。在GitHub上翻找现成的轮子时,我发现了VedantParanjape的esp-wifi-logger项目。这个项目名字直白得很,就是一个运行在ESP32上的Wi-Fi数据记录器。但别被它的名字骗了,它可不是简单地连上Wi-Fi发个“Hello World”就完事了。我深入研究后发现,它实际上是一个设计精巧的、用于系统化采集和上报周边Wi-Fi环境数据的固件方案。
简单来说,你可以把它想象成一个“Wi-Fi嗅探器”或者“无线环境扫描仪”。它的核心工作是让ESP32芯片周期性地扫描周围的Wi-Fi网络,收集诸如SSID(网络名称)、BSSID(路由器的MAC地址)、信号强度(RSSI)、信道、加密方式等关键信息,然后通过某种方式(比如HTTP POST)将这些数据打包发送到你指定的服务器上。这个功能听起来简单,但在实际应用中却非常有用。比如,你可以用它来做室内定位的指纹采集、无线网络质量监控、甚至分析某个区域的Wi-Fi设备密度和活跃度。
我自己上手部署了一套,用它来监控办公室的无线网络环境变化,比如新接入了哪些设备、哪个位置的信号衰减严重。整个过程下来,我觉得这个项目虽然代码量不大,但结构清晰,考虑到了嵌入式开发中常见的稳定性、功耗和数据处理问题,非常适合作为学习ESP32网络编程和物联网数据上报的入门实践,也能直接用于一些轻量级的无线环境监测场景。
2. 项目核心设计思路与架构拆解
2.1 为什么选择ESP32?
这个项目的硬件基石是ESP32,这个选择非常精准。首先,ESP32内置了完整的2.4GHz Wi-Fi和蓝牙射频模块,这意味着我们不需要外接任何无线芯片,硬件成本极低,一个NodeMCU-32S开发板也就二三十块钱。其次,它的双核处理器和充足的内存(通常4MB Flash)足以应付周期性的Wi-Fi扫描、数据封装和网络通信任务,而不会显得力不从心。最后,ESP-IDF(乐鑫官方开发框架)提供了强大且稳定的Wi-Fi API,从简单的Station/AP模式到底层的Promiscuous(混杂)模式监听都支持,这为esp-wifi-logger的实现提供了坚实的软件基础。
项目的核心逻辑是一个典型的“采集-处理-上报”循环。它没有采用复杂的实时操作系统概念,而是基于FreeRTOS,创建了若干个任务来协同工作。主要任务通常包括:一个负责定时触发Wi-Fi扫描的任务;一个负责处理扫描结果、过滤重复数据、格式化JSON的任务;以及一个负责通过HTTP或MQTT等协议将数据发送到云端服务器的网络任务。这种任务分离的设计提高了代码的模块化程度,也便于调试和功能扩展。
2.2 数据流与关键组件解析
让我们顺着数据流动的路径,看看这个记录器内部是怎么工作的:
扫描触发器:通常是一个定时器或一个FreeRTOS任务,按照预设的间隔(例如每30秒)发出扫描指令。这里的一个设计关键是平衡数据新鲜度和系统功耗。扫描过于频繁会快速消耗电池电量(如果使用电池供电),而间隔太长又会丢失重要的网络状态变化事件。
Wi-Fi扫描引擎:调用ESP-IDF的
esp_wifi_scan_start()API。这里有几个重要参数需要配置:scan_type:主动扫描还是被动扫描。主动扫描更快,但功耗略高;被动扫描更隐蔽,但耗时更长。项目通常采用主动扫描。channel:指定扫描哪个信道。设置为0表示扫描所有信道(1-13),这能获得最全面的信息,但时间最长。也可以指定扫描特定信道,适用于专注监控某个频段。show_hidden:是否扫描隐藏SSID的网络。对于完整的环境分析,需要将其设置为true。
数据处理器:扫描结束后,通过回调函数获取到一组
wifi_ap_record_t结构体数组。原始数据是庞杂的,这个环节的工作就是“提炼”。处理器需要遍历所有发现的AP(接入点),提取我们关心的字段,并很可能进行一些初步的过滤,比如信号强度太弱(RSSI < -90 dBm)的AP可以直接忽略,以减少无用数据的上传量。数据格式化器:将过滤后的AP列表转换成便于传输和解析的格式,最常用的就是JSON。一个典型的AP数据包可能长这样:
{ "device_id": "ESP32_ABCDEF", "timestamp": 1678886400, "scan_results": [ { "ssid": "HomeWiFi", "bssid": "AA:BB:CC:DD:EE:FF", "rssi": -65, "channel": 6, "auth_mode": "WPA2_PSK", "is_hidden": false }, { "ssid": "", "bssid": "11:22:33:44:55:66", "rssi": -82, "channel": 11, "auth_mode": "OPEN", "is_hidden": true } ] }注意,这里加入了
device_id(设备唯一标识)和timestamp(时间戳),这对于服务器端区分数据来源和进行时序分析至关重要。网络发送器:这是数据旅程的最后一站。项目通常使用HTTP Client将上述JSON数据POST到一个预设的服务器端点。这里需要考虑网络异常处理(重试机制)、JSON数据包的压缩(如果AP数量很多)以及可能的HTTPS加密传输。有些变种项目也会集成MQTT,这对于需要低延迟或双向通信的场景更合适。
注意:在实际部署中,务必确保你的服务器API能够处理可能的高频或突发请求,并做好数据去重和验证,防止恶意数据注入。
3. 从零开始构建与部署实战
3.1 硬件准备与开发环境搭建
你需要一块ESP32开发板,比如流行的ESP32-DevKitC或NodeMCU-32S。一根Micro-USB数据线用于供电和编程。软件方面,你需要搭建ESP-IDF开发环境。我强烈推荐使用VSCode加上乐鑫官方的ESP-IDF扩展,这能极大简化环境配置和编译下载流程。
获取源代码:使用Git克隆
esp-wifi-logger仓库到本地。git clone https://github.com/VedantParanjape/esp-wifi-logger.git cd esp-wifi-logger配置项目:在项目根目录,执行
idf.py menuconfig。这是最关键的一步,所有硬件和软件行为都在这里配置。- 串口配置:在
Serial flasher config里设置正确的端口号。 - Wi-Fi扫描配置:项目通常有专门的配置菜单(可能叫
WiFi Logger Configuration),在这里设置扫描间隔时间(Scan Interval)、是否扫描隐藏网络等。 - 网络配置:这是核心。你需要配置设备连接到一个可用的Wi-Fi网络(作为Station),以便它能访问互联网上报数据。在
Example Connection Configuration里填入目标Wi-Fi的SSID和密码。 - 服务器配置:找到HTTP服务器相关的配置项,填入你后端服务器的URL地址,例如
http://your-server.com/api/wifi-scan。如果服务器需要认证,可能还需要配置API Key或Token。
- 串口配置:在
3.2 关键代码逻辑剖析与定制
让我们深入核心源文件(通常是main.c或wifi_logger.c),看看如何根据需求进行定制。
扫描任务实现:
static void wifi_scan_task(void *pvParameters) { while (1) { // 发起Wi-Fi扫描 esp_wifi_scan_start(&scan_config, true); // 第二个参数true表示阻塞等待扫描完成 // 获取扫描结果数量 uint16_t ap_count = 0; esp_wifi_scan_get_ap_num(&ap_count); if (ap_count > 0) { wifi_ap_record_t *ap_list = malloc(sizeof(wifi_ap_record_t) * ap_count); ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_list)); // 处理并发送数据 process_and_send_scan_results(ap_list, ap_count); free(ap_list); } // 等待下一个扫描周期 vTaskDelay(SCAN_INTERVAL_MS / portTICK_PERIOD_MS); } }在这个循环中,SCAN_INTERVAL_MS决定了扫描频率。对于环境监测,设置为30秒到5分钟都是合理的。太短会浪费电和网络资源,太长则可能错过重要事件。
数据处理与发送逻辑: 在process_and_send_scan_results函数中,我们需要构建JSON并发送。这里使用cJSON库非常方便。
void process_and_send_scan_results(wifi_ap_record_t *ap_list, uint16_t count) { cJSON *root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "device_id", DEVICE_ID); cJSON_AddNumberToObject(root, "timestamp", (double)time(NULL)); cJSON *results = cJSON_CreateArray(); for (int i = 0; i < count; i++) { // 简单过滤:只上报信号强度大于-85dBm的网络 if (ap_list[i].rssi > -85) { cJSON *ap = cJSON_CreateObject(); // 注意:ssid可能不是以NULL结尾,需要小心处理 char ssid_str[33] = {0}; strncpy(ssid_str, (char*)ap_list[i].ssid, 32); cJSON_AddStringToObject(ap, "ssid", ssid_str); char bssid_str[18]; snprintf(bssid_str, sizeof(bssid_str), "%02X:%02X:%02X:%02X:%02X:%02X", ap_list[i].bssid[0], ap_list[i].bssid[1], ap_list[i].bssid[2], ap_list[i].bssid[3], ap_list[i].bssid[4], ap_list[i].bssid[5]); cJSON_AddStringToObject(ap, "bssid", bssid_str); cJSON_AddNumberToObject(ap, "rssi", ap_list[i].rssi); cJSON_AddNumberToObject(ap, "channel", ap_list[i].primary); cJSON_AddStringToObject(ap, "auth", auth_mode_to_str(ap_list[i].authmode)); cJSON_AddBoolToObject(ap, "hidden", (ap_list[i].ssid[0] == 0)); // SSID长度为0可能是隐藏网络 cJSON_AddItemToArray(results, ap); } } cJSON_AddItemToObject(root, "scan_results", results); char *json_str = cJSON_PrintUnformatted(root); // 调用HTTP POST函数发送json_str http_post_data(json_str); free(json_str); cJSON_Delete(root); }这段代码有几个实操要点:第一,对ssid的拷贝必须谨慎,因为wifi_ap_record_t中的ssid字段不一定以\0结尾。第二,BSSID(MAC地址)需要格式化成可读的字符串。第三,我添加了一个简单的RSSI过滤,这在AP密集的环境中可以显著减少数据量。
3.3 编译、烧录与上电测试
配置完成后,在项目根目录执行编译和烧录命令:
idf.py build idf.py -p /dev/ttyUSB0 flash monitorflash命令会将固件烧录到ESP32,monitor则会打开串口监视器,这是调试的窗口。上电后,观察串口日志。你应该会看到设备首先连接到你配置的Wi-Fi,然后开始周期性的扫描,并打印出发现的AP数量和发送HTTP请求的状态码(如HTTP POST Status = 200)。
首次运行验证清单:
- 串口日志显示Wi-Fi连接成功(Got IP)。
- 扫描任务按预设间隔启动,并打印发现
X个AP。 - HTTP POST请求返回状态码200或201(成功)。
- 登录你的服务器,检查是否收到了格式正确的JSON数据。
4. 深入优化与高级应用场景
4.1 功耗优化策略
如果项目需要电池供电,功耗就是生命线。原始的周期性全信道扫描是耗电大户。我们可以进行多级优化:
- 延长扫描间隔:这是最直接有效的方法。根据监测需求,将间隔从30秒调整为5分钟甚至更长。
- 智能扫描策略:并非每次都需要全信道扫描。可以设计一个“学习阶段”,先进行几次全扫描,找出目标区域内最常出现的几个信道(例如办公室Wi-Fi固定在1、6、11信道)。之后的大部分扫描只针对这几个信道,大幅缩短扫描时间。偶尔(比如每10次)再进行一次全扫描以发现新网络。
- 深度睡眠模式:在两次扫描间隔,让ESP32进入深度睡眠(Deep Sleep)。定时器唤醒后,完成一次扫描和上报,然后立刻再次进入深度睡眠。这种模式下,平均电流可以降到毫安甚至微安级别。代码架构需要调整为“深度睡眠-唤醒-初始化-执行任务-深度睡眠”的循环,而不是原来的
vTaskDelay。 - 选择性上报:如果扫描结果与上一次完全一致(没有新AP,信号强度变化不大),可以考虑不上报,或者只上报一个“心跳”包,告诉服务器设备在线但环境无变化。
4.2 数据持久化与断网续传
在网络不稳定的环境中,数据上报可能失败。一个健壮的记录器应该具备本地缓存能力。
- 使用SPIFFS或LittleFS:当HTTP发送失败时,可以将本次的扫描数据以文件形式保存到ESP32的Flash文件系统中。可以设计一个简单的环形缓冲区,只保留最近N次的数据。
- 设计重传机制:在网络恢复后,设备除了发送当前数据,还应检查文件系统中是否有未成功发送的历史数据,并按时间顺序尝试重传。重传时需要标记数据的时间戳,以便服务器端正确处理。
- 数据压缩:如果存储空间紧张或为了节省流量,可以在存储或发送前对JSON数据进行压缩(例如使用deflate算法)。ESP-IDF提供了zlib库的支持。
4.3 扩展应用场景构想
这个基础框架的潜力远不止于简单的环境记录。
室内定位与轨迹追踪:在商场、仓库或博物馆内部署多个固定的
esp-wifi-logger节点。每个节点持续扫描并上报周围顾客手机或设备发出的Wi-Fi Probe Request帧(虽然标准扫描不直接捕获,但ESP32可设置为混杂模式监听)。通过多个节点对同一设备信号强度的三角测量或指纹匹配,可以大致定位设备的位置和移动轨迹。这比部署专用的蓝牙信标成本更低。无线网络健康度监控:在公司或校园网络部署多个记录器。它们不仅可以监测信号覆盖盲区(RSSI持续过弱),还可以分析信道利用率(通过统计每个信道上AP的数量和信号强度,推断干扰程度),甚至检测非法钓鱼AP(出现未授权的、仿冒知名SSID的接入点)。数据汇总到后台可以生成热力图和告警。
客流分析与区域热度:在零售店门口部署。通过统计不同时间段内扫描到的唯一移动设备MAC地址数量(需注意隐私规范,可能需要对MAC地址进行即时哈希匿名化处理),可以估算客流量和驻留时长。结合多个节点的数据,还能分析顾客在店内的流动路径。
智能家居场景联动:记录器检测到主人的手机(特定BSSID)信号强度由弱变强并超过阈值,推断主人回家,自动触发“回家模式”(打开灯光、空调)。反之,信号消失一段时间后,触发“离家模式”。
5. 常见问题排查与实战心得
在实际部署和调试esp-wifi-logger的过程中,我遇到了不少坑,这里总结一下,希望能帮你绕过去。
5.1 编译与烧录问题
问题:
idf.py build失败,提示找不到头文件或函数定义。- 排查:这通常是因为ESP-IDF环境没有正确设置,或者项目依赖的组件(components)路径不对。确保你使用了正确的ESP-IDF版本(查看项目的README或
requirements.txt)。在VSCode中,检查左下角是否显示了正确的ESP-IDF版本和芯片目标(ESP32)。 - 解决:运行
idf.py fullclean然后重新build。如果问题依旧,尝试删除build和sdkconfig目录,重新运行idf.py menuconfig进行配置。
- 排查:这通常是因为ESP-IDF环境没有正确设置,或者项目依赖的组件(components)路径不对。确保你使用了正确的ESP-IDF版本(查看项目的README或
问题:烧录成功,但设备不断重启,串口打印错误信息。
- 排查:仔细阅读重启前的最后几行日志。常见错误有:
E (xx) wifi: wifi nvs_open fail:Wi-Fi初始化失败,可能是NVS(非易失性存储)分区损坏。E (xx) task_wdt: Task watchdog got triggered:某个任务长时间阻塞,看门狗触发重启。Guru Meditation Error:通常是内存访问错误,如空指针、数组越界。
- 解决:对于NVS错误,可以尝试擦除整个Flash:
idf.py -p PORT erase_flash。对于看门狗问题,检查扫描或网络发送任务中是否有死循环或长时间阻塞的调用(如未设置超时的网络请求)。对于内存错误,使用heap_caps_print_heap_info()检查内存泄漏。
- 排查:仔细阅读重启前的最后几行日志。常见错误有:
5.2 运行时网络与数据问题
问题:设备无法连接到配置的Wi-Fi。
- 排查:首先确认SSID和密码正确,并且目标网络是2.4GHz(ESP32不支持5GHz)。检查路由器是否设置了MAC地址过滤。查看串口日志,连接失败通常会有明确的原因代码。
- 解决:确保代码中处理了各种Wi-Fi事件(如
SYSTEM_EVENT_STA_DISCONNECTED),并实现了重连逻辑。可以增加一段代码,在多次连接失败后尝试切换到备份的Wi-Fi网络。
问题:HTTP POST上报失败,返回错误码4xx或5xx。
- 排查:首先在电脑上用Postman或curl工具,手动构造一个相同的JSON数据包,发送到服务器地址,验证服务器接口是否正常、认证是否正确。然后对比设备发送的请求头(可以在代码中打印出来),检查
Content-Type是否为application/json,以及是否有遗漏必要的Header(如Authorization)。 - 解决:在HTTP客户端代码中增加更详细的错误日志,打印出服务器返回的响应体,这通常包含了具体的错误信息。确保服务器端有处理
CORS(跨域资源共享)的策略,如果是从网页前端调试的话。
- 排查:首先在电脑上用Postman或curl工具,手动构造一个相同的JSON数据包,发送到服务器地址,验证服务器接口是否正常、认证是否正确。然后对比设备发送的请求头(可以在代码中打印出来),检查
问题:扫描到的AP数量远少于手机Wi-Fi列表看到的。
- 排查:ESP32的Wi-Fi扫描有其局限性。一次扫描的持续时间是有限的(由
scan_time参数控制),如果时间太短,可能无法完成所有信道的扫描。另外,ESP32可能无法解析某些特定厂商的复杂信标帧。 - 解决:在
menuconfig中适当增加主动扫描每信道的时间(scan_time.active.min和max)。将show_hidden设置为true。但要注意,增加扫描时间会延长每次扫描的周期和功耗。
- 排查:ESP32的Wi-Fi扫描有其局限性。一次扫描的持续时间是有限的(由
5.3 稳定性与性能优化心得
给网络操作加上超时和重试:这是提升稳定性的黄金法则。无论是Wi-Fi连接还是HTTP请求,都必须设置合理的超时时间(例如10-30秒),并实现指数退避算法的重试机制(第一次失败等1秒重试,第二次等2秒,第三次等4秒...)。避免因一次网络波动导致整个任务卡死。
合理管理内存:嵌入式开发中内存是稀缺资源。在
process_and_send_scan_results函数中,使用cJSON_PrintUnformatted生成字符串后,以及http_post_data发送完成后,务必及时free掉分配的动态内存。定期检查堆内存大小,防止内存碎片化。重视日志输出:在关键步骤(开始扫描、获取到AP数量、构建JSON完成、开始发送、发送结果)都打印信息性日志。但在最终稳定版本中,考虑将日志级别调低(如从
INFO改为WARN或ERROR),以减少串口输出对性能的微小影响和Flash磨损。电源管理:如果使用USB供电,问题不大。但如果使用电池,一定要测量不同工作模式下的电流。使用深度睡眠时,确认所有不需要的 peripherals(外设)都已关闭,GPIO引脚处于正确的状态(避免漏电)。一个万用表串联在电源中实测电流,比任何计算都靠谱。
这个项目就像一把瑞士军刀,基础功能简单明了,但通过不同的打磨和扩展,能应对各种复杂的无线环境感知需求。我最喜欢它的一点是,整个系统是自包含的,从数据采集到上报形成了一个完整的闭环,让你能集中精力在业务逻辑和数据分析上,而不是纠缠于底层驱动和协议。如果你正准备踏入物联网数据采集的门,或者正需要一个轻量级的无线环境监控方案,从esp-wifi-logger开始折腾,准没错。
