Development record of developer who study hard everyday.

레이블이 안드로이드ble연결인 게시물을 표시합니다. 모든 게시물 표시
레이블이 안드로이드ble연결인 게시물을 표시합니다. 모든 게시물 표시
, , , ,

안드로이드 BLE 연결하기 feat.안드로이드 공식문서

 안드로이드 BLE 연결하기 feat.안드로이드 공식문서

android develop blog


이전 글에 이어서 안드로이드 공식문서를 토대로 BLE 연결하는 방식을 소개하겠습니다.

GATT server와 연결을 한 후,  BLE 기기가 제공하는 Service를 찾고 데이터를 보내고 데이터를 받을 수 있습니다.


Discover services

다시 한 번 강조하지만, 여기서 Service와 안드로이드 기본 구성요소 Service를 혼동하시면 안됩니다.

BLE Service는 Ble기기가 전달하는 data 즉 Characterisic의 모음입니다.

혹시 이게 와닿지 않는다면, 그냥 Ble기기가 제공하는 서비스(역할)이라고 이해하시면 좋을 것 같습니다.

private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// successfully connected to the GATT Server
Log.d(TAG, "onConnectionStateChange, coneccted to the GATT Server");
connectionState = STATE_CONNECTED;
broadcastUpdate(ACTION_GATT_CONNECTED);
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return;
}
boolean result = gatt.discoverServices();
Log.d(TAG, "onConnectionStateChange, discoverServices: " + result);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// disconnected from the GATT Server
Log.d(TAG, "onConnectionStateChange, disconnected to the GATT Server");
connectionState = STATE_DISCONNECTED;
broadcastUpdate(ACTION_GATT_DISCONNECTED);
}
}

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (ActivityCompat.checkSelfPermission(BluetoothLeService.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "onServicesDiscovered: Ble Permission Denied");
return;
}

if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
List<BluetoothGattService> gattServices = gatt.getServices();
Log.d(TAG, "onServicesDiscovered, List of gattServices: " + gattServices.toString());
bluetoothGattWriteService = gattServices.get(3);
notifyCharacteristic = bluetoothGattWriteService.getCharacteristics().get(1);
notifyDescriptor = notifyCharacteristic.getDescriptor(Constants.READ_DESCRIPTOR_UUID);
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean notificationDescriptor = gatt.writeDescriptor(notifyDescriptor);
Log.d(TAG, "onServicesDiscovered, notifyCharaceristic UUID: " + notifyCharacteristic.getUuid().toString());
if(notifyCharacteristic != null) {
boolean notificationResult = gatt.setCharacteristicNotification(notifyCharacteristic, true);
Log.d(TAG, "notificationResult: " + notificationResult);
}

if(notifyDescriptor != null){
Log.d(TAG, "notify Descriptor: " + notificationDescriptor);
}
for (BluetoothGattService service : gattServices) {
Log.d(TAG, "onServicesDiscovered, Service UUID: " + service.getUuid().toString());

List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
Log.d(TAG, "onServicesDiscovered, characteristics: " + characteristics);
for (BluetoothGattCharacteristic characteristic : characteristics) {
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
//bjs: 여기서 ble 데이터 읽는다
Log.d(TAG, "onServicesDiscovered, characteristic: " + characteristic);
boolean result = gatt.readCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, read result: " + result);
}

if (hasProperty(characteristic, PROPERTY_WRITE)) {
boolean writeResult = gatt.writeCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, writeResult: " + writeResult);
}
}
}
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.d(TAG, "onCharacteristicRead: " + "gatt = " + gatt + "status = " + status);
byte[] value = characteristic.getValue();
String readData = byteArrayToHexaString(value);
Log.d(TAG, "onCharacteristicRead, readData: " + readData);

}

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
if(status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "OnCharacteristicWrite: Write Success");
byte[] writeData = characteristic.getValue();
if(writeData != null) {
//String writeMessage = writeData.toString();
Log.d(TAG, "writeData: " + byteArrayToHexaString(writeData));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_WRITE);
sendIntent.putExtra(Constants.SEND_MSG, new String(writeData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}
}
}

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.d(TAG, "onCharacteristicChanged: notify");
byte[] notifyData = characteristic.getValue();
if(notifyData != null) {
Log.d(TAG, "Notify Value: " + byteArrayToHexaString(characteristic.getValue()));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_READ);
sendIntent.putExtra(Constants.SEND_MSG, new String(notifyData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}


}
};

Ble를 통해 데이터를 주고받기 전에 서비스를 찾는 과정이 필요합니다.

discoverServices() 메서드를 실행하면 onServicesDiscovered 콜백이 호출됩니다.

저 같은 경우에는 BluetoothGattCallback에서 GATT Server와 연결이 이루어진 후 

