JavaScriptを介したBluetoothデバイスとの通信

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

François Beaufort
François Beaufort

セキュリティとプライバシーが保護された状態で、ウェブサイトが近くのBluetoothデバイスと通信できると言ったら信じられますか?これが可能であれば、心拍数モニター、歌う電球、さらにはカメでさえ、ウェブサイトと直接対話することができることになります。

これまで、Bluetoothデバイスと対話する機能は、プラットフォーム固有のアプリでのみ可能でした。 Web Bluetooth APIはこれを変更し、ウェブブラウザでも実現することを目的としています。

まず始めに

この記事は、Bluetooth Low Energy(BLE)とGeneric Attribute Profile (GATT)がどのように機能するかについての基本的な知識があることを前提としています。

Web Bluetooth API仕様はまだ完成していませんが、仕様の作成者は、このAPIを試して、仕様に関するフィードバック実装に関するフィードバックを提供する意気込みのある開発者を積極的に探しています。

Web Bluetooth APIのサブセットは、ChromeOS、Chrome for Android 6.0、Mac(Chrome 56)、およびWindows 10(Chrome 70)で利用できます。つまり、近くのBluetooth Low Energyデバイスをリクエストして接続し、Bluetoothの特性を読み/書きし、GATT通知を受信してBluetoothデバイスの切断を認識し、Bluetooth記述子の読み書きさえも行うことができということです。詳細については、MDNのブラウザ互換性テーブルを参照してください。

Linuxおよび以前のバージョンのWindowsの場合は、about://flags#experimental-web-platform-featuresフラグを有効にしてください。

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

現場でWeb Bluetooth APIを使用している開発者から可能な限り多くのフィードバックを得るために、Chromeは以前、この機能をChromeOS、Android、およびMacのオリジントライアルとしてChrome53に追加しました。

このトライアルは2017年1月に成功して終了しました。

セキュリティ要件

セキュリティのトレードオフを理解するために、ChromeチームのソフトウェアエンジニアとしてWeb Bluetooth APIの仕様に取り組むJeffreyYasskinが投稿した「Web Bluetooth Security Model」という記事を読むことをお勧めします。

HTTPSのみ

この実験的なAPIはウェブに追加された強力な新機能であるため、セキュリティで保護されたコンテキストでのみ使用できます。つまり、TLSを念頭にビルドする必要があります。

ユーザージェスチャーが必要

セキュリティ機能として、navigator.bluetooth.requestDeviceでBluetoothデバイスを検出するには、タッチやマウスクリックなどのユーザージェスチャによるトリガーが必要です。pointerupclicktouchendイベントをリスンするということです。

button.addEventListener('pointerup', function(event) {
  // navigator.bluetooth.requestDeviceを呼び出す
});

コードを確認する

Web Bluetooth APIは、JavaScriptのPromisesに大きく依存しています。それらに精通していない場合は、このすばらしいPromisesチュートリアルをご覧ください。また、() => {}は単にECMAScript 2015のArrow関数です。

Bluetoothデバイスを要求する

このバージョンのWeb Bluetooth API仕様では、Centralロールで実行しているウェブサイトが、BLE接続を介してリモートGATTサーバーに接続できるようになっています。Bluetooth4.0以降を実装したデバイス間の通信をサポートしています。

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

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

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

サービスフィルター

たとえば、Bluetooth GATT Battery ServiceをアドバタイズするBluetoothデバイスを要求するには、次のように行います。

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

ただし、Bluetooth GATT Serviceが標準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'] // 後でサービスにアクセスするために必要です。
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

メーカーデータフィルター

manufacturerDataフィルターキーを使ってアドバタイズされているメーカー固有のデータに基づいてBluetoothデバイスをリクエストすることもできます。このキーは、companyIdentifierという名前の必須のBluetooth会社IDキーを持つオブジェクトの配列です。それで始まるBluetoothデバイスからのメーカーデータをフィルタリングするデータプレフィックスを指定することもできます。サービスフィルターに含まれていないサービスにアクセスできるようにoptionalServicesキーも定義する必要があることに注意してください。そうしない場合、後でそれらにアクセスしようとしたときにエラーが発生します。

