기술자료

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

스마트폰을 품은 하드웨어 : 안드로이드 운영체제에서 블루투스 연결 장치 개발 (1)

기술자료
DBMS별 분류
Etc
작성자
dataonair
작성일
2014-04-11 00:00
조회
17110


◎ 연재기사 ◎


스마트폰을 품은 하드웨어 : 안드로이드 운영체제에서 블루투스 연결 장치 개발 (1)


스마트폰을 품은 하드웨어 : 안드로이드 운영체제에서 블루투스 연결 장치 개발 (2)


스마트폰을 품은 하드웨어 : 안드로이드 운영체제에서 블루투스 연결 장치 개발 (3)


스마트폰을 품은 하드웨어 : 실전 안드로이드 연결 장치 프로그래밍 (1)


스마트폰을 품은 하드웨어 : 실전 안드로이드 연결 장치 프로그래밍 (2)



스마트폰을 품은 하드웨어

안드로이드 운영체제에서 블루투스 연결 장치 개발 (1)



안드로이드 운영체제는 비교적 쉽게 블루투스 통신 방식을 사용할 수 있도록 지원하고 있어 현재까지 다양한 용도로 활용되고 있다. 특히 아이폰 운영체제와 달리 SPP(시리얼 통신 프로파일)와 같은 부분에서 자유로운 사용이 가능한 것이 장점이다. 하지만 BLE(Bluetooth Low Energy) 부분에서는 상황이 조금 복잡하다. 이와 관련해 안드로이드 운영체제에서 블루투스 연결 장치를 개발하고 프로그래밍하는 방법을 살펴본다.



안드로이드 4.3 이전 버전에서는 BLE 스택이 내장되지 않아서 업체마다 다른 방법으로 지원하곤 했다. 삼성이나 모토로라와 같은 회사들이 자체 스택을 내장하고 지원하기 때문에 앱을 개발하는 방법이 달랐고, 또 호환성에도 문제가 있었다. 이러한 점이 초기 스마트폰 앱세서리들이 안드로이드 폰들을 지원하기 위해서 겪었던 문제이고 고민이었다.

tech_img1216.jpg

<그림 1>처럼 삼성의 안드로이드 폰에는 고유의 BLE 스택이 탑재돼 있었고, 이 기능을 사용하기 위해서는 별도의 SDK를 다운로드해 개발해야 했다. BLE는 무척 다양한 명칭으로 불리는데, 공식적인 마케팅 용어로는 ‘블루투스 스마트(Bluetooth Smart)’와 ‘블루투스 스마트 레디(Bluetooth Smart Ready)’를 사용한다. 둘의 차이점은 저에너지 기능(Low Energy)만 지원하면 스마트, 기존 스펙을 모두 지원하면 스마트 레디라고 부른다. 여기서 특이한 점은 BLE만 지원할 수도 있다는 점이다. BLE는 기존 블루투스 프로토콜과 호환되지 않기 때문에 BLE만 지원하는 디바이스도 나올 수 있는 것이다. 실제로도 안드로이드와 아이폰 플랫폼을 지원하기 위해서 블루투스 4.0과 블루투스 2.1을 동시에 지원하는 칩을 쓴 경우가 많았다.

이렇게 BLE는 기존 블루투스와 다르기 때문에 개발자 입장에서 BLE를 사용하려면 기존과는 다른 방법을 사용해야 한다. iOS7부터는 BLE를 위해 코어 블루투스(Core Bluetooth)라는 라이브러리로 프로그래밍할 수 있었다. 다행히도 안드로이드 4.3부터 기존 블루투스 라이브러리에 BLE 지원 기능이 추가됐다.



안드로이드 운영체제에서의 블루투스

안드로이드 플랫폼은 블루투스 장비들과 무선으로 데이터를 교환할 수 있는 블루투스 네트워크 스택을 지원한다. 이 애플리케이션 프레임워크는 안드로이드 블루투스 API를 이용해 블루투스 기능들을 사용하는 방법을 제공한다. API들은 블루투스 장비를 무선으로 연결하고 1대 1(point-to-point) 혹은 1대 다 무선통신도 지원한다.

tech_img1217.jpg

블루투스 기능을 사용하는 안드로이드 앱은 다음과 같은 과정을 통해 블루투스 통신을 설정한다.

1) 다른 블루투스 장비들을 스캔한다. 스마트폰과 연결할 블루투스 장비가 있는지 검색하는 과정이다.
2) 블루투스 장비를 페어링하기 위해 로컬 블루투스 어댑터 클래스를 호출해 정보를 얻는다. 연결 가능한 블루투스 장비가 있을 때는 블루투스 어댑터 클래스를 통해 페어링 정보를 가져온다.
3) RFCOMM 채널들을 성립시킨다. 통신을 위한 클래스를 연동하는 것이다.
4) 서비스 탐색을 통해 다른 장비들과 연결한다. 장치에서 지원하는 기능을 확인하고 해당 기능에 대해 연결하는 과정이다.
5) 다른 장비와 데이터를 주고받는다. 이때부터 스마트폰과 블루투스 장치 간의 통신이 시작된다.
6) 다중(multiple) 연결에 대한 부분을 관리한다.