boolean result = gatt.discoverServices();
Log.d(TAG, "onConnectionStateChange, discoverServices: " + result);

를 통해서 서비스를 찾았습니다.

여기서 내가 사용할 서비스가 무엇인지 찾아야합니다.

이 과정에서 외부앱을 사용하면 굉장히 편리하다.

nRF Connect

바로 nRF Connect 앱이다.

이 앱을 간단히 설명하자면 기기와 ble연결을 하여 데이터를 주고받는 통신을 테스트할 수 있는 앱이다.

기기를 이 앱과 연결하는 것은 UI상 직관적이므로 생략하겠다.

연결 이후에 화면을 보자.

nRF Connect

위 화면에서 보이는 것처럼 Unknown Service라는 것이 보인다.
저것이 바로 우리가 사용하여 기기와 데이터를 주고받는데 사용할 Service이다.

나도 아직은 잘 모르는데 위의 3개의 Service는 ble모듈 자체 내장되어있는 Service이고 Unknown Service가 그 모듈에다 하드웨어 개발자에게 요청하여 개발한 Service가 아닐까 생각된다.


nRF Connect

Unknown Service를 클릭하면 2개의 Characteristic이 나오는데 WRITE Properties는 말그대로 데이터를 쓸 때 사용하는 Characteristic이고 NOTIFY Properties는 기기가 notify할 때 사용하는 Characteristic이다.

각 Characteristic의 UUID를 기억해두자!!
저 값을 코드에서도 사용해야한다.

Write Characteristic UUID = 0xFFF1
Notify Characteristic UUID = 0xFFF2

데이터를 주고받는 테스트를 할 때는 아래 화면처럼 X자가 보이게 만들어주어야한다.
그래야 notify가 된다.
나중에 설명하겠지만 setNotification을 설정하는 과정이다.

nRF Connect

그리고 아래 화면에 화살표 표시한 버튼을 누르면

nRF Connect

아래 화면처럼 데이터를 Write하는 팝업창이 뜬다.

nRF Connect

 데이터는 보통 Byte배열로 주고받는다.

16진수로 주고받는 경우가 많으니 참고하길 바란다.

프로토콜 개발한 측에서 보내준 형식대로 16진수 byte 배열을 보내주면

nRF Connect

 nRF Connect

순서대로 표시한 버튼을 클릭하면 아래처럼 로그가 나온다

nRF Connect

로그에 화살표 표시한 것이 Write Characteristic의 UUID와 Notify Characteristic의 UUID입니다.

저 두 UUID를 Constants 클래스에 저장해서 코드에서 활용하시면 됩니다.

이렇게 Ble로 데이터를 주고받는 테스트가 완료되었습니다.

이제 다시 BluetoothCallback으로 돌아가서 데이터를 Write할 준비를 하겠습니다.

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (ActivityCompat.checkSelfPermission(BluetoothLeService.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "onServicesDiscovered: Ble Permission Denied");
return;
}

if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
List<BluetoothGattService> gattServices = gatt.getServices();
Log.d(TAG, "onServicesDiscovered, List of gattServices: " + gattServices.toString());
bluetoothGattWriteService = gattServices.get(3);
notifyCharacteristic = bluetoothGattWriteService.getCharacteristics().get(1);
notifyDescriptor = notifyCharacteristic.getDescriptor(Constants.READ_DESCRIPTOR_UUID);
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean notificationDescriptor = gatt.writeDescriptor(notifyDescriptor);
Log.d(TAG, "onServicesDiscovered, notifyCharaceristic UUID: " + notifyCharacteristic.getUuid().toString());
if(notifyCharacteristic != null) {
boolean notificationResult = gatt.setCharacteristicNotification(notifyCharacteristic, true);
Log.d(TAG, "notificationResult: " + notificationResult);
}

if(notifyDescriptor != null){
Log.d(TAG, "notify Descriptor: " + notificationDescriptor);
}
for (BluetoothGattService service : gattServices) {
Log.d(TAG, "onServicesDiscovered, Service UUID: " + service.getUuid().toString());

List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
Log.d(TAG, "onServicesDiscovered, characteristics: " + characteristics);
for (BluetoothGattCharacteristic characteristic : characteristics) {
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {

Log.d(TAG, "onServicesDiscovered, characteristic: " + characteristic);
boolean result = gatt.readCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, read result: " + result);
}

if (hasProperty(characteristic, PROPERTY_WRITE)) {
boolean writeResult = gatt.writeCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, writeResult: " + writeResult);
}
}
}
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
gatt.discoverServices()를 통해서 호출된 onServicesDiscovered() 콜백에서 for문을 통해서 내가 필요한 서비스와 캐릭터리스틱을 변수에 저장합니다.
 
 저 같은 경우에는 write 할 때 필요한 Write 서비스를 bluetoothGattWriteService에 저장합니다.

