관리 메뉴

드럼치는 프로그래머

[안드로이드] Android 블루투스 개발 본문

★─Programing/☆─Android

[안드로이드] Android 블루투스 개발

드럼치는한동이 2016. 6. 3. 13:13

안드로이드 Bluetooth 프로그래밍을 위해 http://developer.android.com/guide/topics/connectivity/bluetooth.html의 내용을 정리함.

안드로이드 플랫폼은 블루투스네트웍스택을 제공하여 다음의 블루투스의 기능을 제공한다.

- 다른 디바이스의 스캔

- 페어링을 위해 로칼디바이스에 쿼리 

- RFCOMM채널 연결 가능

- 서비스 디스커버리를 통한 타디바이스 연결

- 디바이스간 데이터 전송

- 다중연결 관리


기초

블루투스셋업, 페어링된 또는 유효한 디바이스 찾기, 연결하기 그리고 데이터 전송하기에 대한 기초적인 내용을 기술하겠다.

android.bluetooth패키지에 모든 API들이 정의되어 있다. 다음에 설명되는 클래스들이 대표적인 클래스들이다.

BluetoothAdapter

  로컬블루투스어댑터를 대표한다. 이 객체를 통해서 디바이스를 찾고 연결하고 디바이스를 활성화하여 통신을 수행하는등의 작업이 가능하다.

BluetoothDevice

  원격블루투스디바이스를 대표한다. 이 객체를 통해 블루투스소켓을 통한 연결이나 디바이스의 속성정보를 요청할 수 있다.

BluetoothSocket

  블루투스소켓의 인터페이스를 대표한다. TCP socket과 유사하다. 입출력스트림을 통해 다른 디바이스와 데이터를 주고받을 수 있다.

BluetoothServerSocket

  들어오는 요청을 수신할 수 있는 서버소캣을 대표한다. TCP ServerSocket과 유사하다. 

BluetoothClass

  블루투스디바이스의 특성과 사양을 나타낸다. 읽기전용속성이며 major, minor 디바이스 클래스로 디바이스의 성격을 규정한다.  

BluetoothProfile

  블루투스프로파일을 나타낸다. 블루투스기반의 통신을 위한 무선인터페이스 규격을 말한다. 예를 들어 핸즈프리 프로파일같은 것을 말한다. 더 자세한 내용은 http://developer.android.com/guide/topics/connectivity/bluetooth.html#Profiles 를 참조하라.

BluetoothHeadset

  모바일폰을 위한 블루투스헤드셋을 지원한다. 핸즈프리(v1.5)프로파일을 포함한다.

BluetoothA2dp

  오디오전송을 위한 품질을 정의한다. A2DP(Advanced Audio Distribution Profile)

BluetoothHealth

  건강관련 디바이스 프로파일

BluetoothHealthCallback

  BluetoothHealth 콜백을 구현하기 위한 추상클래스. 애플리케이션의 등록상태나 블루투스채널의 변경이나 업데이트의 수신을 감지하기 위해 이 클래스를 반드시 구현해야 한다.

BluetoothHealthAppCOnfiguration

  Bluetooth Health 3rd-party애플리케이션을 health device에 연결하기 위한 설정

BluetoothProfile.ServiceListener

  Bluetooth 서비스가 연결 또는 차단되었을 때 BluetoothProfile IPC 클라이언트에 통지를 한다. 즉 내부에서 서비스는 각각의 프로파일로 동작한다.


블루투스 권환

BLUETOOTH와 BLUETOOTH_ADMIN 권한이 준비되어 있다.

BLUETOOTH : 블루투스통신을 위한 권한

BLUETOOTH_ADMIN :  디바이스 디스커버리나 장치설정등을 할 수 있는 권한.


블루투스 셋업

블루투스를 사용하기 전에 디바이스가 블루투스가 가능한지 살펴야 한다.

1. BluetoothAdapter를 얻어낸다.

  1. BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
       
    // Device does not support Bluetooth
    }

2. 블루투스를 활성화한다.

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

