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~