Developer's Blog

【連載】Bluetooth LE (3) iOS デバイスを Bluetooth LE 機器にする

top

こんにちは。共同開発部 門多です。

Bluetooth LE (以降 BLE)連載の第3回です。
今回は iOS デバイスを BLE 機器として使えるようにするために、簡単なアプリを実装していきたいと思います。
第2回で、Texas Instruments CC2541 SensorTag 開発キットを使ってアプリから BLE 通信を行いましたが、
SensorTag と同等の役割を iOS デバイス(のアプリ)にもたせることができます。

iOS デバイスが BLE 機器になりますので、たとえば iOS デバイス同士で通信を行ったり、
Android や Windows からも iOS デバイスと通信することができるようになります。
この記事では簡単に通信ができる程度のサンプルを作って、説明していきたいと思います。

今回作るサンプルアプリの説明

以下の環境でアプリを作っていきます(第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 通信を行います。

  1. BLE が準備できたことをデリゲートで受け取る
  2. 公開するサービスとキャラクタリスティックを準備する
  3. サービスを CBPeripheralManager に登録する
  4. アドバタイズを開始して、他の iOS デバイス等から検索可能にする
  5. 接続された 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 等から接続ができる状態になっているはずです。

BLExplr で Read した画像

アドバタイズは、他のデバイスから接続されたからといって止まるものではありません。
今回作成したアプリが他のデバイスから接続されてもアドバタイズは出し続けていますので、
さらに他のデバイスも接続することができる状態のままになっています。
要件によっては、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 通信を取り扱う予定です。

フェンリルの開発者アカウントでは、フェンリルプロダクトの最新開発情報などをつぶやいています。よろしければフォローしてください!

 

Copyright © 2019 Fenrir Inc. All rights reserved.