(hasProperty 함수는 characteristic의 속성을 확인하는 함수인데 이 프로젝트에서는 딱히 쓸모가 없었습니다.)

그냥 함수 선언만 참고하고 가세요.
//what is property of Characteristic
public boolean hasProperty(BluetoothGattCharacteristic characteristic, int property) {
int prop = characteristic.getProperties() & property;
return prop == property;
}

그리고 여기서 
 bluetoothGattWriteService = gattServices.get(3);
notifyCharacteristic = bluetoothGattWriteService.getCharacteristics().get(1);
notifyDescriptor = notifyCharacteristic.getDescriptor(Constants.READ_DESCRIPTOR_UUID);
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean notificationDescriptor = gatt.writeDescriptor(notifyDescriptor);
Log.d(TAG, "onServicesDiscovered, notifyCharaceristic UUID: " + notifyCharacteristic.getUuid().toString());
if(notifyCharacteristic != null) {
boolean notificationResult = gatt.setCharacteristicNotification(notifyCharacteristic, true);
Log.d(TAG, "notificationResult: " + notificationResult);
}
notifyCharacteristic와 notifyDescriptor를 저장해서 gatt.setCharacterisitcNotification을 통해 기기의 notification을 설정해줍니다.

private boolean writeByBle(byte[] msg) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return false;
}

if (bluetoothGatt == null || bleAdapter == null) {
Log.d(TAG, "writeByBle: BluetoothAdapter and bluetoothGatt not initialized");
return false;
}

if(bluetoothGattWriteService == null){
Log.d(TAG, "writeByBle: bluetoothGattService is null!!");
}
BluetoothGattCharacteristic characteristic = bluetoothGattWriteService.getCharacteristic(Constants.WRITE_CHAR_UUID);

characteristic.setValue(msg);

return bluetoothGatt.writeCharacteristic(characteristic);
}
그리고 위 writeByBle 함수에서 캐릭터리스틱을 쓰면 됩니다.

bluetoothGattWriteService와 nRf Connect 앱을 통해 얻은 write characteristic uuid를 활용하여 Write Characteristic을 얻고
characteristic.setValue 함수에다 보낼 byte 배열을 넣어주면 됩니다.

그리고 bluetoothGatt.writeCharacteristic(characteristic)으로 기기에다 데이터를 보내주면 끝!

마지막으로 이 기기 같은 경우에는 데이터를 쓰면 기기가 앱에 notify해주는 기능이 있는데요.

이때 주의할 점이 연결후 discoverServices() 함수를 호출할 때 동시에 set

데이터를 Write하게 되면 BluetoothGattCallback의 onCharacteritricChanged 콜백에서 기기가 notify한 데이터가 넘어오게 됩니다.

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.d(TAG, "onCharacteristicChanged: notify");
byte[] notifyData = characteristic.getValue();
if(notifyData != null) {
Log.d(TAG, "Notify Value: " + byteArrayToHexaString(characteristic.getValue()));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_READ);
sendIntent.putExtra(Constants.SEND_MSG, new String(notifyData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}
}

characteristic.getValue를 통해 받은 데이터를 저장하구요.

이 데이터를 브로드캐스트를 통해 액티비티에 전달해주었습니다.

byteArrayToHexaString 함수는 구글링하다 발견한 함수인데 함수이름처럼 byteArray를 16진수 String으로 만들어주는 함수입니다.

public static String byteArrayToHexaString(byte[] bytes) {
StringBuilder builder = new StringBuilder();

for (byte data : bytes) {
builder.append(String.format("%02X ", data));
}

return builder.toString();
}

지금까지 ble연결을 통해서 데이터를 읽고 쓰는 것을 해보았습니다.

저도 처음 해보는 거라 여러 시행착오가 있었기 때문에 이 글을 읽는 개발자분들은 저와같은 시행착오를 줄이셨으면 좋겠네요.

긴 글 읽으시느라 수고 많으셨습니다.


Share:
Read More
, , , ,

안드로이드 BLE 개념, 스캔, 연결 fea.안드로이드 공식 문서

안드로이드 Ble 스캔 및 연결하기 


회사에서 ble연결을 활용한 앱을 만들일이 생겼다.

원래는 블루투스 기기를 활용하여 앱과 연결한 후 땅의 깊이나 기울기를 측정하는 서비스였다.

그런데 기기를 업데이트하면서 이젠 블루투스 연결 뿐만 아니라 ble연결을 활용하여 기기와 앱을 연결을 해야한다.

그리고 데이터를 주고받으며 측정까지하는 업무였다.


우선, 안드로이드 공식문서를 여러번 읽어보았다.

한글로도 읽고 영어로된 원서도 읽었다.

처음엔 무슨 말인지 몰랐는데 2-3번 읽다보니 적응이 되었다.

