[Android][Java]BLE 통신 분석 및 예제(3)
BluetoothLeService.java
BluetoothLeService 클래스는 서비스 연결과 BLE 기기로부터 받은 GATT 서버와의 데이터 통신을 관리한다.
먼저 AndroidManifest.xml에 service를 등록해야 한다.
MainActivity 클래스의 onCreate에서 bindService()를 호출하게 되면 service를 상속받은 BluetoothLeSerivce에서 onBind()를 호출해 IBinder를 생성하여 반환해준다.
Binder 구현을 통해서 서비스 내의 메서드에 접근할 수 있는 권한을 얻는다.
public class BluetoothLeService extends Service
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
private final IBinder binder = new LocalBinder();
이제 서비스에 접근 권한이 생겼고 Anroid 시스템이 클라이언트와 서비스를 생성하였기 때문에 MainActivity 클래스에서는 onServiceConnected() 콜백을 사용하여 Binder를 받을 수 있다.
LocalBinder 클래스는 BluetoothLeService의 인스턴스를 검색하기 위한 getService()를 반환해준다.
이렇게 하면 MainActivity 클래스에서 서비스 내의 public 메소드들을 호출할 수 있게 된다.
public class LocalBinder extends Binder {
BluetoothLeService getService() {
return BluetoothLeService.this;
}
}
initialize() 에서는 BluetoothManager를 통해 Bluetooth adapter를 가져온다.
/**
* Initializes a reference to the local Bluetooth adapter.
*
* @return Return true if the initialization is successful.
*/
public boolean initialize() {
// For API level 18 and above, get a reference to BluetoothAdapter through
// BluetoothManager.
if (bluetoothManager == null) {
bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null) {
Log.e(TAG, "Unable to initialize BluetoothManager");
return false;
}
}
bluetoothAdapter = bluetoothManager.getAdapter();
if (bluetoothAdapter == null) {
Log.e(TAG, "Unable to obtain a BluetoothAdapter");
return false;
}
return true;
}
BluetoothService를 초기화하면 서비스는 먼저 BluetoothAdapter에서 getRemoteDevice()를 호출하여 장치에 접근한다.
서비스에 연결할 장치를 찾고 정상적으로 연결이 되면, connectGatt()로 GATT 서버에 연결(디바이스와의 연결)한다.
연결 상태, 서비스 발견, 서비스의 특성을 읽고 알리는 작업을 위한 BluetoothGattCallback 함수도 담는다.
public boolean connect(final String address) {
if (bluetoothAdapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address");
return false;
}
// Previously connected device. Try to reconnect.
if (bluetoothDeviceAddress != null && address.equals(bluetoothDeviceAddress)
&& bluetoothGatt != null) {
Log.d(TAG, "Trying to use an existing bluetoothGatt for connection");
if (bluetoothGatt.connect()) {
connectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
if (device == null) {
Log.w(TAG, "Device not found. Unable to connect.");
return false;
}
// We want to directly connect to the device, so we are setting the autoConnect
// parameter to false.
bluetoothGatt = device.connectGatt(this, false, gattCallback);
Log.d(TAG, "Trying to create a new connection.");
bluetoothDeviceAddress = address;
connectionState = STATE_CONNECTING;
return true;
}
BluetoothGattCallbcak은 연결 상태, 서비스 발견, 서비스 내의 값에 변화가 생기면 호출되는 콜백 메소드이다.
// Various callback methods defined by the BLE API.
private final BluetoothGattCallback gattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
String intentAction;
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
connectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
boolean result = bluetoothGatt.discoverServices();
Log.i(TAG, "discoverServices " + result);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
connectionState = STATE_DISCONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Disconnected from GATT server.");
}
}
@Override
// New services discovered
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
@Override
// Result of a characteristic read operation
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
};
onConnectionStateChange()에서는 기기와의 연결 상태를 확인한다.
연결 상태가 변경되면 broadcastUpdate()로 전달한다.
연결 상태가 ACTION_GATT_CONNECTED이면 gatt에서 discoverServices()를 통해 제공할 서비스를 검색하고
onServicesDiscovered()에서는 GATT 서비스 발견 상태를 확인한다.
여기에서도 서비스 발견했음을 broadcastUpdate()로 전달한다.
broadcastUpdate() 함수는 두 종류가 있는데, 기기와의 연결 상태, GATT 서비스 발견 상태를 알리는 broadcastUpdate()에는 인자가 하나 들어간다. sendBroadcast()를 통해 MainActivity 클래스에 상태를 알려준다.
private void broadcastUpdate(final String action) {
final Intent intent = new Intent(action);
sendBroadcast(intent);
}
onCharacteristicRead(), onCharacteristicChanged()에서는 서비스 내의 특성을 읽고 그 특성 값이 바뀔 때마다 broadcastUpdate()를 호출한다. 여기의 broadcastUpdate()는 인자가 두 개가 필요하다. 상태와 특성 값을 넣어준다.
역시 sendBroadcast()를 통해 MainActivity 클래스에 상태를 알려준다.
private void broadcastUpdate(final String action,
final BluetoothGattCharacteristic characteristic) {
final Intent intent = new Intent(action);
// This is special handling for the Heart Rate Measurement profile.
// Data parsing is carried out as per profile specifications.
// 샘플 UUID_HEART_RATE_MEASUREMENT 와 특성 값의 UUID 를 비교한다.
// 비교값이 같을 경우
if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
int flag = characteristic.getProperties();
int format = -1;
// 특성 값의 속성이 16비트 또는 8비트를 변수 format 에 저장한다.
if ((flag & 0x01) != 0) {
format = BluetoothGattCharacteristic.FORMAT_UINT16;
Log.d(TAG, "Heart rate format UINT16.");
} else {
format = BluetoothGattCharacteristic.FORMAT_UINT8;
Log.d(TAG, "Heart rate format UINT8.");
}
final int heartRate = characteristic.getIntValue(format, 1);
Log.d(TAG, String.format("Received heart rate: %d", heartRate));
intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
}
// 비교값이 다를 경우
else {
// For all other profiles, writes the data formatted in HEX.
final byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(data.length);
// 특성 값을 바이트 배열에 넣고
for (byte byteChar : data)
// 바이트 배열을 16진수로 변환한다.
stringBuilder.append(String.format("%02X ", byteChar));
// stringBuilder 에 넣어준다.
intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
}
}
sendBroadcast(intent);
}
BluetoothLeService 클래스에서 보낸 데이터들은 [Android]BLE 통신 분석 및 예제(2) 에서 정리했던 BroadcastReceiver에서 받아서 처리하게 된다.
블루투스 앱은 통신하는 기기의 특정 특성이 변경되면 알림을 받도록 할 수 있다.
setCharacteristicNotification()을 사용하여 그에 대한 콜백을 받도록 한다.
/**
* Enables or disables notification on a give characteristic.
*
* @param characteristic Characteristic to act on.
* @param enabled If true, enable notification. False otherwise.
*/
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
if( bluetoothAdapter == null || bluetoothGatt == null ) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
// This is specific to Heart Rate Measurement.
if( UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid()) ) {
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
}
}
특성에 대한 알림이 활성화가 되어있고 BLE 기기의 특성이나 Descriptor의 값이 변경되면 onCharacteristicChanged() 콜백이 트리거된다.
bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
readCharacteristic()은 BLE 장치의 특성을 읽어온다.
/**
* Request a read on a given {@code BluetoothGattCharacteristic}.
* The read result is reported asynchronously through
* the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
* callback.
*
* @param characteristic The characteristic to read from.
*/
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
if( bluetoothAdapter == null || bluetoothGatt == null ) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
bluetoothGatt.readCharacteristic(characteristic);
}
읽어오는데 성공하면 onCharacteristicRead()가 콜백된다.
@Override
// Result of a characteristic read operation
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
disconnect(), close()는 두 기기간의 BLE 연결을 끊어주고 종료시킨다.
에러가 발생하거나 재 연결이 필요할 때 두 메소드를 모두 호출해서 종료 후 service를 다시 시작해야 한다.
/**
* Disconnects an existing connection or cancel a pending connection. The disconnection result
* is reported asynchronously through the
* {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
* callback.
*/
public void disconnect() {
if (bluetoothAdapter == null || bluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
bluetoothGatt.disconnect();
}
/**
* After using a given BLE device, the app must call this method to ensure resources are
* released properly.
*/
public void close() {
if (bluetoothGatt == null) {
return;
}
bluetoothGatt.close();
bluetoothGatt = null;
}
끄읏.