注册
web

浅析小程序蓝牙技术

认识蓝牙



蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。



传统蓝牙和低功耗蓝牙


根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT)低功耗蓝牙模块(BLE)。传统蓝牙模块常用在对数据传输带宽有一定要求的场景上。低功耗蓝牙是从蓝牙4.0起支持的协议,特点是耗电极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中。


技术指标经典蓝牙BT低功耗蓝牙BLE
无线电频率2.4GHz2.4GHz
距离10米最大100米
发送数据所需时间100ms<3ms
响应延时约100ms6ms
安全性64/128-bit及用户自定义的应用层128-bitAES及用户自定义的应用层
能耗100%(ref)1%-50%
空中传输数据速率1-3Mb/s1Mb/s
主要用途手机,游戏机,耳机,音箱,汽车和PC等鼠标,键盘,手表,体育健身,医疗保健,智能穿戴设备,汽车,家用电子等
适用场景较高数据量传输、对传输带宽有要求续航要求较高、数据量小

蓝牙技术目前已经发展到5.0+版本,为现阶段最高级的蓝牙协议标准。BLE技术更契合新时代物联网的需求:更快、更省、更远、更便捷,也是我们小程序开发者在物联网项目最常用的技术。


蓝牙通信概述


低功耗蓝牙协议给设备定义了若干角色,其中最主要的角色是:外围设备(Peripheral)中心设备(Central)。




  • 外围设备:用来提供数据,通过不停地向外广播数据,让中心设备发现自己。




  • 中心设备:扫描外围设备,发现有外围设备存在后,可以与之建立连接,之后就可以使用外围设备提供的服务(Service)。




在两个蓝牙设备建立连接之后,双方的数据交互是基于一个叫做 GATT (Generic Attribute Profile,通用属性配置文件) 的规范,根据该规范可以定义出一个配置文件(Profile),描述该蓝牙设备提供的服务(Service)。


在整个通信过程中,有三个最主要的概念:配置文件(Profile)服务(Service)特征(Characteristic)


Characteristic:在 GATT 规范中最小的逻辑数据单元。实际上,在与蓝牙设备打交道,主要就是通过读写 Characteristic 的 value 完成。Characteristic 是通过一个 16bit 或 128bit 的 UUID 唯一标识。


Service:可以理解为蓝牙设备提供的服务,一个蓝牙设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个 Service 又包含多个 Characteristic 特性值,比如电量信息服务就会有个 Characteristic 表示电量数据。同时也有一个 16bit 或 128bit 的 UUID 唯一标识该服务。


Profile:并不真实存在于蓝牙设备中,它只是被蓝牙标准预先定义的一些 Service 的集合。如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。


Desciptor: 描述符是描述特征值的已定义属性。例如,Desciptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Desciptor 由一个 UUID 唯一标识。


总结:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,根据蓝牙设备的协议,用对应的 Characteristic 进行读写,即可达到与其通信的目的。


蓝牙开发实践


蓝牙通信过程介绍


a28336a8bd0944c495e2e03c8fc2ef18~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image

整体上看,蓝牙通信的开发主要分为三部分:



  1. 蓝牙资源和状态管理:包括蓝牙生命周期管理、蓝牙状态管理(开关、适配器、设备连接、数据接收等)、错误异常处理。
  2. 搜寻外围设备并建立连接:包括搜寻设备、监听设备发现、处理获取到的设备信息、连接/断开设备等。
  3. 读写数据:包括寻找目标服务和特征值、订阅特征值、监听并接收设备数据、分包处理数据等。

蓝牙数据读写


在小程序蓝牙开发联调中,推荐使用TLV协议对数据进行封包,TLV协议(Tag、Length、Value)是常见的一种面向物联网的通讯协议,对于不同的传输场景,甚至演变出混合型、指针型、循环型等不同类型的格式。


比如,在实践中往往只需要最简单的L-TLV格式,以下使用十六进制(Hex)表示:



  • 数据包总长(L)
  • 数据的类型Tag/Type(T)
  • Value的长度Length(L)
  • 数据的值Value(V)