지금부터 설명할 내용은 안드로이드 공식문서 번역 + 회사 프로젝트에 사용한 코드 + BLE 관련 구글링 내용을 적절히 섞어서 녹아내려고 한다.

기본 용어와 개념

Generic Attribute Profile (GATT)
GATT는 데이터 통신을 위한 서버라고 생각하면된다.

Profiles
음... 좀 애매할 수 있는데 프로파일이란 블루투스 연결에 사용되는 여러 객체의 대리자라고 생각하면 이해하기 쉽지않을까한다.

Attribute Protocol (ATT)
BLE 작업을 하면서 특별히 신경써야할 개념은 아니였다.
공식문서를 그대로 번역하자면 GATT가 ATT위에 만들어졌다고한다.

Characteristic
중요한 개념이다.
우리가 BLE를 통해 데이터를 주고받을 때 실질적으로 값을 갖고있는 녀석이다.
Characteristic은 또 여러개의 Descriptor를 가지고 있다.

Descriptor
Characteristic의 값을 가지고 있는 녀석이다.

Service
매우 중요한 개념이다.
BLE 디바이스가 제공하는 역할이라고 생각하면된다.
서비스는 여러개의 Characteristic으로 구성되어있다고 생각하면 된다.

Central vs Peripheral
BLE 연결을 성공적으로 하기 위해서는 휴대폰과 BLE기기 중 각각 Central과 Peripheral 역할을 맡아줘야한다.
Central은 스캔을 하여 BLE 기기를 찾고 Peripheral(BLE 기기)은 advertise를 하여 Central에게 자신이 BLE 연결 가능한 상태임을 알린다.

GATT server vs GATT client
GATT는 위에서 언급한대로 데이터를 주고받을 때 사용하는 개념이다.
데이터를 주고받기 위해서는 휴대폰과 BLE기기 중 각각 GATT server와 GATT client역할을 맡아줘야한다.

블루투스 권한


👆위 링크를 클릭해서 글을 읽어보자.
Android 12부터 블루투스 권한에 변화가 생겼다.
그리 어렵지 않으니 찬찬히 읽어보고 따라하면 문제가 없을 것이다.

BLE 기기 찾기(스캔)

간단히 말하자면, startScan 메소드로 스캔을 시작하고 ScanCallback을 구현하여 scan 결과값을 처리하면된다.

스캔은 배터리 소모가 많은 작업이라서 
1. 기기를 찾으면 바로 스캔을 종료한다
2. 스캔을 할 때는 시간을 꼭 정해준다.
이 두가지를 꼭 지켜주라고 공식문서에 나와있다.

private BluetoothLeScanner bleScanner;
private boolean scanning;
// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;
private Handler handler = new BTHandler();
public BluetoothDevice foundDevice;
private void scanLeDevice() {
if (!scanning) {
// Stops scanning after a predefined scan period.
handler.postDelayed(new Runnable() {
@Override
public void run() {
scanning = false;
if (checkBlePermission(blePermissions))
bleScanner.stopScan(leScanCallback);
}
}, SCAN_PERIOD);

scanning = true;
bleScanner.startScan(leScanCallback);
} else {
scanning = false;
bleScanner.stopScan(leScanCallback);
}
}
private final ScanCallback leScanCallback =
new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
/*leDeviceListAdapter.addDevice(result.getDevice());
leDeviceListAdapter.notifyDataSetChanged();*/
if (result != null && checkBlePermission(blePermissions)) {
BluetoothDevice bleDevice = result.getDevice();
Log.d(TAG, "onScanResult: " + bleDevice.getName() + ", " + bleDevice.getAddress());
//scanSet.add(new Pair(bleDevice.getName(), bleDevice.getAddress()));
if (bleDevice.getName() != null && bleDevice.getName().contains("ACE"))
foundDevice = bleDevice;
//scanSet.add(bleDevice);
}
//Log.d(TAG, "onScanResult, scanSet's size : " + scanSet.size() + "scanSet : " + scanSet.toString());

}
};
protected Boolean checkBlePermission(String[] permissions) {
for (String permission : permissions) {
if (checkSelfPermission(permission) == PackageManager.PERMISSION_DENIED) return false;
}
return true;
}
public static String[] blePermissions = new String[] {
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADVERTISE,
};

안드로이드 공식문서를 토대로 그냥 거의 그대로 베낀 수준이다.
크게 어려운 것은 없고, 회사 프로젝트가 자바로 되어있어서 나도 자바로 코드를 짰다.

ScanCallback을 구현할 때 스캔한 장비의 이름을 활용하여 내가 원하는 장비를 찾아서 foundDevice 변수에 저장해두었다.

기기와 BLE연결을 하기위해서는 디바이스의 MAC주소가 필요하다.
그래서 따로 저장해두었다.

