MacとiOSをBLEで連携 - Peripheral編
諸事情により、OpenCVはいったんお休みして、MacとiPhoneをBluetooth Low Energy(BLE)で連携してみることにしました。
やりたいこと
MacとiPhoneを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
- BTLE Central Peripheral Transfer
- CoreBluetoothの概要 - Reinforce-Lab.'s Blog
- Peripheralの実装 - エンジニアの備忘録
- Core Bluetooth プログラミングガイド - Apple Developer
- 【連載】Bluetooth LE (2) iOS デバイスで Bluetooth LE 機器を使う - フェンリル デベロッパーズブログ
参考書籍
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について。