22.03.31 추가 : readCharacter부분수정 및 writeCharacter 추가
어느 날, BLE 를 사용하는 임베디드 기기의 앱 개발건을 맡아 출시 이후 유지보수 단계인데, 난데없는 이슈사항이 하나 있었다.
보통의 상황이라면 (내가 더럽게 짠 코드라면), NullPointerException 을 단발마처럼 남기며 예외 상황에 대응을 안한 나를 탓하고 있을 것이다.
하지만 디버깅 로그를 찍어보니 정말 아무 로그도 들어오고 있지 않았다.
이 상태로 30분을 넘게 켜놔도 묵묵부답이었다. 혹시나 싶어 내 개발용 휴대폰 중 하나인 S6을 꺼내서 디버깅 해봤다.
오기가 생겨 디바이스 몇개를 테스트 해 보니 대충 아래와 같았다
기기 | 작동여부 |
---|---|
갤럭시 S6 | O |
갤럭시 S7 | X |
갤럭시 S8 | X |
갤럭시 S9 | O |
갤럭시 S10 | O |
갤럭시 S20 | O |
갤럭시 S20 Ultra | X |
갤럭시 Z Flip 5G | X |
갤럭시 Z Fold 1 | X |
LG V50 | O |
LG V20 | O |
LG G6 | X |
LG G7 | O |
상대적 구형 디바이스라면 호환성 표에 등록하고 지원을 안하면 끝나지만, 그 당시 최신 디바이스에서 작동을 안하는건 매우 큰 문제라, 해결 하기로 마음먹었다.
일단 모든 로그를 보기 위해 코드를 약간 수정하였다.
@Override
public void log(final int priority, @NonNull final String message) {
Log.println(priority, "MyBleManager ", message);
}
문제가 된 디바이스인 S7에서는 아래와 같은 로그가 나온다.
V/MyBleManager: Connecting...
D/MyBleManager: gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, LE 1M)
D/MyBleManager: [Callback] Connection state changed with status: 0 and new state: 2 (CONNECTED)
I/MyBleManager: Connected to EA:C4:26:58:9A:10
D/MyBleManager: wait(300)
V/MyBleManager: Discovering services...
D/MyBleManager: gatt.discoverServices()
I/MyBleManager: Connection parameters updated (interval: 30.0ms, latency: 0, timeout: 2000ms)
I/MyBleManager: Connection parameters updated (interval: 7.5ms, latency: 0, timeout: 5000ms)
I/MyBleManager: Services discovered
V/MyBleManager: Primary service found
V/MyBleManager: Requesting new MTU...
D/MyBleManager: gatt.requestMtu(247)
I/MyBleManager: Connection parameters updated (interval: 30.0ms, latency: 0, timeout: 2000ms)
I/MyBleManager: MTU changed to: 23
I/MyBleManager: MTU set to 23
V/MyBleManager: Requesting preferred PHYs...
D/MyBleManager: gatt.setPreferredPhy(LE 2M, LE 2M, coding option = No preferred)
V/MyBleManager: Disconnecting...
D/MyBleManager: gatt.disconnect()
D/MyBleManager: [Callback] Connection state changed with status: 0 and new state: 0 (DISCONNECTED)
I/MyBleManager: Disconnected
D/MyBleManager: gatt.close()
I/MyBleManager: Target initialized
이상하게 PHY요청 부분에서 Requsting 시도만 하면 Disconnection이 뜨는거로 봐선, 호스트 장비에서 PHY Specs를 맞추지 못하여 시도가 거절 되는 느낌이었다.
바로 호스트 장비의 칩 Specs를 확인 해 보았다. Nordic사의 NRF51822 칩을 사용하는데, BLE 4.0 규격의 칩이었다.
어디선가 BLE 5.0규격에서 새로 PHY 2M이 추가되었단 걸 광고하던게 생각났다. 그래서 바로 PHY요청 부분을 삭제해 보았다.
@Override
protected void initialize() {
// You may enqueue multiple operations. A queue ensures that all operations are
// performed one after another, but it is not required.
beginAtomicRequestQueue()
.add(requestMtu(247) // Remember, GATT needs 3 bytes extra. This will allow packet size of 244 bytes.
.with((device, mtu) -> log(Log.INFO, "MTU set to " + mtu))
.fail((device, status) -> log(Log.WARN, "Requested MTU not supported: " + status)))
/* .add(setPreferredPhy(PhyRequest.PHY_LE_2M_MASK, PhyRequest.PHY_LE_2M_MASK, PhyRequest.PHY_OPTION_NO_PREFERRED)
.fail((device, status) -> log(Log.WARN, "Requested PHY not supported: " + status)))*/
// PHY 요청 비활성화
.add(enableNotifications(firstCharacteristic))
.done(device -> log(Log.INFO, "Target initialized"))
.enqueue();
setNotificationCallback(firstCharacteristic).with(new DataReceivedCallback() {
@Override
public void onDataReceived(@NonNull BluetoothDevice device, @NonNull Data data) {
Log.d("MyBleManager", data.getStringValue(0));
}
});
}
다시 디버깅을 실행하니 유레카, 위에 있는 모든 표의 장비가 정상 동작 하였다.
I/MyBleManager: Connected to EA:C4:26:58:9A:10
D/MyBleManager: wait(300)
V/MyBleManager: Discovering services...
D/MyBleManager: gatt.discoverServices()
D/BluetoothGatt: discoverServices() - device: EA:C4:26:58:9A:10
D/BluetoothGatt: onConnectionUpdated() - Device=EA:C4:26:58:9A:10 interval=24 latency=0 timeout=200 status=0
I/MyBleManager: Connection parameters updated (interval: 30.0ms, latency: 0, timeout: 2000ms)
D/BluetoothGatt: onConnectionUpdated() - Device=EA:C4:26:58:9A:10 interval=6 latency=0 timeout=500 status=0
I/MyBleManager: Connection parameters updated (interval: 7.5ms, latency: 0, timeout: 5000ms)
D/BluetoothGatt: onSearchComplete() = Device=EA:C4:26:58:9A:10 Status=0
I/MyBleManager: Services discovered
V/MyBleManager: Primary service found
V/MyBleManager: Requesting new MTU...
D/MyBleManager: gatt.requestMtu(247)
D/BluetoothGatt: configureMTU() - device: EA:C4:26:58:9A:10 mtu: 247
D/BluetoothGatt: onConfigureMTU() - Device=EA:C4:26:58:9A:10 mtu=23 status=0
I/MyBleManager: MTU changed to: 23
I/MyBleManager: MTU set to 23
D/MyBleManager: gatt.setCharacteristicNotification(6e400003-b5a3-f393-e0a9-e50e24dcca9e, true)
D/BluetoothGatt: setCharacteristicNotification() - uuid: 6e400003-b5a3-f393-e0a9-e50e24dcca9e enable: true
V/MyBleManager: Enabling notifications for 6e400003-b5a3-f393-e0a9-e50e24dcca9e
D/MyBleManager: gatt.writeDescriptor(00002902-0000-1000-8000-00805f9b34fb, value=0x01-00)
D/BluetoothGatt: onConnectionUpdated() - Device=EA:C4:26:58:9A:10 interval=24 latency=0 timeout=200 status=0
I/MyBleManager: Connection parameters updated (interval: 30.0ms, latency: 0, timeout: 2000ms)
I/MyBleManager: Data written to descr. 00002902-0000-1000-8000-00805f9b34fb, value: (0x) 01-00
I/MyBleManager: Notifications enabled
I/MyBleManager: Target initialized
I/MainActivity: Device initiated
I/MyBleManager: Notification received from 6e400003-b5a3-f393-e0a9-e50e24dcca9e, value: (0x) 76-65-72-3A-31-2E-30-2E-30
D/MyBleManager: ver:1.0.0
이 얼마나 반가운 디버깅 메세지인지..!
하지만 왜 지원하지 않는 PHY를 입력 했음에도 작동하는 이유가 궁금 해 Nordic측에 질문을 해보니,
"기기마다 동작하는 방법이 약간 씩 다른데 PHY 1M -> 2M 이런식으로 요청 하는 경우도 있고 바로 PHY 2M을 요청하는 경우도 있다. 안되는 기기들은 후자에 해당하여 작동을 안한 것 같다" 라고 답변이 왔다.
사실 BLE Connection 문제는 조금만 둘러보기만 해도 기기별, 칩셋별, 라이브러리 별 문제가 혼합되어 있다.
특히나 연결은 되는데 나같이 데이터가 안들어오는 상황이라면, 정말 눈물겨운 상황이다.
이 글을 통해 누군가 나같은 사례가 있으면 어서 해결하고 밥먹으러 갔으면 좋겠다
끝!
1. build.gradle
implementation 'no.nordicsemi.android:ble:2.2.4 '
2. BLEConnector.java
public class BLEConnector extends BleManager {
private static final String TAG = "BLEConnector";
private static final UUID SERVICE_UUID = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
private static final UUID WRITE_CHAR = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
private static final UUID READ_CHAR = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
private BluetoothGattCharacteristic readCharacteristic;
private BluetoothGattCharacteristic writeCharacteristic;
public BLEConnector(@NonNull final Context context) {
super(context);
}
@NonNull
@Override
protected BleManagerGattCallback getGattCallback() {
return new MyManagerGattCallback();
}
@Override
public void log(final int priority, @NonNull final String message) {
Log.println(priority, "BLEConnector", message);
}
private class MyManagerGattCallback extends BleManagerGattCallback {
@Override
public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(SERVICE_UUID);
if (service != null) {
readCharacteristic = service.getCharacteristic(READ_CHAR);
writeCharacteristic = service.getCharacteristic(WRITE_CHAR);
}
boolean notify = false;
if (readCharacteristic != null) {
final int properties = readCharacteristic.getProperties();
notify = (properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0;
}
return readCharacteristic != null && writeCharacteristic != null && notify;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
return super.isOptionalServiceSupported(gatt);
}
@Override
protected void initialize() {
beginAtomicRequestQueue()
.add(requestMtu(247) // Remember, GATT needs 3 bytes extra. This will allow packet size of 244 bytes.
.with((device, mtu) -> log(Log.INFO, "MTU set to " + mtu))
.fail((device, status) -> log(Log.WARN, "Requested MTU not supported: " + status)))
.add(enableNotifications(readCharacteristic))
.add(enableNotifications(writeCharacteristic))
.done(device -> log(Log.INFO, "Target initialized"))
.enqueue();
// Set a callback for your notifications. You may also use waitForNotification(...).
// Both callbacks will be called when notification is received.
setNotificationCallback(readCharacteristic).with(new DataReceiveHandler());
enableNotifications(readCharacteristic)
.done(device -> log(Log.INFO, "Read notifications enabled"))
.fail((device, status) -> log(Log.WARN, "Read characteristic not found"))
// PHY 요청부분 삭제
.enqueue();
enableNotifications(writeCharacteristic)
.done(device -> log(Log.INFO, "Write notifications enabled"))
.fail((device, status) -> log(Log.WARN, "Write characteristic not found"))
.enqueue();
}
@Override
protected void onDeviceDisconnected() {
log(Log.INFO, "Target disconnected");
}
@Override
protected void onServicesInvalidated() {
}
}
private class DataReceiveHandler implements ProfileDataCallback {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
String receiveInfo = device.getAddress() + "#" + data.getStringValue(0); // byte로 받을 때 getValue로 받으면 byte
Log.d(TAG, receiveInfo);
}
}
public void writeCharacter(String data) {
if (writeCharacteristic == null) {
return;
}
writeCharacteristic(
writeCharacteristic,
data.getBytes(StandardCharsets.UTF_8),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
.enqueue();
}
public void abort() {
cancelQueue();
}
}
3. MainActivity.java
public class MainActivity extends AppCompatActivity {
///
public void connect(String address) {
final BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
BLEConnector manager = new BLEConnector(this);
manager.connect(d)
.timeout(100000)
.retry(5, 400)
.usePreferredPhy(PhyRequest.PHY_LE_1M_MASK | PhyRequest.PHY_LE_2M_MASK)
.done(device -> Log.i("BLEConnector", "Device initiated"))
.fail((device, status) -> {
if (status == FailCallback.REASON_DEVICE_DISCONNECTED) {
Log.e("BLEConnector", "Device Connection is Disconnected");
} else if (status == FailCallback.REASON_DEVICE_NOT_SUPPORTED) {
Log.e("BLEConnector", "Device Connection is Not Supported");
} else if (status == FailCallback.REASON_REQUEST_FAILED) {
Log.e("BLEConnector", "Device Connection is Request Failed");
} else if (status == FailCallback.REASON_VALIDATION) {
Log.e("BLEConnector", "Device Connection is Not Validation");
} else if (status == FailCallback.REASON_TIMEOUT) {
Log.e("BLEConnector", "Device Connection is Timeout");
}
})
.useAutoConnect(false)
.enqueue();
}
///
}
혹시 두개의쌍이(총4개) 서로를 잘 찾아 연결할 수 있게 UUID를 다르게 한다던지 해서 페어링 하는 방법이 있을까요?