위와 같이 호출하면 블루투스활성화여부 팝업이 뜨며 사용자가 확인을 선택하면 RESULT_OK가 onActivityResult()에서 리턴되고 그러지 않으면 RESULT_CANCELED가 리턴된다.

이 때 앱은 onPause와 onResume루틴을 타게 되니 유의한다.


디바이스 찾기

BluetoothAdapter를 통해 근처의 디바이스들을 찾을 수 있으며 페어링된 다바이스리스트도 얻을 수 있다.

디바이스디스커버리는 주변의 블루투스가 활성화되어 있는 디바이스들을 스캔한다. 그리고 각 디바이스의 정보를 수집한다. 수집이 가능한 디바이스는 블루투스가 활성화되어 있고 검색가능한 상태로 셋팅되어 있어야 한다. 그러면 검색되어 지고 디바이스명, 클래스 그리고 MAC주소가 수집이 가능하다. 이 정보를 기반으로 특정 디바이스를 선택하여 연결할 수 있다.

한번 연결이 이루어진 디바이스는 자동으로 페어링이 요청되게 된다. 디스커버리할때 저장된 MAC주소정보를 통해 디서커버리 단계 없이 바로 연결을 할 수 있다.

페어링과 연결은 한가지 차이점이 있다. 페어링은 두 디바이스가 서로 존재함을 인식하는 것이고 인증을 위해 공유된 link-key를 가지고 서로 암호화된 연결을 확립할 수 있다.

연결은 디바이스간에 RFCOMM채널을 공유하였다는 것이고 서로 데이터를 주고 받을 수 있다는 것이다. 페어링이 먼저 되어야 통신을 수행할 수 있다.

안드로이드가 탑재된 제품들은 discoverable이 꺼진 상태이다. 따라서 페어링을 위해 discoverable을 활성화하여야 한다. http://developer.android.com/guide/topics/connectivity/bluetooth.html#EnablingDiscoverability 를 참조하라.


페어링된 다바이스 얻기

디스커버리를 수행하기 전에 현재 페어링된 디바이스를 알아낼수 있다. getBondedDevices()메서드로 페어링된 디바이스들의 리스트를 얻을 수 있다.

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// If there are paired devices
if (pairedDevices.size() > 0) {
   
// Loop through paired devices
   
for (BluetoothDevice device : pairedDevices) {
       
// Add the name and address to an array adapter to show in a ListView
        mArrayAdapter
.add(device.getName() + "\n" + device.getAddress());
   
}
}

검색된 디바이스에는 연결을 위한 MAC주소정보가 포함되어 있으며 연결하기는 다음을 참조하라

http://developer.android.com/guide/topics/connectivity/bluetooth.html#ConnectingDevices


디바이스 디스커버리

startDiscovery()메서드를 수행하면 된다. 프로세스는 비동기로 동작하고 메서드는 바로 리턴된다. 

비동기로 진행되는 과정에 디바이스가 검색이 되면 ACTION_FOUND 인텐트가 브로드캐스트 되며 이 이벤트를 받기 위해 BroadcastReceiver를 사용해야 한다.

// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
   
public void onReceive(Context context, Intent intent) {
       
String action = intent.getAction();
       
// When discovery finds a device
       
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
           
// Get the BluetoothDevice object from the Intent
           
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
           
// Add the name and address to an array adapter to show in a ListView
            mArrayAdapter
.add(device.getName() + "\n" + device.getAddress());
       
}
   
}
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver
(mReceiver, filter); // Don't forget to unregister during onDestroy

디스커버리 과정은 전원을 많이 소비사는 과정이므로 검색이 다 끝나면 반드시 cancelDiscovery()를 호출해주어야 한다. 그리고 연결이 되어 있는 상태에서 디스커버리는 통신데이터 bandwidth를 현저하기 떨어뜨리기 때문에 연결중에는 디스커버리를 하지 않아야 한다.


디스커버러블 활성화

ACTION_REQUEST_DISCOVERABLE 액션인텐트를 startActivityForResult로 호출한다. EXTRA_DISCOVERABLE_DURATION 으로 탐색시간을 파라메터로 넘길 수 있다. 최대시간은 3600초이고 0이하는 내부적으로 120초로 자동 셋팅된다. 다음은 300초로 셋팅한 예이다.

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

