透過 JavaScript 與藍牙裝置通訊

Web Bluetooth API 可讓網站與藍牙裝置通訊。

François Beaufort
François Beaufort

假如我告訴你,網站可以透過安全且兼顧隱私權的方式,與附近的藍牙裝置通訊,你會怎麼想?這樣一來,心率監測器、會唱歌的燈泡,甚至是烏龜,都能直接與網站互動。

在此之前,只有特定平台的應用程式才能與藍牙裝置互動。Web Bluetooth API 旨在改變這種情況,並將其帶入網頁瀏覽器。

事前準備

本文假設您具備藍牙低功耗 (BLE) 和通用屬性設定檔的基礎知識。

雖然 Web Bluetooth API 規格尚未定案,但規格作者正積極尋找熱心的開發人員來試用這個 API,並提供規格相關意見實作相關意見

ChromeOS、Android 版 Chrome 6.0、Mac (Chrome 56) 和 Windows 10 (Chrome 70) 提供 Web Bluetooth API 的子集。也就是說,您應該可以要求連線至附近的藍牙低功耗裝置、讀取/寫入藍牙特性、接收 GATT 通知,以及瞭解藍牙裝置斷線的時間,甚至讀取及寫入藍牙描述符。詳情請參閱 MDN 的「瀏覽器相容性」表格。

如果是 Linux 和舊版 Windows,請在 about://flags 中啟用 #experimental-web-platform-features 標記。

適用於原始測試

為了盡可能從在該領域使用 Web Bluetooth API 的開發人員那裡獲得意見回饋,Chrome 先前已在 Chrome 53 中新增這項功能,做為 ChromeOS、Android 和 Mac 的原始版本試用版

試用期已於 2017 年 1 月順利結束。

安全性規定

如要瞭解安全性取捨,建議您參閱 Jeffrey Yasskin 的文章「Web Bluetooth 安全性模型」,他是 Chrome 團隊的軟體工程師,負責 Web Bluetooth API 規格。

僅限 HTTPS

由於這個實驗性 API 是新增至網際網路的強大新功能,因此只提供給安全情境。也就是說,您需要考量 TLS 來建構應用程式。

需要使用者手勢

為了確保安全性,使用 navigator.bluetooth.requestDevice 探索藍牙裝置時,必須由使用者動作觸發,例如輕觸或滑鼠點選。我們要談的是監聽 pointerupclicktouchend 事件。

button.addEventListener('pointerup', function(event) {
  // Call navigator.bluetooth.requestDevice
});

進入程式碼

Web Bluetooth API 大量仰賴 JavaScript Promise。如果您不熟悉這些概念,請參閱這份實用的承諾教學課程。另外,() => {} 是 ECMAScript 2015 箭頭函式

要求藍牙裝置

這個版本的 Web Bluetooth API 規格可讓在中央角色中執行的網站,透過 BLE 連線連線至遠端 GATT 伺服器。支援實作藍牙 4.0 以上版本的裝置間通訊。

當網站使用 navigator.bluetooth.requestDevice 要求鄰近裝置的存取權時,瀏覽器會提示使用者使用裝置選擇器,讓他們選擇裝置或取消要求。

藍牙裝置使用者提示。

navigator.bluetooth.requestDevice() 函式會採用定義篩選條件的必要物件。這些篩選器只會傳回符合部分宣傳的藍牙 GATT 服務和/或裝置名稱的裝置。

服務篩選器

舉例來說,如要要求藍牙裝置宣傳 Bluetooth GATT 電池服務

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });

不過,如果您的藍牙 GATT 服務不在標準化藍牙 GATT 服務清單中,您可以提供完整的藍牙 UUID 或 16 或 32 位元短格式。

