레이블이 Android bluetooth programming인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Android bluetooth programming인 게시물을 표시합니다. 모든 게시물 표시

2015년 10월 1일 목요일

안드로이드 BluetoothLeScanner 클래스 사용하기



안드로이드에서 Bluetooth LE (Low Energy)를 사용할 때 가장 기본적인 샘플 예제 코드로 https://github.com/googlesamples/android-BluetoothLeGatt 를 많이 사용한다.

하지만 API level 21부터는 API가 약간 바뀌어 LE 디바이스를 스캔할 때 사용하는 BluetoothAdapter 클래스의 startLeScan(), stopLeScan() 메소드가 deprecated 되어 빌드해 실행하는데 문제는 없어도 android studio에서 소스를 보면  mBluetoothAdapter.startLeScan(mLeScanCallback); 과 mBluetoothAdapter.stopLeScan(mLeScanCallback); 부분에 메소드 이름에 빨간색 줄이 쳐져 있는걸 보게 될 것이다.
API level 21부터는 BluetoothAdapter 클래스에서 스캔을 하지 않고 BluetoothLeScanner라는 클래스가 추가되어 이 클래스의 startScan(), stopScan() 메소드를 사용해 BLE 디바이스를 검색하도록 변하였다.
콜백 역시 Bluetooth.LeScanCallback 대신 ScanCallback 클래스를 사용해야 한다.

그러므로 android-BluetoothLeGatt 예제를 API level 21의 변화에 맞게 수정하려면 다음과 같다.

먼저 사용할 클래스  import를 추가한다. DeviceScanActivity.java 소스코드 위쪽의 import 부분에 아래 내용을 추가한다.

import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import java.util.List;

다음 DeviceScanActivity 클래스의 private 멤버 선언부에 밑줄 친 빨간색 내용을 추가한다.

public class DeviceScanActivity extends ListActivity {
    private LeDeviceListAdapter mLeDeviceListAdapter;
    private BluetoothAdapter mBluetoothAdapter;
    private boolean mScanning;
    private Handler mHandler; 
    private BluetoothLeScanner mBLEScanner;

다음 onCreate 메소드의 맨 뒤쪽 부분에 다음 줄을 추가해 BluetoothLeScanner 클래스의 인스턴스를 얻는다.

    if (mBluetoothAdapter == null) {
        Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show();
        finish();
        return;
    }
    mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
    // Checks if Bluetooth LE Scanner is available.    if (mBLEScanner == null) {
        Toast.makeText(this, "Can not find BLE Scanner", Toast.LENGTH_SHORT).show();
        finish();
        return;
    }
}

다음은 onListItemClick() 메소드와 scanLeDevice() 메소드 안에 있는 빨간색 줄이 쳐져 있는 startLeScan(), stopLeScan() 부분을 각각 다음 내용으로 바꿔준다.

mBLEScanner.startScan(mScanCallback); // mBluetoothAdapter.startLeScan() 부분
 
mBLEScanner.stopScan(mScanCallback);  // mBluetoothAdapter.stopLeScan() 부분

그리고 기존의 콜백 정의부분은 없애고 다음의 내용을 추가한다.

즉 아래 부분은 통채로 삭제

// Device scan callback.private BluetoothAdapter.LeScanCallback mLeScanCallback =        new BluetoothAdapter.LeScanCallback() {
    @Override    public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {        runOnUiThread(new Runnable() {            @Override            public void run() {                mLeDeviceListAdapter.addDevice(device);                mLeDeviceListAdapter.notifyDataSetChanged();            }        });    }};
 
위쪽 내용을 삭제한 부분에 아래 내용을 추가


    private ScanCallback mScanCallback = new ScanCallback() {
        @Override        public void onScanResult(int callbackType, ScanResult result) {
            processResult(result);
        }

        @Override        public void onBatchScanResults(List<ScanResult> results) {
            for (ScanResult result : results) {
                processResult(result);
            }
        }

        @Override        public void onScanFailed(int errorCode) {
        }

        private void processResult(final ScanResult result) {
            runOnUiThread(new Runnable() {
                @Override                public void run() {
                    mLeDeviceListAdapter.addDevice(result.getDevice());
                    mLeDeviceListAdapter.notifyDataSetChanged();
                }
            });
        }
    };
















2014년 6월 4일 수요일

Bluetooth Programming in Android (2/2)

이전 포스트(Bluetooth Programming in Android (1/2))에 이어 이제부터는 실제로 블루투스 디바이스와 연결해 데이터를 보내는 방법에 대해 이야기하겠다.

Profile and UUID


블루투스 표준은 블루투스 디바이스들이 제공하는 기능들에 대한 몇가지 프로파일을 정의하고 있다.

간단하게 말하자면 블루투스 프로파일은 디바이스가 무엇을 할 수 있는가에 대응한다. 예를 들어 블루투스 헤드셋은 오디오 스트림을 어떻게 주고 받고, 페어링 된 휴대폰에 기본적인 명령(응답, 볼륨 조절 등)을 어떻게 보내는가를 정의한 HSP(Handset Service Profile)을 구현한다. 일부 고급 헤드셋은 A2DP(Advanced Audio Distribution Profile)도 구현해서 사용자가 휴대폰에서 스트리밍하는 음악도 고음질 스테레오로 들을 수 있게 해 준다.



연결을 설정할 때, 연결을 시작하는 디바이스는 SDP(Service Discovery Protocol) 프로토콜을 사용해서 상대방 디바이스가 어떤 서비스를 제공하는가, 즉 어떤 프로파일을 구현했는가를 알아낼 수도 있다.

각 서비스는 128비트 숫자 식별자(UUID)를 사용해 정의되어 있다. 일반적으로는 이 식별자의 짧은 형태가 사용된다.

  • 0x00000000-0000-1000-8000-00805f9b34fb같은 128비트 베이스 UUID가 정의되어 있다.
  • 서비스 UUID의 짧은 형태는 앞쪽의 8개 0을 대치한다.
  • 이것이 완전한 서비스 UUID이다.
예를 들어 HSP 서비스의 짧은 형태 UUID는 0x1108이므로 완전한 UUID는 0x00001108-0000-1000-8000-00805f9b34fb가 된다.

SPP


가장 간단하고 임베디드 디바이스와 통신하는데 가장 많이 사용되는 것이 Serial Port Profile(SPP)로 짧은 UUID는 0x1101이다.

이 프로파일은 두 디바이스간 시리얼 링크를 에뮬레이션 한다.

Android


이전 예제에서 스마트폰에 페어링 된 디바이스들을 어떻게 나열하는가를 배웠다. 각 디바이스는 BluetoothDevice 오브젝트의 인스턴스에 대응한다. 이 오브젝트는 통신채널을 열기 위해 두 가지 메소드를 제공한다.

  • createRfcommSocketToServiceRecord(UUID)
  • createInsecureRfcommSocketToServiceRecord(UUID)