디바이스가 블루투스가 활성화가 안되어 있다면 자동으로 활성화후 진행한다.

디스커버러블모드는 백그라운드에서 지정한 시간만큼 유지되었다가 끝나게 된다. 만일 이 상태변화를 처리하고 싶다면 ACTION_SCAN_MODE_CHANGED인텐트의 리시버를 통해서 처리하면 된다. 이 인텐트는 추가정보로 EXTRA_SCAN_MODE, EXTRA_PREVIOUS_SCAN_MODE로 현재와 이전 스캔모드를 얻을 수 있다. 

모드는 3가지이다.

- SCAN_MODE_CONNECTABLE_DISCOVERABLE :    디바이스가 둘다 가능한 상태

- SCAN_MODE_CONNECTABLE :  디스커버러블모드는 아니고 연결은 가능한 상태.

-  SCAN_MODE_NONE : 둘다 안되는 상태

디바이스와 이미 연결이 초기화된 상태이면 굳이 디스커버러블을 활성화할 필요는 없다. 디스커버러블을 활성화하는 경우는 앱이 서버소켓을 제공하여 연결시도를 허락하기 위한 경우만 해당된다. 왜냐하면 원격디바이스들은 연결이 가능해지기 이전에 먼저 검색이 가능해야 하기 때문이다.


디바이스 연결

앱이 두단말간에 연결을 생성하기 위해서 서버와 클라이언트단을 모두 구현해야 한다. 하나는 서버가 되어서 대기하고 하나는 연결을 시도해야 하기 때문이다. 연결시 사용하는 정보는 서버의 MAC주소가 된다. 서버와 단말이 둘다 동일 RFCOMM채널상에서 BluetoothSocket으로 연결되면 상호간에 연결된것으로 간주된다. 이 때 입출력스트림을 통해 데이터를 주고받을 수 있다. 

서버와 클라이언트 디바이스는 각각 다른 방식으로 요구되는 BluetoothSocket을 얻는다. 서버는 요청받은 연결을 허락했을 때 얻어지고 클라이언트는 서버와 RFCOMM채널이 열렸을 때 얻어진다.

두 단말이 모두 자동으로 서버모드로 대기하고 서로간에 연결을 시도하는 방식과 명시적으로 하나는 호스트로써 서버로 대기하고 다른 하나가 연결을 시도하는 방식이 있다.

두 디바이스가 페어링된적이 없으면 안드로이드는 자동으로 페어링요청 통지나 팝업을 연결과정에 띄운다. 그래서 연결시도과정에 두 단말이 페어링된적이 있는지 알 필요가 없다. 연결시도는 페어링과정이 끝난 이후 계속 진행된다.


서버 연결

BluetoothServerSocket으로 서버로 동작시킬 수 있다. 서버소켓은 연결요청을 받아들이기 위한것이다.

1. BluetoothServerSocket을 얻는다.

   lisetnUsingRfcommWithServiceRecord(String, UUID)로 얻는다. String는 서비스명이고 UUID는 클라이언트디바이스와 연결 동의에 기반으로 사용된다. 다시 말해 클라이언트디바이스가 연결을 시도할 때 UUID가 전달되어진다. 다음단계에서 UUID는 서로 맷치가 되어야 한다.

2. accept()메서드로 연결요청을 대기한다.

  연결이 허용되거나 예외가 발생할 때 까지 이 메서드는 블럭된다.  클라이언트의 연결요청이 UUID가 일치하였을 때만 accept가 성공하고 BluetoothSocket을 리턴한다.

3. 더이상 연결을 허용하지 않을거라면 close()를 호출한다.

  서버소켓을 종료하고 모든 리소스를 해재한다. 연결된 상태는 유지된다.  TCP/IP와 다르게 RFCOMM은 하나의 채널에 하나의 연결만 허용한다. 따라서 accept()로 연결이 성립되면 서버소켓을 close()로 닫아준다.