GATT server와 연결하기

GATT 서버와 연결한다고 생각하면 되게 어렵게 느껴진다.
근데, 간단히 말하면 기기와 BLE연결을 한다는 것이 기기의 GATT 서버와 연결한다는 것과 같은 말이라고 생각하면 쉽다.
bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);
위 코드가 디바이스의 GATT server와 연결하는 코드이다.
연결이 성공적으로 완료되면 BluetoothGatt 객체를 넘겨준다.
이 과정에서 GATT server는 Ble디바이스가 되고 Android App이 GATT client가 된다.

GATT server와 연결결과는 BluetoothGattCallback을 통해서 넘어온다.

서비스 시작하기


BLE 기기와 신호를 주고받기위해서 서비스 객체가 필요하다.

public class BluetoothLeService extends Service {
   
    private Binder binder = new LocalBinder();
    @Override     @Nullable
    public IBinder onBind(Intent intent) {
    return binder;
    }

    public class LocalBinder extends Binder {
    public BluetoothLeService getService() {
    return BluetoothLeService.this;
    //return getInstance();
    }
    }
}

나 같은 경우에는 처음부터 코드를 짠 것이 아니라 예전 사람이 회사에서 만든 코드를 수정하는 경우였다.
근데, 그 분이 Application 객체에다가 서비스를 시작했다;;
처음엔 이해가 안되어서 나 같은 경우에는 BaseActivity에다가 서비스를 시작했었는데 문제가 액티비티가 닫을 때마다 BaseActivity도 닫혔다 다시 열리면서 BluetoothService가 다시 초기화 되는 문제가 생겼다.

그래서 나도 그냥 Application 객체에다가 BluetoothLeService 변수를 넣어서 초기화했다.
좋은 코드 구조인지는 모르겠다;;

내가 원하는 곳에서 bindService()를 호출하면 서비스가 시작된다.
서비스 객체는 인텐트를 통해서 전달된다.

이 BluetoothLeService가 제대로 bind 되었는지(연결 되었는지) 확인하려면 ServiceConnection을 구현하여 콜백으로 연결 상태를 받아볼 수 있다.
(주의할 점 : BLE 데이터를 주고받을 때 Characteristic의 집합인 서비스와 헷갈리면 안된다!!)

public void initBle() {
Log.d(TAG, "initBle");
Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
boolean serviceConnected = bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
Log.d(TAG, "initBle, serviceConnected: " + serviceConnected);


bleScanner = bleAdapter.getBluetoothLeScanner();
if (checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED)
scanLeDevice();
}
난 Application 클래스에 initBle() 함수를 만들어서
BluetoothLeService를 바인드하는 함수를 만들어주었다.

Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
boolean serviceConnected = bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
헷갈린다면 위 코드만 보면된다.
BluetoothLeService를 시작하는 코드다.

나 같은 경우에는 MainActivity가 시작하면 getApplicationContext().initBle() 로 BluetoothLeService를 시작했다.

private final ServiceConnection serviceConnection = new ServiceConnection() {

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected: ");
bleService = ((BluetoothLeService.LocalBinder) service).getService();
if (bleService != null) {
if (!bleService.initialize()) {
Log.e(TAG, "Unable to initialize Bluetooth");
}
// call functions on service to check connection and connect to devices

if (checkBlePermission(blePermissions)) {
if (foundDevice != null) bleService.connect(foundDevice.getAddress());
}
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected: ");
bleService = null;
}
};
그리고 마찬가지로 Application 클래스에 ServiceConnection 객체를 만들어서 BluetoothLeService가 제대로 바인드 되었는지 신호를 받는다.

블루투스 어댑터 설정하기

서비스를 시작하고나면 블루투스 어댑터가 필요하다.
public class BluetoothLeService extends Service {
public static final String TAG = "BluetoothLeService";


private BluetoothAdapter bleAdapter;


public boolean initialize() {
mContext = this;
bleAdapter = BluetoothAdapter.getDefaultAdapter();
bluetoothLeScanner = bleAdapter.getBluetoothLeScanner();
if (bleAdapter == null) {
Log.e(TAG, "initialize: Unable to obtain a BluetoothAdapter.");
return false;
}
return true;
} ......
}

initialize 함수를 통해서 블루투스 어댑터 객체를 얻자.

BLE 디바이스와 연결하기

BluetoothLeService가 시작된 후, BLE 디바이스와 BLE 연결을 할 수 있다.