// メーカーデータのバイトが [0x01, 0x02] で開始するGoogle会社のBluetoothデバイスを
// フィルターします。
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // 後でサービスにアクセスするために必要です。
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

マスクをデータプレフィックスとともに使用して、メーカーデータの一部のパターンに一致させることもできます。詳細については、 Bluetoothデータフィルターの説明をご覧ください。

フィルターなし

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

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // 後でサービスにアクセスするために必要です。
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Bluetoothデバイスに接続する

BluetoothDeviceを取得したら、次はどうすればよいでしょうか。サービスと特性の定義を保持しているBluetoothリモートGATTサーバーに接続することにしましょう。

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // 人間が読み取れるデバイス名。
  console.log(device.name);

  // リモートGATTサーバーへの接続を試行。
  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 => {
  // Battery Serviceを取得中…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Battery Level Characteristicを取得中…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Battery Levelを読み取り中…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

カスタムBluetoothGATT特性を使用する場合は、完全なBluetooth UUIDまたは短い16ビットまたは32ビット形式のいずれかを指定して、service.getCharacteristicを構成します。

characteristicvaluechangedイベントリスナーを追加して、その値の読み取りを処理することもできることに注意してください。以降でのGATT通知もオプションで処理する方法については、「Read Characteristic Value Changed Sample」をご覧ください。

…
.then(characteristic => {
  // 特性値が変化したときのイベントリスナーをセットアップ。
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // 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特性への書き込みは、読み取るのと同じくらい簡単です。今回は、Heart Rate Control Point(心拍数コントロールポイント)を使用して、心拍数モニターデバイスの [Energy Expended](消費エネルギー)フィールドの値を0にリセットしましょう。

これは特別な技ではなく、すべてHeart Rate Control Point Characteristicsページで説明されています。

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 => {
  // 1の書き込みは、使用されたエネルギーをリセットするシグナルとなります。
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

GATT通知を受け取る

次に、デバイスでHeart Rate Measurement(心拍数測定)特性が変更されたときに通知を受ける方法を見てみましょう。

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: Heart Rate Measurement値を解析する。
  // 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 => {
  // デバイスが切断されたときのイベントリスナーをセットアップ。
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // リモートGATTサーバーへの接続を試行。
  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デバイスの通信は停止しないことに注意してください。詳細については、 Device Disconnect Sample(デバイス切断サンプル)とAutomatic Reconnect Sample(自動再接続サンプル)をご覧ください。

Bluetooth記述子の読み書き

Bluetooth GATT記述子は、特性値を説明する属性です。 Bluetooth GATTの特性と同様の方法で、それらを読み書きできます。

たとえば、デバイスの体温計の測定間隔のユーザー説明を読み取る方法を見てみましょう。

以下の例では、health_thermometerHealth Thermometerサービスmeasurement_intervalMeasurement 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); });

サンプル、デモ、コードラボ

以下のすべてのWeb Bluetoothサンプルは正常に検証済みです。これらのサンプルをフルに活用するには、Battery Service、Heart Rate Service、またはHealth Thermometer Serviceを使用してBLEペリフェラルをシミュレートするBLE Peripheral Simulator Android Appをインストールすることをお勧めします。

初心者

  • Device Infor - デバイス情報。BLEデバイスから基本的なデバイス情報を取得します。
  • Battery Level - バッテリーレベル。バッテリー情報をアドバタイズするBLEデバイスからバッテリー情報を取得します。
  • Reset Energy - エネルギーのリセット。心拍数をアドバタイズするBLEデバイスから消費されるエネルギーをリセットします。
  • Characteristic Properties - 特性プロパティ。BLEデバイスの特定の特性のすべてのプロパティを表示します。
  • Notifications - 通知。BLEデバイスからの特徴的な通知を開始および停止します。
  • Device Disconnect - デバイスの切断。BLEデバイスに接続した後、接続を解除して、BLEデバイスの切断から通知を受け取ります。
  • Get Characteristics - 特性の取得。BLEデバイスからアドバタイズされたサービスのすべての特性を取得します。
  • Get Descriptors - 記述子の取得。BLEデバイスからアドバタイズされたサービスのすべての特性の記述子を取得します。
  • Manufacturer Data Filter - メーカーデータフィルター。メーカーデータと一致するBLEデバイスから基本的なデバイス情報を取得します。

複数の操作を組み合わせる

  • GAP Characteristics - GAP特性。BLEデバイスのすべてのGAP特性を取得します。
  • Device Information Characteristics - デバイス情報の特性。BLEデバイスのすべてのデバイス情報の特性を取得します。
  • Link Loss - リンク損失。BLEデバイスのアラートレベル特性(readValueおよびwriteValue)を設定します。
  • Discover Services & Characteristics - サービスと特性の検出。BLEデバイスからアクセス可能なすべてのプライマリサービスとその特性を検出します。
  • Automatic Reconnect - 自動再接続。指数バックオフアルゴリズムを使用して、切断されたBLEデバイスに再接続します。
  • Read Characteristic Value Changed - 変更された特性値の読み取り。バッテリーレベルを読み取り、BLEデバイスから変更が通知されます。
  • Read Descriptors - 記述子の読み取り。BLEデバイスからサービスのすべての特性の記述子を読み取ります。
  • Write Descriptor - 記述子の書き込み。BLEデバイス上の記述「Characteristic User Description」に書き込みます。

厳選されたWeb Bluetooth デモ公式のWeb Bluetooth Codelabsもご覧ください。

ライブラリ

  • web-bluetooth-utilsは、APIにいくつかの便利な関数を追加するnpmモジュールです。
  • Web Bluetooth APIシムは、最も人気のあるNode.js BLE 中央モジュールであるnobleで利用できます。これにより、WebSocketサーバーやその他のプラグインを必要とせずに、nobleをwebpack/browserifyで処理することができます。
  • angle-web-bluetoothは、Web Bluetooth APIの構成に必要なすべてのボイラープレートを抽象化するAngularのモジュールです。

ツール

  • Get Started with Web Bluetoothは、Bluetoothデバイスとの対話を開始するためのすべてのJavaScriptボイラープレートコードを生成するシンプルなウェブアプリです。デバイス名、サービス、特性を入力し、そのプロパティを定義すれば、準備は完了です。
  • すでにBluetooth開発者である場合、 Web Bluetooth Developer Studio Pluginでも、Bluetoothデバイス用のWeb Bluetooth JavaScriptコードを生成できます。

ヒント

Bluetooth InternalsページはChromeのabout://bluetooth-internalsで利用できるため、近くのBluetoothデバイスに関するすべて(ステータス、サービス、特性、記述子)を調べることができます。

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

また、Bluetoothのデバッグが難しい場合があるため、公式のHow to file Web Bluetooth bugs(Web Bluetoothバグの報告方法)ページを確認することもお勧めします。

今後の学習

まず、ブラウザとプラットフォームの実装ステータスを確認して、Web Bluetooth APIのどの部分が現在実装されているかを確認してください。

まだ不完全ですが、近い将来に期待される内容が簡単に説明されています。

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

APIのサポートを表示する

Web Bluetooth APIを使用することをぽ考えですか?あなたのパブリックサポートは、Chromeチームが機能に優先順位を付け、他のブラウザベンダーにそれらをサポートすることがいかに重要であるかを示す上でとても役立ちます。

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

リソース

謝辞

この記事をレビューしてくれたKayce Basquesにお礼申し上げます。ヒーロー画像提供: SparkFun Electronics(米国、ボルダー)