두 메소드는 프로파일의 UUID를 요구하고, 첫번째 메소드는 암호화 된 커넥션을 만드는것만 다르다.
메소드가 성공하면 스마트폰과 페어링 된 디바이스간 통신 채널에 대응하는 BluetoothSocket 오브젝트를 리턴한다.

SPP 프로파일을 구현한 디바이스에 어떻게 데이터를 보내는 지 보도록 하자.

먼저 프로파일의 UUID를 정의한다.

UUID SPP_UUID = java.util.UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");

그리고 나서 디바이스와의 통신 채널을 가져온다.

BluetoothSocket btSocket = null;
try {
  btSocket = targetDevice.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
} catch (IOException e) {
  Toast.makeText(this, "Unable to open a serial socket with the device", Toast.LENGTH_SHORT).show();
}

이 지점은 아직 채널이 열리지 않았으니 connect() 메소드를 사용해 디바이스와 연결한다.

 try {
  btSocket.connect();
} catch (IOException e) {
  Toast.makeText(this, "Unable to connect to the device", Toast.LENGTH_SHORT).show();
}

연결되면 BluetoothSocket 오브젝트는 두 개의 Stream을 제공한다. 하나는 데이터를 보내기 위한 것(OutputStream)이고 나머지는 데이터를 받기 위한 스트림(InputStream)이다. 편하게 사용하기 위해 스트림을 통해 글자들을 쉽게 보낼 수 있게 해 주는 OutputStreamWriter 오브젝트를 사용할 수 있다.

try {
  OutputStreamWriter writer = new OutputStreamWriter(btSocket.getOutputStream());
  writer.write("Hello World!\r\n");
  writer.flush();
} catch (IOException e) {
  Toast.makeText(this, "Unable to send message to the device.", Toast.LENGTH_SHORT).show();
}

StreamWriter는 로컬 버퍼를 가지고 있다. 모든 데이터가 보내졌는지 확실하게 하기 위해 마지막에 flush()를 호출하는걸 잊지 말자.

마지막에 소켓을 닫는걸 잊으면 안된다.

try {
  btSocket.close();
} catch (IOException e) {
  Toast.makeText(this, "Unable to close the connection to the device", Toast.LENGTH_SHORT).show();
}

Say hello to...


이 앱은 첫번째 코드를 확장했다. 소스코드는 역시 저자의 github에서 다운받을 수 있다.
  • 폰에 페어링 된 디바이스를 가져온다.
  • 한 디바이스를 클릭하면 SPP를 사용해 연결한 다음 "Hello World!" 문자열을 보낸다.

앱을 테스트 해 보려면 블루투스가 장착된 PC가 필요하고 incoming connection을 허용하도록 설정 해 줘야 한다.

윈도우 트레이에 있는 블루투스 아이콘 위에서 마우스 오른쪽 버튼을 클릭한 다음 Open Settings를 선택한다.


COM Ports 탭을 선택한 다음 Add...을 클릭한다.


Incoming을 선택한다.


새 시리얼 포트의 이름을 기록해 둔다. 여기서는 COM56 이다.


터미널 에뮬레이터(여기서는 PuTTY를 사용)를 사용해서 시리얼 포트에 연결한다.


모든게 문제 없이 잘 되었으면 스마트폰에서 앱을 실행하고 PC를 클릭하면 앱이 블루투스를 통해 메시지를 보내고 전송 확인 메시지가 표시된다.

그리고 PC를 확인해 보면 전송된 메시지가 화면에 표시된다.





위의 앱을 테스트 해 보면 한가지 작은 문제점을 발견할 수 있었을 것이다. 커넥션이 연결되고 “Hello World” 메시지가 보내질 때 까지 앱의 GUI가 응답하지 않는다. 이유는 간단하다. 대부분의 메소드는 “blocking”을 사용한다. 즉 결과를 얻을 때 까지 (또는 타임아웃이 될 때 까지) 프로세스의 실행을 중단한다.

데이터를 예를 들어 만일 소켓에서 데이터를 가져오기 위해 read() 메소드를 호출하면 읽어 올 데이터가 들어올 때 까지 메소드는 실행을 중단한다.

이 문제를 해결하기 위해서 멀티태스킹 앱, 즉 앱이 다른 프로세스를 가지고 있어 각각이 서로 독립적으로 실행되는 앱을 작성하는 법을 배워야만 한다. 


스레드와 GUI


간단하게 하기 위해 앱은 한개 또는 그 이상의 프로세스(스레드)로 구성될 수 있고, 이 프로세스들은 안드로이드 OS에 의해 병렬로 실행된다. 이전 예제와 같이 간단한 앱은 메인 스레드라 불리는 하나의 스레드만 가지고 있다. 이 스레드는 앱의 GUI를 구성하는 컴포넌트들(텍스트 박스, 이미지, 버튼 등)을 관리한다.

멀티스레드 앱을 작성할 때 고려해야만 하는 첫번째 규칙은 “메인 스레드만이 GUI를 업데이트 할 수 있다”는 것이다.


이 규칙은 종종 프로그래머를 골치아프게 만든다. 별도 스레드가 블루투스 소켓에서 데이터를 받는 때를 생각해보자. 일반적으로 명령을 받으면 그에 따라 GUI를 업데이트 해야 한다…

보통 권장되는 해결책은 메인스레드에게 GUI를 업데이트 해 주도록 요구하는 것이다. 여기서는 AsyncTask 오브젝트를 사용하는 다른 방법을 소개하겠다.

AsyncTask


AsyncTask 오브젝트는 안드로이드에 포함되어 백그라운드에서 실행되면서 앱의 GUI와 상호작용을 해야만 하는 태스크들을 쉽게 관리할 수 있게 해 준다.

장점으로는 일부 메소드는 GUI(메인) 스레드에서 실행되고 나머지는 독립된 전용 스레드에서 실행된다는 것이다.

그러므로 개발자는 인터페이스를 업데이트 하기 위해서는 GUI 스레드에서 실행되는 메소드를 사용하고, (예를 들어, 소켓을 통해 데이터를 전송/수신하는것 같이) 메인 스레드를 블럭하면 안되는 백그라운드 동작은 두번째 스레드에서 실행되게 할 수 있다.


메소드들을 좀 더 자세히 보도록 하겠다.

  • onPreExecution() - GUI - 백그라운드 액티비티를 시작하기 바로 전에 실행. 사용자에게 애니메이션이나 메시지 등으로 요구한 동작이 시작된다는 것을 알려주는데 사용할 수 있음
  • doInBackground() - background - 백그라운드 태스크를 수행하는 메인 메소드
  • publishProgress() - background - 보통 doInBackground()에서 호출되어 테스크가 실행되는 중에 “progress”를 알려주는데 사용
  • onProgressUpdate() - GUI -  pubilshProgress()에 의해 호출되 GUI가 실행의 “progress”를 업데이트 할 수 있게 함
  • onPostExecute() & onCancelled() - GUI -  태스크의 끝에 (테스크가 캔슬될 때) 실행되는 메소드 
  • Android application