public class BluetoothLeService extends Service {

..............

public boolean connect(final String address) {
if (bleAdapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
return false;
}

try {
final BluetoothDevice device = bleAdapter.getRemoteDevice(address);
// connect to the GATT server on the device
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return false;
}

bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);
return true;
} catch (IllegalArgumentException exception) {
Log.w(TAG, "Device not found with provided address.");
return false;
}
} }
connect() 함수를 보면 bleAdapter(BluetoothAdapter 객체 저장한 변수)에서 getRemoteDevice()를 통해 Device 객체를 얻습니다.
getRemoteDevice()의 매개변수 address는 스캔할 때 내가 원하던 기기를 저장한 foundDevice 변수를 통해 전달하였습니다.

그리고 device.connectGatt() 함수를 통해서 ble 연결을 시도합니다.

GATT callback 선언하기

앱 구성요소(예: 액티비티)가 서비스한테 어떤 기기와 연결할 것을 요청하면 서비스는 BLE기기의 GATT 서버와 연결을 해야합니다.
이 과정에서 BluetoothGattCallback이 필요합니다.
BluetoothGattCallback의 역할은 ble기기와의 연결상태, service 찾기, characteristic 읽기, characteristic 쓰기에 대한 notification을 알려주는 것입니다.

아래 구현한 BluetoothGattCallback은 BluetoothLEService 클래스에 구현한 코드입니다.
private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// successfully connected to the GATT Server
Log.d(TAG, "onConnectionStateChange, coneccted to the GATT Server");
connectionState = STATE_CONNECTED;
broadcastUpdate(ACTION_GATT_CONNECTED);
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return;
}
boolean result = gatt.discoverServices();
Log.d(TAG, "onConnectionStateChange, discoverServices: " + result);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// disconnected from the GATT Server
Log.d(TAG, "onConnectionStateChange, disconnected to the GATT Server");
connectionState = STATE_DISCONNECTED;
broadcastUpdate(ACTION_GATT_DISCONNECTED);
}
}

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (ActivityCompat.checkSelfPermission(BluetoothLeService.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "onServicesDiscovered: Ble Permission Denied");
return;
}

if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
List<BluetoothGattService> gattServices = gatt.getServices();
Log.d(TAG, "onServicesDiscovered, List of gattServices: " + gattServices.toString());
bluetoothGattWriteService = gattServices.get(3);
notifyCharacteristic = bluetoothGattWriteService.getCharacteristics().get(1);
notifyDescriptor = notifyCharacteristic.getDescriptor(Constants.READ_DESCRIPTOR_UUID);
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean notificationDescriptor = gatt.writeDescriptor(notifyDescriptor);
Log.d(TAG, "onServicesDiscovered, notifyCharaceristic UUID: " + notifyCharacteristic.getUuid().toString());
if(notifyCharacteristic != null) {
boolean notificationResult = gatt.setCharacteristicNotification(notifyCharacteristic, true);
Log.d(TAG, "notificationResult: " + notificationResult);
}

if(notifyDescriptor != null){
Log.d(TAG, "notify Descriptor: " + notificationDescriptor);
}
for (BluetoothGattService service : gattServices) {
Log.d(TAG, "onServicesDiscovered, Service UUID: " + service.getUuid().toString());

List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
Log.d(TAG, "onServicesDiscovered, characteristics: " + characteristics);
for (BluetoothGattCharacteristic characteristic : characteristics) {
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
//bjs: 여기서 ble 데이터 읽는다
Log.d(TAG, "onServicesDiscovered, characteristic: " + characteristic);
boolean result = gatt.readCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, read result: " + result);
}

if (hasProperty(characteristic, PROPERTY_WRITE)) {
boolean writeResult = gatt.writeCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, writeResult: " + writeResult);
}
}
}
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.d(TAG, "onCharacteristicRead: " + "gatt = " + gatt + "status = " + status);
byte[] value = characteristic.getValue();
String readData = byteArrayToHexaString(value);
Log.d(TAG, "onCharacteristicRead, readData: " + readData);

}

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
if(status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "OnCharacteristicWrite: Write Success");
byte[] writeData = characteristic.getValue();
if(writeData != null) {
//String writeMessage = writeData.toString();
Log.d(TAG, "writeData: " + byteArrayToHexaString(writeData));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_WRITE);
sendIntent.putExtra(Constants.SEND_MSG, new String(writeData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}
}
}

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.d(TAG, "onCharacteristicChanged: notify");
byte[] notifyData = characteristic.getValue();
if(notifyData != null) {
Log.d(TAG, "Notify Value: " + byteArrayToHexaString(characteristic.getValue()));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_READ);
sendIntent.putExtra(Constants.SEND_MSG, new String(notifyData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}


}
};
onConnectionStateChanged()는  ble기기의 GATT서버와의 연결상태에 변화가 있을 때 호출됩니다.
onServicesDiscovered()는 gatt.discoverServices()를 실행했을 때 호출됩니다.
onCharacteristicRead()는 gatt.readCharacteristic()을 실행했을 때 호출됩니다.
onCharacteristicWrite()는 gatt.writeCharacteristic()을 실행했을 때 호출됩니다.
onCharacteristicChanged()는 gatt.setCharacterisitcNotification()을 실행했을 때, 기기의 notification characteristic을 받는 콜백함수입니다.

