在职公司主要做蓝牙智能设备的,被迫踩坑
一个阳光明媚的早晨,哥们带着困意去上班,早上开会,帅气(**)的产品经理说:安卓、iOS、小程序啊个平台都有相对应的应用,那咱Web端是不是也有技术可以实现呢。
我当时心里一万只草泥马路过,他娘的又给我增加工作量,没办法,哥们也是出来讨碗饭吃,迫于权威,会后只能含泪继续接受压迫。
废话不多说
该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
})
第一个大坑
这里看似没问题,项目上线后,测试同学的笔记本上,周围一万个蓝牙设备,死活都扫不出来一个设备,好家伙我那是一会儿百度搜,一会儿谷歌搜,愣是出不来,一下午找不出来啥情况,只能去电脑系统蓝牙设置里去看看了,瞎猫碰到死耗子
把这个勾上,问题就解决了,焯!
对蓝牙设备进行过滤
使用过滤,就得把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都出现过,我现在都怀疑是我司的蓝牙设备有问题。
连接蓝牙设备
选择一个蓝牙设备点击配对,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为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!