こんにちは。共同開発部 門多です。
Bluetooth LE (以降 BLE)連載の第3回です。
今回は iOS デバイスを BLE 機器として使えるようにするために、簡単なアプリを実装していきたいと思います。
第2回で、Texas Instruments CC2541 SensorTag 開発キットを使ってアプリから BLE 通信を行いましたが、
SensorTag と同等の役割を iOS デバイス(のアプリ)にもたせることができます。
iOS デバイスが BLE 機器になりますので、たとえば iOS デバイス同士で通信を行ったり、
Android や Windows からも iOS デバイスと通信することができるようになります。
この記事では簡単に通信ができる程度のサンプルを作って、説明していきたいと思います。
- 第1回 Bluetooth Low Energy の基礎
- 第2回 iOS デバイスで Bluetooth LE 機器を使う
- 第3回 iOS デバイスを Bluetooth LE 機器にする
- 第4回 Windows 8.1 の Windows ストアアプリで BLE を使う
今回作るサンプルアプリの説明
以下の環境でアプリを作っていきます(第2回とだいたい同じですが、iOS デバイスが2台必要です)。
- Xcode 5
- iOS 7 がインストールされた iPhone
- BLExplr 等の BLE 通信ができるアプリがインストールされた iOS デバイス
機能としては、BLExplr で Read / Write できて、
値の変更があったときに接続されている iOS デバイスへ変更が通知できるという仕様にします。
実装
アプリ概要
iOS デバイスを BLE 機器として使うためには、第2回でざっくりと説明しているクラス群から、
主に CBPeripheralManager を使います。
具体的には、CBPeripheralManagerDelegate プロトコルを実装したクラスを用意して、
CBPeripheralManager のイニシャライザへ渡します。
そうすると、BLE が利用可能になる・
他のデバイスから Characteristic の Read を依頼されるといったイベントが発生したとき、
対応する CBPeripheralManager のデリゲートが呼ばれますので、
適切に状態を変更したり、応答を返したりすることによって BLE 通信を行います。
- BLE が準備できたことをデリゲートで受け取る
- 公開するサービスとキャラクタリスティックを準備する
- サービスを CBPeripheralManager に登録する
- アドバタイズを開始して、他の iOS デバイス等から検索可能にする
- 接続された iOS デバイスからの Read / Write を処理する
BLE が準備できたことをデリゲートで受け取る
最初に、CBPeripheralManager のインスタンスを生成しておく必要があります。
// CBPeripheralManagerDelegate の実装を第1引数に渡す self.manager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
BLE の状態が変わったとき、CBPeripheralManagerDelegate の
peripheralManagerDidUpdateState:peripheral: メソッドが呼ばれますので、
state プロパティの値をみて、CBPeripheralManagerStatePoweredOn であることを確認してください。
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { if (peripheral.state == CBPeripheralManagerStatePoweredOn) { } }
公開するサービスとキャラクタリスティックを準備する
CBPeripheralManagerStatePoweredOn が確認できたら、
公開するサービスとキャラクタリスティックを構築して公開します
(構築だけなら、CBPeripheralManagerStatePoweredOn の前に作っておいても構いません)。
まずは値を送受信するキャラクタリスティックを構築します。
// UUID 文字列は uuidgen(1) コマンド等で生成しておく CBUUID *characteristicUUID = [CBUUID UUIDWithString:kValueCharacteristicUUIDString]; // 許可する操作ビットを立てて CBMutableCharacteristic を生成 CBCharacteristicProperties props = CBCharacteristicPropertyRead; Byte value = 0x10; CBMutableCharacteristic *characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID properties:props value:[NSData dataWithBytes:&value length:1] permissions:CBAttributePermissionsReadable];
CBMutableCharacteristic の value が nil 以外なら
値が CoreBluetooth によってキャッシュされます。nil ならキャッシュしません。
この記事の後半で、nil の場合に動的に値を作るような実装を行いますが、
今は固定で設定しておくことにします。
サービスを CBPeripheralManager に登録する
サービスの準備をします。
// UUID 文字列は uuidgen(1) で生成しておく CBUUID *serviceUUID = [CBUUID UUIDWithString:UUIDString]; CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
CBPeripheralManager へ上記で構築したサービスを登録します。
service.characteristics = @[ characteristic ]; self.sampleService = service; [self.manager addService:self.sampleService];
登録完了時には peripheralManager:didAddService:error: デリゲートが呼ばれます。
- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error { if (error) { // エラー処理 } }
今回は、1つのサービスがキャラクタリスティックをひとつだけ提供するアプリなので以上です。
複数のキャラクタリスティックを提供したい場合は、
service.characteristics に提供したいだけの CBMutableCharacteristic を渡せば渡しただけ扱われます。
アドバタイズを開始して、他の iOS デバイス等から検索可能にする
peripheralManager:didAddService:error: でエラーがなければサービスを公開するための準備ができていますので、
startAdvertising: メソッドでアドバタイズを開始します。
NSDictionary *advertising = @{ CBAdvertisementDataLocalNameKey: kLocalName, CBAdvertisementDataServiceUUIDsKey: @[ self.sampleService.UUID, ], }; [self.manager startAdvertising:advertising];
startAdvertising: もアドバタイズの結果を peripheralManagerDidStartAdvertising:error: で受け取ります。
error が nil であれば iOS デバイスが外部に対してアドバタイズを送出している状態になります。
ここまでくれば、BLExplr 等から接続ができる状態になっているはずです。
アドバタイズは、他のデバイスから接続されたからといって止まるものではありません。
今回作成したアプリが他のデバイスから接続されてもアドバタイズは出し続けていますので、
さらに他のデバイスも接続することができる状態のままになっています。
要件によっては、1対1の接続しか許可しないことがあるかもしれませんが、
今回はサンプルなので複数接続が可能のままにしています。
アドバタイズを停止したい場合は CBPeripheralManager の stopAdvertising メソッドを呼んで停止させてください。
接続された iOS デバイスからの Read を処理する
ここまでで、キャラクタリスティックの値を読むことはできていますが、値が変わらなくて面白くないので、
Read するたびに変わるような実装にしてみます。
まず、CBMutableCharacteristic の initWithType:properties:value:permissions: から、
value を nil に変更します。nil にすることで、CoreBluetooth によってキャッシュされることを防ぎ、
peripheralManager:didReceiveReadRequest: デリゲートが通知されるようになります。
ランダムで値を変更してみましょう。
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request { Byte value = arc4random()&0xff; NSData *data = [NSData dataWithBytes:&value length:1]; request.value = data; [self.manager respondToRequest:request withResult:CBATTErrorSuccess]; }
簡単ですね。BLExplr で Read を繰り返せば、値が変更されているのを確認できます。
接続された iOS デバイスからの Write を処理する
Write は、initWithType:properties:value:permissions: で Write を許可する必要があります。
CBCharacteristicProperties で CBCharacteristicPropertyRead だけ指定していましたが、
CBCharacteristicPropertyWrite も追加しなければなりません。
Notification を送るためには、CBCharacteristicPropertyNotify も必要です。
また、CBAttributePermissions も CBAttributePermissionsWriteable を加えておきます。
CBCharacteristicProperties prop = CBCharacteristicPropertyRead| CBCharacteristicPropertyWrite| CBCharacteristicPropertyNotify; CBAttributePermissions perm = CBAttributePermissionsReadable| CBAttributePermissionsWriteable; CBMutableCharacteristic *characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID properties:prop value:nil permissions:perm];
次に、peripheralManager:didReceiveWriteRequest: デリゲートを受け取れるように実装します。
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests { for (CBATTRequest *r in requests) { CBMutableCharacteristic *characteristic = self.sampleService.characteristics[0]; if ([characteristic isEqual:r.characteristic]) { self.currentValue = r.value; [self.manager respondToRequest:r withResult:CBATTErrorSuccess]; [self.manager updateValue:self.currentValue forCharacteristic:characteristic onSubscribedCentrals:nil]; return; } [self.manager respondToRequest:r withResult:CBATTErrorWriteNotPermitted]; } }
updateVaue:forCharacteristic:onSubscrivedCentrals: は、
キャラクタリスティックの値に変更があったことを Notification で接続先へ通知するメソッドです。
第3引数を nil にすると、Notification 受信要求を受け付けている Central すべてに通知を行います。
Read では、最後に Write したデータを返すように変更します。
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request { request.value = self.currentValue; [self.manager respondToRequest:request withResult:CBATTErrorSuccess]; }
BLExplr で Write したあとに Read すると、書いた値が参照できるようになっています。簡単ですね。
BLExplr の Enable Notify ボタンで Notification を受ける様にしたあとで、
Write をするとすぐに値が更新されるのが分かると思います
(Notification を投げていない場合は BLExplr で Read しなければ値が更新されません)。
また、余談ですが、
Assertion failure in [CBConcretePeripheralManager addService:] …のようなエラーが発生する場合、
initWithType:properties:value:permissions: で与えた値の組み合わせがおかしいことが考えられます。
実際には考慮すべき事
updateValue:forCharacteristic:onSubscribedCentrals: は BLE 通信が混在している場合に NO を返します。NO であれば再送が必要です。
再送可能になったなら peripheralManagerIsReadyToUpdateSubscribers: デリゲートで通知されます。
まとめ
一通り、BLE 機器としてふるまうアプリを作成しましたが、いかがだったでしょうか?
今回の記事では書いていませんが、第2回で作成したアプリと今回のアプリで通信を行えるように機能追加をするのは容易ですので、
興味がありましたらお試しください。以下の動画のようになります。
次回は、Windows 8.1 での BLE 通信を取り扱う予定です。