cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 4 セントラル編 (前編)
前回の続き。
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 1 仕組み・準備編
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 2 ペリフェラル編 (前編)
- cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 3 ペリフェラル編 (後編)
前回までで、ペリフェラル側の実装が完了した。今回はセントラル側の実装に移る。
セントラル端末側は、アドバタイジング名から通信対象のペリフェラル端末を特定して「アドレス」を取得する。そのアドレスで対象のペリフェラル機器と接続し、サービス・キャラクタリスティック名を指定して要求を送信するワケである。
セントラル側の画面の実装
まずはペリフェラル側と同様に、セントラル側の画面となるコンポーネントを実装する。
<h1>セントラル</h1>
<dl>
<dt>ペリフェラルに送信するテキスト</dt>
<dd>
<p><input type-"text" name="c-send-text" [(ngModel)]="cSendText"></p>
</dd>
<dt>ペリフェラルから受信した応答テキスト (読取専用)</dt>
<dd>
<p><input type="text" name="c-received-text" [value]="cReceivedText" readonly></p>
</dd>
</dl>
<p>
<input type="button" (click)="execCentral()" value="セントラル通信開始">
</p>
<!-- 動作の進捗を示すメッセージ表示欄 -->
<p>{{ message }}</p>
コンポーネントの実装は以下のような感じ。
@Component({
selector: 'app-central',
templateUrl: './central.component.html',
styleUrls: ['./central.component.scss']
})
export class CentralComponent {
/** ペリフェラルに送信するテキスト : デフォルト値を設定しておく */
cSendText: string = 'セントラルから送信';
/** ペリフェラルから受信した応答テキスト */
cReceivedText: string = '';
/** 動作の進捗を示すメッセージ表示欄 : デフォルト値を設定しておく */
message: string = '「セントラル通信開始」ボタンを押してください';
/** 「セントラル通信開始」ボタン押下時の処理 */
execCentral() {
// TODO : これから実装していく
}
セントラル通信に必要な API の Promise 化
続いて cordova-plugin-bluetoothle プラグインの API のうち、セントラル側で使用する API を Promise 化したサービスをこしらえる。
今回使用する API は以下のとおり。
- initialize … 第2引数 failureCallback、第3引数 options なし
- startScan … 後述
- isScanning … startScan と合わせて実装・後述
- stopScan … startScan と合わせて実装・後述
- connect
- discover
- write
- read
- disconnect
- close
initialize
は第2引数の errorCallback
なし。startScan
・isScanning
・stopScan
は組み合わせで実装するので後述。それ以外は successCallback
・failureCallback
・options
の順で引数を取るので、以下のように実装する。
@Injectable()
export class CentralService {
/** セントラル端末の初期化処理 */
initialize(): Promise<any> {
return new Promise((resolve) => {
(window as any).bluetoothle.respond(
// successCallback のみ
(result) => { resolve(result); }
);
});
}
/** スキャン開始処理 */
startScan(): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.startScan(
(result) => {
// TODO : 後で isScanning・stopScan との組み合わせで実装する
resolve(result);
},
(error) => { reject(error); }
);
});
}
/** 指定のアドレスのペリフェラル端末と接続する */
connect(options: Object): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.connect(
(result) => { resolve(result); },
(error) => { reject(error); },
options
);
});
}
/** 指定のアドレスのペリフェラル端末の情報を取得する */
discover(options: Object): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.discover(
(result) => { resolve(result); },
(error) => { reject(error); },
options
);
});
}
/** 指定のペリフェラル端末に write 要求を送信する */
write(options: Object): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.write(
(result) => { resolve(result); },
(error) => { reject(error); },
options
);
});
}
/** 指定のペリフェラル端末に read 要求を送信する */
read(options: Object): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.read(
(result) => { resolve(result); },
(error) => { reject(error); },
options
);
});
}
/** 指定のアドレスのペリフェラル端末との接続を切断する */
disconnect(options: Object): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.disconnect(
(result) => { resolve(result); },
(error) => { reject(error); },
options
);
});
}
/** 指定のアドレスのペリフェラル端末との通信を終了する */
close(options: Object): Promise<any> {
return new Promise((resolve, reject) => {
(window as any).bluetoothle.close(
(result) => { resolve(result); },
(error) => { reject(error); },
options
);
});
}
}
スキャンの開始と終了を自動化する
上述のサービスでは startScan()
というラッパーメソッドを作ったが、startScan
はペリフェラル端末のスキャンを開始するだけで、自動的にスキャンを停止したりしてくれない。そこで、目的のペリフェラル端末を見つけたり、指定秒数以内に見つからなかったりした時にスキャンを停止する処理を盛り込もうと思う。
/** スキャン開始処理 : 引数で指定したアドバタイジング名の端末のアドレスを返却する */
startScan(advertisingName: string): Promise<any> {
return new Promise((resolve, reject) => {
// 探索したペリフェラル端末のアドレスを控えておく退避変数
let address;
// スキャン停止処理を用意する
const stopScan = () => {
(window as any).bluetoothle.isScanning((result) => {
// スキャン中なら停止処理を呼ぶ
if(result.isScanning) {
(window as any).bluetoothle.stopScan((scanResult) => {
// アドレスが取得できていればアドレスを Resolve する
if(address) {
resolve(address);
}
else {
reject('探索失敗');
}
}, (error) => {
reject(error);
});
}
else {
// もしスキャンしていない場合も、アドレスが取得できていればアドレスを Resolve する
if(address) {
resolve(address);
}
else {
reject('探索失敗');
}
}
});
};
// 10秒後にスキャンを停止するタイマーをセットする
const stopScanTimer = setTimeout(() => {
stopScan();
}, 10 * 1000);
// スキャンを開始する
(window as any).bluetoothle.startScan((result) => {
// stopScan() するまでこのコールバック関数が繰り返し呼ばれる
// 指定のアドバタイジング名を探索する
if(result.advertisement && result.advertisement.localName === advertisingName) {
// 退避変数にアドレスを控えておく
address = result.address;
// タイマーを解除した上でスキャンを停止する
clearTimeout(stopScanTimer);
stopScan();
}
}, (error) => {
// スキャン開始に失敗した場合はスキャン停止タイマーを解除して終了する
clearTimeout(stopScanTimer);
reject(error);
});
});
}
一旦コードの前半は飛ばして、startScan
の中身。アドバタイジング名は advertisement.localName
で確認できるので、これが引数で指定した advertisingName
と一致していたら、stopScan()
処理を呼んで終了している。
横着して stopScan()
のネストが深めになっているが、スキャン中ならスキャンを停止するようにしている。Promise を resolve()
するのはこの stopScan()
の中で、退避変数 address
の値を resolve()
するようにしている。これにより、呼び出し側にペリフェラル端末のアドレスが返却されるので、以降でアドレスを指定した処理が呼び出せるというワケ。
startScan
してから一定時間以上経ったら stopScan()
を呼ぶようにするため、setTimeout
を使ったタイマーを定義している。
- ずっと探索中が続いたら、このタイマーにより
stopScan()
が実行されて、恐らくreject()
される。 - 指定のペリフェラル端末が見つかったら、
clearTimeout()
でタイマーを解除し、その上でstopScan()
が呼ばれ、ほぼ確実にresolve()
となる。 - もし
startScan
自体に失敗した場合はエラーコールバックの方でタイマーを解除し、その場でreject()
する。この場合はstopScan()
は実行されない。
タイマーを使った実装は、cordova-plugin-bluetoothle プラグインの作者が AngularJS 向けに作成した ngCordova ラッパーの実装を参考にした。
これでセントラル端末の通信に使う API が用意できた。次回はこれをコンポーネント側から呼び出していく。