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