Android/App개발2014. 7. 16. 13:31


http://developer.android.com/guide/practices/screens-distribution.html

모든 스크린사이즈에 적절하게 표시되도록 화면을 디자인할것을 권고하지만 태블릿만, 폰만 또는 특정크기이상의 스크린만 지원하고자 하는 경우가 있다. 그러려면 지원할 스크린에 대한 설정을 manifest에 추가하여 구글플레이같은 외부서비스에 의해 필터링이 되도록 할 수 있다.

그전에 멀티스크린지원에 대한 정확한 이해가 필요하고 그에 따라 구현해야 한다.

폰만 지원하기

일반적으로 큰스크린에 잘 맞도록 시스템이 동작하기 때문에 큰스크린에 대한 필터는 필요치 않다. Best Practies for Screen Independence에 잘 따랏다면 태블릿에서 아주 잘 동작할것이다.  하지만 스케일업이 생각처럼 동작하지 않는것을 발견할 수 있으며 애플리케이션을 다른 스크린설정으로 두개의 버전을 배포하려고 할 수 있다. 이 경우 <compatible-screens> 로 screen size와 density의 조합으로 조정할 수 있다. Google Play는 이 정보를 참조하여 애플리케이션을 필터링한다. 그리하여 단말에 적합한경우에만 다운로드 설치가 가능하도록 한다.

<screen> 엘리먼트를 포함해야 하는데 각 <screen>은 스크린설정정보(스크린크기, 스크린밀도)를 담는다. 두가지(screenSize, screenDensity)정보를 모두 담아야 한다. 하나라도 빠져있다면 Google Play는 그 필드를 무시할것이다.

만일 small, normal size screen을 지원하고 density는 무시하고자 한다면 총 8개의 <screen> 엘리먼트가 필요하다. 각 사이즈별로 density가 4가지이기 때문이다. 여기서 정의되지 않은 조합은 호환되지 않는것으로 간주된다. 다음은 이 경우에 대한 manifest예이다.

<manifest ... >

    <compatible-screens>

        <!-- all small size screens -->

        <screen android:screenSize="small" android:screenDensity="ldpi" />

        <screen android:screenSize="small" android:screenDensity="mdpi" />

        <screen android:screenSize="small" android:screenDensity="hdpi" />

        <screen android:screenSize="small" android:screenDensity="xhdpi" />

        <!-- all normal size screens -->

        <screen android:screenSize="normal" android:screenDensity="ldpi" />

        <screen android:screenSize="normal" android:screenDensity="mdpi" />

        <screen android:screenSize="normal" android:screenDensity="hdpi" />

        <screen android:screenSize="normal" android:screenDensity="xhdpi" />

    </compatible-screens>

    ...

    <application ... >

        ...

    <application>

</manifest> 


주의) 하지만 2014년 6월 현재 하나가 더 있다. xxhdpi 따라서 이제 10개가 필요하게 되겠다.

Note) <compatible-screens>엘리먼트는 리버스를 통해 호환 여부를 작은사이즈만 지원하는지 알수 있으나 <supports-screens>를 사용하면 density를 요구하지 않기 때문에 피할 수 있다.


태블릿만 지원하기

폰을 지원하지 않겠다면(즉 큰스크린만 지원하겠다면) 또는 작은 스크린에 대해 최적화하는데 시간이 필요하다면 작은스크린 디바이스는 설치안되도록 할 수 있다. 이 방법은 <support-screens>를 통해 가능하다.

<manifest ... >

    <supports-screens android:smallScreens="false"

                      android:normalScreens="false"

                      android:largeScreens="true"

                      android:xlargeScreens="true"

                      android:requiresSmallestWidthDp="600" />

    ...

    <application ... >

        ...

    </application>

</manifest>

두가지 방법이 있다.

  • "small"과 "normal"사이즈를 미지원으로 설정, 일반적으로 이런 디바이스는 태블릿이 아니다
  • 앱이 허용하는 영역이 최소 600dp이상으로 설정

첫번째는 android 3.1과 그 이 하의 디바이스들을 위한것이다. 왜냐하면 그 디바이스들은 일반화된 스크린사이즈에 기반하여 사이즈를 정의하였기 때문이다. requiresSmallestWidthDp속성은 android 3.2와 그이상을 위한것이다. 이는 dip의 최소값에 기반하여 요구되는 사이즈를 정할 수 있다. 예를 들면 600dp로 설정하면 일반적으로 7인치 이상의 스크린을 갖는 디바이스들이 해당된다.

당신이 필요로 하는 사이즈는 당현히 다를수 있다. 만인 9인치 이상의 스크린을 지원하고자 한다면 720dp로 설정할 수 있을 것이다.

앱은 requiresSmallestWidthDp속성을 빌드하기 위해 Android 3.2이상으로 빌드를 해야 한다. 이전버전은 에러가 날것이다. 가장 안전한 방법은 필요로 하는 API level에 맞게 minSdkVersion을 설정하는 것이다. 최종배포 질드시 빌드타겟을 3.2로 변경하고 requeiresSmallestWidthDp를 추가한다. 3.2이전버전은 런타임에 이 값을 무시할것이기 때문에 문제가 없을 것이다.

