▍我为什么要买墨水屏产品
事情的起因是这样的,一年前,社交媒体上就已经有很多技术宅在自己玩一些微雪的墨水屏了,看着他们又是焊板子又是 PCB 的真的是馋死我了。但苦于自己是软件出身,没有一点硬件的底子,我就在想,能不能买一个现成的,然后自己再慢慢地去分析原理呢?于是我就持巨资买了两块超市用的价签,一块 65,带支架,有 App 控制,可以批量添加。然后我拆了其中一块想试下能不能用我树莓派的视频输出去控制它。然后……就悲剧了。我发现这玩意儿的排线跟树莓派根本就不兼容……被我拆掉的尸体,组装回去还能用,但我发现我没法用树莓派去接线驱动他65 块钱就这么没了!另一块则作为我的备件一直躺在箱子里待了半年多。直到今年年初,我 NAS 的硬盘被我用到了 80%,然后迅雷还在卯足了劲儿在往硬盘里塞东西,我每天不得不多次打开我的管理界面去检测 NAS 当前还有多少余量可以用。然后我就想到了它:墨水屏,这东西显示的时候不耗电,只有刷新的时候才会用一点点电,简直就是为监控状态而生的屏幕啊。于是,我又下定了决心去想驱动它的办法。上次用硬件驱动的路子走不通了,学习成本过高。我开始另辟蹊径,往软件驱动上靠:我能不能去找到控制它刷新的协议呢?这类墨水屏走的都是低功耗蓝牙控制的路子,手机蓝牙发送数据都是可以通过 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:校验位,防止当前这段报文在传输过程中有内容没收到或者传输错误,所以增加一个校验位,来保证当前报文是完整的;这种报文一共出现了 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。这不正好是 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,多多提意见。