angular
bluetooth
BLE
ionic
IonicDay 15

IonicでBluetooth連携してみる

More than 1 year has passed since last update.

この記事は Ionic Advent Calendar 2017 - Qiitaの15日目の記事です

はじめに

私自身これまでBackbone.jsを用いたスマホ向けのソーシャルゲーム開発に携わっていたため、
AngularやIonic歴はまだ2ヶ月程度とペーペー状態ですが、
現職ではIonicをベースとしたアプリケーションをメインで開発しているため、
今日はIonicでBluetooth連携してみたお話を少しできればと思います。

なお、Bluetoothで取得したデータをJavaScriptで扱う部分については、
下記の記事で記載しておりますので良かったらそちらも御覧ください。(宣伝ですみません!)

IonicでBluetooth連携

設定

IonicでBluetooth連携はとっても簡単!

公式ドキュメント(Ionic Native - BLE)の通りにまずは必要なpluginをインストール

$ ionic cordova plugin add cordova-plugin-ble-central
$ npm install --save @ionic-native/ble

app.modules.tsに追加

app.modules.ts
import { BLE } from '@ionic-native/ble';

  // ...省略...

@NgModule({
  providers: [
    BLE,

  // ...省略...

BLE連携を行う画面での設定

pages/home.ts
import { Component } from '@angular/core';
import { NavController, Platform } from 'ionic-angular';
import { BLE } from '@ionic-native/ble';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  constructor(public navCtrl: NavController, private ble: BLE, private plt: Platform) {
    // 1. 利用可能になったかをチェックする
    this.plt.ready().then((readySource) => {
      // 2. 接続対象のデバイスをスキャンする
      this.ble.scan([], 100000).subscribe(device => {
        // 3. 対象のデバイスに接続する
        this.ble.connect(device["id"]).subscribe(data=>{

          // 接続が出来れば用意されているメソッドを利用して色々なことができる!(後述)

        });
      });
    });
  }

簡単ですね!導入までのステップ

  1. 利用可能になったかをチェックする
  2. 接続対象のデバイスをスキャンする
    • 端末を特定してスキャン可能
  3. 対象のデバイスに接続する

このステップを踏んで実装すれば問題なく実装可能です。

また端末の設定でBluetoothが有効になっているかは、下記のようにすることで可能ですので、
無効の場合は警告のメッセージを表示するなりしてあげると良さそうですね

this.ble.isEnabled().then(res=>{
  console.log('BLE ON');
 console.log(res);
}).catch(error =>{
 console.log('BLE OFF');
 console.log(error);
})

デバイスとの接続のタイミングについて

個人的に「this.ble.scan」をしなくても、アプリケーション側からBLE接続先のデバイスのIDがわかっていれば
いきなり「this.ble.connect(deviceId)」のようにすることで接続できると思っていたのですが、
BLE接続先のデバイスがBLE接続モード(ペアリング)になっていないと接続は出来ないんですね....

今回対象となるデバイスがBLEでペアリング状態になるのは

  • デバイス起動後に手動でボタンを押すことでペアリングモードにした時
  • デバイスがデータを記録した直後に、記録したデータを連携しようとし、ペアリングモードになった時

の2点なので、アプリケーション側から接続しに行くことはできず、
あくまでアプリケーション側でbleのscanを行っているときに、
デバイス側がペアリングモードになると初めてBLEでデバイス接続できるというものでした。

(分かってみると当たり前かもですが、知らない間は結構ハマっていました。。。)

GATT(Generic Attribute Profile)の受信

デバイスに接続すると「GATT(Generic Attribute Profile)」と呼ばれる
BLEの通信のベースとなるプロファイルのデータを受信することが出来ます。

GATTを簡単にまとめると、どんなサービスが用意されていて、どういう通信アクションでやりとりすれば良いのかというのが記載されています。

実際にデバイス接続後にデバイスに対して命令をだしたりする場合には下記のように

this.ble.read(deviceId, serviceUUID, characteristicUUID)

this.ble.write(deviceId, serviceUUID, characteristicUUID, value)

と「deviceId」「serviceUUID」「characteristicUUID」が必要になります。
これらのデータが「GATT」で定義されている形となります。

詳細は下記の参考記事を御覧ください。

デバイスと接続後について

データの受信

BLEに接続してデータを受信するには、デバイス側がBLEでデータを送ろうとしている必要があります。
今回接続対象となるデバイスの場合だと、デバイスからデータを送る方法は、

  1. デバイスがデータを記録した直後に、記録したデータを連携しようとし、ペアリングモードになった時
  2. アプリケーション側から過去の記録データ全てを送信してくれと命令を出した時

の2パターンです。

実装すると下記のようになります。

1. デバイス側からペアリングモードになりデータを送ろうとしている場合

デバイスに接続したときに受信したGATTから「deviceId」「serviceUUID」「characteristicUUID」を取得し、
startNotificationで受信状態にアプリケーションがなるとデバイスからバイナリデータが送られてきます。

  // データを受信する処理
  this.ble.startNotification(deviceId, serviceUUID, characteristicUUID).subscribe(res => { 
    // 受信したデータはバイナリデータなのでJSで扱えるようにする
    var dv = new DataView(res);
    // 以降は「JavaScriptでバイナリデータを扱ってみる」関連をご参考に...
    this.ble.stopNotification(deviceId, serviceUUID, characteristicUUID);
  });

このstartNotificationのcharacteristicUUIDですが、
扱うデータによってBLEの標準規格でも定められており、今回は「温度」を扱うデバイスだったので、
「Temperature Measurement - org.bluetooth.characteristic.temperature_measurement」は、
「2A1C」であることが下記の公式サイトにも記載されております。

GATT Characteristics - Bluetooth

なお、接続するデバイスの仕様にもよるかと思いますが、今回の端末は未送信のデータ1件しか送られてこなかったので、
最新の記録データを受信するためには過去データを一度すべて消すか、過去データを全て取得するかどちらかを実装する必要がありました。

そのため、アプリケーション側からデバイスに対して何かしらの命令を出す必要がありました。

2. アプリケーション側から過去の記録データ全てを送信してくれと命令を出した時

ということで、今度はアプリケーション側からデバイスに対して命令を出してみようと思います。
こちらの命令はカスタムサービスとして登録されていることが多い(のかな?)ようなので、
各デバイスの仕様書を読み解く必要があるかと思います。

流れとしては、

  1. アプリケーションは、GATTで定義されたカスタムサービスに対して通信アクションとコマンドを実行(今回は全件送信コマンドの実行)
  2. デバイス側がコマンドを認識すると、データの送信モードになる
  3. アプリケーション側はデータ受信モードになりデータを受け取る(先程のstartNotificationを利用)

という形となります。

なおカスタムサービスのserviceUUIDとcharacteristicUUIDは、GATTに記載の下記のような部分があてはまるかと思います。
(このあたり正しくはあまり分かっていないですが...)

    // GATTの情報を一部抜粋
    "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 
    "name": "XXXXXXXXXXXXXXXX", 
    "services": [
        "AAAA", 
        "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
    ],
    "characteristics": [
        {
            "characteristic": "2A1C", 
            "isNotifying": false, 
            "properties": [
                "Indicate"
            ], 
            "service": "AAAA"
        }, 
        {
            "characteristic": "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ", 
            "isNotifying": false, 
            "properties": [
                "Read", 
                "Write"
            ], 
            "service": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
        }
    ],
項目
deviceId XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
serviceUUID YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
characteristicUUID ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ

このようなGATTの情報と各デバイスの仕様書に記載されたコマンドデータを実行することで
カスタムイベントを発火させることが出来ます。

/**************************************
 * 各デバイスの仕様書に記載されたコマンドを定義 *
 **************************************/
// 定められたバイト数のバッファ領域を確保(size)
var buffer = new ArrayBuffer(size);
// バイナリデータを書き込めるようにDataViewを利用
var dv = new DataView(buffer);

// 仕様書で定義されているコマンドを16進数で定義してみる(一例です)
var command = [0x00, 0x00, 0x00]
// 書き込む
command.forEach((value, index) => {
   dv.setUint8(index, value);
});

// 先程取得したカスタムサービスのIDたちと、コマンドのバッファをデバイスに対してwrite通信する
this.ble.write(deviceId, serviceUUID, characteristicUUID, dv.buffer).then(res => {
   console.log('命令の実行');
   console.log(res);
}).catch(e => {
   console.log('実行失敗');
   console.log(e);
});

こうすることで正しくデバイス側にコマンドが認識されれば、過去データ全てを送ろうとしてくれるので、
先程記載した「startNotification」で受信モードで待機していれば過去データ全てを受け取ることが出来ました。

なお個人的には、JavaScriptでバイナリのコマンドデータをどうやって送るねん!ってところのほうが難しく...
いろいろ試した結果、DataViewで書き込んだ後、「dv.buffer」とbufferプロパティの値を渡しれやれば良いというところにたどり着くまでが結構ハマっていました。

デバイスに現在時間を書き込んで見る

デバイスの仕様によるかもしれませんが、実際にデバイスから記録データを取得した際に、
当初時間データが記録されていない状況が発生していました。
なぜかなとよくよく調べてみると、接続時に現在時間の接続をしなければ時間データは付与されないという一文が記載されていたため、
現在では基本的にデバイス接続時にデバイスに時間を書き込む処理を走らせています。

時間データを扱う場合のcharacteristicUUIDは、公式ドキュメントより
「Date Time - org.bluetooth.characteristic.date_time」は、
「2A08」であることがわかります。

また、この[Date Time - Assigned Number: 0x2A08]の仕様についても、標準仕様として定められているため、
公式のドキュメントの形式に合わせて時間情報をバイナリデータで作成し、書き込めば良さそうですね!

上記参考サイトにも記載していますが、
時間データをバイナリデータで作る処理からサンプルコードを記載してみます。

// 「Date Time - Assigned Number: 0x2A08」の仕様に合わせて現在時間を作成
var buffer = new ArrayBuffer(7);
var dv = new DataView(buffer);

var dt = new Date();
// - 年
var year = dt.getFullYear();
// 2byteで記憶させる際にはリトルエンディアン方式でメモリ上では逆順に保存させる
// (↑ここの仕様は端末によって変わるかと思います)
dv.setUint16(0, year, true);

// - 月
var month = dt.getMonth()+1;
dv.setUint8(2, month);
// - 日
var day = dt.getDate();
dv.setUint8(3, day);

// - 時                
var hour = dt.getHours();                
dv.setUint8(4, hour);
// - 分
var minute = dt.getMinutes();                
dv.setUint8(5, minute);
// - 秒
var second = dt.getSeconds();                
dv.setUint8(6, second);

// デバイスにBLE経由で時間データを書き込む!
this.ble.write(device["id"], serviceUUID, characteristicUUID, dv.buffer).then(res => {
  console.log(res);
}).catch(e => {
  console.log(e);
});

デバイスに設定されている時間を読み込んで見る

時間データを書き込む場合と同様に、characteristicUUIDは「2A08」です。
この「2A08」となるserviceUUIDをGATTから検索すれば読み込みできますね。

this.ble.read(deviceId, serviceUUID, characteristicUUID).then(res => {
  // BLEからの受信データを読み書き出来るように
  let dv = new DataView(res);

  // 時間データを解析する(後述)

}).catch(e => {
  console.log('設定の読み込み失敗');
  console.log(e);
});

取得した時間データは先程の書き込み処理と逆のことをすればOKです。

// 2byte以上のデータを扱う際にリトルエンディアン方式だとtrueに設定する
// (↑ここの仕様は端末によって変わるかと思います)
let littleEndian = true;
let offset = 0;

// 時間は、下記の順で計7バイトのデータを扱う
// 2バイト: 年、 1バイト: 月, 日, 時, 分, 秒
let year, month, day, hour, minute, second;

year = dv.getUint16(offset, littleEndian);
offset += 2;

// JavaScriptでMonthは0から始まるため、取得した月から1を引いておく
month = dv.getUint8(offset) - 1;
offset++;

day = dv.getUint8(offset);
offset++;

hour = dv.getUint8(offset);
offset++;

minute = dv.getUint8(offset);
offset++;

second = dv.getUint8(offset);

var dt = new Date(year, month, day, hour, minute, second);
console.log(dt);

簡単ですね!

デバイスのバッテリー情報を取得してみる

バッテリー情報は、「Battery Level - org.bluetooth.characteristic.battery_level」で定義されており、
characteristicUUIDは、「2A19」ですね。

標準仕様通りにデバイス側で実装されている場合には、
uint8の形式で1バイト目にバッテリーレベルが0~100の間で確認することが出来るかと思います。

this.ble.read(deviceId, serviceUUID, characteristicUUID).then(res => {
  let dv = new DataView(res);
  console.log(dv.getUint8(0));
});

まとめ

今回Ionicというよりは、BLEのデータの扱いの方がフォーカスが強かった感じになってしまいましたが、
BLE連携でハマった部分といえば、「アプリケーションとデバイスとの接続の仕様(scanしてからconnect)」と「バイナリデータの扱い」でした。

このあたりの仕様を理解していれば、Ionicを使えば簡単に(多分Ionicじゃなくても同じかな)接続してデータを扱うことが出来るかと思います。

また、現在はiOS/AndroidといったネイティブアプリでしかBLE連携が出来ませんが、
ブラウザ上でBLE連携が可能な「Web Bluetooth API」などもあるため、
アプリ上で動くときにはIonic(cordovaのplugin)を利用し、
Webブラウザ上動くときには「Web Bluetooth API」を利用するなど
実行環境に応じて切り替えられるようになればもっと便利になるなと感じています。

(「Web Bluetooth API」ってどんな形式なのか、まだ触ってないので触ってみたい)