이 튜토리얼을 위해 개발한 안드로이드 앱은 블루투스를 통해 데이터를 송수신하는 것이다.

소스코드는 저자의github에서 다운받을 수 있다.

  • 사용자가 툴바에 있는 버튼으로 디바이스와 연결/연결해제를 할 수 있음
  • 사용자가 두 버튼중에 하나를 클릭하면 각각 “BUTTON1”, “BUTTON2” 명령을 보냄
  • 수신한것을 TextView에 표시

블루투스를 통한 통신은 BTAsyncTask라는 AsyncTask를 사용해 수행된다. 이것이 어떻게 동작하는지 확인해보자.

Connection


사용자가 페어링 된 디바이스를 선택하면 앱은 그 디바이스로 소켓을 오픈하고 BTAsyncTask 오브젝트의 새 인스턴스를 만들어 BluetoothAdapter에서 얻은 소켓을 넘겨준다.

그리고 나면 앱은 BTAsyncTask의 doInBackground 메소드를 시작시키는 execute() 메소드를 호출해서, 페어링 된 디바이스에서 새 데이터가 들어오기를 기다린다.



Data In


BTAsyncTask가 새 데이터를 받으면 publishProgress()를 호출하면서 데이터를 넘겨준다. 위에서 본 것 처럼 publishProgress() 메소드는 내부적으로 GUI 스레드레서 실행되고 있는 onProgressUpdate() 메소드를 호출해 수신한 데이터를 가지고 GUI를 업데이트 할 수 있다. 



Data Out


사용자가 버튼을 클릭하면 메인 스레드는 BTAsyncTask의 sendCommand() 메소드를 호출해 해당되는 명령을 보낸다.



데모


안드로이드 앱에 응답하기 위한 간단한 .Net 어플리케이션을 만들었다. 다음은 앱이 어떻게 동작하는가를 보여주는 짧은 비디오이다.





* 이 글은 Luca Dentella의 튜토리얼 시리즈 Android e Bluetooth 를 저자의 승락을 받고 번역한 글입니다. 흔쾌히 허락해 준 저자 Luca에게 감사드리며...



Un enorme grazie a Luca~

Bluetooth Programming in Android (1/2)

안드로이드 앱에서 블루투스를 사용하려면 각각 기능에 해당하는 permission이 필요하다. Permission은 안드로이드에게 앱이 어떤 기능을 필요로 하는지 알려주는 역할을 한다. 앱을 설치할 때 어떤 permission을 필요로 하는지 알려주고 허가할 것인지 물어보는 창이 열리는걸 본 적이 있을 것이다.

앱이 어떤  permission을 사용하는지 알려주기 위해 필요한 작업은 다음과 같다.

먼저 프로젝트의 AndroidManifest.xml 파일을 연다.


이 파일을 열면 아래와 같은 화면이 나오는데 'Permissions' 탭을 선택한 다음 오른쪽의 Add... 버튼을 누른다.



버튼을 누르면 나오는 화면에서 Uses Permission을 선택한다.



그 다음 android.permissions.BLUETOOTH를 선택하고 파일을 저장한다.



BluetoothAdapter


BluetoothAdapter는 폰의 블루투스 모듈을 사용하기 위한 오브젝트이다.

스태틱 메소드인 getBluetoothAdapter()를 호출해서 오브젝트의 디폴트 인스턴스를 가져온다.

mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

만일 폰에 블루투스 모듈이 없으면 null을 리턴한다.  이 경우 Toast를 사용해 에러메시지를 표시하고 앱을 종료한다.

Toast.makeText(this, "This app requires a bluetooth capable phone", Toast.LENGTH_SHORT).show();
finish();

블루투스 모듈에 대한 인스턴스를 가져온 다음 모듈이 활성화 되어 있는지 확인해야만 한다. 모듈 활성화 여부를 확인하는데 isEnabled() 메소드를 사용한다.

mBluetoothAdapter.isEnabled();

모듈이 활성화 되어 있지 않으면 Intent를 사용해 사용자에게 활성화 시키도록 요청할 수 있다. 먼저 request를 식별하기 위한 상수를 정의한 다음 새 Intent를 만들어 OS에 보낸다.

private final int REQUEST_ENABLE_BT = 1;
...
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

앱은 백그라운드로 들어가고 아래와 같이 사용자에게 앱이 블루투스를 활성화 시키도록 허가할 것인지 묻는 메시지 팝업이 열린다.



사용자가 request를 허가(또는 거부)하면 안드로이드는 앱의 onActivityResult 메소드를 호출해서 request의 허가/거부를 확인할 수 있다.

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (requestCode == REQUEST_ENABLE_BT)
    if (resultCode == RESULT_OK) listPairedDevices();

페어링 된 디바이스


첫번째 예제에서 앱은 페어링 된 디바이스를 화면에 표시한다. 블루투스 디바이스는 연결해서 사용하기 전에 먼저 페어링 되어야만 한다.

getBondedDevices() 메소드는 BluetoothDevice 오브젝트 집합을 리턴한다.

Set pairedDevices = mBluetoothAdapter.getBondedDevices();

각 디바이스는 이름과 (서로 다른) 주소를 가진다. 루프를 사용해 TextArea에 페어링 된 디바이스들을 표시한다.

for (BluetoothDevice pairedDevice : pairedDevices) {
  textView2.append(Html.fromHtml("<strong>" + pairedDevice.getName() + "</strong>"));
  textView2.append(" (" + pairedDevice.getAddress() + ")\n");
}

아래가 앱의 스크린 캡춰이다.



여기서 사용한 소스코드는 전부 저자의 github에서 다운받을 수 있다.

 Discovery 와 Permissions


새 블루투스 디바이스를 찾기 위한 discovery 기능을 사용하려면 새로운 permission(BLUETOOTH_ADMIN)이 필요하다.



새 검색을 시작하려면 BluetoothAdapter 오브젝트의 startDiscovery 메소드를 호출한다.

mBluetoothAdapter.startDiscovery();

이 메소드는 비동기 메소드이다. 즉 이 메소드를 호출하면 안드로이드에게 새 검색을 시작하라는 요청을 넘겨주고 바로 리턴한다. 새 디바이스가 발견되거나 discovery가 끝난걸 앱에게 알려주기 위해서는 BroadcastReceiver를 사용해야만 한다.

Receiver 와 IntentFilter


앱은 안드로이드에게 어떤 이벤트가 발생한 걸 통보해 달라고 요청할 수도 있다. 예를 들어 문자메시지가 오거나 배터리가 거의 없을 때 같은 경우가 될 수 있다. 이런 이벤트들은 broadcast 메시지를 사용해 통보된다. 그러므로 앱은 BroadcastReceiver 오브젝트를 통해 메시지를 받을 수 있다.