tech_img1215.jpg

<표 1>은 안드로이드 운영체제에서 블루투스 연결을 관리하기 위해 사용되는 android.bluetooth 패키지 클래스들을 설명한 것이다.



블루투스 권한 설정

앱에서 블루투스 기능을 사용하기 위해서는 적어도 BLUE TOOTH, BLUETOOTH_ADMIN 중 하나는 선언해야 한다. 연결, 연결수락, 데이터 교환 등의 블루투스 통신을 진행하고 싶으면 BLUETOOTH 권한을 요청해야 하며 블루투스 설정을 조작하거나 초기 장비를 검색하기 위해서는 BLUETOOTH_ ADMIN 권한을 요청해야 한다.

대부분의 앱은 로컬 블루투스 장비들을 검색하기 위해 전적으로 이 권한을 필요로 한다. 만약 사용자가 블루투스 설정을 변경하는 관리자일 필요가 없다면 이 권한을 요청할 필요는 없다. BLUETOOTH_ADMIN 권한을 가지면 BLUETOOTH 권한에서 허가된 기능도 수행할 수 있다.

앱의 manifest 파일에 블루투스 권한 선언은 <리스트 1>과 같이 한다.



<리스트 1> 블루투스 권한 선언< manifest ...]] >
< uses-permission android:name="android.permission. BLUETOOTH"/>
...
< /manifest>

블루투스 프로그래밍

앱이 블루투스로 통신하기 전에 먼저 블루투스가 장비에서 지원하는지, 그리고 활성화돼 있는지를 확인해야 한다. 만약 블루투스가 지원되지 않는다면 앱의 어떠한 블루투스 기능도 자연스럽게 비활성화돼야 하며, 만약 블루투스가 지원되지만 비활성화 돼 있다면 앱을 떠나지 않고 블루투스를 활성화하도록 요청할 수 있다. 이 작업은 BluetoothAdapter를 사용해 처리한다.

<리스트 2> BluetoothAdapter 획득BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// 이 장치는 블루투스 기능을 지원하지 않음
}

안드로이드에서 블루투스와 관련해 할 수 있는 첫 번째 작업은 BluetoothAdapter를 이용하는 것이다. BluetoothAdpater를 얻기 위해서는 static getDefaultAdapter() 메소드를 호출한다. 이 메소드는 장비가 소유하고 있는 블루투스 어댑터에 대한 BluetoothAdapter 객체를 반환하며, 이 어댑터는 전체 시스템에 하나만 존재한다. 그리고 앱은 이 객체를 사용해 어댑터와 연동할 수 있다. 만약 getDefaultAdapter()가 null을 반환하면 장비는 블루투스를 지원하지 않는 것이므로 블루투스에 관한 어떠한 작업도 수행할 수 없다.

<리스트 3> 블루투스 활성화if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

다음으로 블루투스를 활성화시켜야 한다. 먼저 isEnabled() 메소드를 통해 현재 활성화 상태인지를 확인한다. 만약 false이면 블루투스가 비활성화ION_REQUEST_ENABLE 액션 인텐트와 함께 startActivityForResult()를 호출해야 한다.



블루투스 장비 찾기

BluetoothAdapter를 사용하면 장비 탐색 혹은 페어링된 장비 리스트를 쿼리하는 방법으로 원격의 블루투스 장비들을 찾을 수 있다. 장비 탐색 과정을 통해서는 로컬영역에서 활성화된 블루투스 장비를 찾는 스캐닝 작업이 수행되고 각각에 대해 몇 가지 정보가 요청된다. 만약 장비가 발견되면 블루투스 장비에서는 장비의 이름, 클래스, 맥 주소와 같은 공유 정보를 가지고 탐색 요청에 응답할 것이다. 이 정보는 탐색 작업을 수행 중인 장비가 탐색된 장비와의 연결을 초기화하는 데 이용된다.

