JavaScript 経由で Bluetooth デバイスと通信する

Web Bluetooth API を使用すると、ウェブサイトが Bluetooth デバイスと通信できます。

François Beaufort
François Beaufort

ウェブサイトがプライバシーを保護しながら、近くの Bluetooth デバイスと通信できるとしたらどうでしょうか?これにより、心拍数モニター、歌う電球、さらにはカメもウェブサイトと直接やり取りできるようになります。

これまで、Bluetooth デバイスを操作できるのは、プラットフォーム固有のアプリに限られていました。Web Bluetooth API は、この状況を変え、ウェブブラウザにも Bluetooth を導入することを目的としています。

始める前に

このドキュメントは、Bluetooth Low Energy(BLE)と Generic Attribute Profile の仕組みに関する基本的な知識があることを前提としています。

Web Bluetooth API の仕様はまだ確定していませんが、仕様作成者はこの API を試して仕様に関するフィードバック実装に関するフィードバックを提供してくれる熱心なデベロッパーを積極的に求めています。

Web Bluetooth API のサブセットは、ChromeOS、Chrome for Android 6.0、Mac(Chrome 56)、Windows 10(Chrome 70)で使用できます。つまり、近くの Bluetooth Low Energy デバイスをリクエストして接続したり、Bluetooth 特性を読み取り/writeしたり、GATT 通知を受信したり、Bluetooth デバイスの接続が切断されたことを把握したり、Bluetooth 記述子の読み取りと書き込みを行ったりできるようになります。詳しくは、MDN のブラウザの互換性の表をご覧ください。

Linux と以前のバージョンの Windows の場合は、about://flags#experimental-web-platform-features フラグを有効にします。

オリジン トライアルで利用可能

現場で Web Bluetooth API を使用しているデベロッパーからできるだけ多くのフィードバックを収集するため、Chrome では以前、ChromeOS、Android、Mac 向けのオリジン トライアルとして、Chrome 53 にこの機能を追加しました。

このトライアルは、2017 年 1 月に終了いたしました。

セキュリティ要件

セキュリティのトレードオフを理解するには、ウェブ Bluetooth API 仕様に取り組む Chrome チームのソフトウェア エンジニア Jeffrey Yasskin による、Web Bluetooth セキュリティ モデルの投稿をご覧ください。

HTTPS のみ

この試験運用版 API はウェブに追加された強力な新機能であるため、安全なコンテキストでのみ使用できます。つまり、TLS を念頭に置いてビルドする必要があります。

ユーザー操作が必要です

セキュリティ機能として、navigator.bluetooth.requestDevice による Bluetooth デバイスの検出は、タップやマウスクリックなどのユーザー操作によってトリガーする必要があります。ここでは、pointerupclicktouchend イベントのリッスンについて説明します。

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

コードを確認する

Web Bluetooth API は、JavaScript の Promise に大きく依存しています。Promise をよくご存じない場合は、Promise のチュートリアルをご覧ください。もう 1 つ、() => {} は ECMAScript 2015 のアロー関数です。

Bluetooth デバイスをリクエストする

このバージョンの Web Bluetooth API 仕様では、セントラル ロールで実行されているウェブサイトが、BLE 接続を介してリモート GATT サーバーに接続できます。Bluetooth 4.0 以降を実装するデバイス間の通信をサポートしています。

ウェブサイトが navigator.bluetooth.requestDevice を使用して近くのデバイスへのアクセスをリクエストすると、ブラウザはデバイス選択ツールを表示します。ユーザーは、1 つのデバイスを選択するか、リクエストをキャンセルできます。

Bluetooth デバイスのユーザー プロンプト

navigator.bluetooth.requestDevice() 関数は、フィルタを定義する必須のオブジェクトを受け取ります。これらのフィルタは、アドバタイズされた Bluetooth GATT サービスやデバイス名の一部と一致するデバイスのみを返すために使用されます。

サービス フィルタ

たとえば、Bluetooth GATT バッテリー サービスをアドバタイズしている Bluetooth デバイスをリクエストするには、次のようにします。

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

