Developer's Blog

【連載】Bluetooth LE (2) iOS デバイスで Bluetooth LE 機器を使う

BluetoothLESample

こんにちは。共同開発部 開発担当の図子です。

先週 からはじまりました Bluetooth LE (以降 BLE)の連載。今回は iOS デバイスで BLE 機器と通信できるようにするために簡単なアプリを作って説明していきたいと思います。

CoreBluetooth Framework

iOS で BLE を通信をするためには CoreBluetooth.framework を使います。CoreBluetooth は iOS 5 から登場したフレームワークで BLE 通信を行うためのフレームワークです。登場当初は既存の BLE 通信が可能な機器との通信しか出来ませんでしたが、iOS 6 からは iOS 自身が BLE 機器として振る舞えるようにするためのクラスも追加されました。機器との通信は非同期で行われます。非同期処理のやりとりは Delegate パターンが使われています。

CoreBluetooth に登場するクラスは主に以下のようになっています。

  • iOS 5 から
    • CBCentralManager
    • CBService
    • CBCharacteristic
    • CBDescriptor
    • CBPeripheral
    • CBUUID
  • iOS 6 から追加
    • CBCentral
    • CBPeripheralManager
    • CBATTRequest
    • CBMutableCharacteristic
    • CBMutableDescriptor
    • CBMutableService

iOS 6 から追加されたクラスに関しては今後別のエントリとして紹介しようと思います。今回は iOS 5 から存在するクラスについて触れてみたいと思います。

CBCentralManager

CBCentralManager クラスは iOS と BLE 機器と接続するための要となるクラスです。主なタスクとしては、

  • BLE 機器の検索
  • BLE 機器との接続

があります。

CBCentralManager に関する Delegate は CBCentralManagerDelegate として定義されています。

CBPeripheral

CBPeripheral は BLE 機器を表すクラスです。BLE 機器に対する以下の主なタスクを行います。 CBPeripheral に関する Delegate は CBPeripheralDelegate として定義されています。

  • Service の検索
  • Characteristic の検索
  • Descriptor の検索
  • Characteristic, Descriptor への書き込み
  • Characteristic, Descriptor からの読み出し
  • Notification の受信

CBService

CBService は先週の記事でも紹介されていた「サービス」を表すクラスです。CBPeripheral にぶら下がるモデルクラスという位置づけです。

CBCharacteristic

CBCharacteristic も先週の記事でも紹介されていた「キャラクタリスティック」を表すクラスです。CBService にぶら下がるモデルクラスという位置づけです。具体的に通信する対象となります。

CBDescriptor

CBDescriptor は CBCharacteristic のさらに下層にいるモデルクラスです。あまり使うシチュエーションは少ないかも知れないですがさらなる詳細な情報を取得できます。

CBUUID

CoreBluetooth 用の UUID クラスです。特徴として 16-bit UUID を 128-bit UUID に変換してくれます。 NSUUID クラスとは別のクラスですので混同しないように気をつけましょう。

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

以下の環境でアプリを作ってみたいと思います。

  • Xcode 5
  • iOS 7 がインストールされた iPhone
  • Texas Instruments CC2541 SensorTag 開発キット

はじめということで SensorTag のボタン押下状態を取得し表示する最低限のシンプルな機能にしたいと思います。

  • SensorTag と接続
  • SensorTag の左右のボタンが押されたらその状態を iPhone 上に表示

主なステップは以下の通りです。

  1. SensorTag の検索
  2. SensorTag と接続
  3. SensorTag の KeyPress Service の検索
  4. SensorTag の KeyPress Service の Characteristic の検索
  5. Characteristic からの Notification の受信要求
  6. Notification を受信してデータの取得

実装

SensorTag の検索の前の準備

SensorTag の検索をするためには CBCentralManager が必要です。

/// BLE 通信全般の処理が実行される queue を第 2 引数としてわたす。nil を渡した場合は mainQueue で実行される。
_centralManagerSerialGCDQueue = dispatch_queue_create("jp.co.fenrir.BLESample.centralmanager", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:_centralManagerSerialGCDQueue];

/// 接続するペリフェラルを保持するためのセットを生成
self.peripherals = [NSMutableSet set];

CBCentralManager のイニシャライザ initWithDelegate:queue: の第 1 引数には Delegate をセットします。第 2 引数には GCD の queue を渡します。ここで渡した queue 上で BLE 通信全般の処理が実行されます。nil を渡した場合は main queue が使われるようになります。main queue の場合、通信が頻繁になってくると少なからず UI 側の処理に影響がでてきます。かといって concurrent queue を使うと機器からのデータが完全に並列で飛んでくることも考えられます。これらのバランスをみてここでは seriral queue を生成して渡しています。

