よいちろ日記

忘れないようにメモ。

CoreBluetoothをバックグラウンドで動かすiOSアプリ作った。

サンプルコード

iOSでCoreBluetooth使って、Bluetooth通信する基本的なアプリを作った。
サンプルコードはここ。(他のクラスメソッドもいろいろ混じっているけど...)
スキャン開始ボタンだとかUI部分は今回は省略。
 

大まかな流れ

① PeripheralにAdvertiseさせる。(ハード側の設定済み前提。)
② CentralManagerを準備する。
③ Advertiseされている情報をCentral側でスキャンする。
④ PeripheralとCentralでハンドシェイク。
⑤ PeripheralのService情報やCharacteristics情報を取得。
⑥ やりたいことに応じて、Peripheralの該当ServiceのCharacteristicsに情報書き込み。
⑦ 書き込みできたことをPeripheral側で確認。(ハード側でLEDを光らせるとかして。)
 
これをCoreBluetoothを使って実際に実装するとどうなるかを書いていく。
特にバックグラウンドでの通信部分でハマったので忘れないようにメモ。
 
Bluetoothの接続周りのメソッドはすでにデリゲートメソッドとして用意されている。
雪崩式にメソッドを呼び出していくので、どこでどうやって次のステップに進んでいるかを把握するのがコツ。
 
BluetoothConnectionクラスを定義して、BluetoothConnection.h, BluetoothConnection.mとヘッダーファイルと実装ファイルに書いていく。
 

実装メモ

① PeripheralにAdvertiseさせる。(ハード側の設定済み前提。)

事前にどのようなパケットをAdvertiseさせるかを決めていると思うので、それをヘッダーファイルに定義。(既存の製品を使う場合は調べて、それを書く。)
Bluetooth機器のAdvertising情報、ServiceUUID、CharacteristicsUUID調べる時には、以下のアプリが便利。
 
BluetoothConnection.hには以下のように記述。(一部省略)
#import <Foundation/Foundation.h>
//CoreBluetoothのライブラリを読み込み。
#import <CoreBluetooth/CoreBluetooth.h>
 
//事前に調べた(Peripheralを自分で作った場合は自分で設定した)UUIDを定義。
#define kBatteryServiceUUID     @"180F" //これは割りとどのBluetooth機器でも共通らしい。
#define kBatteryCharUUID        @"2A19"
#define kScratch1ServiceUUID    @"A495FF20-C5B1-4B44-B512-1370F02D74DE"
#define kScratch1CharUUID       @"A495FF21-C5B1-4B44-B512-1370F02D74DE"
#define kAdServiceUUID          @"A495FF10-C5B1-4B44-B512-1370F02D74DE"
 
@interface BluetoothConnection : NSObject
 
//シングルトンのオブジェクトを宣言。
+ (id)sharedManager;
 
//セントラルマネージャのプロパティ宣言。
@property(nonatomic)BOOL isCentralBluetoothPoweredOn;
@property(nonatomic)BOOL isCentralScanning;
@property(nonatomic)BOOL isCentralConnectedToPeripheral;
 
//ペリフェラルのプロパティを宣言。
@property(nonatomic)int peripheralBatteryLevel;
@property(nonatomic)int peripheralScratch1Data;
 
//デリゲートプロパティを宣言。
//デリゲートプロパティを使用する際には、BTNotificationというプロパティ名を使用する必要がある。
@property(nonatomic, assign)id <bluetoothNotificationDelegate> BTNotification;
//他クラス(主にViewController)から呼び出すメソッドを宣言。
-(void)startScanning;
-(void)stopScanning;
-(void)disconnect;
-(void)disconnectIntrinsic;
-(void)changeScratch1Characteristics:(NSInteger *)state;
-(int)getBluetoothConnectionState;
-(void)callNotificationDelegateMethod:(NSString *)notificationMessage;
@end
 
