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

这篇具有很好参考价值的文章主要介绍了微信小程序蓝牙功能开发与问题记录。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、蓝牙支持情况

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.startDiscovering
    let 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.isConnnected
    let errCallback = () => {
        // 未开启逻辑
    }
    // 如果蓝牙开启状态 就去连接
    this.getBlueState(errCallback, sucCallback)
    this.isAutoConnect = false
},
methods: {
    async autoConnect(){
        this.isFound = false
        this.$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 = true
                this.handleConnect(this.connectingDevice)
                wx.offBluetoothDeviceFound(this.found) // 防止回调函数重复执行导致重复连接
            }
        })
    },
    notFound(){
        let stopLoading = () => {
            // 其它逻辑
        }
        this.$stopDiscoveryBlue(stopLoading, stopLoading)
    },
    async handleConnect(deviceInfo){
        let { deviceId, idCode } = deviceInfo
        this.$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.connectStatus
        let 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].uuid
                resolve(characteristicId)
            },
            fail(err) {
                console.error(err)
            }
        })
    })
}
function $notifyBlue(deviceId, callback, serviceId, characteristicId) {
    return new Promise((resolve, reject) => {
        uni.notifyBLECharacteristicValueChange({
            state: true, // 启用 notify 功能
            deviceId, // 设备id
            serviceId: 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);
    else
      val += "," + 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;  //获取函数所在作用域this
      var 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 加不加这个参数都可以正常搜索到设备。文章来源地址https://www.toymoban.com/news/detail-484194.html

到了这里,关于微信小程序蓝牙功能开发与问题记录的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 微信小程序云开发学习记录--1

    目录 1.配置小程序项目 2. 云创建 3.新建云文件夹 4.数据库的建立和使用 5.增、删、改、查四种数据库基本操作 增加数据 查询数据 修改数据 删除数据 拿到微信小程序,首先可以先配置好自己的服务器或者是云环境,服务器的方法就不说了,我们今天来主要看一下云环境的配

    2024年02月05日
    浏览(55)
  • uniapp微信小程序兼容性问题记录(持续记录)

    “vue”: { “version”: “2.6.14” } “uview-ui”: { “version”: “1.8.7” }, 用如上方式在H5端运行时没有问题的,但在微信小程序端就找不到组件,所以修改为全部在main.js中引入 官方解释如下 https://ask.dcloud.net.cn/question/145410 H5端运行效果 微信小程序端运行效果 一开始以为:style没

    2024年02月09日
    浏览(47)
  • 微信小程序 云开发 聊天功能

    。功能要求为:一对一聊天,可以发送文字,图片,语音,文件,视频,含消息列表页。 暑假没事干来写篇博客复盘一下。框架和样式部分就是采用了colorUI 的组件,没啥好说的,这方面我也不会  这是聊天内容部分 浅浅地小讲一下 scroll-into-view string 值应为某子元素id(id不

    2024年02月09日
    浏览(44)
  • 【问题记录】微信小程序无法打开公众号文章

    参考链接:小程序通过webview打开公众号的文章 - 简书 问题记录: 问题1:公众号文章链接不对 解决办法:从公众号后台获取文章链接即可(不用更改业务域名) 问题2:小程序未关联公众号(检查路径:设置-关联设置-关联公众号) 解决办法:登录“公众号管理后台-小程序

    2024年02月07日
    浏览(52)
  • 【微信开发】微信小程序实现实时聊天功能

    最近在做一个项目,需要运用到实时聊天功能,分享一下。      分为: 1.界面如何布局以及细节; 2.如何实现实时更新; 3.全部代码展示;         一、界面如何布局以及细节:         1.说到底,聊天界面就是循环一个数组,每一行为一个单位(item),循环(wx.for)我

    2024年02月04日
    浏览(53)
  • 微信小程序(二)开发审核踩坑记录

            作者在开发微信小程序《目的地到了》的过程中遇到过许多问题,这里讲讲一些技术和经验问题。         基本目录机构:         获取定位一定要在app.json里面申明,不然是没办法获取定位信息的         getLocation获取坐标的时需要指定坐标系,不然会偏移很

    2024年02月04日
    浏览(43)
  • 苹果手机微信小程序fixed失效问题记录

    遇到个奇怪的问题,在微信小程序页面中添加一个底部固定按钮,设置样式为fixed,结果在iphone11上会失效,即按钮会跟着页面布局移动,修改过程在此记录一下,遇到相同问题的朋友可以看下是否你也是相同问题。 手机版本: iphone 11  重现代码: index.wxml文件内容: index.

    2024年02月09日
    浏览(53)
  • SpringBoot对接微信小程序支付功能开发(一,下单功能)

    1,接入前准备: 接入模式选择直连模式; 申请小程序,得到APPID,并开通微信支付; 申请微信商户号,得到mchid,并绑定APPID; 配置商户API key,下载并配置商户证书,根据微信官方文档操作:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml 上面都配置完之后会得到:小

    2024年02月10日
    浏览(59)
  • 微信小程序接入客服功能开发流程

    首先看官方文档这样说的:1、小程序内:开发者在小程序内添加 客服消息按钮组件 ,用户可在小程序内唤起客服会话页面,给小程序发消息。客服消息使用指南 | 微信开放文档 客服消息按钮组件 button | 微信开放文档 在线客服是通过按钮组件来绑定的。所以我们小程序代码

    2024年02月08日
    浏览(49)
  • 微信小程序原生开发功能合集十五:个人主页功能实现

      本章个人主页功能实现,展示当前登录用户信息、个人主页、修改密码、浏览记录、我的收藏、常见问题、意见反馈、关于我们等界面及对应功能实现。   另外还提供小程序开发基础知识讲解课程,包括小程序开发基础知识、组件封装、常用接口组件使用及常用功能实

    2024年02月06日
    浏览(88)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包