前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

小程序蓝牙开发实践 BLE 连接流程与踩坑记录

首页2019-08-31 16:50:32Front-End
小程序蓝牙BLE

第一次接硬件的时候我以为跟调接口差不多,拿到设备 ID、连上、发数据、收回复,四步搞定。真写起来才知道差得远。安卓上跑得好好的流程,换到 iPhone 上搜不到设备;开完 notify 立刻发消息,回调一次都不进;设备 ID 在安卓上是固定的 MAC 地址,在 iOS 上却每次都变。这些坑都不在文档的显眼位置,得自己撞一遍。这篇把小程序蓝牙从基础概念到完整连接流程整理了一遍,包括那 18 个 API 各自管什么、双端差异怎么处理、哪几个地方必须加延时,以及两个可以直接拿去改的完整例子。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 蓝牙、BLE、GATT 这几个词分别指什么,写代码前必须先分清
  • deviceId、serviceId、characteristicId 三层结构是怎么套起来的
  • 小程序蓝牙 18 个 API 的分工,哪些是动作,哪些是监听
  • 从开适配器到收发数据的完整九步流程,每一步的返回值怎么用
  • iOS 拿不到 MAC 地址时,靠什么来认设备
  • 两处必须加 setTimeout 的地方,不加就是必现失败
  • ArrayBuffer 和十六进制字符串怎么互转,为什么必须转
  • 两个完整可跑的示例,以及一份踩坑清单

# 一、先把蓝牙这几个概念分清

蓝牙是爱立信公司创立的一种无线技术标准,为短距离的硬件设备提供低成本的通信规范。蓝牙规范由蓝牙技术联盟(Bluetooth Special Interest Group,简称 SIG)管理,在计算机、手机、传真机、耳机、汽车、家用电器等很多场景广泛使用。蓝牙有这么几个特点:

  • 免费使用:工作频段在 2.4GHz 的工科医(ISM)频段,无需申请许可证
  • 功耗低:BLE4.0 包含了一个低功耗标准(Bluetooth Low Energy),可以让蓝牙的功耗显著降低
  • 安全性高:蓝牙规范提供了一套安全加密机制和授权机制,可以有效防范数据被窃取
  • 传输率高:BLE4.0 版本理论传输速率可达 3Mbit/s(实际肯定达不到),理论覆盖范围可达 100 米

第四条要补充一句。蓝牙 4.0 这个规范其实包含两条并行的技术线,经典蓝牙那条(BR/EDR)速率高,3Mbit/s 说的是它;低功耗那条(LE)物理层速率低得多,换来的是省电。小程序里操作的是低功耗这条线,所以 API 名字里全都带着 BLE。真实的吞吐能力跟这个理论值差着一个量级,做产品设计的时候按小数据包来规划,别指望拿蓝牙传文件。

搞清楚这个区分很重要,因为它直接决定了你的协议要怎么设计。

# 二、小程序蓝牙 API 总览

# 2.1 几个必须先了解的术语

小程序 API 提供了一套蓝牙操作接口,作为前端开发人员可以更方便地进行蓝牙设备开发,而无需了解安卓和 iOS 的各种蓝牙底层概念。小程序的蓝牙操作大多都是通过异步调用来处理的,这里面就存在着一些坑,后面会详细介绍。

在使用小程序蓝牙 API 之前,有几个术语需要预先了解:

  • 蓝牙终端:我们常说的硬件设备,包括手机、电脑等等
  • UUID:由字母和数字组成的标识串,跟硬件设备关联的唯一 ID
  • 设备地址:每个蓝牙设备都有一个设备地址 deviceId,但是安卓和 iOS 差别很大。安卓下设备地址就是 MAC 地址,但是 iOS 无法获取 MAC 地址,所以设备地址是针对本机范围有效的 UUID,这里需要注意
  • 设备服务列表:每个设备都存在一些服务列表,可以跟不同的设备进行通信,服务有一个 serviceId 来维护,每个服务包含了一组特征值
  • 服务特征值:包含一个单独的 value 值和 0 到 n 个用来描述 characteristic 值(value)的 descriptors。一个 characteristic 可以被认为是一种类型的,类似于一个类
  • ArrayBuffer:小程序中对蓝牙数据的传递是使用 ArrayBuffer 的二进制类型来的,所以在我们的使用过程中需要进行转码