//CoreBluetoothクラスには接続に関する諸々のデリゲートメソッドが用意されていて、それらを使用する。
//その際に必要になるCoreBluetooth用の変数を宣言。
@interface BluetoothConnection() <CBCentralManagerDelegate, CBPeripheralDelegate>
@property(nonatomic,strong)  CBCentralManager *centralManager;
@property(nonatomic,strong)  CBPeripheral *peripheral;
@property(nonatomic,strong)  CBUUID *batteryServiceUUID;
@property(nonatomic,strong)  CBUUID *batteryServiceCharacteristicsUUID;
@property(nonatomic,strong)  CBUUID *scratch1ServiceUUID;
@property(nonatomic,strong)  CBUUID *scratch1ServiceCharacteristicsUUID;
@property(nonatomic,strong)  CBUUID *adServiceUUID;
@property(nonatomic,strong)  CBCharacteristic *batteryServiceCharacteristics;
@property(nonatomic,strong)  CBCharacteristic *scratch1ServiceCharacteristics;
@end
シングルトンのオブジェクトを宣言している部分がバックグラウンドでのBluetooth通信にはとても重要。これがわからなくて長いことハマった。
Scratchというのは、LightBlue Beanに用意されている、Characteristics書き込み用の番地名のようなもの。
簡略化して書くと、Peripheral→ServiceUUID→CharacteristicsUUID→Characteristicsという構造になっている。
 

② CentralManagerを準備する。

//シングルトンインスタンス生成。
+ (id)sharedManager {
    static id instance = nil;
 
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
        [instance initInstance];
    });
 
    return instance;
}
 
- (void)initInstance {
    //シングルトンインスタンスをself(BluetoothConnectionクラス)で生成し、
    //そこに紐づくプロパティへ、Core Bluetooth関連の変数を代入。
    if (self) {
        _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
        _batteryServiceUUID = [CBUUID UUIDWithString:kBatteryServiceUUID];
        _batteryServiceCharacteristicsUUID = [CBUUID UUIDWithString:kBatteryCharUUID];
        _scratch1ServiceUUID = [CBUUID UUIDWithString:kScratch1ServiceUUID];
        _scratch1ServiceCharacteristicsUUID = [CBUUID UUIDWithString:kScratch1CharUUID];
        //バックグラウンドでスキャンするときは、サービス指定が必要。それ用。
        _adServiceUUID = [CBUUID UUIDWithString:kAdServiceUUID];
    }
}
dispatch_onceして、アプリ立ち上げで一度のみインスタンス化。自動的に破棄されない、複数クラス間で共有するときにいろいろ考えなくてもよいというメリットがある。
Bluetoothをバックグラウンドで動かしたい場合、シングルトンじゃないと、アプリがバックグラウンドに行った時点でBluetoothCentralManagerが開放されてしまっている(?)らしく、うまくいかない。
 
また、CoreBluetoothではバックグラウンドスキャンするときは、対象サービスをしていしないとエラーになる。
少なくとも1つは、PeripheralがAdvertiseしているServiceUUIDが必要。

f:id:yoichiro0903:20160521171335j:plain

 

③ Advertiseされている情報をCentral側でスキャンする。

//ViewControllerから呼び出されて、scanForPeripheralsWithServicesを開始。
-(void)startScanning
{
    if (!self.isCentralBluetoothPoweredOn) {
        return;
    }
    if (self.isCentralScanning) {
        return;
    }
    NSDictionary *scanOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:CBCentralManagerScanOptionAllowDuplicatesKey];
    //バックグラウンドでスキャンしたい場合、scanするUUIDを指定する必要がある。
    //scanするUUIDを指定しない場合:[_centralManager scanForPeripheralsWithServices:nil options:scanOptions];
    NSArray *serviceUUIDArray = [NSArray arrayWithObjects:_adServiceUUID, nil];
 
    //ペリフェラルを_adServiceUUIDでスキャン開始。ペリフェラルが見つかると、didDiscoverPeripheralが実行される。
    [_centralManager scanForPeripheralsWithServices:serviceUUIDArray options:scanOptions];
    self.isCentralScanning = YES;
    NSLog(@"%s","Central started scanning...");
}
isCentral…的な変数は、Centralのステータスを格納している。端末のBluetoothがオンになっているかどうかとか。
ハード的に何をしているかがブラックボックスではあるが、接続が完了すると、didDiscoverPeripheralメソッドが呼び出される。
 