왜 가로사이즈만으로 스크린을 구분하는지 궁금하면 다음 링크를 참조해라. -> New Tools for Managing Screen Sizes.

여러화면을 지원하기 위해 필요한 모든 기술을 적용하여 가능한 많은 장치에 앱을 사용할 수 있도록 하기 위해 <compatible-screens>를 사용하거나 모든 화면구성에 호환성을 제공할 수 없을 경우에만 <support-screens>을 사용하거나 또는 특정한 화면구성의 다른 세트에 대한 다른 버전의 앱을 제공할 수 있다.


다른 화면을 위한 여러개의 APK제공

하나의 APK만을 제공하는것이 권장사항이긴 하지만 구글플레이는 여러 화면설정에 따라 여러개의 APK를 동일 앱에 대하여 제공하는것을 허용한다. 예를 들어 폰과 태블릿버전을 모두 제공하고자 하는데 동일 APK로 만들수 없는 경우에 실제로 2개의 APK를 게시할 수 있다. 구글 플레이는 디바이스 화면에 맞춰서 APK를 배포할것이다.

하지만 하나로 게시하는제 좋을거다. 라는게 구글의 여전한 권장사항이다. 

더 자세한 정보를 원하면 다음 링크를 더 살펴봐라. Multiple APK Support.



Posted by 삼스
HTML52014. 6. 20. 15:36


http://www.elabs.se/blog/66-using-websockets-in-native-ios-and-android-apps

이 샘플은 WebSocket으로 실시간으로 browser, iOS, android 클라이언트와 통신하는 방법에 대한 것이다.

서버는 웹소켓서버를 운영하고 여러가지 플랫폼과 통신을 수행이 가능해진다. 전체 소스는  https://github.com/elabs/mobile-websocket-example 을 참고하라.


서버

아주 간단한 웹소켓서버의 예는 아래와 같다.EM-WebSocket gem을 사용하고 Ruby로 구현되었다.


equire "em-websocket" EventMachine.run do @channel = EM::Channel.new EventMachine::WebSocket.start(host: "0.0.0.0", port: 8080, debug: true) do |ws| ws.onopen do sid = @channel.subscribe { |msg| ws.send(msg) } @channel.push("#{sid} connected") ws.onmessage { |msg| @channel.push("<#{sid}> #{msg}") } ws.onclose { @channel.unsubscribe(sid) } end end 

end 

웹소켓연결을 8080포트에서 수락한고 새로운 클라이언트가 연결되면 내부 채널을 구독한다. 클라이언트가 데이터를 이 채널에 전송하여 채널에 데이터가 유효해질때마다 모든 클라이언트에 데이터를 전송한다. 클라이언트는 구독ID를 통해 구별이 가능하다. 클라이언트가 연결이 끊어지면 채널의 구독을 중단한다.

위코드를 실행하려면 bundle install , ruby server.rb을 순서대로 실행한다.

Browser client


$(document).ready(function() { ws = new WebSocket("ws://" + location.hostname + ":8080/"); ws.onmessage = function(event) { $("#messages").append("<p>" + event.data + "</p>"); }; ws.onclose = function() { console.log("Socket closed"); }; ws.onopen = function() { console.log("Connected"); ws.send("Hello from " + navigator.userAgent); }; $("#new-message").bind("submit", function(event) { event.preventDefault(); ws.send($("#message-text").val()); $("#message-text").val(""); }); 

}); 

서버에 8080포트로 연결을 하고 연결이 되었을 때, 메세지가 도달하였을 때, 그리고 연결이 닫혔을떄의 콜백을 구현하였다.

새로운 메세지를 작성하고 메세지를 전송하기 위한 submit form도 작성하였다.

이 코드는 모던브라우져만 지원한다. 오래된 브라우져에서는 Flash로 가능하다.


iOS client

SocketRocket이라는 라이브러리로 웹소켓을 사용할 수 있다. 이것은 Cocoapods로 설치가 가능하다. 먼저 Cocoapods를 설치해야 한다.

Podfile을 아래와 같이 iOS프로젝트가 있는 폴더에서 만든다.

platform :ios, "7.0"

pos "SocketRocket" 

Cocoapods가 설치되어 잇지 않다면 gem install cocoapods 을 실행하고 pod setup을 실행 이어서 pod install을 Podfile이 들어 있는 iOS프로젝트 폴더에서 실행한다.

관련 코드는 ViewController.m에 웹소켓서버에 연결하는 코드를 작성한다.


-(void) connectWebSocket {

  webSocket.delegate = nil;

  webSocket = nil;

  NSString *urlString = @"ws://localhost:8080";

  SRWebSocket *newWebSocket  = [[SRWebSocket alloc] initWithURL initWithURL:[NSURL URLWithString:urlString]];

  newWebSocket.delegate = self;

  [newWebSocket open];
}

localhost부분을 서버주소로 바꾸면 된다. 

다음은 SRWebSocketDelegete protocol의 구현이다.