[0x07, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01]
[数据总长,typelength,value,typelength,value]

举例


假设业务规定各字段type如下


字段名称type字段类型备注
account0x00String账号
Password0x01String密码

想要向设备传输一条写入account的指令,value为ABC。


ABC 通过 UTF-8 编码转 Hex String 分别是0x41、0x42、0x43。


那么数据包总长6字节,type是0,value总长3字节。


字符集编码


实际业务场景中,如果需要传输中文字符,则需要通过协商好的字符集进行转换。


常见字符集有:ASCII字符集、GB2312字符集、GBK字符集、 GB18030字符集、Unicode字符集等。


字符集描述
ASCII美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言
GB2312中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
GBK汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
GB18030中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
Unicode每个国家都像中国一样,把自己的语言进行编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 Unicode ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。
UTF-8、 UTF-16Unicode 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。

比如小写字母a,ASCII编码对应的Hex值是0x61,而GB2312字符集编码对应的Hex值是253631


将文本字符串转换为Hex字符串的时候,不同的字符集编码对应的Hex值不一样,所以小程序与蓝牙设备应当使用同一套字符集编码。推荐统一使用Unicode的UTF-8标准。


以下是字符转换示例:


// 中文转UTF-8
encodeURI('好').replace(/%/g, ''); // 'E5A5BD'

// UTF-8转中文
hex2String('E5A5BD'); // '好'

/**
* * read UTF-8
* @param { number[] } arr
* @returns {string}
*/

const readUTF = (arr: number [] ) => {
let UTF = '';
const _arr = arr;
for (let i = 0; i < _arr.length; i++) {
// 10进制转2进制
const one = _arr[i].toString(2);
const v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
const bytesLength = v[0].length;
let store = _arr[i].toString(2).slice(7 - bytesLength);
for (let st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
// 二进制序列转charCode,再拼接
UTF += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
UTF += String.fromCharCode(_arr[i]);
}
}
return UTF;
};

/**
* * transfer hex to string
* @param { string } str
* @returns {string}
*/

const hex2String = (hex: string) => {
const buf = [];
// 转10进制数组
for (let i = 0; i < hex.length; i += 2) {
buf.push(parseInt(hex.substring(i, i + 2), 16));
}

return readUTF(buf);
};

蓝牙分包


但是实际场景往往不是传输几个字母这么简单。虽然小程序不会对写入数据包大小做限制,但与蓝牙设备传输数据时,数据量超过 MTU (最大传输单元) 容易导致系统错误,所以要主动对数据进行分片传输。


参考各小程序开放平台文档:


开放平台文档描述
微信小程序在与蓝牙设备传输数据时,需要注意 MTU(最大传输单元)。如果数据量超过 MTU 会导致错误,建议根据蓝牙设备协议进行分片传输。Android设备可以调用 wx.setBLEMTU 进行 MTU 协商。在 MTU 未知的情况下,建议使用 20 字节为单位传输。
飞书小程序蓝牙设备特征值对应的值,为 16 进制字符串,限制在 20 字节内
支付宝小程序写入特征值需要使用 16 进制的字符串,并限制在 20 字节内。
Taro小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。

分包的过程,需要用到 ArrayBuffer



ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。


ArrayBuffer 是对固定长度的连续内存空间的引用。



在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。


ArrayBuffer 只是一个内存区域,里面存储着一些原始的字节序列,它和普通的Array完全不是一个概念,它的长度是固定的,无法增加或减少,也无法直接用buffer[index]进行访问。


要想写入值、遍历它或者访问单个字节,需要使用视图(View) 进行操作,以下为一些常用的视图:


Uint8Array :将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。称为 “8 位无符号整数”。


Uint16Array:将每 2 个字节视为一个 0 到 65535 之间的整数。称为 “16 位无符号整数”。


所有这些视图(Uint8Array,Uint32Array 等)的通用术语是 TypedArray(类型化数组)。它们都享有同一组方法和属性,类似于常规数组,具有索引,并且是可迭代的。


实际上,不同平台的小程序API定义的数据接口,都多少会用到ArrayBuffer


9687639dfba94854ba24b59c8e6869bd~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image


