你不知道的前端藍牙應用實踐 -- 心率帶

一、背景

最近開啓了減肥計劃,購入了一條心率帶,期望在使用划船機過程中監測心率情況。購入後的情況如下:

於是萌生了在自己的節拍器小程序內監聽心率數據的想法,即 Taro 小程序中的藍牙應用實踐。

二、簡單瞭解藍牙

中心設備 / 外圍設備

客戶端(中心設備):在本次實踐中爲筆者的手機。

服務端(外圍設備):在本次實踐中爲心率帶、耳機等設備。

BLE(Bluetooth Low Energy)

藍牙低能耗技術,實現設備間的連接與通信。顧名思義,能耗與成本都更低,與之相對應的則稱爲經典藍牙。基於筆者的開發目的,本文簡單瞭解設備連接前(GAP)和連接後(GATT)所涉及的兩個協議。

(瞭解更多:藍牙低能耗——百度百科 [1]、一文讀懂藍牙技術從 1.0 到 5.0 的前世今生 [2])

GAP(Generic Access Profile)

主要用來控制設備連接和廣播。通常是由外圍設備主動、間隔性地廣播設備信息,等待中心設備發現並建立連接。需要注意的是,這種連接方式是獨佔的,在建立連接後,外圍設備將停止廣播。

另一方面,還存在僅向外廣播而不建立連接的 iBeacon 設備。

GATT(Generic Attribute Profile)

定義了兩個設備間的數據傳輸方式。GATT 中有兩個關鍵概念:service(服務)與 characteristic(特徵)。

Service 就是一個獨立的邏輯項,它包含一個或多個 Characteristic。

Characteristic 是 GATT 中最小的邏輯數據單元,其屬性包含 properties,標識該特性是否能夠被 read、write、notify、indicate。notify 與 indicate 的區別在於,indicate 需要有回覆才能發送下一個數據包。

每個服務和特性都具有唯一的 UUID 標識,其中部分是由 Bluetooth SIG 官方定義的, Assigned Numbers | Bluetooth® Technology Website[3],如設備名、心率數據等常用屬性都是官方定義來統一規範。此外 UUID 也可以由硬件工程師來自定義實現。

(瞭解更多:BLE 相關協議(GAP&GATT)[4])

三、API 簡介

Taro 中的藍牙 API

Taro.openBluetoothAdapter(option) | Taro 文檔 [5]

初始化藍牙模塊

Taro.openBluetoothAdapter({
      success: function (res) {
        console.log("藍牙環境已啓動:", res);
        setInitStatus(true);
      },
      fail: function (err) {
        if (err.errMsg.includes("fail already opened")) {
          console.log("藍牙環境原先已啓動:", err);
          setInitStatus(true);
        } else {
          console.log("藍牙環境啓動失敗:", err);
          setInitStatus(false);
        }
      },
    });

Taro.getBluetoothDevices(option) | Taro 文檔 [6]

獲取在藍牙模塊生效期間所有已發現的藍牙設備。包括已經和本機處於連接狀態的設備。

 discoverInterval = setInterval(() ={
        Taro.getBluetoothDevices({
          success: async function (res) {
            const canConnectDevices = res.devices.filter(
              (item) =>
                // 信號強度大於-80
                item.RSSI > -80 &&
                // 含有設備名
                !["未知設備""MBeacon"].includes(item.name)
            );
            console.log("獲取藍牙設備列表成功:", canConnectDevices);

            setDeviceList(() => canConnectDevices);
          },
        });
      }, 2000);

也可以使用 Taro.onBluetoothDeviceFound(callback) | Taro 文檔 [7] 來發現設備。

Taro.createBLEConnection(option) | Taro 文檔 [8]

連接低功耗藍牙設備。