关于 UUID 那条,原文写的是「40 个字符串的序号」,这个数字不太准确,这里顺手改一下。UUID 是 128 位,标准的字符串表示是 32 个十六进制字符加 4 个连字符,一共 36 个字符。不过小程序里你还会碰到 16 位的短 UUID,比如 0000FFE0-0000-1000-8000-00805F9B34FB 这种蓝牙 SIG 规定的标准服务,日常写代码时按拿到的原值用就行,别自己拼。

设备、服务、特征值三者是层层嵌套的关系,这张图把它们的包含关系画出来了:

蓝牙设备、服务与特征值的层级关系示意图

理解这张图是写好蓝牙代码的前提。一台设备下面挂多个服务,一个服务下面挂多个特征值,真正能读写的只有最底层的特征值。所以从连上设备到能发数据,中间必须走完「找服务」和「找特征值」两步,每一步都是异步的。

这也是为什么蓝牙代码天生就是一串回调套回调。

# 2.2 18 个 API 各管什么

小程序对蓝牙设备的操作有 18 个 API:

API名称 说明
openBluetoothAdapter 初始化蓝牙适配器,在此可用判断蓝牙是否可用
closeBluetoothAdapter 关闭蓝牙连接,释放资源
getBluetoothAdapterState 获取蓝牙适配器状态,如果蓝牙未开或不可用,这里可用检测到
onBluetoothAdapterStateChange 蓝牙适配器状态发生变化事件,这里可用监控蓝牙的关闭和打开动作
startBluetoothDevicesDiscovery 开始搜索设备,蓝牙初始化成功后就可以搜索设备
stopBluetoothDevicesDiscovery 当找到目标设备以后需要停止搜索,因为搜索设备是比较消耗资源的操作
getBluetoothDevices 获取已经搜索到的设备列表
onBluetoothDeviceFound 当搜索到一个设备时的事件,在此可用过滤目标设备
getConnectedBluetoothDevices 获取已连接的设备
createBLEConnection 创建BLE连接
closeBLEConnection 关闭BLE连接
getBLEDeviceServices 获取设备的服务列表,每个蓝牙设备都有一些服务
getBLEDeviceCharacteristics 获取蓝牙设备某个服务的特征值列表
readBLECharacteristicValue 读取低功耗蓝牙设备的特征值的二进制数据值
writeBLECharacteristicValue 向蓝牙设备写入数据
notifyBLECharacteristicValueChange 开启蓝牙设备notify提醒功能,只有开启这个功能才能接受到蓝牙推送的数据
onBLEConnectionStateChange 监听蓝牙设备错误事件,包括异常断开等等
onBLECharacteristicValueChange 监听蓝牙推送的数据,也就是notify数据

这 18 个 API 可以按名字前缀分成三类,分清楚之后就好记多了。

on 开头的是监听,注册一次之后被动等回调,它们不是「执行一次拿结果」的那种。get 开头的是查询,一次调用返回一次快照。剩下的是动作,开关适配器、开关搜索、连接断连、读写数据。

另外注意 API 名字里带不带 BLE。带 BLE 的是针对已连接的某台低功耗设备做操作,不带的是针对本机适配器或者搜索行为。搞混这一层,你会发现自己在没连设备的时候调了 getBLEDeviceServices,然后收到一个看不懂的错误码。

# 三、完整流程走一遍

蓝牙通信的一个正常流程是下面的图示:

小程序蓝牙从初始化到收发数据的完整流程图

这九步是有严格先后顺序的,前一步的返回值就是后一步的入参,跳步就会失败。下面逐步拆。

# 3.1 开启蓝牙与检查状态

调用 openBluetoothAdapter 来开启和初始化蓝牙,这时候可以根据状态判断用户设备是否支持蓝牙。接着调用 getBluetoothAdapterState 来检查蓝牙是否开启,没有开启就在这里提醒用户开启,并且能在开启后自动启动下面的步骤。

这里有一个坑:iOS 里面蓝牙状态变化以后不能马上开始搜索,否则会搜索不到设备,必须要等待 2 秒以上。

