6月的时候, 我的高中朋友群里一群人想撮合一个自习室, 暑假一起自习. 他们中大多是二战的, 也有备战雅思的, 我虽然已经保研了, 但是我也想着如果位置够的话, 我也去凑个热闹. 因为之前听科科说他之前暑假跟朋友们一起租的房子当自习室, 一个月一个人才两百大洋, 我觉得挺划算.
一切的开始
后来, xsh说他们家正好有闲置的房子, 不需要用来改善家庭收入, 可以拿来当自习室, 就是交通不太方便. 我去地图上查了一下, 发现我之前去过这个地方, 离我们家还很近, 当时也是跟xsh去找他们家不需要跑滴滴的闲置私家车去的. 其他人也都有自己的电动车, 通勤也很方便. 在我们这对电动车没啥特别严格要求的小地方, 一个小电驴就能让你全市畅行无阻. 所以这事就这么定了.
根据我的经验, 五六个人需要一起做事的时候, 就一定需要一个人来统筹规划一下. 又根据我跟他们相处七年的经验, 不会有别人出来当这个人, 只有我. 所以我做了一个统计表, 统计了一下一共有多少人愿意去, 每个人什么时候开始去, 每天什么时候去之类的, 用于后面安排打扫卫生和置办桌子.
一共五个人填了表, 还有两个没填表但是说要来, 所以一共是七个人. 那个房子一共有两个屋子有空调, 如果只是自习的话应该是可以安排得下的. 并且按照我对他们的经验, 同一时间能有四五个人一起出现在屋子里就已经很了不起了.
徐家大院最大的不方便是周围没有什么公交车, 在我们这主城区之外的东边一点点. 刚好我家就在主城区的最东边, 走过去大概两三公里的样子. 如果要是坐车的话只能坐一站路, 并且去公交站和下了公交站再走走估计也得快两公里, 于是我每天就步行通勤了.
根据安排, 七月初我们组织了一次打扫卫生. 当时一共去了五六个的样子. 房间也不是很脏. 有空调的两个屋子分别是主卧和次卧, 次卧的床不太好搬走, 也不需要搬. 主卧的床垫搬到了客厅, 床架立了起来靠墙放, 就已经有很大的空间了. 次卧有一个砌在墙上的小桌子, 客厅有一个最多坐3个人学习的餐桌, 一共能坐四个人. 所以又安排了三个长期呆在这的, 不是暑假结束就走的人花了几十块钱买了个小桌子搬过来, 这样7个人就都有位置坐了. 事实证明其实是不需要三个人都买桌子的, 因为从开始到现在七个人从来没有到齐过.
主卧放了一个餐桌, 又放了两个小桌子, 能坐五个. 侧卧放了一个小桌子和一个本来就有的小桌子, 能坐两个. 本来我的安排是, 我因为有时候需要开会, 就和还在备战雅思, 需要练口语的gry坐一个屋子. 他们五个考研的就坐在主卧. 后来因为他们五个人总有几个人不来, 为了省空调费我也就坐了主卧, 次卧只有谁要说话的时候才过去.
打扫完卫生后, 我们还举行了隆重的开业仪式. 先用小爱同学奏了国歌, 然后在xsh翻出来的一个蹦迪灯的灯光下放了很多喜庆的音乐, 比如春节序曲, 云宫迅音等.
之后, 我和xsh开始研究网络布局. 他们家是一个标准的光猫入户结构, 墙上的所有网线口都是直连光猫. 但是, 光猫的无线信号太弱了, 而且光猫是有客户端数目限制的, 所以我们想用一个自己带的路由器当dhcp服务器. 并且我还带了一个nuc当服务器, 需要插在墙上的网线口. gry从家里带了一个ax6000, 此时最好的方法应该是让路由器来拨号, 并且墙上的网口都从路由器接出来. 但是当我意识到这一点的时候, 从主卧搬出来的床垫已经死死的挡住了弱电箱, 我们也懒得搬了, 就用光猫拨号, 自带路由器当一个二级路由. 除了我的小服务器, 其他的都无线连到路由器上. 我在小服务器上跑了adguardhome, 发现这样的网络结构还起到了保护隐私的作用, 在adguardhome的log里面只能看到是路由器发出的dns请求, 看不到具体是谁发的.
起因—电梯卡
徐家大院在一个一共有20层的高层里, 是16楼, 坐电梯需要刷卡, 刷了卡之后只能按自己的楼层. 然而小区的物业只给一个屋最多发三个电梯卡, 一个电梯卡要30大洋, 并且一年还要换一次.
刚开始, 我们只能靠qq电话, 先进到电梯里, 打电话让已经在屋子里的人给我们按一下. 但是大家都是来学习的, 这样一会按一次一会按一次肯定是不能忍受的, 并且有的人学习的时候还会开勿扰, 打电话也找不到.
过了两三天, gry说他家里有一个nfc写卡器, 高中的时候尝试破hbyz饭卡的时候买的, 虽然当时没成功, 但是用来复制一个电梯卡还是绰绰有余的. 借ltc的安卓机一用, 确定了这个卡就是普通的Mifare卡之后, 就在网上买了十张CUID卡准备开干.
我看某宝上很多可以定制CUID卡的, 就那种可以在卡上印图案, 还有镂空卡号的那种, 我想着我也整个徐家大院定制卡. 但是问了一圈才知道10张没人愿意给做定制的, 唯一一个愿意定制十张的出价150大洋, 还是算了吧.
之前没接触过这一块, 刚开始连基本的概念都不懂, 也不知道该去某宝上买什么卡. 后来恶补了一下相关知识才算是有了头绪. 操作确实也不难, 遇到的唯一困难就是github上clone下来的libnfc的代码, 自己编译后不太好用, 找了半天找到了一个人的patch, 打上了之后重新编译才好用.
mfoc一下子就破开了电梯卡的密钥, 读了电梯卡之后把内容写进买来的CUID卡就行. 不能定制也得想个办法做个标记, 我就在门口便利店买了个记号笔, 让每个人都在卡上写点东西(真·写卡).
写完卡之后去电梯里测试一下发现能用, 电梯卡这事本应该结束了.
xsh说, 他记得所有的电梯卡好像都能刷开最高楼, 能去天台背书, 但是试了一下不行, 是他记错了. 但是这事让我想起来我的卡现在还只能刷16层. 正好我刚了解这些卡, 对这一块还比较好奇, 就想试试能不能看看电梯是怎么知道你能去第几层的.
看了一遍读出来的卡的内容, 发现了两个奇异的字节:
我灵机一动, 这是不是就是第1层和第16层的意思? 于是我把他改成了FF FF, 试了一下, 果然16层以下的都能按了.
收到强烈正反馈的我对这玩意更有兴趣了, 于是过了两天我就构思了一个东西.
打卡机
徐家大院开张前一天晚上, 考虑到只有两个人有钥匙和电梯卡, 我就在群里发了个接龙, 方便没有钥匙和电梯卡的人看什么时候能过去. 但是一些人觉得没必要, 也不配合接龙, 并且第一天没发生什么意外, 我也不想麻烦他们, 第一天晚上就没发接龙.
于是第二天上午, 没有电梯卡没有钥匙, 又累又热还拎着一桶2L的怡宝的我因为没有人在徐家大院被困在了一楼, 还是在我提前一个多小时就在群里说我要去的情况下. 两个有钥匙的人都觉得对方会在, 并且都在群里屁都不放一个.
我气急败坏, 在群里扣了十几分钟字, 在楼下等了一小时后终于等来了个有钥匙的人, 把我放进了徐家大院. 这时候我就在想能不能做一个能知道徐家大院到底有没有人的东西.
xsh的计划是让所有人手机关掉mac地址伪装, 记录每个人手机的mac地址, 进屋后手机连上wifi就可以知道他来了. 本来我觉得这个计划还可以, 后来经过了研究电梯卡之后, 我又有了个更好的想法.
电梯卡是一个Mifare 1K卡, 一共有16个扇区, 但是只有第1和第12个扇区是有内容的. 我想着只要电梯不在刷卡的时候做一个校验, 我就可以利用剩下14个扇区记录很多东西. 顺着这个思路, 我就想我可以往卡里写个id, 然后就可以利用读卡器做一个打卡机? 这样正好我还有个小nuc在这当服务器.
我往第二个扇区第一个字节里写了个99, 拿出去尝试了一下, 还是能坐电梯, 证明电梯不会校验其他扇区的内容. 我找了几个libnfc的示例程序, 我就觉得我可以了.
我和xsh起初构想的是用pushdeer来推送消息, 因为tx现在封qq机器人封的厉害, 我们也不想花费太大的精力每天去维护qq机器人. xsh就在我小服务器上开了个mysql的docker, 里面记录着所有人的卡的id, pushdeer的token, 以及姓名, qq号和手机号.
后来发现安卓机上pushdeer推送不是很好用, 就换成了企业微信, 用企业微信的机器人api来推送通知. 之后我因为操作失误把小服务器的系统升级到了debian 13, 还是测试版, 一堆莫名其妙的问题, 我就把系统重装回了12, 但是忘了备份数据库. 因为不用pushdeer了, qq号和手机号我也想不到有啥用处, 我重建数据库的时候就去掉了这几项.
之前没怎么用c++写过东西, 用c++仅限于打acm写垃圾代码, 所以这些东西我就准备用c++写, 也学习一下modern c++和cmake之类的东西. 后来发现c++确实比rust, go之流麻烦太多, 就目前而言我用c++的开发效率是远不如rust的, 更别提跟go比了.
因为nfc读卡器有驱动, 我就没敢用docker部署, 直接用tmux部署大法跑在服务器上了. 本来连接数据库用的是oracle的mysql-connector-cpp库, 后来一直遇到莫名其妙的小问题, 怀疑是库不太阳间, 就换成了sqlpp库, 看文档的时候还水了一个fix typo的pr, 成为了GitHub 2k+ star项目的contributor.
现在还是感觉不太好用, 有时候时间一长会遇到特殊情况崩溃, 然后我就加个了大try-catch. 有时候过了一两天之后响应速度会巨慢, 这两天调整了一下进程的优先级, 不知道有没有用. 刚开始打卡也没有什么声音提示, 只能靠企业微信里的消息来提示. 后来我去网上找了一个全家的那个进门的声音(就那个噔噔噔噔噔噔 噔噔噔噔噔)当上班打卡的提示音, gry找了一个番茄花园windows xp的关机声音当下班打卡的声音.
门铃
电梯和打卡的需求解决了, 下一个需求就是开门. 因为很多人学习的时候手机是没声音的, 打电话也不接, 就需要一个电子门铃. 本来我想的是网页上一个按钮就行, 点一下屋子里的音箱就放一个声音. 然后xsh说他来写, 第二天发现他上线了一个类似对讲机的东西, 手机上说话音箱那边就可以实时放. 他说他把他之前写的一个对讲机拿过来直接用了. 虽然感觉没必要, 可能还不如点一下放一个声音来的好用一点, 但是这也不是不行. 而且后来发现这个还挺好玩的, 比如突然被外面的不知道谁放了一首Returns.
这个对讲机是用python写的, 因为小服务器没有公网ip, xsh就用了一个🇯🇵的azure当的中转. azure上跑了一个服务端, 然后写了个前端当客户端, 客户端录音, 把pcm格式的声音通过websocket发出去. 客户端和播放端都用websocket主动连接服务端, 服务端只负责把ws流量从客户端转发到播放端就行. 播放端收到pcm声音数据后播放就行. 但是因为做的比较粗糙, 当有多个客户端连接的时候, 服务端会把这些流量一起转发到播放端, 播放端收到的数据就乱了, 放出的声音也是时分复用的.
后来我把服务端放在了我国内的服务器上, 效果好了点, 但是不多. 不知道是不是python的问题, 感觉性能还是很烂, 无论是延迟还是声音的流畅度上.
第二版xsh用kotlin写了一个, 效果好了一点, 但是还是没解决多个客户端的问题. 而且服务端在转发声音的时候, cpu占用能飙到20%. 这也成为了被我这个java黑子利用的可攻击的点.
其实在kotlin之前, xsh尝试用c++写了一个, 但是好像多线程同步的时候写挫了, 一直有问题. 后来修了这个问题部署上去了, 转发流量的时候几乎没有cpu占用.
之后就用着那个单线程的c++服务端一段时间, 播放端这边因为感觉没什么性能瓶颈, 就一直用的python实现. 我也用react重写了个更好看点的前端.
音频服务器
之后我觉得中转这个方案完全没必要, 因为我和xsh之前已经用wireguard把网络基础设施建好了, wiregurad层负责流量转发就行. 然后我也一直想写个支持多说话人的, 不仅仅是说话人, 之后所有的应用, 比如打卡的时候播放一个音效, 也可以通过这个接口简单的播放一个声音. 我就决定重写一下这个东西.
重构之后的结构只有客户端和服务端, 客户端的流量到我的服务器后通过wiregurad会直接转发到我在徐家大院的小服务器上. 服务端也直接监听ws请求, 对于每一个ws请求单独开一个线程和声音流. 本来我以为想要同时播放几个声音需要自己混响来着, 还准备调一波ffmpeg, 后来想到电脑上多个应用程序也可以同时播放声音, 好像这个所谓混响根本不是我这一层需要考虑的问题.
我也是第一次接触这种音视频流相关的开发, 遇到的最大问题就是延迟和播放卡顿的问题. 如果什么策略都不加, 缓冲队列会越来越长, 延迟会越来越大, 而且增长的速度很快, 用个不到一分钟延迟就已经大的很离谱了. 然后我想到可以稍微调高一点播放端的采样率, 这样消耗缓冲队列的速度会快一点, 但是这样会出现队列长度总是为0, 播放一卡一卡的. 然后我们用固定延迟的策略, 强制播放端等待个1秒钟, 让缓冲队列先堆积起来, 这样效果就好了很多, 虽然不是什么完美的解决方法.
虽然最后打卡机并没有使用这个接口, 但是至少多说话人的一个“门铃”现在是很好用的. 对于没用c++写过什么像模像样的东西的我也算是一个很好的练手项目了, 以后写什么正经东西我还是用rust和go吧.
暂时就这些了, 我最近也没什么特别好的创意, 也不知道还可以写点什么东西, 今天xsh说我打卡程序有内存泄漏, 过两天修一下吧.