一、蓝牙支持情况

1. 微信小程序对蓝牙的支持情况

目前普遍使用的蓝牙规格:经典蓝牙和蓝牙低功耗。

经典蓝牙(蓝牙基础率/增强数据率):常用在对数据传输带宽有一定要求的大数据量传输场景上,比如需要传输音频数据的蓝牙音箱、蓝牙耳机等;

蓝牙低功耗 (Bluetooth Low Energy, BLE): 从蓝牙 4.0 起支持的协议,特点就是功耗极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中,比如智能穿戴设备、智能家电、传感器等。

IOS 安卓

基础库

(当前开发基础库2.23+)

经典蓝牙 不支持 不支持,规划中 /
蓝牙低功耗 主机模式(手机作为客户端,主动连接) 微信客户端6.5.6及以上 微信客户端6.5.7及以上 1.1.0及以上
从机模式(手机作为服务端,被动连接) 支持 支持 2.10.3及以上
蓝牙信标(持续广播,但不建立连接) 支持 支持 1.2.0及以上

2. IOS和安卓设备对蓝牙低功耗的支持情况

由于项目所使用的设备是低功耗蓝牙,故对此做调研:

IOS

安卓

连接设备数量 20个 6-8个
连接速度 正常 部分安卓机经常出现连接速度慢、连接超时的现象
传输数据量(MTU)  20字节  20字节
设备搜索 支持 6.0及以上版本需要打开定位权限

注意点:1)数据量超过 MTU  (20 字节)会导致错误,需要根据蓝牙设备协议进行分片传输。其中安卓分片之间的传输需要做延迟 250ms。

2)由于Android 从微信 6.5.7 开始支持,iOS 从微信 6.5.6 开始支持,因此小程序中需要做好版本检测(wx.getSystemInfoSync 获取系统信息)。

二、基本需求

1. 添加页面:开启蓝牙搜索,选择设备并输入识别码(蓝牙连接后发送识别码,匹配成功则与设备正式建立了连接)。

2. 首页:可进行蓝牙连接、切换、断开、消息监听与数据发送(识别码匹配)。

三、蓝牙API的基本使用

整理上述涉及蓝牙API的使用:

1. 添加页面(搜索蓝牙逻辑)

