我为什么要买墨水屏产品
事情的起因是这样的,一年前,社交媒体上就已经有很多技术宅在自己玩一些微雪的墨水屏了,看着他们又是焊板子又是 PCB 的真的是馋死我了。
但苦于自己是软件出身,没有一点硬件的底子,我就在想,能不能买一个现成的,然后自己再慢慢地去分析原理呢?
于是我就持巨资买了两块超市用的价签,一块 65,带支架,有 App 控制,可以批量添加。
然后我拆了其中一块想试下能不能用我树莓派的视频输出去控制它。
然后……就悲剧了。
我发现这玩意儿的排线跟树莓派根本就不兼容……
66 块钱就这么没了!
还有另一块作为我的备件一直躺在箱子里待了半年多。
废物再利用,另辟蹊径
直到今年年初,我 NAS 的硬盘被我用到了 80%,然后迅雷还在卯足了劲儿在往硬盘里塞东西,我每天不得不多次打开我的管理界面去检测 NAS 当前还有多少余量可以用。
然后我就想到了它,墨水屏,这东西显示的时候不耗电,只有刷新的时候才会用一点点电,简直就是为监控状态而生的屏幕啊。
所以我就又下定了决心去想驱动它的办法。
上次用硬件驱动的路子走不通了,学习成本过高。我开始另辟蹊径,往软件驱动上靠:我能不能去找到控制它刷新的协议呢?
答案是肯定的。
这类墨水屏走的都是低功耗蓝牙控制的路子,手机蓝牙发送数据都是可以通过 logcat 这个工具进行抓包,说干就干。
这是 logcat 抓取的所有的手机运行日志:
显然有很多是我们不需要的信息,这严重干扰了我的分析工作,那就加一个过滤条件,只接受蓝牙相关的信息。
还是看不出什么内容,只是很不相关的日志输出:
只能根据时间节点一点点翻 logcat 日志了,此时我发现一个值得关注的信息,在蓝牙发送的那段时间的日志中,有一个非常规律的,也很像有点儿内容的写(write)命令:
于是我就把这个关键字作为单独的日志过滤条件重新进行了一次抓包,事情突然就明朗了,这就是蓝牙墨水屏的手机 App 控制协议,而且协议的内容也非常容易理解:
竖向看,注意到那个 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 个二进制数,其值 1
和 0
分别对应墨水屏的黑色和白色状态。这样,每个 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,多多提意见。
最终效果
屏幕其实挺小的,2.9 寸
在这块小屏幕上,分别展示了
- 我的 NAS 的两块存储池的使用情况
- NAS 的 CPU、内存占用情况
- 树莓派的内存状态
- 树莓派的 CPU 状态
- 当前信息的刷新时间
不多,但是也够用了。
其实在代码上我还有一些内容忘记聊了,关于显示内容的控制。
上边的信息看起来挺简单的,但把它换算成一个一个像素点其实还是蛮复杂的,需要分析字库字形、图形缩放等等,时间成本过高。
我突然想到了 Canvas。Canvas 就是一个可以定制显示画布像素的完美工具,而且在上边可以做几乎任何事情。说干就干,我把需要显示的内容通过 Canvas 绘制出来,然后再转换成一个一个的像素信息,然后再通过黑白红颜色的 RGB 值来决定最终发送的报文信息,搞定!
我把这个程序跑在我的树莓派上,然后 NAS(见我之前写过的文章)上设置了一个定时任务,每隔一段时间就推送信息到树莓派上,然后树莓派再通过蓝牙把信息发送给墨水屏。
如果你也有兴趣使用,可以找一个树莓派这样的低功耗设备,克隆我的 GitHub 项目,然后运行:
yarn
node app.js
更多说明请参考项目的 Readme。
这感觉,才叫自动化啊 😄 😄 😄
总结
整个项目如果从买设备开始算跨度倒还挺大的,不过此次 Hack 的时间倒不长,前前后后也就一两周时间,而且都是晚上下班或者用周末时间来做的。
当然啦,如果是熟悉这个领域的大佬来搞,可能所用的时间更短,毕竟我还是一个小前端。哈哈哈。
希望我可以保持这种 Hack 精神,因为这真的是太有意思了。
希望大家也可以分享更多类似的经历,毕竟我知道自己在这个折腾的过程中很开心,相信大家知道了之后也会替我开心(并不),尤其是那些想动手还没动手的人,说不定已经跃跃欲试了。
Fun to life, fun to coding~