accept는 Main activity에서 호출하지 않아야 블럭되는것을 방지할 수 있다. 블럭중에 중단하려면 다른 스레드에서 close()를 호출하면 된다.

다음은 서버구현의 예이다.

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
= mBluetoothAdapter.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) { }
   
}
}


클라이언트 연결

리모트디바이스와 연결하기 위해서는 일단 리모트디바이스객체를 얻어야 한다. 페어링된 디바이스를 얻거나 디스커버리 과정을 통해서 연결하고자 하는 BluetoothDevice를 얻어야 한다.

1. BluetoothDevice.createRfcommSocketToServiceRecord(UUID) 를 호출

  BluetoothSocket을 초기화한다. 이 때 서버디바이스에서 제공하는 UUID를 인자로 넘긴다. 이 값이 동일해야 연결이 성립된다.

2. connect()로 연결 시도

  UUID가 매칭되는 디바이스와 연결을 시도하고 성공하면 소켓을 리턴한다. 

connect()메서드가 블럭되는 호출이기 때문에 분리된 스레드에서 수행되어야 한다.

connect()과정 중에는 디스커버리가 진행되면 안된다. 만일 진행하게 되면 연결과정이 아주 느리게 진행되거나 실패하게 된다.

다음은 간략한 예제이다.

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
        mBluetoothAdapter
.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) { }
   
}
}


연결관리

디바이스간 연결이 완료되면 BluetoothSocket을 각각 소유하게 된다. 이넘을 통해서 데이터를 주고받을 수 있다.

1. 소켓으로부터 InputStream과 OutputStream을 얻는다.

2.  read(byte[]), write(byte[])로 읽거나 쓰면 끝...

두 메서드 모두 블럭되기 때문에 스레드로 분리되어야 한다.

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) { }
   
}
}


프로파일로 동작하기

안드로이드 3.0부터 블루투스프로파일로 동작이 가능해졌다. 블루투스프로파일은 무선인터페이스 규격으로 블루투스기반의 디바이스간 통신에 기반한것이다. 예를 들어 헨즈프리프로파일같은 것이 있는데 모바일폰과 헤드셋간의 연결이 가능하려면 두 디바이스 모두 핸즈프리 프로파일을 지원해야 한다.

블루투스프로파일을 직접 커스텀하게 작성이 가능하다. 안드로이드 블루투스 API는 다음의 프로파일의 구현을 지원한다.

- 헤드셋

- A2DP

- Health Device

프로파일기반으로 동작시키려면 다음과정으로 진행한다.

1. 블루투스어댑터를 얻는다.

2. 프로파일 프록시 객체를 얻는다 getProfileProxy(). 이 객체로 연결을 달성하게 된다. 아래 예제에서는 BluetoothHeadset의 인스턴스를 프록시객체로 사용하였다.

3. BluetoothProfile.ServiceListener를 셋업하여 연결과 해재 이벤트를 처리한다.

4. onServiceConnected()에서 프록시객체의 핸들을 얻는다.

5. 프록시객체를 얻으면 이 객체를 통하여 연결상태를 모니터링하고 해당 프로파일과 연관된 동작들을 수행할 수 있다.

다음은 BluetoothHeadset 프록시객체를 사용하여 해드셋프로파일을 제어하는 예이다.

BluetoothHeadset mBluetoothHeadset;
 
// Get the default adapter
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
// Establish connection to the proxy.
mBluetoothAdapter
.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET);
 
private BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() {
   
public void onServiceConnected(int profile, BluetoothProfile proxy) {
       
if (profile == BluetoothProfile.HEADSET) {
            mBluetoothHeadset
= (BluetoothHeadset) proxy;
       
}
   
}
   
public void onServiceDisconnected(int profile) {
       
if (profile == BluetoothProfile.HEADSET) {
            mBluetoothHeadset
= null;
       
}
   
}
};
 
// ... call functions on mBluetoothHeadset
 
// Close proxy connection after use.
mBluetoothAdapter
.closeProfileProxy(mBluetoothHeadset);


[출처] http://samse.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4-%EA%B0%9C%EB%B0%9C

Comments