- (void)webSocketDidOpen:(SRWebSocket *)newWebSocket {

  webSocket = newWebSocket;

  [webSocket send:[NSString stringWithFormat:@"Hello from %@", [UIDevice currentDevice].name]];

}

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {

  [self connectWebSocket];

}

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {

  [self connectWebSocket];

}

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {

  self.messagesTextView.text = [NSString stringWithFormat:@"%@\n%@", self.messagesTextView.text, message];

} 

브라우져에서 사용하는 자바스크립트 API와 완전 비슷하다.

메세지를 보낼때는 다음과 같이


-(IBAction)sendMessage:(id)sender {

  [webSocket send:self.messageTextField.text];

  self.messageTextField.text =nil;

} 


Android client

Android Studio를 사용한다면 Java WebSockets라이브러리를 gradle로 설치한다

dependencies {

  compile "org.java-websocket:Java-WebSocket:1.3.0"

} 

ADT를 사용한다면 maven으로 java WebSocket을 설치하던가 아니면 별도로 배포되는 jar을 받아서 사용하던가 하면 된다


private void connectWebSocket() {

  URI uri;

  try {

    uri = new URI("ws://websockethost:8080");

  } catch (URISyntaxException e) {

    e.printStackTrace();

    return;

  }


  mWebSocketClient = new WebSocketClient(uri) {

    @Override

    public void onOpen(ServerHandshake serverHandshake) {

      Log.i("Websocket", "Opened");

      mWebSocketClient.send("Hello from " + Build.MANUFACTURER + " " + Build.MODEL);

    }


    @Override

    public void onMessage(String s) {

      final String message = s;

      runOnUiThread(new Runnable() {

        @Override

        public void run() {

          TextView textView = (TextView)findViewById(R.id.messages);

          textView.setText(textView.getText() + "\n" + message);

        }

      });

    }


    @Override

    public void onClose(int i, String s, boolean b) {

      Log.i("Websocket", "Closed " + s);

    }


    @Override

    public void onError(Exception e) {

      Log.i("Websocket", "Error " + e.getMessage());

    }

  };

  mWebSocketClient.connect();

} 

websockethost는 수정하라.

메세지 보 낼때는 이렇게

public void sendMessage(View view) {

  EditText editText = (EditText)findViewById(R.id.message);

  mWebSocketClient.send(editText.getText().toString());

  editText.setText("");

}

android.permission.INTERNET권한이 있어야 하고 앱타겟을 Android 4.0+이다.













Posted by 삼스
iOS2014. 5. 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] forCharacteristic:self.transferCharacteristic onSubscribedCentrals:nil];
         
        if (didSend) {
            // It did, so mark it as sent
            sendingEOM = NO;
        }
        // didn't send, so we'll exit and wait for peripheralManagerIsReadyToUpdateSubscribers to call sendData again
        return;
    }
     
    // We're sending data
    // Is there any left to send?
    if (self.sendDataIndex >= self.dataToSend.length) {
        // No data left.  Do nothing
        return;
    }
     
    // There's data left, so send until the callback fails, or we're done.
    BOOL didSend = YES;
     
    while (didSend) {
        // Work out how big it should be
        NSInteger amountToSend = self.dataToSend.length - self.sendDataIndex;
         
        // Can't be longer than 20 bytes
        if (amountToSend > NOTIFY_MTU) amountToSend = NOTIFY_MTU;
         
        // Copy out the data we want
        NSData *chunk = [NSData dataWithBytes:self.dataToSend.bytes+self.sendDataIndex length:amountToSend];
         
        didSend = [self.peripheralManager updateValue:chunk forCharacteristic:self.transferCharacteristic onSubscribedCentrals:nil];
         
        // If it didn't work, drop out and wait for the callback
        if (!didSend) {
            return;
        }
         
        NSString *stringFromData = [[NSString alloc] initWithData:chunk encoding:NSUTF8StringEncoding];
        NSLog(@"Sent: %@", stringFromData);
         
        // It did send, so update our index
        self.sendDataIndex += amountToSend;
         
        // Was it the last one?
        if (self.sendDataIndex >= self.dataToSend.length) {
             
            // Set this so if the send fails, we'll send it next time
            sendingEOM = YES;
 
            BOOL eomSent = [self.peripheralManager updateValue:[@"EOM" dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:self.transferCharacteristic onSubscribedCentrals:nil];
             
            if (eomSent) {
                // It sent, we're all done
                sendingEOM = NO;
                NSLog(@"Sent: EOM");
            }
             
            return;
        }
    }
}

 마지막으로 PeripheralManager가 다음 chunk를 전송할 수 있는 준비가 될때 콜백을 구현해주어야 한다. 이 콜백은 전송한 데이터가 모두 완료되었음을 의미하며 - (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral 이다. 

1
2
3
- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral {
    [self sendData];
}

인자 앱을 빌드하고 실행해보아라 다음과 같은 화면이 나올것이다.



인자 BTLE프로그래밍에 대한 기본적이 이해를 코드로서 완료하였다.

이 정보를 알아서 잘 사용하길 바란다.


소스코드 다운로드 링크 

CBTutorial-Initial.zip








Posted by 삼스