function connect(){
  wx.openBluetoothAdapter({
    success: function (res) {
    },
    fail(res){
    },
    complete(res){
      wx.onBluetoothAdapterStateChange(function(res) {
        if(res.available){
          setTimeout(function(){
            connect();
          },2000);
        }
      })
   //开始搜索  
    }
  })
}
@前端进阶之旅: 代码已经复制到剪贴板

这段代码的思路是:不管 openBluetoothAdapter 成功还是失败,都先把状态监听挂上。用户当时没开蓝牙,openBluetoothAdapter 会失败;等他去系统设置里打开了,onBluetoothAdapterStateChange 就会触发,res.available 变成 true,延时 2 秒之后重新走一遍 connect。

那 2 秒是必须的。系统上报「蓝牙可用」和蓝牙协议栈真正准备好之间有一段空档,iOS 上尤其明显。这个空档里发起搜索,回调一次都不进,也不报错,就是安安静静地什么都搜不到。排查这种没有报错的问题最费时间,因为你会先怀疑自己的过滤逻辑写错了。

不加延时不是「可能失败」,是「必现失败」。

# 3.2 搜索设备,以及双端最大的差异

startBluetoothDevicesDiscovery 开始搜索设备,当发现一个设备会触发 onBluetoothDeviceFound 事件。先看下标准 API:

onBluetoothDeviceFound 回调返回的设备对象字段说明

由于 iOS 无法获取 MAC 地址,所以这里需要区分两个场景。

安卓下可以根据 MAC 地址来搜索设备,或者跳过此步直接连接到设备。当搜索到一个设备以后,可以在 onBluetoothDeviceFound 事件回调中判断当前设备的 deviceId 是否为指定的 MAC 地址:

let mac = "XXXXXXXXXXXXXXX";
wx.startBluetoothDevicesDiscovery({
  services:[],
  success(res) {
    wx.onBluetoothDeviceFound(res=>{
        let devices = res.devices;
        for(let i = 0;i<devices.length;i++){
          if(devices[i].deviceId === mac){
            console.log("find");
            wx.stopBluetoothDevicesDiscovery({
              success:res=>console.log(res),
              fail:res=>console.log(res),
            })
          }
        }
    });

  },
  fail(res){
      console.log(res);
  }
})
@前端进阶之旅: 代码已经复制到剪贴板

这里改了原文的一处代码错误。原文写的是 if(devices[i].deviceId = mac),一个等号,那是赋值不是比较,结果就是每次判断都返回真值,第一个搜到的设备就会被当成目标。这类错误编译不报、eslint 不开也不提示,跑起来的现象是「随便连上一台设备然后一直失败」,非常难查。改成了 ===。

iOS 下获取设备 MAC 地址的方法已经被屏蔽,所以不存在 MAC 地址,此时只能通过其他方式来判断,比如在蓝牙设备 advertisData 字段添加一些特别的信息来判断,可以转字符串来判断,也可以直接用二进制来判断:

fe
  • 一、先把蓝牙这几个概念分清
  • 二、小程序蓝牙 API 总览
    • 2.1 几个必须先了解的术语
    • 2.2 18 个 API 各管什么
  • 三、完整流程走一遍
    • 3.1 开启蓝牙与检查状态
    • 3.2 搜索设备,以及双端最大的差异
    • 3.3 连接设备与获取服务列表
    • 3.4 获取服务特征值
    • 3.5 开启 notify 与收发数据
  • 四、两个完整例子
    • 4.1 例子一:把流程封装成一个对象
    • 4.2 例子二:带设备列表的页面
  • 五、踩坑清单
    • 5.1 等待响应
    • 5.2 MAC 和 UUID
    • 5.3 iOS 下只有搜索可以省略
    • 5.4 监听到的消息要进行过滤处理
    • 5.5 操作完成后要及时关闭连接
  • 六、ArrayBuffer 和二进制数据
  • 七、设备唯一标识怎么选
  • 八、GATT 术语速查
  • 九、这篇写于 2019 年,有几处需要更新
  • 总结
  • 参考

← Taro中使用ECharts小结 小程序与H5双端图表组件封装React 设计模式和最佳实践总结,从组件拆分到 Hooks →