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를 업데이트 해야 한다…
보통 권장되는 해결책은 메인스레드에게 GUI를 업데이트 해 주도록 요구하는 것이다. 여기서는 AsyncTask 오브젝트를 사용하는 다른 방법을 소개하겠다.
AsyncTask 오브젝트는 안드로이드에 포함되어 백그라운드에서 실행되면서 앱의 GUI와 상호작용을 해야만 하는 태스크들을 쉽게 관리할 수 있게 해 준다.
장점으로는 일부 메소드는 GUI(메인) 스레드에서 실행되고 나머지는 독립된 전용 스레드에서 실행된다는 것이다.
그러므로 개발자는 인터페이스를 업데이트 하기 위해서는 GUI 스레드에서 실행되는 메소드를 사용하고, (예를 들어, 소켓을 통해 데이터를 전송/수신하는것 같이) 메인 스레드를 블럭하면 안되는 백그라운드 동작은 두번째 스레드에서 실행되게 할 수 있다.
보통 권장되는 해결책은 메인스레드에게 GUI를 업데이트 해 주도록 요구하는 것이다. 여기서는 AsyncTask 오브젝트를 사용하는 다른 방법을 소개하겠다.
AsyncTask
AsyncTask 오브젝트는 안드로이드에 포함되어 백그라운드에서 실행되면서 앱의 GUI와 상호작용을 해야만 하는 태스크들을 쉽게 관리할 수 있게 해 준다.
장점으로는 일부 메소드는 GUI(메인) 스레드에서 실행되고 나머지는 독립된 전용 스레드에서 실행된다는 것이다.
그러므로 개발자는 인터페이스를 업데이트 하기 위해서는 GUI 스레드에서 실행되는 메소드를 사용하고, (예를 들어, 소켓을 통해 데이터를 전송/수신하는것 같이) 메인 스레드를 블럭하면 안되는 백그라운드 동작은 두번째 스레드에서 실행되게 할 수 있다.
메소드들을 좀 더 자세히 보도록 하겠다.
이 튜토리얼을 위해 개발한 안드로이드 앱은 블루투스를 통해 데이터를 송수신하는 것이다.
소스코드는 저자의github에서 다운받을 수 있다.
- onPreExecution() - GUI - 백그라운드 액티비티를 시작하기 바로 전에 실행. 사용자에게 애니메이션이나 메시지 등으로 요구한 동작이 시작된다는 것을 알려주는데 사용할 수 있음
- doInBackground() - background - 백그라운드 태스크를 수행하는 메인 메소드
- publishProgress() - background - 보통 doInBackground()에서 호출되어 테스크가 실행되는 중에 “progress”를 알려주는데 사용
- onProgressUpdate() - GUI - pubilshProgress()에 의해 호출되 GUI가 실행의 “progress”를 업데이트 할 수 있게 함
- onPostExecute() & onCancelled() - GUI - 태스크의 끝에 (테스크가 캔슬될 때) 실행되는 메소드
- Android application
이 튜토리얼을 위해 개발한 안드로이드 앱은 블루투스를 통해 데이터를 송수신하는 것이다.
소스코드는 저자의github에서 다운받을 수 있다.
- 사용자가 툴바에 있는 버튼으로 디바이스와 연결/연결해제를 할 수 있음
- 사용자가 두 버튼중에 하나를 클릭하면 각각 “BUTTON1”, “BUTTON2” 명령을 보냄
- 수신한것을 TextView에 표시
블루투스를 통한 통신은 BTAsyncTask라는 AsyncTask를 사용해 수행된다. 이것이 어떻게 동작하는지 확인해보자.
사용자가 페어링 된 디바이스를 선택하면 앱은 그 디바이스로 소켓을 오픈하고 BTAsyncTask 오브젝트의 새 인스턴스를 만들어 BluetoothAdapter에서 얻은 소켓을 넘겨준다.
그리고 나면 앱은 BTAsyncTask의 doInBackground 메소드를 시작시키는 execute() 메소드를 호출해서, 페어링 된 디바이스에서 새 데이터가 들어오기를 기다린다.
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~