基于ESP8266 WiFi嗅探的低成本人员流量统计方案

基于ESP8266 WiFi嗅探的低成本人员流量统计方案
1. 项目概述用ESP8266打造一个“会数数”的WiFi热点几年前我在一个小型线下活动帮忙主办方想知道每个时段有多少人进出主会场好调整茶歇和活动节奏。当时用的是最土的办法——派两个人在门口手动计数结果可想而知不是漏了就是重复数据一团糟。从那时起我就琢磨着能不能用手里玩剩下的ESP8266开发板做个低成本、自动化的“电子眼”来数人头这个想法就是今天要聊的“ESP8266 WiFi People Counter”的起点。简单说这是一个利用ESP8266单片机的WiFi信号探测能力来统计特定区域内人员数量的装置。它不依赖摄像头不涉及复杂的图像识别而是通过监听和分析周围设备主要是手机发出的WiFi探测请求帧来估算人数。听起来有点玄乎其实原理很直接现代人的手机只要WiFi功能开着即使没连上任何网络就会周期性地广播一种名为“Probe Request”的帧寻找曾经连接过的WiFi网络。我们的ESP8266就像一个安静的监听者捕捉这些广播并去重计数。这个项目能解决什么问题呢首先是低成本的人员流量统计。无论是小店、展厅、图书馆的安静区域还是家庭办公室你都可以用它来了解空间的使用情况又不用承担高昂的商业客流统计系统费用。其次它保护隐私。与摄像头方案不同我们获取的只是设备的MAC地址一个唯一的硬件标识符不涉及人脸等生物信息。最后它极具可玩性和扩展性。数据可以通过WiFi上传到服务器、显示在本地屏幕甚至触发其他智能设备比如人多了自动开空调。适合谁来玩如果你对物联网、单片机编程特别是Arduino生态有初步了解或者是个喜欢动手解决实际问题的创客那么这个项目会非常对胃口。即使你是新手只要跟着步骤走也能一步步实现。整个过程你会接触到网络嗅探、数据过滤、MQTT通信、Web服务器搭建等多个实用技能点绝对干货满满。2. 核心原理与方案选型为什么是ESP8266和WiFi嗅探2.1 技术原理深度拆解这个项目的核心在于理解“WiFi探测请求”。当你的手机WiFi开启时它为了能快速连接到已知网络比如家里的、公司的会不断地在后台“喊话”“XX咖啡馆的WiFi在吗”“我家的路由器在吗”。这种“喊话”就是Probe Request帧。每个帧里都包含了发送设备的MAC地址。ESP8266有一个非常强大的功能就是可以将其WiFi芯片设置为“监听模式”。在这个模式下它不再作为一个普通的WiFi客户端或接入点而是变成一个被动的无线电接收器能够捕获空中所有它所能听到的WiFi数据包包括这些Probe Request。我们的计数逻辑就建立在捕获这些包的基础上捕获ESP8266持续监听空中的WiFi数据包。过滤只提取类型为“管理帧”且子类型为“Probe Request”的包。提取从这些包中解析出源MAC地址。去重与计数我们将MAC地址加入一个列表中并设置一个“观察窗口”比如60秒。如果一个MAC地址在窗口内首次出现我们就认为发现了一个新设备可能对应一个人。如果一个已知的MAC地址超过窗口时间没有再出现我们就认为该设备已离开并从列表中移除。统计当前列表中的唯一MAC地址数量就是估算的在场人数。注意这里存在一个关键假设——“一人一设备”。虽然不绝对有人可能带多台设备也有人可能关闭WiFi但对于大多数公共场景的流量趋势分析这个估算具有很高的参考价值。我们的目标是观察相对变化和趋势而非绝对精确的个位数。2.2 为什么选择ESP8266而不是其他方案市面上能做人数统计的方案很多为什么独独推荐ESP8266这套方案呢我们来做个对比方案成本隐私性复杂度适用场景精准度ESP8266 WiFi嗅探极低 (约20元)高 (仅MAC地址)中等趋势分析、流量监控中等 (设备级估算)红外对射传感器低极高 (无个人信息)低门口单向计数高 (但需安装对齐)摄像头AI识别高低 (涉及人脸)高安防、精准客流高商用客流统计器非常高中/高 (取决于型号)低商场、店铺高选择ESP8266的核心理由成本与性能的完美平衡一块NodeMCU开发板不到一杯咖啡的钱却集成了WiFi、MCU和足够的内存性能足以胜任此任务。非侵入式部署无需像红外对射那样精确对准安装位置也无需像摄像头那样引人注目。它可以被巧妙地隐藏在任何角落。强大的生态支持Arduino Core for ESP8266和丰富的库函数使得开发监听模式、处理网络包变得相对简单。数据可集成性计数结果可以轻松通过WiFi发送到MQTT服务器、Web接口或本地显示屏无缝融入现有的智能家居或物联网系统。2.3 硬件选型与清单你不需要准备太多东西以下是核心清单ESP8266开发板推荐NodeMCU或Wemos D1 Mini。它们自带USB转串口芯片方便烧录和调试。USB数据线用于供电和程序烧录。电源如果长期部署需要一个5V/1A的USB电源适配器。可选显示屏如OLED SSD1306 (I2C接口)用于本地实时显示人数。可选外壳3D打印或找一个合适的小盒子让项目更美观。这里有个实操心得在购买ESP8266时注意一下芯片的具体型号。ESP-12E/F系列是主流内存更大更稳定。对于这个项目任何一款支持Arduino且能运行监听模式的ESP8266板子都可以。3. 开发环境搭建与核心代码解析3.1 软件环境准备我们将在Arduino IDE中进行开发。首先需要做好基础配置安装Arduino IDE从官网下载并安装最新版。添加ESP8266开发板支持打开Arduino IDE进入“文件”-“首选项”。在“附加开发板管理器网址”中填入http://arduino.esp8266.com/stable/package_esp8266com_index.json点击“确定”然后进入“工具”-“开发板”-“开发板管理器”。搜索“esp8266”找到并安装“esp8266 by ESP8266 Community”。安装必要的库我们主要依赖两个库ESP8266WiFi这是核心已经包含在开发板支持包里用于设置监听模式。ArduinoJson可选用于数据上传通过库管理器搜索安装。SSD1306或SH1106可选用于OLED显示通过库管理器搜索“Adafruit SSD1306”和“Adafruit GFX”并安装。3.2 核心代码结构与解析下面我将分块解析代码的关键部分。完整的代码会整合在最后。第一部分全局变量与设置#include ESP8266WiFi.h #include list #include map // 设置监听的信道1-13通常设为所有信道循环扫描或固定到目标设备常用信道 #define CHANNEL 1 #define MAX_DEVICES 100 // 最大跟踪设备数防止内存溢出 #define SCAN_INTERVAL 1000 // 主循环扫描间隔毫秒 #define DEVICE_TIMEOUT 60000 // 设备超时时间毫秒例如60秒 // 用于存储设备信息的结构体 struct Device { String mac; unsigned long lastSeen; }; std::listDevice deviceList; // 使用链表存储当前在场的设备 unsigned long lastChannelHop 0; int currentChannel CHANNEL; // 嗅探回调函数原型 void sniffer_callback(uint8_t *buf, uint16_t len);这里我们使用std::list来动态管理设备列表。选择链表是因为我们需要频繁地进行插入和删除操作设备加入和超时移除链表的效率更高。DEVICE_TIMEOUT是关键参数它定义了“离开”的判断标准。设置太短人会“被离开”得太快设置太长统计结果会滞后。需要根据实际场景调整。第二部分初始化与监听模式设置void setup() { Serial.begin(115200); Serial.println(\n[] ESP8266 WiFi People Counter Starting...); // 将WiFi设置为混杂模式监听模式 wifi_set_opmode(STATION_MODE); wifi_promiscuous_enable(0); // 先禁用 WiFi.disconnect(); // 设置嗅探回调函数 wifi_set_promiscuous_rx_cb(sniffer_callback); wifi_promiscuous_enable(1); // 启用混杂模式 Serial.println([] Sniffer started. Listening for probe requests...); }wifi_set_opmode(STATION_MODE)和WiFi.disconnect()确保ESP8266不连接任何网络专心做监听。wifi_set_promiscuous_rx_cb是核心它注册了一个回调函数。此后每收到一个WiFi数据包这个回调函数都会被自动调用。第三部分数据包嗅探与解析核心中的核心void sniffer_callback(uint8_t *buf, uint16_t len) { // 确保数据包足够长包含MAC头 if (len 28) return; // 解析数据包类型字节12的低4位 uint8_t typeSubtype buf[12]; uint8_t type (typeSubtype 2) 0x03; // 帧类型 uint8_t subtype (typeSubtype 4) 0x0F; // 帧子类型 // 只处理“管理帧”Type0中的“探测请求”Subtype4 if (type ! 0 || subtype ! 4) return; // 提取源MAC地址位于数据包偏移22字节处 char macAddr[18]; snprintf(macAddr, sizeof(macAddr), %02X:%02X:%02X:%02X:%02X:%02X, buf[22], buf[23], buf[24], buf[25], buf[26], buf[27]); // 更新或添加设备到列表 updateDeviceList(String(macAddr)); }这段代码是项目的引擎。它像流水线上的质检员快速判断每个数据包是不是我们想要的“Probe Request”。buf[12]这个位置存放着帧的控制信息我们通过位运算提取出类型和子类型。MAC地址在数据包中有固定的位置这里是第22到27字节我们将其格式化成常见的“XX:XX:XX:XX:XX:XX”形式。重要提示数据包结构是基于IEEE 802.11标准的。不同的库或底层驱动偏移量可能微有差异。如果收不到设备可以尝试将偏移量22微调如试一下26。这是调试过程中可能遇到的第一个坑。第四部分设备列表管理与去重逻辑void updateDeviceList(String mac) { unsigned long currentTime millis(); // 1. 查找设备是否已在列表中 for (auto dev : deviceList) { if (dev.mac mac) { dev.lastSeen currentTime; // 更新最后出现时间 return; // 已存在直接返回 } } // 2. 如果不在列表中且列表未满则添加为新设备 if (deviceList.size() MAX_DEVICES) { Device newDev; newDev.mac mac; newDev.lastSeen currentTime; deviceList.push_back(newDev); Serial.printf([] New device detected: %s. Total: %d\n, mac.c_str(), deviceList.size()); } else { Serial.println([!] Device list full!); } } void cleanupDeviceList() { unsigned long currentTime millis(); // 使用迭代器安全地移除超时设备 for (auto it deviceList.begin(); it ! deviceList.end(); ) { if (currentTime - it-lastSeen DEVICE_TIMEOUT) { Serial.printf([-] Device removed (timeout): %s\n, it-mac.c_str()); it deviceList.erase(it); // erase返回下一个有效迭代器 } else { it; } } }updateDeviceList实现了去重和添加。cleanupDeviceList则定期清理“僵尸”设备。这里使用millis()记录时间避免了delay()带来的阻塞问题让系统能持续响应。注意链表遍历中删除元素的操作it deviceList.erase(it)是标准的安全写法。第五部分主循环与信道跳变void loop() { unsigned long currentMillis millis(); // 定期清理超时设备 cleanupDeviceList(); // 可选信道跳变以捕获更多设备有些设备可能固定在某些信道发送探测请求 if (currentMillis - lastChannelHop 500) { // 每500ms跳一次信道 lastChannelHop currentMillis; currentChannel (currentChannel % 13) 1; // 在1-13信道间循环 wifi_set_channel(currentChannel); } // 这里可以添加数据上报、显示刷新等逻辑 // 例如每5秒通过串口输出一次当前人数 static unsigned long lastReport 0; if (currentMillis - lastReport 5000) { lastReport currentMillis; Serial.printf([*] Current estimated people count: %d\n, deviceList.size()); } delay(SCAN_INTERVAL); }信道跳变是一个高级技巧。WiFi有多个信道设备可能在特定信道上发送探测请求。循环跳变能提高捕获概率。但跳变太快也可能错过数据包500ms是一个经验值。主循环的核心就是维持“监听-清理-报告”这个流程。4. 功能扩展与数据可视化基础计数功能实现后我们可以让数据“活”起来这里介绍两种最实用的扩展方向。4.1 本地OLED显示屏输出添加一个I2C接口的OLED屏可以实时显示人数无需连接电脑查看串口。接线非常简单ESP8266的D1接屏的SCLD2接SDA再连接VCC和GND。代码上需要引入Wire.h和Adafruit_SSD1306.h库。在setup()中初始化屏幕然后在主循环中定期刷新显示。// 在loop()的报告部分替换串口输出为屏幕显示 display.clearDisplay(); display.setCursor(0,0); display.printf(People: %d, deviceList.size()); display.display();实测下来0.96寸的OLED屏显示效果非常清晰适合放在前台或入口处做一个迷你看板。4.2 通过MQTT上传数据至Home Assistant或服务器这是将项目融入智能家居或进行远程监控的关键。MQTT是一种轻量级的物联网消息协议。搭建MQTT Broker可以使用本地的Mosquitto或者云服务如EMQX Cloud。安装PubSubClient库在Arduino库管理中搜索并安装。修改代码#include PubSubClient.h WiFiClient espClient; PubSubClient client(espClient); // 配置MQTT服务器地址、端口、用户名、密码 const char* mqtt_server your_broker_ip; ... void reconnectMQTT() { while (!client.connected()) { if (client.connect(ESP8266PeopleCounter)) { client.publish(home/peoplecounter/status, online); } else { delay(5000); } } } // 在loop()中维护连接并定时发布数据 if (!client.connected()) { reconnectMQTT(); } client.loop(); if (currentMillis - lastMqttReport 10000) { // 每10秒上报一次 char msg[50]; sprintf(msg, %d, deviceList.size()); client.publish(home/peoplecounter/count, msg); lastMqttReport currentMillis; }在Home Assistant中集成在configuration.yaml中添加一个MQTT传感器订阅home/peoplecounter/count主题就可以在HA仪表盘上看到实时人数并设置自动化如人数大于5自动打开客厅主灯。这个扩展让项目从一个独立玩具变成了智能家居系统的一个感知节点价值大大提升。5. 部署优化与实战避坑指南5.1 天线与部署位置的选择ESP8266的信号强度直接影响探测范围。板载PCB天线通常足够覆盖一个标准房间约50-80平米。如果想覆盖更大区域可以考虑外接一根2.4GHz全向天线。部署位置至关重要最佳高度1.5米至2.5米。这个高度避开了大部分地面障碍也与人们携带手机的高度大致相当。避开干扰源远离微波炉、蓝牙音箱、大型金属物体等强烈的2.4GHz干扰源。中心 vs 入口如果统计整个区域人数尽量放在区域中心。如果只统计进出可以放在门口但要注意调整DEVICE_TIMEOUT避免人站在门口时被反复计数。实测技巧部署后用自己的手机开关飞行模式观察串口日志看设备是否能被稳定捕获和移除。这是最直接的验证方法。5.2 参数调优与准确性提升默认参数不一定适合所有场景需要微调DEVICE_TIMEOUT设备超时时间快餐店/展厅人流快建议30-45秒。人停留时间短需要快速移除。办公室/图书馆人流慢建议90-120秒。避免人坐着不动一会儿就被认为离开。调试方法先设一个较长时间如120秒观察一个人从进入、静止到离开的完整日志根据“离开”信号的延迟来调整。RSSI过滤信号强度过滤 在sniffer_callback函数中可以获取数据包的RSSI值信号强度指示。通过只处理RSSI大于某个阈值如-70dBm的数据包可以限定探测范围避免统计到隔壁房间或路过的人。int rssi buf[1] - 256; // 一种常见的RSSI计算方式 if (rssi -70) return; // 忽略信号过弱的设备这个技巧能显著提升在密集环境如公寓楼下的统计准确性。MAC地址随机化带来的挑战 现代操作系统iOS 8, Android 6为了隐私在发送探测请求时会使用随机的MAC地址。这意味着同一台手机在不同时间或不同地点其探测请求中的MAC地址是变化的。这是本项目最大的误差来源。应对策略接受趋势而非绝对数这是最重要的心态调整。项目依然能有效反映“人变多了”、“人变少了”、“高峰期在几点”等趋势信息。关注设备OUIMAC地址的前6位是厂商标识OUI。虽然地址随机化但OUI通常不变还是苹果、三星等。可以粗略按OUI分类但无法精确到台数。结合其他传感器对于需要更高精度的场景可以结合被动式红外PIR传感器做辅助判断。5.3 常见问题排查速查表问题现象可能原因排查步骤与解决方案串口无任何输出/不启动接线错误、电源不足、板子型号选错1. 检查USB线是否可传输数据。2. 尝试换一个USB口或电源。3. 在Arduino IDE中确认开发板型号和端口选择正确。能启动但检测不到任何设备WiFi未进入监听模式、信道不对、代码偏移量错误1. 检查setup()中监听模式设置代码。2. 关闭手机4G确保WiFi开启。3. 尝试注释掉信道跳变固定信道为1、6、11试试。4. 调整sniffer_callback中MAC地址的偏移量尝试22或26。计数数字剧烈跳动或虚高未做去重、DEVICE_TIMEOUT太短、探测到大量无关设备1. 检查updateDeviceList中的去重逻辑。2. 增大DEVICE_TIMEOUT。3. 增加RSSI过滤屏蔽远处弱信号设备。设备离开后很久才从计数中消失DEVICE_TIMEOUT设置过长根据场景调小DEVICE_TIMEOUT参数。MQTT无法连接网络问题、Broker地址/密码错误、防火墙1. 确保ESP8266能连接WiFi。2. 检查MQTT Broker地址、端口、用户名、密码。3. 检查Broker服务是否运行防火墙是否放行1883端口。OLED屏不显示I2C地址不对、接线错误、库未安装1. 用I2C扫描程序确认OLED屏的地址通常是0x3C或0x3D。2. 检查SDA、SCL是否接反。3. 确认已安装Adafruit SSD1306和GFX库。5.4 电源管理与长期运行如果打算7x24小时运行电源稳定性很重要不要依赖电脑USB口使用独立的5V/1A USB电源适配器。注意发热ESP8266在持续工作时会微热确保部署环境通风避免密闭空间。看门狗与异常重启可以在代码中加入软件看门狗或者利用ESP8266的硬件看门狗防止程序跑飞。ESP.wdtDisable(); // 谨慎使用仅用于调试 // 在主循环中定期喂狗 ESP.wdtFeed();日志与监控将计数数据定期上报到服务器不仅可以可视化还能在数据长时间不变时触发警报提示设备可能离线或异常。走完整个流程从原理理解、环境搭建、代码编写、功能扩展到最后的部署调优你应该已经拥有了一个完全属于自己的、低成本的WiFi人流计数器。它可能不是百分百精确但其提供的趋势数据对于很多非商业的洞察需求已经绰绰有余。更重要的是这个过程让你深入理解了无线网络通信的一个侧面以及如何将简单的硬件转化为一个有用的数据感知终端。我自己的那个计数器已经在小工作室里默默运行了半年多帮我优化了好几次会议室的预约安排这就是动手创造的乐趣所在。

最新新闻

日新闻

周新闻

月新闻