Bluetooth GATT サービスが標準化された Bluetooth GATT サービスのリストに含まれていない場合は、完全な Bluetooth UUID を指定するか、短い 16 ビットまたは 32 ビット形式を指定できます。

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

名前フィルタ

また、name フィルタキーでアドバタイズされているデバイス名、または namePrefix フィルタキーでこの名前の接頭辞に基づいて Bluetooth デバイスをリクエストすることもできます。この場合、サービス フィルタに含まれていないサービスにアクセスできるように、optionalServices キーを定義する必要もあります。これを行わないと、後でアクセスしようとするとエラーが発生します。

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

メーカーデータフィルタ

manufacturerData フィルタキーでアドバタイズされるメーカー固有のデータに基づいて、Bluetooth デバイスをリクエストすることもできます。このキーは、companyIdentifier という名前の必須の Bluetooth 企業識別子キーを持つオブジェクトの配列です。データ接頭辞を指定して、その接頭辞で始まる Bluetooth デバイスのメーカーデータをフィルタすることもできます。サービス フィルタに含まれていないサービスにアクセスできるように、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); });

メーカーデータの一部のパターンを照合するために、データ接頭辞とともにマスクを使用することもできます。詳しくは、Bluetooth データフィルタの説明をご覧ください。

をご覧ください。

除外フィルタ

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); });

フィルタなし

最後に、filters の代わりに acceptAllDevices キーを使用して、近くにあるすべての Bluetooth デバイスを表示できます。また、一部のサービスにアクセスするには、optionalServices キーを定義する必要があります。これを行わないと、後でアクセスしようとしたときにエラーが発生します。

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

Bluetooth デバイスに接続する

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); });

Bluetooth 特性を読み取る

ここでは、リモート Bluetooth デバイスの 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); });

カスタムの Bluetooth GATT 特性を使用する場合は、完全な Bluetooth 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);
}

Bluetooth 特性に書き込む

Bluetooth GATT 特性への書き込みは、読み取るのと同じくらい簡単です。今回は、心拍数コントロール ポイントを使用して、心拍数モニター デバイスの [消費カロリー] フィールドの値を 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
}

通知のサンプルでは、stopNotifications() で通知を停止し、追加された characteristicvaluechanged イベント リスナーを適切に削除する方法を示しています。

Bluetooth デバイスとの接続を解除する

ユーザー エクスペリエンスを向上させるには、切断イベントをリッスンして、ユーザーに再接続を促すことができます。

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() を呼び出して、ウェブアプリと Bluetooth デバイスの接続を解除することもできます。これにより、既存の gattserverdisconnected イベント リスナーがトリガーされます。別のアプリがすでに Bluetooth デバイスと通信している場合、Bluetooth デバイスの通信は停止しません。詳しくは、デバイスの切断サンプル自動再接続サンプルをご覧ください。

Bluetooth 記述子の読み取りと書き込み

Bluetooth GATT 記述子は、特性値を記述する属性です。Bluetooth GATT 特性と同様に読み取りと書き込みを行うことができます。

たとえば、デバイスの健康状態の温度計の測定間隔に関するユーザーの説明を読み取る方法を見てみましょう。

以下の例で、health_thermometer健康温度計サービスmeasurement_interval測定間隔の特性gatt.characteristic_user_descriptionCharacteristic 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); });

サンプル、デモ、Codelab

以下のウェブ Bluetooth サンプルはすべて正常にテストされています。これらのサンプルを最大限に活用するには、[BLE ペリフェラル シミュレーター Android アプリ] をインストールすることをおすすめします。このアプリは、バッテリー サービス、心拍数サービス、健康測定用温度計サービスを使用して BLE ペリフェラルをシミュレートします。

初級

  • デバイス情報 - BLE デバイスから基本的なデバイス情報を取得します。
  • バッテリー残量 - バッテリー残量をアドバタイズしている BLE デバイスからバッテリー情報を取得します。
  • エネルギーをリセット - BLE デバイスが広告する心拍数で消費されたエネルギーをリセットします。
  • Characteristic Properties - BLE デバイスの特定の特性のプロパティをすべて表示します。
  • 通知 - BLE デバイスからの特性通知を開始および停止します。
  • デバイスの切断 - BLE デバイスに接続した後に切断し、切断を通知します。
  • Get Characteristics - BLE デバイスからアドバタイズされたサービスのすべての特性を取得します。
  • Get Descriptors - BLE デバイスからアドバタイズされたサービスのすべての特性の記述子を取得します。
  • メーカーデータ フィルタ - メーカーデータと一致する BLE デバイスから基本的なデバイス情報を取得します。
  • 除外フィルタ - 基本的な除外フィルタを使用して、BLE デバイスから基本的なデバイス情報を取得します。

