cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 5 セントラル編 (後編)
前回の続き。
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 1 仕組み・準備編
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 2 ペリフェラル編 (前編)
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 3 ペリフェラル編 (後編)
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 4 セントラル編 (前編)
前回までで、セントラル端末として動作するのに必要な API を Promise 化して用意するところまでができた。
今回は各 API にオプションを渡し、実際に動作させるためのコンポーネント側の実装を進めていく。
まずは処理を並べてみる
ペリフェラル側のときと同じように、まずは処理を並べてみる。
@Component({ /* 省略 */ })
export class CentralComponent {
// プロパティの類、およびサービスクラスの DI は省略
/** 「セントラル通信開始」ボタン押下時の処理 */
execCentral() {
// メッセージ表示 (updateMessage() の実装は PeripheralComponent と同じなので省略)
this.updateMessage('セントラル通信開始');
// ペリフェラル端末のアドレスを控えておく退避変数
let address;
// セントラルの初期化処理
this.centralService.initialize()
.then(() => {
this.updateMessage('初期化完了・スキャン開始');
// 定数から探索対象のアドバタイジング名を指定する
return this.centralService.startScan(bluetoothConstants.advertisingName);
})
.then((targetAddress) => {
this.updateMessage('スキャン完了・接続開始');
// スキャンしたペリフェラル端末のアドレスを退避変数に控えておく
address = targetAddress;
// オプションに address を指定して接続する
return this.centralService.connect({
address: address
});
})
.then(() => {
this.updateMessage('接続完了・サービス情報取得開始');
return this.centralService.discover({
address: address
});
})
.then(() => {
this.updateMessage('サービス情報取得完了・write 要求送信開始');
return this.centralService.write( /* TODO : オプション */ );
})
.then(() => {
this.updateMessage('write 要求送信完了・read 要求送信開始');
return this.centralService.read( /* TODO : オプション */ );
})
.then((readResult) => {
// value 値があれば read 要求に対する応答の受信に成功
if(readResult.value) {
this.updateMessage('read 要求送信完了・応答メッセージを出力');
// メッセージをデコードしてテキストボックスに出力する
this.cReceiveText = this.bluetoothService.decodeText(readResult.value);
}
this.updateMessage('切断処理開始');
return this.centralService.disconnect({
address: address
});
})
.then(() => {
this.updateMessage('切断処理完了・通信終了処理開始');
return this.centralService.close({
address: address
});
})
.then(() => {
this.updateMessage('通信終了処理完了・セントラル通信の終了処理が完了');
})
.catch((error) => {
// どこかの処理で失敗したらエラーメッセージを表示
this.updateMessage(`ペリフェラル通信終了処理に失敗しました : ${error}`);
});
}
}
startScan
にはアドバタイジング名を渡す。connect
・discover
・disconnect
・close
はいずれも address
プロパティに接続対象のアドレスを指定するだけなので実装してしまった。
スキャン後の接続開始時は、connect
を呼び、そのあとに discover
を呼んでからでないと、write
や read
ができない。write
・read
の詳細はこのあと説明する。
通信の切断処理も、disconnect
してから close
を呼ばないと、完全に通信を切ることができない。ココはお決まりのパターンなので固定で覚えてしまう。
write
と read
のオプション設定
それでは、write
と read
のオプション設定にうつる。
// execCentral() 内
// 省略
.then(() => {
this.updateMessage('サービス情報取得完了・write 要求送信開始');
return this.centralService.write({
// 退避変数のアドレスを指定する
address: address,
// 定数からサービス・キャラクタリスティックを指定する
service: bluetoothConstants.serviceUuid,
characteristic: bluetoothConstants.characteristicUuid,
// テキストボックスの文字列をエンコードして送信する
value: this.bluetoothService.encodeText(this.cSendText),
// ペリフェラル端末からの応答を待たずに write を成功させる
type: 'noResponse'
});
})
.then(() => {
this.updateMessage('write 要求送信完了・read 要求送信開始');
return this.centralService.read({
// 退避変数のアドレスを指定する
address: address,
// 定数からサービス・キャラクタリスティックを指定する
service: bluetoothConstants.serviceUuid,
characteristic: bluetoothConstants.characteristicUuid
});
})
.then((readResult) => {
// value 値があれば read 要求に対する応答の受信に成功
if(readResult.value) {
this.updateMessage('read 要求送信完了・応答メッセージを出力');
// メッセージをデコードしてテキストボックスに出力する
this.cReceiveText = this.bluetoothService.decodeText(readResult.value);
}
// 以後略
ココで定数の情報を使い、サービスおよびキャラクタリスティックを指定する。この情報自体は discover
の結果から取得することも可能ではあるが、どうせ1つのサービス・キャラクタリスティックしか提供していないので、定数から直に指定すれば良い。
write
の value
プロパティに、エンコードした文字列を渡す。これが、ペリフェラル側では writeRequested
で受け取れる文字列となるワケだ。type: 'noResponse'
を指定することで、write
要求に対するペリフェラル側の respond()
を待たなくなる。ペリフェラル側で指定した writeWithoutResponse: true
と合わせて指定しておくことで確実になる。
read
は、ペリフェラル側に「何か送り返して〜」と読み取りを要求するだけなので、指定するのはアドレス・サービス・キャラクタリスティックのみ。この read
要求に対し、ペリフェラル側が readRequested
で respond
処理を行うと、セントラル側の read
のコールバック関数が実行される、という動きになる。結果オブジェクト readResult
の value
プロパティが respond
された値になっているので、これをデコードして readonly
なテキストボックスに表示させてやれば OK。
write
・read
と respond
に関しては、どうも連続してやり取りしようとすると上手くいかなくなるところがあった。
例えば、connect
してから一度でも read
すると、その後 write
要求に respond
しても、応答メッセージが直前の read
に対する respond
で送信した値しか渡らなくなってしまうのだ。respond
したペリフェラル側では正しく新たなメッセージを応答しているのだが、なぜかセントラル側では最初の read
で受け取ったメッセージを write
でも受け取ってしまうのだ。
connect
してから disconnect
するまでの間で、write
の前に read
をしない通信パターンなら問題なく write
に対する respond
でメッセージが返せたが、どうも write
に対する respond
が絡むと意図したとおりに動作しないことがあるので、一度 read
か write
をしたら disconnect
して、都度再接続するような作りにした方が安全そうだった。
5回に分けて説明してきた cordova-plugin-bluetoothle プラグインだったが、これでペリフェラル・セントラルの両方の実装ができた。あとは2台の実機にこの Cordova アプリをインストールし、片方はペリフェラル画面、もう片方はセントラル画面を開き、同時に「通信開始」ボタンを押せば良い。今回の実装でいえば、通信の流れは以下のようになる。
- ペリフェラル : 初期化 → サービス追加 → アドバタイジング開始
- セントラル : 初期化 → スキャン開始 → (アドバタイジングが開始したら取得できるようになるので) ペリフェラル端末検知 → スキャン停止
- セントラル : write 要求送信 (画面入力されたテキストを送信する)
- ペリフェラル : writeRequested 発火 → 受信したメッセージを画面に表示する
- セントラル : read 要求送信
- ペリフェラル : readRequested 発火 → 画面入力されたテキストを返送する → ペリフェラル終了処理
- セントラル : read 要求の結果受信 → 返信されたメッセージを画面に表示する → 切断 → セントラル終了処理
今回紹介しなかったが、セントラル側が subscribe()
で受信待機し、ペリフェラル側が notify()
で任意のタイミングでメッセージを発信する、という API もある。こちらは unsubscribe()
するまでずっと受信待機できるし、ペリフェラル側はセントラルとの通信状況に関係なく notify()
で情報発信ができて面白い。
また、今回の実装は「ペリフェラル」画面と「セントラル」画面を分けて実装したが、トグルボタンなどで「ペリフェラルモード」と「セントラルモード」を切り替えて1画面で実装しても良いだろう。
「ペリフェラル」「セントラル」といった役割を意識せずに、自動的にどちらかがペリフェラル、どちらかがセントラルになるような実装ができたら、よりユーザにとって分かりやすい画面になりそう。どうやったらいいんだろうな、はじめはセントラル端末として周辺端末をスキャンしつつ、いなさそうなら initializePeripheral
しちゃえばいいのかしら?
色々と遊べそうな cordova-plugin-bluetoothle でした。本編はココで終わりだが、次回は開発時のモック化の方法を紹介する。