BroadcastReceiver 오브젝트가 인스턴스화 되고 나면 OS에게 어떤 notification을 받을건지 알려줘야만 한다. 받기를 원하는 각 이벤트마다 필터(IntentFilter)를 만든다. 그리고 registerReceiver() 메소드를 사용해 안드로이드가 IntentFilter에 지정되어 있는 이벤트를 주어진 BroadcastReceiver에게 통보하도록 요구한다.



이 예제에서 우리는 2개의 이벤트를 모니터링 할 필요가 있다.
  • Bluetooth.ACTION_FOUND - 새 디바이스가 발견될 때
  • Bluetooth.ACTION_DISCOVERY_FINISHED - discovery가 끝날 때
해당하는 IntentFilter를 만들어 receiver를 등록한다.

IntentFilter deviceFoundFilter = new IntentFilter(Bluetooth.ACTION_FOUND);
IntentFilter discoveryFinishedFilter = new IntentFilter(Bluetooth.ACTION_DISCOVERY_FINISHED);
registerReceiver(mReceiver, deviceFoundFilter);
registerReceiver(mReceiver, discoveryFinishedFilter);

BraodcastReceiver는 onReceive() 메소드를 꼭 만들어줘야만 한다. 이 메소드는 새 이벤트가 발생한 걸 알려주기 위해 안드로이드에 의해 호출된다.

mReceiver = new BroadcastReceiver() {
  public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();

action 스트링을 보면 어떤 이벤트가 발생했는지 알 수 있다.

if (BluetoothDevice.ACTION_FOUND.equals(action)) {
  // show the new device
}
if (BluetoothDevice.ACTION_DISCOVERY_FINISHED).equals(action)) {
  // enable the SCAN button
}


이 예제를 위한 앱은 매우 간단하다. 버튼을 누르면 검색을 시작하고 장치가 발견되면 리스트에 장치의 이름과 주소를 추가한다. 역시 소스코드는 저자의 github에서 다운받으면 된다.


Detect a state change


안드로이드는 멀티태스킹 OS이다. 당신의 앱이 실행되는 동안 다른 앱이나 사용자가 블루투스 모듈의 상태를 바꿔 놓을수도 있다. 예를 들어 모듈을 비활성화 시켜 버릴 수 있다. 그렇기 때문에 안드로이드에게 모듈의 상태에 어떤 변화가 생기면 그걸 통보하도록 요청할 수 있다.

먼저 OS에서의 notification을 받을 BroadcastReceiver를 만든다.

mReceiver = new BroadcastReceiver() {

이 receiver의 onReceive() 메소드에서 들어 온 메시지가 블루투스 모듈의 상태변화에 관한 것인지 확인한다.

public void onReceive(Context context, Intent intent) {
  String action = intent.getAction();
  if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {

상태변화에 관한 것이 맞다면, Intent의 extra parameter에서 변화된 현 상태를 가져올 수 있다. state는 int값이므로 getIntExtra 메소드를 사용해야만 하고 메소드를 호출할 때 파라메타의 이름과 디폴트값을 지정해주면 된다. 디폴트값은 지정한 이름의 파라메타가 없을 때 리턴할 값이다.

int actualState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);

그리고 receiver를 등록하는걸 잊으면 안된다.

IntentFilter stateChangedFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, stateChangedFilter);

블루투스 모듈을 위한 4개의 상태가 정의되어 있다.

  • BluetoothAdapter.STATE_ON
  • BluetoothAdapter.STATE_OFF
  • BluetoothAdapter.STATE_TURNING_ON
  • BluetoothAdapter.STATE_TURNING_OFF
위에 설명한 걸 정리한 앱은 다음과 같다. 앱은 화면에 텍스트와 아이콘으로 현재 상태를 나타내고 상태가 변경될 때 마다 내용을 자동으로 업데이트 한다. 아래 데모 앱의 소스는 저자의 github에서 다운받으면 된다.







* 이 글은 Luca Dentella의 튜토리얼 시리즈 Android e Bluetooth 를 저자의 승락을 받고 번역한 글입니다. 흔쾌히 허락해 준 저자 Luca에게 감사드리며...



Un enorme grazie a Luca~





2010년 5월 5일 수요일

안드로이드 블루투스 프로그래밍 - BlueWatch Project #2





안드로이드는 블루투스 프로토콜 스택을 포함하고 있기 때문에 블루투스 디바이스들과 무선으로 데이터를 교환할 수 있다. 어플리케이션 프레임웍은 안드로이드 블루투스 API를 사용해 블루투스에 억세스 할 수 있다. 블루투스 API를 사용하면 다음과 같은 작업을 할 수 있다.

  • 다른 블루투스 디바이스 검색
  • 페어링 된 블루투스 디바이스를 위한 로컬 블루투스 아답터 퀘리
  • RFCOMM 채널 설정
  • SDP(Service Discovery Protocol)을 통한 다른 디바이스와의 커넥션
  • 양방향 데이터 전송
  • 복수 커넥션 관리

- 기초

이 문서는 블루투스를 사용해 통신하는데 필요한 4가지 주요 태스크(블루투스 셋업, 페어링 되어 있거나 주변에 있는 기기 검색, 디바이스와 연결, 디바이스간 데이터 전송)를 수행하기 위해 안드로이드 블루투스 API를 어떻게 사용하는가를 설명한다.
모든 블루투스 API는 android.bluetooth 패키지에 들어있다.  다음은 블루투스 연결을 만드는데 필요한 클래스들의 요약이다.

  • BluetoothAdapter - 로컬 블루투스 아답터 하드웨어를 나타낸다. BluetoothAdapter는 모든 블루투스를 통한 상호작용의 엔트리포인트이다. 이 객체를 사용해서 다른 블루투스 디바이스 찾기, 페어링 된 디바이스 퀘리, 알려진 MAC address를 사용해 BluetoothDevice 인스턴스 얻기, 다른 디바이스에서 부터의 통신 요구를 기다리기 위한 BluetoothServerSocket 만들기를 할 수 있다.
  • BluetoothDevice - 상대방의 블루투스 디바이스를 나타낸다. 이 객체를 사용하면 BluetoothSocket을 통해 상대방 디바이스와 커넥션을 요구하거나 이름, 주소, 클래스, 페어링 상태등의 정보를 퀘리할 수 있다.
  • BluetoothSocket - 블루투스 소켓을 위한 인터페이스를 나타낸다. 어플리케이션이 InputStream과 OutputStream을 사용해서 다른 블루투스 디바이스와 데이터 교환을 할 수 있는 연결 포인트이다.
  • BluetoothServerSocket – Incoming 리퀘스트를 위해 listen하고 있는 오픈된 서버소켓(TCP ServerSocket과 유사)을 나타낸다. 두대의 안드로이드 디바이스를 연결하기 위해 한쪽의 디바이스는 이 클래스를 사용해서 서버소켓을 오픈해야만 한다. 원격 블루투스 디바이스가 디바이스에 커넥션 리퀘스트를 할 때 BluetoothServerSocket은 커넥션이 연결되면 연결된 BluetoothSocket을 리턴해준다.
  • BluetoothClass - 블루투스 디바이스의 일반적 특성과 기능을 나타낸다. 이 클래스는 디바이스의 디바이스 클래스와 서비스를 정의하는 읽기 전용 속성의 집합이다.

- 블루투스 퍼미션

어플리케이션에서 블루투스 기능을 사용하려면 최소한 BLUETOOTH와 BLUETOOTH_ADMIN 둘중에 하나의 블루투스 퍼미션을 선언해줘야 한다. 커넥션 요구, 커넥션 accept, 데이터 전송등의 블루투스 통신을 하기 위해서는 BLUETOOTH 퍼미션이 필요하다.
디바이스 discovery를 시작하거나 블루투스 설정을 조작하려면 BLUETOOTH_ADMIN 퍼미션이 필요하다.
BLUETOOTH_ADMIN 퍼미션을 사용하려면 BLUETOOTH 퍼미션도 꼭 있어야만 한다. 매니페스트 파일에 블루투스 퍼미션을 선언해준다.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />




- 블루투스 셋업

어플리케이션이 블루투스로 통신을 하기 전에 디바이스가 블루투스를 지원하는지 확인할 필요가 있다. 그리고 블루투스를 지원한다면 활성화 되었는지도 확인해줘야 한다. 만일 블루투스를 지원하지 않으면 블루투스 기능을 비활성화 시켜야 한다. 블루투스를 지원하지만 활성화 되어 있지 않으면 사용자가 어플리케이션을 떠나지 않고 블루투스를 활성화하도록 요구할 수 있다. 이 작업은 BluetoothAdapter를 사용해서 두 단계로 수행할 수 있다.

1.BluetoothAdapter 를 얻는다.
모든 블루투스 액티비티를 위해 BluetoothAdapter가 요구된다. BluetoothAdapter를 얻기 위해서는 스태틱 메소드인 getDefaultAdapter()를 호출하면 된다. 그러면 디바이스의 블루투스 아답터를 나타내는 BluetoothAdapter 인스턴스를 리턴한다.

BluetoothAdapter mBTAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBTAdapter == null) {
    // device does not support Bluetooth
}


