現在まだ仕様策定中の「Web Bluetooth API」ですが、
Chrome 63以降であれば利用することが出来るので、
早速手元にある体温計で計測した体温データをBLEでブラウザに連携して画面上に表示するということをやってみようと思います。
デモ
まずはどういうことか簡単にデモ動画を御覧ください。
このデモ動画では、体温計で計測した36.6度という体温データを
Web Bluetooth APIを利用してブラウザ側で体温データを取得するということを行っております。
では、どのように実装したかを下記で記載していきます。
環境
- 開発PC
- MacOS Sierra 10.12.6
- Google Chrome 65.0
Bluetooth接続の流れを理解する
Bluetoothでデバイスと接続する流れを簡単に記載すると下記のようになります。
流れ
1. BLE連携デバイスのスキャンを実施
2. スキャンに成功したデバイスに接続
3. 実行したい操作からGATTで定められた「Service」や「Characteristic」を設定
4. デバイスの計測値を受信出来るように待機
5. デバイスからデータが送信
6. 受信したバイナリを解析、処理の実施
GATTとは
GATTとは、「Generic Attribute Profile」の略で、Bluetooth通信でデータを送受信する方法や形式が決められている規格になります。
詳しい仕様については下記のサイトに詳しく記載されています。
Serviceについて
プロファイル情報などが定義されたものになります。
今回利用した体温計の製品情報をネットで調べてみると、Bluetooth仕様の欄に「Health Thermometer Profile」と記載があるため、「GATT Services」の**「Health Thermometer」**のServiceあたりを利用すれば良さそうです。
Characteristic
データを格納するのに用いられ、各データの保存フォーマットなどが細かく定められています。
GATTのCharacteristicを見ていると**「Battery Level」や「Date Time」などの情報も扱われていることが分かります。その中で、今回は体温のデータを扱いたいので、調べてみると「Temperature Measurement」**というのがあったので、このあたりを利用すれば良さそうです。
バイナリデータの形式
「Temperature Measurement」のデータ形式を見てみると、バイナリデータでどのようにデータが保存されているかを細かく確認することが出来ます。そのため、データ連携して受信したデータをこの形式に則ってバイナリデータを解析すれば必要なデータが取り出せるかと思います。
JavaScriptでバイナリデータ
JavaScriptでバイナリデータをどのように扱うのかは、下記の記事でまとめているため、もし興味あれば御覧ください。
- [基礎編]JavaScriptでバイナリデータを扱ってみる
- [応用編]JavaScriptでバイナリデータを扱ってみる~Bluetoothの温度データ形式を理解する~(1/3)
- [応用編]JavaScriptでバイナリデータを扱ってみる~IEEE-754とIEEE-11073の浮動小数点~(2/3)
- [応用編]JavaScriptでバイナリデータを扱ってみる~Bluetoothから取得した温度データを解析~(3/3)
長くなりましたが、前置きとしてのBluetooth連携の流れはここまでにし、
次にWeb Bluetooth APIを試してみようと思います。
Web Bluetooth APIを試してみる
具体的な仕様やブラウザの対応状況は下記を見ればなんとなく分かるかと思います。
コードを記載してみる
// 1.BLEデバイスをスキャンする
navigator.bluetooth.requestDevice({
acceptAllDevices:true, // 全てのデバイスを対象にスキャンを実施する
optionalServices:['利用するServiceのUniform Type Identifierを予め指定する']
}).then(device => {
// 2.デバイスに接続
return device.gatt.connect();
}).then(server =>{
// 3-1.「Service」を指定
return server.getPrimaryService("ServiceのUniform Type Identifierを指定");
}).then(service =>{
// 3-2.「Characteristc」を指定
return service.getCharacteristic("CharacteristcのUniform Type Identifierを指定");
}).then((characteristic) => {
//4.受信準備を行う
return characteristic.startNotifications().then(char => {
//5.受信したバイナリを解析、処理の実施
characteristic.addEventListener('characteristicvaluechanged', (event) => {
// 「event.target.value」がDataView型で渡ってくるのでこれを解析
});
});
});
Bluetooth連携の流れを理解していれば書き方としてはそこまで難しくない印象です。
Serviceを指定してCharacteristicを指定してとひとつずつ実施しなければいけないのがちょっとめんどくさい印象です。
現在の仕様でハマったこと
1. デバイススキャン実施時に引数を指定しないとエラーになる
navigator.bluetooth.requestDevice()
上記のようにrequestDeviceで引数を指定しなかった場合、下記のようなエラーで怒られました。
TypeError: Failed to execute 'requestDevice' on 'Bluetooth': Either 'filters' should be present or 'acceptAllDevices' should be true, but not both.
これはW3Cのドキュメントにも記載されている仕様ではあったのですが、
filtersで条件を指定して検索するか、acceptAllDevicesがtrueとして全スキャンのどちらかの指定が無いと駄目とのことでした。
なので今回は、acceptAllDevicesを使って全てのデバイスをスキャン可能にしました。
navigator.bluetooth.requestDevice({acceptAllDevices:true})
.catch(error => console.log(error));
2. 1回目ペアリングを成功させると2回目以降connectをするとエラーが出る
これは単純なバグかもしれませんが、BLEデバイスと接続する際には初回のみ**「ペアリング」**というものが行われ、接続するデバイス同士でデバイスの認識が行われます。
しかし、このペアリングが一度実行された状態で、下記のようにデバイススキャン後接続を試みようとするとエラーが表示されてしまいます。
navigator.bluetooth.requestDevice({acceptAllDevices:true})
.then(device => {
console.log("device", device);
return device.gatt.connect(); // ← ここでエラーになる
}).then(server =>{
console.log("server", server);
}).catch(error => console.log(error));
表示されるエラーは下記。この時BLEデバイスの方もペアリングモードからすぐに接続が遮断されてしまうので、Web Bluetooth APIのバグなのかなと思っている。
connect Error DOMException: Connection failed for unknown reason.
このままだとうまくいかないので、毎回ペアリングを解除することでデバッグを実施。
「システム環境設定」 -> 「Bluetooth」 -> 「デバイス」の欄でペアリング済みのデータがあれば「☓」ボタンを押してペアリングを解除することが可能
3. バッテリーの情報を取得しようとServiceとCharacteristicにUUIDを指定したらエラーになった
navigator.bluetooth.requestDevice({
acceptAllDevices:true
}).then(device => {
return device.gatt.connect();
}).then(server =>{
return server.getPrimaryService('180F');
}).then((service) =>{
return service.getCharacteristic('2A19');
}).then((value) =>{
let batteryLevel = value.getUint8(0);
console.log(batteryLevel);
}).catch((error) => {
console.log(error);
});
これで実行すると
TypeError: Failed to execute 'getPrimaryService' on 'BluetoothRemoteGATTServer': Invalid Service name: '180F'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. 'xxxxxxxx-yyyyyyyy-zzzzzzzzzz-aaaa'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/services e.g. 'alert_notification'.
0xを付けた形か、長い形式のUUIDか、GATT ServicesのStandard Nameのどれかを指定しろという意味っぽい。
ちなみにHTML5でアプリを作成することが出来るAngularとCordovaをベースにしたIonicでBLE連携を実装したことがあるが、その際は4桁の数字でもいけたので今回もいけるのかなと思ったが駄目だった...
参考: BLE - Ionic
ここでは、「battery_service」と名称を設定することで回避しました。
return server.getPrimaryService('battery_service');
4. デバイスをスキャンする際に、対象の操作(Serviceかな)を指定してスキャンする必要がある
単純に「{acceptAllDevices:true}」だけを引数として実行すると下記のようなエラーがでました。
DOMException: Origin is not allowed to access any service. Tip: Add the service UUID to 'optionalServices' in requestDevice() options. https://goo.gl/HxfxSQ
Deviceに接続のリクエストを投げる際に、オプションで検索する必要がある模様。
下記のように記載することで、権限の許可を得るまたは許可されているものを検索するという意味になるのかな。。。
navigator.bluetooth.requestDevice({
acceptAllDevices:true,
optionalServices:['battery_service']
})
optionalServicesに利用したいService名を記載することエラーを回避。
5. requestDeviceを呼び出すには、何かしらのユーザジェスチャーが必要
ここを知らずに結構ハマってしまったのですが、現在の仕様では、
デバイスをスキャンする際に、何かしらのユーザジェスチャーが無いと実行されません。
そのため、ボタンを押したらBLE連携のためのスキャン処理が始まり...というような流れにしました。
その他
今回Bluetooth接続で体温計側で挙動がおかしくなったり、ペアリングがおかしくなったりが何度か発生したのですが、その際の対応としては、
- Bluetoothの接続を一度切断する
- Chromeを再起動する
- Bluetoothの設定を一度OFFにする
などの対応を行えば、大概は元の状態に戻って、正常にデバッグが出来るような状態になっていました。
まとめ
Bluetooth接続の流れが把握できていれば、今回のWeb Bluetooth APIの利用もそこまで難しくないなぁと感じており、
WebブラウザでBluetooth連携が普及しだすともっと色々なことが出来るなぁとワクワクしています!
とはいえ、まだまだ議論が活発に行われている段階でもあるので、2~3年後にサービスとして本格的に活用されていることを夢見て、楽しみしておきたいと思います!
参考
- 公式ドキュメント
- WebBluetoothでブラウザ上からBluetoothLE通信
- WebBluetoothCG/web-bluetooth - GitHub
- Chrome OSにおけるWeb Bluetoothの実装状況 - Qiita
- ブラウザからBluetooth Low Energyを使うことができるWeb Bluetoothがアツいと思っている。 - Qiita
- hine/microbitble - GitHub
- micro:bitのBluetoothとMacのChromeをWeb Bluetooth APIでつなぐ - Qiita
- 【BLEを使う】GATT(Generic Attribute Profile)概要