vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

MacとiOSをBLEで連携 - Peripheral編

諸事情により、OpenCVはいったんお休みして、MaciPhoneBluetooth Low Energy(BLE)で連携してみることにしました。

やりたいこと

MaciPhoneをBLEでつなぎ、iPhoneはボタンを押したときに、Macは一定時間(1秒)ごとに乱数を生成してそれぞれ送信しあう。

前提

  • BLEを使用するために、MacではIOBluetooth.frameworkを、iOSではCoreBluetooth.frameworkを使用します。
  • BLEで連携を行う場合、2台の端末はCentralとPeripheralに別れ、今回はiPhoneをCentralに、MacをPeripheralとしています。

Central : Peripheral端末の探索、書き込み・読み込みリクエストの送信などを行う。 Peripheral : Centralが探索可能にするためAdvertisingを行ったり、リクエストへの応答などを行う。

Peripheral

今回はMac側、Peripheralについて。
BLEのコントロール部分はPeripheralControllerで、UIなどその他の部分はAppDelegateで実行しています。

PeripheralController.h

#import < Foundation / Foundation.h >
@interface PeripheralController : NSObject
- (void) initPeripheralController;
- (void) close;
- (NSString *) getCentralValue;
- (BOOL) updatePeripheralValue:(int) intSendData;
@end

PeripheralController.m

#import < IOBluetooth / IOBluetooth.h >
#import "PeripheralController.h"

#define CHARACTERISTIC_UUID @"7F855F82-9378-4508-A3D2-CD989104AF22"
#define SERVICE_UUID @"2B1DA6DE-9C29-4D6C-A930-B990EA2F12BB"

@interface PeripheralController() < CBPeripheralManagerDelegate >
@property (strong, nonatomic) CBPeripheralManager       *cpmPeripheralManager;
@property (strong, nonatomic) CBMutableCharacteristic   *cmcCharacteristic;
@property (strong, nonatomic) NSMutableData             *mdtSendValue;
@property (strong, nonatomic) NSString                  *strGotValue;
@property (nonatomic) BOOL                              isSubscribed;
@end
@implementation PeripheralController
- (void) initPeripheralController
{
    // PeripheralManagerの初期化. Delegateにselfを設定し、起動時にBluetoothがOffならアラートを表示する.
    _cpmPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:@{CBPeripheralManagerOptionShowPowerAlertKey:@YES}];
    
    _isSubscribed = NO;
    _strGotValue = @"";
}
- (void) close
{
    // Advertisingをストップ.
    [self.cpmPeripheralManager stopAdvertising];
}
- (NSString *) getCentralValue
{
    // Centralの書き込みリクエストで受け取った値を返す.
    return _strGotValue;
}
// Bluetoothの状態が変わったら実行される.
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
    // BluetoothがOffならリターン.
    if (peripheral.state != CBPeripheralManagerStatePoweredOn) {
        return;
    }
    // Characteristicの初期化. ここで設定したIDをもとにCentralが検索する.
    // Centralからの読み出し、書き込みを可能にする.
    _cmcCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:CHARACTERISTIC_UUID]
                                                                     properties:(CBCharacteristicPropertyNotify|CBCharacteristicPropertyRead|CBCharacteristicPropertyWrite)
                                                                          value:nil
                                                                    permissions:(CBAttributePermissionsReadable|CBAttributePermissionsWriteable)];
    
    // Serviceの初期化. ここで設定したIDをもとにCentralがServiceを検索する.
    CBMutableService *cmsService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:SERVICE_UUID]
                                                                       primary:YES];
    
    // ServiceのCharacteristicを設定する.
    cmsService.characteristics = @[_cmcCharacteristic];
    
    // PeripheralManagerにServiceを追加する.
    [_cpmPeripheralManager addService:cmsService];
    
    // Advertisingの開始.Centralから探索可能にする.
    [_cpmPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : @CBUUID UUIDWithString:SERVICE_UUID }];

}
// Peripheralで設定した値を更新したら、Centralに通知がいくようにする(Centralからのリクエストで実行).
- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic
{
    _isSubscribed = YES;
}
- (BOOL) updatePeripheralValue:(int) intSendData
{
    if(_isSubscribed)
    {
        // Centralからの読み出しリクエストのための値を更新する.
        _mdtSendValue = (NSMutableData *)[[NSString stringWithFormat:@"%d", intSendData] dataUsingEncoding:NSUTF8StringEncoding];
        if([_cpmPeripheralManager updateValue:_mdtSendValue forCharacteristic:_cmcCharacteristic onSubscribedCentrals:nil])
        {
            return YES;
        }
    }
    return NO;
}
// Centralから書き込みリクエストを受けたら実行.
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
{
    for (CBATTRequest *rqs in requests) {
        if ([_cmcCharacteristic isEqual:rqs.characteristic])
        {
            _strGotValue = [[NSString alloc] initWithData:rqs.value encoding:NSUTF8StringEncoding];
            
            [_cpmPeripheralManager respondToRequest:rqs
                                withResult:CBATTErrorSuccess];
        }
    }
}
@end