2.블루투스 활성화
블루투스가 활성화 되어있는지 확인해야 한다. isEnabled()를 호출해서 블루투스가 현재 활성화되어 있는지 확인한다. 메소드가 false를 리턴하면 블루투스가 비활성화되어 있는 것이다. 블루투스를 활성화 시키려면 ACTION_REQUEST_ENABLE 인텐트로 startActivityForResult()를 호출하면 된다.

If (!mBTAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}


그림 1과 같이 블루투스를 활성화하기 위한 퍼미션을 요구하는 대화창이 나타난다. 사용자가 “Yes”를 선택하면 시스템은 블루투스를 활성화시키고 그 과정이 끝나면 어플리케이션으로 포커스가 돌아오게 된다.
블루투스 활성화가 성공하면 액티비티는 onActivityResult() 콜백에서 RESULT_OK를 리턴받게 된다. 블루투스가 에러로 인해 (또는 사용자가 “No”를 선택해서) 활성화되지 못하면 RESULT_CANCELED가 리턴된다. 옵션으로 블루투스 상태가 변경될 때 마다 시스템이 브로드캐스하는 ACTION_STATE_CHANGED 인텐트를 listen하도록 할 수도 있다.

- 디바이스 검색

BluetoothAdapter를 사용하면 디바이스 discovery 또는 페어링 된 디바이스 목록을 퀘리해서 원격 블루투스 디바이스를 찾을 수 있다.
디바이스 discovery는 주변의 활성화 된 블루투스 디바이스를 찾고 각각에 대한 정보를 요구하는 검색 단계이다. 하지만 통신가능 범위에 들어있는 블루투스 디바이스라 해도 현재 discoverable 하도록 활성화 되어 있어야만 discovery 요구에 응답한다. 디바이스가 discoverable 상태인 경우 discovery 요구에 디바이스 이름, 클래스, MAC 주소같은 정보를 공유함으로서 응답한다. 이 정보를 사용해서 discovery를 수행한 디바이스는 발견된 디바이스에 커넥션을 시작하도록 선택할 수 있다.
일단 원격 디바이스와 처음으로 연결이 이루어지면 자동으로 사용자에게 페어링을 할 것인가 물어보게 된다.  디바이스 페어링이 이루어지면 상대 디바이스에 대한 기본 정보(디아비스 이름, 클래스, MAC 주소 등)가 저장되고 그 내용은 블루투스 API를 통해 읽을 수 있게 된다. 이미 알고 있는 원격디바이스의 MAC 주소를 사용하면 아무때나 (물론 해당 디바이스가 통신 가능범위에 있다는 가정 하에) discovery를 수행할 필요 없이 바로 커넥션 과정을 시작할 수 있다.
페어링과 연결된것의 차이점은 잘 알고 있어야 한다. 페어링은 두 디바이스가 각자 상대방의 존재를 알고 있고 인증과정에 사용할 link-key를 공유하고 있어 서로간에 암호화 된 연결을 설정할 수 있다는걸 의미한다. 연결된것은 디바이스가 현재 RFCOMM 채널을 공유하고 있어 서로 데이터를 전송할 수 있는 상태를 의미한다.
현재 안드로이드 블루투스 API는 RFCOMM 커넥션을 설정하기 전에 디바이스가 페어링 되어야만 한다. (블루투스 API에서 암호화된 커넥션을 시작하려고 할 때 페어링이 자동을 이루어진다.)
다음의 섹션은 페어링 된 디바이스를 찾거나, 디바이스 discovery를 사용해 새 디바이스를 찾는 방법을 설명한다.
주: 안드로이드 디바이스는 기본적으로 not discoverable 상태이다. 시스템 설정을 통해 짧은 시간동안 디바이스를 discoverable 상태로 만들거나 어플리케이션에서 직접 discoverable 상태로 만들어 줄 수 있다.

- 페어링 된 디바이스 퀘리

디바이스 discovery를 수행하기 전에 원하는 디바이스가 이미 페어링 되어 있는가 확인해 볼 필요가 있다.  확인하기 위해서 getBondedDevices()를 호출하면 된다. 그러면 페어링 된 디바이스들의 집합인 BluetoothDevices 를 돌려준다. 예를 들어 페어링 된 모든 디바이스를 퀘리한 다음 ArrayAdapter를 사용해 페어링 된 각 디바이스의 이름을 보여줄 수 있다.

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() <> 0) {
    for (BluetoothDevice device : pairedDevices) {
        mArrayAdapter.add(device.getName() + “\n” + device.getAddress());
    }
}


