iOS2014.05.27 16:27


http://code.tutsplus.com/tutorials/ios-7-sdk-core-bluetooth-practical-lesson--mobile-20741

Core Bluetooth framework은 iOS앱에서 Bluetooth low energy기술을 사용할 수 있게 해준다. 이 튜토리얼은 iOS5에서 iOS7으로 CB가 어떻게 진화하였는지 안내할것이다. CB central과 peripheral을 어떻게 설정하고 서로간에 통신을 하며 어떻게 CB로 코딩이 가능한지에 대해서 설명한다.

Introduction

CB 튜토리얼은 두개의 장으로 나누어지는데 첫번쨰 장에서는 CB에 대한 이론을 설명한다. 반면에 이 튜토리얼은 완전한 예제를 통한 실용적인 과정이 될것이다 관련된 전체 소스코드가 수록되어 있다.

1. 소스코드 다운로드

이 튜토리얼의 목적은 CB framework을 어떻게 사용하는지에 대해 가르치는 것이다. 샘플코드를 통해 쉽게 알수 있다. 코드를  다운로드 먼저 하라.

CB framework에 집중할것이기 때문에 당신이 XCode와 iOS에 대해 잘 알고 있다고 가정할것이다. 샘플코드는 다음을 포함한다.

  • 앱은 navigation controller, 3개의 view 그리고 상속한 controller들로 구성된다.
  • 초기 뷰컨트롤로인 ViewController는 2개의 버튼으로 구성된다.
  • CBCentrolManagerViewController는 custom iBeacon을 생성한다.
  • CBPeripheralViewController는 iBeacon과 상속한 정보를 수신한다.
  • SERVICES헤더파일 : 앱에서 사용하는 변수들 

기본적인 구현은 다 되어 있으며 CB process관련하여 코드를 추가하면 된다. 프로젝트를 열고 실행하면 된다.

SERVICES.h파일은 2개의 UUID가 있는데 uuidgen으로 생성한 값이다. 이 값은 재생성해서 변경해도 된다.

이 테스트는 iOS장비가 있어야 테스트가 가능하다.

앱을 실행하면 다음 화면이 나온다.



2.  Central Role 프로그래밍

이 튜토리얼에서  CBCentralManagerViewController class가 중앙에 있다. 첫번째 과정은 CBCentralManager와 CBPeripheral을 지원하기 위한 2개의 프로토콜을 추가하는것 이다. 이 프로토콜들의 선언은 메소드를 정의한다. interface는 다음과 같을 것이다.

1
@interface CBCentralManagerViewController : UIViewController < CBCentralManagerDelegate, CBPeripheralDelegate>

이제 반드시 3개의 속성을 정의해야 한다. CBCentralManager, CBperipheral그리고 NSMutableData가 그것이다. 두개는 명백한 정보이고 하나는 디바이스간에 주고받는 정보이다.

1
2
3
@property (strong, nonatomic) CBCentralManager *centralManager;
@property (strong, nonatomic) CBPeripheral *discoveredPeripheral;
@property (strong, nonatomic) NSMutableData *data;

에러가 날거고 수정해야 한다. centralManager와 data 객체는 초기화해야 한다. centralManager는 sele delegate로 시작하고 viewDidLoad에서 다음과 같이 수행한다.

1
2
_centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
_data = [[NSMutableData alloc] init];

warning을 없에려면 다음 메소드를 추가한다.

- (void)centralManagerDidUpdateState:(CBCentralManager *)central

프로토콜메소드로이며 디바이스의 상태를 체크한다. 몇가지 상태가 정의되어 있다. 앱에서는 항상 상태를 확인해야 한다.

  • CBCentralManagerStateUnknown
  • CBCentralManagerStateResetting
  • CBCentralManagerStateUnsupported
  • CBCentralManagerStateUnauthorized
  • CBCentralManagerStatePoweredOff
  • CBCentralManagerStatePoweredOn

예를 들어, Bluetooth 4.0을 지원하지 않는 디바이스에서 이 앱을 실행하면 CBCentralManagerStateUnsupported 을 받게 될것이다.

