记一次WebBluetooth踩坑过程~

lxf2023-12-20 00:50:02

在职公司主要做蓝牙智能设备的,被迫踩坑

一个阳光明媚的早晨,哥们带着困意去上班,早上开会,帅气(**)的产品经理说:安卓、iOS、小程序啊个平台都有相对应的应用,那咱Web端是不是也有技术可以实现呢。 记一次WebBluetooth踩坑过程~ 我当时心里一万只草泥马路过,他娘的又给我增加工作量,没办法,哥们也是出来讨碗饭吃,迫于权威,会后只能含泪继续接受压迫。

废话不多说

该API只支持本地调试和HTTPS环境下才可以使用!!!

不管搞啥技术,咱是不是得先找到官方文档

  • W3C官方对WebBluetooth的定义及使用说明:Web Bluetooth
  • MDN:Bluetooth

官方文档是找到了,但是对高考英语25的我... 英文不好的小伙伴准备好CtrlCV翻译吧(我知道你要说浏览器有机翻,但是哥们翻了一万遍,没反应,焯!)

再来看看兼容性

  • caniuse

基本上基于Chromium的高版本浏览器都支持

蓝牙操作流程

可能有小伙伴对蓝牙的操作流程还不是很了解,咱们先大概的了解一下,看下图

graph TD
初始化蓝牙 --> 搜索周围蓝牙设备 --> 连接蓝牙设备 --> 获取设备服务 --> 获取服务的特征值 --> 对特征值进行读写监听通知

开始

上图是基本流程,正常还是得先判断一下浏览器版本是否支持

// 浏览器版本过低,没有这个API,建议升级浏览器
if(!navigator.bluetooth){
    return 
}

好,浏览器是支持了,现在得看一下设备上有没有蓝牙适配器(一般台式电脑是没有蓝牙的,调试的话小伙伴们得插个蓝牙适配器)

const isAvailability = await navigator.bluetooth.getAvailability()

if(isAvailability){
    // 有蓝牙适配器
}else{
    // 没有蓝牙适配器
}

扫描周围设备

蓝牙适配器也有了,可以开始继续下一步了,弄这个之前咱也弄过微信小程序和uniapp上的蓝牙,他俩都要先初始化然后才能扫描蓝牙设备,但是WebBluetooth这边是直接可以扫描设备然后进行连接,可能它内部做了处理,具体不太清楚,有大佬的话可以给大伙讲讲,我也学习一下。

不对蓝牙设备进行过滤,展示所有蓝牙设备

/**
 * acceptAllDevices 不过滤蓝牙设备,接受所有蓝牙设备
 */
navigator.bluetooth.requestDevice({
    acceptAllDevices: true
})

第一个大坑

这里看似没问题,项目上线后,测试同学的笔记本上,周围一万个蓝牙设备,死活都扫不出来一个设备,好家伙我那是一会儿百度搜,一会儿谷歌搜,愣是出不来,一下午找不出来啥情况,只能去电脑系统蓝牙设置里去看看了,瞎猫碰到死耗子

记一次WebBluetooth踩坑过程~

把这个勾上,问题就解决了,焯!

对蓝牙设备进行过滤

使用过滤,就得把acceptAllDevices设为false,或者不填,不然报错

过滤条件,可以通过设备名称前缀、设备名、蓝牙服务、广播数据过滤等,有很多详见requestDevice(options)

因为蓝牙设备名称都有不一样的,不可能通过名称来过滤,这里主要描述一下根据services和manufacturerData过滤

  • services为数组,元素为蓝牙设备服务的UUID
navigator.bluetooth.requestDevice({
    filters: [{ 
        services: [0xFFFF, 0xEEEE] 
    }]
})

第二个大坑

我在services中设置了我想要过滤的设备的服务UUID,结果有很多有该服务的设备被过滤了,我??? 最后没办法,只能通过广播包里的厂商标识ID来判断了,整!

navigator.bluetooth.requestDevice({
    filters: [{ 
         manufacturerData: [{
             companyIdentifier: 0x3231
         }]
    }]
})

这样,基本上就把需要的设备过滤出来了。这个坑不光是WebBluetooth有,小程序,uniapp都出现过,我现在都怀疑是我司的蓝牙设备有问题。

连接蓝牙设备

记一次WebBluetooth踩坑过程~

选择一个蓝牙设备点击配对,requestDevice().then 并返回该设备信息

为了看的舒服后续都用async await来写,先走流程不考虑异常情况!

const device = await navigator.bluetooth.requestDevice({...})

// 监听设备断开
device.addEventListener('gattserverdisconnected', e => {...})
// 或
device.ongattserverdisconnected = (e) => {...}

device类型为BluetoothDevice

...
const gatt = await device.gatt.connect()

gatt类型为BluetoothRemoteGATTServer

获取服务列表

...
// 获取所有服务
const services = await gatt.getPrimaryServices()
// services为该蓝牙设备的服务列表 [BluetoothRemoteGATTService,BluetoothRemoteGATTService,...]

services类型为Array<BluetoothRemoteGATTService>