BluetoothDevice 객체에서 연결을 시작하기 위해 필요한 정보는 MAC address만 있으면 된다. 위의 예제에서 이 정보는 사용자에게 보여지는 ArrayAdapter의 일부분에 저장되어 있다. MAC 주소는 나중에 연결을 시작하기 위해 추출할수도 있다.

- 디바이스 discovery

디바이스 discovery를 시작하려면 startDiscovery()를 호출하면 된다. 이 과정은 비동기식이라 메소드를 호출하면 discovery가 성공적으로 시작되었나 결과를 알려주는 boolean값을 곧바로 돌려준다. Discovery과정은 보통 12초간의 inquiry scan후 발견된 각 디바이스에 대해 이름을 가져오기 위한 page scan으로 이루어진다.
어플리케이션은 각 발견된 디바이스에 대한 정보를 받기 위해  ACTION_FOUND 인텐트를 위한 BroadcastReceiver를 등록해야만 한다. 각 디바이스마다 시스템이 ACTION_FOUND 인텐트를 브로드캐스트 한다. 이 인텐트는 각각 BluetoothDevice와  BluetoothClass가 들어있는 EXTRA_DEVICE와 EXTRA_CLASS 필드를 전달한다. 예제로 디바이스가 발견되었을 때 브로드캐스트를 처리하는 핸들러를 등록하는 방법이다.

Final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            mArrayAdapter.add(device.getName() + “\n” + device.getAddress());
        }
    }
};

BroadcastReceiverIntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND;
registerReceiver(mReceiver, filter);


커넥션을 시작하기 위해 BluetoothDevice 객체에서 필요한 정보는 MAC 주소뿐이다. 이 예에서는 사용자에게 보여지는 ArrayAdapter의 일부분에 저장되어 있다.

주의: 디바이스  discovery를 수행하는건 블루투스 아답터에게 매우 부담이 큰 작업으로 매우 많은 리소스를 요구한다. 커넥션 할 디바이스를 찾았다면 커넥션을 시작하려고 시도하기 전에  cancelDiscovery()를 호출해서 discovery를 멈춰야 한다. 또한 이미 다른 디바이스와 커넥션 되어 있으면   discovery과정동안  대역폭이 활 떨어질수도 있기 때문에 커넥션 된  상태에서는 discovery를 하지 않아야 한다.                                                          
- Discoverable 활성화

다른 디바이스가 자신의 디바이스를 검색할 수 있도록 해 주려면 startActivityForResult(Intent, int)에 ACTION_REQUEST_DISCOVERABLE 액션 인텐트를 넣어 호출해주면 된다. 이 메소드를 호출하면 어플리케이션을 멈추지 않고 시스템 설정을 통해 discoverable 모드를 활성화 하도록 요청한다. 기본적으로 디바이스는 120초동안 discoverable 모드로 있게 된다. EXTRA_DISCOVERABLE_DURATION 인텐트 extra를 추가해서 이 시간을 바꿔줄 수 있다. (최대 300초)

Intent discoverableIntent = new Intent(BluetoothAdpater.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);




그림 2와 같은 다이얼로그가 떠서 사용자에게 디바이스를 discoverable 상태로 만들도록 허가할 것인지 묻는다. “Yes”를 선택하면 디바이스는 정해진 시간동안 discoverable상태가 된다. 액티비티는 result code에 디바이스가 discoverable되는 시간값이 들어가서 onActivityResult() 콜백을 호출받게 된다. 사용자가 “No”를 선택하거나 에러가 발생하면 result code는 Activity.RESULT_CANCELLED가 된다.
디바이스는 discoverable 시간동안 아무 반응이 없이 조용히 있는다. 만일 discoverable모드가 변경될 때 통보를 받고 싶으면 ACTION_SCAN_MODE_CHANGED 인텐트에 대한 BroadcastReceiver를 등록할 수 있다. 이 인텐트에는 각각 이전 스캔모드와 변경된 새 스캔모드가 들어있는 EXTRA_PREVIOUS_SCAN_MODE와 EXTRA_SCAN_MODE라는 extra 필드를 가지고 있다. 각 필드에 들어갈 수 있는 값은 SCAN_MODE_CONNECTABLE_DISCOVERABLE,  SCAN_MODE_CONNECTABLE,  SCAN_MODE_NONE으로 각각 discoverable 모드, discoverable은 아니지만 커넥션을 받아들일 수는 있는 모드, discoverable도 아니고 커넥션도 받아들일 수 없는 모드를 나타낸다.
원격 디바이스와 커넥션을 시작하고 싶은 경우는 자신의 디바이스를 discoverable모드로 만들 필요는 없다. 원격 디바이스가 커넥션을 시작하기 전에 디바이스를 발견해야만 하기 때문에 내 디바이스의 discoverable 모드를 활성화 시키는건 어플리케이션이 서버소켓을 사용해서 incoming 연결을 accept할 때만 필요하다.

- 디바이스 커넥션

두 디바이스에서 실행되는 어플리케이션간에 커넥션을 만들기 위해서는 서버쪽과 클라이언트쪽 메카니즘을 모두 구현해 줘야만 한다. 한 디바이스는 서버소켓을 열어줘야 하고 다른 디바이스가 서버 디바이스의 MAC 주소를 사용해서 커넥션을 시작해야만 하기 때문이다. 서버와 클라이언트는 같은 RFCOMM 채널에 각각 커넥션 된 BluetoothSocket을 가지고 있을 때 서로 커넥트 된 것으로 간주된다. 이 지점에서 각 디바이스는 입, 출력 스트림을 얻어 데이터 전송을 시작할 수 있다. 이 섹션에서는 두 디바이스간에 커넥션을 시작하는 방법에 대해서 설명한다.
서버 디바이스와 클라이언트 디바이스는 서로 다른 방법으로 필요한 BluetoothSocket을 얻는다. 서버는 incoming 연결이 accept될 때 소켓을 받게 된다. 클라이언트는 서버로의 RFCOMM 채널을 열 때 소켓을 받게 된다.



한가지 구현 테크닉은 두 디바이스를 모두 서버로 동작하도록 하기 위해 서버소켓을 열고 커넥션을 기다리는 것이다. 그러면 어느 디바이스건 클라이언트로서 상대 디바이스로 커넥션을 시작할 수 있다. 다른 방법으로는 한 디바이스는 명시적으로 서버로 지정해 서버소켓을 열고 커넥션을 기다리고 다른 디바이스는 단순히 클라이언트로 커넥션을 시작할 수 있다.
주) 두 디바이스가 미리 페어링 되어 있지 않으면 안드로이드 프레임웍은 그림 3과 같이 자동으로 페어링을 요구하는 다이얼로그를 띄워준다. 그러므로 디바이스를 커넥트 하려고 할 때 어플리케이션은 디바이스가 미리 페어링 되어 있는지 여부를 걱정할 필요가 없다. RFCOMM 커넥션 시도는 사용자가 성공적으로 페어링을 마치거나 페어링을 거부하거나 또는 어떤 이유로건 페어링이 실패할 때 까지 블럭된다.

