Fitbit / bitgatt

The FitbitGatt API is designed to provide a strong state machine around all Android gatt operations with the aim of making Android BLE development across Android vendors as straightforward and side-effect free as possible.
Mozilla Public License 2.0
57 stars 17 forks source link

Unable to receive characteristic notifications. #33

Closed NathanRussak closed 4 years ago

NathanRussak commented 4 years ago

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:

ionutlepi commented 4 years ago

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
     }
}
NathanRussak commented 4 years ago

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.

ionutlepi commented 4 years ago

@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

NathanRussak commented 4 years ago

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?

ionutlepi commented 4 years ago

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

Yes, v0.9.1 is stable and production usable.