CBCentralManagerStatePoweredOn 상태가 되면 디바이스를 스캔을 시작할 수 있다. scanForPeripheralsWithService메소드로 스캔한다. 첫번째 파라메터에 nil을 넣게 되면 CBCentralmanager는 모든 디바이스를 검색한다. UUID를 사용하게 되면 UUID에 해당하는 디바이스를 찾게 된다.

01
02
03
04
05
06
07
08
09
10
11
12
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    // You should test all scenarios
    if (central.state != CBCentralManagerStatePoweredOn) {
        return;
    }
     
    if (central.state == CBCentralManagerStatePoweredOn) {
        // Scan for devices
        [_centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
        NSLog(@"Scanning started");
    }
}

  이 때 앱은 다른  디바이스를 검색한다. 디바이스가 실제로 있음에도 불구하고 정보를 찾지 못할 수 있다. 이를 해결하기 위해서는 다음 메소드를 추가해야 한다.

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI

이 메소든느 디바이스가 발견될때마다 호출된것이다. 하지만 TRANSFER_SERVICE_UUID로 게시중인 peripherals들에 대해서만 반응할것이다.

추가적으로 캐시시스템을 사용하여 추후에 참조하여 더 빠르게 통신할 수 있도록 디바이스정보를 저장할 수 있다. 다음코드를 보라

01
02
03
04
05
06
07
08
09
10
11
12
13
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
     
    NSLog(@"Discovered %@ at %@", peripheral.name, RSSI);
     
    if (_discoveredPeripheral != peripheral) {
        // Save a local copy of the peripheral, so CoreBluetooth doesn't get rid of it
        _discoveredPeripheral = peripheral;
         
        // And connect
        NSLog(@"Connecting to peripheral %@", peripheral);
        [_centralManager connectPeripheral:peripheral options:nil];
    }
}

연결이 실패할 수 있다. 이 경우 다음 메소드로 알수 있다.

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error

1
2
3
4
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    NSLog(@"Failed to connect");
    [self cleanup];
}

사용자에게 경고할 수 있다. cleanup메소드는 아직 정의되지 않았다. 이제 정의해보자!. 

이 메소드는 원격디바이스에 대한 모든 subscription정보를 취소하거나 연결을 끊어버린다. 서비스에 속한 특성들을 루프를 돌면서 바인드되어 있는것들을 제거한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
- (void)cleanup {
     
    // See if we are subscribed to a characteristic on the peripheral
    if (_discoveredPeripheral.services != nil) {
        for (CBService *service in _discoveredPeripheral.services) {
            if (service.characteristics != nil) {
                for (CBCharacteristic *characteristic in service.characteristics) {
                    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
                        if (characteristic.isNotifying) {
                            [_discoveredPeripheral setNotifyValue:NO forCharacteristic:characteristic];
                            return;
                        }
                    }
                }
            }
        }
    }
 
    [_centralManager cancelPeripheralConnection:_discoveredPeripheral];
}

성공적으로 연결이 되었다면 이젠 서비스와 특성(characteristic)을 알아내야 한다. 다음 메소드를 통해 가능하다.

 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral

연결이 완료되고 나서 스캔프로세스는 중단한다. 그리고나서 UUID(TRANSFER_SERVICE_UUID)에 맷칭되는 서비스를 검색한다.

01
02
03
04
05
06
07
08
09
10
11
12
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"Connected");
     
    [_centralManager stopScan];
    NSLog(@"Scanning stopped");
     
    [_data setLength:0];
     
    peripheral.delegate = self;
 
    [peripheral discoverServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]]];
}

peripheral의 delegate를 통해 몇가지 콜백이 호출된다. 그 중 하나가 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error 인데 이 메소드는 서비스에 속한 특성들을 찾아준다. 에러가 났을 때는 하지말고 성공했을 때만 찾아야 한다. 

