[iOS]すれちがい通信(その7)
iBeaconでの通信が成功したというのは間違いだった。申し訳ない。
CLLocationManagerDelegateの-locationManager:didRangeBeacons:inRegion:は検出できなくても周期的に呼ばれるようで、それで検出できたと考えたが、見つけたビーコン配列は空だった。そればかりが、動いていたCore Bluetoothの通信もできなくなっている!
結論をいうとInfo.plistのBackgraound Modesの「Location updates」と「Uses Bluetooth LE accessories」、「Acts as a Bluetooth LE accessory」を一緒に設定するとBluetooth LE通信ができなくなるようだ。
この設定をしなくすると動作するようになった。Core BluetoothとiBeaconの両方を使ってiOS機器同士で通信する場合は、注意が必要なようだ。
[iOS]すれちがい通信(その6)
Core Bluetoothと使ったすれちがい通信だと、アプリケーションがバッググラウンドで動作している場合、通信の頻度が低くなるので、iBeaconを試してみる事にした。
自分で調べた訳ではないので間違っていたら申し訳ないが、iBeaconはBluetooth LEのAdvertisingパケットにiBeacon用の情報を埋め込んだもののようだ。つまり、サービスのスキャンで情報を受け取れるという事か?
まずは、バックグラウンドでもすれちがい通信できるようにInfo.plistを設定する。
iBeacon関連のフレームワークを使うとコードは簡素になる。Periperal側は以下のとおり。
@implementation BeaconPeripheralResponseParser
...
- (void)parse
{
self.state = kBeaconPeripheralStateAdvertising;
/* CBPeripheralManagerを生成 */
self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self
queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
if (! self.peripheralManager) {
/* CBPeripheralManagerの初期化失敗 */
self.state = kBeaconPeripheralStateError;
self.error = [self _errorWithCode:kBeaconPeripheralResponseParserGenericError
localizedDescription:@"CBPeripheralManagerの初期化に失敗しました。"];
return;
}
/* ビーコン領域を生成 */
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:BEACON_SERVICE_UUID];
self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid
major:12345
minor:67890
identifier:@"demo.Wibree.BeaconCentralResponseParser"];
if (! self.beaconRegion) {
/* ビーコン領域の初期化失敗 */
self.state = kBeaconPeripheralStateError;
self.error = [self _errorWithCode:kBeaconPeripheralResponseParserGenericError
localizedDescription:@"ビーコン領域の初期化に失敗しました。"];
self.peripheralManager = nil;
return;
}
/* 告知開始 */
NSDictionary *dictionary = [self.beaconRegion peripheralDataWithMeasuredPower:nil];
[self.peripheralManager startAdvertising:dictionary];
}
...
@end
Central側はこうなる。
@implementation BeaconCentralResponseParser
...
- (void)parse
{
self.state = kBeaconCentralStateScanning;
/* CLLocationManagerを生成 */
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
if (! self.locationManager) {
/* CLLocationManagerの初期化失敗 */
self.state = kBeaconCentralStateError;
self.error = [self _errorWithCode:kBeaconCentralResponseParserGenericError
localizedDescription:@"CLLocationManagerの初期化に失敗しました。"];
return;
}
/* ビーコン領域を生成 */
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:BEACON_SERVICE_UUID];
self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:@"demo.Wibree.BeaconCentralResponseParser"];
if (! self.beaconRegion) {
/* ビーコン領域の初期化失敗 */
self.state = kBeaconCentralStateError;
self.error = [self _errorWithCode:kBeaconCentralResponseParserGenericError
localizedDescription:@"ビーコン領域の初期化に失敗しました。"];
self.locationManager = nil;
return;
}
/* ビーコン領域の出入りを監視 */
[self.locationManager startMonitoringForRegion:self.beaconRegion];
/* 距離を監視 */
[self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
}
...
@end
ビーコンが見つかるとCLLocationManagerDelegateのデリゲート・メソッドが呼ばれるので、それを実装する事になる。
@implementation BeaconCentralResponseParser
...
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
if ([self.delegate respondsToSelector:@selector(beaconCentralResponseParser:didEnterRegion:)]) {
[self.delegate beaconCentralResponseParser:self didEnterRegion:region];
}
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
if ([self.delegate respondsToSelector:@selector(beaconCentralResponseParser:didExitRegion:)]) {
[self.delegate beaconCentralResponseParser:self didExitRegion:region];
}
}
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region
{
if ([self.delegate respondsToSelector:@selector(beaconCentralResponseParser:didRangeBeacons:inRegion:)]) {
[self.delegate beaconCentralResponseParser:self didRangeBeacons:beacons inRegion:region];
}
}
...
@end
[iOS]すれちがい通信(その5)
今回は、Peripheral側だ。
Peripheralが対応しているサービスUUIDを登録する。
...
queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
...
Centralの場合と同様に、Peripheralが利用可能になったら、対応するサービスUUIDを登録する。
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
if (peripheral.state != CBPeripheralManagerStatePoweredOn) {
return;
}
self.transferCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:WIBREE_CHARACTERISTIC_UUID]
properties:CBCharacteristicPropertyNotify
value:nil
permissions:CBAttributePermissionsReadable];
CBMutableService *transferService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:WIBREE_SERVICE_UUID]
primary:YES];
transferService.characteristics = @[self.transferCharacteristic];
[self.peripheralManager addService:transferService];
}
- startAdvertising:でもサービスUUIDを設定しているが?だが、Centralに見つけてもらうサービスUUIDとCentralと接続後に問い合わせを受けるサービスUUIDが一致していなくてもいいからのようだ。
Centralからキャラクタリスティックの問い合わせが来たら場合の処理。
- (void)peripheralManager:(CBPeripheralManager *)peripheral
central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic
{
self.dataToSend = [[Document sharedDocument].uniqueIdentifier dataUsingEncoding:NSUTF8StringEncoding];
self.sendDataIndex = 0;
[self sendData];
}
- (void)sendData
{
static BOOL sendingEOM = NO;
if (sendingEOM) {
BOOL didSend = [self.peripheralManager updateValue:[@"EOM" dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:self.transferCharacteristic onSubscribedCentrals:nil];
if (didSend) {
sendingEOM = NO;
}
return;
}
if (self.sendDataIndex >= self.dataToSend.length) {
return;
}
BOOL didSend = YES;
while (didSend) {
NSInteger amountToSend = self.dataToSend.length - self.sendDataIndex;
if (amountToSend > NOTIFY_MTU) amountToSend = NOTIFY_MTU;
NSData *chunk = [NSData dataWithBytes:self.dataToSend.bytes+self.sendDataIndex length:amountToSend];
didSend = [self.peripheralManager updateValue:chunk forCharacteristic:self.transferCharacteristic onSubscribedCentrals:nil];
if (!didSend) {
return;
}
NSString *stringFromData = [[NSString alloc] initWithData:chunk encoding:NSUTF8StringEncoding];
self.sendDataIndex += amountToSend;
if (self.sendDataIndex >= self.dataToSend.length) {
sendingEOM = YES;
BOOL eomSent = [self.peripheralManager updateValue:[@"EOM" dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:self.transferCharacteristic onSubscribedCentrals:nil];
if (eomSent) {
sendingEOM = NO;
}
return;
}
}
}
- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral
{
[self sendData];
}
BLEでは、一回の通信で遅れるデータサイズは小さいので細切れにして送っている。
ソースコード GitHubからどうぞ。https://github.com/murakami/workbook/tree/master/ios/Wibree - GitHub
関連情報 Core Bluetooth Programming Guide
BTLE Central Peripheral Transfer
iPhoneアプリ開発エキスパートガイド iOS 6対応
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)
- iPhoneアプリ開発エキスパートガイド iOS 6対応/インプレスジャパン
- ¥3,990
- Amazon.co.jp
[iOS]すれちがい通信(その4)
それでは、見つける側となるCentralのサンプルコードを見ていこう。
CBCentralManagerのインスタンスを生成すると、すぐにスキャンできるようになるわけではない。stateがCBCentralManagerStatePoweredOnになったらスキャンを開始する事になる。
...
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self
queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
...
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
if (central.state != CBCentralManagerStatePoweredOn) {
return;
}
/* スキャン */
[self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:WIBREE_SERVICE_UUID]]
options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
}
UUIDがWIBREE_SERVICE_UUIDのサービスをスキャンしている。
スキャンしているPeripheralが見つかるとメソッドが呼ばれるので、見つけたPeripheralと接続する。
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI
{
if (self.discoveredPeripheral != peripheral) {
self.discoveredPeripheral = peripheral;
[self.centralManager connectPeripheral:peripheral options:nil];
}
}
接続できたらサービスUUIDとキャラクタリスティックUUIDを指定して、識別子を受け取る。
- (void)centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral
{
[self.centralManager stopScan];
[self.data setLength:0];
peripheral.delegate = self;
[peripheral discoverServices:@[[CBUUID UUIDWithString:WIBREE_SERVICE_UUID]]];
}
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
for (CBService *service in peripheral.services) {
[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:WIBREE_CHARACTERISTIC_UUID]] forService:service];
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(NSError *)error
{
for (CBCharacteristic *characteristic in service.characteristics) {
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:WIBREE_CHARACTERISTIC_UUID]]) {
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
}
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
NSString *stringFromData = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
if ([stringFromData isEqualToString:@"EOM"]) {
NSString *uniqueIdentifier = [[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding];
DBGMSG(@"%s UUID(%@)", __func__, uniqueIdentifier);
[peripheral setNotifyValue:NO forCharacteristic:characteristic];
[self.centralManager cancelPeripheralConnection:peripheral];
}
[self.data appendData:characteristic.value];
}
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error
{
if (![characteristic.UUID isEqual:[CBUUID UUIDWithString:WIBREE_CHARACTERISTIC_UUID]]) {
return;
}
if (characteristic.isNotifying) {
}
else {
[self.centralManager cancelPeripheralConnection:peripheral];
}
}
識別子を受け取ったら切断しているので、切断したら呼ばれるメソッドで、スキャンを再開する。
- (void)centralManager:(CBCentralManager *)central
didDisconnectPeripheral:(CBPeripheral *)peripheral
error:(NSError *)error
{
self.discoveredPeripheral = nil;
[self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:WIBREE_SERVICE_UUID]]
options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
}
[iOS]すれちがい通信(その3)
今回制作するサンプルコードは、アプリケーションが生成した識別子をBluetooth LEを使って交換するという内容だ。
識別子は端末毎にユニークな値なので、一種の名刺交換ということになる。
[iOS]すれちがい通信(その2)
すれちがい通信にはBluetooth LEを使うのだが、BLuetoothについて説明する。
Bluetoothは省電力な電波を使った無線通信で、最新の4.xでは対応機器は次の3つに分類される。
Bluetooth Smart | 4.0で追加されたBluetooth Low Energyのみ対応。 |
Bluetooth Smart Ready | Bluetooth LEと従来のBluetoothの両方に対応。 |
Bluetooth | 従来のBluetoothのみ対応。 |
iOSでBluetoothに対応する方法を整理してみる。
従来のBLuetooth | MFi機器に対してExternal Accessory Frameworkで通信。 |
Game Kit | |
Bluetooth LE | Core Bluetooth Framework |
iOSで、例えば、市販されているBluetooth機器と自由に通信するということになると、Bluetooth LEという事になると思う。
[iOS]すれちがい通信(その1)
Bluetooth LEを使って、すれ違い時に識別子(ID)を交換するアプリケーションのひな形を作成してみようと思う。
今回はざっとした概要だけ
Apple Developer Siteに『BTLE Central Peripheral Transfer』というBluetooth LEのサンプルコードがあるので、これを入手する。このサンプルコードは、Bluetooth LEの検出側(Central)と周辺機器側(Peripheral)の両方が実装されているのが、これが流用できる。
これを流用して作成したのが、今回のサンプルコード『Wibree』だ。愛読している『 iOS開発におけるパターンによるオートマティズム』のコネクタ/パーサの方式となっている。
すれちがい通信させる為には、アプリケーションがバックグラウンドになっていても通信できないといけないが、そのため、Info.plistの[UIBackgroundModes]キーに[bluetooth-central]と[bluetooth-peripheral]を設定する。
また、サービスとキャラクタリスティックのUUIDが必要になるが、それは、先日のuuidプログラムで取得する。
$ uuid
EAD5D6C9-BFCF-44EE-91D4-45C2501456E2
EAD5D6C9BFCF44EE91D445C2501456E2
$ uuid
22AD9740-FBED-44E8-9B7B-5F9A12974D2F
22AD9740FBED44E89B7B5F9A12974D2F
Bluetooth LEを使った、すれちがい通信。以前、やったことがあるのだが、これだけではうまくいかなかった。そのとき、どうやったかを思い出しながら記事にしていこうと思っている。
『その1』は、ここまで。
ソースコード GitHubからどうぞ。https://github.com/murakami/workbook/tree/master/ios/Wibree - GitHub
関連情報 Core Bluetooth Programming Guide
BTLE Central Peripheral Transfer
iPhoneアプリ開発エキスパートガイド iOS 6対応
iPhoneアプリ開発エキスパートガイド iOS 6対応
http://ameblo.jp/bitz/(ミラー・サイト)
[OSX][iOS]UUID
作業中にUUIDが必要になることがあると思う。プログラム中にUUIDを取得する処理を追加するのが問題ないが、定数として埋め込みたい場合、いちいち、取得する為のプログラムを書くのは面倒だ。
そこで、コマンドラインでUUIDを吐き出すプログラムを用意してみた。
int main(int argc, const char * argv[])
{
@autoreleasepool {
NSUUID *uuid = [NSUUID UUID];
NSString *uuidString = [uuid UUIDString];
NSString *str32 = [uuidString stringByReplacingOccurrencesOfString:@"-" withString:@""];
DBGMSG(@"%s UUID(%@)", __func__, uuidString);
printf("%s\n", str32.UTF8String);
}
return 0;
}
作ったプログラムは~/binに置いた。そうそう、ここにパスを通すのをお忘れなく。
$ cat .profile
PATH="$PATH:~/bin"
$ source .profile
車輪の再発明になっていそうで怖いが。
ソースコード GitHubからどうぞ。https://github.com/murakami/workbook/tree/master/mac/uuid - GitHub
関連情報 NSUUID Class Reference
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)
[OSX][iOS]Sprite Kit(その5)
先日のCocoa勉強会で話題となってテクスチャ・アトラスについて調べてみた。
グラフィックのハードウェアからの制限だと思うが、OpenGL ESでは扱えるテクスチャ画像の最大サイズやユニット数に制限がある。このユニット数の制限への対策として、複数の画像を合体して一つのテクスチャで扱うことによって利用するユニット数を減し、このままでは扱いづらいので元の画像単位で利用できるする仕組みがテクスチャ・アトラスと呼ばれる方法で、Sprite Kitには、この方法を簡単に利用する為の機能が用意されている。
宇宙船に10枚の画像をパラパラ動画で表示させる場合、テクスチャ・アトラスを使わないと以下のようになる。
NSMutableArray *textureArray = [[NSMutableArray alloc] init];
for (int i = 1; i <= 10; i++) {
NSString *filename = [NSString stringWithFormat:@"spaceship%02d", i];
SKTexture *texture = [SKTexture textureWithImageNamed:filename];
[textureArray addObject:texture];
}
SKAction *animationAction = [SKAction animateWithTextures:textureArray timePerFrame:0.1];
[hull runAction:[SKAction repeatActionForever:animationAction]];
テクスチャ・アトラスを利用場合は、サフィックスが.atlasのフォルダを用意して、そこに画像ファイルを格納する。
.atlasのフォルダの画像をSKTextureAtlasクラスで読み込んで、そこからファイル名で取り出すように、さっきのコードを変更する。
NSMutableArray *textureArray = [[NSMutableArray alloc] init];
SKTextureAtlas *spaceshipTextureAtlas = [SKTextureAtlas atlasNamed:@"spaceship"];
for (int i = 1; i <= 10; i++) {
NSString *filename = [NSString stringWithFormat:@"spaceship%02d", i];
SKTexture *texture = [spaceshipTextureAtlas textureNamed:filename];
[textureArray addObject:texture];
}
SKAction *animationAction = [SKAction animateWithTextures:textureArray timePerFrame:0.1];
[hull runAction:[SKAction repeatActionForever:animationAction]];
ソースコード
GitHubからどうぞ。https://github.com/murakami/workbook/tree/master/ios/Spiritus - GitHub
関連情報 iOSヒューマンインターフェイスガイドライン
iOS Technology Overview
UIKit Dynamics Catalog
Sprite Kit Programming Guide
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)
[OSX][iOS]関東64回Cocoa勉強会
朝からの雪で開催が危ぶまれたが、今回はじっくりとやり取りが出来て、いい会になったのではないかと思う。
発表内容をざっと説明すると、「CoreDataとUndo」はUndoを実装する場合のCoreDataの使い方。CameraRemoteAPIはレンズ・カメラの制御について。日本語の逐次検索を試してみた。独自のリマインダーを指定するには。SpriteKidを使ってみる。そして、iOS機器に接続するゲームパッドについてだ。
関連情報 Cocoa勉強会connpass
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)