大家好,我叫“WiFi探针系统”。别被这名字唬住,其实我就是四个指甲盖大小的ESP8266电路板,分别蹲在XX大学图书馆的四个角落里,干着一件特别“八卦”的事情——偷听手机们的悄悄话。
我知道你肯定会问:手机们能有什么悄悄话?嘿,多了去了。你每打开一次WiFi开关,你的手机就像个找不到家的孩子,嗷嗷喊着“有WiFi吗?有WiFi吗?”——这句喊话在802.11协议里有个正经名字,叫Probe Request帧。而我干的事情,就是把这些喊话记下来:谁喊的(MAC地址)、喊多大声(RSSI信号强度)、朝谁喊的(BSSID目标地址)。
就这么简单?对,就这么简单。但要把这点简单的事儿干利索,背后的故事可不少。
一、硬件篇:一个“穷鬼”的自我修养
先说说我的“身体”吧。四个ESP8266 ESP01S模块,单价不到15块钱。有人可能会撇嘴:这么便宜能靠谱吗?
说实话,确实不太靠谱。
首先,这玩意儿内存只有80KB。80KB是什么概念?你手机里随便一张照片都比它大几十倍。就这么点内存,我还要同时干两件事:一是开着混杂模式抓包——说白了就是把耳朵竖起来听所有经过的WiFi信号,不管是不是喊给我的;二是连上WiFi把数据传到服务器。
问题来了:ESP8266在混杂模式下没法连WiFi。这不是bug,是硬件层面的“生理缺陷”——就像你不能同时吃饭和唱歌一样。
怎么办?我们设计了一套“轮班制”:
采集30秒 → 关掉混杂模式 → 连上WiFi → 把攒的20条数据发出去 → 断开WiFi → 重新开混杂模式
整个过程像极了打工人的日常:埋头干活(采集)→ 抬头交差(发送)→ 再埋头干活。一个循环大概35秒,数据延迟就这么来的。没办法,穷有穷的活法。
二、数据篇:33个字节的“极限压缩”
攒下来的数据怎么传?我们没选JSON——那玩意儿虽然人类看得懂,但太啰嗦了。一条记录动不动上百字节,对于我这种每次只能连几分钟WiFi的“穷鬼”来说,太奢侈了。
我们设计了一个33字节的二进制协议。来,我给你拆开看看:
| 位置 | 内容 | 大小 | 说明 |
|---|---|---|---|
| 第0字节 | RSSI | 1字节 | 信号强度,负值,越大表示离得越近 |
| 第1-6字节 | MAC地址 | 6字节 | 设备的“身份证号” |
| 第7-12字节 | BSSID | 6字节 | 目标AP地址,全F表示设备在“广播式扫描” |
| 第13-16字节 | 时间戳 | 4字节 | Unix时间,精确到秒 |
| 第17字节 | 信道 | 1字节 | 当前WiFi信道(1-13) |
| 第20字节 | 估算距离 | 1字节 | 根据RSSI粗算的距离 |
| 第25字节 | 连接状态 | 1字节 | 0=扫描中,1=已连接 |
33个字节,把该记的都记了。你说这算不算“螺蛳壳里做道场”?
对了,这里有个特别有意思的细节——BSSID字段。如果BSSID是FF:FF:FF:FF:FF:FF,说明设备正在“广播式扫描”,也就是还没连上任何WiFi,像个无头苍蝇一样到处问“谁家有网”。如果BSSID是个具体的MAC地址,说明设备已经连上某个AP了,只是偶尔还发个探测帧刷存在感。
这个区别很重要。因为已连接设备用的往往是真实MAC地址,而那些还在扫描的设备,很可能用了随机MAC——这事儿我们后面再聊。
三、信道篇:一个“陀螺”的自我修养
WiFi有13个信道,手机可能在任何信道上喊话。我只有一双耳朵,一次只能听一个信道。怎么办?
轮巡。每500毫秒切一次信道,1→2→3→...→13→1→2→...像个不知疲倦的陀螺。
这方法笨,但有效。不过也有翻车的时候——高峰期某些信道流量特别大,500毫秒根本不够用,数据就丢了。后来我们改成了“流量自适应”:某个信道如果连续几次都抓不到东西,就少待会儿;反之就多待会儿。效果嘛,提升了大概5%的捕获率——折腾了好几天,就提了5%。写论文的时候我就在想:这事儿到底值不值得写?后来想通了,“做了但效果一般”也是经验,总比假装没做过强。
四、定位篇:加权质心的“算术魔法”
好,数据有了——四个节点分别报告了某个设备的RSSI值。接下来就是算位置了。
我们用了一个叫加权质心的算法。名字听着唬人,原理特别简单:
把每个节点的RSSI换算成“权重”,信号越强权重越大,然后拿四个节点的坐标做加权平均。
公式长这样:
P = Σ(w_i × P_i) / Σ(w_i)
其中w_i = 10^(RSSI_i / 10),就是把dBm换算成毫瓦。
说白了就是:谁的信号强,设备就大概率在谁附近。
这法子简单粗暴,但也有硬伤——RSSI太不靠谱了。你站在同一个地方不动,RSSI能上下波动10个dBm。为什么?因为人走来走去会遮挡信号,墙壁会反射信号形成多径效应,甚至空气湿度都能影响一点点。
所以我们从来不说“定位精度1米”这种大话。实测下来,开阔大厅平均误差1.8米,书架之间能干到3.5米,最差一次6米。结论是:找楼层没问题,找座位别指望。
为了对抗RSSI的“情绪波动”,我们还加了个滑动窗口平滑——把最近5次定位结果取平均。稳定性提升了40%,代价是延迟多了3秒。这买卖划算。
五、去重篇:MAC地址的“5秒记忆”
同一个设备可能每秒发好几个Probe Request,要是不去重,数据就爆炸了。
我们的去重策略很朴素:维护一个MAC地址缓存,5秒内重复出现的MAC直接忽略。
bool is_duplicate(uint8_t *mac) { for (int i = 0; i < CACHE_SIZE; i++) { if (memcmp(mac_cache[i].mac, mac, 6) == 0) { if (now - mac_cache[i].timestamp < 5000) { return true; // 5秒内重复,扔掉 } } } // 新MAC,记下来 }就这么几行代码,能把数据量砍掉70%以上。有时候解决问题的办法就是这么朴实无华。
六、随机化篇:当“身份证”变成了“马甲”
前面说过,MAC地址是设备的“身份证”。但自从Android 10和iOS 14开始,手机们在扫描WiFi时会随机生成MAC地址。也就是说,同一部手机,每隔5-10分钟就换一件“马甲”。
这对我们来说是降维打击。
本来想追踪“张三”在图书馆待了多久,结果“张三”待了6分钟变成了“李四”,“李四”又待了8分钟变成了“王五”。你还追个啥?
我们一开始很沮丧。后来想通了——我们到底要追“人”还是追“数”?
如果是追“数”——这层楼有多少设备——那随机化根本不是问题。不管马甲怎么换,总数就在那里。我们把每个随机MAC都当做一个独立的“观测样本”,统计总数和变化趋势。
放弃个体追踪,拥抱群体统计。这叫“因祸得福”——既规避了隐私风险,又简化了问题。
七、行为篇:占座与滞留的“硬规则”
有了位置数据,就能做点“聪明”的事情了。
占座检测的逻辑特别简单:如果某个设备在某片区域连续出现超过30分钟,而且RSSI几乎没怎么变(说明设备没怎么移动),就触发“占座疑似”告警。
闭馆滞留检测更简单:系统时间过了22:30,定位结果还在馆内的设备,统统告警。
这两个逻辑硬邦邦的,没有任何机器学习,没有任何深度学习,就是if-else。
你可能会说:这也太糙了吧?
确实糙。有一次系统在23:00告警“有人滞留”,管理员跑过去一看——保洁阿姨的手机搁窗台上充电呢。手机不认识“阿姨”,只认识“信号”,它就老老实实报告“有设备”。这事儿告诉我们:任何自动化系统都需要人工复核,别指望机器替你搞定一切。
八、总结:不完美的实用主义
回过头来看这套系统,毛病一大堆:
分时复用导致35秒延迟
RSSI抖动让定位精度飘忽不定
MAC随机化让个体追踪成了笑话
占座检测会把充电的手机当成“占座党”
但你猜怎么着?图书馆管理员老李用得很开心。
因为他不需要知道“张三在哪个座位”,他只需要知道“二楼现在人多不多,要不要加椅子”;他不需要精准到厘米,他只需要知道“闭馆了还有没有人没走”。
这就是我想说的:技术不是为了完美,是为了解决问题。用15块钱的东西、33字节的协议、加权平均的算法,去回答一个管理学的核心问题——“这层楼到底有多少人?”——这事儿,我觉得挺值的。
至于那些不完美的地方?留给下一版吧。毕竟,工程的真谛不是消灭所有bug,而是让bug都变得可控。
(本文技术内容基于XX大学图书馆4节点WiFi探针系统的实际部署经验,数据真实可溯。欢迎复现,更欢迎吐槽。)