<리스트 4> 페어링된 장비들에 대해 쿼리하기Set< BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// 페어링된 장비가 있을 때
if (pairedDevices.size() > 0) {
// 페어링된 장비의 수
for (BluetoothDevice device : pairedDevices) {
// 리스트뷰에 페이링된 장비의 이름과 맥 주소를 표시한다.
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}

장비 탐색을 진행하기에 앞서 만약 연결하고자 하는 장비가 이미 페어링된 것인지를 확인하기 위해 페어링된 장비들의 집합을 쿼리할 필요가 있다. 이 작업은 getBondedDevices() 메소드를 호출하는 것으로 수행할 수 있다. 이를 통해 페어링된 장비들을 나타내는 BluetoothDevice들의 집합을 반환하면 된다. <리스트 4>는 모든 페어링된 장비들을 쿼리하고 사용자에게 Array Adapter를 이용해 장비의 이름을 보여주는 코드다. BluetoothDevice 객체에서 연결을 위해 필요한 것은 맥 주소다. 여기서는 단순히 사용자에게 장비의 맥 주소를 표시하지만, 연결을 초기화하는 데 사용하기도 한다.



장비 탐색

장비 탐색을 시작하기 위해서는 startDiscovery()를 호출하면 된다. 이 메소드의 진행은 비동기적이다. 또한 탐색이 성공적으로 시작됐는지 아닌지에 대한 불리언(boolean) 형식의 결과 값을 반환한다. 탐색 작업은 보통 12초 정도이며 페이지에는 스캔해서 찾은 장비들의 블루투스 이름이 표시된다. 앱에서 각 장비 탐색에 대한 정보를 받으려면 ACTION_ FOUND 인텐트에 대한 브로드캐스트 리시버를 등록해야 한다. 장비를 찾을 때마다 시스템은 ACTION_FOUND 인텐트를 브로드캐스트한다. 이 인텐트는 BluetoothDevice 객체가 있는 EXTRA_DEVICE와 BluetoothClass 객체가 있는 EXTRA _CLASS를 포함하고 있다.

<리스트 5> 브로드캐스트 리시버 예제// ACTION_FOUND에 대한 브로드캐스트 리시버
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// 장비가 발견됐을 때 처리
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// BluetoothDevice 객체를 가져온다.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 리스트뷰에 이름과 맥 주소 추가
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
};
// BroadcastReceiver 등록
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);

연결을 초기화하는 데 필요한 정보인 맥 주소는 Bluetooth Device 객체에 있다.



탐색 활성화

만약 장비를 다른 장비들에서도 탐색될 수 있도록 만들고 싶다면 ACTION_REQUEST_DISCOVERABLE 액션 인텐트와 함께 startActivityForResult(Intent, int)를 호출한다. 이것은 앱을 중지시키지 않고 환경설정의 탐색모드를 활성화할 수 있다. 기본적으로 장비는 120초 동안 탐색될 수 있는 상태가 된다. 이를 변경하고 싶다면 EXTRA_DISCOVERABLE_DURA TION의 extra 값을 인텐트에 추가해 호출하면 된다. 앱은 최고 3600초까지 설정할 수 있다. 그리고 0 값이면 항상 탐색모드를 유지하겠다는 의미다. 만약 0 미만이거나 3600초보다 크면 자동으로 120초로 설정된다.

<리스트 6> 300초로 설정하는 예제Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

만약 원격 장비와의 연결을 초기화할 생각이라면, 장비 탐색을 활성화할 필요는 없다. 앱은 원격 장비를 탐색해 연결을 초기화할 수 있기 때문에 장비 탐색 활성화는 오직 앱에서 서버소켓을 통해 연결을 수락할 때만 필요하다.



서버로 연결

2개의 장비에서 실행되고 있는 앱 사이 연결자를 생성하기 위해서는 서버사이드와 클라이언트사이드를 구현해야 한다. 2개의 장비를 연결하려면 하나는 서버로서 BluetoothServer Socket을 오픈해야 한다. 서버소켓은 연결 요청을 듣기 위함이고 하나가 연결될 때 연결된 BluetoothSocket을 제공받는다. 서버소켓을 설정하고 연결을 수락하는 방법은 다음과 같다.

- listenUsingRfcommWithServiceRecord(String, UUID) 메소드를 호출해 BluetoothServerSoket을 얻는다.
- accept() 메소드를 호출해 연결 요청을 듣기 시작한다.
- 만약 추가적인 연결을 수락하지 않으려면 close() 메소드를 호출한다.

<리스트 7> 스레드로 연결을 수락하는 서버 컴포넌트 구현private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;public AcceptThread() {
BluetoothServerSocket tmp = null;
try {
// MY_UUID는 앱의 UUID
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord (NAME, MY_UUID);
} catch (IOException e) { }
mmServerSocket = tmp;
}public void run() {
BluetoothSocket socket = null;
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
break;
}
// 연결이 이뤄짐
if (socket != null) {
// 연결 관리
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) { }
}
}

<리스트 7>은 오직 하나의 연결만 허용하고 있다. 연결이 수락되자마자 BluetoothSocket을 획득하고 획득한 Bluetooth Socket을 별도의 스레드로 보낸 후, 클라이언트 코드에서 하는 것과 같은 connect() 메소드를 호출하지 않게 된다. manageConnectedSocket()은 앱에서 데이터를 송수신하기 위한 스레드를 초기화하는 메소드이다.