서버로 동작
두 디바이스를 커넥트하려고 할 때 하나의 디바이스는 BluetoothServerSocket을 열어 서버로 동작해야만 한다. 서버소켓의 목적은 incoming 커넥션 요구를 기다리다 accept되면 커넥션 된 BluetoothSocket을 제공해 주는 것이다. BluetoothServerSocket에서 BluetoothSocket이 얻어지고 더 이상의 커넥션을 accept할 필요가 없으면  BluetoothServerSocket은 제거해도 된다.

UUID란...
Universally Unique IDentifier(UUID)는 유일하게 정보를 식별하는데 사용하기 위한 128비트 포맷의 표준화 된 문자열 ID이다. UUID의 포인트는 이 숫자가 충분히 크기 때문에 랜덤하게 아무 숫자나 골라도 다른 UUID들과 겹치지 않는다는 것이다. 여기서는 어플리케이션의 블루투스 서비스를 식별하는데 사용된다. 어플리케이션에 사용할 UUID를 얻기 위해서 인터넷상의 여러가지 랜덤 UUID 생성기중에 하나를 사용할 수 있고 fromString(String)으로 UUID를 초기화 하면 된다.
서버소켓을 셋업하고 연결을 accept하는 기본적인 절차이다.

1.listenUsingRfcommWithServiceRecord(String, UUID)를 호출해서 BluetoothServerSocket을 얻어온다.
스트링은 서비스에 대한 식별할 수 있는 이름으로 시스템이 디바이스의 새 SDP 데이터베이스 엔트리에 자동으로 그 이름을 기록한다. UUID 또한 SDP엔트리에 포함되어 클라이언트와 커넥션 agreement를 위한 기초가 된다. 즉 클라이언트가 디바이스와 커넥션하려고 시도할 때 커넥션하길 원하는 서비스를 유일하게 식별하는 UUID를 제공한다. 커넥션이 이뤄지기 위해서는 이 UUID가 일치해야만 한다.

2.accept()를 호출해서 커넥션 요구를 listen하기 시작한다.
이 메소드는 블럭킹 호출이다. 커넥션이 accept되거나 익셉션이 발생해야만 리턴된다. 리모트 디바이스가 listen하고 있는 서버소켓에 등록한 UUID와 일치하는 커넥션 요구에만 연결이 만들어진다. 성공하면 accept()는 커넥션 된 BluetoothSocket을 리턴한다.


3.더 이상의 추가 커넥션이 필요하지 않으면 close()를 호출한다.
이 메소드를 호출하면 서버소켓과 관련된 리소스를 release한다. 하지만 accept()가 리턴한 커넥션 된 BluetoothSocket은 닫지 않는다. TCP/IP와 달리 RFCOMM은 클라이언트에서 한번에 하나의 커넥션만 허용하기 때문에 대부분의 경우에 커넥션이 만들어지면 곧바로 BluetoothServerSocket을 close()하는게 합리적이다.

accept()는 블럭킹 메소드라 어플리케이션의 다른 동작을 막기 때문에 메인 액티비티 UI 스레드에서 호출하면 안된다. 일반적으로 새로운 스레드에서 BluetoothSocket이나 BluetoothServerSocket에 관련된 모든 작업을 처리하는게 합리적이다. 다른 스레드에서 BluetoothServerSocket의 accept() 같이 블럭킹 된 것을 취소하고 바로 리턴하도록 하려면 close()를 호출하면 된다. 그리고 BluetoothServerSocket 또는 BluetoothSocket의 모든 메소드는 스레드-세이프하다.

예제) incoming 연결을 accept하는 서버 컴포넌트를 위한 간단한 스레드
private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;
  
    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code
            tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }
    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // If a connection was accepted
            if (socket != null) {
                // Do work to manage the connection (in a separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
    /** Will cancel the listening socket, and cause the thread to finish */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}
이 예제에서 한개의 incoming 커넥션만 필요하기 때문에 커넥션이 accept되고 BluetoothSocket이 얻어지자 마자 어플리케이션은 얻은 BluetoothSocket을 별도의 스레드로 보낸 다음 BluetoothServerSocket을 닫고 루프를 빠져나온다.
accept()가 BluetoothSocket을 리턴할 때 소켓은 이미 커넥션 되어 있기 때문에 따로 connect()를 호출할 필요는 없다. manageConnectedSocket()은 어플리케이션에서 데이터 전송을 위한 스레드를 시작하는 fictional 메소드이다.
일반적으로 incoming 커넥션을 listen하는게 끝나면 곧바로 BluetoothServerSocket을 닫아준다. 이 예제에서도 BluetoothSocket이 얻어지자 마자 close()를 호출했다. 또한 listen하고 있는 서버소켓을 멈출 필요가 있을 때 private BluetoothSocket을 닫을 수 있는 public 메소드를 스레드에서 제공하기도 한다.

클라이언트로 동작
원격 디바이스와 커넥션을 시작하려면 우선 원격 디바이스를 나타내는 BluetoothDevice 객체를 얻어야만 한다. 그리고 나면 BluetoothDevice를 사용해서 BluetoothSocket을 얻어 커넥션을 시작한다.

기본적인 절차이다.

1.BluetoothDevice를 사용해서 createRfcommSocketToServiceRecord(UUID)를 호출해서 BluetoothSocket을 얻는다.
이 호출은 BluetoothDevice에 연결하는 BluetoothSocket을 초기화한다. 여기서 건네지는 UUID는 서버 디바이스가 자신의 BluetoothServerSocket(listenUsingRfcommWithServiceRecord(String, UUID)를 사용해서)을 열었을 때 사용한 UUID와 일치해야만 한다. 동일한 UUID를 사용하는건 UUID스트링을 어플리케이션 코드에 하드코딩하고 서버와 클라이언트 양쪽 코드에서 그걸 참조하면 되는 간단한 문제이다.

2.connect()를 호출해서 연결을 시작한다.
시스템은 UUID를 매치하기 위해 원격 디바이스 SDP lookup을 수행한다. Lookup이 성공하고 원격 디바이스가 커넥션을 accept하면 연결동안 사용할 RFCOMM채널을 공유하고 connect()가 리턴한다. 이 메소드는 블럭킹 호출이다. 어떤 이유로건 커넥션이 실패하거나 connect() 메소드가 time out (약 12초)이 되면 exception을 발생한다.
connect()는 블럭킹 호출이기 때문에 이 커넥션 절차는 언제나 메인 액티비티 스레드와 독립된 별개의 스레드에서 수행되어야만 한다.
주: connect()를 호출할 때 디바이스는 언제나 디바이스 discovery를 수행하고 있지 않는지 확인해야만 한다. Discovery가 진행중이면 커넥션 시도는 확연히 느려져서 실패할 가능성이 커진다.