複数のオペレーションの組み合わせ

  • GAP 特性 - BLE デバイスのすべての GAP 特性を取得します。
  • デバイス情報の特性 - BLE デバイスのすべてのデバイス情報の特性を取得します。
  • リンク切れ - BLE デバイスのアラートレベル特性を設定します(readValue と writeValue)。
  • サービスと特性の検出 - BLE デバイスからアクセス可能なすべてのプライマリ サービスとその特性を検出します。
  • 自動再接続 - 指数バックオフ アルゴリズムを使用して、接続解除された BLE デバイスに再接続します。
  • 特性値の読み取り - バッテリー残量を読み取って、BLE デバイスからの変更に関する通知を受け取ります。
  • Read Descriptors - BLE デバイスからサービスのすべての特性の記述子を読み取ります。
  • Write Descriptor - BLE デバイスの「Characteristic User Description」デスクリプタに書き込みます。

おすすめのウェブ Bluetooth デモウェブ Bluetooth 公式 Codelab もご覧ください。

ライブラリ

  • web-bluetooth-utils は、API に便利な関数を追加する npm モジュールです。
  • Web Bluetooth API シムは、最も一般的な Node.js BLE セントラル モジュールである noble で利用できます。これにより、WebSocket サーバーやその他のプラグインを必要とせずに、webpack や Browserify を使用できます。
  • angular-web-bluetoothAngular のモジュールであり、ウェブ Bluetooth API の構成に必要なボイラープレートをすべて抽象化します。

ツール

  • Web Bluetooth のスタートガイドは、Bluetooth デバイスの操作を開始するための JavaScript ボイラープレート コードをすべて生成するシンプルなウェブアプリです。デバイス名、サービス、特性を入力し、そのプロパティを定義すれば、準備は完了です。
  • すでに Bluetooth デベロッパーである場合は、Web Bluetooth Developer Studio プラグインによって、Bluetooth デバイス用の Web Bluetooth JavaScript コードも生成されます。

ヒント

Chrome では、Bluetooth の仕組みのページ(about://bluetooth-internals)が利用できます。このページでは、ステータス、サービス、特性、記述子など、付近の Bluetooth デバイスに関するあらゆる情報を検査できます。

Chrome で Bluetooth をデバッグするための内部ページのスクリーンショット
Bluetooth デバイスのデバッグ用に Chrome の内部ページ。

Bluetooth のデバッグは難しい場合があるため、公式の ウェブ Bluetooth バグを報告する方法のページも確認することをおすすめします。

次のステップ

まず、ブラウザとプラットフォームの実装状況を確認して、現在実装されている Web Bluetooth API の部分を確認します。

まだ完成していませんが、近い将来に予定されている機能のプレビューは次のとおりです。

  • navigator.bluetooth.requestLEScan() は、付近の BLE アドバタイズをスキャンします。
  • 新しい serviceadded イベントは新しく検出された Bluetooth GATT サービスを追跡し、serviceremoved イベントは削除された Bluetooth GATT サービスを追跡します。新しい servicechanged イベントは、Bluetooth GATT サービスに特性や記述子が追加または削除されたときに発生します。

API のサポートを表示する

Web Bluetooth API を使用する予定はありますか?皆様の公開サポートは、Chrome チームが機能の優先度を判断し、そのサポートの重要性を他のブラウザ ベンダーに説明するのに役立ちます。

ハッシュタグ #WebBluetooth を使用して @ChromiumDev にツイートを送信し、どこでどのように使用しているかをお知らせください。

リソース

謝辞

この記事を確認していただいた Kayce Basques に感謝します。 米国ボールダーの SparkFun Electronics のヒーロー画像。