클라이언트로 연결

원격 장비에 연결하기 위해 먼저 원격 장비에 대한 Bluetooth Device 객체를 얻어야 한다. BluetoothDevice를 통해 Blue toothSocket을 얻고 연결을 시도한다. 이에 대한 기본 흐름은 다음과 같다.

- BluetoothDevice의 createRfcommSocketToServiceRecord(UUID) 메소드를 통해 BluetoothSocket을 얻는다. 이 메소드는 Bluetooth Device에 연결할 수 있도록 BluetoothSocket을 초기화하며 여기에 파라미터로 보내는 UUID는 서버에서 BluetoothServerSocket을 오픈할 때 사용한 UUID와 일치해야 한다.
- connect() 메소드를 호출해 연결을 시도한다.

<리스트 8> 블루투스 연결을 시도하는 스레드의 기본 코드private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;public ConnectThread(BluetoothDevice device) {
BluetoothSocket tmp = null;
mmDevice = device; // 연결시도
try {
// MY_UUID는 UUID로, 서버 앱에 있는 MY_UUID와 동일해야 한다.
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mmSocket = tmp;
}public void run() {
mBluetoothAdapter.cancelDiscovery();try {
// 소켓을 통해 연결
mmSocket.connect();
} catch (IOException connectException) {
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
manageConnectedSocket(mmSocket);
}public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}



연결 관리

성공적으로 2개의 장비가 연결되면 양쪽 다 BluetoothSocket을 갖게 된다. 이것을 통해 장비 간에 데이터를 공유하면서 재미있는 기능을 만들어 볼 수 있다.
- 소켓을 통해 송수신을 핸들링할 수 있는 InputStream과 Output Stream을 getInputStream()과 getOutputStream()을 통해 얻는다.
- 이 스트림들의 read(byte[])와 write(byte[]) 메소드를 통해 데이터를 읽고 쓴다.

<리스트 9> 통신을 위한 간단한 예제 프로그램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; // 송수신을 위한 소켓 스트림을 오픈한다.
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }mmInStream = tmpIn;
mmOutStream = tmpOut;
}public void run() {
byte[] buffer = new byte[1024]; // 통신을 위한 버퍼
int bytes; // 수신된 바이트 수 // InputStream을 통해 데이터를 수신 받는다.
while (true) {
try {
// 데이터가 있으면 InputStream을 통해 읽어 들인다.
bytes = mmInStream.read(buffer);
// UI에 읽은 데이터 표시
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
} /* 데이터 전송을 위한 루틴 */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}

안드로이드 개발자 문서를 통해 BLE를 사용하는 방법을 간단히 살펴보자. BLE 디바이스를 찾는 부분까지의 프로세스를 차례대로 확인해 본다.

1) Permission을 획득한다.
블루투스의 권한을 얻는 방법과 동일하게 manifest 파일에 추가하면 된다. 디바이스를 찾고 페어링하기 위해서는 ADMIN 권한을 얻어야 한다.

<리스트 10> 권한 획득

BLE가 존재하는 디 추가한다.

<리스트 11> BLE가 존재하는 디바이스에서만 동작하도록 설정

2) BLE 동작을 설정한다.
기존 블루투스를 사용하는 방법과 동일하다.

<리스트 12> BLE 동작 설정// 블루투스 어댑터를 초기화한다.
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();// 블루투스가 활성화되지 않은 상황이면 켤 수 있는 액티비티를 실행한다.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

3) BLE 디바이스를 찾는다.
mBluetoothAdapter.startLeScan() 메소드로 BLE 지원 디바이스를 찾을 수 있다. 해당 메소드의 인자로 들어가는 게 Blue toothAdapter.LeScanCallback 인스턴스다. 이 인스턴스에 구현하는 onLeScan 추상 함수를 통해 검색된 해당 디바이스 정보가 콜백으로 들어와 처리된다.

<리스트 13> BLE 디바이스 검색privatevoid scanLeDevice(finalboolean enable) {
if (enable) {
// Stops scanning after a pre-defined scan period.
mHandler.postDelayed(new Runnable() {
@Override
publicvoid run() {
mScanning = false;
mBluetoothAdapter.stopLeScan (mLeScanCallback);
}
}, SCAN_PERIOD);

mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
} else {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
...
}// 디바이스 스캔 뒤, 콜백 메소드 구현
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();
}
});
}
};

지금까지 안드로이드 운영체제에서 블루투스를 통한 통신 방법과 BLE를 통한 통신 방법에 대해 살펴봤다. 다음 시간부터는 좀더 기술적인 내용에 대해 살펴본다.