navigator.bluetooth.requestDevice({
  filters: [{
    services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
  }]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

名稱篩選器

您也可以使用 name 篩選器鍵,根據宣傳的裝置名稱來要求藍牙裝置,甚至可以使用 namePrefix 篩選器鍵,根據這個名稱的前置字串來要求裝置。請注意,在這種情況下,您還需要定義 optionalServices 鍵,才能存取服務篩選器中未包含的任何服務。如果未執行這項操作,日後嘗試存取這些資料時,系統會顯示錯誤訊息。

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

製造商資料篩選器

您也可以根據使用 manufacturerData 篩選器鍵宣傳的製造商特定資料,要求藍牙裝置。這個鍵是物件陣列,其中包含名為 companyIdentifier 的強制 Bluetooth 公司 ID 鍵。您也可以提供資料前置字串,篩選出以該字串開頭的藍牙裝置製造商資料。請注意,您還需要定義 optionalServices 鍵,才能存取服務篩選器中未包含的任何服務。如果沒有,日後嘗試存取這些資料時,系統會顯示錯誤訊息。

// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

您也可以搭配資料前置字元使用遮罩,以便比對製造商資料中的部分模式。詳情請參閱藍牙資料篩選器說明

排除篩選器

navigator.bluetooth.requestDevice() 中的 exclusionFilters 選項可讓您從瀏覽器挑選器中排除部分裝置。可用於排除符合較廣泛篩選器但不受支援的裝置。

// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
  filters: [{
    namePrefix: "Created by"
  }],
  exclusionFilters: [{
    name: "Created by Francois"
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

沒有篩選器

最後,您可以使用 acceptAllDevices 鍵,而非 filters 顯示附近的所有藍牙裝置。您也需要定義 optionalServices 金鑰,才能存取部分服務。如果未執行這項操作,日後嘗試存取這些資料時,系統就會顯示錯誤訊息。

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

連線至藍牙裝置

那麼,現在您有 BluetoothDevice 了,該怎麼做呢?讓我們連線至負責保存服務和特徵定義的 Bluetooth 遠端 GATT 伺服器。

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Human-readable name of the device.
  console.log(device.name);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

讀取藍牙特徵

我們會在此連線至遠端藍牙裝置的 GATT 伺服器。現在,我們要取得主要 GATT 服務,並讀取屬於此服務的特徵。舉例來說,我們來試試讀取裝置電池目前的電量。

在後續範例中,battery_level標準化的電池電量特性

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Getting Battery Service…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Getting Battery Level Characteristic…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Reading Battery Level…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

如果您使用自訂的藍牙 GATT 特性,可以將完整的藍牙 UUID 或 16 或 32 位元簡短格式提供給 service.getCharacteristic

請注意,您也可以在特性上新增 characteristicvaluechanged 事件監聽器,以便處理讀取其值的作業。請參閱「讀取特徵值變更的範例」,瞭解如何視需要處理即將到來的 GATT 通知。


.then(characteristic => {
  // Set up event listener for when characteristic value changes.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Reading Battery Level…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Battery percentage is ' + batteryLevel);
}

寫入藍牙特徵

寫入藍牙 GATT 特徵的操作與讀取操作一樣簡單。這次,我們將使用心率控制點,將心率監測器裝置的「Energy Expended」欄位值重設為 0。

我保證這裡沒有任何魔法。詳情請參閱「心率控制點特性」頁面。

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
  // Writing 1 is the signal to reset energy expended.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

接收 GATT 通知

接下來,我們來看看如何在裝置上收到「心率測量」特性變更的通知:

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleCharacteristicValueChanged);
  console.log('Notifications have been started.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Received ' + value);
  // TODO: Parse Heart Rate Measurement value.
  // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

「Notifications Sample」會說明如何使用 stopNotifications() 停止通知,並正確移除已新增的 characteristicvaluechanged 事件監聽器。

中斷與藍牙裝置的連線

為了提供更優質的使用者體驗,您可能需要監聽中斷連線事件,並邀請使用者重新連線:

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Set up event listener for when device gets disconnected.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Device ${device.name} is disconnected.`);
}

您也可以呼叫 device.gatt.disconnect(),將網頁應用程式與藍牙裝置斷開連線。這麼做會觸發現有的 gattserverdisconnected 事件監聽器。請注意,如果其他應用程式已與藍牙裝置通訊,此應用程式不會停止藍牙裝置通訊。如需進一步瞭解,請參閱「裝置斷線範例」和「自動重新連線範例」。

讀取及寫入藍牙描述元

Bluetooth GATT 描述符是用來描述特徵值的屬性。您可以使用類似於藍牙 GATT 特性的方式讀取及寫入這些內容。

舉例來說,我們來看看如何讀取使用者對裝置健康溫度計測量間隔的說明。

在下方範例中,health_thermometer健康溫度計服務measurement_interval測量間隔特性gatt.characteristic_user_description特性使用者說明描述符

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
  const decoder = new TextDecoder('utf-8');
  console.log(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

我們已經讀取使用者對裝置健康溫度計測量間隔的說明,現在來看看如何更新並寫入自訂值。

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
  const encoder = new TextEncoder('utf-8');
  const userDescription = encoder.encode('Defines the time between measurements.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

範例、試用版和程式碼研究室

以下所有 Web Bluetooth 範例都已通過測試。為了充分享受這些範例,建議您安裝 [BLE 周邊模擬器 Android 應用程式],該應用程式可模擬使用電池服務、心率服務或健康溫度計服務的 BLE 周邊裝置。

新手

  • 裝置資訊:從 BLE 裝置擷取基本裝置資訊。
  • Battery Level:從 BLE 裝置廣告的電池資訊中擷取電池資訊。
  • Reset Energy:重設 BLE 裝置廣播心率所耗用的能量。
  • 特性屬性:顯示 BLE 裝置特定特性的所有屬性。
  • 通知:開始和停止來自 BLE 裝置的特徵通知。
  • Device Disconnect:中斷連線,並在連線後收到 BLE 裝置中斷連線的通知。
  • Get Characteristics:取得 BLE 裝置廣告服務的所有特性。
  • Get Descriptors:取得 BLE 裝置宣傳服務的所有屬性描述項。
  • 製造商資料篩選器:從符合製造商資料的 BLE 裝置中擷取基本裝置資訊。
  • 排除篩選器:從 BLE 裝置擷取基本裝置資訊,並提供基本排除篩選器。

合併多項作業

歡迎參閱我們精選的 Web Bluetooth 示範官方 Web Bluetooth 程式碼研究室

程式庫

  • web-bluetooth-utils 是 npm 模組,可為 API 新增一些方便的函式。
  • noble 是目前最熱門的 Node.js BLE 中樞模組,其中提供 Web Bluetooth API 墊片。這樣一來,您就能使用 webpack/browserify noble,而不需要 WebSocket 伺服器或其他外掛程式。
  • angular-web-bluetoothAngular 模組,可抽離設定 Web Bluetooth API 所需的所有樣板。

工具

提示

Chrome 中的 藍牙內部頁面 (about://bluetooth-internals) 可讓您檢查附近藍牙裝置的所有資訊,包括狀態、服務、特性和描述符。

在 Chrome 中偵錯藍牙的內部頁面螢幕截圖
Chrome 中的內部頁面,可用於偵錯藍牙裝置。

我還建議您查看官方的「如何回報 Web Bluetooth 錯誤」頁面,因為有時要對藍牙進行偵錯作業可能會很困難。

後續步驟

請先檢查瀏覽器和平台實作狀態,瞭解目前正在實作的 Web Bluetooth API 部分。

雖然仍未完成,但我們先搶先一窺近期的更新內容:

  • navigator.bluetooth.requestLEScan()掃描附近的 BLE 廣告
  • 新的 serviceadded 事件會追蹤新發現的藍牙 GATT 服務,而 serviceremoved 事件則會追蹤已移除的服務。當任何特徵和/或描述元從藍牙 GATT 服務中新增或移除時,系統會觸發新的 servicechanged 事件。

顯示對 API 的支援

您打算使用 Web Bluetooth API 嗎?你的公開支持有助於 Chrome 團隊決定功能優先順序,並向其他瀏覽器供應商顯示支援這些功能的重要性。

使用主題標記 #WebBluetooth 發送推文給 @ChromiumDev,告訴我們你在何處使用這項功能,以及使用方式。

資源

特別銘謝

感謝 Kayce Basques 審查本文。主頁橫幅圖片由 美國博爾德的 SparkFun Electronics 提供