当前位置: 首页 > 科技观察

你所不知道的蓝牙前端应用实践-心率带

时间:2023-03-14 00:32:14 科技观察

本文为字节教育-成人与创新前端团队成员的文章,已获得ELab授权发布。1.背景我最近开始减肥计划,买了一条心率带,希望在使用划船机的同时监测自己的心率。购买后情况如下:心率带不直接显示数值,需要连接APP或相关设备使用。官方APP仅实时显示心率数据,无法生成心率统计图表。通过咕咚APP连接心率带,开始运动后即可监测心率变化,但划船机不在支持的运动范围内。我简单实现了一个划船机节拍器的小程序。于是萌生了在我的节拍器小程序中监测心率数据的想法,也就是太郎小程序中的蓝牙应用实践。2.简单了解蓝牙的概念。毕竟作者不是专业人士。有兴趣的同学可以通过百科、搜索引擎等渠道了解一下。这里只是简单介绍一下接下来会用到的一些知识。Centraldevice/peripheraldeviceclient(中央设备):在本实践中,为作者的手机。服务器(周边设备):本练习中为心率带、耳机等设备。BLE(BluetoothLowEnergy)蓝牙低功耗技术实现了设备之间的连接和通信。顾名思义,能耗和成本更低,对应的叫经典蓝牙。基于笔者的开发目的,本文简单了解一下设备连接前(GAP)和连接后(GATT)所涉及的两种协议。(了解更多:BluetoothLowEnergy-百度百科[1],一文了解蓝牙技术从1.0到5.0的前世今生[2])GAP(GenericAccessProfile)主要用于控制设备连接和广播。通常,外围设备主动间歇性地广播设备信息,等待中心设备发现并建立连接。需要注意的是,这种连接方式是排他性的,连接建立后外围设备会停止广播。另一方面,也有iBeacon设备只向外广播但不建立连接。GATT(GenericAttributeProfile)定义了两个设备之间的数据传输方式。GATT中有两个关键概念:服务(service)和特征(characteristic)。服务是一个独立的逻辑项,包含一个或多个特征。Characteristic是GATT中最小的逻辑数据单元,它的属性包括properties,标识该characteristic是否可以读、写、通知或指示。notify和indicate的区别在于indicate在发送下一个数据包之前需要一个回复。每个服务和功能都有一个唯一的UUID标识符,其中一些由蓝牙SIG正式定义,AssignedNumbers|Bluetooth?TechnologyWebsite[3],官方定义了设备名称、心率数据等常用属性,以统一规范。另外,UUID也可以由硬件工程师来实现。(了解更多:BLE相关协议(GAP&GATT)[4])三、API介绍Taro中的BluetoothAPITaro.openBluetoothAdapter(option)|芋头文档【5】初始化蓝牙模块console.log("蓝牙环境已经启动:",res);设置初始状态(真);},fail:function(err){if(err.errMsg.includes("failalreadyopened")){console.log("蓝牙环境之前启动过:",err);设置初始状态(真);}else{console.log("蓝牙环境启动失败:",err);设置初始状态(假);}},});Taro.getBluetoothDevices(选项)|Taro文档[6]获取蓝牙模块活跃期间所有发现的蓝牙设备。包括已经连接到本机的设备。discoverInterval=setInterval(()=>{Taro.getBluetoothDevices({success:asyncfunction(res){constcanConnectDevices=res.devices.filter((item)=>//信号强度大于-80item.RSSI>-80&&//包含设备名称!["Unknowndevice","MBeacon"].includes(item.name));console.log("成功获取蓝牙设备列表:",canConnectDevices);setDeviceList(()=>canConnectDevices);},});},2000);也可以使用Taro.onBluetoothDeviceFound(callback)|发现设备芋头文档[7]。Taro.createBLEConnection(选项)|Taro文档[8]连接到低功耗蓝牙设备。如果小程序之前搜索过蓝牙设备并成功建立连接,则可以直接传入之前搜索得到的deviceId,无需搜索直接尝试连接该设备。Taro.createBLEConnection({deviceId,success:function(res){console.log("设备连接成功",res);setConnectDeviceId(deviceId);Taro.onBLEConnectionStateChange(function(res){//可以使用该方法回调处理意外断开等异常情况}});},});Taro.getBLEDeviceServices(选项)|太郎文档[9]获取蓝牙设备的所有服务。Taro.getBLEDeviceServices({//这里的deviceId需要已经通过createBLEConnectiondeviceId与对应的设备建立了链接,success:function(res){console.log('deviceservices:',res.services)}})Taro.getBLEDeviceCharacteristics(选项)|太郎文档[10]获取蓝牙设备的一个服务中的所有特征值(characteristic)。Taro.getBLEDeviceCharacteristics({//这里的deviceId需要已经通过createBLEConnectiondeviceId与对应的设备建立了链接,//这里的serviceId需要在getBLEDeviceServices接口中获取serviceId,success:function(res){console.log('devicegetBLEDeviceCharacteristics:',res.characteristics)}})Taro.readBLECharacteristicValue(选项)|Taro文档[11]读取蓝牙低功耗设备的特征值的二进制数据值。注意:设备的特征值必须支持读取才能成功调用。接口读取的信息需要在onBLECharacteristicValueChange方法注册的回调中获取。Taro.notifyBLECharacteristicValueChange(选项)|Taro文献[12]开启低功耗蓝牙设备特征值发生变化时的notify功能,订阅该特征值。注意:设备的feature值必须支持notify或者indicate才能调用成功。另外必须先开启notifyBLECharacteristicValueChange,才能监听设备characteristicValueChange事件Taro.onBLECharacteristicValueChange(callback)|Taro文献[13]监测低功耗蓝牙设备的特征值变化事件。必须先开启notifyBLECharacteristicValueChange接口才能接收设备推送的通知。WEBBluetoothAPI在此发布一些信息,如果您有兴趣,可以阅读WebBluetoothAPI-WebAPIs|MDN[14]通过JavaScript与蓝牙设备通信[15]4.设备名称(DeviceName)详情首先getBLEDeviceServices获取服务列表:查询信息显示0x1800是我们需要的服务。getBLEDeviceCharacteristics获取其特征列表:查询数据显示0x2A00是我们需要的特征。此时可以看到read属性为true。我们通过onBLECharacteristicValueChange和readBLECharacteristicValue来读取数据。Taro.onBLECharacteristicValueChange(function(characteristic){constbuffer=characteristic.valueconstunit8Array=newUint8Array(buffer);console.log("unit8Array:",unit8Array);//转换字符串constencodedString=String.fromCodePoint.apply(null,unit8Array);console.log('设备名称:',encodedString)});Taro.readBLECharacteristicValue({deviceId,serviceId:DeviceNameService,characteristicId:DeviceNameCharacteristics,});得到输出,一些小米耳机:你应该已经发现了,给定的特征值其实是ArrayBuffer格式的。(了解更多:浅谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64——掘金[16])此时我们需要将其转换为字符串。除了上述方法,还可以先转十六进制,再转字符串://转十六进制consthexString=Array.prototype.map.call(unit8Array,function(bit){return("00"+bit.toString(16)).slice(-2);}).join("");console.log("hexString:",hexString);//十六进制转字符串consthex2String=(hexString:string)=>{if(hexString.length%2)return"";vartmp="";for(leti=0;ivoid)=>{Taro.onBLECharacteristicValueChange(function(characteristic){constheartRateValue=getHeartRateValue(characteristic.value);onHeartRateChange});价值(心脏)Taro.notifyBLECharacteristicValueChange({state:true,//开启通知功能deviceId,serviceId:HEART_RATE_SERVICE_UUID,characteristicId:HEART_RATE_CHARACTERISTIC_UUID,});};至此我们已经可以获取心率带发送的心率数据如下。但是这时候如果按照上面解析设备名的方法转成字符串,就会得到一串乱码。因此,这些数据需要根据协议文档(https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/)进行解读。Flag域:二进制:10110,十进制:22,十六进制:16,bit0为心率格式位,决定心率数据是uint8(bit0===0)还是unit16(bit0===1))bit1,Bit2为传感器接触状态位。11表示支持皮肤接触测试,接触正常。Bit3为能耗状态位,0表示能耗字段不存在。Bit4为RR-Interval状态位,为1表示有一个或多个RR-Interval值。心率值:二进制:1100101,十进制:101,十六进制:65当心率小于255时,unit8足够。但如果需要支持更高的心率值(部分动物),需要设置成unit16。这时候我们已经可以根据flag域0得到需要的心率值:101。那么剩下的两个字段是什么意思呢?RR-intreval心率间隔:从前面的flag字段分析,我们发现设备不支持能耗状态,但是支持RR-interval。RR-interval可以是一个或多个。那么我们如何读取这个数字呢?首先,顾名思义,心率间隔就是两次心跳之间的间隔。由上面的心率值计算出来:60*1000/101≈594ms接下来看这两个数字。二进制:1010010,十进制:82,十六进制:52二进制:10,十进制:2,十六进制:02试试2*256+82=594,谜团就解开了~6.总结与疑惑至此整个蓝牙心的实现心率设备数据采集完成,您可以在使用节拍器的同时监测自己的心率数据。总体来说,整个开发过程还是比较简单的。毕竟API文档写的很清楚,主要时间花在了解读心率数据的过程上。后来很容易理解,应该是从协议文档上去解读。由于上述能力是业余时间实现的,开发过程中有很多疑惑没有时间去研究。这里先提两个问题,希望了解相关知识的同学能给我指点一下。在arraybuffer转16进制的过程中("00"+bit.toString(16)).slice(-2);,为什么要先"00"+,再.slice(-2)?直接bit.toString(16)可以吗?对于某款小米耳机,笔者暂时没有在服务和功能中找到与电池信息相关的数据,那么移动设备如何获取耳机的电池电量呢?猜测BatteryService和Characteristics分别是0x180FBatteryService和0x2A19BatteryLevel,但是在上图中耳机返回的服务列表中并没有找到该服务。本文作者正在等待您的帮助。参考[1]低功耗蓝牙——百度百科:https://baike.baidu.com/item/%E8%93%9D%E7%89%99%E4%BD%8E%E8%83%BD%E8%80%97[2]一篇了解蓝牙技术从1.0到5.0的前世今生的文章:https://zhuanlan.zhihu.com/p/37717509[3]AssignedNumbers|Bluetooth?技术网站:https://www.bluetooth.com/specifications/assigned-numbers/[4]BLE相关协议(GAP&GATT):https://www.jianshu.com/p/62eb2f5407c9[5]Taro。打开蓝牙适配器(选项)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth/openBluetoothAdapter[6]Taro.getBluetoothDevices(option)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth/getBluetoothDevices[7]Taro.onBluetoothDeviceFound(callback)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth/onBluetoothDeviceFound[8]Taro.createBLEConnection(option)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth-ble/createBLEConnection[9]Taro.getBLEDeviceServices(选项)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth-ble/getBLEDeviceServices[10]Taro.getBLEDeviceCharacterist集成电路(选项)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth-ble/getBLEDeviceCharacteristics[11]Taro.readBLECharacteristicValue(option)|Taro文档:https://docs.taro。zone/docs/apis/device/bluetooth-ble/readBLECharacteristicValue[12]Taro.notifyBLECharacteristicValueChange(选项)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth-ble/notifyBLECharacteristicValueChange[13]Taro.onBLECharacteristicValueChange(callback)|Taro文档:https://docs.taro.zone/docs/apis/device/bluetooth-ble/onBLECharacteristicValueChange[14]Web蓝牙API-WebAPI|MDN:https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API[15]通过JavaScript与蓝牙设备通信:https://web.dev/i18n/zh/bluetooth/[谈论JS二进制:File、Blob、FileReader、ArrayBuffer、Base64——掘金:https://juejin.cn/post/7148254347401363463#heading-9