若小程序在之前已有搜索過某個藍牙設備,併成功建立連接,可直接傳入之前搜索獲取的 deviceId 直接嘗試連接該設備,無需進行搜索操作。

 Taro.createBLEConnection({
        deviceId,
        success: function (res) {
          console.log("設備連接成功", res);
          setConnectDeviceId(deviceId);

          Taro.onBLEConnectionStateChange(function (res) {
            // 該方法回調中可以用於處理連接意外斷開等異常情況
            console.log(
              `設備 ${res.deviceId}連接狀態發生變化: ${res.connected}`
            );
            if (!res.connected) {
              setConnectDeviceId("");
            }
          });
        },
      });

Taro.getBLEDeviceServices(option) | Taro 文檔 [9]

獲取藍牙設備所有服務 (service)。

Taro.getBLEDeviceServices({
  // 這裏的 deviceId 需要已經通過 createBLEConnection 與對應設備建立鏈接
  deviceId,
  success: function (res) {
    console.log('device services:', res.services)
  }
})

Taro.getBLEDeviceCharacteristics(option) | Taro 文檔 [10]

獲取藍牙設備某個服務中所有特徵值 (characteristic)。

Taro.getBLEDeviceCharacteristics({
  // 這裏的 deviceId 需要已經通過 createBLEConnection 與對應設備建立鏈接
  deviceId,
  // 這裏的 serviceId 需要在 getBLEDeviceServices 接口中獲取
  serviceId,
  success: function (res) {
    console.log('device getBLEDeviceCharacteristics:', res.characteristics)
  }
})

Taro.readBLECharacteristicValue(option) | Taro 文檔 [11]

讀取低功耗藍牙設備的特徵值的二進制數據值。注意:必須設備的特徵值支持 read 纔可以成功調用。

Taro.notifyBLECharacteristicValueChange(option) | Taro 文檔 [12]

啓用低功耗藍牙設備特徵值變化時的 notify 功能,訂閱特徵值。注意:必須設備的特徵值支持 notify 或者 indicate 纔可以成功調用。

另外,必須先啓用 notifyBLECharacteristicValueChange 才能監聽到設備 characteristicValueChange 事件

Taro.onBLECharacteristicValueChange(callback) | Taro 文檔 [13]

監聽低功耗藍牙設備的特徵值變化事件。必須先啓用 notifyBLECharacteristicValueChange 接口才能接收到設備推送的 notification。

WEB 藍牙 API

此處貼一些資料,感興趣可自行閱讀

Web Bluetooth API - Web APIs | MDN[14]

通過 JavaScript 與藍牙設備通信 [15]

四、設備名稱(Device Name)詳解

首先 getBLEDeviceServices 獲取到服務列表:

查詢資料

可知 0x1800 是我們需要的服務。getBLEDeviceCharacteristics 獲取其特徵列表:

查詢資料

可知 0x2A00 是我們需要的特徵。此時可以看到 read 屬性爲 true,我們通過 onBLECharacteristicValueChange 和 readBLECharacteristicValue 讀一下數據看看。

  Taro.onBLECharacteristicValueChange(function (characteristic) {
      const buffer = characteristic.value
      const unit8Array = new Uint8Array(buffer);
      console.log("unit8Array: ", unit8Array);
      
      // 轉字符串
      const encodedString = String.fromCodePoint.apply(null, unit8Array);
      console.log('設備名:', encodedString)
  });
  
  Taro.readBLECharacteristicValue({
    deviceId,
    serviceId: DeviceNameService,
    characteristicId: DeviceNameCharacteristics,
  });

得到輸出,某米耳機:

大家應該已經發現,給到的特徵值其實是 ArrayBuffer 格式。

(瞭解更多:談談 JS 二進制: File、Blob、FileReader、ArrayBuffer、Base64 - 掘金 [16])

此時需要我們將其轉化爲字符串。除了上面的方法外,還可以先轉 16 進制,再轉字符串:

