Android에서 Nordic BLE 사용 시 특정 기기에서 Connection 이슈 해결하기

NamYounSu·2021년 9월 30일
2

22.03.31 추가 : readCharacter부분수정 및 writeCharacter 추가

서론

어느 날, BLE 를 사용하는 임베디드 기기의 앱 개발건을 맡아 출시 이후 유지보수 단계인데, 난데없는 이슈사항이 하나 있었다.

"기기가 연결되어도 아무 반응이 없어요"


난 그만 정신이 아득해지는걸 느꼈다.

상황 파악

보통의 상황이라면 (내가 더럽게 짠 코드라면), NullPointerException 을 단발마처럼 남기며 예외 상황에 대응을 안한 나를 탓하고 있을 것이다.

하지만 디버깅 로그를 찍어보니 정말 아무 로그도 들어오고 있지 않았다.

이 상태로 30분을 넘게 켜놔도 묵묵부답이었다. 혹시나 싶어 내 개발용 휴대폰 중 하나인 S6을 꺼내서 디버깅 해봤다.

오기가 생겨 디바이스 몇개를 테스트 해 보니 대충 아래와 같았다

기기작동여부
갤럭시 S6O
갤럭시 S7X
갤럭시 S8X
갤럭시 S9O
갤럭시 S10O
갤럭시 S20O
갤럭시 S20 UltraX
갤럭시 Z Flip 5GX
갤럭시 Z Fold 1X
LG V50O
LG V20O
LG G6X
LG G7O

상대적 구형 디바이스라면 호환성 표에 등록하고 지원을 안하면 끝나지만, 그 당시 최신 디바이스에서 작동을 안하는건 매우 큰 문제라, 해결 하기로 마음먹었다.

상황 해결

일단 모든 로그를 보기 위해 코드를 약간 수정하였다.

    @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();
    }
    ///
}

NRF51822 Eval Kit Specs
NRF Android lib Github

profile
The Power of Dreams

4개의 댓글

comment-user-thumbnail
2022년 10월 27일

혹시 두개의쌍이(총4개) 서로를 잘 찾아 연결할 수 있게 UUID를 다르게 한다던지 해서 페어링 하는 방법이 있을까요?

1개의 답글
comment-user-thumbnail
2023년 4월 19일

BLE Scan에 대해 궁금한게 있습니다. BLE SCAN시 37~39 채널의 3가지 채널을 사용하는데.
하나,또는 둘의 채널에서만 adv data를 수신할 수 있는 방법 (채널 마스킹) 이 있을까요?
구글링을해도 딱히 비슷한 정보가 보이지 않아서 BLE사용하신 선배님께 질문을 남깁니다

1개의 답글