예제) Bluetooth 커넥션을 시작하는 스레드

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket,
        // because mmSocket is final
        BluetoothSocket tmp = null;
        mmDevice = device;
        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            // MY_UUID is the app's UUID string, also used by the server code
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it will slow down the connection
        mAdapter.cancelDiscovery();
        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }
    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

cancelDiscovery()는 커넥션이 만들어지기 전에 호출되는걸 볼 수 있다. 커넥션이 되기 전에라도 언제나 호출할 수 있고 실제적으로 실행 여부를 확인하지 않고 호출해도 안전하다. (하지만 그래도 상태를 확인하고 싶으면 isDiscovering()을 사용하면 된다.) manageConnectedSocket()은 데이터 전송을 위한 스레드를 시작하는 어플리케이션에 있는 fictional 메소드이다.
BluetoothSocket이 끝나면 clean up을 위해 언제나 close()를 호출해줘야 한다. 이 메소드를 호출해 줌으로서 곧바로 커넥션 된 소켓을 닫고 내부 리소스를 clean up 하게 된다.

- 연결 관리

두 디바이스를 성공적으로 커넥션하게 되면 각 디바이스는 커넥션 된 BluetoothSocket을 가지게 된다. 이 소켓을 통해 디바이스간에 데이터를 교환할 수 있게 된다. BluetoothSocket을 사용해서 임의의 데이터를 전송하기 위한 일반적 절차는 매우 간단하다.

1.각각 getInputStream()과 getOutputStream()을 사용해 소켓을 통한 전송을 처리할 InputStream과 OutputStream을 얻는다.
2.read(byte[])와 write(byte[])를 사용해서 데이터를 읽고 쓴다.

물론 implementation을 위해 고려해야 할 세부사항들이 있다. 먼저 무엇보다 모든 읽고 쓰기를 위한 별도의 스레드를 사용해야 한다. 이건 read(byte[])와 write(byte[])는 모두 블럭킹 호출이기 때문에 매우 중요하다. read(byte[])는 스트림에서 무언가 읽을게 있을때까지 블럭되어 있는다. write(byte[])는 일반적으로는 블럭되지 않지만 원격 디바이스가 충분히 빠르게 read(byte[])를 호출하지 않아 버퍼가 꽉 차는 경우 플로우 컨트롤을 위해 블럭될수도 있다. 그러므로 스레드의 메인 루프는 InputStream으로부터 읽기 전용으로 사용되어야 한다. 스레드의 분리된 public 메소드가 OutputStream으로 쓰기를 시작하도록 사용될 수 있다.

예제)
private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;

    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        // Get the input and output streams, using temp objects because
        // member streams are final
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }

    public void run() {
        byte[] buffer = new byte[1024];    // buffer store for the stream
        int bytes; // bytes returned from read()
        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI Activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
    /* Call this from the main Activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }

    /* Call this from the main Activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}
컨스트럭터가 필요한 스트림을 얻고 한번 실행되면 스레드는 InputStream을 통해 들어오는 데이터를 기다린다. read(byte[])가 스트림에서의 데이터를 리턴하면 그 데이터는 부모 클래스의 Handler 멤버를 사용해 메인 액티비티로 보내진다. 그리고 다시 스트림에서 데이터를 읽기 위해 기다리기 위해 돌아간다. Outgoing 데이터를 보내는건 단순히 메인 액티비티에서 스레드의 write() 메소드를 호출해 전송할 데이터를 전달해주면 된다.

스레드의 cancel() 메소드는 아무때나 BluetoothSocket을 닫아 connection을 멈출 수 있기 때문에 중요하다. 이 메소드는 블루투스 connection 사용이 끝나면 언제나 호출되어야 한다.

-----

Bluetooth programming in Android 1/2

Bluetooth programming in Android 2/2

두개의 관련 포스트가 추가되었습니다.

2010년 4월 29일 목요일

BlueWatch Project #1



Bluetooth Watch 연동 프로그램을 만들려면 먼저 폰에 언제 전화가 오거나 문자메시지를 받았는지 알 수 있어야만 한다. 안드로이드는 이벤트가 발생하면 해당 intent를 브로드캐스트 해 주기 때문에 BroadcastReceiver를 설정해주면 원하는 인텐트를 수신할 수 있게 된다.

        // TelephonyManager for receiving phone state change
        // (IDLE, OFFHOOK, RINGING)

        MyPhoneStateListener phoneListener=new MyPhoneStateListener();
        TelephonyManager telephony = (TelephonyManager)
        getSystemService(Context.TELEPHONY_SERVICE);
        telephony.listen(phoneListener,PhoneStateListener.LISTEN_CALL_STATE);

전화상태 변경(OFFHOOK, IDLE, RINGING)에 대한 브로드캐스트를 받는 리시버를 설정해주는 코드로 여기서 지정해 준 callback 함수인 phoneListener가 전화기의 상태가 변경될 때 마다 호출된다. 
               
        // BroadcastReceiver for SMS
        BroadcastReceiver rcvIncoming = new BroadcastReceiver() {
            public void onReceive(Context context, Intent intent) {
            Log.i("NAUTES", "SMS received");
            Bundle data=intent.getExtras();
                if (data!=null) {
                    Object pdus[] = (Object[])data.get("pdus");
                    String message = "New message:\n";
                    String sender = null;
                    for (Object pdu:pdus) {
                        SmsMessage part=SmsMessage.createFromPdu((byte[])pdu);
                        message += part.getDisplayMessageBody();
                        if (sender==null) {
                            sender = part.getDisplayOriginatingAddress();
                        }
                    }
                    Log.i("NAUTES", "From: " + sender);
                    Log.i("NAUTES", "Message: " + message);
                    // Do something using received message
                }
            }
        };
        registerReceiver(rcvIncoming, new IntentFilter("android.provider.Telephony.SMS_RECEIVED"));

rcvIncoming은 SMS가 수신되었다는 브로드캐스트를 받는 리시버로 문자메시지가 수신될 때 마다 호출된다.

위의 코드를 사용혀려면 AndroidManifest.xml파일에

<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
<uses-permission android:name="android.permission.RECEIVE_SMS"></uses-permission>

가 추가되어 있어야만 한다.

--
MyPhoneStateListener의 코드는 다음과 같다. 호출되는 경우 파라미터로 state에 변경된 상태, incomingNumber에 발신자 번호가 들어있다.

public class MyPhoneStateListener extends PhoneStateListener {
      public void onCallStateChanged(int state,String incomingNumber){
      switch(state){
        case TelephonyManager.CALL_STATE_IDLE:
        break;
        case TelephonyManager.CALL_STATE_OFFHOOK:
            Log.i("NAUTES", "OFFHOOK");
        break;
        case TelephonyManager.CALL_STATE_RINGING:
            Log.i("NAUTES", "RINGING from " + incomingNumber);
        break;
        }
      }
}