惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Last Week in AI
Last Week in AI
The Cloudflare Blog
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
S
SegmentFault 最新的问题
量子位
大猫的无限游戏
大猫的无限游戏
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
I
InfoQ
人人都是产品经理
人人都是产品经理
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Engineering at Meta
Engineering at Meta

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派
抓包解协议,代码造布局:电子墨水价签改造 NAS 监控屏小记 -...
2023-03-26 · via 少数派

我为什么要买墨水屏产品

事情的起因是这样的,一年前,社交媒体上就已经有很多技术宅在自己玩一些微雪的墨水屏了,看着他们又是焊板子又是 PCB 的真的是馋死我了。

但苦于自己是软件出身,没有一点硬件的底子,我就在想,能不能买一个现成的,然后自己再慢慢地去分析原理呢?

于是我就持巨资买了两块超市用的价签,一块 65,带支架,有 App 控制,可以批量添加。

image-zglb-dppx.png
现在居然还涨价了……理财产品实锤

然后我拆了其中一块想试下能不能用我树莓派的视频输出去控制它。

然后……就悲剧了。

我发现这玩意儿的排线跟树莓派根本就不兼容……

image-eosd-rxjp-nuvf.png
被我拆掉的尸体,组装回去还能用,但我发现我没法用树莓派去接线驱动他

66 块钱就这么没了!

还有另一块作为我的备件一直躺在箱子里待了半年多。

废物再利用,另辟蹊径

直到今年年初,我 NAS 的硬盘被我用到了 80%,然后迅雷还在卯足了劲儿在往硬盘里塞东西,我每天不得不多次打开我的管理界面去检测 NAS 当前还有多少余量可以用。

然后我就想到了它,墨水屏,这东西显示的时候不耗电,只有刷新的时候才会用一点点电,简直就是为监控状态而生的屏幕啊。

所以我就又下定了决心去想驱动它的办法。

上次用硬件驱动的路子走不通了,学习成本过高。我开始另辟蹊径,往软件驱动上靠:我能不能去找到控制它刷新的协议呢?

答案是肯定的。

这类墨水屏走的都是低功耗蓝牙控制的路子,手机蓝牙发送数据都是可以通过 logcat 这个工具进行抓包,说干就干。

这是 logcat 抓取的所有的手机运行日志:

image-fzlx-bmwp.png

显然有很多是我们不需要的信息,这严重干扰了我的分析工作,那就加一个过滤条件,只接受蓝牙相关的信息。

还是看不出什么内容,只是很不相关的日志输出:

image-yijd-rcmn-nrqm.png

只能根据时间节点一点点翻 logcat 日志了,此时我发现一个值得关注的信息,在蓝牙发送的那段时间的日志中,有一个非常规律的,也很像有点儿内容的写(write)命令:

image-qedh-eiwd-pzkk.png
识别度 AAAA

于是我就把这个关键字作为单独的日志过滤条件重新进行了一次抓包,事情突然就明朗了,这就是蓝牙墨水屏的手机 App 控制协议,而且协议的内容也非常容易理解:

image-jlrd-hial-pnub.png

竖向看,注意到那个 01 02 03 04 05 06 了嘛?

为了信息发送的稳定性,开发者把这个屏幕的发送信息切成了一段一段的内容,分开发给蓝牙墨水屏屏幕。在发送结束后,再发送命令给墨水屏统一控制刷新一次,这样就完成了屏幕的刷新。

接下来我详细分析一下我对这段协议的理解。

报文分析,找到协议规律

可以参考上边的报文内容,并结合当前的分析内容看。

01-25 17:11:24.844 25455 25455 E write: total=1,13 01 b4 ff ... ff ff 4c

这是蓝牙发送协议的内容部分,报文的起始内容是 13 01 b4 ff ... ff ff 4c,其中:

  • 13:命令字,标识当前的内容是显示内容,对应到墨水屏的显示控制部分的地址
  • 01:报文顺序,通过上边截图(竖着看),也能看出,开发者是把屏幕的内容切割成一段一段分开发给屏幕的
  • b4:16 进制数字,代表 180,数一下 b4 后边的报文也就是 180 的长度(最后还有一个较短的尾巴,看下边说明)
  • 4c:校验位,防止当前这段报文在传输过程中有内容没收到或者传输错误,所以增加一个校验位,来保证当前报文是完整的
  • ff:这以下是报文内容,下边细说。

这种报文一共出现了 26 次,第二十七次有一点不同:

01-25 17:11:34.142 25455 25455 E write: total=1,13 1b 38 ff ... ff ff c8

可以看到,除了长度(38,即十进制 56)以外,报文的规则仍然符合我们上述的分析。

