Fitbit/bitgatt

Unable to receive characteristic notifications.

NathanRussak opened this issue · 5 comments

Describe the bug
I have a simple BLE device that begins advertising data immediately after being turned on. I am attempting to write an Android app that can detect and parse that advertisement. Following the instructions found in the README I was able to:

  1. Scan for the device.
  2. Connect to it.
  3. Discover it's services and characteristics.
  4. Subscribe for a characteristic's notifications.

My application is never notified of characteristic changes. Did I overlook anything in my setup?

override fun onStart() {
    super.onStart()
    fitGatt.registerGattEventListener(this)
    fitGatt.clientCallback
    fitGatt.startGattServer(this)
}

override fun onGattServerStarted(serverConnection: GattServerConnection?) {
    fitGatt.addDeviceNameScanFilter("EXAMPLE_DEVICE_NAME")
    fitGatt.startPeriodicScan(this);
}

override fun onBluetoothPeripheralDiscovered(connection: GattConnection?) {
    fitGatt.cancelScan(this)

    // Connect
    connection.runTx(GattConnectTransaction(connection, GattState.CONNECTED)) { connResult ->

        // Discover services / characteristic
        connection.runTx(GattClientDiscoverServicesTransaction(connection, GattState.DISCOVERY_SUCCESS)) { discResult ->
            val service = discResult.services.find { it.uuid == UUID.fromString("EXAMPLE_SERVICE_UUID") }
            val characteristic = service?.characteristics?.find { it.uuid == UUID.fromString("EXAMPLE_CHARACTERISTIC_UUID") }
            if (characteristic == null) return@runTx

            // Confirmed that this characteristic has the "notify" property.

            // Subscribe
            connection.runTx(SubscribeToCharacteristicNotificationsTransaction(connection, GattState.ENABLE_CHARACTERISTIC_NOTIFICATION_SUCCESS, characteristic)) {
                // All done?
            }
        }
    }

    connection.registerConnectionEventListener(object: ConnectionEventListener {
        override fun onClientCharacteristicChanged(result: TransactionResult, connection: GattConnection) { 
            // Never being called.
        }
        ...
    })
}

Expected behavior
The ConnectionEventListener should be notified every time my app detects a new advertisement from the subscribed characteristic.

Peripheral:
Samico ADF-B06 Pulse Oximeter

Smartphone:

  • Device: Google Pixel 2 XL
  • OS: Android Q
  • Patch Level [e.g. 22]

Thanks for reaching out.

For onStart you should start the GATT client. Gatt server (startGattServer) should be used for when your application is hosting a gatt server of its own

If you wish to start everything you can also call fitGatt.start(this)

override fun onStart() {
    super.onStart()
    fitGatt.registerGattEventListener(this)
    fitGatt.startGattClient(this)
}

You can confirm start success/failure of fitbitgatt via FitbitGattCallback.onGattClientStarted/onGattClientStartError callbacks. For example if bluetooth is off it will result in a start failure with BluetoothNotEnabledException

As well the transaction results should be validated if they are successful for any ran transaction.

Here is how it should look:

connection.runTx(GattConnectTransaction(connection, GattState.CONNECTED)) { connResult ->
    if(discResult.resultStatus == TransactionResultStatus.SUCCESS) {
        // ...continue.
     } else {
        // log or check the type of failure
     }
}

Thanks for the quick response! I was actually doing the result error checking you suggested. I just removed it from the code snippet I pasted to prevent it from getting too long.