//iphoneBluetoothがどうなっているかを都度検知する。アプリが起動するとまず実行される。
-(void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch ([_centralManager state]) {
        case CBCentralManagerStatePoweredOff:
            self.isCentralBluetoothPoweredOn = NO;
            self.isCentralScanning = NO;
            self.isCentralConnectedToPeripheral = NO;
            NSLog(@"CBCentralManagerStatePoweredOff");
            break;
        case CBCentralManagerStatePoweredOn:
            self.isCentralBluetoothPoweredOn = YES;
            NSLog(@"CBCentralManagerStatePoweredOn");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@"CBCentralManagerStateResetting");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@"CBCentralManagerStateUnauthorized");
            break;
        case CBCentralManagerStateUnknown:
            NSLog(@"CBCentralManagerStateUnknown");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@"CBCentralManagerStateUnsupported");
            break;
    }
}
端末のBluetooth状態を都度チェックするためのメソッドcentralManagerDidUpdateState。
 

④ PeripheralとCentralでハンドシェイク。

-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    if (_peripheral != nil) {
        NSLog(@"%s", "Already Discoverd peripheral.");
        return;
    }
    NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
    if (localName != nil) {
        _peripheral = peripheral;
 
        NSLog(@"RSSI : %@",RSSI);
        NSLog(@"Device name is %@", localName);
        //didConnectPeripheralを呼び出す。
        //NSLog(@"Discovered %@", peripheral.name);という形で見つかったペリフェラルのリストを吐き出す。
        [central connectPeripheral:_peripheral options:nil];
        [self stopScanning];
    } else {
        NSLog(@"Device name is nil. localName:%@", localName);
    }
}
advertisementDataパラメータは発見されたデバイスの情報がNSDictionary型で格納されている。(iPhoneアプリでBluetooth通信を使うための基礎知識
connectPeripheralメソッド呼び出しで、didConnectPeripheralが呼び出される。
 
 

⑤ PeripheralのService情報やCharacteristics情報を取得。

-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    _peripheral.delegate = self;
    self.isCentralConnectedToPeripheral = YES;
    NSLog(@"did connect peripheral");
    //didDiscoverServicesを呼び出す。
    [peripheral discoverServices:[NSArray arrayWithObjects:_batteryServiceUUID, _scratch1ServiceUUID, nil]];
}
Peripheralとの接続が完了すると、次はServiceUUIDを指定してPeripheral内を探索し始める。今回は電池情報とScratch情報を格納しているサービスを探索。
 
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    for (CBService *service in peripheral.services) {
        if ([service.UUID.data isEqualToData:_batteryServiceUUID.data]) {
            //_batteryServiceCharacteristicsUUIDをスキャンする。
            //didDiscoverCharacteristicsForServiceを呼び出す。
            [peripheral discoverCharacteristics:[NSArray arrayWithObjects:_batteryServiceCharacteristicsUUID, nil] forService:service];
            NSLog(@"%s", "Discovered battery service");
        } else if ([service.UUID.data isEqualToData:_scratch1ServiceUUID.data]) {
            //_scratch1ServiceCharacteristicsUUIDをスキャンする。
            //didDiscoverCharacteristicsForServiceを呼び出す。
            [peripheral discoverCharacteristics:[NSArray arrayWithObjects:_scratch1ServiceCharacteristicsUUID, nil] forService:service];
            NSLog(@"%s", "Discovered scratch1 service");
        }
        NSLog(@"service : %@", service);
    }
}
discoverServicesで探索したサービスが見つかったら、CharacteristicsUUIDを指定して探索開始。
 
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    if ([service.UUID.data isEqualToData:_batteryServiceUUID.data]) {
        _batteryServiceCharacteristics = [self findCharacteristics:service.characteristics uuid:_batteryServiceCharacteristicsUUID];
        //didUpdateValueForCharacteristicを呼び出す。
        [peripheral readValueForCharacteristic:_batteryServiceCharacteristics];
        NSLog(@"%s", "Discovered battery characteristics");
    } else if ([service.UUID.data isEqualToData:_scratch1ServiceUUID.data]) {
        _scratch1ServiceCharacteristics = [self findCharacteristics:service.characteristics uuid:_scratch1ServiceCharacteristicsUUID];
        //didUpdateValueForCharacteristicを呼び出す。
        [peripheral readValueForCharacteristic:_scratch1ServiceCharacteristics];
        NSLog(@"%s", "Discovered scratch1 characteristics");
    }
}
service.UUID.dataをPeripheralからすべて取得して、指定した_batteryServiceUUID.data、_scratch1ServiceUUID.dataと一致するものがあったら、
Characteristicsを読み込みに行く。見つけたCharacteristicsを引数にして、didUpdateValueForCharacteristicメソッドを呼ぶ。
 
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    uint8_t b;
    if (characteristic == _batteryServiceCharacteristics) {
        [characteristic.value getBytes:&b length:1];
        self.peripheralBatteryLevel = b;
        NSLog(@"Battery level is %d" , b);
    } else if (characteristic == _scratch1ServiceCharacteristics){
        _scratch1ServiceCharacteristics = characteristic;
        NSLog(@"Found Scratch Chara: %@", _scratch1ServiceCharacteristics);
    }
}
格納されているデータ型に注意する。単純にStringとかではない。
 
 