01
02
03
04
05
06
07
08
09
10
11
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    if (error) {
        [self cleanup];
        return;
    }
 
    for (CBService *service in peripheral.services) {
        [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]] forService:service];
    }
    // Discover other characteristics
}

모든게 정확하다면 특성이 발견될것이다. 다시 콜백이 호출될건데.. - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error 가 호출된다. 이렇게 발견이 되면 CBCentralManager가 peripheral로부터 받고자 하는 데이터를 받을 수 있도록 subscript할 수 있다.

에러시 처리가 되어야 한다. 믿을만하다면 특성을 바로 구독할 수 있지만 반드시 특성을 하나하나 확인해볼것을 권장한다. 그리고 확인이 되었을 때 만 구독하라. 이 과정까지 완료되면 데이터가 오는것을 기다리면된다.

01
02
03
04
05
06
07
08
09
10
11
12
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    if (error) {
        [self cleanup];
        return;
    }
     
    for (CBCharacteristic *characteristic in service.characteristics) {
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
            [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        }
    }
}

peripheral이 새로운 데이터를 보낼때마다 다음 콜백이 호출된다.

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 

두번째 인자는 특성을 포함한다. 

초기에 특성값을 저장하려고 NSString을 생성할것이다. 그런 후 데이터 수신이 모두 수신되었는지 또는 더 전달이 될것인지 확인할 것이다. 새로운  데이터가 수신되자마자 textview에 갱신될것이다. 모든데이터가 완료되면 특성과 장치에 대한 연결을 해재할수 있다.

다시 말해 데이터가 수신되면 대기하든지 연결을 해재하든지 알아서 하면 된다. 이 콜백은 특성에 따라 추가적으로 데이터가 도달할지에 대해 알 수 있다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error");
        return;
    }
     
    NSString *stringFromData = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
     
    // Have we got everything we need?
    if ([stringFromData isEqualToString:@"EOM"]) {
     
        [_textview setText:[[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding]];
         
        [peripheral setNotifyValue:NO forCharacteristic:characteristic];
         
        [_centralManager cancelPeripheralConnection:peripheral];
    }
     
    [_data appendData:characteristic.value];
}

추가적으로 CBCentral이 특성 변경에 대한 통지상태를 알수 있는 메소드가 있다. 

- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error

특성통지가 중단되었는지 반드시 확인해야 하며 중단될경우 연결을 해재해야 한다.

- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
     
    if (![characteristic.UUID isEqual:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]]) {
        return;
    }
     
    if (characteristic.isNotifying) {
        NSLog(@"Notification began on %@", characteristic);
    } else {
        // Notification has stopped
        [_centralManager cancelPeripheralConnection:peripheral];
    }
}

디바이스들간에 연결해제가 발생하면 로컬카피로 저장해둔 peripheral을 clean up해야 한다. 다음 메소드를 사용해서..

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error 

이 메소든느 단순하고 peripheral을 0으로 셋팅한다. 이어서 스켄을 다시 시작할 지 그냥 둘지 앱에서 정해서 하면 된다. 여기서는 다시 스캔하고 있다.

1
2
3
4
5
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    _discoveredPeripheral = nil;
 
    [_centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
}

최종적으로 하나의 단계가 더 있다. 뷰가 화면에서 가려질때마다 scan process를 중단해야 한다.

1
[_centralManager stopScan];

앱을 실행하자 하지만 peripherl앱이 있어야 데이터를 받을 수 있다. 다음 이미지는 최종 인터페이스를 보여준다.



3. Peripheral Role의 프로그래밍

이 튜터리얼에서는 CBPeripheralViewController에 집중한다. 먼저 CBPeripheralManagerDelegate와 UITextViewDelegate를 추가한다. 다음과 같은 인터페이스가 필요하다.

1
@interface CBPeripheralViewController : UIViewController < CBPeripheralManagerDelegate, UITextViewDelegate>

이어서 다음 4개의 속성을 추가한다. 처음 2개는 peripheral manager와 특성을 의미하고 3번째는 전송할 데이터, 그리고 마지막은 데이터 인덱스이다.