저 같은 경우는 각 콜백함수에서 값을 받으면 브로드캐스트 리시버로 전달해주었습니다. ㅎ

GATT service와 연결하기

BluetoothGattCallback을 선언했으면 BluetoothLeService는 GATT 서버와 연결할 수 있습니다.
(GATT service = GATT server 라고 보시면 될거 같아요.)

public boolean connect(final String address) {
if (bleAdapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
return false;
}

try {
final BluetoothDevice device = bleAdapter.getRemoteDevice(address);
// connect to the GATT server on the device
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return false;
}

bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);
return true;
} catch (IllegalArgumentException exception) {
Log.w(TAG, "Device not found with provided address.");
return false;
}
}
위 코드는 BluetoothLeService 클래스에 구현한 connect 함수입니다.
ble기기의 GATT 서버와 연결할 때는 connectGatt() 함수를 사용하구요.
매개변수로 Context 객체, autoConnect boolean flag, 그리고 앞서 구현한 BluetoothGattCallback이 필요합니다.

Broadcast updates

GATT 서버와 연결을 시도한 후 그 결과를 앱의 구성요소(예 : 액티비티)에 알려주어야 상화엥 맞게 코드를 진행하겠죠?
이때 안드로이드 공식문서에서는 BroadcastReceiver를 활용하는 것을 예시로 보여줍니다.

private void broadcastUpdate(final String action) {
final Intent intent = new Intent(action);
getApplicationContext().sendBroadcast(intent);
}
BluetoothLeService 클래스에 broadcastUpdate 함수를 선언해줍니다.

private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// successfully connected to the GATT Server
Log.d(TAG, "onConnectionStateChange, coneccted to the GATT Server");
connectionState = STATE_CONNECTED;
broadcastUpdate(ACTION_GATT_CONNECTED);
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return;
}
boolean result = gatt.discoverServices();
Log.d(TAG, "onConnectionStateChange, discoverServices: " + result);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// disconnected from the GATT Server
Log.d(TAG, "onConnectionStateChange, disconnected to the GATT Server");
connectionState = STATE_DISCONNECTED;
broadcastUpdate(ACTION_GATT_DISCONNECTED);
}
}

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (ActivityCompat.checkSelfPermission(BluetoothLeService.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "onServicesDiscovered: Ble Permission Denied");
return;
}

if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
List<BluetoothGattService> gattServices = gatt.getServices();
Log.d(TAG, "onServicesDiscovered, List of gattServices: " + gattServices.toString());
bluetoothGattWriteService = gattServices.get(3);
notifyCharacteristic = bluetoothGattWriteService.getCharacteristics().get(1);
notifyDescriptor = notifyCharacteristic.getDescriptor(Constants.READ_DESCRIPTOR_UUID);
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
boolean notificationDescriptor = gatt.writeDescriptor(notifyDescriptor);
Log.d(TAG, "onServicesDiscovered, notifyCharaceristic UUID: " + notifyCharacteristic.getUuid().toString());
if(notifyCharacteristic != null) {
boolean notificationResult = gatt.setCharacteristicNotification(notifyCharacteristic, true);
Log.d(TAG, "notificationResult: " + notificationResult);
}

if(notifyDescriptor != null){
Log.d(TAG, "notify Descriptor: " + notificationDescriptor);
}
for (BluetoothGattService service : gattServices) {
Log.d(TAG, "onServicesDiscovered, Service UUID: " + service.getUuid().toString());

List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
Log.d(TAG, "onServicesDiscovered, characteristics: " + characteristics);
for (BluetoothGattCharacteristic characteristic : characteristics) {
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
//bjs: 여기서 ble 데이터 읽는다
Log.d(TAG, "onServicesDiscovered, characteristic: " + characteristic);
boolean result = gatt.readCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, read result: " + result);
}

if (hasProperty(characteristic, PROPERTY_WRITE)) {
boolean writeResult = gatt.writeCharacteristic(characteristic);
Log.d(TAG, "onServicesDiscovered, writeResult: " + writeResult);
}
}
}
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.d(TAG, "onCharacteristicRead: " + "gatt = " + gatt + "status = " + status);
byte[] value = characteristic.getValue();
String readData = byteArrayToHexaString(value);
Log.d(TAG, "onCharacteristicRead, readData: " + readData);

}

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
if(status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "OnCharacteristicWrite: Write Success");
byte[] writeData = characteristic.getValue();
if(writeData != null) {
//String writeMessage = writeData.toString();
Log.d(TAG, "writeData: " + byteArrayToHexaString(writeData));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_WRITE);
sendIntent.putExtra(Constants.SEND_MSG, new String(writeData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}
}
}

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.d(TAG, "onCharacteristicChanged: notify");
byte[] notifyData = characteristic.getValue();
if(notifyData != null) {
Log.d(TAG, "Notify Value: " + byteArrayToHexaString(characteristic.getValue()));
Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_READ);
sendIntent.putExtra(Constants.SEND_MSG, new String(notifyData));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(sendIntent);
}


}
};
그리고 앞에서 본 BluetoothGattCallback에서 broadcastUpdate 함수를 활용하여 변화된 Gatt서버의 상태를 전달합니다.

