我Hack了一个蓝牙墨水屏,还挺好玩

少数派首发

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

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

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

现在居然还涨价了。。。
现在居然还涨价了。。。

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

然后。。。就悲剧了。

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

被我拆掉的尸体,组装回去还能用,但我发现我没法用树莓派去接线驱动
被我拆掉的尸体,组装回去还能用,但我发现我没法用树莓派去接线驱动

66块钱就这么没了!!

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

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

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

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

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

答案是肯定的。

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

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

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

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

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

识别度AAAA!!!
识别度AAAA!!!

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

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

为了信息发送的稳定性,开发者把这个屏幕的发送信息切成了一段一段的内容,分开发给蓝牙墨水屏屏幕,然后在发送结束后,再发送命令给墨水屏统一控制刷新一次,这样就完成了屏幕的刷新,接下来我详细分析一下我对这段协议的理解。

报文分析,找到协议规律
可以参考上边的报文内容,并结合当前的分析内容看。

1
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 + 1的长度,这个+1看下边说明

4c:校验位,防止当前这段报文在传输过程中有内容没收到或者传输错误,所以增加一个校验位,来保证当前报文是

ff:报文内容,这个下边再细说。

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

01-25 17:11:34.142 25455 25455 E write: total=1,13 1b 38 ff ……ff ff c8
可以看到,除了长度以外,报文的规则仍然符合我们上述的分析。

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

18026+56 = 4736 = 29616

已知的情况是:我们的墨水屏屏幕分辨率为296128,128 = 168

那缺失的这个系数8哪去了呢?

我们知道,ff是16进制数,

换算为10进制是255,

换算为2进制是11111111

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

事情开始明了了,

一个ff数据,二进制形式下是11111111,通过1和0来区分墨水屏的黑色和白色状态,进而可以控制八个墨水屏像素。

那我们的等式18026+56 = 4736 = 29616 * 8 = 296*128就正式成立了!!!

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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寸

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

1、我的Nas的两块存储池的使用情况

2、Nas的cpu、内存占用情况

3、树莓派的内存状态

4、树莓派的CPU状态

5、当前信息的刷新时间

不多,但是也够用了。

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

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

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

Nas是个好东西

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

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

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

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

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

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

毕竟我只是一名小前端嘛~