また、この段階で接続するペリフェラルを保持するための Set を生成しておきます。検索し見つかったペリフェラル(BLE 機器)に対して接続を行う際にペリフェラルのインスタンスを自分で保持しておかないとそのペリフェラルが解放されてしまいます。

SensorTag の検索

BLE 機器の検索を行います。検索には scanForPeripheralsWithServices:options: メソッドを使います。第 1 引数には任意のサービスを持った機器のみを対象としたい場合にはそのサービスの CBUUID を作り配列として渡します。この指定を行うことで不要な機器の検出を抑止できます。指定しない場合には nil を渡します。第 2 引数はオプションです。Scan 時にペリフェラルの重複を許可したりできます。

/// Scan 時に任意の Service を持っている機器だけを探すように指定できるが SensorTag は Advertise に Service 情報を載せてくれていないため、ここでは nil を指定。
[self.centralManager scanForPeripheralsWithServices:nil options:options];

SensorTag との接続

検索をはじめ、ペリフェラルが見つかると CBCentralManagerDelegate の centralManager:didDiscoverPeripheral:advertisementData:dRSSI が呼ばれます。ここでは見つかったペリフェラルの CBPeripheral インスタンスや取得出来たアドバタイズデータやRSSI(電波強度)などが取得出来ます。

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
    if ([localName length] && [localName rangeOfString:@"SensorTag"].location != NSNotFound) {

        [self updateLabel:self.connectionStatusLabel text:@"接続開始"];

        /// Scan を停止させる
        [self.centralManager stopScan];

        /// CBPeripheral のインスタンスを保持しなければならない
        [self.peripherals addObject:peripheral];

        [self.centralManager connectPeripheral:peripheral options:nil];
    }
}

要件によっては、見つかったペリフェラルの一覧を画面として表示しユーザーに選択させるようなこともあるかと思いますが、 上の例では見つかったペリフェラルのアドバタイズデータに含まれる名称が "SensorTag" だった場合に無条件で接続しにいくような処理を行っています。

目的の機器が見つかったら接続を行います。その前に Scan が不要な状況であればここで stopScan を呼んで Scan を止めてください。明示的のこちらから止めに行かないと Scan は止まりません。すでに触れていますが、検索し見つかったペリフェラル(BLE 機器)に対して接続を行う際にペリフェラルのインスタンスを自分で保持しておかないとそのペリフェラルが解放されてしまいますので Set に追加しておきます。接続は connectPeripheral:options: で行います。第 1 引数には接続対象のペリフェラル、第 2 引数はオプションです。オプションは Background 動作を指定している時の振る舞いのオプションです。今回は Background 動作は指定せずに行っているため nil を渡しておきます。

SensorTag の KeyPress Service の検索

接続が無事に終わると centralManager:didConnectPeripheral: が呼ばれます。これで接続処理自体は完了です。ただし、この段階ではまだ具体的な通信はできません。ここから探索の旅がはじまります。ペリフェラルが持つサービスを探し、さらにキャラクタリスティックを探す必要があります。まずはサービスを探します。

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    [self updateLabel:self.connectionStatusLabel text:@"Service検索中"];

    /// Peripheral にデリゲートをセットし、Service を探す。今回はボタン押下のサービスのみを探す
    peripheral.delegate = self;
    [peripheral discoverServices:@[[CBUUID UUIDWithString:@"ffe0"]]];
}

このように、接続が完了したらここからは CBPeriperal とのやりとりを行います。CBPeripheral のコールバックを Delegate として受けるためにまずペリフェラルの Delegate をセットします。その後にサービスを探します。discoverServices: で探したいサービスの CBUUID インスタンスの配列を渡します。今回はボタン操作の状態変化を知りたいだけなのでそのサービスの UUID を渡しています。SensorTag のサービスやキャラクタリスティック等の仕様はこちらに記載されています。

SensorTag の KeyPress Service の Characteristic の検索