// 轉16進制
 const hexString = Array.prototype.map
    .call(unit8Array, function (bit) {
      return ("00" + bit.toString(16)).slice(-2);
    })
    .join("");
 console.log("hexString: ", hexString);
  
 // 16進制轉字符串
 const hex2String = (hexString:string) ={
     if (hexString.length % 2) return "";
     var tmp = "";
     for (let i = 0; i < hexString.length; i += 2) {
        tmp += "%" + hexString.charAt(i) + hexString.charAt(i + 1);
     }
     return decodeURI(tmp);
 }

五、心率測量(Heart Rate Measurement)詳解

同樣,首先我們拿到了所需的服務和特徵如下。

export const HEART_RATE_SERVICE_UUID =
    "0000180D-0000-1000-8000-00805F9B34FB";
export const HEART_RATE_CHARACTERISTIC_UUID =
     "00002A37-0000-1000-8000-00805F9B34FB";

在設備連接後,通過 onBLECharacteristicValueChange 和 notifyBLECharacteristicValueChange 訂閱特徵值變化。

export const blueToothGetHeartRate = (
  deviceId: string,
  onHeartRateChange: (newHeartRate: number) => void
) ={
  Taro.onBLECharacteristicValueChange(function (characteristic) {
    const heartRateValue = getHeartRateValue(characteristic.value);
    onHeartRateChange(heartRateValue);
  });
  Taro.notifyBLECharacteristicValueChange({
    state: true, // 啓用 notify 功能
    deviceId,
    serviceId: HEART_RATE_SERVICE_UUID,
    characteristicId: HEART_RATE_CHARACTERISTIC_UUID,
  });
};

此時我們已經能夠獲取到心率帶發送的心率數據如下。

但是此時如果按照上文解析設備名稱的方式,將其轉化爲字符串,將得到一串亂碼。

所以需要根據協議文檔(https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/)來解讀這幾個數據。

  1. 標誌字段:

  1. 心率數值:

  1. RR-intreval 心率間隔:

六、總結與疑惑

至此整個藍牙心率設備數據獲取的實現就完成了,可以在使用節拍器的同時監控自身的心率數據了。

整體來說整個開發過程還是比較簡單的,畢竟 API 文檔描述的非常清晰,主要時間耗費在解讀心率數據這個過程,後來知道應該從協議文檔出發解讀就好說了。

由於是在業餘時間實現的上述能力,開發過程中存在不少疑惑沒來得及研究。這裏先拋兩個問題出來,希望瞭解相關知識的同學能夠不吝賜教。

  1. 在 arraybuffer 轉 16 進制過程中("00" + bit.toString(16)).slice(-2);,爲什麼要先"00" +,然後再.slice(-2)?直接bit.toString(16)是否可行?

  2. 針對某米耳機,筆者暫時沒有在服務和特徵中找到電量信息相關的數據,那麼手機設備又是如何獲取到耳機電量的呢?

  3. 猜測電池服務和特性分別是0x180F Battery service0x2A19 Battery Level ,但上圖耳機返回的 service 列表中沒找到該服務。

本文作者正在等待你的幫助。

❤️ 謝謝支持

以上便是本次分享的全部內容,希望對你有所幫助 ^_^

歡迎關注公衆號 ELab 團隊 收貨大廠一手好文章~

參考資料

[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]

Assigned Numbers | Bluetooth® Technology Website: https://www.bluetooth.com/specifications/assigned-numbers/

[4]

BLE 相關協議(GAP&GATT): https://www.jianshu.com/p/62eb2f5407c9

[5]

Taro.openBluetoothAdapter(option) | 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(option) | Taro 文檔: https://docs.taro.zone/docs/apis/device/bluetooth-ble/getBLEDeviceServices

[10]

Taro.getBLEDeviceCharacteristics(option) | 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(option) | 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 Bluetooth API - Web APIs | MDN: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API

[15]

通過 JavaScript 與藍牙設備通信: https://web.dev/i18n/zh/bluetooth/

[16]

談談 JS 二進制: File、Blob、FileReader、ArrayBuffer、Base64 - 掘金: https://juejin.cn/post/7148254347401363463#heading-9

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/n2rED1sXOftPckPFPFFBAg