⑥ やりたいことに応じて、Peripheralの該当ServiceのCharacteristicsに情報書き込み。

-(void)changeScratch1Characteristics:(NSInteger *)state
{
    NSMutableData *data;
    ushort value; 
    value = 1; //for test
    data = [NSMutableData dataWithBytes:&value length:2];
 
    //ここでcharacteristicを書き込み。
    [_peripheral writeValue:data forCharacteristic:_scratch1ServiceCharacteristics type:CBCharacteristicWriteWithResponse];
    NSLog(@"Wrote to scratch1 value of : %d chara:%@", value, _scratch1ServiceCharacteristics);
}
試しにLightBlue BeanのScratchデータを書き換えるメソッドを書いた。Scratchは5つまで用意されている。
  

⑦ 書き込みできたことをPeripheral側で確認。(ハード側でLEDを光らせるとかして。)

ここはBluetooth機器に別途確認用の動作を実装しておくか、LightBlue Explorer アプリで覗いてみることで確認。
 

その他

特にバックグラウンドでのBluetooth通信に関して書くと、BluetoothConnectionクラスCentralオブジェクトはシングルトンで生成しないとバックグラウンドに行った時点でCentralインスタンスがうんともすんとも言ってくれなかった。ここがキーだった気がする。もちろん普通にplistとかの設定も別途必要。といってもポチポチするだけだけど。

f:id:yoichiro0903:20160521173449p:plain

 
 
またAppDelegate.mにて、
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
 
    NSLog(@"Called application:performFetchWithCompletionHandler:");
    _BT = [[BluetoothConnection alloc] init];
    [_BT startScanning];
    [_BT changeCharacteristics];
 
    // Download complete.
    completionHandler(UIBackgroundFetchResultNoData);
}
ってやっていたんだけど、バックグラウンド状態でイニシャライズされたインスタンスがちゃんと保持されておらず何も起こらなかった。
 
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
 
    NSLog(@"Called application:performFetchWithCompletionHandler:");
    [[BluetoothConnection sharedManager]startScanning];
 
    // Download complete.
    completionHandler(UIBackgroundFetchResultNoData);
}
ってしたらうまく行った。
あくまでバックグラウンドで定期的にフェッチするためのものなので、通信が切れない前提ならいらない。
 
あと、初回いきなりだったり久しぶりにバックグラウンドで通信しようとするとうまく接続されないので、その辺りはUIでうまく調整する必要がある。まあ、ハード側と接続がうまく行っているか確認してください、みたいな説明出すんだろうな。
 

参考にさせていただいたサイト、書籍