探しているサービスが見つかったら peripheral:didDiscoverServices: が呼ばれます。サービスが見つかったあとはキャラクタリスティックを探します。discoverCharacteristics:forService: を呼ぶことでキャラクタリスティックを探します。第 1 引数には探したいキャラクタリスティックの CBUUID の配列を渡します。特に指定が無い場合は nil を渡します。第 2 引数はどのサービスのキャラクタリスティックか?という情報を渡します。サービスはここでは探索済みなので CBPeripheral のインスタンスのプロパティ services で取得出来ます。以下の例では取得済みの全てのサービスの全てのキャラクタリスティックを探すようにしています。

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    [self updateLabel:self.connectionStatusLabel text:@"Characteristic検索中"];

    /// Characteristic を全て探す
    for (CBService * service in peripheral.services) {
        [peripheral discoverCharacteristics:nil forService:service];
    }
}

Characteristic からの Notification の受信要求

探しているキャラクタリスティックが見つかったら peripheral:didDiscoverCharacteristicsForService:error: が呼ばれます。ここまでで探索の旅は終わりです。最後に Notification を受信出来るようにします。ここでいう Notofication は NSNotification でも APNs とかでもない BLE の Notification です。SensorTag のボタン状況は Notification として機器側がから発信されます。

Notification の受信のためには CBPeripheral の setNotifyValue:forCharacteristic: を呼び出します。第 1 引数には BOOL を渡します。受信したい場合には YES, 不要ならば NO です。第 2 引数にはその対象となるキャラクタリスティックを渡します。

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    [self updateLabel:self.connectionStatusLabel text:@"Notifyセット"];

    /// Characteristic に対して Notify を受け取れるようにする
    for (CBService *service in peripheral.services) {
        for (CBCharacteristic *characteristic in service.characteristics) {
            [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        }
    }
}

setNotifyValue:forCharacteristic: の要求が完了したら peripheral:didUpdateNotificationStateForCharacteristic:error: が呼び出されます。

Notification を受信してデータの取得

これでやっと SensorTag とのコミュニケーションがとれるようになりました。SensorTag の上部の2つのボタンが押されると SensorTag は Notification を発信します。受信出来た場合には peripheral:didUpdateValueForCharacteristic:error: が呼ばれます。このデリゲートメソッドは Notification を受信したときや、今回説明しませんでしたが read を行った場合にも呼ばれます。今回は Notification しか使っていませんが read を行いたい場合は readValueForCharacteristic: で read リクエストを送り、完了時に呼ばれる Delegate は 先ほど説明したように Notification の時と同じ peripheral:didUpdateValueForCharacteristic:error: です。write を行いたい場合は writeValue:forCharacteristic:type: を使います。完了時に呼ばれる Delegate は peripheral:didWriteValueForCharacteristic:error: です。

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    /** 
     Sensor Tag の場合 ボタンから
      * <00>
         * ボタンが離された
      * <01>
         * 右ボタンが押された
      * <02>
         * 左ボタンが押された
     のいずれかの NSData が返ってくる
     */

    UInt8 keyPress = 0;
    [characteristic.value getBytes:&keyPress length:1];

    NSString *text = @"押されていない";
    if (keyPress == 1) {
        text = @"右";
    } else if (keyPress == 2) {
        text = @"左";
    } else if (keyPress == 3) {
        text = @"両方";
    }

    /// BLE の通信がバックグラウンドスレッドメインスレッドでラベルの更新
    [self updateLabel:self.pressedButtonLabel text:text];
}

データの取得はキャラクタリスティックの value プロパティで取得できます。取得した NSData をみてどのボタンが押されたのか離されたのかを判定しています。

完成するとこのような感じです

実際には考慮するべき事

今回は簡単なサンプルにするため、本来配慮するべき事を省略しています。

  • 基本的に CoreBluetooth 側では時間のかかる非同期処理でもタイムアウトを持っていないため自分でタイムアウトを NSTimer などを使って実装する必要がある
  • peripheral:didUpdateValueForCharacteristic:error: など複数のデータが飛んでくる場所ではどの情報が飛んできたのか振り分けを行う必要がある
  • 各種メソッドのエラー処理
  • Bluetooth の On/Off 切り替わり

参考リンク

CoreBluetooth

SensorTag

サンプルコード、ライブラリ

まとめ

少し駆け足でしたが一通り BLE 機器と接続し、機器からのデータを iOS で受信出来るようになりました。SensorTag にはボタン押下以外にも様々なセンサーが付いています。これらを活用して何か作ってみるのもおもしろいかも知れません。次回は iOS デバイスを BLE 機器として扱えるようにする実装をご紹介する予定です。SensorTag を持っていない方でも BLE での通信を体験できるようになるのでお楽しみください。


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

Copyright © 2019 Fenrir Inc. All rights reserved.