1
2
3
4
@property (strong, nonatomic) CBPeripheralManager *peripheralManager;
@property (strong, nonatomic) CBMutableCharacteristic *transferCharacteristic;
@property (strong, nonatomic) NSData *dataToSend;
@property (nonatomic, readwrite) NSInteger sendDataIndex;

먼저 _peripheralManager를 초기화하고 게시를 시작하기 위한 설정을 해야 한다. 서비스게시는 서비스UUID를 초기화하는것이다. viewDidLoad에서 다음과 같이 하면 된다.

1
2
3
4
5
6
7
- (void)viewDidLoad {
    [super viewDidLoad];
 
    _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
     
    [_peripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : @[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] }];
}

warning을 없애려면 CBCentramManager처럼 다음 메소드를 정의해야 한다.  - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral 상태가 CBPeripheralManagerStatePoweredOn이면 서비스와 특성을 빌드하고 정의해야 한다.

각 서비스와 특성은 Unique UUID이어야 한다. 

특성의 값들이 어떻게 사용되어질지를 정의하는 속성들이 있다.

  • CBCharacteristicPropertyBroadcast
  • CBCharacteristicPropertyRead
  • CBCharacteristicPropertyWriteWithoutResponse
  • CBCharacteristicPropertyWrite
  • CBCharacteristicPropertyWrite
  • CBCharacteristicPropertyNotify
  • CBCharacteristicPropertyIndicate
  • CBCharacteristicPropertyAuthenticatedSignedWrites
  • CBCharacteristicPropertyExtendedProperties
  • CBCharacteristicPropertyNotifyEncryptionRequired
  • CBCharacteristicPropertyIndicateEncryptionRequired

이 속석들에 대해 더 자세히 알고 싶으면 다음 링크를 참고해라.

 CBCharacteristic Class Reference.

i nit의 마지막 인자는 read, write 그리고 속성에 대한 안호화권한이다.

  • CBAttributePermissionsReadable
  • CBAttributePermissionsWriteable
  • CBAttributePermissionsReadEncryptionRequired
  • CBAttributePermissionsWriteEncryptionRequired

특성을 정의 완료하면  CBMutableService를 사용하여 서비스를 정의할 시간이다. 서비스는 TRANSFER_CHARACTERISTIC_UUID로 정의되어야 한다. 서비스에 특성을 추가하고 peripheral에 추가한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
    if (peripheral.state != CBPeripheralManagerStatePoweredOn) {
        return;
    }
     
    if (peripheral.state == CBPeripheralManagerStatePoweredOn) {
        self.transferCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID] properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
         
        CBMutableService *transferService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID] primary:YES];
         
        transferService.characteristics = @[_transferCharacteristic];
         
        [_peripheralManager addService:transferService];
    }
}

자 인자 서비스와 특성도 정의가 되었다. 인자 디바이스가 연결되었을 때 그리고 그에 반응하는것이 남았다.

- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic 메소드는 지원하는 특성에 맞는 누군가 연결이 되었을 때 호출된는데 이 때 데이터를 전송할 수 있다.

앱은 textview에서 데이터가 유효하면 전송한다. 사용자가 값을 수정하면 앱은 구독중인 central에 바로 전송한다. sendData메소드는 custom메소드이다.

1
2
3
4
5
6
7
8
- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
     
    _dataToSend = [_textView.text dataUsingEncoding:NSUTF8StringEncoding];
 
    _sendDataIndex = 0;
 
    [self sendData];
}

sendData는 데이터를 전송하는 메소드이다. 다음과 같은 몇가지 동작을 수행할 것이다.

  • Send data
  • Send the end of communication flag
  • Test if the app sent the data
  • Check if all data was sent
  • React to all of the previous topics

전체코드를 아래에 나타내었다. 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
- (void)sendData {
 
    static BOOL sendingEOM = NO;
     
    // end of message?
    if (sendingEOM) {
        BOOL didSend = [self.peripheralManager updateValue:[@"EOM" dataUsingEncoding:NSUTF8StringEncoding]