I tried updating my onStart() per your suggestion and added more detailed logging. Unfortunately I am still not getting notifications. Here is a more verbose code snippet...

    private val fitGatt = FitbitGatt.getInstance()

    override fun onStart() {
        super.onStart()
        fitGatt.registerGattEventListener(this)
        fitGatt.startGattClient(this)
    }

    // Misc FitbitGattCallback overrides are all here with log statements in them.

    override fun onGattClientStarted() {
        Log.v("FitbitGattCallback", "onGattClientStarted()")
        Log.d("MyApp", "About to start periodic scan...")
        fitGatt.addDeviceNameScanFilter("EXAMPLE_DEVICE_NAME")
        fitGatt.startPeriodicScan(this);
    }

    override fun onBluetoothPeripheralDiscovered(connection: GattConnection?) {
        Log.v("FitbitGattCallback", "onBluetoothPeripheralDiscovered()")
        if (connection == null || !fitGatt.isScanning || connection.device.name != "EXAMPLE_DEVICE_NAME") return

        Log.d("MyApp", "Pulse oximeter discovered! Cancelling scan...")
        fitGatt.cancelScan(this)

        Log.d("MyApp", "Running GattConnectTransaction...")
        connection.runTx(GattConnectTransaction(connection, GattState.CONNECTED)) { connResult ->
            if (connResult.resultStatus != TransactionResult.TransactionResultStatus.SUCCESS) {
                Log.e("MyApp", "Failed to connect - $connResult")
                return@runTx
            }

            Log.d("MyApp", "Connection successful! Running GattClientDiscoverServicesTransaction...")
            connection.runTx(GattClientDiscoverServicesTransaction(connection, GattState.DISCOVERY_SUCCESS)) { discResult ->
                if (discResult.resultStatus != TransactionResult.TransactionResultStatus.SUCCESS) {
                    Log.e("MyApp", "Failed to discover services - $discResult")
                    return@runTx
                }

                Log.d("MyApp", "Service discovery complete!")
                val service = discResult.services.find { it.uuid == UUID.fromString("EXAMPLE_SERVICE_UUID") }
                val notifyCharacteristic = service?.characteristics?.find { it.uuid == UUID.fromString("EXAMPLE_CHARACTERISTIC_UUID") }
                if (notifyCharacteristic == null) {
                    Log.e("MyApp", "Failed to find notifying characteristic.")
                    return@runTx
                }

                Log.d("MyApp", "Notifying characteristic found. Running SubscribeToCharacteristicNotificationsTransaction...")
                connection.runTx(SubscribeToCharacteristicNotificationsTransaction(connection, GattState.ENABLE_CHARACTERISTIC_NOTIFICATION_SUCCESS, notifyCharacteristic)) { subResult ->
                    if (subResult.resultStatus != TransactionResult.TransactionResultStatus.SUCCESS) {
                        Log.e("MyApp", "Failed to subscribe - $subResult")
                    } else {
                        Log.i("MyApp", "Subscribed to characteristic- $subResult")
                    }
                }
            }
        }

        connection.registerConnectionEventListener(object: ConnectionEventListener {
            // ConnectionEventListener overrides are all here with log statements.
        })
    }

When I view my logs I see this:

2020-08-14 10:12:53.898 9893-9893/com.example.app V/FitbitGattCallback: onGattClientStarted()
2020-08-14 10:12:53.898 9893-9893/com.example.app D/MyApp: About to start periodic scan...
2020-08-14 10:12:53.905 9893-9893/com.example.app V/FitbitGattCallback: onScanStarted()
2020-08-14 10:12:53.923 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.924 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.929 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.935 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.941 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.947 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.951 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.956 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:12:53.960 9893-9955/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:13:00.624 9893-9893/com.example.app V/FitbitGattCallback: onBluetoothPeripheralDiscovered()
2020-08-14 10:13:00.624 9893-9893/com.example.app D/MyApp: Pulse oximeter discovered! Cancelling scan...
2020-08-14 10:13:00.634 9893-9893/com.example.app V/FitbitGattCallback: onScanStopped()
2020-08-14 10:13:00.634 9893-9893/com.example.app D/MyApp: Running GattConnectTransaction...
2020-08-14 10:13:01.802 9893-9955/com.example.app D/MyApp: Connection successful! Running GattClientDiscoverServicesTransaction...
2020-08-14 10:13:01.813 9893-9955/com.example.app V/ConnectionEventListener: onClientConnectionStateChanged() - Transaction Name: Unknown, Gatt State: DISCOVERING - State Type: IN_PROGRESS, Transaction Result Status: SUCCESS, Response Status: GATT_SUCCESS, rssi: 0, mtu: 0, Characteristic UUID: null, Service UUID: null, Descriptor UUID: null, Data: null, Offset: 0, txPhy: 1, rxPhy: 1, transaction results: []
2020-08-14 10:13:02.778 9893-9955/com.example.app D/MyApp: Service discovery complete!
2020-08-14 10:13:02.780 9893-9955/com.example.app D/MyApp: Notifying characteristic found. Running SubscribeToCharacteristicNotificationsTransaction...
2020-08-14 10:13:02.789 9893-9955/com.example.app V/ConnectionEventListener: onServicesDiscovered() - [android.bluetooth.BluetoothGattService@5423233, android.bluetooth.BluetoothGattService@eed19f0, android.bluetooth.BluetoothGattService@87b5b69, android.bluetooth.BluetoothGattService@e9619ee]
2020-08-14 10:13:02.811 9893-9893/com.example.app I/MyApp: Subscribed to characteristic- Transaction Name: SubscribeToCharacteristicNotificationsTransaction, Gatt State: ENABLE_CHARACTERISTIC_NOTIFICATION_SUCCESS - State Type: IDLE, Transaction Result Status: SUCCESS, Response Status: GATT_SUCCESS, rssi: 0, mtu: 0, Characteristic UUID: cdeacb81-5235-4c07-8846-93a37ee6b86d, Service UUID: cdeacb80-5235-4c07-8846-93a37ee6b86d, Descriptor UUID: null, Data: null, Offset: 0, txPhy: 1, rxPhy: 1, transaction results: []

