Search code examples
rxandroidble

How to receive all notifications in RxAndroidBle


I'm trying to communicate with a BLE data logger/sensor using rxBleAndroid running on both an Android phone as well as the Raspberry Pi using Android Things.

I'm currently having an issue, however, where up to about 5 of the first notifications are never received by my app.

I have verified that the BLE device is actually successfully sending all the expected notifications. I have done that through the nRF Connect app and everything works as expected through there.

When I do it through the nRF Connect app, these are the steps I take:

  1. Write to password characteristic to unlock device
  2. Write to mode characteristic to put device in correct mode
  3. Subscribe to notifications (and notifications immediately start working)

When doing it through RxAndroidBle, I suspect it may be that the .subscribe() is not being setup fast enough.

Is there maybe some way to do setupNotification(), and then write the characteristics to tell the device to start sending notifications?

Here is my current code:

rxBleClient = RxBleClient.create(this);
RxBleDevice device = rxBleClient.getBleDevice(mac_address);

device.establishConnection(false)
        .flatMap(rxBleConnection -> rxBleConnection.writeCharacteristic(pword_uuid, pword)
                .flatMap(ignored1 -> rxBleConnection.writeCharacteristic(mode_uuid, mode))
                .flatMap(ignored2 -> rxBleConnection.setupNotification(log_uuid))
        )
        .flatMap(notificationObservable -> notificationObservable)
        .subscribe(
                bytes -> {
                    System.out.println(">>> data from device " + bytesToHex(bytes));
                },
                throwable -> {
                    System.out.println("error");
                    System.out.println(throwable);
                });

Solution

  • Most actions that one can do over BLE are asynchronous and take some time to finish. Setting up the notifications is no exception—it is a two-step procedure:

    1. setup a local notification
    2. write a Client Characteristic Configuration Descriptor of the characteristic one wants to get notifications from

    If your peripheral is first set to send notifications before the notifications are ready to be received by the central then some of the data may get lost during the notifications setup procedure.

    Is there maybe some way to do setupNotification(), and then write the characteristics to tell the device to start sending notifications?

    Of course (this is how usually similar scenarios are handled)—there are multiple possible implementations. One of them could look like this:

    device.establishConnection(false) // establish the connection
            .flatMap(rxBleConnection -> rxBleConnection.setupNotification(log_uuid) // once the connection is available setup the notification
                    .flatMap(logDataObservable -> Observable.merge( // when the notification is setup start doing three things at once
                            rxBleConnection.writeCharacteristic(pword_uuid, pword).ignoreElements(), // writing the `pword` but ignore the result so the output of this .merge() will contain only log data
                            rxBleConnection.writeCharacteristic(mode_uuid, mode).ignoreElements(), // same as the line above but for `mode`
                            logDataObservable // observing the log data notifications
                    ))
            )
            .subscribe(
                    bytes -> System.out.println(">>> data from device " + bytesToHex(bytes)),
                    throwable -> {
                        System.out.println("error");
                        System.out.println(throwable);
                    }
            );
    

    Edit:

    As it was mentioned in the comments below—the peripheral does not allow for any BLE interactions before setting the mode and writing the password. As I have written above setting the notifications is a two step-step procedure with a local step and remote (executed on peripheral) one which is executed before the mode/password in the above code snippet. It is possible to separate those two steps by using the NotificationSetupMode.COMPAT mode and writing the Client Characteristic Configuration Descriptor manually later:

    UUID clientCharacteristicConfigDescriptorUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
    device.establishConnection(false) // establish the connection
            .flatMap(
                    RxBleConnection::discoverServices,  // once the connection is available discover the services of the peripheral
                    (rxBleConnection, rxBleDeviceServices) -> // and when we have the connection and services
                            rxBleDeviceServices.getCharacteristic(log_uuid) // we get the log characteristic (on which we will setup the notification and write the descriptor)
                                    .flatMap(logDataCharacteristic -> // once the log characteristic is retrieved
                                            rxBleConnection.setupNotification(logDataCharacteristic, NotificationSetupMode.COMPAT) // we setup the notification on it in the COMPAT mode (without writing the CCC descriptor)
                                                    .flatMap(logDataObservable -> Observable.merge( // when the notification is setup start doing four things at once
                                                            rxBleConnection.writeCharacteristic(pword_uuid, pword).ignoreElements(), // writing the `pword` but ignore the result so the output of this .merge() will contain only log data
                                                            rxBleConnection.writeCharacteristic(mode_uuid, mode).ignoreElements(), // same as the line above but for `mode`
                                                            rxBleConnection.writeDescriptor(logDataCharacteristic.getDescriptor(clientCharacteristicConfigDescriptorUuid), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE).ignoreElements(), // and we write the CCC descriptor manually
                                                            logDataObservable // observing the log data notifications
                                                    ))
                                    )
            )
            .flatMap(observable -> observable) // flatMap to get the raw byte[]
            .subscribe(
                    bytes -> System.out.println(">>> data from device " + bytesToHex(bytes)),
                    throwable -> {
                        System.out.println("error");
                        System.out.println(throwable);
                    }
            );
    

    rxBleConnection.discoverServices() call may be omitted if we would know the Log Characteristic Service UUID and use rxBleConnection.writeDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid function.

    UUID clientCharacteristicConfigDescriptorUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
    device.establishConnection(false) // establish the connection
            .flatMap(rxBleConnection -> rxBleConnection.setupNotification(log_uuid, NotificationSetupMode.COMPAT) // once the connection is available setup the notification w/o setting Client Characteristic Config Descriptor
                    .flatMap(logDataObservable -> Observable.merge( // when the notification is setup start doing three things at once
                            rxBleConnection.writeCharacteristic(pword_uuid, pword).ignoreElements(), // writing the `pword` but ignore the result so the output of this .merge() will contain only log data
                            rxBleConnection.writeCharacteristic(mode_uuid, mode).ignoreElements(), // same as the line above but for `mode`
                            rxBleConnection.writeDescriptor(log_service_uuid, log_uuid, clientCharacteristicConfigDescriptorUuid).ignoreElements(), // same as the above line but for writing the CCC descriptor
                            logDataObservable // observing the log data notifications
                    ))
            )
            .subscribe(
                    bytes -> System.out.println(">>> data from device " + bytesToHex(bytes)),
                    throwable -> {
                        System.out.println("error");
                        System.out.println(throwable);
                    }
            );
    

    Edit 2:

    Since version 1.8.0 there is a new NotificationSetupMode.QUICK_SETUP which first turns on internal notifications and then writes the CCC descriptor value.

    rxBleConnection.setupNotification(log_uuid, NotificationSetupMode.QUICK_SETUP)
    

    Pros:

    • Observable<byte[]> is emitted before the descriptor is written allowing for notification observation right from the beginning (if the beginning is writing the descriptor)

    Cons:

    • There is no way to tell when exactly the descriptor has been written.