微信小程序-写入特征值


4314c98f75e14e7993a398c4294773e0~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image


飞书小程序-获取设备信息


但也不排除有些操作,开发平台已经帮忙处理了


2f444063536e4eef90399baf00b5587b~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image


飞书小程序-写入特征值


因此学习并使用 ArrayBuffer,可以:




  1. 方便操作分包,方便读取设备返回的数据、向设备写入数据。




  2. 在不同小程序平台灵活处理,更好地兼容




回到主题,蓝牙分包的思路是:


Text String --> Hex String --> ArrayBuffer(分包)


举个例子,上文中想要向设备传输一条写入password的指令,value为bytedance123456789ABC


[数据总长,type,length,value] MTU为20字节
[0x14, 0x01, 0x11, 0x62, 0x79, 0x74, ...] 第一个包 bytedance12345678
[0x07, 0x01, 0x04, 0x39, 0x41, 0x42, ...] 第二个包 9ABC

设备端会将多个相同type的包的值追加,而不是覆盖。


如何与设备端协商分包交互机制?



  1. 规定服务、特征值UUID,建议不同操作使用不同的UUID,读、写、订阅分开。
  2. 遵循TLV协议,双方协商好Type对应的字段类型和含义。
  3. 双方使用同一套字符编码集。
  4. 约定连在一起的两次(或多次)相同类型的设置,应该把它们的值追加连接,而不是覆盖
  5. 可约定在一次涉及业务逻辑的通信过程中,发送“开始”和“结束”的蓝牙包,告知设备处于这两个信号之间的蓝牙包为一次完整的通信数据流。
  6. 双方共同约定一个超时时间,若在此时间内由于各种原因未能完成读/写通信,则认为通信失败,小程序端必须给予用户友好提示。

问题排查手段


在开发过程中可能会遇到调用API失败、连接断开等问题



  1. 检查API调用顺序

小程序的蓝牙API使用起来比较简单,但是需要严格遵循一定的调用顺序(参考上文的流程图)。比如检查是否在开关蓝牙适配器之外进行操作,或者是否在特征值发生变化后才进行事件监听等



  1. 对比测试


  • 业务小程序、开放平台官方蓝牙demo 对比
  • 开放平台(非微信)官方蓝牙demo、微信官方demo 对比
  • 同厂商设备、同芯片、同蓝牙模组,多台设备对比
  • iOS、Android,蓝牙调试软件 与小程序的对比 (iOS:LightBlue,Android:BLE调试宝、nRF Connect)

经过以上对比测试,基本可以缩小问题范围,定位问题究竟是出在哪一方。但并不百分之百准确。




  1. 一些Tips:



    • 设备Server端在自定义特征值UUID时未遵循GATT的Attribute Structure,而蓝牙服务iOS的实现会比Android更严格。
    • 外围设备使用deviceId作为唯一标识,但iOS 和 Android在拿到的信息上有所差异。Android上获取到的deviceId为设备MAC地址,iOS上则是系统根据外围设备 MAC 地址及发现设备的时间生成的 UUID,因此deviceId不能硬编码。
    • 蓝牙模块比较耗费系统资源,做好生命周期管理必不可少,比如建立连接和断开连接应该成对出现,如果未能及时关闭连接释放资源,容易导致连接异常。另外,大多数蓝牙模组只支持单链路,最大连接数量为1,若未能及时断开连接,必然出现设备搜寻不到或连接不上的情况。



  2. 日志排查




作为小程序的开发者,很多疑难问题往往不能直观看出。如果你有对应的资源可以联系到开放平台的维护人员,即可拿到日志。我们项目组曾与飞书开放平台建立蓝牙专项问题解决渠道,结合开平和设备端同学捕获的日志,可以加快排查速度。


参考文章


http://www.bluetooth.com/learn-about…
http://www.cnblogs.com/chusiyong/p…
http://www.jianshu.com/p/62eb2f540…
zh.javascript.info/arraybuffer…


作者:HenryZheng
来源:juejin.cn/post/7221794170868351034

0 个评论

要回复文章请先登录注册