My ConnectionEventListener is still never receiving onClientCharacteristicChanged() calls despite everything looking good during discovery and setup.

@NathanRussak Thank you for the full snippet of code.

You need also to write to the characteristic descriptor.
The descriptor should have the following UUID 00002902-0000-1000-8000-00805f9b34fb.

This lets the peripheral (your BLE device) to start updating that characteristic with new data.

You can do that with a WriteGattDescriptorTransaction after subscribing.

For this case, I would also recommend combining the subscribe and write descriptor transaction using a CompositeClientTransaction. Would help reducing nesting too many callbacks.

Sample code without CompositeClientTransaction

val service = discResult.services.find { it.uuid == UUID.fromString("EXAMPLE_SERVICE_UUID") }
val notifyCharacteristic = service?.characteristics?.find { it.uuid == UUID.fromString("EXAMPLE_CHARACTERISTIC_UUID") }
if (notifyCharacteristic == null) {
    Log.e("MyApp", "Failed to find notifying characteristic.")
    return@runTx
}

val descriptor: BluetoothGattDescriptor? = notifyCharacteristic.getDescriptor("00002902-0000-1000-8000-00805f9b34fb")

if (descriptor == null) {
    Log.e("MyApp", "Failed to find notifying characteristic descriptor.")
    return@runTx
}
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE

Log.d("MyApp", "Notifying characteristic found. Running SubscribeToCharacteristicNotificationsTransaction...")
connection.runTx(SubscribeToCharacteristicNotificationsTransaction(connection, GattState.ENABLE_CHARACTERISTIC_NOTIFICATION_SUCCESS, notifyCharacteristic)) { subResult ->
    if (subResult.resultStatus != TransactionResult.TransactionResultStatus.SUCCESS) {
        Log.e("MyApp", "Failed to subscribe - $subResult")
    } else {
       
        connection.runTx(WriteGattDescriptorTransaction(connection, GattState.WRITE_DESCRIPTOR_SUCCESS, descriptor), { charWriteResult -> 
            if (charWriteResult.resultStatus != TransactionResult.TransactionResultStatus.SUCCESS) {
                Log.e("MyApp", "Failed to write descriptor - $charWriteResult")
            } else {
                val descriptor: BluetoothGattDescriptor? = characteristic.getDescriptor("00002902-0000-1000-8000-00805f9b34fb")
                connection.runTx(WriteGattDescriptorTransaction(connection, GattState.WRITE_DESCRIPTOR_SUCCESS, descriptor), { charWriteResult -> 
                    //...handle result
                })
            }
        })
    }
}

Sample code with CompositeClientTransaction

val service = discResult.services.find { it.uuid == UUID.fromString("EXAMPLE_SERVICE_UUID") }
val notifyCharacteristic = service?.characteristics?.find { it.uuid == UUID.fromString("EXAMPLE_CHARACTERISTIC_UUID") }
if (notifyCharacteristic == null) {
    Log.e("MyApp", "Failed to find notifying characteristic.")
    return@runTx
}

val descriptor: BluetoothGattDescriptor? = notifyCharacteristic.getDescriptor("00002902-0000-1000-8000-00805f9b34fb")

if (descriptor == null) {
    Log.e("MyApp", "Failed to find notifying characteristic descriptor.")
    return@runTx
}

descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE

Log.d("MyApp", "Notifying characteristic found. Running SubscribeToCharacteristicNotificationsTransaction...")
connection.runTx(CompositeClientTransaction(connection, arrayListOf(SubscribeToCharacteristicNotificationsTransaction(connection, GattState.ENABLE_CHARACTERISTIC_NOTIFICATION_SUCCESS, notifyCharacteristic), WriteGattDescriptorTransaction(connection, GattState.WRITE_DESCRIPTOR_SUCCESS, descriptor))) { result ->
    if (result.resultStatus != TransactionResult.TransactionResultStatus.SUCCESS) {
        Log.e("MyApp", "Failed  - $result")
    } else {
        Log.i("CompositeClientTransaction Success")
    }
}

*Updated with what value to write to the descriptor

That was it! Thank you for all of the assistance @ionutlepi!

While I have your attention: The latest release is v0.9.1. Do you know how close you are to a final 1.0 release? And if that is still a ways out, do you think the v0.9.1 tag is stable enough to be included in production apps?

We aim for 1.0 release by the end of the year.

Yes, v0.9.1 is stable and production usable.