那么我们现在就可以来总结一下这个命令字 0x13 的所有内容了:

  • 总长度为 180×26+56 = 4736(180 内容长度的报文重复了 26 次,又加上了一个 56 内容长度的尾巴)
  • 已知的情况是:我们的墨水屏屏幕分辨率为 296×128,是 4736 的 8 倍。那缺失的这个系数 8 哪去了呢?

我们知道,ff 是 16 进制数,换算为 10 进制是 255,换算为 2 进制是 11111111

11111111,这不正好是 8 个长度位吗?

事情开始明了了,每个 16 进制数含有 8 个二进制数,其值 10 分别对应墨水屏的黑色和白色状态。这样,每个 16 进制数就可以控制八个墨水屏像素。

那我们的等式就成立了:报文含有 4736 个 16 进制数,正好可以控制 8 倍于它的像素总数 296×128。

因为我买的是三色显示,所以除了显示黑白,这个屏幕还可以显示红色。红色报文的控制跟上述类似,只是命令字不同(从 0x13 变成了 0x12),就不再赘述。

新的问题,蓝牙连接

搞定了这块屏幕的控制协议,接下来就是怎么把协议发过去了。到了这一阶段就彻底没有可能通过报文分析,只能盲猜了。

我查了一些低功耗蓝牙的连接资料,同时一步一步地写代码来测试,最终还是摸索到了连接蓝牙并发送的方法了

const onCharacteristicFound = (_error, services, _characteristics) => {
      const characteristic = services[0].characteristics[0]
      characteristic.once('notify', (state) => { log('notify: ' + state) });
      characteristic.once('write', () => { }, (res) => {
                             log('write get response: ' + res)
      })
      setTimeout(() => {
            sendData(characteristic)
      }, 250)
}

const onDescover = (peripheral) => {
      log('searning...')
      if (peripheral.connectable === true && peripheral.state === 'disconnected') {
            if (peripheral.advertisement.localName === '你的蓝牙设备识别码') {
                  stopScanning()
                  currentDevice = peripheral
                  peripheral.once('connect', () => {
                        log('Connected !!! ')
                        peripheral.discoverSomeServicesAndCharacteristics(
                                    ['服务特征码1'], 
                                    ['服务特征码2'],
                                    onCharacteristicFound);
                  });
                  peripheral.once('disconnect', () => {
                        log('disconnected !!! ')
                  });
                  peripheral.connect()
            }
      }
}

以上代码就是搜索蓝牙、连接蓝牙、连接服务、发送数据的全部代码了,当然,完整的项目代码晚点我也会上传 GitHub,欢迎大家多多 Star,多多提意见。

最终效果

image-igpa-anwy-jvhg.png

屏幕其实挺小的,2.9 寸

在这块小屏幕上,分别展示了

  1. 我的 NAS 的两块存储池的使用情况
  2. NAS 的 CPU、内存占用情况
  3. 树莓派的内存状态
  4. 树莓派的 CPU 状态
  5. 当前信息的刷新时间

不多,但是也够用了。

其实在代码上我还有一些内容忘记聊了,关于显示内容的控制。

上边的信息看起来挺简单的,但把它换算成一个一个像素点其实还是蛮复杂的,需要分析字库字形、图形缩放等等,时间成本过高。

我突然想到了 Canvas。Canvas 就是一个可以定制显示画布像素的完美工具,而且在上边可以做几乎任何事情。说干就干,我把需要显示的内容通过 Canvas 绘制出来,然后再转换成一个一个的像素信息,然后再通过黑白红颜色的 RGB 值来决定最终发送的报文信息,搞定!

我把这个程序跑在我的树莓派上,然后 NAS(见我之前写过的文章)上设置了一个定时任务,每隔一段时间就推送信息到树莓派上,然后树莓派再通过蓝牙把信息发送给墨水屏。

image-jgup-bsod-tlyy.png

如果你也有兴趣使用,可以找一个树莓派这样的低功耗设备,克隆我的 GitHub 项目,然后运行:

yarn
node app.js

更多说明请参考项目的 Readme。

这感觉,才叫自动化啊 😄 😄 😄

总结

整个项目如果从买设备开始算跨度倒还挺大的,不过此次 Hack 的时间倒不长,前前后后也就一两周时间,而且都是晚上下班或者用周末时间来做的。

当然啦,如果是熟悉这个领域的大佬来搞,可能所用的时间更短,毕竟我还是一个小前端。哈哈哈。

希望我可以保持这种 Hack 精神,因为这真的是太有意思了。

希望大家也可以分享更多类似的经历,毕竟我知道自己在这个折腾的过程中很开心,相信大家知道了之后也会替我开心(并不),尤其是那些想动手还没动手的人,说不定已经跃跃欲试了。

Fun to life, fun to coding~