获取服务的特征值

...
// 获取某个服务下的所有特征值
const characteristics = await service.getCharacteristics()
// characteristics为该服务的特征值 [BluetoothRemoteGATTCharacteristic,BluetoothRemoteGATTCharacteristic,...]

characteristics类型为Array<BluetoothRemoteGATTCharacteristic>

根据特征值属性来读/写/监听

...
// 获取服务下某个特征值
const characteristic = await server.getCharacteristic(BluetoothRemoteGATTCharacteristic.uuid)
const { write, writeWithoutResponse, read, notify } = characteristic.properties
if(write){
    // 可写
    characteristic.writeValue(value)
}
if(writeWithoutResponse){
    // 可写
    characteristic.writeWithoutResponse(value)
}
if(read){
    // 可读
    characteristic.readValue(value)
}
if(notify){
    // 通知
    // 监听通知函数
    characteristic.addEventListener(
        "characteristicvaluechanged",
        async (event) => {
            console.log(event.target.value);
        }
    );
    // 开启通知
    await characteristic.startNotifications();
    // 关闭通知 await characteristic.startNotifications();
}

整个流程

// 弹窗中选择设备,点击配对
const device = await navigator.bluetooth.requestDevice({...})

// 监听设备断开 
device.addEventListener('gattserverdisconnected', e => {...})

// 连接gatt服务
const gatt = await device.gatt.connect()

// 获取所有服务
const services = await gatt.getPrimaryServices()

// 获取所有服务的所有特征值
const characteristics = await Promise.all(services.map(service => service.getCharacteristics))

// 例如特征值列表中第一个特征值为读,第二个为写, 第三个为通知, 实际要通过properties来判断
const [readCharacteristics,
    writeCharacteristics,
    notifyCharacteristics,
    ...otherCharacteristics ] = characteristics
    
// 先监听可通知的特征值
// 先绑定监听函数
notifyCharacteristics.addEventListener("characteristicvaluechanged",(event) => {
    console.log(event.target.value);
});
// 再打开通知,避免丢失通知信息
notifyCharacteristics.startNotifications()

// 此时数据交互通道已经打通,业务上可以理解为连接成功了

// 读取特征值value
readCharacteristics.readValue()

// 写入特征值value
// !!!官方标注writeValue后续版本将会删除,请用writeValueWithResponse或writeValueWithoutResponse
// 读特征值和写特征值数据格式都为ArrayBuffer
writeCharacteristics.writeValueWithoutResponse()
// 比如 我要向特征值写入三个byte 0xAA 0xBB 0xCC
writeCharacteristics.writeValueWithoutResponse(new Uint8Array([0xAA, 0xBB, 0xCC])))

以上就是基本使用

数据交互封装

我不知道是不是所有蓝牙设备数据交互都是这样,我司的蓝牙设备,数据交互就是写入特征值,返回值从notify中出来,数据收发不在同一个上下文,这操作起来不是很麻烦吗。没办法,为了写业务的时候方便,直接利用发布订阅+Promise,将数据收发放在同一上下文,再加上个超时机制,开整!

import bus from "xxx"

notifyCharacteristics.addEventListener("characteristicvaluechanged",(event) => {
    // 比如value的第一位是标识
    const id = event.target.value[0]
    bus.emit(id, event.target.value)
});

// 封装指令写入
function writeValue(value){
    const id = value[0]
    return new Promise((resolve,reject)=>{
         // 开启超时定时器,三秒超时
        const timer = setTimeout(()=>{
            // 超时先触发
            // 移除bus监听
            bus.off(id,fn)
            // 移除setTimeout
            clearTimeout(timer)
            // 将promise reject
            reject({ code: -1, msg:"指令超时" })
        }, 3000)
        const fn = (value)=> {
            // bus.on先触发
            // 移除bus监听
            bus.off(id,fn)
            // 取消超时定时器
            clearTimeout(timer)
            // 将promise resolve
            resolve({ code: 0, data: value, msg: "操作成功" })
        }
        // 先监听
        bus.on(id, fn)
        // 写入特征值
        writeCharacteristics.writeValueWithoutResponse(new Uint8Array(value))
    })
}


// 比如这条指令,发送[0xAA,0xFF],返回[0xAA,0x01],表示位为0xAA
writeValue([0xAA,0xFF]).then((res)=>{
    // 操作成功
    // res.data = [0xAA, 0x01]
    console.log("数据", res.data)
}).catch((err)=>{
    // 指令超时
})

以上就是简易封装,不知各位大佬还有没有什么好的解决方案

总结

WebBluetooth还是实验性技术,使用时请考虑风险性!

当时弄这个弄得真的头大,到处找资料,很多也只是个介绍,并未详细讲解,特别是那个搜索设备的过滤,搞一下午,也可能是英文不好的原因,看官方文档比较吃力导致的,我做梦的时候赶紧恶补一下英语。

对蓝牙也不是很了解,所以导致踩了很多坑。

第一次写,写的还是比较乱,有错误的地方评论区叼我,哥们马上改,最后还是希望能帮助到需要的小伙伴!

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!