UUID

連携をするときにCentral端末はPeripheral端末を見つけたあと、[Characteristic]と[Service]を探索します。そのときに必要になるのがUUIDです。

ただ数字を羅列すれば良いわけではないのですが、Terminalを開いて「uuidgen」とコマンドを入力すれば自動生成されます。便利です。

このIDは接続する2台の端末で一致している必要があります。

リクエスト

Central端末からリクエストがあった場合に応答します。今回は書き込み(write)と通知(Nortify)を使用しています。

書き込みリクエストがあった場合はdidReceiveWriteRequestsが、通知リクエストはdidSubscribeToCharacteristicが実行されます。

また、通知リクエストは値を更新すると(updatePeripheralValueで実行)、Central端末に自動で通知が届き、更新した値を渡すことができます。

なお、Central端末からリクエストできるようにするため、権限を設定しておく必要があります(peripheralManagerDidUpdateStateの、_cmcCharacteristicの初期化時に実行)。

課題

  • initPeripheralControllerでCBPeripheralManagerの初期化時に、Appを起動したときにアラートが表示されるようオプションを追加していますが、Bluetoothオフの状態で実行してもアラートは表示されません。

  • CoreBluetoothの場合、Appがバックグラウンドでも動作するよう、CBPeripheralManagerのオプションとして「CBPeripheralManagerOptionRestoreIdentifierKey」が設定できますが、IOBluetoothの場合は使用できないようです。バックグラウンドでの動作は不可能なのか、それとも別に何か設定方法があるのでしょうか。

UI部分は次回。

参考

BLE

参考書籍

上を目指すプログラマーのためのiPhoneアプリ開発テクニック iOS 7編

MacとiOSをBLEで連携 - Peripheral編2

前回の続きです。

UI部分とBLEの呼び出し部分を作成します。

やりたいこと

  • 0.05秒に一度ずつ、Central端末から受け取った値を更新してTextFieldに表示します。
  • 1秒に一度ずつ値を更新して、Central端末にNotifyが届くようにする。

AppDelegate.h

プロジェクト作成時に生成されたものから変更はありません。

#import < Cocoa / Cocoa.h >

@interface AppDelegate : NSObject 
@end

AppDelegate.m

#import "AppDelegate.h"
#import "PeripheralController.h"

@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property (weak) IBOutlet NSTextField               *txtGotValue;
@property (weak) IBOutlet NSTextField               *txtSendValue;
@property (weak) IBOutlet NSButton                  *btnStop;
@property (strong, nonatomic) PeripheralController  *ctrPeripheral;
@property (strong, nonatomic) NSTimer               *tmrUpdateText;
@property (strong, nonatomic) NSTimer               *tmrSendValue;
@property (nonatomic) int                           intSendValue;
- (IBAction)btnStopClicked:(id)sender;
@end
@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    _ctrPeripheral = [[PeripheralController alloc] init];
    // Bluetoothの使用準備. PeripheralManagerの初期化.
    [_ctrPeripheral initPeripheralController];
    
    // タイマーの起動.
    [self startUpdateTextTimer];
    [self startSendValueTimer];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
    // Insert code here to tear down your application
}
- (void)startUpdateTextTimer
{
    // 0.05秒ごとにCentralから取得した値を更新.
    _tmrUpdateText = [NSTimer scheduledTimerWithTimeInterval:0.05f target:self selector:@selector(updateText:) userInfo:nil repeats:YES];
}
- (void)updateText:(NSTimer *)timer
{
    // Centralから取得した値をTextFieldに入れる.
    _txtGotValue.stringValue = [_ctrPeripheral getCentralValue];
}
- (void)stopUpdateLabelTimer
{
    if(_tmrUpdateText)
    {
        [_tmrUpdateText invalidate];
        _tmrUpdateText = nil;
    }
}
- (void)startSendValueTimer
{
    // Centralに送信する値の更新は1秒ごとに実行.
    _tmrSendValue = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(sendValue:) userInfo:nil repeats:YES];
}
- (void)sendValue:(NSTimer *)timer
{
    // 999までの乱数をCentralに送信する.
    _intSendValue = (int)arc4random_uniform(999);
    [_ctrPeripheral updatePeripheralValue:_intSendValue];
    
    _txtSendValue.stringValue = [NSString stringWithFormat:@"%d", _intSendValue];
}
- (void)stopSendValueTimer
{
    if(_tmrSendValue)
    {
        [_tmrSendValue invalidate];
        _tmrSendValue = nil;
    }
}

- (IBAction)btnStopClicked:(id)sender
{
    [self stopUpdateLabelTimer];
    [self stopSendValueTimer];
    [_ctrPeripheral close];
}
@end

ほぼコメント通りなのですが、NSTimerはSelectorを実行する時間を設定して初期化した時点でタイマーが動作開始されるのですね。

2つ同時に実行しても、動作上特に問題はなさそうでしたので、UnityやopenFrameworksでのupdate関数のような動作を実現したい場合に使えそうです。

次はiPhone側、Centralについて。

参考

Objective-C(Timer, 乱数)