Broadcast 받기

broadcastUpdate()를 통해 GATT 서버의 상태를 알렸다면 받는 곳이 있어야겠죠?
저 같은 경우에는 Application 클래스에 브로드캐스트 리시버를 등록했어요.
안드로이드 공식 문서에서는 액티비티에 브로드 캐스트 리시버를 등록하는 예를 보여주고 있구요.


public class BTApplication extends Application {
private static final String TAG = "BTApplication";


String[] permissions = new String[]{
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADVERTISE,
};

/*
bjs: 새로운 기기는 ble모듈을 장착한다.
*/

public BluetoothLeService bleService;
private BluetoothAdapter bleAdapter = BluetoothAdapter.getDefaultAdapter();
public BluetoothDevice foundDevice;

boolean bleConnected;

private BluetoothLeScanner bleScanner;
private boolean scanning;
// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;
//

@Override
public void onCreate() {
// Log.d(TAG, "onCreate()");
super.onCreate();

/* bjs: 새로운 기기는 ble모듈을 장착한다. */
this.registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter());
if (bleService != null && foundDevice != null) {
final boolean result = bleService.connect(foundDevice.getAddress());
Log.d(TAG, "Connect request result=" + result);
}
//
}



@Override
public void onTerminate() {
Log.d(TAG, "onTerminate()");

super.onTerminate();

if (mBtService != null)
mBtService.stop();

if (mBluetoothAdapter != null)
mBluetoothAdapter.disable();

if (receiver != null)
unregisterReceiver(receiver);

unregisterReceiver(gattUpdateReceiver);

//bjs : ble
unbindService(serviceConnection); //메모리 릭 방지
unregisterReceiver(receiver);
//
}



private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
bleConnected = true;
setBleConnected(true);
ActivityUtil.showToast(getBaseContext(), "Ble Connected");
//updateConnectionState(R.string.connected);
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
bleConnected = false;
setBleConnected(false);
ActivityUtil.showToast(getBaseContext(), "Ble Disconnected");
//updateConnectionState(R.string.disconnected);
} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
Log.d(TAG, "onReceive: Service Discovered");
//Show all the supported services and characteristics on the user interface
//displayGattServices(bleService.getSupportedGattServices());
}
}
};

private static IntentFilter makeGattUpdateIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
return intentFilter;
}

protected Boolean checkBlePermission(String[] permissions) {
for (String permission : permissions) {
if (checkSelfPermission(permission) == PackageManager.PERMISSION_DENIED) return false;
}
return true;
}


public boolean isBleConnected() {
return bleConnected;
}

public void setBleConnected(boolean bleConnected) {
this.bleConnected = bleConnected;
}
}
일단 makeGattUpdateIntentFilter() 함수를 활용하여 BroadcastReceiver를 선언해줍니다.
onCreate()에서 브로드캐스트 리시버를 등록해주구요.
onTerminate()에서 브로드캐스트 리시버를 등록해제시킵니다.

이 글은 BLE 연결에 대해 다루기때문에 브로드캐스트 리시버에 대해서는 자세히 다루지 않을게요.

제 블로그에 브로드캐스트 리시버에 대한 글을 다룬 적이 있어요.
자세한 내용은 아래 링크를 참고해주세요.👇

Close GATT connection

GATT 서버와의 연결을 끊는 과정도 역시 중요하죠?

class BluetoothService extends Service {

...

      @Override
      public boolean onUnbind(Intent intent) {
          close();
          return super.onUnbind(intent);
      }

      private void close() {
          if (bluetoothGatt == null) {
              Return;
          }
          bluetoothGatt.close();
          bluetoothGatt = null;
      }
}
 위 코드는 안드로이드 공식문서에 있는 코드를 그대로 가져온 것입니다.
서비스 클래스에서 onUnbind() 함수에서 close() 함수를 호출하여 GATT 서버와의 연결을 해제합니다.

여기까지 BLE연결에 대한 개념, 스캔, GATT 서버와의 연결까지 다루어봤습니다.

다음 글에서는 BLE기기와 데이터를 주고받는 방법에 대해 알아보도록 하겠습니다.




















































Share:
Read More