// 添加页
// 检查蓝牙是否开启
checkBluetoothOn(){let sucCallback = this.startDiscoveringlet errCallback = () => {// 蓝牙未开启逻辑处理} // 如果蓝牙开启状态,就去搜索设备this.getBlueState(errCallback, sucCallback)
},
// 判断手机蓝牙是否打开
getBlueState (errCallback, sucCallback) {this.$getBlueState(errCallback).then(res=>{if(res.errno === 0){sucCallback ? sucCallback() : ''} else {}});
},
// 蓝牙搜索逻辑 15s关闭
startDiscovering(){// 开始搜寻附近设备this.$discoveryBlue(this.found)setTimeout(()=>{this.handleStopDiscovery()}, 15000)
},
// 找到新设备就触发该方法 处理数据逻辑
found(res) {var devices = res.devices;devices.map(async item => {// 对设备信息处理})
},
handleStopDiscovery(){let stopLoading = () => {this.loading = false// 其它关闭逻辑}this.$stopDiscoveryBlue(stopLoading, stopLoading)
},

2. 首页(连接逻辑)

onShow() {// 第一次进来如果没有连接 去自动连接let sucCallback = (this.blueDeviceList?.length && this.isAutoConnect) ? this.autoConnect : this.isConnnectedlet errCallback = () => {// 未开启逻辑}// 如果蓝牙开启状态 就去连接this.getBlueState(errCallback, sucCallback)this.isAutoConnect = false
},
methods: {async autoConnect(){this.isFound = falsethis.$discoveryBlue(this.found).catch((err) => {// 查找失败逻辑处理})// 15s后未找到数据setTimeout(()=>{if(!this.isFound){this.notFound()}}, 15000)},// 找到新设备就触发该方法 处理数据逻辑found(res) {var devices = res.devices;devices.map(async item => {// 以 deviceId 为唯一标识,过滤重复设备if(item.deviceId == this.connectingDevice.deviceId) {this.isFound = truethis.handleConnect(this.connectingDevice)wx.offBluetoothDeviceFound(this.found) // 防止回调函数重复执行导致重复连接}})},notFound(){let stopLoading = () => {// 其它逻辑}this.$stopDiscoveryBlue(stopLoading, stopLoading)},async handleConnect(deviceInfo){let { deviceId, idCode } = deviceInfothis.$connectBlue(deviceId).then(async (res)=>{const {notifyServiceId, writeServiceId, notifyCharacteristicId, writeCharacteristicId} = await this.getBasicIds(deviceId)let listenValueChange = () => {this.$listenCharacteristicValueChange(this.getInfoFromBluetooth)}this.$notifyBlue(deviceId, listenValueChange, notifyServiceId, notifyCharacteristicId).then(()=>{let { buffer } = this.$getBuffer({body: idCode, length: 11, // 帧长度command, // 绑定命令字})this.$sendBlue(deviceId, buffer, writeServiceId, writeCharacteristicId ).catch(async(err) => {await this.stopConnect(deviceInfo.deviceId)// 其它逻辑})}).catch(async()=>{await this.stopConnect(deviceInfo.deviceId)// 其它逻辑})}).catch(()=>{// 逻辑处理})},// 获取蓝牙传输过来的数据处理getInfoFromBluetooth(res){},stopConnect(deviceId){return new Promise((resolve) => {let connectedDevice = this.getStorageInfo('connectedDevice', {})let connectedDeviceId = deviceId || connectedDevice?.deviceId if(!connectedDeviceId) return resolve()this.$closeConnection(connectedDeviceId).then((res) => {// 逻辑处理resolve()}).catch((err)=>{// 断连失败逻辑处理})})},isConnnected(){const { connected } = this.connectStatuslet connectedDevice = this.blueDeviceList.find(item => item.status == connected)if(connectedDevice){this.$getServicesBlue(connectedDevice.deviceId).then(async()=>{// 如果连接状态,需要重新建立消息通道const {notifyServiceId, notifyCharacteristicId} = await this.getBasicIds(connectedDevice.deviceId)let listenValueChange = () => {this.$listenCharacteristicValueChange(this.getInfoFromBluetooth)}this.$notifyBlue(connectedDevice.deviceId, listenValueChange, notifyServiceId, notifyCharacteristicId).catch(async()=>{await this.stopConnect(deviceInfo.deviceId)// 其它逻辑})}).catch(err => {// 连接已断开if(err.errCode == 10006){// 其它逻辑}})}},getBasicIds(deviceId){return new Promise(async(resolve, reject) => {let {notifyServiceId, writeServiceId} = await this.$getServicesBlue(deviceId)let notifyCharacteristicId = await this.$getCharacteristicsBlue(deviceId, notifyServiceId)let writeCharacteristicId = await this.$getCharacteristicsBlue(deviceId, writeServiceId)resolve({notifyServiceId, writeServiceId, notifyCharacteristicId, writeCharacteristicId})})},
}

3. 公共方法

// 公共方法封装小程序蓝牙api
function $getBlueState(errCallback) {return new Promise((resolve, reject) => {$initBlue().then(res=>{resolve(res)}).catch(err=> {if(err.errCode === 10001){return errCallback ? errCallback() :}})})
}
function $initBlue() {return new Promise((resolve, reject) => {uni.openBluetoothAdapter({success(res) {resolve(res)},fail(err) {reject(err)}})})
}
function $discoveryBlue(callback) {return new Promise((resolve, reject) => {uni.startBluetoothDevicesDiscovery({services: mainServiceIds,allowDuplicatesKey: true,success(res) {uni.onBluetoothDeviceFound(callback)},fail(err) {console.error(err)reject(err)}})})
}
function $stopDiscoveryBlue(sucCallback, errCallback) {uni.stopBluetoothDevicesDiscovery({success(res) {console.log('停止设备搜索')sucCallback ? sucCallback() : ''},fail(err) {console.log('停止搜索设备失败')console.error(err)errCallback ? errCallback() : ''}})
}
function $getServicesBlue(deviceId) {return new Promise((resolve, reject) => {uni.getBLEDeviceServices({deviceId,success(res) {// 逻辑(根据硬件给的协议取对应服务ID)resolve({notifyServiceId,writeServiceId})},fail(err) {console.error(err)reject(err)}})})
}
function $getCharacteristicsBlue(deviceId, serviceId) {return new Promise((resolve, reject) => {uni.getBLEDeviceCharacteristics({deviceId,serviceId,success(res) {const characteristicId = res.characteristics[0].uuidresolve(characteristicId)},fail(err) {console.error(err)}})})
}
function $notifyBlue(deviceId, callback, serviceId, characteristicId) {return new Promise((resolve, reject) => {uni.notifyBLECharacteristicValueChange({state: true, // 启用 notify 功能deviceId, // 设备idserviceId: serviceId, // 监听指定的服务characteristicId: characteristicId, // 监听对应的特征值success(res) {callback()resolve()},fail(err) {console.log(serialDataChannel.serviceId, serialDataChannel.characteristicId)console.error(err)reject()}})})
}
function $sendBlue(deviceId, buffer, serviceId, characteristicId) {return new Promise((resolve, reject) => {uni.writeBLECharacteristicValue({deviceId,serviceId: serviceId,characteristicId: characteristicId,value: buffer,success(res) {resolve(res)},fail(err) {console.error(err)reject()}})})
}
function $listenCharacteristicValueChange(callback) {uni.onBLECharacteristicValueChange(res => {callback(res)})
}

四、问题记录

1. 手机开启了蓝牙,但是api openBluetoothAdapter 仍然调用失败?

检查是否给微信授权了蓝牙功能。

2. onBluetoothDeviceFound 搜索到设备以后需要建立连接。接口持续搜索会导致重复连接。

防止回调函数重复执行导致重复连接,需要调用 wx.offBluetoothDeviceFound(this.found) 。

3. 设备Id、特征值Id、服务Id是否是唯一?
设备Id:唯一。


特征值Id和服务Id不唯一:不同蓝牙的服务Id和特征值Id可能是一样的;同一蓝牙设备的服务Id和特征值Id是固定的。

4. 既然蓝牙的特征值Id和服务Id是固定的,那是否可以写死,直接调用读写api( notifyBLECharacteristicValueChange 和 writeBLECharacteristicValue)?

不能。api会调用失败。需要先调用 getBLEDeviceServices 和 getBLEDeviceCharacteristics 。

5. 蓝牙读写通信的数据如何转化?

通信过程涉及到的转化包括:10进制和16进制互相转化、16进制和 ArrayBuffer 互相转化。

1)10进制转16进制:

let deci = 172;
console.log(deci.toString(16))

2)16进制转10进制:

let hex = '0xAC'
console.log(parseInt(hex, 16))

3)16进制转字符串:

let hexToString = (hex) => {let str = '';for (let i = 0; i < hex.length; i += 2) {let v = parseInt(hex.substr(i, 2), 16);if (v) str += String.fromCharCode(v);}return str;
}
hexToString('68656c6c6f')

4)字符串转16进制:

let stringToHex = (str) => {let val= "";for(let i = 0; i < str.length; i++){if(val == "")val = str.charCodeAt(i).toString(16);elseval += "," + str.charCodeAt(i).toString(16);}return val;
}
stringToHex('hello')

5)字符串转16进制转 ArrayBuffer:

let info = 'hello'
const buffer = new ArrayBuffer(info.length)
const dataView = new DataView(buffer)
for (var i = 0; i < info.length; i++) {dataView.setUint8(i, info.charAt(i).charCodeAt())
}
wx.writeBLECharacteristicValue({deviceId,serviceId,characteristicId,value: buffer,success (res) {console.log('writeBLECharacteristicValue success', res)}
})

6)ArrayBuffer 转 16进制:

function ab2hex(buffer) {let hexArr = Array.prototype.map.call(new Uint8Array(buffer),function(bit) {return ('00' + bit.toString(16)).slice(-2)})return hexArr.join('');
}
wx.onBLECharacteristicValueChange((res) => {console.log(ab2hex(res.value))
})

6. 蓝牙帧发送出现分包的情况?

有时蓝牙设备传来的帧会有不完整的情况,需要做拼接处理。此处逻辑为:在监听函数中,获取到不完整的帧时,如果帧头正确,保存并等下一帧,否则舍弃。帧头正确并获取到下一帧后进行拼接。当获取到的帧符合我们期待的长度时,进行之后的帧校验与业务逻辑处理。如果指定时间没有接受到有效帧数据,则断连。

7. 超时间没有获取到接收有效帧数据的断连逻辑?

本项目的协议上规定,状态帧每30s上传一次。如果接收到有效状态帧,则更新状态,并对关闭连接进行延迟;如果没有获取到有效状态帧,则2min后会断开连接。具体如下:

// 有效帧获取到后执行
this.delayStopConnect(this.connectedDevice.deviceId)// 对 delayStopConnect 进行 debounce,状态帧超过2分钟没有回复则断连
delayStopConnect: utils.debounce(async function(deviceId){await this.stopConnect(deviceId)uni.hideLoading();this.showModalTips('设备连接异常')
}, 120000),// debounce 函数
function debounce(fn, delay, isImmediate) {var timer = null;  //初始化timer,作为计时清除依据return function() {var context = this;  //获取函数所在作用域thisvar args = arguments;  //取得传入参数clearTimeout(timer);if(isImmediate && timer === null) {//时间间隔外立即执行fn.apply(context,args);timer = 0;return;}timer = setTimeout(function() {fn.apply(context,args);timer = null;}, delay);}
}

8. 如何保证帧的有效性(帧校验)?

一般来说,硬件会在协议上说明。一般是拿到回复帧后,取其中某几段进行和校验。如果校验得到的值和回复帧的值相同,则校验成功。例如:一个回复帧包含帧头+帧长度+命令字+识别码+校验码+帧尾。校验码等于命令字与识别码的和。假设命令字是 0xAB,识别码是 0x01,则正确的校验码是 0xAC 。把它跟蓝牙传来的回复帧的校验码进行比较即可。

9. 保证一次连接一个设备的逻辑处理?

由于当前项目的需求是,一个手机只能连接一个蓝牙设备(小程序做处理),但是实际手机是支持连接多个蓝牙设备的,所以如果用户一次性点了很多个设备,需要做相关处理。我的思路是创建一个数组堆,记录连接的设备。如果连接上则 push(),如果发现数组长度大于1,则 shift() 掉最先连接的。

10. 小程序能否主动监听到蓝牙开启与关闭?
可以通过 onBLEConnectionStateChange 监听到蓝牙断开;蓝牙开启监听不到,除非手动定时请求api判断(如果官方有相关监听api,欢迎指正)。

11. 切换页面,能否持续收到监听数据,是否要重新建立监听?
在页面A建立消息通道(notifyBLECharacteristicValueChange)以后,跳转到页面B,仍然可以监听到数据(不论是 navigateTo 还是 navigateBack)。但如果其它页面此时再次调用此api,会覆盖掉之前的消息通道。

12. 设备A连接后,断开连接。设备B立刻连接,连接不上,需要过1分钟左右(安卓机尤其明显)?
因为程序做了连接超时则请求失败的处理。经排查发现,连接超时的api是设备搜索(startBluetoothDevicesDiscovery)和设备连接(getConnectedBluetoothDevices)。
一个不完美,但管用的解决方法:这两个 api 不设置 services 参数。

13. 关闭再次打开小程序,蓝牙连接是否会中断?
理论上是,实际上有时不会。
首先,2023.23.3 官方回复目前暂时不支持后台下的蓝牙功能(https://developers.weixin.qq.com/community/develop/doc/0008a409c889c0f8d76fd1ec356400?highLine=%25E8%2593%259D%25E7%2589%2599%25E5%2590%258E%25E5%258F%25B0%25E5%259C%25BA%25E6%2599%25AF)
其次,小程序的运行机制是(https://developers.weixin.qq.com/miniprogram/dev/framework/runtime/operating-mechanism.html):
小程序切换后台(包括息屏)5s,微信会停止小程序js线程执行,在小程序再次进入前台时事件和接口回调会触发;小程序切换后台30分钟,小程序销毁。
二者结合,理论上蓝牙应该在30分钟后被断开。但事实上,发现有时候ios和安卓都没有断开。因为其它手机搜索不到相应设备。

14. 一些 ios 和安卓的 API 兼容问题

1)关于断开连接(closeBLEConnection):ios 设备在没有连接蓝牙设备时,调用断开接口,显示调用成功;但是安卓机会得到错误码 10006 (当前设备已断开连接)。

2)关于搜索设备(startBluetoothDevicesDiscovery):安卓机搜索一次以后,再次调用该接口,刚才已经搜索出来的设备搜索不到了,除非加上参数:allowDuplicatesKey。ios 加不加这个参数都可以正常搜索到设备。

微信小程序蓝牙功能开发与问题记录相关推荐

  1. 微信小程序蓝牙BLE开发——关于进制转换(四)

    微信小程序蓝牙BLE开发--进制转换 这段时间开发共享设备,对接蓝牙BLE设备通信协议,过程中用到一些进制转换, 记录下方便使用. 有些参考大神们,感谢分享. 文章目录 微信小程序蓝牙BLE开发--进 ...

  2. 微信小程序蓝牙BLE开发实战——案例(二)

    微信小程序蓝牙BLE开发实战(二) 上篇主要介绍在开发过程中应用到相关API操作.接下来介绍个人在项目开发中应用蓝牙BLE一些事情. 由于时间比较仓促, 有些注释没那么详细.请理解~写的不好欢迎各位大 ...

  3. SpringBoot对接微信小程序支付功能开发(一,下单功能)

    1,接入前准备: 接入模式选择直连模式: 申请小程序,得到APPID,并开通微信支付: 申请微信商户号,得到mchid,并绑定APPID: 配置商户API key,下载并配置商户证书,根据微信官方文档 ...

  4. SpringBoot对接微信小程序支付功能开发(二,支付回调功能)

    接着上一篇: SpringBoot对接微信小程序支付功能开发(一,下单功能) 在上一篇下单功能中我们有传支付结果回调地址. 下面是回调接口实现 package com.office.miniapp.c ...

  5. 微信小程序分享功能开发及调试方法

    首先说一下使用方法如下,Button组件设置open-type="share"即可触发onShareAppMessage完成分享功能 <button class=" ...

  6. 上拉加载更多后台数据_6-7【微信小程序全栈开发课程】记录页面(七)--分页加载记录数据...

    现在是一次性加载所有的记录数据,数据多的时候,会加载比较慢,所以我们改成分页加载,一次最多加载15条数据 每次拉倒底部都会自动加载下一页的数据,知道所有的数据加载完成 1.添加data变量 编辑rec ...

  7. 微信小程序蓝牙BLE开发——写入一串16进制数据,低字节在前(五)

    微信小程序BLE发送一串16进制数据,低字节在前 文章目录 微信小程序BLE发送一串16进制数据,低字节在前 场景 获取UTC时间 写入数据 向设备发送16进制数据 字节转换 验证发送UTC时间 场景 ...

  8. 微信小程序蓝牙功能全套开发流程介绍

    一.开发流程 流程图:流程图作者原文章 实现模块顺序 1.1初始化蓝牙模块(打开蓝牙适配器) 初次加载,自动获取获取系统信息,检查蓝牙适配器是否可用 初始化蓝牙,提示打开GPS和蓝牙,开始自动搜索蓝牙 ...

  9. wx.getBLEDeviceCharacteristics 微信小程序蓝牙 微信小程序热敏打印机

    1 微信小程序蓝牙功能开发概述 第一步 判断当前微信版本,是否支持蓝牙 通信 第二步 打开蓝牙 第三步扫描设备 第四步连接设备 第五步 获取服务与特征值 看是否支持读写数据操作 第六步 发送数据 本文 ...

最新文章

  1. 一文详解LOAM-SLAM原理深度解析
  2. 在linux CentOS 上安装chrome 谷歌浏览器
  3. shanghai road map
  4. Linux C 中连接操作符##
  5. TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析
  6. 一种求凸多边形内部似最大圆的算法
  7. 2018java多线程面试题_2018年最全Java面试通关秘籍汇总集!
  8. 用自己的语言解释一段简单的汇编
  9. Python实践通过使用XGBoost中的尽早停止【Early Stopping】策略来避免过度拟合
  10. 四叶草clover配置工具Clover Configurator 5 Ma汉化版
  11. Android 测试点归纳总结
  12. 上海展盟网络科技有限公司的 gamebox 组件注入进程导致软件崩溃
  13. php中根据数字月份返回月份的英文缩写
  14. 宾州州立计算机科学世界排名,宾州州立大学帕克分校qs世界排名
  15. 计划的主体部分应有哪些内容_计划练习题
  16. linux pthread_cleanup_push 线程实现,线程清理(pthread_cleanup_push函数和pthread_cleanup_pop函数)...
  17. 奇迹 与服务器连接中断,奇迹少女连接服务器失败 连接不上网络怎么办
  18. 关于贯彻《财政部 税务总局关于调整增值税税率的通知》的通知 〔2018〕405
  19. 数据结构-赫夫曼树(三)
  20. AppCan开发框架

热门文章

  1. 机器学习_深度学习毕设题目汇总——皮肤_癌症_糖尿病
  2. 关于智能共享出行,政界、学界和业界的专家都说了什么? | SMC 2018
  3. 自动提取word文本,并保存到excel
  4. seo排名工具_网站排名优化常用工具
  5. 使用Notepad++将windows格式转为linux 的unix
  6. 我看你骨骼惊奇,是块做CTO的材料! 我看还是不要了
  7. ffmpeg 过滤器libavfilter的使用
  8. Auto Layout界面自动布局系列2-使用Xcode的Interface Builder添加布局约束
  9. 如何做毕业论文的ppt、准备答辩?
  10. 6本最值得观看的,企业知识管理书籍清单出炉,附带工具推荐!